diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 000000000..d175b8231 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,654 @@ +{ + "projectName": "python-igraph", + "projectOwner": "igraph", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "CONTRIBUTORS.md" + ], + "imageSize": 100, + "commit": false, + "commitConvention": "none", + "contributors": [ + { + "login": "ntamas", + "name": "Tamás Nepusz", + "avatar_url": "https://avatars.githubusercontent.com/u/195637?v=4", + "profile": "https://collmot.com/", + "contributions": [ + "code" + ] + }, + { + "login": "iosonofabio", + "name": "Fabio Zanini", + "avatar_url": "https://avatars.githubusercontent.com/u/1200640?v=4", + "profile": "https://fabilab.org/", + "contributions": [ + "code" + ] + }, + { + "login": "Gomango999", + "name": "Kevin Zhu", + "avatar_url": "https://avatars.githubusercontent.com/u/37771462?v=4", + "profile": "https://github.com/Gomango999", + "contributions": [ + "code" + ] + }, + { + "login": "gaborcsardi", + "name": "Gábor Csárdi", + "avatar_url": "https://avatars.githubusercontent.com/u/660288?v=4", + "profile": "https://github.com/gaborcsardi", + "contributions": [ + "code" + ] + }, + { + "login": "szhorvat", + "name": "Szabolcs Horvát", + "avatar_url": "https://avatars.githubusercontent.com/u/1212871?v=4", + "profile": "http://szhorvat.net/", + "contributions": [ + "code" + ] + }, + { + "login": "vtraag", + "name": "Vincent Traag", + "avatar_url": "https://avatars.githubusercontent.com/u/6057804?v=4", + "profile": "http://www.traag.net/", + "contributions": [ + "code" + ] + }, + { + "login": "deeenes", + "name": "deeenes", + "avatar_url": "https://avatars.githubusercontent.com/u/2679889?v=4", + "profile": "https://github.com/deeenes", + "contributions": [ + "code" + ] + }, + { + "login": "h5jam", + "name": "Seungoh Han", + "avatar_url": "https://avatars.githubusercontent.com/u/46439899?v=4", + "profile": "https://h5jam.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "luav", + "name": "Artem V L", + "avatar_url": "https://avatars.githubusercontent.com/u/6162969?v=4", + "profile": "http://linkedin.com/in/artemvl", + "contributions": [ + "code" + ] + }, + { + "login": "Isaac-Lee", + "name": "Yesung(Isaac) Lee", + "avatar_url": "https://avatars.githubusercontent.com/u/49810053?v=4", + "profile": "https://github.com/Isaac-Lee", + "contributions": [ + "code" + ] + }, + { + "login": "jboynyc", + "name": "John Boy", + "avatar_url": "https://avatars.githubusercontent.com/u/2187261?v=4", + "profile": "https://www.jboy.space/", + "contributions": [ + "code" + ] + }, + { + "login": "casperdcl", + "name": "Casper da Costa-Luis", + "avatar_url": "https://avatars.githubusercontent.com/u/10780059?v=4", + "profile": "https://cdcl.ml/", + "contributions": [ + "code" + ] + }, + { + "login": "albertoalcolea", + "name": "Alberto Alcolea", + "avatar_url": "https://avatars.githubusercontent.com/u/1153725?v=4", + "profile": "https://albertoalcolea.com/", + "contributions": [ + "code" + ] + }, + { + "login": "horvatha", + "name": "Árpád Horváth", + "avatar_url": "https://avatars.githubusercontent.com/u/951303?v=4", + "profile": "https://pyedu.hu/arpad/", + "contributions": [ + "code" + ] + }, + { + "login": "ebraminio", + "name": "ebraminio", + "avatar_url": "https://avatars.githubusercontent.com/u/833473?v=4", + "profile": "https://github.com/ebraminio", + "contributions": [ + "code" + ] + }, + { + "login": "fwitter", + "name": "Fabian Witter", + "avatar_url": "https://avatars.githubusercontent.com/u/10985458?v=4", + "profile": "https://github.com/fwitter", + "contributions": [ + "code" + ] + }, + { + "login": "jankatins", + "name": "Jan Katins", + "avatar_url": "https://avatars.githubusercontent.com/u/890156?v=4", + "profile": "http://www.katzien.de/", + "contributions": [ + "code" + ] + }, + { + "login": "nickeubank", + "name": "Nick Eubank", + "avatar_url": "https://avatars.githubusercontent.com/u/9683693?v=4", + "profile": "https://github.com/nickeubank", + "contributions": [ + "code" + ] + }, + { + "login": "PeterScott", + "name": "Peter Scott", + "avatar_url": "https://avatars.githubusercontent.com/u/406445?v=4", + "profile": "http://finger-tree.blogspot.com/", + "contributions": [ + "code" + ] + }, + { + "login": "Sriram-Pattabiraman", + "name": "Sriram-Pattabiraman", + "avatar_url": "https://avatars.githubusercontent.com/u/59712515?v=4", + "profile": "https://github.com/Sriram-Pattabiraman", + "contributions": [ + "code" + ] + }, + { + "login": "iggisv9t", + "name": "Sviatoslav", + "avatar_url": "https://avatars.githubusercontent.com/u/19172517?v=4", + "profile": "https://iggisv9t.xyz/", + "contributions": [ + "code" + ] + }, + { + "login": "ah00ee", + "name": "Ah-Young Nho", + "avatar_url": "https://avatars.githubusercontent.com/u/68725978?v=4", + "profile": "https://github.com/ah00ee", + "contributions": [ + "code" + ] + }, + { + "login": "frederik-h", + "name": "Frederik Harwath", + "avatar_url": "https://avatars.githubusercontent.com/u/22046314?v=4", + "profile": "https://github.com/frederik-h", + "contributions": [ + "code" + ] + }, + { + "login": "naviddianati", + "name": "Navid Dianati", + "avatar_url": "https://avatars.githubusercontent.com/u/5558232?v=4", + "profile": "https://github.com/naviddianati", + "contributions": [ + "code" + ] + }, + { + "login": "abe-winter", + "name": "abe-winter", + "avatar_url": "https://avatars.githubusercontent.com/u/7256523?v=4", + "profile": "https://github.com/abe-winter", + "contributions": [ + "code" + ] + }, + { + "login": "arivero", + "name": "Alejandro Rivero", + "avatar_url": "https://avatars.githubusercontent.com/u/43174?v=4", + "profile": "https://github.com/arivero", + "contributions": [ + "code" + ] + }, + { + "login": "Ariki", + "name": "Ariki", + "avatar_url": "https://avatars.githubusercontent.com/u/519412?v=4", + "profile": "https://github.com/Ariki", + "contributions": [ + "code" + ] + }, + { + "login": "cvanelteren", + "name": "Casper van Elteren", + "avatar_url": "https://avatars.githubusercontent.com/u/19485143?v=4", + "profile": "https://cvanelteren.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "cthoyt", + "name": "Charles Tapley Hoyt", + "avatar_url": "https://avatars.githubusercontent.com/u/5069736?v=4", + "profile": "https://cthoyt.com/", + "contributions": [ + "code" + ] + }, + { + "login": "cgohlke", + "name": "Christoph Gohlke", + "avatar_url": "https://avatars.githubusercontent.com/u/483428?v=4", + "profile": "https://www.cgohlke.com/", + "contributions": [ + "code" + ] + }, + { + "login": "chrisfalter", + "name": "Christopher Falter", + "avatar_url": "https://avatars.githubusercontent.com/u/4177499?v=4", + "profile": "https://github.com/chrisfalter", + "contributions": [ + "code" + ] + }, + { + "login": "ReblochonMasque", + "name": "FredInChina", + "avatar_url": "https://avatars.githubusercontent.com/u/6275531?v=4", + "profile": "https://github.com/ReblochonMasque", + "contributions": [ + "code" + ] + }, + { + "login": "friso", + "name": "Friso van Vollenhoven", + "avatar_url": "https://avatars.githubusercontent.com/u/273638?v=4", + "profile": "https://friso.lol/", + "contributions": [ + "code" + ] + }, + { + "login": "szarnyasg", + "name": "Gabor Szarnyas", + "avatar_url": "https://avatars.githubusercontent.com/u/1402801?v=4", + "profile": "https://szarnyasg.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "GaoFangshu", + "name": "Gao Fangshu", + "avatar_url": "https://avatars.githubusercontent.com/u/11488742?v=4", + "profile": "https://github.com/GaoFangshu", + "contributions": [ + "code" + ] + }, + { + "login": "gchilczuk", + "name": "Grzegorz Chilczuk", + "avatar_url": "https://avatars.githubusercontent.com/u/16257695?v=4", + "profile": "https://github.com/gchilczuk", + "contributions": [ + "code" + ] + }, + { + "login": "limburgher", + "name": "Gwyn Ciesla", + "avatar_url": "https://avatars.githubusercontent.com/u/2363820?v=4", + "profile": "http://cecinestpasunefromage.wordpress.com/", + "contributions": [ + "code" + ] + }, + { + "login": "xuhdev", + "name": "Hong Xu", + "avatar_url": "https://avatars.githubusercontent.com/u/325476?v=4", + "profile": "https://www.topbug.net/", + "contributions": [ + "code" + ] + }, + { + "login": "jhsmith", + "name": "Jay Smith", + "avatar_url": "https://avatars.githubusercontent.com/u/974519?v=4", + "profile": "https://github.com/jhsmith", + "contributions": [ + "code" + ] + }, + { + "login": "MapleCCC", + "name": "MapleCCC", + "avatar_url": "https://avatars.githubusercontent.com/u/25131775?v=4", + "profile": "https://mapleccc.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "theCapypara", + "name": "Marco Köpcke", + "avatar_url": "https://avatars.githubusercontent.com/u/3512122?v=4", + "profile": "https://www.linkedin.com/in/marco-koepcke/", + "contributions": [ + "code" + ] + }, + { + "login": "elfring", + "name": "Markus Elfring", + "avatar_url": "https://avatars.githubusercontent.com/u/660477?v=4", + "profile": "https://github.com/elfring", + "contributions": [ + "code" + ] + }, + { + "login": "MartinoMensio", + "name": "Martino Mensio", + "avatar_url": "https://avatars.githubusercontent.com/u/11597393?v=4", + "profile": "https://martinomensio.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "lauzadis", + "name": "Matas", + "avatar_url": "https://avatars.githubusercontent.com/u/30608308?v=4", + "profile": "https://github.com/lauzadis", + "contributions": [ + "code" + ] + }, + { + "login": "mlissner", + "name": "Mike Lissner", + "avatar_url": "https://avatars.githubusercontent.com/u/236970?v=4", + "profile": "http://www.michaeljaylissner.com/", + "contributions": [ + "code" + ] + }, + { + "login": "flying-sheep", + "name": "Philipp A.", + "avatar_url": "https://avatars.githubusercontent.com/u/291575?v=4", + "profile": "https://phil.red/", + "contributions": [ + "code" + ] + }, + { + "login": "PuneethaPai", + "name": "Puneetha Pai", + "avatar_url": "https://avatars.githubusercontent.com/u/21996583?v=4", + "profile": "https://puneethapai.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "sr-murthy", + "name": "S Murthy", + "avatar_url": "https://avatars.githubusercontent.com/u/9358070?v=4", + "profile": "https://github.com/sr-murthy", + "contributions": [ + "code" + ] + }, + { + "login": "scottgigante", + "name": "Scott Gigante", + "avatar_url": "https://avatars.githubusercontent.com/u/8499679?v=4", + "profile": "https://github.com/scottgigante", + "contributions": [ + "code" + ] + }, + { + "login": "thierry-FreeBSD", + "name": "Thierry Thomas", + "avatar_url": "https://avatars.githubusercontent.com/u/6819982?v=4", + "profile": "http://people.freebsd.org/~thierry/", + "contributions": [ + "code" + ] + }, + { + "login": "willemvandenboom", + "name": "Willem van den Boom", + "avatar_url": "https://avatars.githubusercontent.com/u/41558513?v=4", + "profile": "https://github.com/willemvandenboom", + "contributions": [ + "code" + ] + }, + { + "login": "remysucre", + "name": "Yisu Remy Wang", + "avatar_url": "https://avatars.githubusercontent.com/u/6758001?v=4", + "profile": "https://github.com/remysucre", + "contributions": [ + "code" + ] + }, + { + "login": "yy", + "name": "YY Ahn", + "avatar_url": "https://avatars.githubusercontent.com/u/24250?v=4", + "profile": "https://yongyeol.com/", + "contributions": [ + "code" + ] + }, + { + "login": "kmankinen", + "name": "kmankinen", + "avatar_url": "https://avatars.githubusercontent.com/u/22212710?v=4", + "profile": "https://github.com/kmankinen", + "contributions": [ + "code" + ] + }, + { + "login": "odidev", + "name": "odidev", + "avatar_url": "https://avatars.githubusercontent.com/u/40816837?v=4", + "profile": "https://github.com/odidev", + "contributions": [ + "code" + ] + }, + { + "login": "sombreslames", + "name": "sombreslames", + "avatar_url": "https://avatars.githubusercontent.com/u/4037102?v=4", + "profile": "https://github.com/sombreslames", + "contributions": [ + "code" + ] + }, + { + "login": "szcf-weiya", + "name": "szcf-weiya", + "avatar_url": "https://avatars.githubusercontent.com/u/13688320?v=4", + "profile": "https://hohoweiya.xyz/", + "contributions": [ + "code" + ] + }, + { + "login": "tristanlatr", + "name": "tristanlatr", + "avatar_url": "https://avatars.githubusercontent.com/u/19967168?v=4", + "profile": "https://github.com/tristanlatr", + "contributions": [ + "code" + ] + }, + { + "login": "JDPowell648", + "name": "JDPowell648", + "avatar_url": "https://avatars.githubusercontent.com/u/41934552?v=4", + "profile": "https://github.com/JDPowell648", + "contributions": [ + "doc" + ] + }, + { + "login": "Adriankhl", + "name": "k.h.lai", + "avatar_url": "https://avatars.githubusercontent.com/u/16377650?v=4", + "profile": "https://github.com/Adriankhl", + "contributions": [ + "code" + ] + }, + { + "login": "gruebel", + "name": "Anton Grübel", + "avatar_url": "https://avatars.githubusercontent.com/u/33207684?v=4", + "profile": "https://github.com/gruebel", + "contributions": [ + "code" + ] + }, + { + "login": "flange-ipb", + "name": "flange-ipb", + "avatar_url": "https://avatars.githubusercontent.com/u/34936695?v=4", + "profile": "https://github.com/flange-ipb", + "contributions": [ + "code" + ] + }, + { + "login": "pmp-p", + "name": "Paul m. p. Peny", + "avatar_url": "https://avatars.githubusercontent.com/u/16009100?v=4", + "profile": "https://discuss.afpy.org/", + "contributions": [ + "code" + ] + }, + { + "login": "DavidRConnell", + "name": "David R. Connell", + "avatar_url": "https://avatars.githubusercontent.com/u/35470740?v=4", + "profile": "https://davidrconnell.github.io/", + "contributions": [ + "code" + ] + }, + { + "login": "rmmaf", + "name": "Rodrigo Monteiro de Moraes de Arruda Falcão", + "avatar_url": "https://avatars.githubusercontent.com/u/23747884?v=4", + "profile": "https://www.linkedin.com/in/rmmaf/", + "contributions": [ + "code" + ] + }, + { + "login": "Kreijstal", + "name": "Kreijstal", + "avatar_url": "https://avatars.githubusercontent.com/u/2415206?v=4", + "profile": "https://github.com/Kreijstal", + "contributions": [ + "code" + ] + }, + { + "login": "m1-s", + "name": "Michael Schneider", + "avatar_url": "https://avatars.githubusercontent.com/u/94642227?v=4", + "profile": "https://github.com/m1-s", + "contributions": [ + "code" + ] + }, + { + "login": "aothms", + "name": "Thomas Krijnen", + "avatar_url": "https://avatars.githubusercontent.com/u/1096535?v=4", + "profile": "http://thomaskrijnen.com/", + "contributions": [ + "code" + ] + }, + { + "login": "GenieTim", + "name": "Tim Bernhard", + "avatar_url": "https://avatars.githubusercontent.com/u/8596965?v=4", + "profile": "https://github.com/GenieTim", + "contributions": [ + "code" + ] + }, + { + "login": "BeaMarton13", + "name": "Bea Márton", + "avatar_url": "https://avatars.githubusercontent.com/u/204701577?v=4", + "profile": "https://github.com/BeaMarton13", + "contributions": [ + "code" + ] + }, + { + "login": "SKG24", + "name": "Sanat Kumar Gupta", + "avatar_url": "https://avatars.githubusercontent.com/u/123228827?v=4", + "profile": "https://github.com/SKG24", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7 +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..f14524756 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +# ignore git's own files +.git + +# ignore build commands so we don't trigger unnecessary rebuilds during iteration +docker/* +Makefile + +# ignore build and dist folders for python +build/* +dist/* + +# also ignore build folder for vendored stuff +vendor/build/* +vendor/install/* diff --git a/.git_archival.json b/.git_archival.json new file mode 100644 index 000000000..9869b5923 --- /dev/null +++ b/.git_archival.json @@ -0,0 +1,7 @@ +{ + "hash-full": "$Format:%H$", + "hash-short": "$Format:%h$", + "timestamp": "$Format:%cI$", + "refs": "$Format:%D$", + "describe": "$Format:%(describe:tags=true,match=[0-9]*)$" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..56669188e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.json export-subst diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 000000000..75005ccfc --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,7 @@ +Reporting issues +================ + +We use the issue tracker for tracking bugs and feature requests _only_. +If you do not have a bug or feature request but you only need help with using +igraph, send your question to the igraph-help mailing list instead at +igraph-help@nongnu.org. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..71e72eb9a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: igraph +open_collective: igraph diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..24c3b711c --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,4 @@ +Make sure that these boxes are checked before submitting your issue -- thank you! + +- [ ] This issue is for the Python interface of igraph. +- [ ] This issue is a bug report or a feature request, not a support question. 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..60eeaa013 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +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). diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..5ace4600a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..686944ff0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ + + + + +- [ ] By submitting this pull request, I assign the copyright of my contribution to _The igraph development team_. diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..58e48a20f --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,24 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 + +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 14 + +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - todo + - wishlist + +# Label to use when marking an issue as stale +staleLabel: stale + +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed in 14 days if no further activity occurs. + Thank you for your contributions. + +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..2192d7c72 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,352 @@ +# Name cannot contain commas because of setup-emsdk job +name: Build and test + +on: [push, pull_request] +env: + CIBW_ENABLE: pypy + CIBW_ENVIRONMENT_PASS_LINUX: PYTEST_TIMEOUT + CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9" + CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test]' && python -m pytest -v tests" + # Free-threaded builds excluded for Python 3.14 because they do not support the limited API + CIBW_SKIP: "cp314t-*" + PYTEST_TIMEOUT: 60 + +jobs: + build_wheel_linux: + name: Build wheels on Linux (x86_64) + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: 0 + + - name: Build wheels (manylinux) + uses: pypa/cibuildwheel@v3.3.0 + env: + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BUILD: "*-manylinux_x86_64" + + - name: Build wheels (musllinux) + uses: pypa/cibuildwheel@v3.3.0 + env: + CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_BUILD: "*-musllinux_x86_64" + CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" + + - uses: actions/upload-artifact@v6 + with: + name: wheels-linux-x86_64 + path: ./wheelhouse/*.whl + + build_wheel_linux_aarch64_manylinux: + name: Build wheels on Linux (aarch64/manylinux) + runs-on: ubuntu-22.04-arm + steps: + - uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: 0 + + - name: Build wheels (manylinux) + uses: pypa/cibuildwheel@v3.3.0 + env: + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel cairo-devel && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_ARCHS_LINUX: aarch64 + CIBW_BUILD: "*-manylinux_aarch64" + + - uses: actions/upload-artifact@v6 + with: + name: wheels-linux-aarch64-manylinux + path: ./wheelhouse/*.whl + + build_wheel_linux_aarch64_musllinux: + name: Build wheels on Linux (aarch64/musllinux) + runs-on: ubuntu-22.04-arm + steps: + - uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: 0 + + - name: Build wheels (musllinux) + uses: pypa/cibuildwheel@v3.3.0 + env: + CIBW_BEFORE_BUILD: "apk add flex bison libxml2-dev zlib-dev cairo-dev && pip install -U cmake pip setuptools wheel && python setup.py build_c_core" + CIBW_ARCHS_LINUX: aarch64 + CIBW_BUILD: "*-musllinux_aarch64" + CIBW_TEST_COMMAND: "cd {project} && pip install --prefer-binary '.[test-musl]' && python -m pytest -v tests" + + - uses: actions/upload-artifact@v6 + with: + name: wheels-linux-aarch64-musllinux + path: ./wheelhouse/*.whl + + build_wheel_macos: + name: Build wheels on macOS (${{ matrix.wheel_arch }}) + runs-on: macos-latest + env: + LLVM_VERSION: "14.0.5" + MACOSX_DEPLOYMENT_TARGET: "10.15" + strategy: + matrix: + include: + - cmake_arch: x86_64 + wheel_arch: x86_64 + - cmake_arch: arm64 + cmake_extra_args: -DF2C_EXTERNAL_ARITH_HEADER=../../../etc/arith_apple_m1.h -DIEEE754_DOUBLE_ENDIANNESS_MATCHES=ON + wheel_arch: arm64 + + steps: + - uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: 0 + + - name: Cache installed C core + id: cache-c-core + uses: actions/cache@v5 + with: + path: vendor/install + key: C-core-cache-${{ runner.os }}-${{ matrix.cmake_arch }}-llvm${{ env.LLVM_VERSION }}-${{ hashFiles('.git/modules/**/HEAD') }} + + - name: Cache C core dependencies + id: cache-c-deps + uses: actions/cache@v5 + with: + path: ~/local + key: deps-cache-v2-${{ runner.os }}-${{ matrix.cmake_arch }}-llvm${{ env.LLVM_VERSION }} + + - name: Install OS dependencies + if: steps.cache-c-core.outputs.cache-hit != 'true' || steps.cache-c-deps.outputs.cache-hit != 'true' # Only needed when building the C core or libomp + run: brew install autoconf automake libtool + + - name: Install OpenMP library + if: steps.cache-c-deps.outputs.cache-hit != 'true' + run: | + wget https://github.com/llvm/llvm-project/releases/download/llvmorg-$LLVM_VERSION/openmp-$LLVM_VERSION.src.tar.xz + tar xf openmp-$LLVM_VERSION.src.tar.xz + cd openmp-$LLVM_VERSION.src + mkdir build && cd build + cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/local -DLIBOMP_ENABLE_SHARED=OFF -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} + cmake --build . + cmake --install . + + - name: Build wheels + uses: pypa/cibuildwheel@v3.3.0 + env: + CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" + CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" + CIBW_ENVIRONMENT: "LDFLAGS=-L$HOME/local/lib" + IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} -DCMAKE_PREFIX_PATH=$HOME/local + + - uses: actions/upload-artifact@v6 + with: + name: wheels-macos-${{ matrix.wheel_arch }} + path: ./wheelhouse/*.whl + + build_wheel_wasm: + name: Build wheels for WebAssembly + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: 0 + + - uses: actions/setup-python@v6 + name: Install Python + with: + python-version: "3.12.1" + + - name: Install OS dependencies + run: sudo apt install ninja-build cmake flex bison + + - uses: mymindstorm/setup-emsdk@v14 + with: + version: "3.1.58" + actions-cache-folder: "emsdk-cache" + + - name: Build wheel + run: | + pip install pyodide-build==0.26.2 + python3 scripts/fix_pyodide_build.py + pyodide build + + - name: Setup upterm session + uses: lhotari/action-upterm@v1 + if: ${{ failure() }} + with: + limit-access-to-actor: true + wait-timeout-minutes: 5 + + - uses: actions/upload-artifact@v6 + with: + name: wheels-wasm + path: ./dist/*.whl + + build_wheel_win: + name: Build wheels on Windows (${{ matrix.cmake_arch }}) + strategy: + matrix: + include: + - cmake_arch: Win32 + wheel_arch: win32 + vcpkg_arch: x86 + os: windows-2022 + test_extra: test + - cmake_arch: x64 + wheel_arch: win_amd64 + vcpkg_arch: x64 + os: windows-2022 + test_extra: test + - cmake_arch: ARM64 + wheel_arch: win_arm64 + vcpkg_arch: arm64 + os: windows-11-arm + test_extra: test-win-arm64 + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: 0 + + - name: Cache installed C core + id: cache-c-core + uses: actions/cache@v5 + with: + path: vendor/install + key: C-core-cache-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/**/HEAD') }} + + - name: Cache VCPKG + uses: actions/cache@v5 + 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: | + %VCPKG_INSTALLATION_ROOT%\vcpkg.exe integrate install + %VCPKG_INSTALLATION_ROOT%\vcpkg.exe install liblzma:${{ matrix.vcpkg_arch }}-windows-static-md libxml2:${{ matrix.vcpkg_arch }}-windows-static-md + shell: cmd + + - name: Build wheels + uses: pypa/cibuildwheel@v3.3.0 + env: + CIBW_BEFORE_BUILD: "pip install -U setuptools && python setup.py build_c_core" + CIBW_BUILD: "*-${{ matrix.wheel_arch }}" + CIBW_TEST_COMMAND: 'cd /d {project} && pip install --prefer-binary ".[${{ matrix.test_extra }}]" && python -m pytest tests' + # Skip tests for Python 3.10 onwards because SciPy does not have + # 32-bit wheels for Windows any more + CIBW_TEST_SKIP: "cp310-win32 cp311-win32 cp312-win32 cp313-win32 cp314-win32" + 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,bcrypt + IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 + + - uses: actions/upload-artifact@v6 + with: + name: wheels-win-${{ matrix.wheel_arch }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Build sdist and test extra dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: 0 + + - name: Cache installed C core + id: cache-c-core + uses: actions/cache@v5 + with: + path: | + vendor/install + key: C-core-cache-${{ runner.os }}-${{ hashFiles('.git/modules/**/HEAD') }} + + - 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 + + - uses: actions/setup-python@v6 + name: Install Python + with: + python-version: "3.9" + + - name: Build sdist + run: | + python setup.py build_c_core + python setup.py sdist + pip install . + + - name: Test + run: | + pip install '.[test]' + python -m pytest -v tests + + - uses: actions/upload-artifact@v6 + with: + name: sdist + path: dist/*.tar.gz + + # When updating 'runs-on', the ASan/UBSan library paths/versions must also be updated for LD_PRELOAD + # for the "Test" step below. + build_with_sanitizer: + name: Build with sanitizers for debugging purposes + runs-on: ubuntu-latest + env: + IGRAPH_CMAKE_EXTRA_ARGS: -DFORCE_COLORED_OUTPUT=ON + steps: + - uses: actions/checkout@v5 + with: + submodules: true + fetch-depth: 0 + + - name: Cache installed C core + id: cache-c-core + uses: actions/cache@v5 + with: + path: | + vendor/build + vendor/install + key: C-core-build-sanitizer-v1-${{ runner.os }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} + + - 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 + + - uses: actions/setup-python@v6 + name: Install Python + with: + python-version: "3.12" + + - name: Build and install Python extension + env: + IGRAPH_USE_SANITIZERS: 1 + run: | + # We cannot install the test dependency group because many test dependencies cause + # false positives in the sanitizer + pip install --prefer-binary networkx pytest pytest-timeout + pip install -e . + + # Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD. + # The ASan/UBSan library versions need to be updated when switching to a newer Ubuntu/GCC. + # LD_PRELOAD needs to be specified in the "run" section to ensure that we + # do not pick up memory leaks in the wrapper shell (e.g., /bin/bash) + - name: Test + env: + ASAN_OPTIONS: "detect_stack_use_after_return=1" + LSAN_OPTIONS: "suppressions=etc/lsan-suppr.txt:print_suppressions=false" + run: | + sudo sysctl vm.mmap_rnd_bits=28 + LD_PRELOAD=/lib/x86_64-linux-gnu/libasan.so.8:/lib/x86_64-linux-gnu/libubsan.so.1 python -m pytest --capture=sys tests diff --git a/.gitignore b/.gitignore index c42b22ca6..46689eff9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,24 @@ *.pyc build/ dist/ -doc/api/ -igraph/*.so +src/igraph/*.so +result_images/ *.egg-info/ .python-version +.eggs/ .tox +.venv/ +.venv-*/ +.vscode/ +vendor/build/ +vendor/install/ +.DS_Store + +doc/source/gallery.rst +doc/source/tutorials +doc/linkcheck +doc/api/ +doc/dash/ +doc/html/ +doc/jekyll_tools/vendor +doc/examples_sphinx-gallery/social_network.* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..bb3f72ea4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/source/igraph"] + path = vendor/source/igraph + url = https://github.com/igraph/igraph diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..e67e6d125 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +fail_fast: true +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: end-of-file-fixer + exclude: ^tests/drawing/plotly/baseline_images + - id: trailing-whitespace + + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.3.5 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..7de01065a --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,48 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +submodules: + include: + - vendor/source/igraph + recursive: true + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + apt_packages: + - cmake + - flex + - bison + - libxml2-dev + - zlib1g-dev + + tools: + python: "3.11" + # You can also specify other tool versions: + # nodejs: "16" + # rust: "1.55" + # golang: "1.17" + + jobs: + pre_build: + - bash ./scripts/rtd_prebuild.sh + # One website complains about legacy SSL renegotiation (?), skip for now + #- python -m sphinx -b linkcheck doc/source/ _build/linkcheck + + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: doc/source/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +# formats: +# - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: doc/source/requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 67da26e79..000000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: python - -python: - - "2.7" - - "3.4" - - "pypy" - - "pypy3" - -addons: - apt: - packages: - - gfortran - - flex - - bison - -install: - - pip install tox-travis - -script: - - tox - -notifications: - email: - on_success: change - on_failure: always - -sudo: false \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..f2424f4fb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,793 @@ +# igraph Python interface changelog + +## 1.0.1 - 2025-12-26 + +### Changed + +- The C core of igraph was updated to version 1.0.1. + +## [1.0.0] - 2025-10-23 + +### Added + +- Added `Graph.Nearest_Neighbor_Graph()`. + +- Added `node_in_weights` argument to `Graph.community_leiden()`. + +- Added `align_layout()` to align the principal axes of a layout nicely + with screen dimensions. + +- Added `Graph.commnity_voronoi()`. + +- Added `Graph.commnity_fluid_communities()`. + +### Changed + +- The C core of igraph was updated to version 1.0.0. + +- Most layouts are now auto-aligned using `align_layout()`. + +- Dropped support for PyPy 3.9 and PyPy 3.10 as they are now EOL. + +### Miscellaneous + +- Documentation improvements. + +- This is the last version that supports Python 3.9 as it will reach its + end of life at the end of October 2025. + +## [0.11.9] - 2025-06-11 + +### Changed + +- Dropped support for Python 3.8 as it has now reached its end of life. + +- The C core of igraph was updated to version 0.10.16. + +- Added `Graph.simple_cycles()` to find simple cycles in the graph. + +## [0.11.8] - 2024-10-25 + +### Fixed + +- Fixed documentation build on Read The Docs. No other changes compared to + 0.11.7. + +## [0.11.7] - 2024-10-24 + +### Added + +- Added `Graph.feedback_vertex_set()` to calculate a feedback vertex set of the + graph. + +- Added new methods to `Graph.feedback_arc_set()` that allows the user to + select the specific integer problem formulation used by the underlying + solver. + +### Changed + +- Ensured compatibility with Python 3.13. + +- The C core of igraph was updated to an interim commit (3dd336a) between + version 0.10.13 and version 0.10.15. Earlier versions of this changelog + mistakenly marked this revision as version 0.10.14. + +### Fixed + +- Fixed a potential memory leak in the `Graph.get_shortest_path_astar()` heuristic + function callback + +## [0.11.6] - 2024-07-08 + +### Added + +- Added `Graph.Hypercube()` for creating n-dimensional hypercube graphs. + +- Added `Graph.Chung_Lu()` for sampling from the Chung-Lu model as well as + several related models. + +- Added `Graph.is_complete()` to test if there is a connection between all + distinct pairs of vertices. + +- Added `Graph.is_clique()` to test if a set of vertices forms a clique. + +- Added `Graph.is_independent_vertex_set()` to test if some vertices form an + independent set. + +- Added `Graph.mean_degree()` for a convenient way to compute the average + degree of a graph. + +### Changed + +- The C core of igraph was updated to version 0.10.13. + +- `Graph.rewire()` now attempts to perform edge swaps 10 times the number of + edges by default. + +- Error messages issued when an attribute is not found now mention the name + and type of that attribute. + +## [0.11.5] - 2024-05-07 + +### Added + +- Added a `prefixattr=...` keyword argument to `Graph.write_graphml()` that + allows the user to strip the `g_`, `v_` and `e_` prefixes from GraphML files + written by igraph. + +### Changed + +- `Graph.are_connected()` has now been renamed to `Graph.are_adjacent()`, + following up a similar change in the C core. The old name of the function + is deprecated but will be kept around until at least 0.12.0. + +- The C core of igraph was updated to version 0.10.12. + +- Deprecated `PyCObject` API calls in the C code were replaced by calls to + `PyCapsule`, thanks to @DavidRConnell in + + +- `get_shortest_path()` documentation was clarified by @JDPowell648 in + + +- It is now possible to link to an existing igraph C core on MSYS2, thanks to + @Kreijstal in + +### Fixed + +- Bugfix in the NetworkX graph conversion code by @rmmaf in + + +## [0.11.4] + +### Added + +- Added `Graph.Prufer()` to construct a graph from a Prüfer sequence. + +- Added `Graph.Bipartite_Degree_Sequence()` to construct a bipartite graph from + a bidegree sequence. + +- Added `Graph.is_biconnected()` to check if a graph is biconnected. + +### Fixed + +- Fixed import of `graph-tool` graphs for vertex properties where each property + has a vector value. + +- `Graph.Adjacency()` now accepts `Matrix` instances and other sequences as an + input, it is not limited to lists-of-lists-of-ints any more. + +## [0.11.3] - 2023-11-19 + +### Added + +- Added `Graph.__invalidate_cache()` for debugging and benchmarking purposes. + +### Changed + +- The C core of igraph was updated to version 0.10.8. + +### Fixed + +- Removed incorrectly added `loops=...` argument of `Graph.is_bigraphical()`. + +- Fixed a bug in the Matplotlib graph drawing backend that filled the interior of undirected curved edges. + +## [0.11.2] - 2023-10-12 + +### Fixed + +- Fixed plotting of null graphs with the Matplotlib backend. + +## [0.11.0] - 2023-10-12 + +### Added + +- `python-igraph` is now tested in Python 3.12. + +- Added `weights=...` keyword argument to `Graph.layout_kamada_kawai()`. + +### Changed + +- The `matplotlib` plotting infrastructure underwent major surgery and is now able to show consistent vertex and edge drawings at any level of zoom, including with animations, and for any aspect ratio. +- As a consequence of the restructuring at the previous point, vertex sizes are now specified in figure points and are not affected by axis limits or zoom. With the current conventions, `vertex_size=25` is a reasonable size for `igraph.plot`. +- As another consequence of the above, vertex labels now support offsets from the vertex center, in figure point units. +- As another consequence of the above, self loops are now looking better and their size can be controlled using the `edge_loop_size` argument in `igraph.plot`. +- As another consequence of the above, if using the `matplotlib` backend when plotting a graph, `igraph.plot` now does not return the `Axes` anymore. Instead, it returns a container artist called `GraphArtist`, which contains as children the elements of the graph plot: a `VertexCollection` for the vertices, and `EdgeCollection` for the edges, and so on. These objects can be used to modify the plot after the initial rendering, e.g. inside a Jupyter notebook, to fine tune the appearance of the plot. While documentation on specific graphic elements is still scant, more descriptive examples will follow in the future. + +### Fixed + +- Fixed drawing order of vertices in the Plotly backend (#691). + +### Removed + +- Dropped support for Python 3.7 as it has reached its end of life. + +## [0.10.8] - 2023-09-12 + +### Added + +- Added `is_bigraphical()` to test whether a pair of integer sequences can be the degree sequence of some bipartite graph. + +- Added `weights=...` keyword argument to `Graph.radius()` and `Graph.eccentricity()`. + +## [0.10.7] - 2023-09-04 + +### Added + +- `Graph.distances()`, `Graph.get_shortest_path()` and `Graph.get_shortest_paths()` now allow the user to select the algorithm to be used explicitly. + +### Changed + +- The C core of igraph was updated to version 0.10.7. + +- `Graph.distances()` now uses Dijkstra's algorithm when there are zero weights but no negative weights. Earlier versions switched to Bellman-Ford or Johnson in the presence of zero weights unnecessarily. + +### Fixed + +- Fixed a bug in `EdgeSeq.select(_incident=...)` for undirected graphs. + +- Fixed a memory leak in `Graph.distances()` when attempting to use Johnson's algorithm with `mode != "out"` + +## [0.10.6] - 2023-07-13 + +### Changed + +- The C core of igraph was updated to version 0.10.6. + +- `Graph.Incidence()` is now deprecated in favour of `Graph.Biadjacency()` as it constructs a bipartite graph from a _bipartite adjacency_ matrix. (The previous name was a mistake). Future versions might re-introduce `Graph.Incidence()` to construct a graph from its incidence matrix. + +- `Graph.get_incidence()` is now deprecated in favour of `Graph.get_biadjacency()` as it returns the _bipartite adjacency_ matrix of a graph and not its incidence matrix. (The previous name was a mistake). Future versions might re-introduce `Graph.get_incidence()` to return the incidence matrix of a graph. + +- Reverted the change in 0.10.5 that prevented adding vertices with integers as vertex names. Now we show a deprecation warning instead, and the addition of vertices with integer names will be prevented from version 0.11.0 only. + +### Fixed + +- Fixed a minor memory leak in `Graph.decompose()`. + +- The default vertex size of the Plotly backend was fixed so the vertices are + now visible by default without specifying an explicit size for them. + +## [0.10.5] - 2023-06-30 + +### Added + +- The `plot()` function now takes a `backend` keyword argument that can be used + to specify the plotting backend explicitly. + +- The `VertexClustering` object returned from `Graph.community_leiden()` now + contains an extra property named `quality` that stores the value of the + internal quality function optimized by the algorithm. + +- `Graph.Adjacency()` and `Graph.Weighted_Adjacency()` now supports + `loops="once"`, `loops="twice"` and `loops="ignore"` to control how loop + edges are handled in a more granular way. `loops=True` and `loops=False` + keep on working as in earlier versions. + +- Added `Graph.get_shortest_path()` as a convenience function for cases when + only one shortest path is needed between a given source and target vertices. + +- Added `Graph.get_shortest_path_astar()` to calculate the shortest path + between two vertices using the A-star algorithm and an appropriate + heuristic function. + +- Added `Graph.count_automorphisms()` to count the number of automorphisms + of a graph and `Graph.automorphism_group()` to calculate the generators of + the automorphism group of a graph. + +- The `VertexCover` constructor now allows referring to vertices by names + instead of IDs. + +### Fixed + +- `resolution` parameter is now correctly taken into account when calling + `Graph.modularity()` + +- `VertexClustering.giant()` now accepts the null graph. The giant component of + a null graph is the null graph according to our conventions. + +- `Graph.layout_reingold_tilford()` now accepts vertex names in the `roots=...` + keyword argument. + +- The plotting of curved directed edges with the Cairo backend is now fixed; + arrowheads were placed at the wrong position before this fix. + +### Changed + +- The C core of igraph was updated to version 0.10.5. + +### Removed + +- Removed defunct `Graph.community_leading_eigenvector_naive()` method. Not a + breaking change because it was already removed from the C core a long time + ago so the function in the Python interface did not do anything useful + either. + +## [0.10.4] - 2023-01-27 + +### Added + +- Added `Graph.vertex_coloring_greedy()` to calculate a greedy vertex coloring + for the graph. + +- Betweenness and edge betweenness scores can now be calculated for a subset of + the shortest paths originating from or terminating in a certain set of + vertices only. + +### Fixed + +- Fixed the drawing of `VertexDendrogram` instances, both in the Cairo and the + Matplotlib backends. +- The `cutoff` and `normalized` arguments of `Graph.closeness()` did not function correctly. + +## [0.10.3] - 2022-12-31 + +### Changed + +- The C core of igraph was updated to version 0.10.3. + +- UMAP layout now exposes the computation of the symmetrized edge weights via + `umap_compute_weights()`. The layout function, `Graph.layout_umap()`, can + now be called either on a directed graph with edge distances, or on an + undirected graph with edge weights, typically computed via + `umap_compute_weights()` or precomputed by the user. Moreover, the + `sampling_prob` argument was faulty and has been removed. See PR + [#613](https://github.com/igraph/python-igraph/pull/613) for details. + +- The `resolution_parameter` argument of `Graph.community_leiden()` was renamed + to `resolution` for sake of consistency. The old variant still works with a + deprecation warning, but will be removed in a future version. + +### Fixed + +- `Graph.Data_Frame()` now handles the `Int64` data type from `pandas`, thanks + to [@Adriankhl](https://github.com/Adriankhl). See PR + [#609](https://github.com/igraph/python-igraph/pull/609) for details. + +- `Graph.layout_lgl()` `root` argument is now optional (as it should have been). + +- The `VertexClustering` class now handles partial dendrograms correctly. + +## [0.10.2] - 2022-10-14 + +### Added + +- `python-igraph` is now tested in Python 3.11. + +- Added `Graph.modularity_matrix()` to calculate the modularity matrix of + a graph. + +- Added `Graph.get_k_shortest_paths()`, thanks to + [@sombreslames](https://github.com/user/sombreslames). See PR + [#577](https://github.com/igraph/python-igraph/pull/577) for details. + +- The `setup.py` script now also accepts environment variables instead of + command line arguments to configure several aspects of the build process + (i.e. whether a fully static extension is being built, or whether it is + allowed to use `pkg-config` to retrieve the compiler and linker flags for + an external `igraph` library instead of the vendored one). The environment + variables are named similarly to the command line arguments but in + uppercase, dashes replaced with underscores, and they are prefixed with + `IGRAPH_` (i.e. `--use-pkg-config` becomes `IGRAPH_USE_PKG_CONFIG`). + +### Changed + +- The C core of igraph was updated to version 0.10.2, fixing a range of bugs + originating from the C core. + +### Fixed + +- Fixed a crash in `Graph.decompose()` that was accidentally introduced in + 0.10.0 during the transition to `igraph_graph_list_t` in the C core. + +- `Clustering.sizes()` now works correctly even if the membership vector + contains `None` items. + +- `Graph.modularity()` and `Graph.community_multilevel()` now correctly expose + the `resolution` parameter. + +- Fixed a reference leak in `Graph.is_chordal()` that decreased the reference + count of Python's built-in `True` and `False` constants unnecessarily. + +- Unit tests updated to get rid of deprecation warnings in Python 3.11. + +## [0.10.1] - 2022-09-12 + +### Added + +- Added `Graph.minimum_cycle_basis()` and `Graph.fundamental_cycles()` + +- `Graph.average_path_length()` now supports edge weights. + +### Fixed + +- Restored missing exports from `igraph.__all__` that used to be in the main + `igraph` package before 0.10.0. + +## [0.10.0] - 2022-09-05 + +### 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. + +- `Graph.list_triangles()` lists all triangles in a graph. + +- `Graph.reverse_edges()` reverses some or all edges of a graph. + +- `Graph.Degree_Sequence()` now supports the `"no_multiple_uniform"` generation + method, which generates simple graphs, sampled uniformly, using rejection + sampling. + +- `Graph.Lattice()` now supports per-dimension periodicity control. + +- `Graph.get_adjacency()` now allows the user to specify whether loop edges + should be counted once or twice, or not at all. + +- `Graph.get_laplacian()` now supports left-, right- and symmetric normalization. + +- `Graph.modularity()` now supports setting the resolution with the + `resolution=...` parameter. + +### Changed + +- The C core of igraph was updated to version 0.10.0. + +- We now publish `abi3` wheels on PyPI from CPython 3.9 onwards, making it + possible to use an already-built Python wheel with newer minor Python + releases (and also reducing the number of wheels we actually need to + publish). Releases for CPython 3.7 and 3.8 still use version-specific wheels + because the code of the C part of the extension contains conditional macros + for CPython 3.7 and 3.8. + +- Changed default value of the `use_vids=...` argument of `Graph.DataFrame()` + to `True`, thanks to [@fwitter](https://github.com/user/fwitter). + +- `Graph.Degree_Sequence()` now accepts all sorts of sequences as inputs, not + only lists. + +### Fixed + +- The Matplotlib backend now allows `edge_color` and `edge_width` to be set + on an edge-by-edge basis. + +### Removed + +- Dropped support for Python 3.6. + +- 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. + +- Removed the `eids` keyword argument of `get_adjacency()`. + +### Deprecated + +- `Graph.clusters()` is now deprecated; use `Graph.connected_components()` or + its already existing shorter alias, `Graph.components()`. + +- `Graph.shortest_paths()` is now deprecated; use `Graph.distances()` instead. + +## [0.9.11] + +### Added + +- We now publish `musllinux` wheels on PyPI. + +### Changed + +- Vendored igraph was updated to version 0.9.9. + +### Fixed + +- Graph union and intersection (by name) operators now verify that there are no + duplicate names within the individual graphs. + +- Fixed a memory leak in `Graph.union()` when edge maps were used; see + [#534](https://github.com/igraph/python-igraph/issues/534) for details. + +- Fixed a bug in the Cairo and Matplotlib backends that prevented edges with + labels from being drawn properly; see + [#535](https://github.com/igraph/python-igraph/issues/535) for details. + +## [0.9.10] + +### Changed + +- Vendored igraph was updated to version 0.9.8. + +### Fixed + +- Fixed plotting of curved edges in the Cairo plotting backend. + +- `setup.py` now looks for `igraph.pc` recursively in `vendor/install`; this + fixes building igraph from source in certain Linux distributions + +- `Graph.shortest_paths()` does not crash with zero-length weight vectors any + more + +- Fix a memory leak in `Graph.delete_vertices()` and other functions that + convert a list of vertex IDs internally to an `igraph_vs_t` object, see + [#503](https://github.com/igraph/python-igraph/issues/503) for details. + +- Fixed potential memory leaks in `Graph.maximum_cardinality_search()`, + `Graph.get_all_simple_paths()`, `Graph.get_subisomorphisms_lad()`, + `Graph.community_edge_betweenness()`, as well as the `union` and `intersection` + operators. + +- Fix a crash that happened when subclassing `Graph` and overriding `__new__()` + in the subclass; see [#496](https://github.com/igraph/python-igraph/issues/496) + for more details. + +- Documentation now mentions that we now support graphs of size 5 or 6 for + isomorphism / motif calculations if the graph is undirected + +## [0.9.9] + +### Changed + +- Vendored igraph was updated to version 0.9.6. + +### Fixed + +- Fixed a performance bottleneck in `VertexSeq.select()` and `EdgeSeq.select()` + for the case when the `VertexSeq` or the `EdgeSeq` represents the whole + graph. See [#494](https://github.com/igraph/python-igraph/issues/494) for + more details. + +- 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 + +- `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 + [@limburgher](https://github.com/limburgher). + ([#449](https://github.com/igraph/python-igraph/pull/449)) + +## [0.9.7] + +### Added + +- 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. + +- `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 + [@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. + +- 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. + +- 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 + +- 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 + +- `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. + +- 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 + +- 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. + +- `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.4. + +### Fixed + +- 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 + 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. + +- `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. + 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 + +- 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. + +- 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 + 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 Github Actions 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 for +a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 +are documented above. + +[1.0.0]: https://github.com/igraph/python-igraph/compare/0.11.9...1.0.0 +[0.11.9]: https://github.com/igraph/python-igraph/compare/0.11.8...0.11.9 +[0.11.8]: https://github.com/igraph/python-igraph/compare/0.11.7...0.11.8 +[0.11.7]: https://github.com/igraph/python-igraph/compare/0.11.6...0.11.7 +[0.11.6]: https://github.com/igraph/python-igraph/compare/0.11.5...0.11.6 +[0.11.5]: https://github.com/igraph/python-igraph/compare/0.11.4...0.11.5 +[0.11.4]: https://github.com/igraph/python-igraph/compare/0.11.3...0.11.4 +[0.11.3]: https://github.com/igraph/python-igraph/compare/0.11.2...0.11.3 +[0.11.2]: https://github.com/igraph/python-igraph/compare/0.11.0...0.11.2 +[0.11.0]: https://github.com/igraph/python-igraph/compare/0.10.8...0.11.0 +[0.10.8]: https://github.com/igraph/python-igraph/compare/0.10.7...0.10.8 +[0.10.7]: https://github.com/igraph/python-igraph/compare/0.10.6...0.10.7 +[0.10.6]: https://github.com/igraph/python-igraph/compare/0.10.5...0.10.6 +[0.10.5]: https://github.com/igraph/python-igraph/compare/0.10.4...0.10.5 +[0.10.4]: https://github.com/igraph/python-igraph/compare/0.10.3...0.10.4 +[0.10.3]: https://github.com/igraph/python-igraph/compare/0.10.2...0.10.3 +[0.10.2]: https://github.com/igraph/python-igraph/compare/0.10.1...0.10.2 +[0.10.1]: https://github.com/igraph/python-igraph/compare/0.10.0...0.10.1 +[0.10.0]: https://github.com/igraph/python-igraph/compare/0.9.11...0.10.0 +[0.9.11]: https://github.com/igraph/python-igraph/compare/0.9.10...0.9.11 +[0.9.10]: https://github.com/igraph/python-igraph/compare/0.9.9...0.9.10 +[0.9.9]: https://github.com/igraph/python-igraph/compare/0.9.8...0.9.9 +[0.9.8]: https://github.com/igraph/python-igraph/compare/0.9.7...0.9.8 +[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 +[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 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..e803bfa79 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,53 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: igraph +message: >- + If you use igraph, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Gábor + family-names: Csárdi + orcid: 'https://orcid.org/0000-0001-7098-9676' + - given-names: Tamás + family-names: Nepusz + orcid: 'https://orcid.org/0000-0002-1451-338X' + - given-names: Szabolcs + family-names: Horvát + orcid: 'https://orcid.org/0000-0002-3100-523X' + - given-names: Vincent Antonio + family-names: Traag + orcid: 'https://orcid.org/0000-0003-3170-3879' + - given-names: Fabio + family-names: Zanini + orcid: 'https://orcid.org/0000-0001-7097-8539' + - given-names: Daniel + family-names: Noom +repository-code: 'https://github.com/igraph/python-igraph' +url: 'https://igraph.org' +abstract: >- + igraph is a C library for complex network analysis and + graph theory, with emphasis on efficiency, portability and + ease of use. +keywords: + - network analysis + - graph theory +license: GPL-2.0-or-later +version: 0.10.5 +date-released: '2023-07-01' +preferred-citation: + type: article + authors: + - given-names: Gábor + family-names: Csárdi + orcid: 'https://orcid.org/0000-0001-7098-9676' + - given-names: Tamás + family-names: Nepusz + orcid: 'https://orcid.org/0000-0002-1451-338X' + journal: "InterJournal, Complex Systems" + start: 1695 # First page number + title: "The igraph software package for complex network research" + year: 2006 + type: article diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000..d22d5bd6f --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,111 @@ +## Contributors ✨ + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tamás Nepusz
Tamás Nepusz

💻
Fabio Zanini
Fabio Zanini

💻
Kevin Zhu
Kevin Zhu

💻
Gábor Csárdi
Gábor Csárdi

💻
Szabolcs Horvát
Szabolcs Horvát

💻
Vincent Traag
Vincent Traag

💻
deeenes
deeenes

💻
Seungoh Han
Seungoh Han

💻
Artem V L
Artem V L

💻
Yesung(Isaac) Lee
Yesung(Isaac) Lee

💻
John Boy
John Boy

💻
Casper da Costa-Luis
Casper da Costa-Luis

💻
Alberto Alcolea
Alberto Alcolea

💻
Árpád Horváth
Árpád Horváth

💻
ebraminio
ebraminio

💻
Fabian Witter
Fabian Witter

💻
Jan Katins
Jan Katins

💻
Nick Eubank
Nick Eubank

💻
Peter Scott
Peter Scott

💻
Sriram-Pattabiraman
Sriram-Pattabiraman

💻
Sviatoslav
Sviatoslav

💻
Ah-Young Nho
Ah-Young Nho

💻
Frederik Harwath
Frederik Harwath

💻
Navid Dianati
Navid Dianati

💻
abe-winter
abe-winter

💻
Alejandro Rivero
Alejandro Rivero

💻
Ariki
Ariki

💻
Casper van Elteren
Casper van Elteren

💻
Charles Tapley Hoyt
Charles Tapley Hoyt

💻
Christoph Gohlke
Christoph Gohlke

💻
Christopher Falter
Christopher Falter

💻
FredInChina
FredInChina

💻
Friso van Vollenhoven
Friso van Vollenhoven

💻
Gabor Szarnyas
Gabor Szarnyas

💻
Gao Fangshu
Gao Fangshu

💻
Grzegorz Chilczuk
Grzegorz Chilczuk

💻
Gwyn Ciesla
Gwyn Ciesla

💻
Hong Xu
Hong Xu

💻
Jay Smith
Jay Smith

💻
MapleCCC
MapleCCC

💻
Marco Köpcke
Marco Köpcke

💻
Markus Elfring
Markus Elfring

💻
Martino Mensio
Martino Mensio

💻
Matas
Matas

💻
Mike Lissner
Mike Lissner

💻
Philipp A.
Philipp A.

💻
Puneetha Pai
Puneetha Pai

💻
S Murthy
S Murthy

💻
Scott Gigante
Scott Gigante

💻
Thierry Thomas
Thierry Thomas

💻
Willem van den Boom
Willem van den Boom

💻
Yisu Remy Wang
Yisu Remy Wang

💻
YY Ahn
YY Ahn

💻
kmankinen
kmankinen

💻
odidev
odidev

💻
sombreslames
sombreslames

💻
szcf-weiya
szcf-weiya

💻
tristanlatr
tristanlatr

💻
JDPowell648
JDPowell648

📖
k.h.lai
k.h.lai

💻
Anton Grübel
Anton Grübel

💻
flange-ipb
flange-ipb

💻
Paul m. p. Peny
Paul m. p. Peny

💻
David R. Connell
David R. Connell

💻
Rodrigo Monteiro de Moraes de Arruda Falcão
Rodrigo Monteiro de Moraes de Arruda Falcão

💻
Kreijstal
Kreijstal

💻
Michael Schneider
Michael Schneider

💻
Thomas Krijnen
Thomas Krijnen

💻
Tim Bernhard
Tim Bernhard

💻
Bea Márton
Bea Márton

💻
Sanat Kumar Gupta
Sanat Kumar Gupta

💻
+ + + + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 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 cfccd9149..39e2ee9fe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,21 @@ -include setup.cfg -include src/*.h +prune docker +prune .git +prune .github + +include src/_igraph/*.h include MANIFEST.in -include COPYING -include scripts/mkdoc.sh -include scripts/epydoc-patched -include scripts/epydoc.cfg -include test/*.py +include scripts/*.sh +include scripts/*.py +include tests/*.py + +include CHANGELOG.md +include CONTRIBUTORS.md +include CITATION.cff + +graft vendor/source/igraph + +graft doc +prune doc/html +prune doc/source/tutorials + +global-exclude .dockerignore .DS_Store .gitattributes .gitignore .gitmodules diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..5ee2b2c09 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +TAG = igraph/manylinux + +build-wheel: + docker build -f docker/manylinux.docker -t $(TAG) . + +copy-wheel: + rm -rf docker/wheelhouse + mkdir docker/wheelhouse + docker run --user `id -u` -v `pwd`/docker/wheelhouse:/output $(TAG) sh -c "cp /wheelhouse/*manylinux* /output" + +.PHONY: build-wheel copy-wheel diff --git a/README.md b/README.md index 2960619bd..1f7162788 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,318 @@ -[![](https://travis-ci.org/igraph/python-igraph.svg?branch=master)](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) +[![PyPI pyversions](https://img.shields.io/pypi/pyversions/igraph)](https://pypi.python.org/pypi/igraph) +[![PyPI wheels](https://img.shields.io/pypi/wheel/igraph.svg)](https://pypi.python.org/pypi/igraph) +[![Documentation Status](https://readthedocs.org/projects/igraph/badge/?version=latest)](https://igraph.readthedocs.io/) -Python interface for the igraph library ---------------------------------------- +# 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. -## Installation +Since version 0.10.2, the documentation is hosted on +[readthedocs](https://igraph.readthedocs.io). Earlier versions are documented +on [our old website](https://igraph.org/python/versions/0.10.1/). + +igraph is a collaborative work of many people from all around the world — +see the [list of contributors here](./CONTRIBUTORS.md). + +## Citation + +If you use igraph in your research, please cite + +> Csardi, G., & Nepusz, T. (2006). The igraph software package for complex network research. InterJournal, Complex Systems, 1695. + +# Installation + +We aim to provide wheels on PyPI for most of the stock Python versions; +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 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 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 +``` + +and then run + +``` +pip install 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 `igraph` from source under Windows for +Python 3.7 and later. Make sure that you have Microsoft Visual Studio 2015 or +later installed, and of course Python 3.7 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 igraph using `pip install .`, assuming that you have `pip` +installed in your Python environment. + +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] ``` -$ sudo python setup.py install + +where `[arch]` is either `Win32` for 32-bit builds or `x64` for 64-bit builds. +Also, when building in MSYS2, you need to set the `SETUPTOOLS_USE_DISTUTILS` +environment variable to `stdlib`; this is because MSYS2 uses a patched version +of `distutils` that conflicts with `setuptools >= 60.0`. + +> [!TIP] +> You need the following packages: +> `$MINGW_PACKAGE_PREFIX-python-pip $MINGW_PACKAGE_PREFIX-python-setuptools $MINGW_PACKAGE_PREFIX-cc $MINGW_PACKAGE_PREFIX-cmake` + +### 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 `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] +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 `igraph` again by simply running `pip install .`. +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 +igraph version that the Python interface should compile against. However, if +you want to link the Python interface to a custom installation of the C core +that has already been compiled and installed on your system, you can ask our +build system to use the pre-compiled version. This option requires that your +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: + +```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 after setting the environment variable named +`IGRAPH_USE_PKG_CONFIG` to `1` to indicate that you want to use an +igraph instance discoverable with `pkg-config`: + +```bash +IGRAPH_USE_PKG_CONFIG=1 pip install igraph ``` -See details in [Installing Python Modules](https://docs.python.org/2/install/). + +Alternatively, if you have already downloaded and extracted the source code +of igraph, you can run `pip install` on the source tree directly: + +```bash +IGRAPH_USE_PKG_CONFIG=1 pip install . +``` + +(Note that you need the `IGRAPH_USE_PKG_CONFIG=1` environment variable +for both invocations, otherwise the call to `pip install` would still +build the vendored C core instead of linking to an existing installation). + +This option is primarily intended for package maintainers in Linux +distributions so they can ensure that the packaged Python interface links to +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 MSVC, it is currently not +possible to build against an external library on MSVC. + +In case you are already using a MSYS2/[MinGW](https://www.mingw-w64.org/) and already have +[mingw-w64-igraph](https://packages.msys2.org/base/mingw-w64-igraph) installed, +simply type: +``` +IGRAPH_USE_PKG_CONFIG=1 SETUPTOOLS_USE_DISTUTILS=stdlib pip install igraph +``` +to build. + +**Warning:** the Python interface is guaranteed to work only with the same +version of the C core that is vendored inside the `vendor/source/igraph` +folder. While we try hard not to break API or ABI in the C core of igraph +between minor versions in the 0.x branch and we will keep on doing so for major +versions once 1.0 is released, there are certain functions in the C API that +are marked as _experimental_ (see the documentation of the C core for details), +and we reserve the right to break the APIs of those functions, even if they are +already exposed in a higher-level interface. This is because the easiest way to +test these functions in real-life research scenarios is to expose them in one +of the higher level interfaces. Therefore, if you unbundle the vendored source +code of igraph and link to an external version instead, we can make no +guarantees about stability unless you link to the exact same version as the +one we have vendored in this source tree. + +If you are curious about which version of the Python interface is compatible +with which version of the C core, you can look up the corresponding tag in +Github and check which revision of the C core the repository points to in +the `vendor/source/igraph` submodule. ## Compiling the development version -If you have downloaded the source code from Github and not PyPI, chances are -that you have the latest development version, which might not be compatible -with the latest release of the C core of igraph. Therefore, to install the -bleeding edge version, you need to instruct the setup script to download the -latest development version of the C core as well: +If you want to install the development version, the easiest way to do so is to +install it using + +```bash +pip install git+https://github.com/igraph/python-igraph +``` + +This automatically fetches the development version from the repository, builds +the package and installs it. By default, this will install the Python interface +from the `main` branch, which is used as the basis for the development of the +current release series. Unstable and breaking changes are being made in the +`develop` branch. You can install this similarly by doing + +```bash +pip install git+https://github.com/igraph/python-igraph@develop +``` + +In addition to `git`, the installation of the development version requires some +additional dependencies, read further below for details. + +For more information about installing directly from `git` using `pip` see +https://pip.pypa.io/en/stable/topics/vcs-support/#git. + +Alternatively, you can clone this repository locally. This repository contains a +matching version of the C core of `igraph` as a git submodule. In order to +install the development version from source, 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 you can install the package directly with `pip` (see also the previous section): + +```bash +pip install . +``` + +If you would like to create a source distribution or a Python wheel instead of +installing the module directly in your Python environment, use a standard build +frontend like [build](https://pypa-build.readthedocs.io/en/stable/). If you +use [pipx](https://pypa.github.io/pipx/) to isolate command-line Python tools +in their own separate virtualenvs, you can simply run: + +```bash +pipx run build +``` + +### Running unit tests + +Unit tests can be executed from within the repository directory with `tox` or +with the built-in `unittest` module: + +```bash +python -m unittest +``` +Note that unit tests have additional dependencies like NumPy, PIL or +`matplotlib`. The unit test suite will try to do its best to skip tests +requiring external dependencies, but if you want to make sure that all the unit +tests are executed, either use `tox` (which will take care of installing the +test dependencies in a virtualenv), or install the module with the `test` +extras: + +```bash +pip install '.[test]' ``` -$ sudo python setup.py develop --c-core-url https://github.com/igraph/igraph/archive/master.tar.gz + +# Contributing + +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 `igraph`, follow the steps above about installing the development version. Make sure that you do so by cloning the repository locally so that you are able to make changes. + +For easier development, you can install `igraph` in "editable" (i.e. +development) mode so your changes in the Python source code are picked up +automatically by Python: + +```bash +pip install -e . ``` -## Notes +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 +`pip install -e .` again. Compilation of the C core of `igraph` is +cached in ``vendor/build`` and ``vendor/install`` so subsequent builds are much +faster than the first one as the C core does not need to be recompiled. + +# Notes + +## 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.9 to 3.13, inclusive. Please refer to [this +page](https://devguide.python.org/versions/) for the status of Python +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. + +## 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/debian/changelog b/debian/changelog deleted file mode 100644 index b9ac4b75a..000000000 --- a/debian/changelog +++ /dev/null @@ -1,20 +0,0 @@ -python-igraph (0.6-3) unstable; urgency=low - - * Added python-docutils to build dependencies. - * Removed PDF documentation from python-igraph-doc. - * Not running unittests during build because some stochastic test - cases fail on i386. - - -- Tamas Nepusz Wed, 27 Jun 2012 22:33:00 +0200 - -python-igraph (0.6-2) unstable; urgency=low - - * Added pkg-config to build dependencies. - - -- Tamas Nepusz Wed, 27 Jun 2012 22:21:00 +0200 - -python-igraph (0.6-1) unstable; urgency=low - - * Initial Release. - - -- Tamas Nepusz Tue, 26 Jun 2012 19:17:00 +0200 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 7f8f011eb..000000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -7 diff --git a/debian/control b/debian/control deleted file mode 100644 index d194b2ced..000000000 --- a/debian/control +++ /dev/null @@ -1,48 +0,0 @@ -Source: python-igraph -Section: python -Priority: optional -Maintainer: Tamas Nepusz -Build-Depends: debhelper (>= 7.0.50~), python-all-dev (>= 2.6.6-3~), - python3-all-dev (>= 3.2), libxml2-dev, libigraph0-dev (>= 0.6~), python-epydoc, - python-setuptools, python3-setuptools, pkg-config, python-docutils -Standards-Version: 3.9.4 -X-Python-Version: >= 2.5 -X-Python3-Version: >= 3.2 -Homepage: http://igraph.org - -Package: python-igraph -Architecture: any -Depends: ${shlibs:Depends}, ${python:Depends}, ${misc:Depends}, libigraph0 (>= 0.6~) -Suggests: python-igraph-doc, python-cairo -Provides: ${python:Provides} -Description: Python interface for the igraph library (Python 2) - 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. - . - This package contains the Python 2 interface of igraph. - -Package: python3-igraph -Architecture: any -Depends: ${shlibs:Depends}, ${python3:Depends}, ${misc:Depends}, libigraph0 (>= 0.6~) -Suggests: python-igraph-doc, python3-cairo -Provides: ${python3:Provides} -Description: Python interface for the igraph library (Python 3) - 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. - . - This package contains the Python 3 interface of igraph. - -Package: python-igraph-doc -Architecture: all -Section: doc -Depends: ${misc:Depends} -Suggests: python-igraph, python-doc -Description: Documentation of the Python interface of the igraph library - 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. - . - This package contains the API documentation of the Python interface of - the igraph library. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 62814f0fb..000000000 --- a/debian/copyright +++ /dev/null @@ -1,29 +0,0 @@ -This package was debianized by Tamas Nepusz on Tue, 26 Jun 2012 19:17:00 +0200. - -It was downloaded from http://pypi.python.org/packages/source/p/python-igraph/python-igraph-0.6.tar.gz - -Upstream Authors: Gabor Csardi, Tamas Nepusz - -Copyright: - -Copyright (C) 2005-2012 Gabor Csardi, Tamas Nepusz - -License: - -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 with -the Debian GNU/Linux distribution in file /usr/share/common-licenses/GPL; -if not, write to the Free Software Foundation, Inc., 51 Franklin Street, -Fifth Floor, Boston, MA 02110-1301 USA - -On Debian systems, the complete text of the GNU General Public -License, version 2, can be found in /usr/share/common-licenses/GPL-2. diff --git a/debian/python-igraph-doc.install b/debian/python-igraph-doc.install deleted file mode 100644 index c96544278..000000000 --- a/debian/python-igraph-doc.install +++ /dev/null @@ -1 +0,0 @@ -doc/api/html/* /usr/share/doc/python-igraph-doc \ No newline at end of file diff --git a/debian/python-igraph.install b/debian/python-igraph.install deleted file mode 100644 index 7dbca8f2a..000000000 --- a/debian/python-igraph.install +++ /dev/null @@ -1,3 +0,0 @@ -/usr/lib/python2*/*-packages/igraph/ -/usr/include/python2*/python-igraph/ -/usr/bin/igraph \ No newline at end of file diff --git a/debian/python3-igraph.install b/debian/python3-igraph.install deleted file mode 100644 index bb348a82f..000000000 --- a/debian/python3-igraph.install +++ /dev/null @@ -1,2 +0,0 @@ -/usr/lib/python3*/*-packages/igraph/ -/usr/include/python3*/python-igraph/ \ No newline at end of file diff --git a/debian/rules b/debian/rules deleted file mode 100755 index c52f42684..000000000 --- a/debian/rules +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- -# Sample debian/rules that uses debhelper. -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. - -# Uncomment this to turn on verbose mode. -# export DH_VERBOSE=1 - -PYTHON2=$(shell pyversions -vr) -PYTHON3=$(shell py3versions -vr) - -%: - dh $@ --with python2,python3 - -# Disabled for the time being -# ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) -# test-python%: -# python$* setup.py test -vv -# -# override_dh_auto_test: $(PYTHON2:%=test-python%) $(PYTHON3:%=test-python%) -# endif - -build-python%: - python$* setup.py build - -override_dh_auto_build: $(PYTHON3:%=build-python%) - dh_auto_build - chmod a+x scripts/epydoc-patched - scripts/mkdoc.sh $(CURDIR)/build/lib.linux-*2.7 - -install-python%: - python$* setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb - -override_dh_auto_install: $(PYTHON3:%=install-python%) - dh_auto_install - -override_dh_auto_clean: - dh_auto_clean - rm -rf build - rm -rf *.egg-info - diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 163aaf8d8..000000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (quilt) diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 2d2eb292b..000000000 --- a/doc/Makefile +++ /dev/null @@ -1,130 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/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 "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-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 "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - make -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/examples_sphinx-gallery/README.rst b/doc/examples_sphinx-gallery/README.rst new file mode 100644 index 000000000..6fe17a0c1 --- /dev/null +++ b/doc/examples_sphinx-gallery/README.rst @@ -0,0 +1,10 @@ +.. _gallery-of-examples: + +Gallery of Examples +=================== + +Gallery of examples for `python-igraph` illustrating graph generation, analysis, and plotting. + +Impatient? Check out the :ref:`tutorials-quickstart`. + +Too little detail? Read the :doc:`extended tutorial <../tutorial>`. diff --git a/doc/examples_sphinx-gallery/articulation_points.py b/doc/examples_sphinx-gallery/articulation_points.py new file mode 100644 index 000000000..6492f1f6f --- /dev/null +++ b/doc/examples_sphinx-gallery/articulation_points.py @@ -0,0 +1,39 @@ +""" +.. _tutorials-articulation-points: + +=================== +Articulation Points +=================== + +This example shows how to compute and visualize the `articulation points `_ in a graph using :meth:`igraph.GraphBase.articulation_points`. For an example on bridges instead, see :ref:`tutorials-bridges`. + +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# First, we construct a graph. This example shows usage of graph formulae: +g = ig.Graph.Formula( + "0-1-2-0, 3:4:5:6 - 3:4:5:6, 2-3-7-8", +) + +# %% +# Now we are aready to find the articulation points as a vertex sequence +articulation_points = g.vs[g.articulation_points()] + +# %% +# Finally, we can plot the graph +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + vertex_size=30, + vertex_color="lightblue", + vertex_label=range(g.vcount()), + vertex_frame_color=["red" if v in articulation_points else "black" for v in g.vs], + vertex_frame_width=[3 if v in articulation_points else 1 for v in g.vs], + edge_width=0.8, + edge_color="gray", +) +plt.show() diff --git a/doc/examples_sphinx-gallery/betweenness.py b/doc/examples_sphinx-gallery/betweenness.py new file mode 100644 index 000000000..7c52bdb08 --- /dev/null +++ b/doc/examples_sphinx-gallery/betweenness.py @@ -0,0 +1,91 @@ +""" +.. _tutorials-betweenness: + +======================= +Betweenness +======================= + +This example demonstrates how to visualize both vertex and edge betweenness with a custom defined color palette. We use the methods :meth:`igraph.GraphBase.betweenness` and :meth:`igraph.GraphBase.edge_betweenness` respectively, and demonstrate the effects on a standard `Krackhardt Kite `_ graph, as well as a `Watts-Strogatz `_ random graph. + +""" +import random +import matplotlib.pyplot as plt +from matplotlib.cm import ScalarMappable +from matplotlib.colors import LinearSegmentedColormap, Normalize +import igraph as ig + + +# %% +# We define a function that plots the graph on a Matplotlib axis, along with +# its vertex and edge betweenness values. The function also generates some +# color bars on the sides to see how they translate to each other. We use +# `Matplotlib's Normalize class `_ +# to ensure that our color bar ranges are correct, as well as *igraph*'s +# :meth:`igraph.utils.rescale` to rescale the betweennesses in the interval +# ``[0, 1]``. +def plot_betweenness(g, vertex_betweenness, edge_betweenness, ax, cax1, cax2): + """Plot vertex/edge betweenness, with colorbars + + Args: + g: the graph to plot. + ax: the Axes for the graph + cax1: the Axes for the vertex betweenness colorbar + cax2: the Axes for the edge betweenness colorbar + """ + + # Rescale betweenness to be between 0.0 and 1.0 + scaled_vertex_betweenness = ig.rescale(vertex_betweenness, clamp=True) + scaled_edge_betweenness = ig.rescale(edge_betweenness, clamp=True) + print(f"vertices: {min(vertex_betweenness)} - {max(vertex_betweenness)}") + print(f"edges: {min(edge_betweenness)} - {max(edge_betweenness)}") + + # Define mappings betweenness -> color + cmap1 = LinearSegmentedColormap.from_list("vertex_cmap", ["pink", "indigo"]) + cmap2 = LinearSegmentedColormap.from_list("edge_cmap", ["lightblue", "midnightblue"]) + + # Plot graph + g.vs["color"] = [cmap1(betweenness) for betweenness in scaled_vertex_betweenness] + g.vs["size"] = ig.rescale(vertex_betweenness, (10, 50)) + g.es["color"] = [cmap2(betweenness) for betweenness in scaled_edge_betweenness] + g.es["width"] = ig.rescale(edge_betweenness, (0.5, 1.0)) + ig.plot( + g, + target=ax, + layout="fruchterman_reingold", + vertex_frame_width=0.2, + ) + + # Color bars + norm1 = ScalarMappable(norm=Normalize(0, max(vertex_betweenness)), cmap=cmap1) + norm2 = ScalarMappable(norm=Normalize(0, max(edge_betweenness)), cmap=cmap2) + plt.colorbar(norm1, cax=cax1, orientation="horizontal", label="Vertex Betweenness") + plt.colorbar(norm2, cax=cax2, orientation="horizontal", label="Edge Betweenness") + + +# %% +# First, generate a graph, e.g. the Krackhardt Kite Graph: +random.seed(0) +g1 = ig.Graph.Famous("Krackhardt_Kite") + +# %% +# Then we can compute vertex and edge betweenness: +vertex_betweenness1 = g1.betweenness() +edge_betweenness1 = g1.edge_betweenness() + +# %% As a second example, we generate and analyze a Watts Strogatz graph: +g2 = ig.Graph.Watts_Strogatz(dim=1, size=150, nei=2, p=0.1) +vertex_betweenness2 = g2.betweenness() +edge_betweenness2 = g2.edge_betweenness() + +# %% +# Finally, we plot the two graphs, each with two colorbars for vertex/edge +# betweenness +fig, axs = plt.subplots( + 3, 2, + figsize=(7, 6), + gridspec_kw={"height_ratios": (20, 1, 1)}, +) +plot_betweenness(g1, vertex_betweenness1, edge_betweenness1, *axs[:, 0]) +plot_betweenness(g2, vertex_betweenness2, edge_betweenness2, *axs[:, 1]) +fig.tight_layout(h_pad=1) +plt.show() diff --git a/doc/examples_sphinx-gallery/bipartite_matching.py b/doc/examples_sphinx-gallery/bipartite_matching.py new file mode 100644 index 000000000..1046e1322 --- /dev/null +++ b/doc/examples_sphinx-gallery/bipartite_matching.py @@ -0,0 +1,54 @@ +""" +.. _tutorials-bipartite-matching: + +========================== +Maximum Bipartite Matching +========================== + +This example demonstrates an efficient way to find and visualise a maximum biparite matching using :meth:`igraph.Graph.maximum_bipartite_matching`. +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# First, we construct a bipartite graph, assigning: +# - nodes 0-4 to one side +# - 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)], +) + +# %% +# We can easily check that the graph is indeed bipartite: +assert g.is_bipartite() + +# %% +# Now can can compute the maximum bipartite matching: +matching = g.maximum_bipartite_matching() + +# %% +# It's easy to print matching pairs of vertices +matching_size = 0 +print("Matching is:") +for i in range(5): + print(f"{i} - {matching.match_of(i)}") + if matching.is_matched(i): + matching_size += 1 +print("Size of maximum matching is:", matching_size) + +# %% +# Finally, we can plot the bipartite graph, highlighting the edges connecting +# maximal matches by a red color: +fig, ax = plt.subplots(figsize=(7, 3)) +ig.plot( + g, + target=ax, + layout=g.layout_bipartite(), + vertex_size=30, + vertex_label=range(g.vcount()), + vertex_color="lightblue", + edge_width=[3 if e.target == matching.match_of(e.source) else 1.0 for e in g.es], + edge_color=["red" if e.target == matching.match_of(e.source) else "black" for e in g.es] +) diff --git a/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py b/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py new file mode 100644 index 000000000..eec39a81b --- /dev/null +++ b/doc/examples_sphinx-gallery/bipartite_matching_maxflow.py @@ -0,0 +1,68 @@ +""" +.. _tutorials-bipartite-matching-maxflow: + +========================================== +Maximum Bipartite Matching by Maximum Flow +========================================== + +This example presents how to visualise bipartite matching using maximum flow (see :meth:`igraph.Graph.maxflow`). + +.. note:: :meth:`igraph.Graph.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`. +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# We start by creating the bipartite directed 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, +) + +# %% +# We assign: +# - nodes 0-3 to one side +# - nodes 4-8 to the other side +g.vs[range(4)]["type"] = True +g.vs[range(4, 9)]["type"] = False + +# %% +# Then we add a source (vertex 9) and a sink (vertex 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("Size of maximum matching (maxflow) is:", flow.value) + +# %% +# Let's compare the output against :meth:`igraph.Graph.maximum_bipartite_matching`: + +# delete the source and sink, which are unneeded for this function. +g2 = g.copy() +g2.delete_vertices([9, 10]) +matching = g2.maximum_bipartite_matching() +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) + +# %% +# Last, we can display the original flow graph nicely with the matchings added. +# To achieve a pleasant visual effect, we set the positions of source and sink +# manually: +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=30, + 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/examples_sphinx-gallery/bridges.py b/doc/examples_sphinx-gallery/bridges.py new file mode 100644 index 000000000..08dc47d48 --- /dev/null +++ b/doc/examples_sphinx-gallery/bridges.py @@ -0,0 +1,81 @@ +""" +.. _tutorials-bridges: + +======== +Bridges +======== + +This example shows how to compute and visualize the `bridges `_ in a graph using :meth:`igraph.GraphBase.bridges`. For an example on articulation points instead, see :ref:`tutorials-articulation-points`. +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# Let's start with a simple example. We begin by constructing a graph that +# includes a few bridges: +g = ig.Graph(14, [(0, 1), (1, 2), (2, 3), (0, 3), (0, 2), (1, 3), (3, 4), + (4, 5), (5, 6), (6, 4), (6, 7), (7, 8), (7, 9), (9, 10), (10 ,11), + (11 ,7), (7, 10), (8, 9), (8, 10), (5, 12), (12, 13)]) + +# %% +# Then we can use a function to actually find the bridges, i.e. the edges that +# connect different parts of the graph: +bridges = g.bridges() + +# %% +# We set a separate color for those edges, to emphasize then in a plot: +g.es["color"] = "gray" +g.es[bridges]["color"] = "red" +g.es["width"] = 0.8 +g.es[bridges]["width"] = 1.2 + +# %% +# Finally, we plot the graph using that emphasis: +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + vertex_size=30, + vertex_color="lightblue", + vertex_label=range(g.vcount()), +) +plt.show() + +# %% +# Advanced: Cutting Effect +# -------------------------- +# Bridges are edges that when removed, will separate the graph into more components then they started with. To emphasise the removal of edges from the graph, we can add small "x" effect to each of the bridges by using edge labels. + +# %% +# As before, we begin by constructing the graph: +g = ig.Graph(14, [(0, 1), (1, 2), (2, 3), (0, 3), (0, 2), (1, 3), (3, 4), + (4, 5), (5, 6), (6, 4), (6, 7), (7, 8), (7, 9), (9, 10), (10 ,11), + (11 ,7), (7, 10), (8, 9), (8, 10), (5, 12), (12, 13)]) + +# %% +# We then find and set the color for the bridges, but this time we also set a +# label for those edges: +bridges = g.bridges() +g.es["color"] = "gray" +g.es[bridges]["color"] = "red" +g.es["width"] = 0.8 +g.es[bridges]["width"] = 1.2 +g.es["label"] = "" +g.es[bridges]["label"] = "x" + +# %% +# Finally, we can plot the graph: +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + vertex_size=30, + vertex_color="lightblue", + vertex_label=range(g.vcount()), + edge_background="#FFF0", # transparent background color + edge_align_label=True, # make sure labels are aligned with the edge + edge_label=g.es["label"], + edge_label_color="red", +) +plt.show() diff --git a/doc/examples_sphinx-gallery/cluster_contraction.py b/doc/examples_sphinx-gallery/cluster_contraction.py new file mode 100644 index 000000000..fc02fc293 --- /dev/null +++ b/doc/examples_sphinx-gallery/cluster_contraction.py @@ -0,0 +1,146 @@ +""" +.. _tutorials-cluster-graph: + +=========================== +Generating Cluster Graphs +=========================== + +This example shows how to find the communities in a graph, then contract each community into a single node using :class:`igraph.clustering.VertexClustering`. For this tutorial, we'll use the *Donald Knuth's Les Miserables Network*, which shows the coapperances of characters in the novel *Les Miserables*. +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# We begin by load the graph from file. The file containing this network can be +# downloaded `here `_. +g = ig.load("./lesmis/lesmis.gml") + +# %% +# Now that we have a graph in memory, we can generate communities using +# :meth:`igraph.Graph.community_edge_betweenness` to separate out vertices into +# clusters. (For a more focused tutorial on just visualising communities, check +# out :ref:`tutorials-visualize-communities`). +communities = g.community_edge_betweenness() + +# %% +# For plots, it is convenient to convert the communities into a VertexClustering: +communities = communities.as_clustering() + +# %% +# We can also easily print out who belongs to each community: +for i, community in enumerate(communities): + print(f"Community {i}:") + for v in community: + print(f"\t{g.vs[v]['label']}") + +# %% +# Finally we can proceed to plotting the graph. In order to make each community +# stand out, we set "community colors" using an igraph palette: +num_communities = len(communities) +palette1 = 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 + +# %% +# We can use a dirty hack to move the labels below the vertices ;-) +g.vs["label"] = ["\n\n" + label for label in g.vs["label"]] + +# %% +# Finally, we can plot the communities: +fig1, ax1 = plt.subplots() +ig.plot( + communities, + target=ax1, + mark_groups=True, + palette=palette1, + vertex_size=15, + edge_width=0.5, +) +fig1.set_size_inches(20, 20) + + +# %% +# Now let's try and contract the information down, using only a single vertex +# to represent each community. We start by defining x, y, and size attributes +# for each node in the original graph: +layout = g.layout_kamada_kawai() +g.vs["x"], g.vs["y"] = list(zip(*layout)) +g.vs["size"] = 15 +g.es["size"] = 15 + +# %% +# Then we can generate the cluster graph that compresses each community into a +# single, "composite" vertex using +# :meth:`igraph.VertexClustering.cluster_graph`: +cluster_graph = communities.cluster_graph( + combine_vertices={ + "x": "mean", + "y": "mean", + "color": "first", + "size": "sum", + }, + combine_edges={ + "size": "sum", + }, +) + +# %% +# .. note:: +# +# We took the mean of x and y values so that the nodes in the cluster +# graph are placed at the centroid of the original cluster. +# +# .. note:: +# +# ``mean``, ``first``, and ``sum`` are all built-in collapsing functions, +# along with ``prod``, ``median``, ``max``, ``min``, ``last``, ``random``. +# You can also define your own custom collapsing functions, which take in a +# list and return a single element representing the combined attribute +# value. For more details on igraph contraction, see +# :meth:`igraph.GraphBase.contract_vertices`. + + +# %% +# Finally, we can assign colors to the clusters and plot the cluster graph, +# including a legend to make things clear: +palette2 = ig.GradientPalette("gainsboro", "black") +g.es["color"] = [ + palette2.get(int(i)) + for i in ig.rescale(cluster_graph.es["size"], (0, 255), clamp=True) +] + +fig2, ax2 = plt.subplots() +ig.plot( + cluster_graph, + target=ax2, + palette=palette1, + # set a minimum size on vertex_size, otherwise vertices are too small + vertex_size=[max(20, size) for size in cluster_graph.vs["size"]], + edge_color=g.es["color"], + edge_width=0.8, +) + +# Add a legend +legend_handles = [] +for i in range(num_communities): + handle = ax2.scatter( + [], + [], + s=100, + facecolor=palette1.get(i), + edgecolor="k", + label=i, + ) + legend_handles.append(handle) + +ax2.legend( + handles=legend_handles, + title="Community:", + bbox_to_anchor=(0, 1.0), + bbox_transform=ax2.transAxes, +) + +fig2.set_size_inches(10, 10) diff --git a/doc/examples_sphinx-gallery/complement.py b/doc/examples_sphinx-gallery/complement.py new file mode 100644 index 000000000..b4f2f17a0 --- /dev/null +++ b/doc/examples_sphinx-gallery/complement.py @@ -0,0 +1,76 @@ +""" +.. _tutorials-complement: + +================ +Complement +================ + +This example shows how to generate the `complement graph `_ of a graph (sometimes known as the anti-graph) using :meth:`igraph.GraphBase.complementer`. +""" + +import igraph as ig +import matplotlib.pyplot as plt +import random + +# %% +# First, we generate a random graph +random.seed(0) +g1 = ig.Graph.Erdos_Renyi(n=10, p=0.5) + +# %% +# .. note:: +# We set the random seed to ensure the graph comes out exactly the same +# each time in the gallery. You don't need to do that if you're exploring +# really random graphs ;-) + +# %% +# Then we generate the complement graph: +g2 = g1.complementer(loops=False) + +# %% +# The union graph of the two is of course the full graph, i.e. a graph with +# edges connecting all vertices to all other vertices. Because we decided to +# ignore loops (aka self-edges) in the complementer, the full graph does not +# include loops either. +g_full = g1 | g2 + +# %% +# In case there was any doubt, the complement of the full graph is an +# empty graph, with the same vertices but no edges: +g_empty = g_full.complementer(loops=False) + +# %% +# To demonstrate these concepts more clearly, here's a layout of each of the +# four graphs we discussed (input, complement, union/full, complement of +# union/empty): +fig, axs = plt.subplots(2, 2) +ig.plot( + g1, + target=axs[0, 0], + layout="circle", + vertex_color="black", +) +axs[0, 0].set_title("Original graph") +ig.plot( + g2, + target=axs[0, 1], + layout="circle", + vertex_color="black", +) +axs[0, 1].set_title("Complement graph") + +ig.plot( + g_full, + target=axs[1, 0], + layout="circle", + vertex_color="black", +) +axs[1, 0].set_title("Union graph") +ig.plot( + g_empty, + target=axs[1, 1], + layout="circle", + vertex_color="black", +) +axs[1, 1].set_title("Complement of union graph") +plt.show() diff --git a/doc/examples_sphinx-gallery/configuration.py b/doc/examples_sphinx-gallery/configuration.py new file mode 100644 index 000000000..4e9c077eb --- /dev/null +++ b/doc/examples_sphinx-gallery/configuration.py @@ -0,0 +1,61 @@ +""" +.. _tutorials-configuration: + +====================== +Configuration Instance +====================== + +This example shows how to use igraph's :class:`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. +""" + +import igraph as ig +import matplotlib.pyplot as plt +import random + +# %% +# First we define the default plotting backend, layout, and color palette. +ig.config["plotting.backend"] = "matplotlib" +ig.config["plotting.layout"] = "fruchterman_reingold" +ig.config["plotting.palette"] = "rainbow" + +# %% +# The updated configuration affects only the current session. Optionally, it +# can be saved using ``ig.config.save()``. By default, this function writes the +# configuration to ``~/.igraphrc`` on Linux and Max OS X systems, and in +# ``%USERPROFILE%\.igraphrc`` on Windows systems. + +# %% +# The configuration only needs to be saved to `.igraphrc` once, and it will +# be automatically used in all future sessions. Whenever you use igraph and +# this file exists, igraph will read its content and use those options as +# defaults. For example, let's create and plot a new graph to demonstrate: +random.seed(1) +g = ig.Graph.Barabasi(n=100, m=1) + +# %% +# We now calculate a color value between 0-200 for all nodes, for instance by +# computing the vertex betweenness: +betweenness = g.betweenness() +colors = [int(i * 200 / max(betweenness)) for i in betweenness] + +# %% +# Finally, we can plot the graph. You will notice that even though we did not +# create a dedicated figure and axes, matplotlib is now used by default: +ig.plot(g, vertex_color=colors, vertex_size=15, edge_width=0.3) +plt.show() + +# %% +# The full list of config settings can be found at +# :class:`igraph.Configuration`. +# +# .. note:: +# +# You can have multiple config files: specify each location via +# ``ig.config.save("./path/to/config/file")``. To load a specific config, +# import igraph and then call ``ig.config.load("./path/to/config/file")`` +# +# +# .. note:: +# +# To use a consistent style between individual plots (e.g. vertex sizes, +# colors, layout etc.) check out :ref:`tutorials-visual-style`. diff --git a/doc/examples_sphinx-gallery/connected_components.py b/doc/examples_sphinx-gallery/connected_components.py new file mode 100644 index 000000000..33c341fee --- /dev/null +++ b/doc/examples_sphinx-gallery/connected_components.py @@ -0,0 +1,45 @@ +""" +.. _tutorials-connected-components: + +===================== +Connected Components +===================== + +This example demonstrates how to visualise the connected components in a graph using :meth:`igraph.GraphBase.connected_components`. +""" + +import igraph as ig +import matplotlib.pyplot as plt +import random + +# %% +# First, we generate a randomized geometric graph with random vertex sizes. The +# seed is set to the example is reproducible in our manual: you don't really +# need it to understand the concepts. +random.seed(0) +g = ig.Graph.GRG(50, 0.15) + +# %% +# Now we can cluster the graph into weakly connected components, i.e. subgraphs +# that have no edges connecting them to one another: +components = g.connected_components(mode="weak") + +# %% +# Finally, we can visualize the distinct connected components of the graph: +fig, ax = plt.subplots() +ig.plot( + components, + target=ax, + palette=ig.RainbowPalette(), + vertex_size=7, + vertex_color=list(map(int, ig.rescale(components.membership, (0, 200), clamp=True))), + edge_width=0.7 +) +plt.show() + +# %% +# .. note:: +# +# We use the integers from 0 to 200 instead of 0 to 255 in our vertex +# colors, since 255 in the :class:`igraph.drawing.colors.RainbowPalette` +# corresponds to looping back to red. This gives us nicely distinct hues. diff --git a/doc/examples_sphinx-gallery/delaunay-triangulation.py b/doc/examples_sphinx-gallery/delaunay-triangulation.py new file mode 100644 index 000000000..3a25eebe1 --- /dev/null +++ b/doc/examples_sphinx-gallery/delaunay-triangulation.py @@ -0,0 +1,99 @@ +""" +.. _tutorials-delaunay-triangulation: + +====================== +Delaunay Triangulation +====================== + +This example demonstrates how to calculate the `Delaunay triangulation `_ of an input graph. We start by generating a set of points on a 2D grid using random ``numpy`` arrays and a graph with those vertex coordinates and no edges. + +""" + +import numpy as np +from scipy.spatial import Delaunay +import igraph as ig +import matplotlib.pyplot as plt + + +# %% +# We start by generating a random graph in the 2D unit cube, fixing the random +# seed to ensure reproducibility. +np.random.seed(0) +x, y = np.random.rand(2, 30) +g = ig.Graph(30) +g.vs["x"] = x +g.vs["y"] = y + +# %% +# Because we already set the `x` and `y` vertex attributes, we can use +# :meth:`igraph.Graph.layout_auto` to wrap them into a :class:`igraph.Layout` +# object. +layout = g.layout_auto() + +# %% +# Now we can calculate the delaunay triangulation using `scipy`'s :class:`scipy:scipy.spatial.Delaunay` class: +delaunay = Delaunay(layout.coords) + +# %% +# Given the triangulation, we can add the edges into the original graph: +for tri in delaunay.simplices: + g.add_edges([ + (tri[0], tri[1]), + (tri[1], tri[2]), + (tri[0], tri[2]), + ]) + +# %% +# Because adjacent triangles share an edge/side, the graph now contains some +# edges more than once. It's useful to simplify the graph to get rid of those +# multiedges, keeping each side only once: +g.simplify() + +# %% +# Finally, plotting the graph gives a good idea of what the triangulation looks +# like: +fig, ax = plt.subplots() +ig.plot( + g, + layout=layout, + target=ax, + vertex_size=4, + vertex_color="lightblue", + edge_width=0.8 +) +plt.show() + +# %% +# Alternative plotting style +# -------------------------- +# We can use :doc:`matplotlib ` to plot the faces of the +# triangles instead of the edges. First, we create a hue gradient for the +# triangle faces: +palette = ig.GradientPalette("midnightblue", "lightblue", 100) + +# %% +# Then we can "plot" the triangles using +# :class:`matplotlib:matplotlib.patches.Polygon` and the graph using +# :func:`igraph.plot() `: +fig, ax = plt.subplots() +for tri in delaunay.simplices: + # get the points of the triangle + tri_points = [delaunay.points[tri[i]] for i in range(3)] + + # calculate the vertical center of the triangle + center = (tri_points[0][1] + tri_points[1][1] + tri_points[2][1]) / 3 + + # draw triangle onto axes + poly = plt.Polygon(tri_points, color=palette.get(int(center * 100))) + ax.add_patch(poly) + +ig.plot( + g, + layout=layout, + target=ax, + vertex_size=0, + edge_width=0.2, + edge_color="white", +) +ax.set(xlim=(0, 1), ylim=(0, 1)) +plt.show() diff --git a/doc/examples_sphinx-gallery/erdos_renyi.py b/doc/examples_sphinx-gallery/erdos_renyi.py new file mode 100644 index 000000000..aeb808992 --- /dev/null +++ b/doc/examples_sphinx-gallery/erdos_renyi.py @@ -0,0 +1,59 @@ +""" +.. _tutorials-random: + +================= +Erdős-Rényi Graph +================= + +This example demonstrates how to generate `Erdős–Rényi graphs `_ using :meth:`igraph.GraphBase.Erdos_Renyi`. There are two variants of graphs: + +- ``Erdos_Renyi(n, p)`` will generate a graph from the so-called :math:`G(n,p)` model where each edge between any two pair of nodes has an independent probability ``p`` of existing. +- ``Erdos_Renyi(n, m)`` will pick a graph uniformly at random out of all graphs with ``n`` nodes and ``m`` edges. This is referred to as the :math:`G(n,m)` model. + +We generate two graphs of each, so we can confirm that our graph generator is truly random. +""" + +import igraph as ig +import matplotlib.pyplot as plt +import random + +# %% +# First, we set a random seed for reproducibility +random.seed(0) + +# %% +# Then, we generate two :math:`G(n,p)` Erdős–Rényi graphs with identical parameters: +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) + +# %% +# For comparison, we also generate two :math:`G(n,m)` Erdős–Rényi graphs with a fixed 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) + +# %% +# We can print out summaries of each graph to verify their randomness +ig.summary(g1) +ig.summary(g2) +ig.summary(g3) +ig.summary(g4) + +# IGRAPH U--- 15 18 -- +# IGRAPH U--- 15 21 -- +# IGRAPH U--- 20 35 -- +# IGRAPH U--- 20 35 -- + +# %% +# Finally, we can plot the graphs to illustrate their structures and +# differences: +fig, axs = plt.subplots(2, 2) +# Probability +ig.plot(g1, target=axs[0, 0], layout="circle", vertex_color="lightblue") +ig.plot(g2, target=axs[0, 1], layout="circle", vertex_color="lightblue") +axs[0, 0].set_ylabel("Probability") +# N edges +ig.plot(g3, target=axs[1, 0], layout="circle", vertex_color="lightblue", vertex_size=15) +ig.plot(g4, target=axs[1, 1], layout="circle", vertex_color="lightblue", vertex_size=15) +axs[1, 0].set_ylabel("N. edges") +plt.show() diff --git a/doc/examples_sphinx-gallery/generate_dag.py b/doc/examples_sphinx-gallery/generate_dag.py new file mode 100644 index 000000000..1565a59ed --- /dev/null +++ b/doc/examples_sphinx-gallery/generate_dag.py @@ -0,0 +1,46 @@ +""" + +.. _tutorials-dag: + +====================== +Directed Acyclic Graph +====================== + +This example demonstrates how to create a random directed acyclic graph (DAG), which is useful in a number of contexts including for Git commit history. +""" + +import igraph as ig +import matplotlib.pyplot as plt +import random + + +# %% +# First, we set a random seed for reproducibility. +random.seed(0) + +# %% +# First, we generate a random undirected graph with a fixed number of edges, without loops. +g = ig.Graph.Erdos_Renyi(n=15, m=30, directed=False, loops=False) + +# %% +# Then we convert it to a DAG *in place*. This method samples DAGs with a given number of edges and vertices uniformly. +g.to_directed(mode="acyclic") + +# %% +# We can print out a summary of the DAG. +ig.summary(g) + + +# %% +# Finally, we can plot the graph using the Sugiyama layout from :meth:`igraph.Graph.layout_sugiyama`: +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + layout="sugiyama", + vertex_size=15, + vertex_color="grey", + edge_color="#222", + edge_width=1, +) +plt.show() diff --git a/doc/examples_sphinx-gallery/isomorphism.py b/doc/examples_sphinx-gallery/isomorphism.py new file mode 100644 index 000000000..57c6034f4 --- /dev/null +++ b/doc/examples_sphinx-gallery/isomorphism.py @@ -0,0 +1,86 @@ +""" +.. _tutorials-isomorphism: + +=========== +Isomorphism +=========== + +This example shows how to check for `isomorphism `_ between small graphs using :meth:`igraph.GraphBase.isomorphic`. +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# First we generate three different graphs: +g1 = ig.Graph([(0, 1), (0, 2), (0, 4), (1, 2), (1, 3), (2, 3), (2, 4), (3, 4)]) +g2 = ig.Graph([(4, 2), (4, 3), (4, 0), (2, 3), (2, 1), (3, 1), (3, 0), (1, 0)]) +g3 = ig.Graph([(4, 1), (4, 3), (4, 0), (2, 3), (2, 1), (3, 1), (3, 0), (1, 0)]) + +# %% +# To check whether they are isomorphic, we can use a simple method: +print("Are the graphs g1 and g2 isomorphic?") +print(g1.isomorphic(g2)) +print("Are the graphs g1 and g3 isomorphic?") +print(g1.isomorphic(g3)) +print("Are the graphs g2 and g3 isomorphic?") +print(g2.isomorphic(g3)) + +# Output: +# Are the graphs g1 and g2 isomorphic? +# True +# Are the graphs g1 and g3 isomorphic? +# False +# Are the graphs g2 and g3 isomorphic? +# False + +# %% +# .. note:: +# `Graph isomorphism `_ is an equivalence +# relationship, i.e. if `g1 ~ g2` and `g2 ~ g3`, then automatically `g1 ~ g3`. Therefore, +# we could have skipped the last check. + +# %% +# We can plot the graphs to get an idea about the problem: +visual_style = { + "vertex_color": "lightblue", + "vertex_label": [0, 1, 2, 3, 4], + "vertex_size": 25, +} + +fig, axs = plt.subplots(1, 3) +ig.plot( + g1, + layout=g1.layout("circle"), + target=axs[0], + **visual_style, +) +ig.plot( + g2, + layout=g1.layout("circle"), + target=axs[1], + **visual_style, +) +ig.plot( + g3, + layout=g1.layout("circle"), + target=axs[2], + **visual_style, +) +fig.text( + 0.38, + 0.5, + "$\\simeq$" if g1.isomorphic(g2) else "$\\neq$", + fontsize=15, + ha="center", + va="center", +) +fig.text( + 0.65, + 0.5, + "$\\simeq$" if g2.isomorphic(g3) else "$\\neq$", + fontsize=15, + ha="center", + va="center", +) +plt.show() diff --git a/doc/examples_sphinx-gallery/lesmis/lesmis.gml b/doc/examples_sphinx-gallery/lesmis/lesmis.gml new file mode 100644 index 000000000..3c0751191 --- /dev/null +++ b/doc/examples_sphinx-gallery/lesmis/lesmis.gml @@ -0,0 +1,1913 @@ +Creator "Mark Newman on Fri Jul 21 12:44:53 2006" +graph +[ + node + [ + id 0 + label "Myriel" + ] + node + [ + id 1 + label "Napoleon" + ] + node + [ + id 2 + label "MlleBaptistine" + ] + node + [ + id 3 + label "MmeMagloire" + ] + node + [ + id 4 + label "CountessDeLo" + ] + node + [ + id 5 + label "Geborand" + ] + node + [ + id 6 + label "Champtercier" + ] + node + [ + id 7 + label "Cravatte" + ] + node + [ + id 8 + label "Count" + ] + node + [ + id 9 + label "OldMan" + ] + node + [ + id 10 + label "Labarre" + ] + node + [ + id 11 + label "Valjean" + ] + node + [ + id 12 + label "Marguerite" + ] + node + [ + id 13 + label "MmeDeR" + ] + node + [ + id 14 + label "Isabeau" + ] + node + [ + id 15 + label "Gervais" + ] + node + [ + id 16 + label "Tholomyes" + ] + node + [ + id 17 + label "Listolier" + ] + node + [ + id 18 + label "Fameuil" + ] + node + [ + id 19 + label "Blacheville" + ] + node + [ + id 20 + label "Favourite" + ] + node + [ + id 21 + label "Dahlia" + ] + node + [ + id 22 + label "Zephine" + ] + node + [ + id 23 + label "Fantine" + ] + node + [ + id 24 + label "MmeThenardier" + ] + node + [ + id 25 + label "Thenardier" + ] + node + [ + id 26 + label "Cosette" + ] + node + [ + id 27 + label "Javert" + ] + node + [ + id 28 + label "Fauchelevent" + ] + node + [ + id 29 + label "Bamatabois" + ] + node + [ + id 30 + label "Perpetue" + ] + node + [ + id 31 + label "Simplice" + ] + node + [ + id 32 + label "Scaufflaire" + ] + node + [ + id 33 + label "Woman1" + ] + node + [ + id 34 + label "Judge" + ] + node + [ + id 35 + label "Champmathieu" + ] + node + [ + id 36 + label "Brevet" + ] + node + [ + id 37 + label "Chenildieu" + ] + node + [ + id 38 + label "Cochepaille" + ] + node + [ + id 39 + label "Pontmercy" + ] + node + [ + id 40 + label "Boulatruelle" + ] + node + [ + id 41 + label "Eponine" + ] + node + [ + id 42 + label "Anzelma" + ] + node + [ + id 43 + label "Woman2" + ] + node + [ + id 44 + label "MotherInnocent" + ] + node + [ + id 45 + label "Gribier" + ] + node + [ + id 46 + label "Jondrette" + ] + node + [ + id 47 + label "MmeBurgon" + ] + node + [ + id 48 + label "Gavroche" + ] + node + [ + id 49 + label "Gillenormand" + ] + node + [ + id 50 + label "Magnon" + ] + node + [ + id 51 + label "MlleGillenormand" + ] + node + [ + id 52 + label "MmePontmercy" + ] + node + [ + id 53 + label "MlleVaubois" + ] + node + [ + id 54 + label "LtGillenormand" + ] + node + [ + id 55 + label "Marius" + ] + node + [ + id 56 + label "BaronessT" + ] + node + [ + id 57 + label "Mabeuf" + ] + node + [ + id 58 + label "Enjolras" + ] + node + [ + id 59 + label "Combeferre" + ] + node + [ + id 60 + label "Prouvaire" + ] + node + [ + id 61 + label "Feuilly" + ] + node + [ + id 62 + label "Courfeyrac" + ] + node + [ + id 63 + label "Bahorel" + ] + node + [ + id 64 + label "Bossuet" + ] + node + [ + id 65 + label "Joly" + ] + node + [ + id 66 + label "Grantaire" + ] + node + [ + id 67 + label "MotherPlutarch" + ] + node + [ + id 68 + label "Gueulemer" + ] + node + [ + id 69 + label "Babet" + ] + node + [ + id 70 + label "Claquesous" + ] + node + [ + id 71 + label "Montparnasse" + ] + node + [ + id 72 + label "Toussaint" + ] + node + [ + id 73 + label "Child1" + ] + node + [ + id 74 + label "Child2" + ] + node + [ + id 75 + label "Brujon" + ] + node + [ + id 76 + label "MmeHucheloup" + ] + edge + [ + source 1 + target 0 + value 1 + ] + edge + [ + source 2 + target 0 + value 8 + ] + edge + [ + source 3 + target 0 + value 10 + ] + edge + [ + source 3 + target 2 + value 6 + ] + edge + [ + source 4 + target 0 + value 1 + ] + edge + [ + source 5 + target 0 + value 1 + ] + edge + [ + source 6 + target 0 + value 1 + ] + edge + [ + source 7 + target 0 + value 1 + ] + edge + [ + source 8 + target 0 + value 2 + ] + edge + [ + source 9 + target 0 + value 1 + ] + edge + [ + source 11 + target 10 + value 1 + ] + edge + [ + source 11 + target 3 + value 3 + ] + edge + [ + source 11 + target 2 + value 3 + ] + edge + [ + source 11 + target 0 + value 5 + ] + edge + [ + source 12 + target 11 + value 1 + ] + edge + [ + source 13 + target 11 + value 1 + ] + edge + [ + source 14 + target 11 + value 1 + ] + edge + [ + source 15 + target 11 + value 1 + ] + edge + [ + source 17 + target 16 + value 4 + ] + edge + [ + source 18 + target 16 + value 4 + ] + edge + [ + source 18 + target 17 + value 4 + ] + edge + [ + source 19 + target 16 + value 4 + ] + edge + [ + source 19 + target 17 + value 4 + ] + edge + [ + source 19 + target 18 + value 4 + ] + edge + [ + source 20 + target 16 + value 3 + ] + edge + [ + source 20 + target 17 + value 3 + ] + edge + [ + source 20 + target 18 + value 3 + ] + edge + [ + source 20 + target 19 + value 4 + ] + edge + [ + source 21 + target 16 + value 3 + ] + edge + [ + source 21 + target 17 + value 3 + ] + edge + [ + source 21 + target 18 + value 3 + ] + edge + [ + source 21 + target 19 + value 3 + ] + edge + [ + source 21 + target 20 + value 5 + ] + edge + [ + source 22 + target 16 + value 3 + ] + edge + [ + source 22 + target 17 + value 3 + ] + edge + [ + source 22 + target 18 + value 3 + ] + edge + [ + source 22 + target 19 + value 3 + ] + edge + [ + source 22 + target 20 + value 4 + ] + edge + [ + source 22 + target 21 + value 4 + ] + edge + [ + source 23 + target 16 + value 3 + ] + edge + [ + source 23 + target 17 + value 3 + ] + edge + [ + source 23 + target 18 + value 3 + ] + edge + [ + source 23 + target 19 + value 3 + ] + edge + [ + source 23 + target 20 + value 4 + ] + edge + [ + source 23 + target 21 + value 4 + ] + edge + [ + source 23 + target 22 + value 4 + ] + edge + [ + source 23 + target 12 + value 2 + ] + edge + [ + source 23 + target 11 + value 9 + ] + edge + [ + source 24 + target 23 + value 2 + ] + edge + [ + source 24 + target 11 + value 7 + ] + edge + [ + source 25 + target 24 + value 13 + ] + edge + [ + source 25 + target 23 + value 1 + ] + edge + [ + source 25 + target 11 + value 12 + ] + edge + [ + source 26 + target 24 + value 4 + ] + edge + [ + source 26 + target 11 + value 31 + ] + edge + [ + source 26 + target 16 + value 1 + ] + edge + [ + source 26 + target 25 + value 1 + ] + edge + [ + source 27 + target 11 + value 17 + ] + edge + [ + source 27 + target 23 + value 5 + ] + edge + [ + source 27 + target 25 + value 5 + ] + edge + [ + source 27 + target 24 + value 1 + ] + edge + [ + source 27 + target 26 + value 1 + ] + edge + [ + source 28 + target 11 + value 8 + ] + edge + [ + source 28 + target 27 + value 1 + ] + edge + [ + source 29 + target 23 + value 1 + ] + edge + [ + source 29 + target 27 + value 1 + ] + edge + [ + source 29 + target 11 + value 2 + ] + edge + [ + source 30 + target 23 + value 1 + ] + edge + [ + source 31 + target 30 + value 2 + ] + edge + [ + source 31 + target 11 + value 3 + ] + edge + [ + source 31 + target 23 + value 2 + ] + edge + [ + source 31 + target 27 + value 1 + ] + edge + [ + source 32 + target 11 + value 1 + ] + edge + [ + source 33 + target 11 + value 2 + ] + edge + [ + source 33 + target 27 + value 1 + ] + edge + [ + source 34 + target 11 + value 3 + ] + edge + [ + source 34 + target 29 + value 2 + ] + edge + [ + source 35 + target 11 + value 3 + ] + edge + [ + source 35 + target 34 + value 3 + ] + edge + [ + source 35 + target 29 + value 2 + ] + edge + [ + source 36 + target 34 + value 2 + ] + edge + [ + source 36 + target 35 + value 2 + ] + edge + [ + source 36 + target 11 + value 2 + ] + edge + [ + source 36 + target 29 + value 1 + ] + edge + [ + source 37 + target 34 + value 2 + ] + edge + [ + source 37 + target 35 + value 2 + ] + edge + [ + source 37 + target 36 + value 2 + ] + edge + [ + source 37 + target 11 + value 2 + ] + edge + [ + source 37 + target 29 + value 1 + ] + edge + [ + source 38 + target 34 + value 2 + ] + edge + [ + source 38 + target 35 + value 2 + ] + edge + [ + source 38 + target 36 + value 2 + ] + edge + [ + source 38 + target 37 + value 2 + ] + edge + [ + source 38 + target 11 + value 2 + ] + edge + [ + source 38 + target 29 + value 1 + ] + edge + [ + source 39 + target 25 + value 1 + ] + edge + [ + source 40 + target 25 + value 1 + ] + edge + [ + source 41 + target 24 + value 2 + ] + edge + [ + source 41 + target 25 + value 3 + ] + edge + [ + source 42 + target 41 + value 2 + ] + edge + [ + source 42 + target 25 + value 2 + ] + edge + [ + source 42 + target 24 + value 1 + ] + edge + [ + source 43 + target 11 + value 3 + ] + edge + [ + source 43 + target 26 + value 1 + ] + edge + [ + source 43 + target 27 + value 1 + ] + edge + [ + source 44 + target 28 + value 3 + ] + edge + [ + source 44 + target 11 + value 1 + ] + edge + [ + source 45 + target 28 + value 2 + ] + edge + [ + source 47 + target 46 + value 1 + ] + edge + [ + source 48 + target 47 + value 2 + ] + edge + [ + source 48 + target 25 + value 1 + ] + edge + [ + source 48 + target 27 + value 1 + ] + edge + [ + source 48 + target 11 + value 1 + ] + edge + [ + source 49 + target 26 + value 3 + ] + edge + [ + source 49 + target 11 + value 2 + ] + edge + [ + source 50 + target 49 + value 1 + ] + edge + [ + source 50 + target 24 + value 1 + ] + edge + [ + source 51 + target 49 + value 9 + ] + edge + [ + source 51 + target 26 + value 2 + ] + edge + [ + source 51 + target 11 + value 2 + ] + edge + [ + source 52 + target 51 + value 1 + ] + edge + [ + source 52 + target 39 + value 1 + ] + edge + [ + source 53 + target 51 + value 1 + ] + edge + [ + source 54 + target 51 + value 2 + ] + edge + [ + source 54 + target 49 + value 1 + ] + edge + [ + source 54 + target 26 + value 1 + ] + edge + [ + source 55 + target 51 + value 6 + ] + edge + [ + source 55 + target 49 + value 12 + ] + edge + [ + source 55 + target 39 + value 1 + ] + edge + [ + source 55 + target 54 + value 1 + ] + edge + [ + source 55 + target 26 + value 21 + ] + edge + [ + source 55 + target 11 + value 19 + ] + edge + [ + source 55 + target 16 + value 1 + ] + edge + [ + source 55 + target 25 + value 2 + ] + edge + [ + source 55 + target 41 + value 5 + ] + edge + [ + source 55 + target 48 + value 4 + ] + edge + [ + source 56 + target 49 + value 1 + ] + edge + [ + source 56 + target 55 + value 1 + ] + edge + [ + source 57 + target 55 + value 1 + ] + edge + [ + source 57 + target 41 + value 1 + ] + edge + [ + source 57 + target 48 + value 1 + ] + edge + [ + source 58 + target 55 + value 7 + ] + edge + [ + source 58 + target 48 + value 7 + ] + edge + [ + source 58 + target 27 + value 6 + ] + edge + [ + source 58 + target 57 + value 1 + ] + edge + [ + source 58 + target 11 + value 4 + ] + edge + [ + source 59 + target 58 + value 15 + ] + edge + [ + source 59 + target 55 + value 5 + ] + edge + [ + source 59 + target 48 + value 6 + ] + edge + [ + source 59 + target 57 + value 2 + ] + edge + [ + source 60 + target 48 + value 1 + ] + edge + [ + source 60 + target 58 + value 4 + ] + edge + [ + source 60 + target 59 + value 2 + ] + edge + [ + source 61 + target 48 + value 2 + ] + edge + [ + source 61 + target 58 + value 6 + ] + edge + [ + source 61 + target 60 + value 2 + ] + edge + [ + source 61 + target 59 + value 5 + ] + edge + [ + source 61 + target 57 + value 1 + ] + edge + [ + source 61 + target 55 + value 1 + ] + edge + [ + source 62 + target 55 + value 9 + ] + edge + [ + source 62 + target 58 + value 17 + ] + edge + [ + source 62 + target 59 + value 13 + ] + edge + [ + source 62 + target 48 + value 7 + ] + edge + [ + source 62 + target 57 + value 2 + ] + edge + [ + source 62 + target 41 + value 1 + ] + edge + [ + source 62 + target 61 + value 6 + ] + edge + [ + source 62 + target 60 + value 3 + ] + edge + [ + source 63 + target 59 + value 5 + ] + edge + [ + source 63 + target 48 + value 5 + ] + edge + [ + source 63 + target 62 + value 6 + ] + edge + [ + source 63 + target 57 + value 2 + ] + edge + [ + source 63 + target 58 + value 4 + ] + edge + [ + source 63 + target 61 + value 3 + ] + edge + [ + source 63 + target 60 + value 2 + ] + edge + [ + source 63 + target 55 + value 1 + ] + edge + [ + source 64 + target 55 + value 5 + ] + edge + [ + source 64 + target 62 + value 12 + ] + edge + [ + source 64 + target 48 + value 5 + ] + edge + [ + source 64 + target 63 + value 4 + ] + edge + [ + source 64 + target 58 + value 10 + ] + edge + [ + source 64 + target 61 + value 6 + ] + edge + [ + source 64 + target 60 + value 2 + ] + edge + [ + source 64 + target 59 + value 9 + ] + edge + [ + source 64 + target 57 + value 1 + ] + edge + [ + source 64 + target 11 + value 1 + ] + edge + [ + source 65 + target 63 + value 5 + ] + edge + [ + source 65 + target 64 + value 7 + ] + edge + [ + source 65 + target 48 + value 3 + ] + edge + [ + source 65 + target 62 + value 5 + ] + edge + [ + source 65 + target 58 + value 5 + ] + edge + [ + source 65 + target 61 + value 5 + ] + edge + [ + source 65 + target 60 + value 2 + ] + edge + [ + source 65 + target 59 + value 5 + ] + edge + [ + source 65 + target 57 + value 1 + ] + edge + [ + source 65 + target 55 + value 2 + ] + edge + [ + source 66 + target 64 + value 3 + ] + edge + [ + source 66 + target 58 + value 3 + ] + edge + [ + source 66 + target 59 + value 1 + ] + edge + [ + source 66 + target 62 + value 2 + ] + edge + [ + source 66 + target 65 + value 2 + ] + edge + [ + source 66 + target 48 + value 1 + ] + edge + [ + source 66 + target 63 + value 1 + ] + edge + [ + source 66 + target 61 + value 1 + ] + edge + [ + source 66 + target 60 + value 1 + ] + edge + [ + source 67 + target 57 + value 3 + ] + edge + [ + source 68 + target 25 + value 5 + ] + edge + [ + source 68 + target 11 + value 1 + ] + edge + [ + source 68 + target 24 + value 1 + ] + edge + [ + source 68 + target 27 + value 1 + ] + edge + [ + source 68 + target 48 + value 1 + ] + edge + [ + source 68 + target 41 + value 1 + ] + edge + [ + source 69 + target 25 + value 6 + ] + edge + [ + source 69 + target 68 + value 6 + ] + edge + [ + source 69 + target 11 + value 1 + ] + edge + [ + source 69 + target 24 + value 1 + ] + edge + [ + source 69 + target 27 + value 2 + ] + edge + [ + source 69 + target 48 + value 1 + ] + edge + [ + source 69 + target 41 + value 1 + ] + edge + [ + source 70 + target 25 + value 4 + ] + edge + [ + source 70 + target 69 + value 4 + ] + edge + [ + source 70 + target 68 + value 4 + ] + edge + [ + source 70 + target 11 + value 1 + ] + edge + [ + source 70 + target 24 + value 1 + ] + edge + [ + source 70 + target 27 + value 1 + ] + edge + [ + source 70 + target 41 + value 1 + ] + edge + [ + source 70 + target 58 + value 1 + ] + edge + [ + source 71 + target 27 + value 1 + ] + edge + [ + source 71 + target 69 + value 2 + ] + edge + [ + source 71 + target 68 + value 2 + ] + edge + [ + source 71 + target 70 + value 2 + ] + edge + [ + source 71 + target 11 + value 1 + ] + edge + [ + source 71 + target 48 + value 1 + ] + edge + [ + source 71 + target 41 + value 1 + ] + edge + [ + source 71 + target 25 + value 1 + ] + edge + [ + source 72 + target 26 + value 2 + ] + edge + [ + source 72 + target 27 + value 1 + ] + edge + [ + source 72 + target 11 + value 1 + ] + edge + [ + source 73 + target 48 + value 2 + ] + edge + [ + source 74 + target 48 + value 2 + ] + edge + [ + source 74 + target 73 + value 3 + ] + edge + [ + source 75 + target 69 + value 3 + ] + edge + [ + source 75 + target 68 + value 3 + ] + edge + [ + source 75 + target 25 + value 3 + ] + edge + [ + source 75 + target 48 + value 1 + ] + edge + [ + source 75 + target 41 + value 1 + ] + edge + [ + source 75 + target 70 + value 1 + ] + edge + [ + source 75 + target 71 + value 1 + ] + edge + [ + source 76 + target 64 + value 1 + ] + edge + [ + source 76 + target 65 + value 1 + ] + edge + [ + source 76 + target 66 + value 1 + ] + edge + [ + source 76 + target 63 + value 1 + ] + edge + [ + source 76 + target 62 + value 1 + ] + edge + [ + source 76 + target 48 + value 1 + ] + edge + [ + source 76 + target 58 + value 1 + ] +] diff --git a/doc/examples_sphinx-gallery/lesmis/lesmis.txt b/doc/examples_sphinx-gallery/lesmis/lesmis.txt new file mode 100644 index 000000000..8772c84b0 --- /dev/null +++ b/doc/examples_sphinx-gallery/lesmis/lesmis.txt @@ -0,0 +1,7 @@ +The file lesmis.gml contains the weighted network of coappearances of +characters in Victor Hugo's novel "Les Miserables". Nodes represent +characters as indicated by the labels and edges connect any pair of +characters that appear in the same chapter of the book. The values on the +edges are the number of such coappearances. The data on coappearances were +taken from D. E. Knuth, The Stanford GraphBase: A Platform for +Combinatorial Computing, Addison-Wesley, Reading, MA (1993). diff --git a/doc/examples_sphinx-gallery/maxflow.py b/doc/examples_sphinx-gallery/maxflow.py new file mode 100644 index 000000000..eb76684a4 --- /dev/null +++ b/doc/examples_sphinx-gallery/maxflow.py @@ -0,0 +1,41 @@ +""" +.. _tutorials-maxflow: + +============ +Maximum Flow +============ + +This example shows how to construct a max flow on a directed graph with edge capacities using :meth:`igraph.Graph.maxflow`. + +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# First, we generate a graph and assign a "capacity" to each edge: +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] + +# %% +# To find the max flow, we can simply run: +flow = g.maxflow(3, 0, capacity=g.es["capacity"]) + +print("Max flow:", flow.value) +print("Edge assignments:", flow.flow) + +# Output: +# Max flow: 6.0 +# Edge assignments [1.0, 5.0, 1.0, 2.0, 3.0, 3.0, 3.0] + +# %% +# Finally, we can plot the directed graph to look at the situation: +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + layout="circle", + vertex_label=range(g.vcount()), + vertex_color="lightblue", +) +plt.show() diff --git a/doc/examples_sphinx-gallery/minimum_spanning_trees.py b/doc/examples_sphinx-gallery/minimum_spanning_trees.py new file mode 100644 index 000000000..9177e976a --- /dev/null +++ b/doc/examples_sphinx-gallery/minimum_spanning_trees.py @@ -0,0 +1,53 @@ +""" +.. _tutorials-minimum-spanning-trees: + +====================== +Minimum Spanning Trees +====================== + +This example shows how to generate a `minimum spanning tree `_ from an input graph using :meth:`igraph.Graph.spanning_tree`. If you only need a regular spanning tree, check out :ref:`tutorials-spanning-trees`. + +""" + +import random +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# We start by generating a grid graph with random integer weights between 1 and +# 20: +random.seed(0) +g = ig.Graph.Lattice([5, 5], circular=False) +g.es["weight"] = [random.randint(1, 20) for _ in g.es] + +# %% +# We can then compute a minimum spanning tree using +# :meth:`igraph.Graph.spanning_tree`, making sure to pass in the randomly +# generated weights. +mst_edges = g.spanning_tree(weights=g.es["weight"], return_tree=False) + +# %% +# We can print out the minimum edge weight sum +print("Minimum edge weight sum:", sum(g.es[mst_edges]["weight"])) + +# Minimum edge weight sum: 136 + +# %% +# Finally, we can plot the graph, highlighting the edges that are part of the +# minimum spanning tree. +g.es["color"] = "lightgray" +g.es[mst_edges]["color"] = "midnightblue" +g.es["width"] = 1.0 +g.es[mst_edges]["width"] = 3.0 + +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + layout="grid", + vertex_color="lightblue", + edge_width=g.es["width"], + edge_label=g.es["weight"], + edge_background="white", +) +plt.show() diff --git a/doc/examples_sphinx-gallery/online_user_actions.py b/doc/examples_sphinx-gallery/online_user_actions.py new file mode 100644 index 000000000..47cc00ff5 --- /dev/null +++ b/doc/examples_sphinx-gallery/online_user_actions.py @@ -0,0 +1,105 @@ +""" +.. _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. +""" + +import igraph as ig +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +# %% +# Let's start by preparing some toy data representing online users. Each row +# indicates a certain action taken by a user (e.g. click on a button within a +# website). Actual user data usually come with time stamp, but that's not +# essential for this example. +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"], +) + +# %% +# The goal of this example is 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: +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: +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: +g = ig.Graph.Weighted_Adjacency(adjacency_matrix, mode="plus") + +# %% +# We can take a look at the graph via plotting functions. We can first make a +# layout: +layout = g.layout("circle") + +# %% +# Then we can prepare vertex sizes based on their closeness to other vertices +vertex_size = g.closeness() +vertex_size = [10 * v**2 if not np.isnan(v) else 10 for v in vertex_size] + +# %% +# Finally, we can plot the graph: +fig, ax = plt.subplots() +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() + +# %% +# Loops indicate "self-collaborations", which are not very meaningful. To +# filter out loops without losing the edge weights, we can use: +g = g.simplify(combine_edges="first") + +fig, ax = plt.subplots() +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/examples_sphinx-gallery/personalized_pagerank.py b/doc/examples_sphinx-gallery/personalized_pagerank.py new file mode 100644 index 000000000..f7e2f9e2b --- /dev/null +++ b/doc/examples_sphinx-gallery/personalized_pagerank.py @@ -0,0 +1,98 @@ +""" +.. _tutorials-personalized_pagerank: + +=============================== +Personalized PageRank on a grid +=============================== + +This example demonstrates how to calculate and visualize personalized PageRank on a grid. We use the :meth:`igraph.Graph.personalized_pagerank` method, and demonstrate the effects on a grid graph. +""" + +# %% +# .. note:: +# +# The PageRank score of a vertex reflects the probability that a random walker will be at that vertex over the long run. At each step the walker has a 1 - damping chance to restart the walk and pick a starting vertex according to the probabilities defined in the reset vector. + +import igraph as ig +import matplotlib.cm as cm +import matplotlib.pyplot as plt +import numpy as np + +# %% +# We define a function that plots the graph on a Matplotlib axis, along with +# its personalized PageRank values. The function also generates a +# color bar on the side to see how the values change. +# We use `Matplotlib's Normalize class `_ +# to set the colors and ensure that our color bar range is correct. + + +def plot_pagerank(graph: ig.Graph, p_pagerank: list[float]): + """Plots personalized PageRank values on a grid graph with a colorbar. + + Parameters + ---------- + graph : ig.Graph + graph to plot + p_pagerank : list[float] + calculated personalized PageRank values + """ + # Create the axis for matplotlib + _, ax = plt.subplots(figsize=(8, 8)) + + # Create a matplotlib colormap + # coolwarm goes from blue (lowest value) to red (highest value) + cmap = cm.coolwarm + + # Normalize the PageRank values for colormap + normalized_pagerank = ig.rescale(p_pagerank) + + graph.vs["color"] = [cmap(pr) for pr in normalized_pagerank] + graph.vs["size"] = ig.rescale(p_pagerank, (20, 40)) + graph.es["color"] = "gray" + graph.es["width"] = 1.5 + + # Plot the graph + ig.plot(graph, target=ax, layout=graph.layout_grid()) + + # Add a colorbar + sm = cm.ScalarMappable( + norm=plt.Normalize(min(p_pagerank), max(p_pagerank)), cmap=cmap + ) + plt.colorbar(sm, ax=ax, label="Personalized PageRank") + + plt.title("Graph with Personalized PageRank") + plt.axis("equal") + plt.show() + + +# %% +# First, we generate a graph, e.g. a Lattice Graph, which basically is a ``dim x dim`` grid: +dim = 5 +grid_size = (dim, dim) # dim rows, dim columns +g = ig.Graph.Lattice(dim=grid_size, circular=False) + +# %% +# Then we initialize the ``reset_vector`` (it's length should be equal to the number of vertices in the graph): +reset_vector = np.zeros(g.vcount()) + +# %% +# Then we set the nodes to prioritize, for example nodes with indices ``0`` and ``18``: +reset_vector[0] = 1 +reset_vector[18] = 0.65 + +# %% +# Then we calculate the personalized PageRank: +personalized_page_rank = g.personalized_pagerank(damping=0.85, reset=reset_vector) + +# %% +# Finally, we plot the graph with the personalized PageRank values: +plot_pagerank(g, personalized_page_rank) + + +# %% +# Alternatively, we can play around with the ``damping`` parameter: +personalized_page_rank = g.personalized_pagerank(damping=0.45, reset=reset_vector) + +# %% +# Here we can see the same plot with the new damping parameter: +plot_pagerank(g, personalized_page_rank) diff --git a/doc/examples_sphinx-gallery/plot_iplotx.py b/doc/examples_sphinx-gallery/plot_iplotx.py new file mode 100644 index 000000000..5c3c12c43 --- /dev/null +++ b/doc/examples_sphinx-gallery/plot_iplotx.py @@ -0,0 +1,63 @@ +""" +.. _tutorials-iplotx: + +============================== +Visualising graphs with iplotx +============================== +``iplotx`` (https://iplotx.readthedocs.io) is a library for visualisation of graphs/networks +with direct compatibility with both igraph and NetworkX. It uses ``matplotlib`` behind the +scenes so the results are compatible with the current igraph matplotlib backend and many +additional chart types (e.g. bar charts, annotations). + +Compared to the standard visualisations shipped with igraph, ``iplotx`` offers: + +- More styling options +- More consistent behaviour across DPI resolutions and backends +- More consistent matplotlib artists for plot editing and animation + +""" + +import igraph as ig +import iplotx as ipx + +# Construct a graph with 5 vertices +n_vertices = 5 +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 using iplotx +ipx.network( + g, + layout="circle", # print nodes in a circular layout + vertex_marker="s", + vertex_size=45, + vertex_linewidth=2, + vertex_facecolor=[ + "lightblue" if gender == "M" else "deeppink" for gender in g.vs["gender"] + ], + vertex_label_color=[ + "black" if gender == "M" else "white" for gender in g.vs["gender"] + ], + vertex_edgecolor="black", + vertex_labels=[name.replace(" ", "\n") for name in g.vs["name"]], + edge_linewidth=[2 if married else 1 for married in g.es["married"]], + edge_color=["#7142cf" if married else "#AAA" for married in g.es["married"]], + edge_padding=3, + aspect=1.0, +) diff --git a/doc/examples_sphinx-gallery/quickstart.py b/doc/examples_sphinx-gallery/quickstart.py new file mode 100644 index 000000000..0735768e5 --- /dev/null +++ b/doc/examples_sphinx-gallery/quickstart.py @@ -0,0 +1,71 @@ +""" +.. _tutorials-quickstart: + +=========== +Quick Start +=========== +For the eager folks out there, this intro will give you a quick overview of the following operations: + +- 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 + +To find out more features that igraph has to offer, check out the :ref:`gallery`! + +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# Construct a graph with 5 vertices +n_vertices = 5 +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=30, + 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=["#7142cf" if married else "#AAA" 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/examples_sphinx-gallery/ring_animation.py b/doc/examples_sphinx-gallery/ring_animation.py new file mode 100644 index 000000000..33bd6a109 --- /dev/null +++ b/doc/examples_sphinx-gallery/ring_animation.py @@ -0,0 +1,92 @@ +""" +.. _tutorials-ring-animation: + +==================== +Ring Graph Animation +==================== + +This example demonstrates how to use :doc:`matplotlib:api/animation_api` in +order to animate a ring graph sequentially being revealed. + +""" + +import igraph as ig +import matplotlib.pyplot as plt +import matplotlib.animation as animation + +# sphinx_gallery_thumbnail_path = '_static/gallery_thumbnails/ring_animation.gif' + +# %% +# Create a ring graph, which we will then animate +g = ig.Graph.Ring(10, directed=True) + +# %% +# Compute a 2D ring layout that looks like an actual ring +layout = g.layout_circle() + + +# %% +# Prepare an update function. This "callback" function will be run at every +# frame and takes as a single argument the frame number. For simplicity, at +# each frame we compute a subgraph with only a fraction of the vertices and +# edges. As time passes, the graph becomes more and more complete until the +# whole ring is closed. +# +# .. note:: +# The beginning and end of the animation are a little tricky because only +# a vertex or edge is added, not both. Don't worry if you cannot understand +# all details immediately. +def _update_graph(frame): + # 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) + + if frame < 10: + # Plot subgraph + gd = g.subgraph(range(frame)) + elif frame == 10: + # In the second-to-last frame, plot all vertices but skip the last + # edge, which will only be shown in the last frame + gd = g.copy() + gd.delete_edges(9) + else: + # Last frame + gd = g + + ig.plot(gd, target=ax, layout=layout[:frame], vertex_color="yellow") + + # Capture handles for blitting + if frame == 0: + nhandles = 0 + elif frame == 1: + nhandles = 1 + elif frame < 11: + # vertex, 2 for each edge + nhandles = 3 * frame + else: + # The final edge closing the circle + nhandles = 3 * (frame - 1) + 2 + + handles = ax.get_children()[:nhandles] + return handles + + +# %% +# Run the animation +fig, ax = plt.subplots() +ani = animation.FuncAnimation(fig, _update_graph, 12, interval=500, blit=True) +plt.ion() +plt.show() + +# %% +# .. note:: +# +# We use *igraph*'s :meth:`Graph.subgraph()` (see +# :meth:`igraph.GraphBase.induced_subgraph`) in order to obtain a section of +# the ring graph at a time for each frame. While sufficient for an easy +# example, this approach is not very efficient. Thinking of more efficient +# approaches, e.g. vertices with zero radius, is a useful exercise to learn +# the combination of igraph and matplotlib. diff --git a/doc/examples_sphinx-gallery/shortest_path_visualisation.py b/doc/examples_sphinx-gallery/shortest_path_visualisation.py new file mode 100644 index 000000000..fedad160a --- /dev/null +++ b/doc/examples_sphinx-gallery/shortest_path_visualisation.py @@ -0,0 +1,77 @@ +""" +.. _tutorials-shortest-paths: + +============== +Shortest Paths +============== + +This example demonstrates how to find the shortest distance between two vertices +of a weighted or an unweighted graph. +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# To find the shortest path or distance between two nodes, we can use :meth:`igraph.GraphBase.get_shortest_paths`. If we're only interested in counting the unweighted distance, then we can do the following: +g = ig.Graph(6, [(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)]) +results = g.get_shortest_paths(1, to=4, output="vpath") + +# results = [[1, 0, 2, 4]] + +# %% +# We can print the result of the computation: +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!") + +# %% +# If the edges have weights, things are a little different. First, let's add +# weights to our graph edges: +g.es["weight"] = [2, 1, 5, 4, 7, 3, 2] + +# %% +# To get the shortest paths on a weighted graph, we pass the weights as an +# argument. For a change, we choose the output format as ``"epath"`` to +# receive the path as an edge list, which can be used to calculate the length +# of the path. +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!") + +# %% +# .. note:: +# +# - :meth:`igraph.GraphBase.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:`igraph.GraphBase.get_all_shortest_paths`. + +# %% +# In case you are wondering how the visualization figure was done, here's the code: +g.es["width"] = 0.5 +g.es[results[0]]["width"] = 2.5 + +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"], + edge_color="#666", + edge_align_label=True, + edge_background="white", +) +plt.show() diff --git a/doc/examples_sphinx-gallery/simplify.py b/doc/examples_sphinx-gallery/simplify.py new file mode 100644 index 000000000..ed36b2da5 --- /dev/null +++ b/doc/examples_sphinx-gallery/simplify.py @@ -0,0 +1,90 @@ +""" +======== +Simplify +======== + +This example shows how to remove self loops and multiple edges using :meth:`igraph.GraphBase.simplify`. +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# We start with a graph that includes loops and multiedges: +g1 = ig.Graph( + [ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 0), + (0, 0), + (1, 4), + (1, 4), + (0, 2), + (2, 4), + (2, 4), + (2, 4), + (3, 3), + ], +) + +# %% +# To simplify the graph, we must remember that the function operates in place, +# i.e. directly changes the graph that it is run on. So we need to first make a +# copy of our graph, and then simplify that copy to keep the original graph +# untouched: +g2 = g1.copy() +g2.simplify() + +# %% +# We can then proceed to plot both graphs to see the difference. First, let's +# choose a consistent visual style: +visual_style = { + "vertex_color": "lightblue", + "vertex_size": 20, + "vertex_label": [0, 1, 2, 3, 4], +} + +# %% +# And finally, let's plot them in twin axes, with rectangular frames around +# each plot: +fig, axs = plt.subplots(1, 2, sharex=True, sharey=True) +ig.plot( + g1, + layout="circle", + target=axs[0], + **visual_style, +) +ig.plot( + g2, + layout="circle", + target=axs[1], + **visual_style, +) +axs[0].set_title("Multigraph...") +axs[1].set_title("...simplified") +# Draw rectangles around axes +axs[0].add_patch( + plt.Rectangle( + (0, 0), + 1, + 1, + fc="none", + ec="k", + lw=4, + transform=axs[0].transAxes, + ) +) +axs[1].add_patch( + plt.Rectangle( + (0, 0), + 1, + 1, + fc="none", + ec="k", + lw=4, + transform=axs[1].transAxes, + ) +) +plt.show() diff --git a/doc/examples_sphinx-gallery/spanning_trees.py b/doc/examples_sphinx-gallery/spanning_trees.py new file mode 100644 index 000000000..be98501af --- /dev/null +++ b/doc/examples_sphinx-gallery/spanning_trees.py @@ -0,0 +1,57 @@ +""" +.. _tutorials-spanning-trees: + +============== +Spanning Trees +============== + +This example shows how to generate a spanning tree from an input graph using :meth:`igraph.Graph.spanning_tree`. For the related idea of finding a *minimum spanning tree*, see :ref:`tutorials-minimum-spanning-trees`. +""" + +import igraph as ig +import matplotlib.pyplot as plt +import random + +# %% +# First we create a two-dimensional, 6 by 6 lattice graph: +g = ig.Graph.Lattice([6, 6], circular=False) + +# %% +# We can compute the 2D layout of the graph: +layout = g.layout("grid") + +# %% +# To spice things up a little, we rearrange the vertex ids and compute a new +# layout. While not terribly useful in this context, it does make for a more +# interesting-looking spanning tree ;-) +random.seed(0) +permutation = list(range(g.vcount())) +random.shuffle(permutation) +g = g.permute_vertices(permutation) +new_layout = g.layout("grid") +for i in range(36): + new_layout[i] = layout[permutation[i]] +layout = new_layout + +# %% +# We can now generate a spanning tree: +spanning_tree = g.spanning_tree(weights=None, return_tree=False) + +# %% +# Finally, we can plot the graph with a highlight color for the spanning tree. +# We follow the usual recipe: first we set a few aesthetic options and then we +# leverage :func:`igraph.plot() ` and matplotlib for the +# heavy lifting: +g.es["color"] = "lightgray" +g.es[spanning_tree]["color"] = "midnightblue" +g.es["width"] = 0.5 +g.es[spanning_tree]["width"] = 3.0 + +fig, ax = plt.subplots() +ig.plot(g, target=ax, layout=layout, vertex_color="lightblue", edge_width=g.es["width"]) +plt.show() + +# %% +# .. note:: +# To invert the y axis such that the root of the tree is on top of the plot, +# you can call `ax.invert_yaxis()` before `plt.show()`. diff --git a/doc/examples_sphinx-gallery/stochastic_variability.py b/doc/examples_sphinx-gallery/stochastic_variability.py new file mode 100644 index 000000000..2afc01153 --- /dev/null +++ b/doc/examples_sphinx-gallery/stochastic_variability.py @@ -0,0 +1,171 @@ +""" +.. _tutorials-stochastic-variability: + +========================================================= +Stochastic Variability in Community Detection Algorithms +========================================================= + +This example demonstrates the use of stochastic community detection methods to check whether a network possesses a strong community structure, and whether the partitionings we obtain are meaningul. Many community detection algorithms are randomized, and return somewhat different results after each run, depending on the random seed that was set. When there is a robust community structure, we expect these results to be similar to each other. When the community structure is weak or non-existent, the results may be noisy and highly variable. We will employ several partion similarity measures to analyse the consistency of the results, including the normalized mutual information (NMI), the variation of information (VI), and the Rand index (RI). + +""" + +# %% +import igraph as ig +import matplotlib.pyplot as plt +import itertools +import random + +# %% +# .. note:: +# We set a random seed to ensure that the results look exactly the same in +# the gallery. You don't need to do this when exploring randomness. +random.seed(42) + +# %% +# We will use Zachary's karate club dataset [1]_, a classic example of a network +# with a strong community structure: +karate = ig.Graph.Famous("Zachary") + +# %% +# We will compare it to an an Erdős-Rényi :math:`G(n, m)` random network having +# the same number of vertices and edges. The parameters 'n' and 'm' refer to the +# vertex and edge count, respectively. Since this is a random network, it should +# have no community structure. +random_graph = ig.Graph.Erdos_Renyi(n=karate.vcount(), m=karate.ecount()) + +# %% +# First, let us plot the two networks for a visual comparison: + +# Create subplots +fig, axes = plt.subplots(1, 2, figsize=(12, 6), subplot_kw={"aspect": "equal"}) + +# Karate club network +ig.plot( + karate, + target=axes[0], + vertex_color="lightblue", + vertex_size=30, + vertex_label=range(karate.vcount()), + vertex_label_size=10, + edge_width=1, +) +axes[0].set_title("Karate club network") + +# Random network +ig.plot( + random_graph, + target=axes[1], + vertex_color="lightcoral", + vertex_size=30, + vertex_label=range(random_graph.vcount()), + vertex_label_size=10, + edge_width=1, +) +axes[1].set_title("Erdős-Rényi random network") + +plt.show() + + +# %% +# Function to compute similarity between partitions using various methods: +def compute_pairwise_similarity(partitions, method): + similarities = [] + + for p1, p2 in itertools.combinations(partitions, 2): + similarity = ig.compare_communities(p1, p2, method=method) + similarities.append(similarity) + + return similarities + + +# %% +# The Leiden method, accessible through :meth:`igraph.Graph.community_leiden()`, +# is a modularity maximization approach for community detection. Since exact +# modularity maximization is NP-hard, the algorithm employs a greedy heuristic +# that processes vertices in a random order. This randomness leads to +# variation in the detected communities across different runs, which is why +# results may differ each time the method is applied. The following function +# runs the Leiden algorithm multiple times: +def run_experiment(graph, iterations=100): + partitions = [ + graph.community_leiden(objective_function="modularity").membership + for _ in range(iterations) + ] + nmi_scores = compute_pairwise_similarity(partitions, method="nmi") + vi_scores = compute_pairwise_similarity(partitions, method="vi") + ri_scores = compute_pairwise_similarity(partitions, method="rand") + return nmi_scores, vi_scores, ri_scores + + +# %% +# Run the experiment on both networks: +nmi_karate, vi_karate, ri_karate = run_experiment(karate) +nmi_random, vi_random, ri_random = run_experiment(random_graph) + +# %% +# Finally, let us plot histograms of the pairwise similarities of the obtained +# partitionings to understand the result: +fig, axes = plt.subplots(2, 3, figsize=(12, 6)) +measures = [ + # Normalized Mutual Information (0-1, higher = more similar) + (nmi_karate, nmi_random, "NMI", 0, 1), + # Variation of Information (0+, lower = more similar) + (vi_karate, vi_random, "VI", 0, max(vi_karate + vi_random)), + # Rand Index (0-1, higher = more similar) + (ri_karate, ri_random, "RI", 0, 1), +] +colors = ["red", "blue", "green"] + +for i, (karate_scores, random_scores, measure, lower, upper) in enumerate(measures): + # Karate club histogram + axes[0][i].hist( + karate_scores, + bins=20, + range=(lower, upper), + density=True, # Probability density + alpha=0.7, + color=colors[i], + edgecolor="black", + ) + axes[0][i].set_title(f"{measure} - Karate club network") + axes[0][i].set_xlabel(f"{measure} score") + axes[0][i].set_ylabel("PDF") + + # Random network histogram + axes[1][i].hist( + random_scores, + bins=20, + range=(lower, upper), + density=True, + alpha=0.7, + color=colors[i], + edgecolor="black", + ) + axes[1][i].set_title(f"{measure} - Random network") + axes[1][i].set_xlabel(f"{measure} score") + axes[0][i].set_ylabel("PDF") + +plt.tight_layout() +plt.show() + +# %% +# We have compared the pairwise similarities using the NMI, VI, and RI measures +# between partitonings obtained for the karate club network (strong community +# structure) and a comparable random graph (which lacks communities). +# +# The Normalized Mutual Information (NMI) and Rand Index (RI) both quantify +# similarity, and take values from :math:`[0,1]`. Higher values indicate more +# similar partitionings, with a value of 1 attained when the partitionings are +# identical. +# +# The Variation of Information (VI) is a distance measure. It takes values from +# :math:`[0,\infty]`, with lower values indicating higher similarities. Identical +# partitionings have a distance of zero. +# +# For the karate club network, NMI and RI value are concentrated near 1, while +# VI is concentrated near 0, suggesting a robust community structure. In contrast +# the values obtained for the random network are much more spread out, showing +# inconsistent partitionings due to the lack of a clear community structure. + +# %% +# .. [1] W. Zachary: "An Information Flow Model for Conflict and Fission in Small Groups". Journal of Anthropological Research 33, no. 4 (1977): 452–73. https://www.jstor.org/stable/3629752 diff --git a/doc/examples_sphinx-gallery/topological_sort.py b/doc/examples_sphinx-gallery/topological_sort.py new file mode 100644 index 000000000..3c558f90b --- /dev/null +++ b/doc/examples_sphinx-gallery/topological_sort.py @@ -0,0 +1,60 @@ +""" +.. _tutorials-topological-sort: + +=================== +Topological sorting +=================== + +This example demonstrates how to get a topological sorting on a directed acyclic graph (DAG). A topological sorting of a directed graph is a linear ordering based on the precedence implied by the directed edges. It exists iff the graph doesn't have any cycle. In ``igraph``, we can use :meth:`igraph.GraphBase.topological_sorting` to get a topological ordering of the vertices. +""" + +import igraph as ig +import matplotlib.pyplot as plt + + +# %% +# First off, we 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, +) + +# %% +# We can verify immediately that this is actually a DAG: +assert g.is_dag + +# %% +# A topological sorting can be computed quite easily by calling +# :meth:`igraph.GraphBase.topological_sorting`, which returns a list of vertex IDs. +# If the given graph is not DAG, the error will occur. +results = g.topological_sorting(mode="out") +print("Topological sort of g (out):", *results) + +# %% +# In fact, there are two modes of :meth:`igraph.GraphBase.topological_sorting`, +# ``'out'`` ``'in'``. ``'out'`` is the default and starts from a node with +# indegree equal to 0. Vice versa, ``'in'`` starts from a node with outdegree +# equal to 0. To call the other mode, we can simply use: +results = g.topological_sorting(mode="in") +print("Topological sort of g (in):", *results) + +# %% +# We can use :meth:`igraph.Vertex.indegree` to find the indegree of the node. +for i in range(g.vcount()): + print("degree of {}: {}".format(i, g.vs[i].indegree())) + +# % +# Finally, we can plot the graph to make the situation a little clearer. +# Just to change things up a bit, we use the matplotlib visualization mode +# inspired by `xkcd _: +with plt.xkcd(): + fig, ax = plt.subplots(figsize=(5, 5)) + ig.plot( + g, + target=ax, + layout="kk", + vertex_size=25, + edge_width=4, + vertex_label=range(g.vcount()), + vertex_color="white", + ) diff --git a/doc/examples_sphinx-gallery/visual_style.py b/doc/examples_sphinx-gallery/visual_style.py new file mode 100644 index 000000000..30c27d73b --- /dev/null +++ b/doc/examples_sphinx-gallery/visual_style.py @@ -0,0 +1,91 @@ +""" +.. _tutorials-visual-style: + +Visual styling +=========================== + +This example shows how to change the visual style of network plots. +""" + +import igraph as ig +import matplotlib.pyplot as plt +import random + +# %% +# To configure the visual style of a plot, we can create a dictionary with the +# various setting we want to customize: +visual_style = { + "edge_width": 0.3, + "vertex_size": 15, + "palette": "heat", + "layout": "fruchterman_reingold", +} + +# %% +# Let's see it in action! First, we generate four random graphs: +random.seed(1) +gs = [ig.Graph.Barabasi(n=30, m=1) for i in range(4)] + +# %% +# Then, we calculate a color colors between 0-255 for all nodes, e.g. using +# betweenness just as an example: +betweenness = [g.betweenness() for g in gs] +colors = [[int(i * 255 / max(btw)) for i in btw] for btw in betweenness] + +# %% +# Finally, we can plot the graphs using the same visual style for all graphs: +fig, axs = plt.subplots(2, 2) +axs = axs.ravel() +for g, color, ax in zip(gs, colors, axs): + ig.plot(g, target=ax, vertex_color=color, **visual_style) +plt.show() + + +# %% +# .. note:: +# If you would like to set global defaults, for example, always using the +# Matplotlib plotting backend, or using a particular color palette by +# default, you can use igraph's `configuration instance +# :class:`igraph.configuration.Configuration`. A quick example on how to use +# it can be found here: :ref:`tutorials-configuration`. + +# %% +# In the matplotlib backend, igraph creates a special container +# :class:`igraph.drawing.matplotlib.graph.GraphArtist` which is a matplotlib Artist +# and the first child of the target Axes. That object can be used to customize +# the plot appearance after the initial drawing, e.g.: +g = ig.Graph.Barabasi(n=30, m=1) +fig, ax = plt.subplots() +ig.plot(g, target=ax) +artist = ax.get_children()[0] +# Option 1: +artist.set(vertex_color="blue") +# Option 2: +artist.set_vertex_color("blue") +plt.show() + +# %% +# .. note:: +# The :meth:`igraph.drawing.matplotlib.graph.GraphArtist.set` method can +# be used to change multiple properties at once and is generally more +# efficient than multiple calls to specific ``artist.set_...`` methods. + +# %% +# In the matplotlib backend, you can also specify the size of self-loops, +# either as a number or a sequence of numbers, e.g.: +g = ig.Graph(n=5) +g.add_edge(2, 3) +g.add_edge(0, 0) +g.add_edge(1, 1) +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + vertex_size=20, + edge_loop_size=[ + 0, # ignored, the first edge is not a loop + 30, # loop for vertex 0 + 80, # loop for vertex 1 + ], +) +plt.show() diff --git a/doc/examples_sphinx-gallery/visualize_cliques.py b/doc/examples_sphinx-gallery/visualize_cliques.py new file mode 100644 index 000000000..c1ebd49d6 --- /dev/null +++ b/doc/examples_sphinx-gallery/visualize_cliques.py @@ -0,0 +1,69 @@ +""" +.. _tutorials-cliques: + +============ +Cliques +============ + +This example shows how to compute and visualize cliques of a graph using :meth:`igraph.GraphBase.cliques`. + +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# First, let's create a graph, for instance the famous karate club graph: +g = ig.Graph.Famous("Zachary") + +# %% +# Computing cliques can be done as follows: +cliques = g.cliques(4, 4) + +# %% +# We can plot the result of the computation. To make things a little more +# interesting, we 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(), + vertex_size=5, + edge_width=0.5, + target=ax, + ) +plt.axis("off") +plt.show() + + +# %% +# 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: +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(), + vertex_size=5, + target=ax, + ) +plt.axis("off") +plt.show() diff --git a/doc/examples_sphinx-gallery/visualize_communities.py b/doc/examples_sphinx-gallery/visualize_communities.py new file mode 100644 index 000000000..9ab696e8e --- /dev/null +++ b/doc/examples_sphinx-gallery/visualize_communities.py @@ -0,0 +1,69 @@ +""" +.. _tutorials-visualize-communities: + +===================== +Communities +===================== + +This example shows how to visualize communities or clusters of a graph. +""" + +import igraph as ig +import matplotlib.pyplot as plt + +# %% +# First, we generate a graph. We use a famous graph here for simplicity: +g = ig.Graph.Famous("Zachary") + +# %% +# Edge betweenness is a standard way to detect communities. We then covert into +# a :class:`igraph.VertexClustering` object for subsequent ease of use: +communities = g.community_edge_betweenness() +communities = communities.as_clustering() + +# %% +# Next, we 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 + + +# %% +# Last, we plot the graph. We use a fancy technique called proxy artists to +# make a legend. You can find more about that in matplotlib's +# :doc:`matplotlib:users/explain/axes/legend_guide`: +fig, ax = plt.subplots() +ig.plot( + communities, + palette=palette, + edge_width=1, + target=ax, + vertex_size=20, +) + +# 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() + +# %% +# For an example on how to generate the cluster graph from a vertex cluster, +# check out :ref:`tutorials-cluster-graph`. diff --git a/doc/source/_pydoctor_templates/extra.css b/doc/source/_pydoctor_templates/extra.css new file mode 100644 index 000000000..17d3e963c --- /dev/null +++ b/doc/source/_pydoctor_templates/extra.css @@ -0,0 +1,6 @@ +/* increase spacing between parameters in a parameter list, and make sure + * that parameter names are aligned to the top */ +.fieldTable tr td { + padding: 3px 4px 10px 10px !important; + vertical-align: top; +} diff --git a/doc/source/_static/bootstrap-3.0.0/css/bootstrap.min.css b/doc/source/_static/bootstrap-3.0.0/css/bootstrap.min.css deleted file mode 100644 index dd89b1155..000000000 --- a/doc/source/_static/bootstrap-3.0.0/css/bootstrap.min.css +++ /dev/null @@ -1,2 +0,0 @@ -/* disable the Bootstrap CSS shipped with sphinx_bootstrap_theme in order - * to use our own from http://igraph.org */ diff --git a/doc/source/_static/bootstrap-3.0.0/js/bootstrap.min.js b/doc/source/_static/bootstrap-3.0.0/js/bootstrap.min.js deleted file mode 100644 index bb1cac443..000000000 --- a/doc/source/_static/bootstrap-3.0.0/js/bootstrap.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/* disable bootstrap.min.js coming from sphinx_bootstrap_theme in order to - * use our own from http://igraph.org */ diff --git a/doc/source/_static/bootstrap-sphinx.js b/doc/source/_static/bootstrap-sphinx.js deleted file mode 100644 index 30e5586d6..000000000 --- a/doc/source/_static/bootstrap-sphinx.js +++ /dev/null @@ -1,160 +0,0 @@ -(function ($) { - /** - * Patch TOC list. - * - * Will mutate the underlying span to have a correct ul for nav. - * - * @param $span: Span containing nested UL's to mutate. - * @param minLevel: Starting level for nested lists. (1: global, 2: local). - */ - var patchToc = function ($ul, minLevel) { - var findA, - patchTables, - $localLi; - - // Find all a "internal" tags, traversing recursively. - findA = function ($elem, level) { - level = level || 0; - var $items = $elem.find("> li > a.internal, > ul, > li > ul"); - - // Iterate everything in order. - $items.each(function (index, item) { - var $item = $(item), - tag = item.tagName.toLowerCase(), - $childrenLi = $item.children('li'), - $parentLi = $($item.parent('li'), $item.parent().parent('li')); - - // Add dropdowns if more children and above minimum level. - if (tag === 'ul' && level >= minLevel && $childrenLi.length > 0) { - $parentLi - .addClass('dropdown-submenu') - .children('a').first().attr('tabindex', -1); - - $item.addClass('dropdown-menu'); - } - - findA($item, level + 1); - }); - }; - - findA($ul); - }; - - /** - * Patch all tables to remove ``docutils`` class and add Bootstrap base - * ``table`` class. - */ - patchTables = function () { - $("table.docutils") - .removeClass("docutils") - .addClass("table") - .attr("border", 0); - }; - - $(window).load(function () { - /* - * Scroll the window to avoid the topnav bar - * https://github.com/twitter/bootstrap/issues/1768 - */ - if ($("#navbar.navbar-fixed-top").length > 0) { - var navHeight = $("#navbar").height(), - shiftWindow = function() { scrollBy(0, -navHeight - 10); }; - - if (location.hash) { - setTimeout(shiftWindow, 1); - } - - window.addEventListener("hashchange", shiftWindow); - } - }); - - $(document).ready(function () { - // Add styling, structure to TOC's. - $(".dropdown-menu").each(function () { - $(this).find("ul").each(function (index, item){ - var $item = $(item); - $item.addClass('unstyled'); - }); - }); - - // Global TOC. - if ($("ul.globaltoc li").length) { - patchToc($("ul.globaltoc"), 1); - } else { - // Remove Global TOC. - $(".globaltoc-container").remove(); - } - - // Local TOC. - $(".bs-sidenav ul").addClass("nav nav-list"); - $(".bs-sidenav > ul > li > a").addClass("nav-header"); - - // back to top - setTimeout(function () { - var $sideBar = $('.bs-sidenav'); - if (!$sideBar || !$sideBar.length) - return; - - $sideBar.affix({ - offset: { - top: function () { - var offsetTop = $sideBar.offset().top; - var sideBarMargin = parseInt($sideBar.children(0).css('margin-top'), 10); - var navOuterHeight = $('#navbar').height(); - - return (this.top = offsetTop - navOuterHeight - sideBarMargin); - } - , bottom: function () { - // add 25 because the footer height doesn't seem to be enough - return (this.bottom = $('.footer').outerHeight(true) + 25); - } - } - }); - }, 100); - - // Local TOC. - patchToc($("ul.localtoc"), 2); - - // Mutate sub-lists (for bs-2.3.0). - $(".dropdown-menu ul").not(".dropdown-menu").each(function () { - var $ul = $(this), - $parent = $ul.parent(), - tag = $parent[0].tagName.toLowerCase(), - $kids = $ul.children().detach(); - - // Replace list with items if submenu header. - if (tag === "ul") { - $ul.replaceWith($kids); - } else if (tag === "li") { - // Insert into previous list. - $parent.after($kids); - $ul.remove(); - } - }); - - // Add divider in page TOC. - $localLi = $("ul.localtoc li"); - if ($localLi.length > 2) { - $localLi.first().after('
  • '); - } - - // Enable dropdown. - // $('.dropdown-toggle').dropdown(); - - // Patch tables. - patchTables(); - - // Add Note, Warning styles. (BS v2,3 compatible). - $('div.note').addClass('alert alert-info'); - $('div.warning').addClass('alert alert-danger alert-error'); - - // Inline code styles to Bootstrap style. - $('tt.docutils.literal').not(".xref").each(function (i, e) { - // ignore references - if (!$(e).parent().hasClass("reference")) { - $(e).replaceWith(function () { - return $("").text($(this).text()); - }); - }}); - }); -}($jqTheme || window.jQuery)); diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css new file mode 100644 index 000000000..7c1a52022 --- /dev/null +++ b/doc/source/_static/custom.css @@ -0,0 +1,10 @@ +/* override table width restrictions */ +.wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal; +} + +.wy-table-responsive { + margin-bottom: 24px; + max-width: 100%; + overflow: visible; +} diff --git a/doc/source/_static/gallery_thumbnails/ring_animation.gif b/doc/source/_static/gallery_thumbnails/ring_animation.gif new file mode 100644 index 000000000..613dcfaac Binary files /dev/null and b/doc/source/_static/gallery_thumbnails/ring_animation.gif differ diff --git a/doc/source/_static/gallery_thumbnails/ring_animation.png b/doc/source/_static/gallery_thumbnails/ring_animation.png new file mode 100644 index 000000000..e459e4714 Binary files /dev/null and b/doc/source/_static/gallery_thumbnails/ring_animation.png differ diff --git a/doc/source/_static/logo-black.svg b/doc/source/_static/logo-black.svg new file mode 100644 index 000000000..303ad1f47 --- /dev/null +++ b/doc/source/_static/logo-black.svg @@ -0,0 +1 @@ +logo-black diff --git a/doc/source/_templates/layout.html b/doc/source/_templates/layout.html deleted file mode 100644 index d89ffba57..000000000 --- a/doc/source/_templates/layout.html +++ /dev/null @@ -1,71 +0,0 @@ ---- -layout: default -title: {{ title|striptags|e }}{{ titlesuffix }} -mainheader: python-igraph Manual -lead: For using igraph from Python -extrahead: {%- block linktags %} - {%- if hasdoc('about') %} - - {%- endif %} - - {%- if parents %} - - {%- endif %} - {%- if next %} - - {%- endif %} - {%- if prev %} - - {%- endif %} -{%- endblock %} - - - -extrafoot: - - ---- - -{# Sidebar: Rework into our Bootstrap nav section. #} -{% macro navBar() %} -{% include "navbar.html" %} -{% endmacro %} - -{# Update the content width and offset #} -{% set bs_content_width_real = "9" %} -{% set bs_content_offset = "1" %} - -{# Tweak the main documentation area #} -{%- block content %} -
    -
    -
    - {{ navBar() }} -
    -
    -
    -
    -
    - {% block body %}{% endblock %} -
    -
    -
    -
    - -{%- endblock %} - -{# Modified basic/layout.html #} - -{%- block doctype -%}{%- endblock %} -{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %} -{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %} -{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and - (sidebars != []) %} -{%- set url_root = pathto('', 1) %} -{# XXX necessary? #} -{%- if url_root == '#' %}{% set url_root = '' %}{% endif %} -{%- if not embedded and docstitle %} - {%- set titlesuffix = " — "|safe + docstitle|e %} -{%- else %} - {%- set titlesuffix = "" %} -{%- endif %} diff --git a/doc/source/_templates/navbar.html b/doc/source/_templates/navbar.html deleted file mode 100644 index ae30cee41..000000000 --- a/doc/source/_templates/navbar.html +++ /dev/null @@ -1,20 +0,0 @@ - - diff --git a/doc/source/_templates/relations.html b/doc/source/_templates/relations.html deleted file mode 100644 index 8ac8d5c80..000000000 --- a/doc/source/_templates/relations.html +++ /dev/null @@ -1,18 +0,0 @@ -{%- if prev %} - - - {{ prev.title|striptags }} - -{%- endif %} - - - -{%- if next %} - - - {{ next.title|striptags }} - -{%- endif %} - diff --git a/doc/source/analysis.rst b/doc/source/analysis.rst index 8b0ff457f..bb911efad 100644 --- a/doc/source/analysis.rst +++ b/doc/source/analysis.rst @@ -1,5 +1,443 @@ +.. include:: include/global.rst + +.. currentmodule:: igraph + + Graph analysis ============== -.. note:: TODO. This is a placeholder section; it is not written yet. +|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 :doc:`api/index` 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:: + + >>> 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 like indexing and slicing 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 the :doc:`api/index` 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_adjacent`. + +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_biadjacency`. + +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.harmonic_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.connected_components`): 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_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` diff --git a/doc/source/api/index.rst b/doc/source/api/index.rst new file mode 100644 index 000000000..72b8f0b7e --- /dev/null +++ b/doc/source/api/index.rst @@ -0,0 +1,7 @@ +.. include:: ../include/global.rst + +.. currentmodule:: igraph + + +API reference +============= diff --git a/doc/source/assets/zachary.zip b/doc/source/assets/zachary.zip new file mode 100644 index 000000000..d6a99901f Binary files /dev/null and b/doc/source/assets/zachary.zip differ diff --git a/doc/source/conf.py b/doc/source/conf.py index c8182b7c4..2dcdfa5fe 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. @@ -11,204 +11,276 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os -import sphinx_bootstrap_theme +from datetime import datetime + +import sys +import os +import importlib +from pathlib import Path + + +# Check if we are inside readthedocs, the conf is quite different there +rtd_version = os.getenv("READTHEDOCS_VERSION", "") +rtd_version_type = os.getenv("READTHEDOCS_VERSION_TYPE", "unknown") + +# Utility functions +# NOTE: these could be improved, esp by importing igraph, but that +# currently generates a conflict with pydoctor. It is funny because pydoctor's +# docs indeed import itself... perhaps there's a decent way to solve this. +def get_root_dir(): + """Get project root folder""" + return str(Path(".").absolute().parent.parent) + + +def get_igraphdir(): + """Get igraph folder""" + return Path(importlib.util.find_spec("igraph").origin).parent + + +def get_igraph_version(): + """Get igraph version""" + if rtd_version and rtd_version_type == "tag": + return rtd_version + + version_file = get_igraphdir() / "version.py" + with open(version_file, "rt") as f: + version_info = f.readline().rstrip("\n").split("=")[1].strip()[1:-1].split(", ") + version = ".".join(version_info) + + return version -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- +_igraph_dir = str(get_igraphdir()) +_igraph_version = get_igraph_version() + # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.coverage'] +extensions = [ + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.intersphinx", + "sphinx_gallery.gen_gallery", + #'sphinx_panels', + "pydoctor.sphinx_ext.build_apidocs", +] -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.append(os.path.abspath('.')) # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'python-igraph' -copyright = u'2010-2013, Tamás Nepusz, Gábor Csárdi' +project = "igraph" +copyright = "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.7' +version = _igraph_version # The full version, including alpha/beta/rc tags. -release = '0.7' +release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['include/*.rst'] +exclude_patterns = ["include/*.rst"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'bootstrap' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -html_theme_options = { - "navbar_fixed_top": "false", - "navbar_class": "navbar navbar-inverse", - "bootstrap_version": "3" -} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() +# Inspired by pydoctor's RTD page itself +# https://github.com/twisted/pydoctor/blob/master/docs/source/conf.py +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] +html_css_files = ["custom.css"] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = "_static/logo-black.svg" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_favicon = None # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} - -# If false, no module index is generated. -html_domain_indices = False - -# If false, no index is generated. -html_use_index = False - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -html_show_sourcelink = False +# html_additional_pages = {} # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'python-igraphdoc' +htmlhelp_basename = "igraphdoc" + +# Integration with Read the Docs since RTD is not manipulating the Sphinx +# config files on its own any more. +# This is according to: +# https://about.readthedocs.com/blog/2024/07/addons-by-default/ +html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") +html_context = {} +if os.environ.get("READTHEDOCS", "") == "True": + html_context["READTHEDOCS"] = True + +# -- Options for pydoctor ------------------------------------------------------ + + +def get_pydoctor_html_outputdir(pydoctor_url_path): + """Get HTML output dir for pydoctor""" + # NOTE: obviously this is a little tricky, but it does work for both + # the sphinx-build script and the python -m sphinx module calls. It works + # locally, on github pages, and on RTD. + return str(Path(sys.argv[-1]) / pydoctor_url_path.strip("/")) + + +# API docs relative to the rest of the docs, needed for pydoctor to play nicely +# with intersphinx (https://pypi.org/project/pydoctor/#pydoctor-21-2-0) +# NOTE: As of 2022 AD, pydoctor requires this to be a subfolder of the docs. +pydoctor_url_path = "api/" + +pydoctor_args = [ + "--project-name=igraph", + "--project-version=" + version, + "--project-url=https://python.igraph.org", + "--introspect-c-modules", + "--docformat=epytext", + "--intersphinx=https://docs.python.org/3/objects.inv", + "--html-output=" + get_pydoctor_html_outputdir(pydoctor_url_path), + "--html-viewsource-base=https://github.com/igraph/python-igraph/tree/main/src/igraph", + "--project-base-dir=" + _igraph_dir, + "--template-dir=" + get_root_dir() + "/doc/source/_pydoctor_templates", + "--theme=readthedocs", + _igraph_dir, +] +pydoctor_url_path = "/en/{rtd_version}/api" + +# -- Options for sphinx-gallery ------------------------------------------------ + +sphinx_gallery_conf = { + "examples_dirs": "../examples_sphinx-gallery", # path to your example scripts + "gallery_dirs": "tutorials", # path to where to save gallery generated output + "filename_pattern": "/", + "matplotlib_animations": True, + "remove_config_comments": True, +} # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # 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', - u'Tamas Nepusz, Gabor Csardi', 'manual'), + ( + "index", + "igraph.tex", + "igraph Documentation", + "The igraph development team", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -216,43 +288,53 @@ # 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', - [u'Tamas Nepusz, Gabor Csardi'], 1) + ("index", "igraph", "igraph Documentation", ["The igraph development team"], 1) ] # -- Options for Epub output --------------------------------------------------- # 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_title = "igraph" +epub_author = "The igraph development team" +epub_publisher = "The igraph development team" +epub_copyright = "2010-2022, The igraph development team" # The language of the text. It defaults to the language option # or en if the language is not set. -#epub_language = '' +# epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' +# epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. -#epub_identifier = '' +# epub_identifier = '' # A unique identification for the text. -#epub_uid = '' +# epub_uid = '' # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_pre_files = [] +# epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_post_files = [] +# epub_post_files = [] # A list of files that should not be packed into the epub file. -#epub_exclude_files = [] +# epub_exclude_files = [] # The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 +# epub_tocdepth = 3 + + +# -- Intersphinx ------------------------------------------------ + +intersphinx_mapping = { + "numpy": ("https://numpy.org/doc/stable/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), + "matplotlib": ("https://matplotlib.org/stable", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + "networkx": ("https://networkx.org/documentation/stable/", None), +} diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst new file mode 100644 index 000000000..93b2a0baa --- /dev/null +++ b/doc/source/configuration.rst @@ -0,0 +1,30 @@ +.. include:: include/global.rst + +.. currentmodule:: igraph + +============= +Configuration +============= +|igraph| includes customization options that can be set via the :class:`configuration.Configuration` object and can be preserved via a configuration file. This file is stored at ``~/.igraphrc`` by default on Linux and Mac OS X systems, and at ``C:\Documents and Settings\username\.igraphrc`` on Windows systems. + +To modify config options and store the result to file for future reuse: + +.. code-block:: python + + import igraph as ig + + # Set configuration variables + ig.config["plotting.backend"] = "matplotlib" + ig.config["plotting.palette"] = "rainbow" + + # Save configuration to default file location + ig.config.save() + +Once your configuration file exists, |igraph| will load its contents automatically upon import. + +It is possible to keep multiple configuration files in nonstandard locations by passing an argument to ``config.save``, e.g. ``ig.config.save("/path/to/config/file")``. To load a specific config the next time you import igraph, use: + +.. code-block:: python + + import igraph as ig + ig.config.load("/path/to/config/file") diff --git a/doc/source/faq.rst b/doc/source/faq.rst new file mode 100644 index 000000000..040a9fa58 --- /dev/null +++ b/doc/source/faq.rst @@ -0,0 +1,115 @@ +.. include:: include/global.rst + +.. currentmodule:: igraph + +========================== +Frequently asked questions +========================== + +I tried to install |igraph| but got an error! What do I do? +----------------------------------------------------------- +First, look at our :doc:`installation instructions ` including the +troubleshooting section. If that does not solve your problem, reach out via +the `igraph forum `_. We'll try our best to +help you! + + +I've just installed |igraph|. What do I do now? +----------------------------------------------- +Take a peek at the :doc:`tutorials/quickstart`! You can then go through a few +more examples in our :ref:`gallery `, read detailed instructions on graph :doc:`generation `, :doc:`analysis ` and :doc:`visualisation `, and check out the full :doc:`API documentation `. + + +I thought |igraph| was an R package, is this the same package? +-------------------------------------------------------------- +|igraph| is a software library written in C with interfaces in various programming +languages such as R, Python, and Mathematica. Many functions will have similar names +and functionality across languages, but the matching is not perfect, so you will +occasionally find functions that are supported in one language but not another. +See the FAQ below for instructions about how to request a feature. + + +I would like to use |igraph| but don't know Python, what to do? +--------------------------------------------------------------- +|igraph| can be used from multiple programming languages such as C, R, Python, +and Mathematica. While the exact function names differ a bit, most functionality +is shared, so if you can code any of them you can use |igraph|: just refer to +the installation instructions for the appropriate language on our +`homepage `_. + +If you are not familiar with programming at all, or if you don't know any Python +but would still like to use the Python interface for |igraph|, you should start by +learning Python first. There are many resources online including online classes, +videos, tutorials, etc. |igraph| does not use a lot of advanced Python-specific +tricks, so once you can use a standard module such as :mod:`pandas` or +:mod:`matplotlib`, |igraph| should be easy to pick up. + + +I would like to have a function for operation/algorithm X, can you add it? +-------------------------------------------------------------------------- +We are continuously extending |igraph| to include new functionality, and +requests from our community are the best way to guide those efforts. Of +course, we are just a few folks so we cannot guarantee that each and every +obscure community detection algorithm will be included in the package. +Please open a new thread on our `forum `_ +describing your request. If your request is to adapt an existing function +or specific piece of code, you can directly open a +`GitHub issue `_ +(make sure a similar issue does not exist yet! - If it does, comment there +instead.) + + +What's the difference between |igraph| and similar packages (networkx, graph-tool)? +----------------------------------------------------------------------------------- +All those packages focus on graph/network analysis. + +.. warning:: + The following differences and similarities are considered correct as of the time of writing (Jan 2022). If you identify incorrect or outdated information, please open a `Github issue `_ and we'll update it. + +**Differences:** + + - |igraph| supports **multiple programming languages** (e.g. C, Python, R, Mathematica). `networkx`_ and `graph-tool`_ are Python only. + - |igraph|'s core library is written in C, which makes it often faster than `networkx`_. `graph-tool`_ is written in heavily templated C++, so it can be as fast as |igraph| but supports fewer architectures. Compiling `graph-tool` can take much longer than |igraph| (hours versus around a minute). + - |igraph| vertices are *ordered with contiguous numerical IDs, from 0 upwards*, and an *optional* "vertex name". `networkx`_ nodes are *defined* by their name and not ordered. + - Same holds for edges, ordered with integer IDs in |igraph|, not so in `networkx`_. + - |igraph| can plot graphs using :mod:`matplotlib` and has experimental support for `plotly`_, so it can produce animations, notebook widgets, and interactive plots (e.g. zoom, panning). `networkx`_ has excellent :mod:`matplotlib` support but no `plotly`_ support. `graph-tool`_ only supports static images via Cairo and GTK+. + - In terms of design, |igraph| really shines when you have a relatively static network that you want to analyse, while it can struggle with very dynamic networks that gain and lose vertices and edges all the time. This might change in the near future as we improve |igraph|'s core C library. At the moment, `networkx`_ is probably better suited for simulating such highly dynamic graphs. + +**Similarities:** + + - Many tasks can be achieved equally well with |igraph|, `graph-tool`_, and `networkx`_. + - All can read and write a number of graph file formats. + - All can visualize graphs, with different strengths and weaknesses. + +.. note:: + |igraph| includes conversion functions from/to `networkx`_, so you can create and manipulate a network with |igraph| and later on convert it to `networkx`_ or `graph-tool`_ if you need. Vice versa, you can load a graph in `networkx`_ or `graph-tool`_ and convert the graph into an |igraph| object if you need more speed, a specific algorithm, matplotlib animations, etc. You can even use |igraph| to convert graphs from `networkx`_ to `graph-tool`_ and vice versa! + + + +I would like to contribute to |igraph|, where do I start? +--------------------------------------------------------- +Thank you for your enthusiasm! |igraph| is a great opportunity to give back +to the open source community or just learn about graphs. Depending on your +skills in software engineering, programming, communication, or data science +some tasks might be better suited than others. + +If you want to code straight away, take a look at the +`GitHub issues `_ and see if +you find one that sounds easy enough and sparks your interest, and write a +message saying you're interested in taking it on. We'll reply ASAP and guide +you as of your next steps. + +The C core library also has various `"theory issues" `_. You can contribute to these issues without any programming +knowledge by researching graph literature or finding the solution to a graph +problem. Once the theory obstacle has been overcome, others can move on to the +coding part: a real team effort! + +If none of those look feasible, or if you have a specific idea, or still if +you would like to contribute in other ways than pure programming, reach out +on our `forum `_ and we'll come up with +some ideas. + + +.. _networkx: https://networkx.org/documentation/stable/ +.. _graph-tool: https://graph-tool.skewed.de/ +.. _plotly: https://plotly.com/python/ 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 000000000..e7b6bf4d3 Binary files /dev/null and b/doc/source/figures/tutorial_social_network_1_mpl.png differ diff --git a/doc/source/figures/tutorial_social_network_2_mpl.png b/doc/source/figures/tutorial_social_network_2_mpl.png new file mode 100644 index 000000000..9f42a39d5 Binary files /dev/null and b/doc/source/figures/tutorial_social_network_2_mpl.png differ diff --git a/doc/source/generation.rst b/doc/source/generation.rst index 475b50e56..b5a828da6 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -1,5 +1,210 @@ +.. include:: include/global.rst + +.. _generation: + +.. currentmodule:: igraph + 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 :doc:`api/index` for details on each function and class. + +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: + + >>> 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']} + ... ) + +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 :doc:`api/index` of each function for details and examples. + +From matrices ++++++++++++++ + +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]]) + +This graph is directed and has edges `[0, 1]`, `[0, 2]` and `[2, 2]` (a self-loop). + +To create a bipartite graph from a bipartite adjacency matrix, use :meth:`Graph.Biadjacency`:: + + >>> g = Graph.Biadjacency([[0, 1, 1], [1, 1, 0]]) + +From files +++++++++++ + +To load a graph from a 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. + +Complete graphs ++++++++++++++++ + +To create a complete 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 included:: + + >>> g = Graph.Full(n=3, directed=True, loops=True) + +A similar method, :meth:`Graph.Full_Bipartite`, generates a complete bipartite graph. Finally, the metho :meth:`Graph.Full_Citation` created the full citation graph, in which a vertex with index `i` has a directed edge to all vertices with index 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 square lattice of the chosen size. For instance:: + + >>> g = Graph.Lattice(dim=[3, 3], circular=False) + +creates a 3×3 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 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) + +Graph Atlas ++++++++++++ + +The book ‘An Atlas of Graphs’ by Roland C. Read and Robin J. Wilson contains all unlabeled 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: + + - Barabási-Albert model: :meth:`Graph.Barabasi` + - Erdős-Rényi: :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` + - 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` + - graphs from LCF notation :meth:`Graph.LCF` + - small graphs of any "isomorphism class" :meth:`Graph.Isoclass` + - graphs with a specified degree sequence :meth:`Graph.Realize_Degree_Sequence` diff --git a/doc/source/icon.png b/doc/source/icon.png new file mode 100644 index 000000000..0735f005b Binary files /dev/null and b/doc/source/icon.png differ diff --git a/doc/source/icon@2x.png b/doc/source/icon@2x.png new file mode 100644 index 000000000..a9260d97b Binary files /dev/null and b/doc/source/icon@2x.png differ diff --git a/doc/source/index.rst b/doc/source/index.rst index cb4afaa66..58df00b41 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,24 +1,115 @@ -.. 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! -========================================= +.. currentmodule:: igraph -Contents: +.. raw:: html + + + + +python-igraph |release| +======================= +Python interface of `igraph`_, a fast and open source C library to manipulate and analyze graphs (aka networks). It can be used to: + + - Create, manipulate, and analyze networks. + - Convert graphs from/to `networkx`_, `graph-tool`_ and many file formats. + - Plot networks using `Cairo`_, `matplotlib`_, and `plotly`_. + + +Installation +============ + +.. container:: twocol + + .. container:: + + Install using `pip `__: + + .. code-block:: bash + + pip install igraph + + .. container:: + + Install using `conda `__: + + .. code-block:: bash + + conda install -c conda-forge python-igraph + +Further details are available in the :doc:`Installation Guide `. + +Documentation +============= + +.. container:: twocol + + .. container:: + + + **Tutorials** + + - :doc:`Quick start ` + - :doc:`Gallery of examples ` + - :doc:`Extended tutorial ` (:doc:`Español `) + + .. container:: + + **Detailed docs** + + - :doc:`Generation ` + - :doc:`Analysis ` + - :doc:`Visualization ` + - :doc:`Configuration ` + +.. container:: twocol + + .. container:: + + **Reference** + + - :doc:`api/index` + - `Source code `_ + + .. container:: + + **Support** + + - :doc:`FAQs ` + - `Forum `_ + +Documentation for `python-igraph <= 0.10.1` is available on our `old website `_. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + :hidden: - intro install + tutorials/index tutorial + tutorial.es + api/index generation analysis visualisation - misc + configuration + faq Indices and tables @@ -27,3 +118,17 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` + +.. _igraph: https://igraph.org +.. _networkx: https://networkx.org/documentation/stable/ +.. _graph-tool: https://graph-tool.skewed.de/ +.. _Cairo: https://www.cairographics.org +.. _matplotlib: https://matplotlib.org +.. _plotly: https://plotly.com/python/ + +Citation +======== + +If you use igraph in your research, please cite + + Csardi, G., & Nepusz, T. (2006). The igraph software package for complex network research. InterJournal, Complex Systems, 1695. diff --git a/doc/source/install.rst b/doc/source/install.rst index e511312ec..65c985bb9 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -8,152 +8,209 @@ Installing |igraph| =================== -This chapter describes how to install the C core of |igraph| and its Python bindings -on various operating systems. - -Which |igraph| is right for you? -================================ - -|igraph| is primarily a library written in C. It is *not* a standalone program, nor it is -a Python package that you can just drop on your Python path to start using it. Therefore, -if you would like to exploit |igraph|'s functionality in Python, you must install -a few packages. Do not worry, though, there are precompiled packages for the major operating -systems, so you will not have to compile |igraph| from source unless you use an esoteric -operating system or you have specific requirements (i.e., adding a custom patch to |igraph|'s -C core). Precompiled packages are often called *binary packages*, while the raw source code -is usually referred to as the *source package*. - -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 from source`_ tells -you how to compile |igraph| from the source package. - -Installation from a binary package -================================== - -|igraph| on Windows -------------------- +Binary package (recommended) +============================ +It is recommended to install a binary package that includes both C core and Python interface. You can choose either of `PyPI `_ or `Conda `_. Linux users can also use their package manager. + +PyPI +---- +PyPI 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 the Python interface of |igraph| globally, use the following command +(you might need administrator/root privileges):: + + $ pip install igraph + +If you prefer to install |igraph| in a user folder using a `virtual environment +`_, use the following commands instead:: + + $ python -m venv my_environment + $ source my_environment/bin/activate + $ pip install igraph + +As usual, if you do not want to activate the virtualenv, you can call the ``pip`` +executable in it directly:: + + $ python -m venv my_environment + $ my_environment/bin/pip install igraph -There is a Windows installer for |igraph|'s Python interface on the -`Python Package Index `_. -Download the one that is suitable for your Python version (currently -there are binary packages for Python 2.6, Python 2.7 and Python 3.2, -though it might change in the future). To test the installed package, launch -your favourite Python IDE and type 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. - -Graph plotting in |igraph| on Windows -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -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 provide -pre-compiled binaries for Windows, but Christoph Gohlke maintains a site -containing unofficial Windows binaries for several Python extension packages, -including Cairo itself. Therefore, the easiest way to install Cairo on Windows -along with its Python bindings is simply to download it from -`Christoph's site `_. Make -sure you use an installer that is suitable for your Windows platform (32-bit or -64-bit) and the version of Python you are using. - -In case you use a version of Python for which the above site does not provide -an installer, you can install it from an alternative source in a slightly more -complicated way by following the steps below: - -1. Get the latest PyCairo for Windows installer from - http://ftp.gnome.org/pub/gnome/binaries/win32/pycairo/1.8. Make sure you - grab the one that matches your Python version. At the time of writing, - the above folder contained installers for Python 2.6 and 2.7 only. You may - also try and go one level up, then down then 1.4 subfolder -- these are - older versions, but they work with Python 2.5 and Python 2.6 as well. - -2. Install PyCairo using the installer. The installer extracts the necessary - files into ``Lib\site-packages\cairo`` within the folder where Python is - installed. Unfortunately there are some extra DLLs which are required to - make Cairo work, so we have to get these as well. - -3. Head to http://ftp.gnome.org/pub/gnome/binaries/win32/dependencies/ and get - the binary versions of Cairo (``cairo_1.8.10-3_win32.zip`` at the time of - writing), Fontconfig (``fontconfig_2.8.0-2_win32.zip``), Freetype - (``freetype_2.4.4-1_win32.zip``), Expat (``expat_2.0.1-1_win32.zip``), - ``libpng`` (``libpng_1.4.3-1_win32.zip``) and ``zlib`` - (``zlib_1.2.5-2_win32.zip``). Version numbers may vary, so be - adaptive! Each ZIP file will contain a ``bin`` subfolder with a DLL file in - it. Put the following DLLs in ``Lib\site-packages\cairo`` within your Python - installation: - - - ``freetype6.dll`` (from ``freetype_2.4.4-1_win32.zip``) - - ``libcairo-2.dll`` (from ``cairo_1.8.10-3_win32.zip``) - - ``libexpat-1.dll`` (from ``expat_2.0.1-1_win32.zip``) - - ``libfontconfig-1.dll`` (from ``fontconfig_2.8.0-2_win32.zip``) - - ``libpng14-14.dll`` (from ``libpng_1.4.3-1_win32.zip``) - - ``zlib1.dll`` (from ``zlib_1.2.5-2_win32.zip``). - -Having done that, you can launch Python again and check if it worked: - - >>> from igraph import * - >>> g = Graph.Famous("petersen") - >>> plot(g) - -|igraph| on Linux ------------------ - -|igraph| on Debian GNU/Linux -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -|igraph| on RedHat Linux -^^^^^^^^^^^^^^^^^^^^^^^^ - -|igraph| on other Linux distributions -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -|igraph| on Mac OS X --------------------- - -There is a Mac OS X installer for |igraph|'s Python interface on the -`Python Package Index `_ -which works for Intel-based Macs running OS X Lion. The default -Python version in Leopard is Python 2.7, so the package is compiled -for this specific version. PowerPC users should compile the package -themselves (see `Compiling igraph from source`_). To test the -installed package, launch your favourite Python IDE or the default -command line interpreter and type 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. - -Graph plotting in |igraph| on Mac OS X -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Graph plotting in |igraph| is implemented using a third-party package -called `Cairo `_. If you want to create -publication-quality plots in |igraph| on Mac OS X, you must also install -Cairo and its Python bindings. The Cairo project does not provide -pre-compiled binaries for Mac OS X, but `MacPorts `_ -and `Fink `_ does, so you can use them to -install Cairo. The `Cairo homepage `_ gives -you some installation instructions. However, this is only one half of the -job, you will also need the Python bindings of Cairo from the -`PyCairo homepage `_. At the moment -there are no precompiled PyCairo packages for Mac OS X either. - -TODO: detailed compilation instructions for PyCairo - -|igraph| on other operating systems ------------------------------------ +Conda +----- +Packages are kindly provided by `conda-forge `_:: + + $ conda install -c conda-forge python-igraph + +Like virtualenv, Conda also offers virtual environments. If you prefer that option:: + + $ conda create -n my_environment + $ conda activate my_environment + $ conda install -c conda-forge python-igraph + +Package managers on Linux and other systems +------------------------------------------- +|igraph|'s Python interface and its dependencies are included in several package management +systems, including those of the most popular Linux distributions (Arch Linux, +Debian and Ubuntu, Fedora, etc.) as well as some cross-platform systems like +NixPkgs or MacPorts. + +.. note:: |igraph| is updated quite often: if you need a more recent version than your + package manager offers, use ``pip`` or ``conda`` as shown above. For bleeding-edge + versions, compile from source (see below). Compiling |igraph| from source ============================== +You might want to compile |igraph| to test a recently added feature ahead of release or +to install |igraph| on architectures not covered by our continuous development pipeline. + +.. note:: In all cases, the Python interface needs to be compiled against + a **matching** version of the |igraph| core C library. If you used ``git`` + to check out the source tree, ``git`` was probably smart enough to check out + the matching version of igraph's C core as a submodule into + ``vendor/source/igraph``. You can use ``git submodule update --init + --recursive`` to check out the submodule manually, or you can run ``git + submodule status`` to print the exact revision of igraph's C core that + should be used with the Python interface. + +Compiling using pip +------------------- + +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 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:' 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 |igraph| step by step from a local checkout, i.e. _not_ relying on +``pip`` to fetch the sources. (You would still need ``pip`` to install from +source, or a PEP 517-compliant build frontend like +`build `_ to build an installable +Python wheel. + +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 |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 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 ``pip`` to compile and install the package from source:: + + $ pip install . + +Alternatively, you can call ``build`` or another PEP 517-compliant build frontend +to build an installable Python wheel. Here we use `pipx `_ +to invoke ``build`` in a separate virtualenv:: + + $ pipx run build + +Testing your installation +------------------------- + +Use ``tox`` or another standard test runner tool to run all the unit tests. +Here we use `pipx `_ to invoke ``tox``:: + + $ pipx run tox + +You can also call ``tox`` directly from the root folder of the igraph source +tree if you already installed ``tox`` system-wide:: + + $ tox + +Troubleshooting +=============== + +Q: I am trying to install |igraph| on Windows but am getting DLL import errors +------------------------------------------------------------------------------ +A: The most common reason for this error 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 `_. + +Q: I am trying to use |igraph| but get errors about something called Cairo +---------------------------------------------------------------------------------- +A: |igraph| by default uses a third-party called `Cairo `_ for plotting. +If Cairo is not installed on your computer, you might get an import error. This error is most commonly +encountered on Windows machines. + +There are two solutions to this problem: installing Cairo or, if you are using a recent versions of +|igraph|, switching to the :mod:`matplotlib` plotting backend. + +**1. Install Cairo**: As explained `here `_, +you need to install Cairo headers using your package manager (Linux) or `homebrew `_ +(macOS) and then:: + + $ pip install pycairo + +To check if Cairo is installed correctly on your system, run the following example:: + + >>> import igraph as ig + >>> g = ig.Graph.Famous("petersen") + >>> ig.plot(g) + +If PyCairo was successfully installed, this will display a Petersen graph. + +**2. Switch to matplotlib**: You can :doc:`configure ` |igraph| to use matplotlib +instead of Cairo. First, install it:: + + $ pip install matplotlib + +To use matplotlib for a single plot, create a :class:`matplotlib.figure.Figure` and +:class:`matplotlib.axes.Axes` beforehand (e.g. using :func:`matplotlib.pyplot.subplots`):: + + >>> import matplotlib.pyplot as plt + >>> import igraph as ig + >>> fig, ax = plt.subplots() + >>> g = ig.Graph.Famous("petersen") + >>> ig.plot(g, target=ax) + >>> plt.show() + +To use matplotlib for a whole session/notebook:: + + >>> import matplotlib.pyplot as plt + >>> import igraph as ig + >>> ig.config["plotting.backend"] = "matplotlib" + >>> g = ig.Graph.Famous("petersen") + >>> ig.plot(g) + >>> plt.show() + +To preserve this preference across sessions/notebooks, you can store it in the default +configuration file used by |igraph|:: + + >>> import igraph as ig + >>> ig.config["plotting.backend"] = "matplotlib" + >>> ig.config.save() -Summary -======= +From now on, |igraph| will default to matplotlib for plotting. diff --git a/doc/source/intro.rst b/doc/source/intro.rst deleted file mode 100644 index 34db9acf7..000000000 --- a/doc/source/intro.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. include:: include/global.rst - -.. Introduction - -============ -Introduction -============ - -What is |igraph|? -================= - -Things you should know before starting out -========================================== - -Reporting bugs and providing feedback -===================================== - diff --git a/doc/source/misc.rst b/doc/source/misc.rst deleted file mode 100644 index 966896a45..000000000 --- a/doc/source/misc.rst +++ /dev/null @@ -1,5 +0,0 @@ -Miscellaneous topics -==================== - -.. note:: TODO. This is a placeholder section; it is not written yet. - diff --git a/doc/source/requirements.txt b/doc/source/requirements.txt new file mode 100644 index 000000000..ef825d09f --- /dev/null +++ b/doc/source/requirements.txt @@ -0,0 +1,14 @@ +pip +wheel +requests>=2.28.1 + +sphinx==7.4.7 +sphinx-gallery>=0.14.0 +sphinx-rtd-theme>=1.3.0 +pydoctor>=23.4.0 +iplotx>=0.6.8 + +numpy +scipy +pandas +matplotlib diff --git a/doc/source/tutorial.es.rst b/doc/source/tutorial.es.rst new file mode 100644 index 000000000..b5758c413 --- /dev/null +++ b/doc/source/tutorial.es.rst @@ -0,0 +1,810 @@ +.. include:: include/global.rst + +.. _tutorial_es: + +.. currentmodule:: igraph + +================== +Tutorial (Español) +================== + +Esta página es un tutorial detallado de las capacidades de |igraph| para Python. Para obtener una impresión rápida de lo que |igraph| puede hacer, consulte el :doc:`tutorials/quickstart`. Si aún no ha instalado |igraph|, siga las instrucciones de :doc:`install`. + +.. note:: + Para el lector impaciente, vea la página :doc:`tutorials/index` para ejemplos cortos y autocontenidos. + +Comenzar con |igraph| +===================== + +La manera más común de usar |igraph| es como una importanción con nombre dentro de un ambiente de Python (por ejemplo, un simple shell de Python, a `IPython`_ shell, un `Jupyter`_ notebook o una instancia JupyterLab, `Google Colab `_, o un `IDE `_):: + + $ python + 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 as ig + +Para llamar a funciones, es necesario anteponerles el prefijo ``ig`` (o el nombre que hayas elegido):: + + >>> import igraph as ig + >>> print(ig.__version__) + 0.9.8 + +.. note:: + Es posible utilizar *importación con asterisco* para |igraph|:: + + >>> from igraph import * + + pero en general se desaconseja `_. + +Hay una segunda forma de iniciar |igraph|, que consiste en llamar al script :command:`igraph` desde tu terminal:: + + $ igraph + No configuration file, using defaults + 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. + >>> + +.. note:: + Para los usuarios de Windows encontrarán el script dentro del subdirectorio file:`scripts` + de Python y puede que tengan que añadirlo manualmente a su ruta. + +Este script inicia un intérprete de comandos apropiado (`IPython`_ o `IDLE `_ si se encuentra, de lo contrario un intérprete de comandos Python puro) y utiliza *importación con asterisco* (véase más arriba). Esto es a veces conveniente para usar las funciones de |igraph|. + +.. note:: + Puede especificar qué shell debe utilizar este script a través + :doc:`configuration` de |igraph|. + +Este tutorial asumirá que has importado igraph usando el de nombres ``ig``. + +Creando un grafo +================ + +La forma más sencilla de crear un grafo es con el constructor :class:`Graph`. Para hacer un grafo vacío: + + >>> g = ig.Graph() + +Para hacer un grafo con 10 nodos (numerados ``0`` to ``9``) y dos aristas que conecten los nodos ``0-1`` y ``0-5``:: + + >>> g = ig.Graph(n=10, edges=[[0, 1], [0, 5]]) + +Podemos imprimir el grafo para obtener un resumen de sus nodos y aristas:: + + >>> print(g) + IGRAPH U--- 10 2 -- + + edges: + 0--1 0--5 + +Tenemos entonces: grafo no dirigido (**U**ndirected) con **10** vértices y **2** aristas, que se enlistan en la última parte. Si el grafo tiene un atributo "nombre", también se imprime. + +.. note:: + ``summary`` es similar a ``print`` pero no enlista las aristas, lo cual + es conveniente para grafos grandes con millones de aristas:: + + >>> summary(g) + IGRAPH U--- 10 2 -- + +Añadir y borrar vértices y aristas +================================== + +Empecemos de nuevo con un grafo vacío. Para añadir vértices a un grafo existente, utiliza :meth:`Graph.add_vertices`:: + + >>> g = ig.Graph() + >>> g.add_vertices(3) + +En |igraph|, los vértices se numeran siempre a partir de cero El número de un vértice es el *ID del vértice*. Un vértice puede tener o no un nombre. + +Del mismo modo, para añadir aristas se utiliza :meth:`Graph.add_edges`:: + + >>> g.add_edges([(0, 1), (1, 2)]) + +Las aristas se añaden especificando el vértice origen y el vértice destino de cada arista. Esta llamada añade dos aristas, una que conecta los vértices ``0`` y ``1``, y otra que conecta los vértices ``1`` y ``2``. Las aristas también se numeran a partir de cero (el *ID del arista*) y tienen un nombre opcional. + +.. warning:: + + Crear un grafo vacío y añadir vértices y aristas como se muestra aquí puede ser mucho más lento que crear un grafo con sus vértices y aristas como se ha demostrado anteriormente. Si la velocidad es una preocupación, deberías evitar especialmente añadir vértices y aristas *de uno en uno*. Si necesitas hacerlo de todos modos, puedes usar :meth:`Graph.add_vertex` y :meth:`Graph.add_edge`. + +Si intentas añadir aristas a vértices con IDs no válidos (por ejemplo, intentas añadir una arista al vértice ``5`` cuando el grafo sólo tiene tres vértices), obtienes un error :exc:`igraph.InternalError`:: + + >>> g.add_edges([(5, 4)]) + Traceback (most recent call last): + File "", line 1, in + File "/usr/lib/python3.10/site-packages/igraph/__init__.py", line 376, in add_edges + res = GraphBase.add_edges(self, es) + igraph._igraph.InternalError: Error at src/graph/type_indexededgelist.c:270: cannot add edges. -- Invalid vertex id + +El mensaje intenta explicar qué ha fallado (``cannot add edges. -- Invalid +vertex id``) junto con la línea correspondiente del código fuente en la que se ha producido el error. + +.. note:: + El rastreo completo, incluida la información sobre el código fuente, es útil cuando + se informa de errores en nuestro + `Página de problemas de GitHub `_. Por favor, inclúyalo + completo si crea un nuevo asunto. + +Añadamos más vértices y aristas a nuestro grafo:: + + >>> 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 + +Ahora tenemos un grafo no dirigido con 6 vértices y 7 aristas. Los IDs de los vértices y aristas son siempre *continuos*, por lo que si eliminas un vértice todos los vértices subsiguientes serán renumerados. Cuando se renumera un vértice, las aristas **no** se renumeran, pero sí sus vértices de origen y destino. Utilice :meth:`Graph.delete_vertices` y :meth:`Graph.delete_edges` para realizar estas operaciones. Por ejemplo, para eliminar la arista que conecta los vértices ``2-3``, obten sus IDs y luego eliminalos:: + + >>> g.get_eid(2, 3) + 3 + >>> g.delete_edges(3) + +Generar grafos +============== + +|igraph| incluye generadores de grafos tanto deterministas como estocásticos. Los generadores *deterministas* producen el mismo grafo cada vez que se llama a la función, por ejemplo:: + + >>> g = ig.Graph.Tree(127, 2) + >>> summary(g) + IGRAPH U--- 127 126 -- + +Utiliza :meth:`Graph.Tree` para generar un grafo regular en forma de árbol con 127 vértices, cada vértice con dos hijos (y un padre, por supuesto). No importa cuántas veces llames a :meth:`Graph.Tree`, el grafo generado será siempre el mismo si utilizas los mismos parámetros:: + + >>> g2 = ig.Graph.Tree(127, 2) + >>> g2.get_edgelist() == g.get_edgelist() + True + +El fragmento de código anterior también muestra el método :meth:`~Graph.get_edgelist()`, que devuelve una lista de vértices de origen y destino para todas las aristas, ordenados por el ID de la arista. Si imprimes los 10 primeros elementos, obtienes:: + + >>> g2.get_edgelist()[:10] + [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6), (3, 7), (3, 8), (4, 9), (4, 10)] + +Los generadores *estocásticos* producen un grafo diferente cada vez; por ejemplo, :meth:`Graph.GRG`:: + + >>> g = ig.Graph.GRG(100, 0.2) + >>> summary(g) + IGRAPH U---- 100 516 -- + + attr: x (v), y (v) + +.. note:: + `+ attr`` muestra atributos para vértices (v) y aristas (e), en este caso dos atributos de + vértice y ningún atributo de arista. + +Esto genera un grafo geométrico aleatorio: Se eligen *n* puntos de forma aleatoria y uniforme dentro del cuadrado unitario y los pares de puntos más cercanos entre sí respecto a una distancia predefinida *d* se conectan mediante una arista. Si se generan GRGs con los mismos parámetros, serán diferentes:: + + >>> g2 = ig.Graph.GRG(100, 0.2) + >>> g.get_edgelist() == g2.get_edgelist() + False + +Una forma un poco más relajada de comprobar si los grafos son equivalentes es mediante :meth:`~Graph.isomorphic()`:: + + >>> g.isomorphic(g2) + False + +Comprobar por el isomorfismo puede llevar un tiempo en el caso de grafos grandes (en este caso, la respuesta puede darse rápidamente comprobando las distribuciones de grados de los dos grafos). + +Establecer y recuperar atributos +================================ + +Como se ha mencionado anteriormente, en |igraph| cada vértice y cada arista tienen un ID numérico de ``0`` en adelante. Por lo tanto, la eliminación de vértices o aristas puede causar la reasignación de los ID de vértices y/o aristas. Además de los IDs, los vértices y aristas pueden tener *atributos* como un nombre, coordenadas para graficar, metadatos y pesos. El propio grafo puede tener estos atributos también (por ejemplo, un nombre, que se mostrará en ``print`` o :meth:`Graph.summary`). En cierto sentido, cada :class:`Graph`, vértice y arista pueden utilizarse como un diccionario de Python para almacenar y recuperar estos atributos. + +Para demostrar el uso de los atributos, creemos una red social sencilla:: + + >>> g = ig.Graph([(0,1), (0,2), (2,3), (3,4), (4,2), (2,5), (5,0), (6,3), (5,6)]) + +Cada vértice representa una persona, por lo que queremos almacenar nombres, edades y géneros:: + + >>> 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] + +:attr:`Graph.vs` y :attr:`Graph.es` son la forma estándar de obtener una secuencia de todos los vértices y aristas respectivamente. El valor debe ser una lista con la misma longitud que los vértices (para :attr:`Graph.vs`) o aristas (para :attr:`Graph.es`). Esto asigna un atributo a *todos* los vértices/aristas a la vez. + +Para asignar o modificar un atributo para un solo vértice/borde, puedes hacer lo siguiente:: + + >>> g.es[0]["is_formal"] = True + +De hecho, un solo vértice se representa mediante la clase :class:`Vertex`, y una sola arista mediante :class:`Edge`. Ambos, junto con :class:`Graph`, pueden ser tecleados como un diccionario para establecer atributos, por ejemplo, para añadir una fecha al grafo:: + + >>> g["date"] = "2009-01-10" + >>> print(g["date"]) + 2009-01-10 + +Para recuperar un diccionario de atributos, puedes utilizar :meth:`Graph.attributes`, :meth:`Vertex.attributes` y :meth:`Edge.attributes`. + +Además, cada :class:`Vertex` tiene una propiedad especial, :attr:`Vertex.index`, que se utiliza para averiguar el ID de un vértice. Cada :class:`Edge` tiene :attr:`Edge.index` más dos propiedades adicionales, :attr:`Edge.source` y :attr:`Edge.target`, que se utilizan para encontrar los IDs de los vértices conectados por esta arista. Para obtener ambas propiedades a la vez, puedes utilizar :attr:`Edge.tuple`. + +Para asignar atributos a un subconjunto de vértices o aristas, puedes utilizar el corte:: + + >>> g.es[:1]["is_formal"] = True + +La salida de ``g.es[:1]`` es una instancia de :class:`~seq.EdgeSeq`, mientras que :class:`~seq.VertexSeq` es la clase equivalente que representa subconjuntos de vértices. + +Para eliminar atributos, puedes utilizar ``del``, por ejemplo:: + + >>> 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' + +.. warning:: + Los atributos pueden ser objetos arbitrarios de Python, pero si está guardando grafos en un + archivo, sólo se conservarán los atributos de cadena ("string") y numéricos. Consulte el + módulo :mod:`pickle` de la biblioteca estándar de Python si busca una forma de guardar otros + tipos de atributos. Puede hacer un pickle de sus atributos individualmente, almacenarlos como + cadenas y guardarlos, o puedes hacer un pickle de todo el :class:`Graph` si sabes que quieres + cargar el grafo en Python. + + +Propiedades estructurales de los grafos +======================================= + +Además de las funciones simples de manipulación de grafos y atributos descritas anteriormente, |igraph| proporciona un amplio conjunto de métodos para calcular varias propiedades estructurales de los grafos. Está más allá del alcance de este tutorial documentar todos ellos, por lo que esta sección sólo presentará algunos de ellos con fines ilustrativos. Trabajaremos con la pequeña red social que construimos en la sección anterior. + +Probablemente, la propiedad más sencilla en la que se puede pensar es el "grado del vértice" (:dfn:`vertex degree`). El grado de un vértice es igual al número de aristas incidentes a él. En el caso de los grafos dirigidos, también podemos definir el ``grado de entrada`` (:dfn:`in-degree`, el número de aristas que apuntan hacia el vértice) y el ``grado de salida`` (:dfn:`out-degree`, el número de aristas que se originan en el vértice):: + + >>> g.degree() + [3, 1, 4, 3, 2, 3, 2] + +Si el grafo fuera dirigido, habríamos podido calcular los grados de entrada y salida por separado utilizando ``g.degree(mode="in")`` y ``g.degree(mode="out")``. También puedes usar un único ID de un vértice o una lista de ID de los vértices a :meth:`~Graph.degree` si quieres calcular los grados sólo para un subconjunto de vértices:: + + >>> g.degree(6) + 2 + >>> g.degree([2,3,4]) + [4, 3, 2] + +Este procedimiento se aplica a la mayoría de las propiedades estructurales que |igraph| puede calcular. Para las propiedades de los vértices, los métodos aceptan un ID o una lista de IDs de los vértices (y si se omiten, el valor predeterminado es el conjunto de todos los vértices). Para las propiedades de las aristas, los métodos también aceptan un único ID de o una lista de IDs de aristas. En lugar de una lista de IDs, también puedes proporcionar una instancia :class:`VertexSeq` o una instancia :class:`EdgeSeq` apropiadamente. Más adelante, en el próximo capítulo "consulta de vértices y aristas", aprenderás a restringirlos exactamente a los vértices o aristas que quieras. + +.. note:: + + Para algunos casos, no tiene sentido realizar el calculo sólo para unos pocos vértices o + aristas en lugar de todo el grafo, ya que de todas formas se tardaría el mismo tiempo. En + este caso, los métodos no aceptan IDs de vértices o aristas, pero se puede restringir la + lista resultante más tarde usando operadores estándar de indexación y de corte. Un ejemplo de + ello es la centralidad de los vectores propios (:meth:`Graph.evcent()`) + +Además de los grados, |igraph| incluye rutinas integradas para calcular muchas otras propiedades de centralidad, como la intermediación de vértices y aristas o el PageRank de Google (:meth:`Graph.pagerank`), por nombrar algunas. Aquí sólo ilustramos la interrelación de aristas:: + + >>> g.edge_betweenness() + [6.0, 6.0, 4.0, 2.0, 4.0, 3.0, 4.0, 3.0. 4.0] + +Ahora también podemos averiguar qué conexiones tienen la mayor centralidad de intermediación +con un poco de magia de Python:: + + >>> 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)] + +La mayoría de las propiedades estructurales también pueden ser obtenidas para un subconjunto de vértices o aristas o para un solo vértice o arista llamando al método apropiado de la clase :class:`VertexSeq` o :class:`EdgeSeq` de interés:: + + >>> 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 + +Busqueda de vértices y aristas basada en atributos +================================================== + +Selección de vértices y aristas +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Tomando como ejemplo la red social anterirormente creada, te gustaría averiguar quién tiene el mayor grado o centralidad de intermediación. Puedes hacerlo con las herramientas presentadas hasta ahora y conocimientos básicos de Python, pero como es una tarea común seleccionar vértices y aristas basándose en atributos o propiedades estructurales, |igraph| te ofrece una forma más fácil de hacerlo:: + + >>> g.vs.select(_degree=g.maxdegree())["name"] + ['Claire'] + +La sintaxis puede parecer un poco rara a primera vista, así que vamos a tratar de interpretarla paso a paso. meth:`~VertexSeq.select` es un método de :class:`VertexSeq` y su único propósito es filtrar un :class:`VertexSeq` basándose en las propiedades de los vértices individuales. La forma en que filtra los vértices depende de sus argumentos posicionales y de palabras clave. Los argumentos posicionales (los que no tienen un nombre explícito como ``_degree`` siempre se procesan antes que los argumentos de palabra clave de la siguiente manera: + +- Si el primer argumento posicional es ``None``, se devuelve una secuencia vacía (que no contiene vértices):: + + >>> seq = g.vs.select(None) + >>> len(seq) + 0 + +- Si el primer argumento posicional es un objeto invocable (es decir, una función, un método vinculado o cualquier cosa que se comporte como una función), el objeto será llamado para cada vértice que esté actualmente en la secuencia. Si la función devuelve ``True``, el vértice será incluido, en caso contrario será excluido:: + + >>> graph = ig.Graph.Full(10) + >>> only_odd_vertices = graph.vs.select(lambda vertex: vertex.index % 2 == 1) + >>> len(only_odd_vertices) + 5 + +- Si el primer argumento posicional es un iterable (es decir, una lista, un generador o cualquier cosa sobre la que se pueda iterar), *debe* devolver enteros y estos enteros se considerarán como índices del conjunto de vértices actual (que *no* es necesariamente todo el grafo). Sólo se incluirán en el conjunto de vértices filtrados los vértices que coincidan con los índices dados. Los numero flotantes, las cadenas y los ID de vértices no válidos seran omitidos:: + + >>> 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 + +- Si el primer argumento posicional es un número entero, se espera que todos los demás argumentos sean también números enteros y se interpretan como índices del conjunto de vértices actual. Esto solo es "azucar sintáctica", se podría conseguir un efecto equivalente pasando una lista como primer argumento posicional, de esta forma se pueden omitir los corchetes:: + + >>> seq = graph.vs.select(2, 3, 7) + >>> len(seq) + 3 + +Los argumentos clave ("keyword argument") pueden utilizarse para filtrar los vértices en función de sus atributos o sus propiedades estructurales. El nombre de cada argumento clave consiste como máximo de dos partes: el nombre del atributo o propiedad estructural y el operador de filtrado. El operador puede omitirse; en ese caso, automáticamente se asume el operador de igualdad. Las posibilidades son las siguientes (donde *name* indica el nombre del atributo o propiedad): + +================ ================================================================ +Keyword argument Significado +================ ================================================================ +``name_eq`` El valor del atributo/propiedad debe ser *igual* a +---------------- ---------------------------------------------------------------- +``name_ne`` El valor del atributo/propiedad debe *no ser igual* a +---------------- ---------------------------------------------------------------- +``name_lt`` El valor del atributo/propiedad debe ser *menos* que +---------------- ---------------------------------------------------------------- +``name_le`` El valor del atributo/propiedad debe ser *inferior o igual a* +---------------- ---------------------------------------------------------------- +``name_gt`` El valor del atributo/propiedad debe ser *mayor que* +---------------- ---------------------------------------------------------------- +``name_ge`` El valor del atributo/propiedad debe ser *mayor o igual a* +---------------- ---------------------------------------------------------------- +``name_in`` El valor del atributo/propiedad debe estar *incluido en*, el cual tiene que ser + una secuencia en este caso +---------------- ---------------------------------------------------------------- +``name_notin`` El valor del atributo/propiedad debe *no estar incluido en* , + el cual tiene que ser una secuencia en este caso +================ ================================================================ + +Por ejemplo, el siguiente comando te da las personas menores de 30 años en nuestra red social imaginaria:: + + >>> g.vs.select(age_lt=30) + +.. note:: + Debido a las restricciones sintácticas de Python, no se puede utilizar la sintaxis más + sencilla de ``g.vs.select(edad < 30)``, ya que en Python sólo se permite que aparezca el + operador de igualdad en una lista de argumentos. + +Para ahorrarte algo de tecleo, puedes incluso omitir el método :meth:`~VertexSeq.select` si +desea:: + + >>> g.vs(age_lt=30) + +También hay algunas propiedades estructurales especiales para seleccionar los aristas: + +- Utilizando ``_source`` or ``_from`` en función de los vértices de donde se originan las aristas. Por ejemplo, para seleccionar todas las aristas procedentes de Claire (que tiene el índice de vértice 2):: + + >>> g.es.select(_source=2) + +- Usar los filtros ``_target`` o ``_to`` en base a los vértices de destino. Esto es diferente de ``_source`` and ``_from`` si el grafo es dirigido. + +- ``_within`` toma un objeto :class:`VertexSeq` o un set de vértices y selecciona todos los aristas que se originan y terminan en un determinado set de vértices. Por ejemplo, la siguiente expresión selecciona todos los aristas entre Claire (índice 2), Dennis (índice 3) y Esther (índice 4):: + + >>> g.es.select(_within=[2,3,4]) + +- ``_between`` toma una tupla que consiste en dos objetos :class:`VertexSeq` o una listas que contienen los indices de los vértices o un objeto :class:`Vertex` y selecciona todas las aristas que se originan en uno de los conjuntos y terminan en el otro. Por ejemplo, para seleccionar todas las aristas que conectan a los hombres con las mujeres:: + + >>> men = g.vs.select(gender="m") + >>> women = g.vs.select(gender="f") + >>> g.es.select(_between=(men, women)) + +Encontrar un solo vértice o arista con algunas propiedades +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +En muchos casos buscamos un solo vértice o arista de un grafo con algunas propiedades, sin importar cuál de las coincidencias se devuelve, ya sea si éxiste mútliples coincidencias, o bien sabemos de antemano que sólo habrá una coincidencia. Un ejemplo típico es buscar vértices por su nombre en la propiedad ``name``. Los objetos :class:`VertexSeq` y :class:`EdgeSeq` proveen el método :meth:`~VertexSeq.find` para esos casos. Esté método funciona de manera similar a :meth:`~VertexSeq.select`, pero devuelve solo la primer coincidencia si hay multiples resultados, y señala una excepción si no se encuentra ninguna coincidencia. Por ejemplo, para buscar el vértice correspondiente a Claire, se puede hacer lo siguiente:: + + >>> claire = g.vs.find(name="Claire") + >>> type(claire) + igraph.Vertex + >>> claire.index + 2 + +La búsqueda de un nombre desconocido dará lugar a una excepción:: + + >>> g.vs.find(name="Joe") + Traceback (most recent call last): + File "", line 1, in + ValueError: no such vertex + +Búsqueda de vértices por nombres +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Buscar vértices por su nombre es una operación muy común, y normalmente es mucho más fácil recordar los nombres de los vértices de un grafo que sus IDs. Para ello, |igraph| trata el atributo ``name`` de los vértices de forma especial; se indexan de forma que los vértices se pueden buscar por sus nombre. Para hacer las cosas incluso más fácil, |igraph| acepta nombres de vértices (casi) en cualquier lugar dónde se espere especificar un ID de un vérice, e incluso, acepta colecciones (tuplas,listas,etc.) de nombres de vértices dónde sea que se esperé una lista de IDs de vértices. Por ejemplo, puedes buscar el grado (número de conexiones) de Dennis de la siguiente manera:: + + >>> g.degree("Dennis") + 3 + +o alternativamente:: + + >>> g.vs.find("Dennis").degree() + 3 + +El mapeo entre los nombres de los vértices y los IDs es mantenido de forma transparente por |igraph| en segundo plano; cada vez que el grafo cambia, |igraph| también actualiza el mapeo interno. Sin embargo, la singularidad de los nombres de los vértices *no* se impone; puedes crear fácilmente un grafo en el que dos vértices tengan el mismo nombre, pero igraph sólo devolverá uno de ellos cuando los busques por nombres, el otro sólo estará disponible por su índice. + +Tratar un grafo como una matriz de adyacencia +============================================= + +La matriz de adyacencia es otra forma de formar un grafo. En la matriz de adyacencia, las filas y columnas están etiquetadas por los vértices del grafo: los elementos de la matriz indican si los vértices *i* y *j* tienen una arista común (*i, j*). La matriz de adyacencia del grafo de nuestra red social imaginaria es:: + + >>> 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] + ]) + +Por ejemplo, Claire (``[1, 0, 0, 1, 1, 1, 0]``) está directamente conectada con Alice (que tiene el índice 0), Dennis (índice 3), Esther (índice 4) y Frank (índice 5), pero no con Bob (índice 1) ni con George (índice 6). + +Diseños ("layouts") y graficar +============================== + +Un grafo es un objeto matemático abstracto sin una representación específica en el espacio 2D o 3D. Esto significa que cuando queremos visualizar un grafo, tenemos que encontrar primero un trazado de los vértices a las coordenadas en el espacio bidimensional o tridimensional, preferiblemente de una manera que sea agradable a la vista. Una rama separada de la teoría de grafos, denominada dibujo de grafos, trata de resolver este problema mediante varios algoritmos de disposición de grafos. igraph implementa varios algoritmos de diseño y también es capaz de dibujarlos en la pantalla o en un archivo PDF, PNG o SVG utilizando la `libreria Cairo `_. + +.. important:: + + Para seguir los ejemplos de esta sección, se requieren de la librería Cairo en Python o + matplotlib. + +Algoritmos de diseños ("layouts") +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Los métodos de diseño en |igraph| se encuentran en el objeto :class:`Graph`, y siempre comienzan con ``layout_``. La siguiente tabla los resume: + +==================================== =============== ============================================= +Method name Short name Algorithm description +==================================== =============== ============================================= +``layout_circle`` ``circle``, Disposición determinista que coloca los + ``circular`` vértices en un círculo +------------------------------------ --------------- --------------------------------------------- +``layout_drl`` ``drl`` El algoritmo [Distributed Recursive Layout] + para grafos grandes +------------------------------------ --------------- --------------------------------------------- +``layout_fruchterman_reingold`` ``fr`` El algoritmo dirigido Fruchterman-Reingold +------------------------------------ --------------- --------------------------------------------- +``layout_fruchterman_reingold_3d`` ``fr3d``, El algoritmo dirigido Fruchterman-Reingold + ``fr_3d`` en tres dimensiones +------------------------------------ --------------- --------------------------------------------- +``layout_kamada_kawai`` ``kk`` El algoritmo dirigido Kamada-Kawai +------------------------------------ --------------- --------------------------------------------- +``layout_kamada_kawai_3d`` ``kk3d``, El algoritmo dirigido Kamada-Kawai + ``kk_3d`` en tres dimensiones +------------------------------------ --------------- --------------------------------------------- +``layout_lgl`` ``large``, El algoritmo [Large Graph Layout] para + ``lgl``, grafos grandes + ``large_graph`` +------------------------------------ --------------- --------------------------------------------- +``layout_random`` ``random`` Coloca los vértices de forma totalmente aleatoria +------------------------------------ --------------- --------------------------------------------- +``layout_random_3d`` ``random_3d`` Coloca los vértices de forma totalmente aleatoria en 3D +------------------------------------ --------------- --------------------------------------------- +``layout_reingold_tilford`` ``rt``, Diseño de árbol de Reingold-Tilford, útil + ``tree`` para grafos (casi) arbóreos +------------------------------------ --------------- --------------------------------------------- +``layout_reingold_tilford_circular`` ``rt_circular`` Diseño de árbol de Reingold-Tilford con una + post-transformación de coordenadas polares, + ``tree`` útil para grafos (casi) arbóreos +------------------------------------ --------------- --------------------------------------------- +``layout_sphere`` ``sphere``, Disposición determinista que coloca los vértices + ``spherical``, de manera uniforme en la superficie de una esfera + ``circular_3d`` +==================================== =============== ============================================= + +.. _Distributed Recursive Layout: https://www.osti.gov/doecode/biblio/54626 +.. _Large Graph Layout: https://sourceforge.net/projects/lgl/ + +Los algoritmos de diseño pueden ser llamados directamente o utilizando :meth:`~Graph.layout`:: + + >>> layout = g.layout_kamada_kawai() + >>> layout = g.layout("kamada_kawai") + +El primer argumento del método :meth:`~Graph.layout` debe ser el nombre corto del algoritmo de diseño (mirar la tabla anterior). Todos los demás argumentos posicionales y de palabra clave se pasan intactos al método de diseño elegido. Por ejemplo, las dos llamadas siguientes son completamente equivalentes:: + + >>> layout = g.layout_reingold_tilford(root=[2]) + >>> layout = g.layout("rt", [2]) + +Los métodos de diseño devuelven un objeto :class:`~layout.Layout` que se comporta principalmente como una lista de listas. Cada entrada de la lista en un objeto :class:`~layout.Layout` corresponde a un vértice en el grafo original y contiene las coordenadas del vértice en el espacio 2D o 3D. Los objetos :class:`~layout.Layout` también contienen algunos métodos útiles para traducir, escalar o rotar las coordenadas en un lote. Sin embargo, la principal utilidad de los objetos :class:`~layout.Layout` es que puedes pasarlos a la función :func:`~drawing.plot` junto con el grafo para obtener un dibujo en 2D. + +Dibujar un grafo utilizando un diseño ("layout") +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Por ejemplo, podemos trazar nuestra red social imaginaria con el algoritmo de distribución Kamada-Kawai de la siguiente manera:: + + >>> layout = g.layout("kk") + >>> ig.plot(g, layout=layout) + +Esto debería abrir un visor de imágenes externo que muestre una representación visual de la red, algo parecido a lo que aparece en la siguiente figura (aunque la colocación exacta de los nodos puede ser diferente en su máquina, ya que la disposición no es determinista): + +.. figure:: figures/tutorial_social_network_1.png + :alt: The visual representation of our social network (Cairo backend) + :align: center + +Nuestra red social con el algoritmo de distribución Kamada-Kawai + +Si prefiere utilizar `matplotlib`_ como motor de trazado, cree un eje y utilice el argumento ``target``:: + + >>> import matplotlib.pyplot as plt + >>> fig, ax = plt.subplots() + >>> ig.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, esto no es demasiado bonito hasta ahora. Una adición trivial sería usar los nombres como etiquetas de los vértices y colorear los vértices según el género. Las etiquetas de los vértices se toman del atributo ``label`` por defecto y los colores de los vértices se determinan por el atributo ``color``:: + + >>> g.vs["label"] = g.vs["name"] + >>> color_dict = {"m": "blue", "f": "pink"} + >>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] + >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20) # Cairo backend + >>> ig.plot(g, layout=layout, target=ax) # matplotlib backend + +Tenga en cuenta que aquí simplemente estamos reutilizando el objeto de diseño anterior, pero también hemos especificado que necesitamos un gráfico más pequeño (300 x 300 píxeles) y un margen mayor alrededor del grafo para que quepan las etiquetas (20 píxeles). El resultado es: + +.. figure:: figures/tutorial_social_network_2.png + :alt: The visual representation of our social network - with names and genders + :align: center + +Nuestra red social - con nombres como etiquetas y géneros como colores + +y para matplotlib: + +.. figure:: figures/tutorial_social_network_2_mpl.png + :alt: The visual representation of our social network - with names and genders + :align: center + +En lugar de especificar las propiedades visuales como atributos de vértices y aristas, también puedes darlas como argumentos a :func:`~drawing.plot`:: + + >>> color_dict = {"m": "blue", "f": "pink"} + >>> ig.plot(g, layout=layout, vertex_color=[color_dict[gender] for gender in g.vs["gender"]]) + +Este último enfoque es preferible si quiere mantener las propiedades de la representación visual de su gráfico separadas del propio gráfico. Puedes simplemente crear un diccionario de Python que contenga los argumentos que contenga las palabras clave que pasarias a la función :func:`~drawing.plot` y luego usar el doble asterisco (``**``) para pasar tus atributos de estilo específicos a :func:`~drawing.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 + >>> ig.plot(g, **visual_style) + +El gráfico final muestra los vínculos formales con líneas gruesas y los informales con líneas finas: + +.. figure:: figures/tutorial_social_network_3.png + :alt: The visual representation of our social network - with names, genders and formal ties + :align: center + + Nuestra red social - también muestra qué vínculos son formales + +Para resumirlo todo: hay propiedades especiales de vértices y aristas que corresponden a la representación visual del grafo. Estos atributos anulan la configuración por defecto de |igraph| (es decir, el color, el peso, el nombre, la forma, el diseño, etc.). Las dos tablas siguientes resumen los atributos visuales más utilizados para los vértices y las aristas, respectivamente: + +Atributos de los vértices que controlan los gráficos +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +=============== ====================== ========================================== +Attribute name Keyword argument Purpose +=============== ====================== ========================================== +``color`` ``vertex_color`` Color del vertice +--------------- ---------------------- ------------------------------------------ +``font`` ``vertex_font`` Familia tipográfica del vértice +--------------- ---------------------- ------------------------------------------ +``label`` ``vertex_label`` Etiqueta del vértice. +--------------- ---------------------- ------------------------------------------ +``label_angle`` ``vertex_label_angle`` Define la posición de las etiquetas de los + vértices, en relación con el centro de los + mismos. Se interpreta como un ángulo en + radianes, cero significa 'a la derecha'. +--------------- ---------------------- ------------------------------------------ +``label_color`` ``vertex_label_color`` Color de la etiqueta del vértice +--------------- ---------------------- ------------------------------------------ +``label_dist`` ``vertex_label_dist`` Distancia de la etiqueta del vértice, + en relación con el tamaño del vértice +--------------- ---------------------- ------------------------------------------ +``label_size`` ``vertex_label_size`` Tamaño de letra de la etiqueta de vértice +--------------- ---------------------- ------------------------------------------ +``order`` ``vertex_order`` Orden de dibujo de los vértices. Vértices + con un parámetro de orden menor se + dibujarán primero. +--------------- ---------------------- ------------------------------------------ +``shape`` ``vertex_shape`` La forma del vértice,. Algunas formas: + ``rectangle``, ``circle``, ``hidden``, + ``triangle-up``, ``triangle-down``. Ver + :data:`drawing.known_shapes`. +--------------- ---------------------- ------------------------------------------ +``size`` ``vertex_size`` El tamaño del vértice en pixels +=============== ====================== ========================================== + +Atributos de las aristas que controlan los gráficos +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +=============== ====================== ========================================== +Attribute name Keyword argument Purpose +=============== ====================== ========================================== +``color`` ``edge_color`` Color de la arista. +--------------- ---------------------- ------------------------------------------ +``curved`` ``edge_curved`` La curvatura de la arista. Valores positivos + corresponden a aristas curvadas en sentido + contrario a las manecillas del reloj, valores + negativos lo contrario. Una curvatura cero + representa aristas rectas. ``True`` significa + una curvatura de 0.5, ``False`` es una + curvatura de cero. +--------------- ---------------------- ------------------------------------------ +``font`` ``edge_font`` Familia tipográfica del arista. +--------------- ---------------------- ------------------------------------------ +``arrow_size`` ``edge_arrow_size`` Tamaño (longitud) de la punta de flecha del + arista si el grafo es dirigido, relativo a + 15 pixels. +--------------- ---------------------- ------------------------------------------ +``arrow_width`` ``edge_arrow_width`` El ancho de las flechas. Relativo a 10 + pixels. +--------------- ---------------------- ------------------------------------------ +``loop_size`` ``edge_loop_size`` Tamaño de los bucles. Puede ser negativo + para escalar con el tamaño del vertice + correspondiente. Este atributo no + es utilizado para otras aristas. Este + atributo sólo existe en el backend + matplotlib. +--------------- ---------------------- ------------------------------------------ +``width`` ``edge_width`` Anchura del borde en píxeles. +--------------- ---------------------- ------------------------------------------ +``label`` ``edge_label`` Si se especifica, añade una etiqueta al borde. +--------------- ---------------------- ------------------------------------------ +``background`` ``edge_background`` Si se especifica, añade una caja rectangular + alrededor de la etiqueta de borde (solo en + matplotlib). +--------------- ---------------------- ------------------------------------------ +``align_label`` ``edge_align_label`` Si es verdadero, gira la etiqueta de la + arista de forma que se alinee con la + dirección de la arista. Las etiquetas que + estarían al revés se voltean (sólo matplotlib). +=============== ====================== ========================================== + +Argumentos genéricos de ``plot()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Estos ajustes se pueden especificar como argumentos de palabra clave a la función ``plot`` para controlar la apariencia general del gráfico. + +================ ================================================================ +Keyword argument Purpose +================ ================================================================ +``autocurve`` Determinación automática de la curvatura de las aristas en grafos + con múltiples aristas. El estandar es ``True`` para grafos + con menos de 10000 aristas y ``False`` para el caso contrario. +---------------- ---------------------------------------------------------------- +``bbox`` La caja delimitadora del gráfico. Debe ser una tupla que contenga + la anchura y la altura deseadas del gráfico. Por default el gráfico + tiene 600 pixels de ancho y 600 pixels de largo. +---------------- ---------------------------------------------------------------- +``layout`` El diseño que se va a utilizar. Puede ser una instancia de ``layout`` + una lista de tuplas que contengan coordenadas X-Y, o el nombre + un algoritmo de diseño. El valor por defecto es ``auto``, que + selecciona un algoritmo de diseño automáticamente basado en el tamaño + y la conectividad del grafo. +---------------- ---------------------------------------------------------------- +``margin`` La cantidad de espacio vacío debajo, encima, a la izquierda y + a la derecha del gráfico. +================ ================================================================ + +Especificación de colores en los gráficos +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|igraph| entiende las siguientes especificaciones de color siempre que espera un color (por ejemplo, colores de aristas, vértices o etiquetas en los respectivos atributos): + +***Nombres de colores X11*** + +Consulta la `lista de nombres de colores X11 `_ en Wikipedia para ver la lista completa. Los nombres de los colores no distinguen entre mayúsculas y minúsculas en |igraph|, por lo que ``"DarkBLue"`` puede escribirse también como ``"darkblue"``. + +***Especificación del color en la sintaxis CSS*** + +Se trata de una cadena según uno de los siguientes formatos (donde *R*, *G* y *B* denotan los componentes rojo, verde y azul, respectivamente): + +- ``#RRGGBB``, los componentes van de 0 a 255 en formato hexadecimal. Ejemplo: ``"#0088ff"`` +- ``#RGB``, los componentes van de 0 a 15 en formato hexadecimal. Ejemplo: ``"#08f"`` +- ``rgb(R, G, B)``, los componentes van de 0 a 255 o de 0% a 100%. Ejemplo: ``"rgb(0, 127, 255)"`` o ``"rgb(0%, 50%, 100%)"``. + +Guardar gráficos +^^^^^^^^^^^^^^^^ + +|igraph| puede usarse para crear gráficos de calidad de publicación solicitando la función :func:`~drawing.plot` que guarde el gráfico en un archivo en lugar de mostrarlo en pantalla. Para ello, basta con pasar el nombre del archivo destino como argumento adicional después del grafo mismo. El formato preferido se deduce de la extensión. |igraph| puede guardar en cualquier cosa que soporte Cairo, incluyendo archivos SVG, PDF y PNG. Los archivos SVG o PDF pueden ser convertidos posteriormente al formato PostScript (``.ps``) o PostScript encapsulado (``.eps``) si lo prefieres, mientras que los archivos PNG pueden ser convertidos a TIF (``.tif``):: + + >>> ig.plot(g, "social_network.pdf", **visual_style) + +Si estas usando matplotlib, puedes guardar el gŕafico como de costumbre:: + + >>> fig, ax = plt.subplots() + >>> ig.plot(g, **visual_style) + >>> fig.savefig("social_network.pdf") + +Muchos formatos de archivos son admitidos por matplotlib. + +|igraph| y el mundo exterior +============================ + +Ningún módulo de grafos estaría completo sin algún tipo de funcionalidad de importación/exportación que permita al paquete comunicarse con programas y kits de herramientas externos. |igraph| no es una excepción: proporciona funciones para leer los formatos de grafos más comunes y para guardar objetos :class:`Graph` en archivos que obedezcan estas especificaciones de formato. La siguiente tabla resume los formatos que igraph puede leer o escribir: + +================ ============= ============================ ============================= +Format Short name Reader method Writer method +================ ============= ============================ ============================= +Adjacency list ``lgl`` :meth:`Graph.Read_Lgl` :meth:`Graph.write_lgl` +(a.k.a. `LGL`_) +---------------- ------------- ---------------------------- ----------------------------- +Adjacency matrix ``adjacency`` :meth:`Graph.Read_Adjacency` :meth:`Graph.write_adjacency` +---------------- ------------- ---------------------------- ----------------------------- +DIMACS ``dimacs`` :meth:`Graph.Read_DIMACS` :meth:`Graph.write_dimacs` +---------------- ------------- ---------------------------- ----------------------------- +DL ``dl`` :meth:`Graph.Read_DL` not supported yet +---------------- ------------- ---------------------------- ----------------------------- +Edge list ``edgelist``, :meth:`Graph.Read_Edgelist` :meth:`Graph.write_edgelist` + ``edges``, + ``edge`` +---------------- ------------- ---------------------------- ----------------------------- +`GraphViz`_ ``graphviz``, not supported yet :meth:`Graph.write_dot` + ``dot`` +---------------- ------------- ---------------------------- ----------------------------- +GML ``gml`` :meth:`Graph.Read_GML` :meth:`Graph.write_gml` +---------------- ------------- ---------------------------- ----------------------------- +GraphML ``graphml`` :meth:`Graph.Read_GraphML` :meth:`Graph.write_graphml` +---------------- ------------- ---------------------------- ----------------------------- +Gzipped GraphML ``graphmlz`` :meth:`Graph.Read_GraphMLz` :meth:`Graph.write_graphmlz` +---------------- ------------- ---------------------------- ----------------------------- +LEDA ``leda`` not supported yet :meth:`Graph.write_leda` +---------------- ------------- ---------------------------- ----------------------------- +Labeled edgelist ``ncol`` :meth:`Graph.Read_Ncol` :meth:`Graph.write_ncol` +(a.k.a. `NCOL`_) +---------------- ------------- ---------------------------- ----------------------------- +`Pajek`_ format ``pajek``, :meth:`Graph.Read_Pajek` :meth:`Graph.write_pajek` + ``net`` +---------------- ------------- ---------------------------- ----------------------------- +Pickled graph ``pickle`` :meth:`Graph.Read_Pickle` :meth:`Graph.write_pickle` +================ ============= ============================ ============================= + +.. _GraphViz: https://www.graphviz.org +.. _LGL: https://lgl.sourceforge.net/#FileFormat +.. _NCOL: https://lgl.sourceforge.net/#FileFormat +.. _Pajek: http://mrvar.fdv.uni-lj.si/pajek/ + +Como ejercicio, descarga la representación gráfica del conocido `Estudio del club de karate de Zacarías `_ en formato graphml. Dado que se trata de un archivo GraphML, debe utilizar el método de lectura GraphML de la tabla anterior (asegúrese de utilizar la ruta adecuada al archivo descargado):: + + >>> karate = ig.Graph.Read_GraphML("zachary.graphml") + >>> ig.summary(karate) + IGRAPH UNW- 34 78 -- Zachary's karate club network + +Si quieres convertir el mismo grafo a, digamos, el formato de Pajek, puedes hacerlo con el método de la tabla anterior:: + + >>> karate.write_pajek("zachary.net") + +.. note:: + La mayoría de los formatos tienen sus propias limitaciones; por ejemplo, no todos pueden + almacenar atributos. Tu mejor opción es probablemente GraphML o GML si quieres guardar los + grafos de |igraph| en un formato que pueda ser leído desde un paquete externo y quieres + preservar los atributos numéricos y de cadena. La lista de aristas y NCOL también están bien + si no tienes atributos (aunque NCOL soporta nombres de vértices y pesos de aristas). Si no + quieres utilizar grafos fuera de |igraph|, pero quieres almacenarlos para una sesión + posterior, el formato de grafos ``pickled`` te garantza que obtendras exactamente el mismo + grafo. El formato de grafos ``pickled`` usa el modulo ``pickle`` de Python para guardar y + leer grafos. + +También existen dos métodos de ayuda: :func:`read` es un punto de entrada genérico para los métodos de lectura que intenta deducir el formato adecuado a partir de la extensión del archivo. :meth:`Graph.write` es lo contrario de :func:`read`: permite guardar un grafo en el que el formato preferido se deduce de nuevo de la extensión. La detección del formato de :func:`read` y :meth:`Graph.write` se puede anular mediante el argumento ``format`` de la palabra clave ("keyword"), la cual acepta los nombres cortos de los otros formatos de la tabla anterior:: + + >>> karate = ig.load("zachary.graphml") + >>> karate.write("zachary.net") + >>> karate.write("zachary.my_extension", format="gml") + +Dónde ir a continuación +======================= + +Este tutorial sólo ha arañado la superficie de lo que |igraph| puede hacer. Los planes a largo plazo son ampliar este tutorial para convertirlo en una documentación adecuada de estilo manual para igraph en los próximos capítulos. Un buen punto de partida es la documentación de la clase `Graph`. Si te quedas atascado, intenta preguntar primero en nuestro `Discourse group`_ - quizás haya alguien que pueda ayudarte inmediatamente. + +.. _Discourse group: https://igraph.discourse.group +.. _matplotlib: https://matplotlib.org/ +.. _IPython: https://ipython.readthedocs.io/en/stable/ +.. _Jupyter: https://jupyter.org/ diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 25068918c..7ba2de653 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -1,275 +1,259 @@ .. include:: include/global.rst -.. Tutorial +.. _tutorial: + +.. currentmodule:: igraph ======== Tutorial ======== -This chapter contains a short overview of |igraph|'s capabilities. It is highly recommended -to read it at least once if you are new to |igraph|. I assume that you have already installed -|igraph|; if 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. Mark Pilgrim's -`Dive Into Python `_ is one that I personally suggest. -If this is the first time you ever try to use a programming language, -`A Byte of Python `_ is even better. 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. +This page is a detailed tutorial of |igraph|'s Python capabilities. To get an quick impression of what |igraph| can do, check out the :doc:`tutorials/quickstart`. If you have not installed |igraph| yet, follow the section titled :doc:`install`. +.. note:: + For the impatient reader, see the :doc:`tutorials/index` page for short, self-contained examples. Starting |igraph| ================= -|igraph| is a Python module, hence it can be imported exactly the same way as any other -ordinary Python module at the Python prompt:: +The most common way to use |igraph| is as a named import within a Python environment (e.g. a bare Python shell, an `IPython`_ shell, a `Jupyter`_ notebook or JupyterLab instance, `Google Colab `_, or an `IDE `_):: $ 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 + >>> import igraph as ig -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: +To call functions, you need to prefix them with ``ig`` (or whatever name you chose):: ->>> import igraph ->>> print igraph.__version__ -0.6 + >>> import igraph as ig + >>> print(ig.__version__) + 0.9.8 -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 -provided by |igraph|: +.. note:: + It is possible to use *star imports* for |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`, -and provided that the script is on your path in the command line of your operating system -(which is almost surely the case on Linux and OS X), you can simply type :command:`igraph` at the -command line. Windows users will find the script inside the :file:`scripts` subdirectory of Python -and you may have to add it manually to your path in order to be able to use the script from -the command line without typing the whole path. + but it is `generally discouraged `_. -When you start the script, you will see something like this:: +There is a second way to start |igraph|, which is to call the script :command:`igraph` from your terminal:: $ 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.6 running inside Python 3.9.6 (default, Jun 29 2021, 05:25:02) Type "copyright", "credits" or "license" for more information. >>> -The command-line startup script imports all of |igraph|'s methods and objects into the main -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 -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 -(see :ref:`configuring-igraph`). - -In general, it is advised to use the command line startup script when using |igraph| -interactively (i.e., when you just want to quickly load or generate some graphs, calculate -some basic properties and save the results somewhere). For non-disposable graph analysis -routines that you intend to re-run from time to time, you should write a script separately -in a ``.py`` source file and import |igraph| using one of the above methods at the start of -the script, then launch the script using the Python interpreter. - -From now on, every example in the documentation will assume that |igraph|'s objects and -methods are imported into the main namespace (i.e., we used ``from igraph import *`` -instead of ``import igraph``). If you let |igraph| take its own namespace, please adjust -all the examples accordingly. - - -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: - ->>> 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: - ->>> 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(g) -IGRAPH U--- 0 0 -- - -TODO: explain it - -This is not too exciting so far; a graph with a single vertex and no edges is not really useful -for us. Let's add some vertices first! - ->>> 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. - -Now our graph has three vertices but no edges, so let's add some edges as well! You can -add edges by calling :meth:`Graph.add_edges` - but in order to add edges, you have to refer to -existing vertices somehow. |igraph| uses integer vertex IDs starting from zero, thus the -first vertex of your graph has index zero, the second vertex has index 1 and so on. -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: - ->>> 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 edges), you will get an -exception: - ->>> g.add_edges((5, 0)) -Traceback (most recent call last): - File "", line 6, in -igraph.core.InternalError: Error at ../../src/type_indexededgelist.c:245: cannot add edges, invalid vertex id - -Most |igraph| functions will raise an :exc:`igraph.core.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 -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: - ->>> 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 -have IDs, similarly to vertices; they also start from zero and edges that were -added later have higher IDs than edges that were added earlier. Vertex and edge -IDs are always *continuous*, and a direct consequence of this fact is that if -you happen to delete an edge, chances are that some (or all) of the edges will -be renumbered. Moreover, if you delete a vertex, even the vertex IDs will -change. Edges can be deleted by :meth:`~Graph.delete_edges` and it requires a -list of edge IDs to be deleted (or a single edge ID). Vertices can be deleted -by :meth:`~Graph.delete_vertices` and you may have already guessed that it -requires a list of vertex IDs to be deleted (or a single vertex ID). If you do -not know the ID of an edge you wish to delete, but you know the IDs of the -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 -- - -: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 -is similar to the output of ``print`` but it does not print the edge list to avoid -cluttering up the display for large graphs. In general, you should use :meth:`summary` -instead of ``print`` when working interactively with large graphs because printing the -edge list of a graph with millions of vertices and edges could take quite a lot of time. +.. note:: + Windows users will find the script inside the :file:`scripts` subdirectory of Python + and might have to add it manually to their path. + +This script starts an appropriate shell (`IPython`_ or `IDLE `_ if found, otherwise a pure Python shell) and uses star imports (see above). This is sometimes convenient to play with |igraph|'s functions. + +.. note:: + You can specify which shell should be used by this script via |igraph|'s + :doc:`configuration` file. + +This tutorial will assume you have imported igraph using the namespace ``ig``. + +Creating a graph +================ + +The simplest way to create a graph is the :class:`Graph` constructor. To make an empty graph:: + + >>> g = ig.Graph() + +To make a graph with 10 nodes (numbered ``0`` to ``9``) and two edges connecting nodes ``0-1`` and ``0-5``:: + + >>> g = ig.Graph(n=10, edges=[[0, 1], [0, 5]]) + +We can print the graph to get a summary of its nodes and edges:: + + >>> print(g) + IGRAPH U--- 10 2 -- + + edges: + 0--1 0--5 + +This means: **U** ndirected graph with **10** vertices and **2** edges, with the exact edges listed out. If the graph has a `name` attribute, it is printed as well. + +.. note:: + + |igraph| also has a :func:`igraph.summary()` function, which is similar to ``print()`` but it does not list the edges. This is convenient for large graphs with millions of edges:: + + >>> ig.summary(g) + IGRAPH U--- 10 2 -- + + +Adding/deleting vertices and edges +================================== +Let's start from the empty graph again. To add vertices to an existing graph, use :meth:`Graph.add_vertices`:: + + >>> g = ig.Graph() + >>> g.add_vertices(3) + +In |igraph|, vertices are always numbered up from zero. The number of a vertex is called the *vertex ID*. A vertex might or might not have a name. + +Similarly, to add edges use :meth:`Graph.add_edges`:: + + >>> g.add_edges([(0, 1), (1, 2)]) + +Edges are added by specifying the source and target vertex for each edge. This call added two edges, one connecting vertices ``0`` and ``1``, and one connecting vertices ``1`` and ``2``. Edges are also numbered up from zero (the *edge ID*) and have an optional name. + +.. warning:: + + Creating an empty graph and adding vertices and edges as shown here can be much slower + than creating a graph with its vertices and edges as demonstrated earlier. If speed is + of concern, you should especially avoid adding vertices and edges *one at a time*. If you + need to do it anyway, you can use :meth:`Graph.add_vertex` and :meth:`Graph.add_edge`. + + +If you try to add edges to vertices with invalid IDs (i.e., you try to add an edge to vertex ``5`` when the graph has only three vertices), you get an :exc:`igraph.InternalError` exception:: + + >>> g.add_edges([(5, 4)]) + Traceback (most recent call last): + File "", line 1, in + File "/usr/lib/python3.10/site-packages/igraph/__init__.py", line 376, in add_edges + res = GraphBase.add_edges(self, es) + igraph._igraph.InternalError: Error at src/graph/type_indexededgelist.c:270: cannot add edges. -- Invalid vertex id + +The message tries to explain what went wrong (``cannot add edges. -- Invalid +vertex id``) along with the corresponding line in the source code where the error +occurred. + +.. note:: + The whole traceback, including info on the source code, is useful when + reporting bugs on our + `GitHub issue page `_. Please include it + if you create a new issue! + + +Let us add some more vertices and edges to our graph:: + + >>> 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 + +We now have an undirected graph with 6 vertices and 7 edges. Vertex and edge IDs are +always *continuous*, so if you delete a vertex all subsequent vertices will be renumbered. +When a vertex is renumbered, edges are **not** renumbered, but their source and target +vertices will. Use :meth:`Graph.delete_vertices` and :meth:`Graph.delete_edges` to perform +these operations. For instance, to delete the edge connecting vertices ``2-3``, get its +ID and then delete it:: + + >>> g.get_eid(2, 3) + 3 + >>> g.delete_edges(3) Generating graphs ================= -|igraph| includes a large set of graph generators which can be divided into two groups: -deterministic and stochastic graph generators. Deterministic generators produce the same -graph if you call them with exactly the same parameters, while stochastic generators -produce a different graph every time. Deterministic generators include methods for -creating trees, regular lattices, rings, extended chordal rings, several famous graphs -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: +|igraph| includes both deterministic and stochastic graph generators (see :doc:`generation`). +*Deterministic* generators produce the same graph every time you call the fuction, e.g.:: ->>> g = Graph.Tree(127, 2) ->>> summary(g) -IGRAPH U--- 127 126 -- + >>> g = ig.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: +uses :meth:`Graph.Tree` to generate a regular tree graph with 127 vertices, each vertex +having two children (and one parent, of course). No matter how many times you call +:meth:`Graph.Tree`, the generated graph will always be the same if you use the same +parameters:: ->>> g2 = Graph.Tree(127, 2) ->>> g2.get_edgelist() == g.get_edgelist() -True + >>> g2 = ig.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 -each edge. The first member of the pair is the source vertex ID and the second member -is the target vertex ID of the corresponding edge. This list is too long, so let's -just print the first 10 elements! +The above code snippet also shows you that the :meth:`~Graph.get_edgelist()` method, +which returns a list of source and target vertices for all edges, sorted by edge ID. +If you print the first 10 elements, you get:: ->>> 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()[: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! +*Stochastic* generators produce a different graph each time, e.g. :meth:`Graph.GRG`:: ->>> g = Graph.GRG(100, 0.2) ->>> summary(g) -IGRAPH U---- 100 516 -- -+ attr: x (v), y (v) + >>> g = ig.Graph.GRG(100, 0.2) + >>> summary(g) + IGRAPH U---- 100 516 -- + + attr: x (v), y (v) -TODO: discuss what the ``+ attr`` line means. +.. note:: ``+ attr`` shows attributes for vertices (v) and edges (e), in this case two + vertex attributes and no edge attributes. -:meth:`Graph.GRG` generates a geometric random graph: *n* points are chosen randomly and +This 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 -distance *d* are connected by an edge. In our case, *n* is 100 and *d* is 0.2. Due to -the random nature of the algorithm, chances are that the exact graph you got is different -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: +distance *d* are connected by an edge. If you generate GRGs with the same parameters, they +will be different:: ->>> g2 = Graph.GRG(100, 0.2) ->>> g.get_edgelist() == g2.get_edgelist() -False ->>> g.isomorphic(g2) -False + >>> g2 = ig.Graph.GRG(100, 0.2) + >>> g.get_edgelist() == g2.get_edgelist() + 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 -answer can quickly be given by checking the degree distributions of the two graphs. +A slightly looser way to check if the graphs are equivalent is via :meth:`~Graph.isomorphic()`:: + + >>> g.isomorphic(g2) + False + +Checking for isomorphism can take a while for large graphs (in this case, the +answer can quickly be given by checking the degree distributions of the two graphs). Setting and retrieving attributes ================================= +As mentioned above, vertices and edges of a graph in |igraph| have numeric IDs from ``0`` upwards. Deleting vertices or edges can therefore cause reassignments of vertex and/or edge IDs. In addition to IDs, vertices and edges can have *attributes* such as a name, coordinates for plotting, metadata, and weights. The graph itself can have such attributes too (e.g. a name, which will show in ``print`` or :meth:`Graph.summary`). In a sense, every :class:`Graph`, vertex and edge can be used as a Python dictionary to store and retrieve these attributes. + +To demonstrate the use of attributes, let us create a simple social network:: + + >>> g = ig.Graph([(0,1), (0,2), (2,3), (3,4), (4,2), (2,5), (5,0), (6,3), (5,6)]) + +Each vertex represents a person, so we want to store names, ages and genders:: + + >>> 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] + +:attr:`Graph.vs` and :attr:`Graph.es` are the standard way to obtain a sequence of all +vertices and edges, respectively. Just like a Python dictionary, we can set each property +using square brackets. The value must be a list with the same length as the vertices (for +:attr:`Graph.vs`) or edges (for :attr:`Graph.es`). This assigns an attribute to *all* vertices/edges at once. + +To assign or modify an attribute for a single vertex/edge, you can use indexing:: + + >>> g.es[0]["is_formal"] = True + +In fact, a single vertex is represented via the class :class:`Vertex`, and a single edge via :class:`Edge`. Both of them plus :class:`Graph` can all be keyed like a dictionary to set attributes, e.g. to add a date to the graph:: + + >>> g["date"] = "2009-01-10" + >>> print(g["date"]) + 2009-01-10 + +To retrieve a dictionary of attributes, you can use :meth:`Graph.attributes`, :meth:`Vertex.attributes`, and :meth:`Edge.attributes`. + +Furthermore, each :class:`Vertex` has a special property, :attr:`Vertex.index`, that is used to find out the ID of a vertex. Each :class:`Edge` has :attr:`Edge.index` plus two additional properties, :attr:`Edge.source` and :attr:`Edge.target`, that are used to find the IDs of the vertices connected by this edge. To get both at once as a tuple, you can use :attr:`Edge.tuple`. + +To assign attributes to a subset of vertices or edges, you can use slicing:: + + >>> g.es[:1]["is_formal"] = True + +The output of ``g.es[:1]`` is an instance of :class:`~seq.EdgeSeq`, whereas :class:`~seq.VertexSeq` is the equivalent class representing subsets of vertices. + +To delete attributes, use the Python keyword ``del``, e.g.:: + + >>> 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' -|igraph| uses vertex and edge IDs in its core. These IDs are integers, starting from zero, -and they are always continuous at any given time instance during the lifetime of the graph. -This means that whenever vertices and edges are deleted, a large set of edge and possibly -vertex IDs will be renumbered to ensure the continuity. Now, let us assume that our graph -is a social network where vertices represent people and edges represent social connections -between them. One way to maintain the association between vertex IDs and say, the corresponding -names is to have an additional Python list that maps from vertex IDs to names. The drawback -of this approach is that this additional list must be maintained in parallel to the -modifications of the original graph. Luckily, |igraph| knows the concept of *attributes*, -i.e., auxiliary objects associated to a given vertex or edge of a graph, or even to the -graph as a whole. Every |igraph| :class:`Graph`, vertex and edge behaves as a standard -Python dictionary in some sense: you can add key-value pairs to any of them, with the key -representing the name of your attribute (the only restriction is that it must be a string) -and the value representing the attribute itself. .. warning:: Attributes can be arbitrary Python objects, but if you are saving graphs to a @@ -280,77 +264,6 @@ and the value representing the attribute itself. back into Python only. -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)]) - -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: - ->>> 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 -of vertices and edges individually by *indexing* :attr:`~Graph.vs` or :attr:`~Graph.es` -with integers as if they were lists (remember, they are sequences, they contain all the -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: - ->>> 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, -the edge ID (zero in our case) and the dictionary of attributes assigned to that edge. -:class:`Edge` objects have some useful attributes, too: the :attr:`~Edge.source` property -gives you the source vertex of that edge, :attr:`~Edge.target` gives you the target vertex, -:attr:`~Edge.index` gives you the corresponding edge ID, :attr:`~Edge.tuple` gives you a -tuple containing the source and target vertices and :meth:`~Edge.attributes` gives you -a dictionary containing the attributes of this edge. :class:`Vertex` instances only have -:attr:`~Vertex.index` and :meth:`~Vertex.attributes`. - -Since :attr:`Graph.es` always represents all the edges in a graph, indexing it by -*i* will always return the edge with ID *i*, and of course the same applies -to :attr:`Graph.vs`. However, keep in mind that an :class:`EdgeSeq` object *in general* -does not necessarily represent the whole edge sequence of a graph; -:ref:`later in this tutorial ` -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: - ->>> 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: - ->>> 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 =============================== @@ -365,20 +278,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(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: +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,32 +310,33 @@ restrict them to exactly the vertices or edges you want. example is eigenvector centrality (:meth:`Graph.evcent()`). 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: +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:: ->>> 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: @@ -435,10 +349,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"] + ['Claire'] 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 @@ -448,50 +362,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 = ig.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 @@ -530,9 +444,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 @@ -540,49 +454,49 @@ 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. - ``_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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -595,20 +509,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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -620,15 +534,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: +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. @@ -639,7 +553,26 @@ 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). + +.. _tutorial-layouts-plotting: Layouts and plotting ==================== @@ -650,17 +583,18 @@ mapping from vertices to coordinates in two- or three-dimensional space first, preferably in a way that is pleasing for the eye. A separate branch of graph theory, namely graph drawing, tries to solve this problem via several graph layout algorithms. |igraph| implements quite a few layout algorithms and is also able to draw them onto -the screen or to a PDF, PNG or SVG file using the `Cairo library `_. +the screen or to a PDF, PNG or SVG file using the `Cairo library `_. .. important:: To follow the examples of this subsection, you need the Python bindings of the - Cairo library. The previous chapter (:ref:`installing-igraph`) tells you more - about how to install Cairo's Python bindings. + Cairo library or matplotlib (depending on what backend is selected). The previous + chapter (:ref:`installing-igraph`) tells you more about how to install Cairo's Python + bindings. Layout algorithms ^^^^^^^^^^^^^^^^^ -The layout methods in |igraph| are to be found in the :class:`Graph` object, and their +The layout methods in |igraph| are to be found in the :class:`Graph` object, and they always start with ``layout_``. The following table summarises them: ==================================== =============== ============================================= @@ -669,6 +603,8 @@ Method name Short name Algorithm description ``layout_circle`` ``circle``, Deterministic layout that places the ``circular`` vertices on a circle ------------------------------------ --------------- --------------------------------------------- +``layout_davidson_harel`` ``dh`` Davidson-Harel simulated annealing algorithm +------------------------------------ --------------- --------------------------------------------- ``layout_drl`` ``drl`` The `Distributed Recursive Layout`_ algorithm for large graphs ------------------------------------ --------------- --------------------------------------------- @@ -677,8 +613,9 @@ 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_graphopt`` ``graphopt`` The GraphOpt algorithm for large graphs +------------------------------------ --------------- --------------------------------------------- +``layout_grid`` ``grid`` Regular grid layout ------------------------------------ --------------- --------------------------------------------- ``layout_kamada_kawai`` ``kk`` Kamada-Kawai force-directed algorithm ------------------------------------ --------------- --------------------------------------------- @@ -689,6 +626,8 @@ Method name Short name Algorithm description ``lgl``, large graphs ``large_graph`` ------------------------------------ --------------- --------------------------------------------- +``layout_mds`` ``mds`` Multidimensional scaling layout +------------------------------------ --------------- --------------------------------------------- ``layout_random`` ``random`` Places the vertices completely randomly ------------------------------------ --------------- --------------------------------------------- ``layout_random_3d`` ``random_3d`` Places the vertices completely randomly in 3D @@ -698,69 +637,87 @@ Method name Short name Algorithm description ------------------------------------ --------------- --------------------------------------------- ``layout_reingold_tilford_circular`` ``rt_circular`` Reingold-Tilford tree layout with a polar coordinate post-transformation, useful for - ``tree`` (almost) tree-like graphs + (almost) tree-like graphs ------------------------------------ --------------- --------------------------------------------- ``layout_sphere`` ``sphere``, Deterministic layout that places the vertices ``spherical``, evenly on the surface of a sphere ``circular_3d`` ==================================== =============== ============================================= -.. _Distributed Recursive Layout: http://www.cs.sandia.gov/~smartin/software.html -.. _Large Graph Layout: http://sourceforge.net/projects/lgl/ +.. _Distributed Recursive Layout: https://www.osti.gov/doecode/biblio/54626 +.. _Large Graph Layout: https://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", root=[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 -and contains the vertex coordinates in the 2D or 3D space. :class:`Layout` objects also +Layout methods return a :class:`~layout.Layout` object, which behaves mostly like a list of lists. +Each list entry in a :class:`~layout.Layout` object corresponds to a vertex in the original graph +and contains the vertex coordinates in the 2D or 3D space. :class:`~layout.Layout` objects also contain some useful methods to translate, scale or rotate the coordinates in a batch. -However, the primary utility of :class:`Layout` objects is that you can pass them to the -:func:`plot` function along with the graph to obtain a 2D drawing. +However, the primary utility of :class:`~layout.Layout` objects is that you can pass them to the +:func:`~drawing.plot` function along with the graph to obtain a 2D drawing. 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") + >>> ig.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 nodes may be different on your machine since the layout is not deterministic): .. figure:: figures/tutorial_social_network_1.png - :alt: The visual representation of our social network + :alt: The visual representation of our social network (Cairo backend) :align: center 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:: + + >>> import matplotlib.pyplot as plt + >>> fig, ax = plt.subplots() + >>> ig.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 + +.. note:: + When plotting rooted trees, Cairo automatically puts the root on top of the image and + the leaves at the bottom. For `matplotlib`_, the root is usually at the bottom instead. + You can easily place the root on top by calling `ax.invert_yaxis()`. + 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) + >>> g.vs["label"] = g.vs["name"] + >>> color_dict = {"m": "blue", "f": "pink"} + >>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] + >>> ig.plot(g, layout=layout, bbox=(300, 300), margin=20) # Cairo backend + >>> ig.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 -to fit the labels (20 pixels). The result is: +Note that we are simply re-using the previous layout object here, but for the Cairo backend +we also specified that we need a smaller plot (300 x 300 pixels) and a larger margin around +the graph to fit the labels (20 pixels). These settings would be ignored for the Matplotlib +backend. The result is: .. figure:: figures/tutorial_social_network_2.png :alt: The visual representation of our social network - with names and genders @@ -768,27 +725,35 @@ 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`: +also give them as keyword arguments to :func:`~drawing.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"} + >>> ig.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 -a Python dictionary containing the keyword arguments you would pass to :func:`plot` +a Python dictionary containing the keyword arguments you would pass to :func:`~drawing.plot` 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) +attributes to :func:`~drawing.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 + >>> ig.plot(g, **visual_style) The final plot shows the formal ties with thick lines while informal ones with thin lines: @@ -800,8 +765,8 @@ The final plot shows the formal ties with thick lines while informal ones with t To sum it all up: there are special vertex and edge properties that correspond to the visual representation of the graph. These attributes override the default settings -of |igraph| (see :ref:`configuring-igraph` for overriding the system-wide defaults). -Furthermore, appropriate keyword arguments supplied to :func:`plot` override the +of |igraph| (see :doc:`configuration` for overriding the system-wide defaults). +Furthermore, appropriate keyword arguments supplied to :func:`~drawing.plot` override the visual properties provided by the vertex and edge attributes. The following two tables summarise the most frequently used visual attributes for vertices and edges, respectively: @@ -814,6 +779,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 @@ -833,8 +800,9 @@ Attribute name Keyword argument Purpose drawn first. --------------- ---------------------- ------------------------------------------ ``shape`` ``vertex_shape`` Shape of the vertex. Known shapes are: - ``rectangle``, ``circle``, ``hidden``, - ``triangle-up``, ``triangle-down``. + ``rectangle``, ``circle``, ``diamond``, + ``hidden``, ``triangle-up``, + ``triangle-down``. Several aliases are also accepted, see :data:`drawing.known_shapes`. --------------- ---------------------- ------------------------------------------ @@ -859,7 +827,9 @@ Attribute name Keyword argument Purpose interpreted as zero. This is useful to make multiple edges visible. See also the ``autocurve`` keyword argument to - :func:`plot`. + :func:`~drawing.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 @@ -868,14 +838,34 @@ Attribute name Keyword argument Purpose ``arrow_width`` ``edge_arrow_width`` Width of the arrowhead on the edge if the graph is directed, relative to 10 pixels. --------------- ---------------------- ------------------------------------------ -``width`` ``edge_width`` Width of the edge in pixels +``loop_size`` ``edge_loop_size`` Size of self-loops. It can be set as a + negative number, in which case it scales + with the size of the corresponding vertex + (e.g. -1.0 means the loop has the same size + as the vertex). This attribute is + ignored by edges that are not loops. + This attribute is available only in the + matplotlib backend. +--------------- ---------------------- ------------------------------------------ +``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). =============== ====================== ========================================== Generic keyword arguments of ``plot()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -These settings can be specified as keyword arguments to the ``plot()`` function +These settings can be specified as keyword arguments to the :func:`~drawing.plot` function to control the overall appearance of the plot. ================ ================================================================ @@ -887,9 +877,11 @@ Keyword argument Purpose ---------------- ---------------------------------------------------------------- ``bbox`` The bounding box of the plot. This must be a tuple containing the desired width and height of the plot. The default plot is - 600 pixels wide and 600 pixels high. + 600 pixels wide and 600 pixels high. Ignored for the + Matplotlib backend. ---------------- ---------------------------------------------------------------- -``layout`` The layout to be used. It can be an instance of :class:`Layout`, +``layout`` The layout to be used. It can be an instance of + :class:`~layout.Layout`, a list of tuples containing X-Y coordinates, or the name of a layout algorithm. The default is ``auto``, which selects a layout algorithm automatically based on the size and @@ -898,7 +890,7 @@ Keyword argument Purpose ``margin`` The top, right, bottom and left margins of the plot in pixels. This argument must be a list or tuple and its elements will be re-used if you specify a list or tuple with less than four - elements. + elements. Ignored for the Matplotlib backend. ================ ================================================================ Specifying colors in plots @@ -908,11 +900,11 @@ Specifying colors in plots color (e.g., edge, vertex or label colors in the respective attributes): X11 color names - See the `list of X11 color names `_ + See the `list of X11 color names `_ in Wikipedia for the complete list. Alternatively you can see the - keys of the igraph.drawing.colors.known_colors dictionary. Color - names are case insensitive in igraph so "DarkBlue" can be written as - "darkblue" as well. + keys of the ``igraph.drawing.colors.known_colors`` dictionary. Color + names are case insensitive in igraph so ``"DarkBlue"`` can be written as + ``"darkblue"`` as well. Color specification in CSS syntax This is a string according to one of the following formats (where *R*, *G* and @@ -925,14 +917,31 @@ 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]``. + +Note that when specifying the same color for all vertices or edges, you can use +a string as-is but not the tuple or list syntax as tuples or lists would be +interpreted as if the *items* in the tuple are for individual vertices or +edges. So, this would work:: + + >>> ig.plot(g, vertex_color="green") + +But this would not, as it would treat the items in the tuple as palette indices +for the first, second and third vertoces:: + + >>> ig.plot(g, vertex_color=(1, 0, 0)) + +In this latter case, you need to expand the color specification for each vertex +explicitly:: + + >>> ig.plot(g, vertex_color=[(1, 0, 0)] * g.vcount()) Saving plots ^^^^^^^^^^^^ -|igraph| can be used to create publication-quality plots by asking the :func:`plot` +|igraph| can be used to create publication-quality plots by asking the :func:`~drawing.plot` function to save the plot into a file instead of showing it on a screen. This can be done simply by passing the target filename as an additional argument after the graph itself. The preferred format is inferred from the extension. |igraph| can @@ -941,8 +950,15 @@ 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) + >>> ig.plot(g, "social_network.pdf", **visual_style) + +If you are using the matplotlib backend, you can save your plot as usual:: + >>> fig, ax = plt.subplots() + >>> ig.plot(g, **visual_style) + >>> fig.savefig("social_network.pdf") + +Many file formats are supported by matplotlib. |igraph| and the outside world ============================== @@ -989,27 +1005,25 @@ Labeled edgelist ``ncol`` :meth:`Graph.Read_Ncol` :meth:`Graph.write_n Pickled graph ``pickle`` :meth:`Graph.Read_Pickle` :meth:`Graph.write_pickle` ================ ============= ============================ ============================= -.. _GraphViz: http://www.graphviz.org -.. _LGL: http://lgl.sourceforge.net/#FileFormat -.. _NCOL: http://lgl.sourceforge.net/#FileFormat -.. _Pajek: http://pajek.imfm.si/doku.php +.. _GraphViz: https://www.graphviz.org +.. _LGL: https://lgl.sourceforge.net/#FileFormat +.. _NCOL: https://lgl.sourceforge.net/#FileFormat +.. _Pajek: http://mrvar.fdv.uni-lj.si/pajek/ 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): +`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):: ->>> karate = Graph.Read_GraphML("karate.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) + >>> karate = ig.Graph.Read_GraphML("zachary.graphml") + >>> ig.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("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 @@ -1021,16 +1035,16 @@ with the Pajek writer method from the table above: ensures that you get exactly the same graph back. The pickled graph format uses Python's ``pickle`` module to store and read graphs. -There are two helper methods as well: :func:`load` is a generic entry point for +There are two helper methods as well: :func:`read` is a generic entry point for reader methods which tries to infer the appropriate format from the file extension. -:meth:`Graph.save` is the opposite of :func:`load`: it lets you save a graph where +:meth:`Graph.write` is the opposite of :func:`read`: 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: +:func:`read` and :meth:`Graph.write` 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 = ig.load("zachary.graphml") + >>> karate.write("zachary.net") + >>> karate.write("zachary.my_extension", format="gml") Where to go next @@ -1039,12 +1053,13 @@ Where to go next This tutorial was only scratching the surface of what |igraph| can do. My long-term plans are to extend this tutorial into a proper manual-style documentation to |igraph| in the next chapters. In the meanwhile, check out the -full `API documentation`_ which should provide information about almost every +:doc:`api/index` which should provide information about almost every |igraph| class, function or method. A good starting point is the documentation -of the `Graph class`_. Should you get stuck, drop a mail to the `igraph mailing -list`_ - maybe there is someone out there who can help you out immediately. - -.. _API documentation: http://igraph.org/python/doc/igraph-module.html -.. _Graph class: http://igraph.org/python/doc/igraph.Graph-class.html -.. _igraph mailing list: http://lists.nongnu.org/mailman/listinfo/igraph-help - +of the :class:`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. + +.. _Discourse group: https://igraph.discourse.group +.. _matplotlib: https://matplotlib.org/ +.. _IPython: https://ipython.readthedocs.io/en/stable/ +.. _Jupyter: https://jupyter.org/ diff --git a/doc/source/visualisation.rst b/doc/source/visualisation.rst index 53b9d0bcb..cf32dbd90 100644 --- a/doc/source/visualisation.rst +++ b/doc/source/visualisation.rst @@ -1,4 +1,239 @@ +.. 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 :doc:`api/index` for details on each function and class. See the :ref:`tutorial ` and +the :doc:`tutorials/index` for examples. + +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 +- `Graph.layout_umap`: Uniform Manifold Approximation and Projection (2D and 3D). UMAP works especially well when the graph is composed + by "clusters" that are loosely connected to each other. + +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. + +.. note:: + When plotting rooted trees, Cairo automatically puts the root on top of the image and + the leaves at the bottom. For `matplotlib`_, the root is usually at the bottom instead. + You can easily place the root on top by calling `ax.invert_yaxis()`. + +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) + >>> artist = ax.get_children()[0] # This is a GraphArtist + >>> dots = artist.get_vertices() + >>> dot.set_facecolors(['tomato'] * g.vcount()) + >>> dot.set_sizes(dot.get_sizes() * 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 `artist.get_children()` is the following: (i) one artist for clustering hulls if requested; + (ii) one artist for edges; (iii) one artist for vertices; (iv) one artist for **each** edge label; (v) one + artist for **each** vertex label. + +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 +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 (using the Cairo backend only):: + + >>> 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 :ref:`tutorial ` for examples and a full list of options. -.. note:: TODO. This is a placeholder section; it is not written yet. +.. _matplotlib: https://matplotlib.org +.. _Jupyter: https://jupyter.org/ +.. _Cairo: https://www.cairographics.org +.. _graphviz: https://www.graphviz.org +.. _networkx: https://networkx.org/ +.. _graph-tool: https://graph-tool.skewed.de/ diff --git a/docker/emscripten/Dockerfile b/docker/emscripten/Dockerfile new file mode 100644 index 000000000..957083097 --- /dev/null +++ b/docker/emscripten/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.10.6-bullseye + +SHELL ["/bin/bash", "-c"] + +COPY setup.py.diff.txt /setup.py.diff + +RUN set -ex \ + && echo "Installing flex and bison" \ + && apt-get update \ + && apt-get -y install flex bison \ + && echo "Installing Emscripten" \ + && git clone https://github.com/emscripten-core/emsdk.git \ + && cd emsdk \ + && ./emsdk install latest \ + && ./emsdk activate latest \ + && source "/emsdk/emsdk_env.sh" \ + && cd .. \ + && echo "Cloning igraph repositories" \ + && git clone https://github.com/igraph/python-igraph.git \ + && cd python-igraph/vendor/source \ + && git clone https://github.com/igraph/igraph.git \ + && cd ../.. \ + && echo "Applying patch to setup.py" \ + && git apply < /setup.py.diff \ + && echo "Installing pyodide-build" \ + && pip install pyodide-build \ + && echo "Building igraph wheel" \ + && pyodide build diff --git a/docker/jupyter/Dockerfile b/docker/jupyter/Dockerfile index 46609ae8f..d7dede8b2 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 diff --git a/docker/jupyter/README.rst b/docker/jupyter/README.rst index 491f7c9ce..c8030fde5 100644 --- a/docker/jupyter/README.rst +++ b/docker/jupyter/README.rst @@ -6,4 +6,4 @@ accessible in a Jupyter notebook. Use the following command line to start the container with Docker:: - $ docker run --rm -it -p 8888:8888 -v "$(pwd):/notebooks" ntamas/python-igraph-notebook \ No newline at end of file + $ docker run --rm -it -p 8888:8888 -v "$(pwd):/notebooks" ntamas/python-igraph-notebook diff --git a/docker/minimal/Dockerfile b/docker/minimal/Dockerfile index 332147d30..4e928c58d 100644 --- a/docker/minimal/Dockerfile +++ b/docker/minimal/Dockerfile @@ -1,7 +1,7 @@ -FROM ubuntu:latest +FROM python:latest MAINTAINER Tamas Nepusz LABEL Description="Simple Docker image that contains 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 pip install python-igraph -RUN pip install cairocffi + +RUN pip3 install igraph cairocffi + CMD /usr/local/bin/igraph 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 diff --git a/etc/lsan-suppr.txt b/etc/lsan-suppr.txt new file mode 100644 index 000000000..74f84dd63 --- /dev/null +++ b/etc/lsan-suppr.txt @@ -0,0 +1,10 @@ +leak:_PyObject_Malloc +leak:_PyObject_Realloc +leak:newlockobject +leak:initumath +leak:uint_arrtype_new +leak:ufunc_generic_fastcall +leak:ffi_call_int +leak:array_empty +leak:array_copy +leak:array_cumsum diff --git a/igraph/__init__.py b/igraph/__init__.py deleted file mode 100644 index dd315c53a..000000000 --- a/igraph/__init__.py +++ /dev/null @@ -1,4186 +0,0 @@ -# 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 __version__, __build_date__ -from igraph.clustering import * -from igraph.cut import * -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.remote.nexus import * -from igraph.statistics import * -from igraph.summary import * -from igraph.utils import * - -import os -import math -import gzip -import sys -import operator - -from collections import defaultdict -from itertools import izip -from shutil import copyfileobj -from tempfile import mkstemp -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. - - This class is built on top of L{GraphBase}, so the order of the - methods in the Epydoc 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 - easier to implement in Python than in pure C. An example is the - attribute handling in the constructor: the constructor of L{Graph} - accepts three dictionaries corresponding to the graph, vertex and edge - attributes while the constructor of L{GraphBase} does not. This extension - was needed to make L{Graph} serializable through the C{pickle} module. - L{Graph} also overrides some functions from L{GraphBase} to provide a - more convenient interface; e.g., layout functions return a L{Layout} - instance from L{Graph} instead of a list of coordinate pairs. - - Graphs can also be indexed by strings or pairs of vertex indices or vertex - names. When a graph is indexed by a string, the operation translates to - the retrieval, creation, modification or deletion of a graph attribute: - - >>> g = Graph.Full(3) - >>> g["name"] = "Triangle graph" - >>> g["name"] - 'Triangle graph' - >>> del g["name"] - - When a graph is indexed by a pair of vertex indices or names, the graph - itself is treated as an adjacency matrix and the corresponding cell of - the matrix is returned: - - >>> g = Graph.Full(3) - >>> g.vs["name"] = ["A", "B", "C"] - >>> g[1, 2] - 1 - >>> g["A", "B"] - 1 - >>> g["A", "B"] = 0 - >>> g.ecount() - 2 - - Assigning values different from zero or one to the adjacency matrix will - be translated to one, unless the graph is weighted, in which case the - numbers will be treated as weights: - - >>> g.is_weighted() - False - >>> g["A", "B"] = 2 - >>> g["A", "B"] - 1 - >>> g.es["weight"] = 1.0 - >>> g.is_weighted() - True - >>> g["A", "B"] = 2 - >>> g["A", "B"] - 2 - >>> g.es["weight"] - [1.0, 1.0, 2] - """ - - # Some useful aliases - omega = GraphBase.clique_number - alpha = GraphBase.independence_number - shell_index = GraphBase.coreness - cut_vertices = GraphBase.articulation_points - blocks = GraphBase.biconnected_components - evcent = GraphBase.eigenvector_centrality - vertex_disjoint_paths = GraphBase.vertex_connectivity - edge_disjoint_paths = GraphBase.edge_connectivity - cohesion = GraphBase.vertex_connectivity - adhesion = GraphBase.edge_connectivity - - # Compatibility aliases - shortest_paths_dijkstra = GraphBase.shortest_paths - subgraph = GraphBase.induced_subgraph - - def __init__(self, *args, **kwds): - """__init__(n=0, edges=None, directed=False, graph_attrs=None, - vertex_attrs=None, edge_attrs=None) - - Constructs a graph from scratch. - - @keyword n: the number of vertices. Can be omitted, the default is - zero. Note that if the edge list contains vertices with indexes - larger than or equal to M{m}, then the number of vertices will - be adjusted accordingly. - @keyword edges: the edge list where every list item is a pair of - integers. If any of the integers is larger than M{n-1}, the number - of vertices is adjusted accordingly. C{None} means no edges. - @keyword directed: whether the graph should be directed - @keyword graph_attrs: the attributes of the graph as a dictionary. - @keyword vertex_attrs: the attributes of the vertices as a dictionary. - Every dictionary value must be an iterable with exactly M{n} items. - @keyword edge_attrs: the attributes of the edges as a dictionary. Every - dictionary value must be an iterable with exactly M{m} items where - M{m} is the number of edges. - """ - # 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"] - params = [0, [], False, {}, {}, {}] - - # Is there any keyword argument in kwds that we don't know? If so, - # freak out. - 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() - )) - - # If the first argument is a list, assume that the number of vertices - # were omitted - args = list(args) - if len(args) > 0: - if isinstance(args[0], list) or isinstance(args[0], tuple): - args.insert(0, params[0]) - - # Override default parameters from args - params[:len(args)] = args - - # Override default parameters from keywords - for idx, k in enumerate(kwd_order): - if k in kwds: - params[idx] = kwds[k] - - # Now, translate the params list to argument names - nverts, edges, directed, graph_attrs, vertex_attrs, edge_attrs = params - - # When the number of vertices is None, assume that the user meant zero - if nverts is None: - nverts = 0 - - # When 'edges' is None, assume that the user meant an empty list - if edges is None: - edges = [] - - # Initialize the graph - GraphBase.__init__(self, nverts, edges, directed) - - # Set the graph attributes - for key, value in graph_attrs.iteritems(): - if isinstance(key, (int, long)): - key = str(key) - self[key] = value - # Set the vertex attributes - for key, value in vertex_attrs.iteritems(): - if isinstance(key, (int, long)): - key = str(key) - self.vs[key] = value - # Set the edge attributes - for key, value in edge_attrs.iteritems(): - if isinstance(key, (int, long)): - key = str(key) - self.es[key] = value - - def add_edge(self, source, target, **kwds): - """add_edge(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. - - @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. - """ - eid = self.ecount() - self.add_edges([(source, target)]) - edge = self.es[eid] - for key, value in kwds.iteritems(): - edge[key] = value - return edge - - def add_edges(self, es): - """add_edges(es) - - 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. - """ - return GraphBase.add_edges(self, es) - - def add_vertex(self, name=None, **kwds): - """add_vertex(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.iteritems(): - vertex[key] = value - if name is not None: - vertex["name"] = name - return vertex - - def add_vertices(self, n): - """add_vertices(n) - - Adds some vertices to the graph. - - @param n: the number of vertices to be added, or the name of a single - vertex to be added, or an iterable of strings, each corresponding to the - name of a vertex to be added. Names will be assigned to the C{name} - vertex attribute. - """ - 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 - 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)) - self.vs[m:]["name"] = names - return result - return GraphBase.add_vertices(self, n) - - def adjacent(self, *args, **kwds): - """adjacent(vertex, mode=OUT) - - Returns the edges a given vertex is incident on. - - @deprecated: replaced by L{Graph.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 - to L{Graph.to_directed()} that is invoked on the copy. - """ - copy = self.copy() - copy.to_directed(*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 - to L{Graph.to_undirected()} that is invoked on the copy. - """ - copy = self.copy() - 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 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 - - 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. - """ - 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)): - 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. - - 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 all_st_cuts(self, 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(self, cut=cut, partition=part) - for cut, part in izip(*GraphBase.all_st_cuts(self, source, target))] - - def all_st_mincuts(self, 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(self, source, target, capacity) - return [Cut(self, value, cut=cut, partition=part) - for cut, part in izip(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 - """ - if return_articulation_points: - trees, aps = GraphBase.biconnected_components(self, True) - else: - 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)) - clustering = VertexCover(self, clusters) - - if return_articulation_points: - return clustering, aps - else: - return clustering - blocks = biconnected_components - - def cohesive_blocks(self): - """cohesive_blocks() - - 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(self, *GraphBase.cohesive_blocks(self)) - - def clusters(self, mode=STRONG): - """clusters(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)) - components = clusters - - def degree_distribution(self, bin_width = 1, *args, **kwds): - """degree_distribution(bin_width=1, ...) - - 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 dyad_census(self, *args, **kwds): - """dyad_census() - - 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 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 xrange(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_adjlist(self, mode=OUT): - """get_adjlist(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 - the predecessors and the successors will be returned. Ignored - for undirected graphs. - """ - return [self.neighbors(idx, mode) for idx in xrange(self.vcount())] - - def get_adjedgelist(self, *args, **kwds): - """get_adjedgelist(mode=OUT) - - Returns the incidence list representation of the graph. - - @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") - return self.get_inclist(*args, **kwds) - - def get_all_simple_paths(self, v, to=None, mode=OUT): - """get_all_simple_paths(v, to=None, 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 - more than once. - - Note that potentially there are exponentially many paths between two - vertices of a graph, especially if your graph is lattice-like. In this - case, you may run out of memory when using this function. - - @param v: the source for the calculated paths - @param to: a vertex selector describing the destination for the calculated - paths. This can be a single vertex ID, a list of vertex IDs, a single - vertex name, a list of vertex names or a L{VertexSeq} object. C{None} - means all the vertices. - @param mode: the directionality of the paths. L{IN} means to calculate - incoming paths, L{OUT} means to calculate outgoing paths, L{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}, - the vertices in a path are returned in reversed order! - """ - paths = self._get_all_simple_paths(v, to, mode) - prev = 0 - result = [] - for index, item in enumerate(paths): - if item < 0: - result.append(paths[prev:index]) - prev = index+1 - return result - - def get_inclist(self, mode=OUT): - """get_inclist(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 L{OUT}, returns the successors of the vertex. If - L{IN}, returns the predecessors of the vertex. If L{ALL}, both - the predecessors and the successors will be returned. Ignored - for undirected graphs. - """ - return [self.incident(idx, mode) for idx in xrange(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 - 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): - """is_named() - - Returns whether the graph is named, i.e. whether it has a "name" - vertex attribute. - """ - return "name" in self.vertex_attributes() - - def is_weighted(self): - """is_weighted() - - Returns whether the graph is weighted, i.e. whether it has a "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 - 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): - """mincut(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): - """st_mincut(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): - """modularity(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): - """path_length_hist(directed=True) - - Returns the path length histogram of the graph - - @param directed: whether to consider directed paths. Ignored for - undirected graphs. - @return: a L{Histogram} object. The object will also have an - C{unconnected} attribute that stores the number of unconnected - vertex pairs (where the second vertex can not be reached from - the first one). The latter one will be of type long (and not - a simple integer), since this can be I{very} large. - """ - 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) - 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 Google 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 = _igraph.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. - - @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 - introduced in igraph 0.6. - @return: the spanning tree as a L{Graph} object if C{return_tree} - is C{True} or the IDs of the edges that constitute the spanning - tree if C{return_tree} is C{False}. - - @newfield ref: Reference - @ref: Prim, R.C.: I{Shortest connection networks and some - generalizations}. Bell System Technical Journal 36:1389-1401, 1957. - """ - result = GraphBase._spanning_tree(self, weights) - if return_tree: - return self.subgraph_edges(result, delete_vertices=False) - return result - - def transitivity_avglocal_undirected(self, mode="nan", weights=None): - """Calculates the average of the vertex transitivities of the graph. - - In the unweighted case, the transitivity measures the probability that - two neighbors of a vertex are connected. In case of the average local - transitivity, this probability is calculated for each vertex and then - the average is taken. Vertices with less than two neighbors require - special treatment, they will either be left out from the calculation - or they will be considered as having zero transitivity, depending on - the I{mode} parameter. The calculation is slightly more involved for - weighted graphs; in this case, weights are taken into account according - to the formula of Barrat et al (see the references). - - Note that this measure is different from the global transitivity - measure (see L{transitivity_undirected()}) as it simply takes the - average local transitivity across the whole network. - - @param mode: defines how to treat vertices with degree less than two. - If C{TRANSITIVITY_ZERO} or C{"zero"}, these vertices will have zero - transitivity. If C{TRANSITIVITY_NAN} or C{"nan"}, these vertices - will be excluded from the average. - @param weights: edge weights to be used. Can be a sequence or iterable - or even an edge attribute name. - - @see: L{transitivity_undirected()}, L{transitivity_local_undirected()} - @newfield ref: Reference - @ref: Watts DJ and Strogatz S: I{Collective dynamics of small-world - networks}. Nature 393(6884):440-442, 1998. - @ref: Barrat A, Barthelemy M, Pastor-Satorras R and Vespignani A: - I{The architecture of complex weighted networks}. PNAS 101, 3747 (2004). - U{http://arxiv.org/abs/cond-mat/0311416}. - """ - if weights is None: - return GraphBase.transitivity_avglocal_undirected(self, mode) - - xs = self.transitivity_local_undirected(mode=mode, weights=weights) - return sum(xs) / float(len(xs)) - - def triad_census(self, *args, **kwds): - """triad_census() - - 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 - 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) - - 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) - - # 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): - """community_leading_eigenvector_naive(clusters=None, - return_merges=False) - - A 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): - """community_leading_eigenvector(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): - """community_label_propagation(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. - @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, basestring): - 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)) - - - 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. - - @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 - - 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. - @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) - 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)) - - 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 - spinglass community detection method of Reichardt & Bornholdt. - - @keyword 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 - 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 - 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. - 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 - 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 - 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 - 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(self, *args, **kwds) - if "weights" in kwds: - modularity_params=dict(weights=kwds["weights"]) - else: - 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 - 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(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)) - - def k_core(self, *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 = xrange(self.vcount()) - return_single = False - else: - return_single = True - indices = [] - for arg in args: - try: - indices.extend(arg) - except: - indices.append(arg) - - if len(indices)>1 or hasattr(args[0], "__iter__"): - return_single = False - - corenesses = self.coreness() - result = [] - vidxs = xrange(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 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{Graph.layout_auto}) - - - C{bipartite}: bipartite layout (see L{Graph.layout_bipartite}) - - - C{circle}, C{circular}: circular layout - (see L{Graph.layout_circle}) - - - C{dh}, C{davidson_harel}: Davidson-Harel layout (see - L{Graph.davidson_harel}) - - - C{drl}: DrL layout for large graphs (see L{Graph.layout_drl}) - - - C{drl_3d}: 3D DrL layout for large graphs - (see L{Graph.layout_drl}) - - - C{fr}, C{fruchterman_reingold}: Fruchterman-Reingold layout - (see L{Graph.layout_fruchterman_reingold}). - - - C{fr_3d}, C{fr3d}, C{fruchterman_reingold_3d}: 3D Fruchterman- - Reingold layout (see L{Graph.layout_fruchterman_reingold}). - - - C{grid}: regular grid layout in 2D (see L{Graph.layout_grid}) - - - C{grid_3d}: regular grid layout in 3D (see L{Graph.layout_grid_3d}) - - - C{graphopt}: the graphopt algorithm (see L{Graph.layout_graphopt}) - - - C{kk}, C{kamada_kawai}: Kamada-Kawai layout - (see L{Graph.layout_kamada_kawai}) - - - C{kk_3d}, C{kk3d}, C{kamada_kawai_3d}: 3D Kamada-Kawai layout - (see L{Graph.layout_kamada_kawai}) - - - C{lgl}, C{large}, C{large_graph}: Large Graph Layout - (see L{Graph.layout_lgl}) - - - C{mds}: multidimensional scaling layout (see L{Graph.layout_mds}) - - - C{random}: random layout (see L{Graph.layout_random}) - - - C{random_3d}: random 3D layout (see L{Graph.layout_random}) - - - C{rt}, C{tree}, C{reingold_tilford}: Reingold-Tilford tree - layout (see L{Graph.layout_reingold_tilford}) - - - C{rt_circular}, C{reingold_tilford_circular}: circular - Reingold-Tilford tree layout - (see L{Graph.layout_reingold_tilford_circular}) - - - C{sphere}, C{spherical}, C{circle_3d}, C{circular_3d}: spherical - layout (see L{Graph.layout_circle}) - - - C{star}: star layout (see L{Graph.layout_star}) - - - C{sugiyama}: Sugiyama layout (see L{Graph.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") - l = method(self, *args, **kwds) - if not isinstance(l, Layout): - l = Layout(l) - return l - - 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{Graph.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()}). - - 5. If everything else above failed, the DrL layout algorithm - will be used (see L{Graph.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(zip(self.vs["x"], self.vs["y"], self.vs["z"])) - else: - return Layout(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_grid_fruchterman_reingold(self, *args, **kwds): - """layout_grid_fruchterman_reingold(*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, 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) - - 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 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 - - 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, basestring): - 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(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 - passed intact to L{Graph.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, basestring): - 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 - row = [float(x) for x in line.split(sep)] - matrix.append(row) - ri += 1 - - f.close() - - if attribute is None: - graph=klass.Adjacency(matrix, *args, **kwds) - else: - kwds["attr"] = attribute - graph=klass.Weighted_Adjacency(matrix, *args, **kwds) - - return graph - - 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: - source = self["source"] - if target is None: - target = self["target"] - if isinstance(capacity, basestring) and \ - capacity not in self.edge_attributes(): - warn("'%s' edge attribute does not exist" % capacity) - capacity = None - return GraphBase.write_dimacs(self, f, source, target, capacity) - - def write_graphmlz(self, 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.""" - from igraph.utils import named_temporary_file - 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): - """Read_DIMACS(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(Graph, cls).Read_DIMACS(f, directed) - graph.es["capacity"] = cap - graph["source"] = source - graph["target"] = target - return graph - - @classmethod - def Read_GraphMLz(cls, f, *params, **kwds): - """Read_GraphMLz(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""" - 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) - - 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 cPickle 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 cPickle as pickle - 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 - def Read_Pickle(klass, 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. The string is assumed to - hold pickled data if it is longer than 40 characters and - contains a substring that's peculiar to pickled versions - of an C{igraph} Graph object. - @return: the created graph object. - """ - import cPickle as pickle - if hasattr(fname, "read"): - # Probably a file or a file-like object - result = pickle.load(fname) - else: - fp = None - try: - fp = open(fname, "rb") - 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.') - if fp is not None: - result = pickle.load(fp) - fp.close() - return result - - @classmethod - def Read_Picklez(klass, 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 cPickle 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")) - return result - - @classmethod - def Read_Picklez(klass, 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 cPickle 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, klass): - raise TypeError("unpickled object is not a %s" % klass.__name__) - 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, 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 xrange(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 xrange(len(labels), vcount)) - colors.extend(["red"] * (vcount - len(colors))) - - if isinstance(fname, basestring): - 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 >> f, '' - print >> f, '' - print >> f - print >> f, '> f, 'width="{0}px" height="{1}px">'.format(width, height), - - - edge_color_dict = {} - print >> 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 >> 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, '' - - edge_color_dict[e_col] = "Arrow2Lend{0}".format(marker_index) - print >> f, '' - print >> 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 >> 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, ' ' - - 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 - # This allows us to dynamically size the width of the nodes - font = tkFont.Font(root=tk.Tk(), font=("Sans", font_size, tkFont.NORMAL)) - - for vidx in range(self.vcount()): - print >> f, ' '.\ - format(vidx, layout[vidx][0], layout[vidx][1]) - 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) - else: - print >> f, ' '.\ - format(str(vertex_size), str(colors[vidx])) - elif shapes[vidx] == 2: - print >> f, ' '.\ - format(vertex_size, vertex_size * 2, vidx, colors[vidx]) - 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, '' - - if our_file: - f.close() - - - @classmethod - def _identify_format(klass, 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: - 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(klass, 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 = klass._identify_format(f) - try: - reader = klass._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) - return reader(f, *args, **kwds) - Load = Read - - - 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 - - - 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 = 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) - 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): - """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(l, n): - result = [None] * n - for i, v in l: - 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.iteritems(): - try: - vertex_attrs[k].append((idx, v)) - except KeyError: - vertex_attrs[k] = [(idx, v)] - n += 1 - for k, v in vertex_attrs.iteritems(): - 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 = klass(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.iteritems(): - 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.iteritems(): - try: - edge_attrs[k].append((idx, v)) - except KeyError: - edge_attrs[k] = [(idx, v)] - m += 1 - for k, v in edge_attrs.iteritems(): - 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.iteritems(): v.extend(more) - vertex_attrs[vertex_name_attr] = vertex_name_map.values() - n = len(vertex_name_map) - - # Create the graph - return klass(n, edge_list, directed, {}, vertex_attrs, edge_attrs) - - ##################################################### - # Constructor for tuple-like representation of graphs - - @classmethod - 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 - 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, basestring): - weights = "weight" - edge_attrs = [weights] - else: - if weights: - raise ValueError("`weights` must be False if `edge_attrs` is " - "not None") - - if isinstance(edge_attrs, basestring): - 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] = idgen.values() - n = len(idgen) - - # Construct the graph - return klass(n, edge_list, directed, {}, vertex_attributes, edge_attributes) - - ################################# - # Constructor for 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) - - ############################################# - # 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. - 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 = klass._Bipartite(types, *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 - 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 = klass._Full_Bipartite(*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 - 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. - """ - result, types = klass._Random_Bipartite(*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. - - 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 = klass._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) - - 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. - - @return: the graph with a binary vertex attribute named C{"type"} that - stores the vertex classes. - """ - result, types = klass._Incidence(*args, **kwds) - result.vs["type"] = types - return result - - 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(Graph, self).bipartite_projection - - if which == False: - which = 0 - elif which == 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): - """bipartite_projection(types="type") - - 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(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 - 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(Graph, self).get_incidence(types, *args, **kwds) - - ########################### - # ctypes support - - @property - def _as_parameter_(self): - return self._raw_pointer() - - ################### - # Custom operators - - def __iadd__(self, other): - """In-place addition (disjoint union). - - @see: L{__add__} - """ - if isinstance(other, (int, basestring)): - 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], basestring): - 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, basestring)): - 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], basestring): - 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. - """ - 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, long, basestring)): - self.delete_vertices(other) - else: - return NotImplemented - elif isinstance(other, _igraph.Vertex): - self.delete_vertices(other) - elif isinstance(other, _igraph.VertexSeq): - self.delete_vertices(other) - elif isinstance(other, _igraph.Edge): - self.delete_edges(other) - elif isinstance(other, _igraph.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, long, basestring)): - 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, long, basestring)): - result.delete_vertices(other) - else: - return NotImplemented - else: - return result - elif isinstance(other, _igraph.Vertex): - result.delete_vertices(other) - elif isinstance(other, _igraph.VertexSeq): - result.delete_vertices(other) - elif isinstance(other, _igraph.Edge): - result.delete_edges(other) - elif isinstance(other, _igraph.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. - """ - 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 __nonzero__(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. - - 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)): - return self, other - if isinstance(other, _igraph.Vertex): - return self, other - if isinstance(other, _igraph.VertexSeq): - return self, other - if isinstance(other, _igraph.Edge): - return self, other - if isinstance(other, _igraph.EdgeSeq): - return self, other - return NotImplemented - - @classmethod - def _reconstruct(cls, attrs, *args, **kwds): - """Reconstructs a Graph object from Python's pickled format. - - This method is for internal use only, it should not be called - directly.""" - result = cls(*args, **kwds) - result.__dict__.update(attrs) - return result - - def __reduce__(self): - """Support for pickling.""" - constructor = self.__class__ - gattrs, vattrs, eattrs = {}, {}, {} - for attr in self.attributes(): - gattrs[attr] = self[attr] - for attr in self.vs.attribute_names(): - 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) - return (constructor, parameters, self.__dict__) - - __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 - - 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{Graph.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 - 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 - - - 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. - - 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). - """ - 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) - - 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", - "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", - "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! - -############################################################## - -class VertexSeq(_igraph.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 - L{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, we try that first because that - # attribute is indexed - 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 args: - args.insert(0, name) - else: - args = [name] - - if args: - # Selecting first based on positional arguments, then checking - # the criteria specified by the (remaining) keyword arguments - vertex = _igraph.VertexSeq.find(self, *args) - if not kwds: - return vertex - vs = self.graph.vs[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 = _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(): - 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, func = keyword, 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(_igraph.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 - L{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 = _igraph.EdgeSeq.find(self, *args) - if not kwds: - return edge - es = self.graph.es[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. - - 2. C{_target} or {_to} means the target vertex of an edge. - - 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. 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 = _igraph.EdgeSeq.select(self, *args) - - 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 } - for keyword, value in kwds.iteritems(): - 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 = "%s_%s" % (attr,op) - func = operators["eq"] - - if attr[0] == '_': - if attr == "_source" or attr == "_from": - values = [e.source for e in es] - if op == "in" or op == "notin": - value = _ensure_set(value) - elif attr == "_target" or attr == "_to": - values = [e.target for e in es] - if op == "in" or op == "notin": - value = _ensure_set(value) - 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 - -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 klass, methods in decorated_methods.iteritems(): - 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")) - -_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 - if name in ("layout_auto", "layout_sugiyama"): - continue - setattr(Graph, name, _layout_method_wrapper(getattr(Graph, name))) - -############################################################## -# 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) -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. - - 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 pair, eids in multiplicities.iteritems(): - # 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.""" - 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"), - # 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") - ] - 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") - - -def read(filename, *args, **kwds): - """Loads a graph from the given filename. - - This is just a convenience function, calls L{Graph.Read} directly. - All arguments are passed unchanged to L{Graph.Read} - - @param filename: the name of the file to be loaded - """ - return Graph.Read(filename, *args, **kwds) -load=read - -def write(graph, filename, *args, **kwds): - """Saves a graph to the given file. - - This is just a convenience function, calls L{Graph.write} directly. - All arguments are passed unchanged to L{Graph.write} - - @param graph: the graph to be saved - @param filename: the name of the file to be written - """ - return graph.write(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 = configuration.init() -del construct_graph_from_formula diff --git a/igraph/compat.py b/igraph/compat.py deleted file mode 100644 index 5c9bbc71a..000000000 --- a/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/igraph/cut.py b/igraph/cut.py deleted file mode 100644 index 6d0100ab8..000000000 --- a/igraph/cut.py +++ /dev/null @@ -1,200 +0,0 @@ -# vim:ts=4:sw=4:sts=4:et -# -*- coding: utf-8 -*- -"""Classes representing cuts and flows on graphs.""" - -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. - - 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: - - - C{value} - the value (capacity) of the cut. It is equal to - the number of edges if there are no capacities on the - edges. - - - C{partition} - vertex IDs in the parts created - after removing edges in the cut - - - C{cut} - edge IDs in the cut - - - C{es} - an edge selector restricted to the edges - in the cut. - - You can use indexing on this object to obtain lists of vertex IDs - for both sides of the partition. - - This class is usually not instantiated directly, everything - is taken care of by the functions that return cuts. - - Examples: - - >>> from igraph import Graph - >>> g = Graph.Ring(20) - >>> mc = g.mincut() - >>> print mc.value - 2.0 - >>> print min(map(len, mc)) - 1 - >>> mc.es["color"] = "red" - """ - - # pylint: disable-msg=R0913 - 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 - the functions that return cuts. - """ - # Input validation - if partition is None or cut is None: - raise ValueError("partition and cut must be given") - - # Set up a membership vector, initialize parent class - membership = [1] * graph.vcount() - for vid in partition: - membership[vid] = 0 - VertexClustering.__init__(self, graph, membership) - - if value is None: - # Value of the cut not given, count the number of edges - value = len(cut) - self._value = float(value) - - self._partition = sorted(partition) - self._cut = cut - - def __repr__(self): - 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) - - # pylint: disable-msg=C0103 - @property - def es(self): - """Returns an edge selector restricted to the cut""" - return self._graph.es.select(self.cut) - - @property - def partition(self): - """Returns the vertex IDs partitioned according to the cut""" - return list(self) - - @property - def cut(self): - """Returns the edge IDs in the cut""" - return self._cut - - @property - def value(self): - """Returns the sum of edge capacities in the cut""" - return self._value - - -class Flow(Cut): - """A flow of a given graph. - - This is a simple class used to represent flows returned by - L{Graph.maxflow}. It has the following attributes: - - - C{graph} - the graph on which this flow is defined - - - 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 - flow on edge M{i}. For undirected graphs, the direction of - the flow is not constrained (since the edges are undirected), - hence positive flow always means a flow from the smaller vertex - ID to the larger, while negative flow means a flow from the - larger vertex ID to the smaller. - - - C{cut} - edge IDs in the minimal cut corresponding to - the flow. - - - C{partition} - vertex IDs in the parts created - after removing edges in the cut - - - C{es} - an edge selector restricted to the edges - in the cut. - - This class is usually not instantiated directly, everything - is taken care of by L{Graph.maxflow}. - - Examples: - - >>> from igraph import Graph - >>> g = Graph.Ring(20) - >>> mf = g.maxflow(0, 10) - >>> print mf.value - 2.0 - >>> mf.es["color"] = "red" - """ - - # pylint: disable-msg=R0913 - def __init__(self, graph, value, flow, cut, partition): - """Initializes the flow. - - This should not be called directly, everything is - taken care of by L{Graph.maxflow}. - """ - super(Flow, self).__init__(graph, value, 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) - - 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) - - @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 - undirected), hence positive flow always means a flow from the smaller - vertex ID to the larger, while negative flow means a flow from the - larger vertex ID to the smaller. - """ - return self._flow - - - diff --git a/igraph/drawing/__init__.py b/igraph/drawing/__init__.py deleted file mode 100644 index 17f9b9b5c..000000000 --- a/igraph/drawing/__init__.py +++ /dev/null @@ -1,487 +0,0 @@ -""" -Drawing and plotting routines for IGraph. - -Plotting is dependent on the C{pycairo} library which provides Python bindings -to the popular U{Cairo library}. This means that -if you don't have U{pycairo} 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 -U{Mozilla Firefox} (free) or edit it in -U{Inkscape} (free), U{Skencil} -(formerly known as Sketch, also free) or Adobe Illustrator (not free, therefore -I'm not linking to it :)). -""" - -from __future__ import with_statement - -from cStringIO import StringIO -from warnings import warn - -import os -import platform -import time - -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.utils import _is_running_in_ipython, named_temporary_file - -__all__ = ["BoundingBox", "DefaultGraphDrawer", "Plot", "Point", "Rectangle", "plot"] - -__license__ = "GPL" - -cairo = find_cairo() - -##################################################################### - -class Plot(object): - """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 that I'm aware of are: - - - 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. - """ - - # 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. - - @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.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 xrange(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 - - # pylint: disable-msg=W0142 - # W0142: used * or ** magic - 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}. - """ - 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.""" - 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) - 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() - if hasattr(result, "encode"): - return result.encode("utf-8") # for Python 2.x - else: - return result.decode("utf-8") # for Python 3.x - - @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): - """Plots the given object to the given target. - - Positional and keyword arguments not explicitly mentioned here will be - passed down to the C{__plot__} method of the object being plotted. - Since you are most likely interested in the keyword arguments available - for graph plots, see L{Graph.__plot__} as well. - - @param obj: the object to be plotted - @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{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. - - @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 - with two integers, it is interpreted as the width and height of the plot - (in pixels for PNG images and on-screen plots, or in points for PDF, - 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. - - @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}. - - @keyword 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. - - @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. - - @keyword inline: whether to try and show the plot object inline in the - current IPython notebook. Passing ``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} - is C{None}. - - @return: an appropriate L{Plot} object. - - @see: Graph.__plot__ - """ - if not isinstance(bbox, BoundingBox): - bbox = BoundingBox(bbox) - - result = Plot(target, bbox, background=kwds.get("background", "white")) - - 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 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 - - # 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, basestring): - result.save() - - # Also return the plot itself - return result - -##################################################################### - diff --git a/igraph/drawing/baseclasses.py b/igraph/drawing/baseclasses.py deleted file mode 100644 index d06e76e4a..000000000 --- a/igraph/drawing/baseclasses.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -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): - """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") - -##################################################################### - -# 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} - 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 - - 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() - -##################################################################### - -class AbstractXMLRPCDrawer(AbstractDrawer): - """Abstract drawer that uses a remote service via XML-RPC - to draw something on a remote display. - """ - - 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 - C{None}, requests will be directed to the server proxy object - constructed by C{xmlrpclib.ServerProxy}; if not C{None}, the - given attribute will be looked up in the server proxy object. - """ - import xmlrpclib - url = self._resolve_hostname(url) - self.server = xmlrpclib.ServerProxy(url) - if service is None: - self.service = self.server - else: - self.service = getattr(self.server, service) - - @staticmethod - def _resolve_hostname(url): - """Parses the given URL, resolves the hostname to an IP address - 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 - import re - - url_parts = urlparse(url) - hostname = url_parts.netloc - if re.match("[0-9.:]+$", hostname): - # the hostname is already an IP address, possibly with a port - return url - - from socket import gethostbyname - if ":" in hostname: - 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/igraph/drawing/colors.py b/igraph/drawing/colors.py deleted file mode 100644 index 14fa328e8..000000000 --- a/igraph/drawing/colors.py +++ /dev/null @@ -1,3118 +0,0 @@ -# vim:ts=4:sw=4:sts=4:et -# -*- coding: utf-8 -*- -""" -Color handling functions. -""" - -__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 igraph.datatypes import Matrix -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"] - -class Palette(object): - """Base class of color palettes. - - Color palettes are mappings that assign integers from the range - 0..M{n-1} to colors (4-tuples). M{n} is called the size or length - of the palette. C{igraph} comes with a number of predefined palettes, - so this class is useful for you only if you want to define your - own palette. This can be done by subclassing this class and implementing - the L{Palette._get} method as necessary. - - 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 = {} - - def clear_cache(self): - """Clears the result cache. - - The return values of L{Palette.get} are cached. Use this method - to clear the cache. - """ - self._cache = {} - - def get(self, v): - """Returns the given color from the palette. - - Values are cached: if the specific value given has already been - looked up, its value will be returned from the cache instead of - calculating it again. Use L{Palette.clear_cache} to clear the cache - if necessary. - - @note: you shouldn't override this method in subclasses, override - L{_get} instead. If you override this method, lookups in the - L{known_colors} dict won't work, so you won't be able to refer to - colors by names or RGBA quadruplets, only by integer indices. The - caching functionality will disappear as well. However, - feel free to override this method if this is exactly the behaviour - you want. - - @param v: the color to be retrieved. If it is an integer, it is - passed to L{Palette._get} to be translated to an RGBA quadruplet. - Otherwise it is passed to L{color_name_to_rgb()} to determine the - RGBA values. - - @return: the color as an RGBA quadruplet""" - if isinstance(v, list): - v = tuple(v) - try: - return self._cache[v] - except KeyError: - pass - if isinstance(v, (int, long)): - if v < 0: - raise IndexError("color index must be non-negative") - if v >= self._length: - raise IndexError("color index too large") - result = self._get(v) - else: - result = color_name_to_rgba(v) - self._cache[v] = result - return result - - def get_many(self, colors): - """Returns multiple colors from the palette. - - Values are cached: if the specific value given has already been - looked upon, its value will be returned from the cache instead of - calculating it again. Use L{Palette.clear_cache} to clear the cache - if necessary. - - @param colors: the list of colors to be retrieved. The palette class - tries to make an educated guess here: if it is not possible to - interpret the value you passed here as a list of colors, the - class will simply try to interpret it as a single color by - forwarding the value to L{Palette.get}. - @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)): - # Single color name or index - return [self.get(colors)] - # Multiple colors - return [self.get(color) for color in colors] - - def _get(self, v): - """Override this method in a subclass to create a custom palette. - - You can safely assume that v is an integer in the range 0..M{n-1} - where M{n} is the size of the palette. - - @param v: numerical index of the color to be retrieved - @return: a 4-tuple containing the RGBA values""" - raise NotImplementedError("abstract class") - - __getitem__ = get - - @property - def length(self): - """Returns the number of colors in this palette""" - return self._length - - 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 - - Supported keyword arguments 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}. - - - C{grid_width}: line width of the grid that separates palette cells. - 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{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}. - """ - border_width = float(kwds.get("border_width", 1.)) - grid_width = float(kwds.get("grid_width", 0.)) - orientation = str_to_orientation(kwds.get("orientation", "lr")) - - # Construct a matrix and plot that - indices = 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) - - def __repr__(self): - return "<%s with %d colors>" % (self.__class__.__name__, self._length) - - -class GradientPalette(Palette): - """Base class for gradient palettes - - Gradient palettes contain a gradient between two given colors. - - Example: - - >>> pal = GradientPalette("red", "blue", 5) - >>> pal.get(0) - (1.0, 0.0, 0.0, 1.0) - >>> pal.get(2) - (0.5, 0.0, 0.5, 1.0) - >>> pal.get(4) - (0.0, 0.0, 1.0, 1.0) - """ - - def __init__(self, color1, color2, n=256): - """Creates a gradient palette. - - @param color1: the color where the gradient starts. - @param color2: the color where the gradient ends. - @param n: the number of colors in the palette. - """ - Palette.__init__(self, n) - self._color1 = color_name_to_rgba(color1) - self._color2 = color_name_to_rgba(color2) - - def _get(self, v): - """Returns the color corresponding to the given color index. - - @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)) - - -class AdvancedGradientPalette(Palette): - """Advanced gradient that consists of more than two base colors. - - Example: - - >>> pal = AdvancedGradientPalette(["red", "black", "blue"], n=9) - >>> pal.get(2) - (0.5, 0.0, 0.0, 1.0) - >>> pal.get(7) - (0.0, 0.0, 0.75, 1.0) - """ - - def __init__(self, colors, indices=None, n=256): - """Creates an advanced gradient palette - - @param colors: the colors in the gradient. - @param indices: the color indices belonging to the given colors. If - C{None}, the colors are distributed equidistantly - @param n: the total number of colors in the palette - """ - Palette.__init__(self, n) - - if indices is None: - diff = float(n-1) / (len(colors)-1) - indices = [i * diff for i in xrange(len(colors))] - elif not hasattr(indices, "__iter__"): - indices = [float(x) for x in indices] - self._indices, self._colors = 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)] - - def _get(self, v): - """Returns the color corresponding to the given color index. - - @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: - 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.) - -class RainbowPalette(Palette): - """A palette that varies the hue of the colors along a scale. - - Colors in a rainbow palette all have the same saturation, value and - alpha components, while the hue is varied between two given extremes - linearly. This palette has the advantage that it wraps around nicely - if the hue is varied between zero and one (which is the default). - - Example: - - >>> pal = RainbowPalette(n=120) - >>> pal.get(0) - (1.0, 0.0, 0.0, 1.0) - >>> pal.get(20) - (1.0, 1.0, 0.0, 1.0) - >>> pal.get(40) - (0.0, 1.0, 0.0, 1.0) - >>> pal = RainbowPalette(n=120, s=1, v=0.5, alpha=0.75) - >>> pal.get(60) - (0.0, 0.5, 0.5, 0.75) - >>> pal.get(80) - (0.0, 0.0, 0.5, 0.75) - >>> pal.get(100) - (0.5, 0.0, 0.5, 0.75) - >>> pal = RainbowPalette(n=120) - >>> pal2 = RainbowPalette(n=120, start=0.5, end=0.5) - >>> pal.get(60) == pal2.get(0) - True - >>> pal.get(90) == pal2.get(30) - True - - This palette was modeled after the C{rainbow} command of R. - """ - - def __init__(self, n=256, s=1, v=1, start=0, end=1, alpha=1): - """Creates a rainbow palette. - - @param n: the number of colors in the palette. - @param s: the saturation of the colors in the palette. - @param v: the value component of the colors in the palette. - @param start: the hue at which the rainbow begins (between 0 and 1). - @param end: the hue at which the rainbow ends (between 0 and 1). - @param alpha: the alpha component of the colors in the palette. - """ - Palette.__init__(self, n) - self._s = float(clamp(s, 0, 1)) - self._v = float(clamp(v, 0, 1)) - self._alpha = float(clamp(alpha, 0, 1)) - self._start = float(start) - if end == self._start: - end += 1 - self._dh = (end - self._start) / n - - def _get(self, v): - """Returns the color corresponding to the given color index. - - @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) - - -class PrecalculatedPalette(Palette): - """A palette that returns colors from a pre-calculated list of colors""" - - def __init__(self, l): - """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): - if isinstance(color, basestring): - color = color_name_to_rgba(color) - self._cache[idx] = color - - def _get(self, v): - """This method will only be called if the requested color index is - outside the size of the palette. In that case, we throw an exception""" - raise ValueError("palette index outside bounds: %s" % v) - - -class ClusterColoringPalette(PrecalculatedPalette): - """A palette suitable for coloring vertices when plotting a clustering. - - This palette tries to make sure that the colors are easily distinguishable. - This is achieved by using a set of base colors and their lighter and darker - variants, depending on the number of elements in the palette. - - When the desired size of the palette is less than or equal to the number of - base colors (denoted by M{n}), only the bsae colors will be used. When the - size of the palette is larger than M{n} but less than M{2*n}, the base colors - and their lighter variants will be used. Between M{2*n} and M{3*n}, the - base colors and their lighter and darker variants will be used. Above M{3*n}, - more darker and lighter variants will be generated, but this makes the individual - colors less and less distinguishable. - """ - - def __init__(self, n): - 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) - colors = base_colors[:] - - blocks_to_add = ceil(float(n - num_base_colors) / num_base_colors) - ratio_increment = 1.0 / (ceil(blocks_to_add / 2.0) + 1) - - adding_darker = True - ratio = ratio_increment - while len(colors) < n: - if adding_darker: - new_block = [darken(color, ratio) for color in base_colors] - else: - new_block = [lighten(color, ratio) for color in base_colors] - ratio += ratio_increment - colors.extend(new_block) - adding_darker = not adding_darker - - colors = colors[0:n] - PrecalculatedPalette.__init__(self, colors) - - -def clamp(value, min_value, max_value): - """Clamps the given value between min and max""" - if value > max_value: - return max_value - if value < min_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. - - This is done by calling L{color_name_to_rgba} and then throwing away - the alpha value. - - @see: color_name_to_rgba for more details about what formats are - understood by this function. - """ - 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. - - Examples: - - >>> color_name_to_rgba("red") - (1.0, 0.0, 0.0, 1.0) - >>> color_name_to_rgba("#ff8000") == (1.0, 128/255.0, 0.0, 1.0) - True - >>> color_name_to_rgba("#ff800080") == (1.0, 128/255.0, 0.0, 128/255.0) - True - >>> color_name_to_rgba("#08f") == (0.0, 136/255.0, 1.0, 1.0) - True - >>> color_name_to_rgba("rgb(100%, 50%, 0%)") - (1.0, 0.5, 0.0, 1.0) - >>> color_name_to_rgba("rgba(100%, 50%, 0%, 25%)") - (1.0, 0.5, 0.0, 0.25) - >>> color_name_to_rgba("hsla(120, 100%, 50%, 0.5)") - (0.0, 1.0, 0.0, 0.5) - >>> color_name_to_rgba("hsl(60, 100%, 50%)") - (1.0, 1.0, 0.0, 1.0) - >>> color_name_to_rgba("hsv(60, 100%, 100%)") - (1.0, 1.0, 0.0, 1.0) - - @param color: the color to be converted in one of the following formats: - - B{CSS3 color specification}: C{#rrggbb}, C{#rgb}, C{#rrggbbaa}, C{#rgba}, - C{rgb(red, green, blue)}, C{rgba(red, green, blue, alpha)}, - C{hsl(hue, saturation, lightness)}, C{hsla(hue, saturation, lightness, alpha)}, - C{hsv(hue, saturation, value)} and C{hsva(hue, saturation, value, alpha)} - where the components are given as hexadecimal numbers in the first four - cases and as decimals or percentages (0%-100%) in the remaining cases. - Red, green and blue components are between 0 and 255; hue is between 0 - and 360; saturation, lightness and value is between 0 and 100; alpha is - between 0 and 1. - - B{Valid HTML color names}, i.e. those that are present in the HTML 4.0 - specification - - B{Valid X11 color names}, see U{http://en.wikipedia.org/wiki/X11_color_names} - - B{Red-green-blue components} given separately in either a comma-, slash- or - whitespace-separated string or a list or a tuple, in the range of 0-255. - An alpha value of 255 (maximal opacity) will be assumed. - - B{Red-green-blue-alpha components} given separately in either a comma-, slash- - or whitespace-separated string or a list or a tuple, in the range of 0-255 - - B{A single palette index} given either as a string or a number. Uses - the palette given in the C{palette} parameter of the method call. - @param palette: the palette to be used if a single number is passed to - the method. Must be an instance of L{colors.Palette}. - - @return: the RGBA values corresponding to the given color in a 4-tuple. - 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 hasattr(color, "__iter__"): - components = list(color) - else: - # A single index is given as a number - try: - components = palette.get(color) - except AttributeError: - raise ValueError("palette index used when no palette was given") - if len(components) < 4: - components += [1.] * (4 - len(components)) - else: - if color[0] == '#': - color = color[1:] - if len(color) == 3: - components = [int(i, 16) * 17. / 255. for i in color] - components.append(1.0) - elif len(color) == 4: - components = [int(i, 16) * 17. / 255. for i in color] - elif len(color) == 6: - components = [int(color[i:i+2], 16) / 255. 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)] - elif color.lower() in known_colors: - components = known_colors[color.lower()] - else: - color_mode = "rgba" - 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_mode = mode[:-1] - if mode[0] == "h": - maximums = (360.0, 100.0, 100.0, 1.0) - break - - if " " in color or "/" in color or "," in color: - color = color.replace(",", " ").replace("/", " ") - components = color.split() - for idx, comp in enumerate(components): - if comp[-1] == "%": - components[idx] = float(comp[:-1])/100. - else: - components[idx] = float(comp)/maximums[idx] - if len(components) < 4: - components += [1.] * (4 - len(components)) - if color_mode[:3] == "hsv": - components = hsva_to_rgba(*components) - elif color_mode[:3] == "hsl": - components = hsla_to_rgba(*components) - else: - components = palette.get(int(color)) - - # At this point, the components are floats - return tuple(clamp(val, 0., 1.) for val in components) - -def color_to_html_format(color): - """Formats a color given as a 3-tuple or 4-tuple in HTML format. - - The HTML format is simply given by C{#rrggbbaa}, where C{rr} gives - the red component in hexadecimal format, C{gg} gives the green - component C{bb} gives the blue component and C{gg} gives the - alpha level. The alpha level is optional. - """ - color = [int(clamp(component * 256, 0, 255)) for component in color] - if len(color) == 4: - 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. - - This is done by mixing the original color with black using the given - ratio. A ratio of 1.0 will yield a completely black color, a ratio - of 0.0 will yield the original color. The alpha values are left intact. - """ - ratio = 1.0 - ratio - red, green, blue, alpha = color - return (red * ratio, green * ratio, blue * ratio, alpha) - -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. - - Each of the HSLA coordinates must be in the range [0, 1]. - """ - # 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. - h1 = int(h1) - if h1 < 3: - if h1 < 1: - return (c+m, x+m, m, alpha) - elif h1 < 2: - return (x+m, c+m, m, alpha) - else: - return (m, c+m, x+m, alpha) - else: - if h1 < 4: - return (m, x+m, c+m, alpha) - elif h1 < 5: - return (x+m, m, c+m, alpha) - else: - return (c+m, m, x+m, alpha) - -def hsl_to_rgb(h, s, l): - """Converts a color given by its HSL coordinates (hue, saturation, - lightness) to RGB coordinates. - - Each of the HSL coordinates must be in the range [0, 1]. - """ - return hsla_to_rgba(h, s, l)[:3] - -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. - - Each of the HSVA coordinates must be in the range [0, 1]. - """ - # 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 - h1 = int(h1) - if h1 < 3: - if h1 < 1: - return (c+m, x+m, m, alpha) - elif h1 < 2: - return (x+m, c+m, m, alpha) - else: - return (m, c+m, x+m, alpha) - else: - if h1 < 4: - return (m, x+m, c+m, alpha) - elif h1 < 5: - return (x+m, m, c+m, alpha) - else: - return (c+m, m, x+m, alpha) - -def hsv_to_rgb(h, s, v): - """Converts a color given by its HSV coordinates (hue, saturation, - value) to RGB coordinates. - - Each of the HSV coordinates must be in the range [0, 1]. - """ - 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). - - Each of the RGBA coordinates must be in the range [0, 1]. - """ - alpha = float(alpha) - rgb_min, rgb_max = float(min(r, g, b)), float(max(r, g, b)) - - if rgb_min == rgb_max: - return 0.0, 0.0, rgb_min, alpha - - lightness = (rgb_min + rgb_max) / 2.0 - d = rgb_max - rgb_min - if lightness > 0.5: - sat = d / (2 - rgb_max - rgb_min) - else: - sat = d / (rgb_max + rgb_min) - - d *= 6.0 - if rgb_max == r: - hue = (g - b) / d - if g < b: - hue += 1 - elif rgb_max == g: - hue = 1/3.0 + (b - r) / d - else: - 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). - - Each of the RGBA coordinates must be in the range [0, 1]. - """ - # This is based on the formulae found at: - # http://en.literateprograms.org/RGB_to_HSV_color_space_conversion_(C) - rgb_min, rgb_max = float(min(r, g, b)), float(max(r, g, b)) - alpha = float(alpha) - value = float(rgb_max) - if value <= 0: - return 0.0, 0.0, 0.0, alpha - sat = 1.0 - rgb_min / value - if sat <= 0: - return 0.0, 0.0, value, alpha - d = rgb_max - rgb_min - r = (r - rgb_min) / d - g = (g - rgb_min) / d - b = (b - rgb_min) / d - rgb_max = max(r, g, b) - if rgb_max == r: - hue = 0.0 + (g - b) / 6.0 - if hue < 0: - hue += 1 - elif rgb_max == g: - hue = 1/3.0 + (b - r) / 6.0 - else: - 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). - - Each of the RGB coordinates must be in the range [0, 1]. - """ - 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). - - Each of the RGB coordinates must be in the range [0, 1]. - """ - 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. - - This is done by mixing the original color with white using the given - ratio. A ratio of 1.0 will yield a completely white color, a ratio - 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)} - -palettes = { - "gray": GradientPalette("black", "white"), - "red-blue": GradientPalette("red", "blue"), - "red-purple-blue": AdvancedGradientPalette(["red", "purple", "blue"]), - "red-green": GradientPalette("red", "green"), - "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%)"]) -} - - diff --git a/igraph/drawing/edge.py b/igraph/drawing/edge.py deleted file mode 100644 index 077e1fc15..000000000 --- a/igraph/drawing/edge.py +++ /dev/null @@ -1,419 +0,0 @@ -""" -Drawers for various edge styles in graph plots. -""" - -__all__ = ["AbstractEdgeDrawer", "AlphaVaryingEdgeDrawer", - "ArrowEdgeDrawer", "DarkToLightEdgeDrawer", - "LightToDarkEdgeDrawer", "TaperedEdgeDrawer"] - -__license__ = "GPL" - -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(object): - """Abstract edge drawer object from which all concrete edge drawer - implementations are derived.""" - - 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 - """ - 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.""" - class VisualEdgeBuilder(AttributeCollectorBase): - """Builder that collects some visual properties of an edge for - drawing""" - _kwds_prefix = "edge_" - arrow_size = 1.0 - arrow_width = 1.0 - 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 - 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. - - 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. - """ - ctx = self.context - 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) - ctx.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: # TODO - return self.draw_loop_edge(edge, src_vertex) - - ctx = self.context - ctx.set_source_rgba(*edge.color) - ctx.set_line_width(edge.width) - ctx.move_to(*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) - ctx.curve_to(aux1[0], aux1[1], aux2[0], aux2[1], *dest_vertex.position) - else: - ctx.line_to(*dest_vertex.position) - - 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., \ - (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): - """Edge drawer implementation that draws undirected edges as - straight lines and directed edges as arrows. - """ - - def draw_directed_edge(self, edge, src_vertex, dest_vertex): - 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) - 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 - 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 - - xt0, yt0 = x3, y3 - 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) - ctx.move_to(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_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 - 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 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) - 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. * edge.arrow_size - arrow_width = 10. / 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 - ctx.line_to(x_arrow_mid, y_arrow_mid) - - # Draw the edge - ctx.stroke() - - - # Draw the arrow head - ctx.move_to(x2, y2) - ctx.line_to(*aux_points[0]) - ctx.line_to(*aux_points[1]) - ctx.line_to(x2, y2) - ctx.fill() - - - -class TaperedEdgeDrawer(AbstractEdgeDrawer): - """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. - """ - - def draw_directed_edge(self, edge, src_vertex, dest_vertex): - 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 - ) - - ctx = self.context - - # 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. - 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)) - ] - ctx.move_to(*dest_pos) - ctx.line_to(*aux_points[0]) - ctx.line_to(*aux_points[1]) - ctx.line_to(*dest_pos) - ctx.fill() - - -class AlphaVaryingEdgeDrawer(AbstractEdgeDrawer): - """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(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.), ) - - def draw_directed_edge(self, edge, src_vertex, dest_vertex): - if src_vertex == dest_vertex: # TODO - return self.draw_loop_edge(edge, src_vertex) - - src_pos, dest_pos = src_vertex.position, dest_vertex.position - ctx = self.context - - # Set up the gradient - lg = cairo.LinearGradient(src_pos[0], src_pos[1], dest_pos[0], dest_pos[1]) - edge_color = edge.color[:3] + self.alpha_at_src - edge_color_end = edge_color[:3] + self.alpha_at_dest - lg.add_color_stop_rgba(0, *edge_color) - lg.add_color_stop_rgba(1, *edge_color_end) - - # Draw the edge - ctx.set_source(lg) - ctx.set_line_width(edge.width) - ctx.move_to(*src_pos) - ctx.line_to(*dest_pos) - ctx.stroke() - - -class LightToDarkEdgeDrawer(AlphaVaryingEdgeDrawer): - """Edge drawer implementation that draws undirected edges as - straight lines and directed edges by using an alpha value of - zero (total transparency) at the source and an alpha value of - one (full opacity) at the destination. The alpha value is - interpolated in-between. - """ - - def __init__(self, context): - super(LightToDarkEdgeDrawer, self).__init__(context, 0.0, 1.0) - - -class DarkToLightEdgeDrawer(AlphaVaryingEdgeDrawer): - """Edge drawer implementation that draws undirected edges as - straight lines and directed edges by using an alpha value of - one (full opacity) at the source and an alpha value of zero - (total transparency) at the destination. The alpha value is - interpolated in-between. - """ - - def __init__(self, context): - super(DarkToLightEdgeDrawer, self).__init__(context, 1.0, 0.0) - diff --git a/igraph/drawing/graph.py b/igraph/drawing/graph.py deleted file mode 100644 index a532d104f..000000000 --- a/igraph/drawing/graph.py +++ /dev/null @@ -1,919 +0,0 @@ -""" -Drawing routines to draw graphs. - -This module contains routines to draw graphs on: - - - Cairo surfaces (L{DefaultGraphDrawer}) - - UbiGraph displays (L{UbiGraphDrawer}, see U{http://ubietylab.net/ubigraph}) - -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 collections import defaultdict -from itertools import izip -from math import atan2, cos, pi, sin, tan -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.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 - -__all__ = ["DefaultGraphDrawer", "UbiGraphDrawer", "CytoscapeGraphDrawer"] -__license__ = "GPL" - -cairo = find_cairo() - -##################################################################### - -# 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") - - 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, basestring): - reverse = reverse.lower().startswith("desc") - attrs = graph.es[edge_order_by] - edge_order = sorted(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, basestring): - reverse = reverse.lower().startswith("desc") - attrs = graph.vs[vertex_order_by] - vertex_order = sorted(range(len(attrs)), key=attrs.__getitem__, - reverse=bool(reverse)) - - 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() - 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"] - - # 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): - group_iter = mark_groups.iteritems() - elif hasattr(mark_groups, "__iter__"): - # Lists, tuples, iterators etc - group_iter = iter(mark_groups) - else: - # False - group_iter = {}.iteritems() - - # 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 = izip(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 = izip(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 = izip(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. - # 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. - 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: - 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: - 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. - 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) - 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. - """ - - 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 - ) - - 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.iteritems(): - display.set_vertex_style_attribute(0, k, str(v)) - for k, v in self.edge_defaults.iteritems(): - 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 xrange(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 izip(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 izip(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)) - -##################################################################### - -class CytoscapeGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): - """Graph drawer that sends/receives graphs to/from Cytoscape using - CytoscapeRPC. - - This graph drawer cooperates with U{Cytoscape} - using U{CytoscapeRPC}. - You need to install the CytoscapeRPC plugin first and start the - XML-RPC server on a given port (port 9000 by default) from the - appropriate Plugins submenu in Cytoscape. - - Graph, vertex and edge attributes are transferred to Cytoscape whenever - possible (i.e. when a suitable mapping exists between a Python type - and a Cytoscape type). If there is no suitable Cytoscape type for a - Python type, the drawer will use a string attribute on the Cytoscape - side and invoke C{str()} on the Python attributes. - - If an attribute to be created on the Cytoscape side already exists with - a different type, an underscore will be appended to the attribute name - to resolve the type conflict. - - You can use the C{network_id} attribute of this class to figure out the - network ID of the last graph drawn with this drawer. - """ - - 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") - self.network_id = None - - 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. - @param create_view: whether to create a view for the network - in Cytoscape.The default is C{True}. - @keyword node_ids: specifies the identifiers of the nodes to - be used in Cytoscape. This must either be the name of a - 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 - - cy = self.service - - # Create the network - if not create_view: - 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.") - network_id = cy.createNetwork(name) - else: - network_id = cy.createNetwork(name) - self.network_id = network_id - - # Create the nodes - if "node_ids" in kwds: - node_ids = kwds["node_ids"] - if isinstance(node_ids, basestring): - node_ids = graph.vs[node_ids] - else: - node_ids = xrange(graph.vcount()) - node_ids = [str(identifier) for identifier in node_ids] - cy.createNodes(network_id, node_ids) - - # Create the edges - edgelists = [[], []] - 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 - ) - - if "layout" in kwds: - # Calculate/get the layout of the graph - 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))) - else: - # Ask Cytoscape to perform the default layout so the user can - # at least see something in Cytoscape while the attributes are - # being transferred - cy.performDefaultLayout(network_id) - - # Send the network attributes - attr_names = set(cy.getNetworkAttributeNames()) - for attr in graph.attributes(): - cy_type, value = self.infer_cytoscape_type([graph[attr]]) - value = value[0] - if value is None: - continue - - # Resolve type conflicts (if any) - try: - while attr in attr_names and \ - cy.getNetworkAttributeType(attr) != cy_type: - attr += "_" - except Fault: - # getNetworkAttributeType is not available in some older versions - # so we simply pass here - pass - cy.addNetworkAttributes(attr, cy_type, {network_id: value}) - - # Send the node attributes - 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) - # Resolve type conflicts (if any) - while attr in attr_names and \ - cy.getNodeAttributeType(attr) != cy_type: - attr += "_" - # Send the attribute values - cy.addNodeAttributes(attr, cy_type, values, True) - - # Send the edge attributes - 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) - # Resolve type conflicts (if any) - 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): - """Fetches the network with the given name from Cytoscape. - - When fetching networks from Cytoscape, the C{canonicalName} attributes - of vertices and edges are not converted by default. Use the - C{keep_canonical_names} parameter to retrieve these attributes as well. - - @param name: the name of the network in Cytoscape. - @param directed: whether the network is directed. - @param keep_canonical_names: whether to keep the C{canonicalName} - vertex/edge attributes that are added automatically by Cytoscape - @return: an appropriately constructed igraph L{Graph}.""" - from igraph import Graph - - cy = self.service - - # Check the version number. Anything older than 1.3 is bad. - version = cy.version() - if " " in version: - 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") - - # 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] - if not network_id: - raise ValueError("no such network: %r" % name) - elif len(network_id) > 1: - raise ValueError("more than one network exists with name: %r" % name) - network_id = network_id[0] - - # Fetch the list of all the nodes and edges - vertices = cy.getNodes(network_id) - edges = cy.getEdges(network_id) - n, m = len(vertices), len(edges) - - # Fetch the graph attributes - graph_attrs = cy.getNetworkAttributes(network_id) - - # Fetch the vertex attributes - vertex_attr_names = cy.getNodeAttributeNames() - vertex_attrs = {} - for attr_name in vertex_attr_names: - if attr_name == "canonicalName" and not keep_canonical_names: - 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] - ) - attrs = [None] * n - for idx, value in izip(filtered, values): - attrs[idx] = value - vertex_attrs[attr_name] = attrs - - # Fetch the edge attributes - edge_attr_names = cy.getEdgeAttributeNames() - edge_attrs = {} - for attr_name in edge_attr_names: - if attr_name == "canonicalName" and not keep_canonical_names: - 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] - ) - attrs = [None] * m - for idx, value in izip(filtered, values): - attrs[idx] = value - edge_attrs[attr_name] = attrs - - # Create a vertex name index - vertex_name_index = dict((v, k) for k, v in enumerate(vertices)) - del vertices - - # Remap the edges list to numeric IDs - edge_list = [] - for edge in edges: - parts = edge.split() - 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) - - @staticmethod - def infer_cytoscape_type(values): - """Returns a Cytoscape type that can be used to represent all the - values in `values` and an appropriately converted copy of `values` that - is suitable for an XML-RPC call. Note that the string type in - Cytoscape is used as a catch-all type; if no other type fits, attribute - values will be converted to string and then posted to Cytoscape. - - ``None`` entries are allowed in `values`, they will be ignored on the - Cytoscape side. - """ - 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): - 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 - ] - -##################################################################### - -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. - - 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. - - 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 - L{GephiGraphStreamer} that is responsible for generating the JSON objects, - encoding them and writing them to a file-like object. If you want to customize - the encoding process, this is the object where you can tweak things to your taste. - """ - - def __init__(self, conn=None, *args, **kwds): - """Constructs a Gephi graph streaming drawer that will post graphs to the - given Gephi connection. If C{conn} is C{None}, the remaining arguments of - the constructor are forwarded intact to the constructor of - L{GephiConnection} in order to create a connection. This means that any of - the following are valid: - - - C{GephiGraphStreamingDrawer()} will construct a drawer that connects to - workspace 0 of the local Gephi instance on port 8080. - - - C{GephiGraphStreamingDrawer(workspace=2)} will connect to workspace 2 - of the local Gephi instance on port 8080. - - - C{GephiGraphStreamingDrawer(port=1234)} will connect to workspace 0 - of the local Gephi instance on port 1234. - - - C{GephiGraphStreamingDrawer(host="remote", port=1234, workspace=7)} - will connect to workspace 7 of the Gephi instance on host C{remote}, - port 1234. - - - C{GephiGraphStreamingDrawer(url="http://remote:1234/workspace7)} is - the same as above, but with an explicit URL. - """ - super(GephiGraphStreamingDrawer, self).__init__() - - from igraph.remote.gephi import GephiGraphStreamer, GephiConnection - self.connection = conn or GephiConnection(*args, **kwds) - self.streamer = GephiGraphStreamer() - - def draw(self, graph, *args, **kwds): - """Draws (i.e. sends) the given graph to the destination of the drawer using - the Gephi graph streaming API. - - The following keyword arguments are allowed: - - - ``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")) - diff --git a/igraph/layout.py b/igraph/layout.py deleted file mode 100644 index 7f347b070..000000000 --- a/igraph/layout.py +++ /dev/null @@ -1,447 +0,0 @@ -# vim:ts=4:sw=4:sts=4:et -# -*- coding: utf-8 -*- -""" -Layout-related code in the IGraph library. - -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"""\ -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. - - A layout is practically a list of coordinates in an n-dimensional - space. This class is generic in the sense that it can store coordinates - in any n-dimensional space. - - Layout objects are not associated directly with a graph. This is deliberate: - there were times when I worked with almost identical copies of the same - graph, the only difference was that they had different colors assigned to - the vertices. It was particularly convenient for me to use the same layout - for all of them, especially when I made figures for a paper. However, - C{igraph} will of course refuse to draw a graph with a layout that has - less coordinates than the node count of the graph. - - Layouts behave exactly like lists when they are accessed using the item - index operator (C{[...]}). They can even be iterated through. Items - returned by the index operator are only copies of the coordinates, - but the stored coordinates can be modified by directly assigning to - an index. - - >>> layout = Layout([(0, 1), (0, 2)]) - >>> coords = layout[1] - >>> print coords - [0, 2] - >>> coords = (0, 3) - >>> print layout[1] - [0, 2] - >>> layout[1] = coords - >>> print layout[1] - [0, 3] - """ - - def __init__(self, coords=None, dim=None): - """Constructor. - - @param coords: the coordinates to be stored in the layout. - @param dim: the number of dimensions. If C{None}, the number of - dimensions is determined automatically from the length of the first - item of the coordinate list. If there are no entries in the coordinate - list, the default will be 2. Generally, this should be given if the - length of the coordinate list is zero, otherwise it should be left as - is. - """ - if coords: - self._coords = [list(coord) for coord in coords] - else: - self._coords = [] - - if dim is None: - if len(self._coords) == 0: - self._dim = 2 - else: - self._dim = len(self._coords[0]) - else: - 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) - - def __len__(self): - return len(self._coords) - - def __getitem__(self, idx): - return self._coords[idx] - - def __setitem__(self, idx, value): - if len(value) != self._dim: - raise ValueError("assigned item must have %d elements" % self._dim) - self._coords[idx] = list(value) - - def __delitem__(self, idx): - del self._coords[idx] - - def __copy__(self): - return self.__class__(self.coords, self.dim) - - def __repr__(self): - if not self.coords: - vertex_count = "no vertices" - elif len(self.coords) == 1: - vertex_count = "1 vertex" - else: - vertex_count = "%d vertices" % len(self.coords) - if self.dim == 1: - dim_count = "1 dimension" - else: - dim_count = "%d dimensions" % self.dim - return "<%s with %s and %s>" % (self.__class__.__name__, - vertex_count, dim_count) - - @property - def dim(self): - """Returns the number of dimensions""" - return self._dim - - @property - def coords(self): - """The coordinates as a list of lists""" - return [row[:] for row in self._coords] - - 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]]) - - def mirror(self, dim): - """Mirrors the layout along the given dimension(s) - - @param dim: the list of dimensions or a single dimension - """ - if isinstance(dim, int): - dim = [dim] - else: - dim = [int(x) for x in dim] - - for current_dim in 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. - - @param angle: the angle of the rotation, specified in degrees. - @param dim1: the first axis of the plane of the rotation. - @param dim2: the second axis of the plane of the rotation. - @keyword origin: the origin of the rotation. If not specified, the - origin will be the origin of the coordinate system. - """ - - origin = list(kwds.get("origin", [0.]*self._dim)) - if len(origin) != self._dim: - raise ValueError("origin must have %d dimensions" % self._dim) - - radian = angle * pi / 180. - 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] - - - def scale(self, *args, **kwds): - """Scales the layout. - - Scaling parameters can be provided either through the C{scale} keyword - argument or through plain unnamed arguments. If a single integer or - float is given, it is interpreted as a uniform multiplier to be applied - on all dimensions. If it is a list or tuple, its length must be equal to - the number of dimensions in the layout, and each element must be an - integer or float describing the scaling coefficient in one of the - dimensions. - - @keyword scale: scaling coefficients (integer, float, list or tuple) - @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)) - if len(origin) != self._dim: - raise ValueError("origin must have %d dimensions" % self._dim) - - scaling = kwds.get("scale") or args - if isinstance(scaling, (int, float)): - scaling = [scaling] - if len(scaling) == 0: - 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 - else: - scaling = scaling[0] - if len(scaling) != 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)] - - def translate(self, *args, **kwds): - """Translates the layout. - - The translation vector can be provided either through the C{v} keyword - argument or through plain unnamed arguments. If unnamed arguments are - used, the vector can be supplied as a single list (or tuple) or just as - a series of arguments. In all cases, the translation vector must have - the same number of dimensions as the layout. - - @keyword v: the translation vector - """ - v = kwds.get("v") or args - if len(v) == 0: - raise ValueError("translation vector must be given") - 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) - - for idx, row in enumerate(self._coords): - self._coords[idx] = [row[d]+v[d] for d in xrange(self._dim)] - - - 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 - layout is transformed to an angle, with min(x) corresponding to - the parameter called I{min_angle} and max(y) corresponding to - I{max_angle}. Angles are given in degrees, zero degree corresponds - to the direction pointing upwards. The Y coordinate is - interpreted as a radius, with min(y) belonging to the minimum and - max(y) to the maximum radius given in the arguments. - - This is not a fully generic polar coordinate transformation, but - it is fairly useful in creating radial tree layouts from ordinary - 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. - - @param min_angle: the angle corresponding to the minimum X value - @param max_angle: the angle corresponding to the maximum X value - @param min_radius: the radius corresponding to the minimum Y value - @param max_radius: the radius corresponding to the maximum Y value - """ - if self._dim != 2: - raise TypeError("implemented only for 2D layouts") - bbox = self.bounding_box() - - while min_angle > max_angle: - max_angle += 360 - while min_angle > 360: - min_angle -= 360 - max_angle -= 360 - while min_angle < 0: - min_angle += 360 - max_angle += 360 - - ratio_x = (max_angle - min_angle) / bbox.width - ratio_x *= pi / 180. - min_angle *= pi / 180. - 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] - - - def transform(self, function, *args, **kwds): - """Performs an arbitrary transformation on the layout - - Additional positional and keyword arguments are passed intact to - the given function. - - @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] - - - 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)] - for row in self._coords: - for dim in xrange(self._dim): - centroid[dim].add(row[dim]) - return [rm.mean for rm in centroid] - - def boundaries(self, border=0): - """Returns the boundaries of the layout. - - The boundaries are the minimum and maximum coordinates along all - dimensions. - - @param border: this value gets subtracted from the minimum bounds - and gets added to the maximum bounds before returning the coordinates - of the box. Defaults to zero. - @return: the minimum and maximum coordinates along all dimensions, - in a tuple containing two lists, one for the minimum coordinates, - the other one for the maximum. - @raises ValueError: if the layout contains no layout items - """ - if not self._coords: - raise ValueError("layout contains no layout items") - - mins, maxs = [], [] - for dim in xrange(self._dim): - col = [row[dim] for row in self._coords] - 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. - - The bounding box of the layout is the smallest box enclosing all the - points in the layout. - - @param border: this value gets subtracted from the minimum bounds - and gets added to the maximum bounds before returning the coordinates - of the box. Defaults to zero. - @return: the coordinates of the lower left and the upper right corner - of the box. "Lower left" means the minimum coordinates and "upper right" - means the maximum. These are encapsulated in a L{BoundingBox} object. - """ - if self._dim != 2: - raise ValueError("Layout.boundary_box() supports 2D layouts only") - - try: - (x0, y0), (x1, y1) = self.boundaries(border) - return BoundingBox(x0, y0, x1, y1) - except ValueError: - return BoundingBox(0, 0, 0, 0) - - - def center(self, *args, **kwds): - """Centers the layout around the given point. - - The point itself can be supplied as multiple unnamed arguments, as a - simple unnamed list or as a keyword argument. This operation moves - the centroid of the layout to the given point. If no point is supplied, - defaults to the origin of the coordinate system. - - @keyword p: the point where the centroid of the layout will be after - 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 = center[0] - if len(center) != 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)] - self.translate(vec) - - - def copy(self): - """Creates an exact copy of the layout.""" - return self.__copy__() - - def fit_into(self, bbox, keep_aspect_ratio=True): - """Fits the layout into the given bounding box. - - The layout will be modified in-place. - - @param bbox: the bounding box in which to fit the layout. If the - dimension of the layout is d, it can either be a d-tuple (defining - the sizes of the box), a 2d-tuple (defining the coordinates of the - top left and the bottom right point of the box), or a L{BoundingBox} - object (for 2D layouts only). - @param keep_aspect_ratio: whether to keep the aspect ratio of the current - layout. If C{False}, the layout will be rescaled to fit exactly into - the bounding box. If C{True}, the original aspect ratio of the layout - will be kept and it will be centered within the bounding box. - """ - if isinstance(bbox, BoundingBox): - if self._dim != 2: - 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) - elif len(bbox) == 2 * self._dim: - corner, opposite_corner = list(bbox[0:self._dim]), list(bbox[self._dim:]) - for i in xrange(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)] - - 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)] - - for i, size in enumerate(sizes): - if size == 0: - sizes[i] = 2 - mins[i] -= 1 - maxs[i] += 1 - - ratios = [float(target_size) / current_size \ - for current_size, target_size in izip(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. - trans -= mins[i] * ratios[i] - corner[i] - translations.append(trans) - - self.scale(*ratios) - self.translate(*translations) - diff --git a/igraph/remote/nexus.py b/igraph/remote/nexus.py deleted file mode 100644 index 266f16a72..000000000 --- a/igraph/remote/nexus.py +++ /dev/null @@ -1,576 +0,0 @@ -# vim:ts=4:sw=4:sts=4:et -# -*- coding: utf-8 -*- -"""Interface to the Nexus online graph repository. - -The classes in this file facilitate access to the Nexus online graph -repository at U{http://nexus.igraph.org}. - -The main entry point of this package is the C{Nexus} variable, which is -an instance of L{NexusConnection}. Use L{NexusConnection.get} to get a particular -network from Nexus, L{NexusConnection.list} to list networks having a given set of -tags, L{NexusConnection.search} to search in the dataset descriptions, or -L{NexusConnection.info} to show the info sheet of a dataset.""" - -from __future__ import print_function - -from gzip import GzipFile -from itertools import izip -from textwrap import TextWrapper -from urllib import urlencode -from urlparse import urlparse, urlunparse - -from igraph.compat import property, BytesIO -from igraph.configuration import Configuration -from igraph.utils import multidict - -import re -import urllib2 - -__all__ = ["Nexus", "NexusConnection"] - -__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 -""" - - -class NexusConnection(object): - ur"""Connection to a remote Nexus server. - - In most cases, you will not have to instantiate this object, just use - the global L{Nexus} variable which is an instance of L{NexusConnection} - and connects to the Nexus repository at U{http://nexus.igraph.org}. - - Example: - - >>> from igraph import summary - >>> karate = Nexus.get("karate") - >>> 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) - - @undocumented: _get_response, _parse_dataset_id, _parse_text_response, - _ensure_uncompressed""" - - def __init__(self, nexus_url=None): - """Constructs a connection to a remote Nexus server. - - @param nexus_url: the root URL of the remote server. Leave it at its - default value (C{None}) unless you have set up your own Nexus server - and you want to connect to that. C{None} fetches the URL from - igraph's configuration file or uses the default URL if no URL - is specified in the configuration file. - """ - self.debug = False - self.url = nexus_url - self._opener = urllib2.build_opener() - - def get(self, id): - """Retrieves the dataset with the given ID from Nexus. - - Dataset IDs are formatted as follows: the name of a dataset on its own - means that a single network should be returned if the dataset contains - a single network, or multiple networks should be returned if the dataset - contains multiple networks. When the name is followed by a dot and a - network ID, only a single network will be returned: the one that has the - given network ID. When the name is followed by a dot and a star, a - dictionary mapping network IDs to networks will be returned even if the - original dataset contains a single network only. - - E.g., getting C{"karate"} would return a single network since the - Zachary karate club dataset contains one network only. Getting - C{"karate.*"} on the other hand would return a dictionary with one - entry that contains the Zachary karate club network. - - @param id: the ID of the dataset to retrieve. - @return: an instance of L{Graph} (if a single graph has to be returned) - or a dictionary mapping network IDs to instances of L{Graph}. - """ - from igraph import load - - dataset_id, network_id = self._parse_dataset_id(id) - - params = dict(format="Python-igraph", id=dataset_id) - response = self._get_response("/api/dataset", params, compressed=True) - result = load(response, format="pickle") - - if network_id is None: - # If result contains a single network only, return that network. - # Otherwise return the whole dictionary - if not isinstance(result, dict): - return result - if len(result) == 1: - return result[result.keys()[0]] - return result - - if network_id == "*": - # Return a dict no matter what - if not isinstance(result, dict): - result = dict(dataset_id=result) - return result - - return result[network_id] - - def info(self, id): - """Retrieves informations about the dataset with the given numeric - or string ID from Nexus. - - @param id: the numeric or string ID of the dataset to retrieve. - @return: an instance of L{NexusDatasetInfo}. - """ - params = dict(format="text", id=id) - response = self._get_response("/api/dataset_info", params) - return NexusDatasetInfo.FromMultiDict(self._parse_text_response(response)) - - def list(self, tags=None, operator="or", order="date"): - """Retrieves a list of datasets matching a set of tags from Nexus. - - @param tags: the tags the returned datasets should have. C{None} - retrieves all the datasets, a single string retrieves datasets - having that given tag. Multiple tags may also be specified as - a list, tuple or any other iterable. - @param operator: when multiple tags are given, this argument - specifies whether the retrieved datasets should match all - the tags (C{"and"}) or any of them (C{"or"}). - @param order: the order of entries; it must be one of C{"date"}, - C{"name"} or C{"popularity"}. - @return: a L{NexusDatasetInfoList} object, which basically acts like a - list and yields L{NexusDatasetInfo} objects. The list is populated - lazily; i.e. the requests will be fired only when needed. - """ - params = dict(format="text", order=order) - if tags is not None: - if not hasattr(tags, "__iter__") or isinstance(tags, basestring): - params["tag"] = str(tags) - else: - params["tag"] = "|".join(str(tag) for tag in tags) - params["operator"] = operator - - return NexusDatasetInfoList(self, "/api/dataset_info", params) - - def search(self, query, order="date"): - """Retrieves a list of datasets matching a query string from Nexus. - - @param query: the query string. Searches are case insensitive and - Nexus searches for complete words only. The special word OR - can be used to find datasets that contain any of the given words - (instead of all of them). Exact phrases must be enclosed in - quotes in the search string. See the Nexus webpage for more - information at U{http://nexus.igraph.org/web/docs#searching}. - @param order: the order of entries; it must be one of C{"date"}, - C{"name"} or C{"popularity"}. - @return: a L{NexusDatasetInfoList} object, which basically acts like a - list and yields L{NexusDatasetInfo} objects. The list is populated - lazily; i.e. the requests will be fired only when needed. - """ - params = dict(q=query, order=order, format="text") - return NexusDatasetInfoList(self, "/api/search", params) - - @staticmethod - def _ensure_uncompressed(response): - """Expects an HTTP response object, checks its Content-Encoding header, - decompresses the data and returns an in-memory buffer holding the - uncompressed data.""" - compressed = response.headers.get("Content-Encoding") == "gzip" - if not compressed: - content_disp = response.headers.get("Content-Disposition", "") - compressed = bool(re.match(r'attachment; *filename=.*\.gz\"?$', - content_disp)) - if compressed: - return GzipFile(fileobj=BytesIO(response.read()), mode="rb") - else: - return response - - def _get_response(self, path, params={}, compressed=False): - """Sends a request to Nexus at the given path with the given parameters - and returns a file-like object for the response. `compressed` denotes - whether we accept compressed responses.""" - if self.url is None: - url = Configuration.instance()["remote.nexus.url"] - else: - url = self.url - url = "%s%s?%s" % (url, path, urlencode(params)) - request = urllib2.Request(url) - if compressed: - request.add_header("Accept-Encoding", "gzip") - if self.debug: - print("[debug] Sending request: %s" % url) - response = self._opener.open(request) - return self._ensure_uncompressed(response) - - @staticmethod - def _parse_dataset_id(id): - """Parses a dataset ID used in the `get` request. - - Returns the dataset ID and the network ID (the latter being C{None} - if the original ID did not contain a network ID ). - """ - dataset_id, _, network_id = str(id).partition(".") - if not network_id: - network_id = None - return dataset_id, network_id - - @staticmethod - def _parse_text_response(response): - """Parses a plain text formatted response from Nexus. - - Plain text formatted responses consist of key-value pairs, separated - by C{":"}. Values may span multiple lines; in this case, the key is - omitted after the first line and the extra lines start with - whitespace. - - The response is assumed to be in UTF-8 encoding. - - Examples: - - >>> d = Nexus._parse_text_response("Id: 17\\nName: foo") - >>> for key, value in sorted(d.items()): - ... print("{0}={1}".format(key, value)) - ... - Id=17 - Name=foo - >>> d = Nexus._parse_text_response("Id: 42\\nName: foo\\n .\\n bar") - >>> for key, value in sorted(d.items()): - ... print("{0}={1}".format(key, value)) - ... - Id=42 - Name=foo - - bar - """ - if hasattr(response, "decode"): - response = response.decode("utf-8") - - if isinstance(response, basestring): - response = response.split("\n") - - result = multidict() - key, value = None, [] - for line in response: - if hasattr(line, "decode"): - line = line.decode("utf-8") - - line = line.rstrip() - if not line: - continue - if key is not None and line[0] in ' \t': - # Line continuation - line = line.lstrip() - if line == '.': - line = '' - value.append(line) - else: - # Key-value pair - if key is not None: - result.add(key, "\n".join(value)) - key, value = line.split(":", 1) - value = [value.strip()] - - if key is not None: - result.add(key, "\n".join(value)) - - return result - - @property - def url(self): - """Returns the root URL of the Nexus repository the connection is - communicating with.""" - return self._url - - @url.setter - def url(self, value): - """Sets the root URL of the Nexus repository the connection is - communicating with.""" - if value is None: - self._url = None - else: - value = str(value) - parts = urlparse(value, "http", False) - self._url = urlunparse(parts) - if self._url and self._url[-1] == "/": - self._url = self._url[:-1] - - -class NexusDatasetInfo(object): - """Information about a dataset in the Nexus repository. - - @undocumented: _update_from_multidict, vertices_edges""" - - def __init__(self, id=None, sid=None, name=None, networks=None, - vertices=None, edges=None, tags=None, attributes=None, - rest=None): - self._conn = None - self.id = id - self.sid = sid - self.name = name - self.vertices = vertices - self.edges = edges - self.tags = tags - self.attributes = attributes - if networks is None: - self.networks = [] - elif not isinstance(networks, (str, unicode)): - self.networks = list(networks) - else: - self.networks = [networks] - if rest: - self.rest = multidict(rest) - else: - self.rest = None - - @property - def vertices_edges(self): - if self.vertices is None or self.edges is None: - return "" - elif isinstance(self.vertices, (list, tuple)) and isinstance(self.edges, (list, tuple)): - return " ".join("%s/%s" % pair for pair in izip(self.vertices, self.edges)) - else: - return "%s/%s" % (self.vertices, self.edges) - - @vertices_edges.setter - def vertices_edges(self, value): - if value is None: - self.vertices, self.edges = None, None - return - - value = value.strip().split(" ") - if len(value) == 0: - self.vertices, self.edges = None, None - elif len(value) == 1: - self.vertices, self.edges = map(int, value[0].split("/")) - else: - self.vertices = [] - self.edges = [] - for ve in value: - v, e = ve.split("/", 1) - self.vertices.append(int(v)) - self.edges.append(int(e)) - - def __repr__(self): - params = "(id=%(id)r, sid=%(sid)r, name=%(name)r, networks=%(networks)r, "\ - "vertices=%(vertices)r, edges=%(edges)r, tags=%(tags)r, "\ - "attributes=%(attributes)r, rest=%(rest)r)" % self.__dict__ - return "%s%s" % (self.__class__.__name__, params) - - def __str__(self): - if self.networks and len(self.networks) > 1: - lines = ["Nexus dataset '%s' (#%s) with %d networks" % - (self.sid, self.id, len(self.networks))] - else: - lines = ["Nexus dataset '%(sid)s' (#%(id)s)" % self.__dict__] - - lines.append("vertices/edges: %s" % self.vertices_edges) - - if self.name: - lines.append("name: %s" % self.name) - if self.tags: - lines.append("tags: %s" % "; ".join(self.tags)) - - if self.rest: - wrapper = TextWrapper(width=76, subsequent_indent=' ') - - keys = sorted(self.rest.iterkeys()) - if "attribute" in self.rest: - keys.remove("attribute") - keys.append("attribute") - - for key in keys: - for value in self.rest.getlist(key): - if not isinstance(value, basestring): - value = str(value) - paragraphs = value.splitlines() - wrapper.initial_indent = "%s: " % key - for paragraph in paragraphs: - ls = wrapper.wrap(paragraph) - if ls: - lines.extend(wrapper.wrap(paragraph)) - else: - lines.append(" .") - wrapper.initial_indent = " " - - return "\n".join(lines) - - def _update_from_multidict(self, params): - """Updates the dataset object from a multidict representation of - key-value pairs, similar to the ones provided by the Nexus API in - plain text response.""" - self.id = params.get("id") - self.sid = params.get("sid") - self.name = params.get("name") - self.vertices = params.get("vertices") - self.edges = params.get("edges") - self.tags = params.get("tags") - - networks = params.get("networks") - if networks: - self.networks = networks.split() - - keys_to_ignore = set("id sid name vertices edges tags networks".split()) - - if self.vertices is None and self.edges is None: - # Try "vertices/edges" - self.vertices_edges = params.get("vertices/edges") - keys_to_ignore.add("vertices/edges") - - if self.rest is None: - self.rest = multidict() - for k in set(params.iterkeys()) - keys_to_ignore: - for v in params.getlist(k): - self.rest.add(k, v) - - if self.id: - self.id = int(self.id) - if self.vertices and not isinstance(self.vertices, (list, tuple)): - self.vertices = int(self.vertices) - if self.edges and not isinstance(self.edges, (list, tuple)): - self.edges = int(self.edges) - if self.tags is not None: - self.tags = self.tags.split(";") - - @classmethod - def FromMultiDict(cls, dict): - """Constructs a Nexus dataset object from a multidict representation - of key-value pairs, similar to the ones provided by the Nexus API in - plain text response.""" - result = cls() - result._update_from_multidict(dict) - return result - - def download(self, network_id=None): - """Retrieves the actual dataset from Nexus. - - @param network_id: if the dataset contains multiple networks, the ID - of the network to be retrieved. C{None} returns a single network if - the dataset contains a single network, or a dictionary of networks - if the dataset contains more than one network. C{"*"} retrieves - a dictionary even if the dataset contains a single network only. - - @return: a L{Graph} instance or a dictionary mapping network names to - L{Graph} instances. - """ - if self.id is None: - raise ValueError("dataset ID is empty") - conn = self._conn or Nexus - if network_id is None: - return conn.get(self.id) - return conn.get("%s.%s" % (self.id, network_id)) - - get = download - - -class NexusDatasetInfoList(object): - """A read-only list-like object that can be used to retrieve the items - from a Nexus search result. - """ - - def __init__(self, connection, method, params): - """Constructs a Nexus dataset list that will use the given connection - and the given parameters to retrieve the search results. - - @param connection: a Nexus connection object - @param method: the URL of the Nexus API method to call - @param params: the parameters to pass in the GET requests, in the - form of a Python dictionary. - """ - self._conn = connection - self._method = str(method) - self._params = params - self._length = None - self._datasets = [] - self._blocksize = 10 - - def _fetch_results(self, index): - """Fetches the results from Nexus such that the result item with the - given index will be available (unless the result list is shorter than - the given index of course).""" - # Calculate the start offset - page = index // self._blocksize - offset = page * self._blocksize - self._params["offset"] = offset - self._params["limit"] = self._blocksize - - # Ensure that self._datasets has the necessary length - diff = (page+1) * self._blocksize - len(self._datasets) - if diff > 0: - self._datasets.extend([None] * diff) - - response = self._conn._get_response(self._method, self._params) - if hasattr(response, "decode"): - response = response.decode("utf-8") - - current_dataset = None - for line in response: - key, value = line.strip().split(": ", 1) - key = key.lower() - - if key == "totalsize": - # Total number of items in the search result - self._length = int(value) - elif key == "id": - # Starting a new dataset - if current_dataset: - self._datasets[offset] = current_dataset - offset += 1 - current_dataset = NexusDatasetInfo(id=int(value)) - current_dataset._conn = self._conn - elif key == "sid": - current_dataset.sid = value - elif key == "name": - current_dataset.name = value - elif key == "vertices": - current_dataset.vertices = int(value) - elif key == "edges": - current_dataset.edges = int(value) - elif key == "vertices/edges": - current_dataset.vertices_edges = value - elif key == "tags": - current_dataset.tags = value.split(";") - - if current_dataset: - self._datasets[offset] = current_dataset - - def __getitem__(self, index): - if len(self._datasets) <= index: - self._fetch_results(index) - elif self._datasets[index] is None: - self._fetch_results(index) - return self._datasets[index] - - def __iter__(self): - for i in xrange(len(self)): - yield self[i] - - def __len__(self): - """Returns the number of result items.""" - if self._length is None: - self._fetch_results(0) - return self._length - - def __str__(self): - """Converts the Nexus result list into a nice human-readable format.""" - max_index_length = len(str(len(self))) + 2 - indent = "\n" + " " * (max_index_length+1) - - result = [] - for index, item in enumerate(self): - formatted_item = ("[%d]" % index).rjust(max_index_length) + " " + \ - str(item).replace("\n", indent) - result.append(formatted_item) - return "\n".join(result) - -Nexus = NexusConnection() \ No newline at end of file diff --git a/igraph/test/__init__.py b/igraph/test/__init__.py deleted file mode 100644 index 5b8272132..000000000 --- a/igraph/test/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -import sys -import unittest -from igraph.test import basic, layouts, games, foreign, structural, flow, \ - spectral, attributes, cliques, decomposition, operators, generators, \ - isomorphism, colortests, vertexseq, edgeseq, iterators, bipartite, \ - conversion, rng, separators, indexing, atlas, matching, homepage, \ - walks - - -def suite(): - return unittest.TestSuite([ - basic.suite(), - layouts.suite(), - generators.suite(), - games.suite(), - foreign.suite(), - structural.suite(), - flow.suite(), - spectral.suite(), - attributes.suite(), - vertexseq.suite(), - edgeseq.suite(), - cliques.suite(), - decomposition.suite(), - conversion.suite(), - operators.suite(), - isomorphism.suite(), - iterators.suite(), - bipartite.suite(), - colortests.suite(), - rng.suite(), - separators.suite(), - indexing.suite(), - atlas.suite(), - matching.suite(), - homepage.suite(), - walks.suite() - ]) - - -def run_tests(verbosity=1): - runner = unittest.TextTestRunner(verbosity=verbosity).run - result = runner(suite()) - return result.wasSuccessful() - - -# Make nosetest skip run_tests -run_tests.__test__ = False - -if __name__ == "__main__": - if run_tests(verbosity=255): - sys.exit(0) - else: - sys.exit(1) diff --git a/igraph/test/atlas.py b/igraph/test/atlas.py deleted file mode 100644 index 28410b0f1..000000000 --- a/igraph/test/atlas.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import division - -import warnings -import unittest -from igraph import * - -class TestBase(unittest.TestCase): - def testPageRank(self): - for idx, g in enumerate(self.__class__.graphs): - try: - pr = g.pagerank() - except Exception, 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)) - - def testEigenvectorCentrality(self): - # Temporarily turn off the warning handler because g.evcent() will print - # a warning for DAGs - warnings.simplefilter("ignore") - - try: - for idx, g in enumerate(self.__class__.graphs): - try: - ec, eval = g.evcent(return_eigenvalue=True) - except Exception, ex: - self.assertTrue(False, msg="Eigenvector centrality threw exception for graph #%d: %s" % (idx, ex)) - raise - - if g.vcount() == 0: - self.assertEqual([], ec) - continue - - if not g.is_connected(): - # Skip disconnected graphs; this will be fixed in igraph 0.7 - continue - - 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) - 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) - - ec2 = [sum(ec[u.index] for u in v.predecessors()) for v in g.vs] - for i in xrange(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)) - finally: - # Reset the warning handler - warnings.resetwarnings() - - def testHubScore(self): - for idx, g in enumerate(self.__class__.graphs): - sc = g.hub_score() - if g.vcount() == 0: - 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) - - def testAuthorityScore(self): - for idx, g in enumerate(self.__class__.graphs): - sc = g.authority_score() - if g.vcount() == 0: - 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) - -class GraphAtlasTests(TestBase): - graphs = [Graph.Atlas(i) for i in xrange(1253)] - -class IsoclassTests(TestBase): - graphs = [Graph.Isoclass(3, i, directed=True) for i in xrange(16)] + \ - [Graph.Isoclass(4, i, directed=True) for i in xrange(218)] - -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/igraph/test/basic.py b/igraph/test/basic.py deleted file mode 100644 index 804ff02b6..000000000 --- a/igraph/test/basic.py +++ /dev/null @@ -1,520 +0,0 @@ -import unittest -from igraph import * - - -class BasicTests(unittest.TestCase): - def testGraphCreation(self): - g = Graph() - self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 0 and g.ecount() == 0 and g.is_directed() == False) - - g=Graph(3, [(0,1), (1,2), (2,0)]) - self.assertTrue(g.vcount() == 3 and g.ecount() == 3 and g.is_directed() == False and g.is_simple() == True) - g=Graph(2, [(0,1), (1,2), (2,3)], True) - self.assertTrue(g.vcount() == 4 and g.ecount() == 3 and g.is_directed() == True and g.is_simple()) - g=Graph([(0,1), (1,2), (2,1)]) - self.assertTrue(g.vcount() == 3 and g.ecount() == 3 and g.is_directed() == False and g.is_simple() == False) - g=Graph([(0,1), (0,0), (1,2)]) - self.assertTrue(g.vcount() == 3 and g.ecount() == 3 and g.is_directed() == False and g.is_simple() == False) - - g=Graph(8, None) - self.assertEqual(8, g.vcount()) - self.assertEqual(0, g.ecount()) - self.assertFalse(g.is_directed()) - - g=Graph(edges=None) - self.assertEqual(0, g.vcount()) - self.assertEqual(0, g.ecount()) - self.assertFalse(g.is_directed()) - - self.assertRaises(TypeError, Graph, edgelist=[(1,2)]) - - def testAddVertex(self): - g = Graph() - - vertex = g.add_vertex() - self.assertTrue(g.vcount() == 1 and g.ecount() == 0) - self.assertEquals(0, vertex.index) - self.assertFalse("name" in g.vertex_attributes()) - - vertex = g.add_vertex("foo") - self.assertTrue(g.vcount() == 2 and g.ecount() == 0) - self.assertEquals(1, vertex.index) - self.assertTrue("name" in g.vertex_attributes()) - self.assertEqual(g.vs["name"], [None, "foo"]) - - vertex = g.add_vertex(3) - self.assertTrue(g.vcount() == 3 and g.ecount() == 0) - self.assertEquals(2, vertex.index) - self.assertTrue("name" in g.vertex_attributes()) - self.assertEqual(g.vs["name"], [None, "foo", 3]) - - vertex = g.add_vertex(name="bar") - self.assertTrue(g.vcount() == 4 and g.ecount() == 0) - self.assertEquals(3, vertex.index) - self.assertTrue("name" in g.vertex_attributes()) - self.assertEqual(g.vs["name"], [None, "foo", 3, "bar"]) - - vertex = g.add_vertex(name="frob", spam="cheese", ham=42) - self.assertTrue(g.vcount() == 5 and g.ecount() == 0) - self.assertEquals(4, vertex.index) - 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]) - - 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"]) - - def testDeleteVertices(self): - g = Graph([(0,1), (1,2), (2,3), (0,2), (3,4), (4,5)]) - self.assertEqual(6, g.vcount()) - self.assertEqual(6, g.ecount()) - - # Delete a single vertex - g.delete_vertices(4) - self.assertEqual(5, g.vcount()) - self.assertEqual(4, g.ecount()) - - # Delete multiple vertices - g.delete_vertices([1,3]) - self.assertEqual(3, g.vcount()) - self.assertEqual(1, g.ecount()) - - # Delete a vertex sequence - g.delete_vertices(g.vs[:2]) - self.assertEqual(1, g.vcount()) - self.assertEqual(0, g.ecount()) - - # Delete a single vertex object - g.vs[0].delete() - self.assertEqual(0, g.vcount()) - self.assertEqual(0, g.ecount()) - - # Delete vertices by name - g = Graph.Full(4) - g.vs["name"] = ["spam", "bacon", "eggs", "ham"] - self.assertEqual(4, g.vcount()) - g.delete_vertices("spam") - self.assertEqual(3, g.vcount()) - g.delete_vertices(["bacon", "ham"]) - self.assertEqual(1, g.vcount()) - - # Deleting a nonexistent vertex - self.assertRaises(ValueError, g.delete_vertices, "no-such-vertex") - self.assertRaises(InternalError, g.delete_vertices, 2) - - def testAddEdge(self): - g = Graph() - g.add_vertices(["spam", "bacon", "eggs", "ham"]) - - edge = g.add_edge(0, 1) - self.assertEqual(g.vcount(), 4) - self.assertEqual(g.get_edgelist(), [(0, 1)]) - self.assertEqual(0, edge.index) - self.assertEqual((0, 1), edge.tuple) - - edge = g.add_edge(1, 2, foo="bar") - self.assertEqual(g.vcount(), 4) - self.assertEqual(g.get_edgelist(), [(0, 1), (1, 2)]) - self.assertEqual(1, edge.index) - self.assertEqual((1, 2), edge.tuple) - self.assertEqual("bar", edge["foo"]) - self.assertEqual([None, "bar"], g.es["foo"]) - - def testAddEdges(self): - g = Graph() - g.add_vertices(["spam", "bacon", "eggs", "ham"]) - - g.add_edges([(0, 1)]) - self.assertEqual(g.vcount(), 4) - self.assertEqual(g.get_edgelist(), [(0, 1)]) - - g.add_edges([(1, 2), (2, 3), (1, 3)]) - self.assertEqual(g.vcount(), 4) - self.assertEqual(g.get_edgelist(), [(0, 1), (1, 2), (2, 3), (1, 3)]) - - 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)]) - - def testDeleteEdges(self): - g = Graph.Famous("petersen") - g.vs["name"] = list("ABCDEFGHIJ") - el = g.get_edgelist() - - self.assertEqual(15, g.ecount()) - - # Deleting single edge - g.delete_edges(14) - el[14:] = [] - self.assertEqual(14, g.ecount()) - self.assertEqual(el, g.get_edgelist()) - - # Deleting multiple edges - g.delete_edges([2,5,7]) - el[7:8] = []; el[5:6] = []; el[2:3] = [] - self.assertEqual(11, g.ecount()) - self.assertEqual(el, g.get_edgelist()) - - # Deleting edge object - g.es[6].delete() - el[6:7] = [] - self.assertEqual(10, g.ecount()) - self.assertEqual(el, g.get_edgelist()) - - # Deleting edge sequence object - g.es[1:4].delete() - el[1:4] = [] - self.assertEqual(7, g.ecount()) - self.assertEqual(el, g.get_edgelist()) - - # Deleting edges by IDs - g.delete_edges([(2,7), (5,8)]) - el[4:5] = []; el[1:2] = [] - self.assertEqual(5, g.ecount()) - self.assertEqual(el, g.get_edgelist()) - - # Deleting edges by names - g.delete_edges([("D", "I"), ("G", "I")]) - el[3:4] = []; el[1:2] = [] - self.assertEqual(3, g.ecount()) - self.assertEqual(el, g.get_edgelist()) - - # 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)]) - - 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.iteritems(): - 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)) - - self.assertRaises(InternalError, g.get_eid, 0, 11) - self.assertRaises(ValueError, g.get_eid, "A", "K") - - def testGraphGetEids(self): - g = Graph.Famous("petersen") - eids = g.get_eids(pairs=[(0,1), (0,5), (1, 6), (4, 9), (8, 6)]) - self.assertTrue(eids == [0, 2, 4, 9, 12]) - eids = g.get_eids(path=[0, 1, 2, 3, 4]) - self.assertTrue(eids == [0, 3, 5, 7]) - eids = g.get_eids(pairs=[(7,9), (9,6)], path = [7, 9, 6]) - self.assertTrue(eids == [14, 13, 14, 13]) - self.assertRaises(InternalError, g.get_eids, pairs=[(0,1), (0,2)]) - - def testAdjacency(self): - g=Graph(4, [(0,1), (1,2), (2,0), (2,3)], directed=True) - self.assertTrue(g.neighbors(2) == [0, 1, 3]) - self.assertTrue(g.predecessors(2) == [1]) - self.assertTrue(g.successors(2) == [0, 3]) - self.assertTrue(g.get_adjlist() == [[1], [2], [0,3], []]) - 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): - 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.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]]) - - - def testMultiplesLoops(self): - g=Graph.Tree(7, 2) - - # has_multiple - self.assertFalse(g.has_multiple()) - - g.add_vertices(1) - 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]) - - # is_multiple - 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]) - - # check if a mutual directed edge pair is reported as multiple - g=Graph(2, [(0,1), (1,0)], directed=True) - self.assertTrue(g.is_multiple() == [False, False]) - - - def testPickling(self): - import pickle - g=Graph([(0,1), (1,2)]) - g["data"]="abcdef" - g.vs["data"]=[3,4,5] - g.es["data"]=["A", "B"] - g.custom_data = None - pickled=pickle.dumps(g) - - g2=pickle.loads(pickled) - self.assertTrue(g["data"] == g2["data"]) - self.assertTrue(g.vs["data"] == g2.vs["data"]) - self.assertTrue(g.es["data"] == g2.es["data"]) - self.assertTrue(g.vcount() == g2.vcount()) - self.assertTrue(g.ecount() == g2.ecount()) - self.assertTrue(g.is_directed() == g2.is_directed()) - self.assertTrue(g2.custom_data == g.custom_data) - - def testHashing(self): - g = Graph([(0,1), (1,2)]) - self.assertRaises(TypeError, hash, g) - - def testIteration(self): - g = Graph() - self.assertRaises(TypeError, iter, g) - - -class DatatypeTests(unittest.TestCase): - def testMatrix(self): - m = Matrix([[1,2,3], [4,5], [6,7,8]]) - self.assertTrue(m.shape == (3, 3)) - - # Reading data - self.assertTrue(m.data == [[1,2,3], [4,5,0], [6,7,8]]) - self.assertTrue(m[1,1] == 5) - self.assertTrue(m[0] == [1,2,3]) - self.assertTrue(m[0,:] == [1,2,3]) - self.assertTrue(m[:,0] == [1,4,6]) - self.assertTrue(m[2,0:2] == [6,7]) - self.assertTrue(m[:,:].data == [[1,2,3], [4,5,0], [6,7,8]]) - self.assertTrue(m[:,1:3].data == [[2,3], [5,0], [7,8]]) - - # Writing data - m[1,1] = 10 - self.assertTrue(m[1,1] == 10) - m[1] = (6,5,4) - self.assertTrue(m[1] == [6,5,4]) - m[1:3] = [[4,5,6], (7,8,9)] - self.assertTrue(m[1:3].data == [[4,5,6], [7,8,9]]) - - # Minimums and maximums - self.assertTrue(m.min() == 1) - self.assertTrue(m.max() == 9) - self.assertTrue(m.min(0) == [1,2,3]) - self.assertTrue(m.max(0) == [7,8,9]) - self.assertTrue(m.min(1) == [1,4,7]) - self.assertTrue(m.max(1) == [3,6,9]) - - # Special constructors - m=Matrix.Fill(2, (3,3)) - self.assertTrue(m.min() == 2 and m.max() == 2 and m.shape == (3,3)) - m=Matrix.Zero(5, 4) - self.assertTrue(m.min() == 0 and m.max() == 0 and m.shape == (5,4)) - m=Matrix.Identity(3) - self.assertTrue(m.data == [[1,0,0], [0,1,0], [0,0,1]]) - m=Matrix.Identity(3, 2) - self.assertTrue(m.data == [[1,0], [0,1], [0,0]]) - - # Conversion to string - m=Matrix.Identity(3) - self.assertTrue(str(m) == "[[1, 0, 0]\n [0, 1, 0]\n [0, 0, 1]]") - self.assertTrue(repr(m) == "Matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]])") - - -class GraphDictListTests(unittest.TestCase): - def setUp(self): - self.vertices = [ \ - {"name": "Alice", "age": 48, "gender": "F"}, - {"name": "Bob", "age": 33, "gender": "M"}, - {"name": "Cecil", "age": 45, "gender": "F"}, - {"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} - ] - - def testGraphFromDictList(self): - g = Graph.DictList(self.vertices, self.edges) - self.checkIfOK(g, "name") - g = Graph.DictList(self.vertices, self.edges, iterative=True) - self.checkIfOK(g, "name") - - 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) - self.checkIfOK(g, "name") - - def testGraphFromDictIteratorNoVertices(self): - g = Graph.DictList(None, iter(self.edges)) - self.checkIfOK(g, "name", check_vertex_attrs=False) - g = Graph.DictList(None, iter(self.edges), iterative=True) - self.checkIfOK(g, "name", check_vertex_attrs=False) - - def testGraphFromDictListExtraVertexName(self): - 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 g.is_directed() == False) - 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)]) - - def testGraphFromDictListAlternativeName(self): - for vdata in self.vertices: - vdata["name_alternative"] = vdata["name"] - del vdata["name"] - g = Graph.DictList(self.vertices, self.edges, vertex_name_attr="name_alternative") - self.checkIfOK(g, "name_alternative") - g = Graph.DictList(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 g.is_directed() == False) - 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]) - self.assertTrue(g.vs["gender"] == ["F", "M", "F", "M"]) - self.assertTrue(g.es["friendship"] == [4, 5, 5, 2, 1]) - self.assertTrue(g.es["advice"] == [4, 5, 5, 4, 2]) - - -class GraphTupleListTests(unittest.TestCase): - def setUp(self): - self.edges = [ \ - ("Alice", "Bob", 4, 4), - ("Cecil", "Bob", 5, 5), - ("Cecil", "Alice", 5, 5), - ("David", "Alice", 2, 4), - ("David", "Bob", 1, 2) - ] - - def testGraphFromTupleList(self): - g = Graph.TupleList(self.edges) - self.checkIfOK(g, "name", ()) - - 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", )) - - def testGraphFromTupleListWithDifferentNameAttribute(self): - g = Graph.TupleList(self.edges, vertex_name_attr="spam") - self.checkIfOK(g, "spam", ()) - - def testGraphFromTupleListWithWeights(self): - g = Graph.TupleList(self.edges, weights=True) - self.checkIfOK(g, "name", ("weight", )) - g = Graph.TupleList(self.edges, weights="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") - - def testNoneForMissingAttributes(self): - 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 g.is_directed() == False) - 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"]) - if edge_attrs: - self.assertTrue(sorted(g.edge_attributes()) == sorted(edge_attrs)) - self.assertTrue(g.es[edge_attrs[0]] == [4, 5, 5, 2, 1]) - if len(edge_attrs) > 1: - self.assertTrue(g.es[edge_attrs[1]] == [4, 5, 5, 4, 2]) - if len(edge_attrs) > 2: - self.assertTrue(g.es[edge_attrs[2]] == [None] * 5) - else: - self.assertTrue(g.edge_attributes() == []) - - -class DegreeSequenceTests(unittest.TestCase): - def testIsDegreeSequence(self): - 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): - 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 suite(): - basic_suite = unittest.makeSuite(BasicTests) - datatype_suite = unittest.makeSuite(DatatypeTests) - graph_dict_list_suite = unittest.makeSuite(GraphDictListTests) - graph_tuple_list_suite = unittest.makeSuite(GraphTupleListTests) - degree_sequence_suite = unittest.makeSuite(DegreeSequenceTests) - return unittest.TestSuite([basic_suite, datatype_suite, graph_dict_list_suite, - graph_tuple_list_suite, degree_sequence_suite]) - -def test(): - runner = unittest.TextTestRunner() - runner.run(suite()) - -if __name__ == "__main__": - test() - diff --git a/igraph/test/bipartite.py b/igraph/test/bipartite.py deleted file mode 100644 index 0eaa7862c..000000000 --- a/igraph/test/bipartite.py +++ /dev/null @@ -1,121 +0,0 @@ -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) - self.assertTrue(g.is_bipartite()) - 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) - expected = sorted([(i, j) for i in xrange(10) for j in xrange(10, 15)]) - self.assertTrue(sorted(g.get_edgelist()) == expected) - 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(sorted(g.get_edgelist()) == expected) - 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) - - g = Graph.Full_Bipartite(10, 5, directed=True) - self.assertTrue(g.vcount() == 15 and g.ecount() == 100 and g.is_directed() == 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) - - 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)]) - - 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)]) - - 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)]) - - 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)]) - - def testGetIncidence(self): - mat = [[0, 1, 1], [1, 1, 0]] - v1, v2 = [0, 1], [2, 3, 4] - g = Graph.Incidence(mat) - self.assertTrue(g.get_incidence() == (mat, v1, v2)) - g.vs["type2"] = g.vs["type"] - self.assertTrue(g.get_incidence("type2") == (mat, v1, v2)) - self.assertTrue(g.get_incidence(g.vs["type2"]) == (mat, v1, v2)) - - def testBipartiteProjection(self): - g = Graph.Full_Bipartite(10, 5) - - g1, g2 = g.bipartite_projection() - self.assertTrue(g1.isomorphic(Graph.Full(10))) - self.assertTrue(g2.isomorphic(Graph.Full(5))) - self.assertTrue(g.bipartite_projection(which=0).isomorphic(g1)) - self.assertTrue(g.bipartite_projection(which=1).isomorphic(g2)) - self.assertTrue(g.bipartite_projection(which=False).isomorphic(g1)) - self.assertTrue(g.bipartite_projection(which=True).isomorphic(g2)) - self.assertTrue(g1.es["weight"] == [5] * 45) - self.assertTrue(g2.es["weight"] == [10] * 10) - self.assertTrue(g.bipartite_projection_size() == (10, 45, 5, 10)) - - g1, g2 = g.bipartite_projection(probe1=10) - self.assertTrue(g1.isomorphic(Graph.Full(5))) - self.assertTrue(g2.isomorphic(Graph.Full(10))) - self.assertTrue(g.bipartite_projection(which=0).isomorphic(g2)) - self.assertTrue(g.bipartite_projection(which=1).isomorphic(g1)) - self.assertTrue(g.bipartite_projection(which=False).isomorphic(g2)) - self.assertTrue(g.bipartite_projection(which=True).isomorphic(g1)) - - g1, g2 = g.bipartite_projection(multiplicity=False) - self.assertTrue(g1.isomorphic(Graph.Full(10))) - self.assertTrue(g2.isomorphic(Graph.Full(5))) - self.assertTrue(g.bipartite_projection(which=0).isomorphic(g1)) - self.assertTrue(g.bipartite_projection(which=1).isomorphic(g2)) - self.assertTrue(g.bipartite_projection(which=False).isomorphic(g1)) - self.assertTrue(g.bipartite_projection(which=True).isomorphic(g2)) - self.assertTrue("weight" not in g1.edge_attributes()) - self.assertTrue("weight" not in g2.edge_attributes()) - - def testIsBipartite(self): - g = Graph.Star(10) - self.assertTrue(g.is_bipartite() == True) - self.assertTrue(g.is_bipartite(True) == (True, [False] + [True]*9)) - g = Graph.Tree(100, 3) - self.assertTrue(g.is_bipartite() == True) - g = Graph.Ring(9) - self.assertTrue(g.is_bipartite() == False) - self.assertTrue(g.is_bipartite(True) == (False, None)) - g = Graph.Ring(10) - self.assertTrue(g.is_bipartite() == 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/igraph/test/cliques.py b/igraph/test/cliques.py deleted file mode 100644 index eabd2e746..000000000 --- a/igraph/test/cliques.py +++ /dev/null @@ -1,224 +0,0 @@ -import unittest -from igraph import * -from igraph.test.foreign import temporary_file -from igraph.test.utils import is_pypy, skipIf - - -class CliqueTests(unittest.TestCase): - def setUp(self): - 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.iteritems(): - 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]]) - - 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]]) - - 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) - - 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)) - - with temporary_file() as fname: - self.g.maximal_cliques(min=4, file=fname) - self.assertEqual([[1, 2, 3, 4], [1, 2, 4, 5]], read_cliques(fname)) - - with temporary_file() as fname: - self.g.maximal_cliques(max=3, file=fname) - self.assertEqual([[0, 3, 4], [0, 4, 5]], read_cliques(fname)) - - def testCliqueNumber(self): - self.assertEqual(self.g.clique_number(), 4) - self.assertEqual(self.g.omega(), 4) - - -class IndependentVertexSetTests(unittest.TestCase): - def setUp(self): - 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.iteritems(): - 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)]) - - 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)]) - - def testIndependenceNumber(self): - self.assertEqual(self.g2.independence_number(), 6) - self.assertEqual(self.g2.alpha(), 6) - - -class MotifTests(unittest.TestCase): - 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 - """ - dc = self.g.dyad_census() - accessors = ["mut", "mutual", "asym", "asymm", "asymmetric", "null"] - for a in accessors: - self.assertTrue(isinstance(getattr(dc, a), int)) - self.assertTrue(isinstance(dc[a], int)) - self.assertTrue(isinstance(list(dc), list)) - self.assertTrue(isinstance(tuple(dc), tuple)) - self.assertTrue(len(list(dc)) == 3) - 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 - """ - 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(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: - - >>> from igraph.test.cliques import CliqueBenchmark - >>> CliqueBenchmark().run() - """ - - def __init__(self): - from time import time - import gc - self.time = time - self.gc_collect = gc.collect - - def run(self): - self.printIntro() - self.testRandom() - self.testMoonMoser() - self.testGRG() - - def printIntro(self): - print "n = number of vertices" - print "#cliques = number of maximal cliques found" - print "t1 = time required to determine the clique number" - print "t2 = time required to determine and save all maximal cliques" - print - - def timeit(self, g): - start = self.time() - omega = g.clique_number() - mid = self.time() - cl = g.maximal_cliques() - end = self.time() - self.gc_collect() - 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]} - - print - print "Erdos-Renyi random graphs" - print " n p #cliques t1 t2" - for n in sorted(np.keys()): - for p in np[n]: - g = Graph.Erdos_Renyi(n, p) - result = self.timeit(g) - print "%8d %8.3f %8d %8.4fs %8.4fs" % \ - tuple([n, p] + list(result)) - - def testMoonMoser(self): - ns = [15, 27, 33] - - print - 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]] - g = Graph(n, el) - result = self.timeit(g) - 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] - - print - print "Geometric random graphs" - print " n d #cliques t1 t2" - for n in ns: - d = 2. / (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)) - - -def suite(): - clique_suite = unittest.makeSuite(CliqueTests) - indvset_suite = unittest.makeSuite(IndependentVertexSetTests) - 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/igraph/test/colortests.py b/igraph/test/colortests.py deleted file mode 100644 index 960687c4f..000000000 --- a/igraph/test/colortests.py +++ /dev/null @@ -1,98 +0,0 @@ -import unittest - -from itertools import izip -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)) - - 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), - ] - self.data = [dict(zip(columns, value)) for value in values] - for row in self.data: - row["h"] /= 360. - - def _testGeneric(self, method, args1, args2=("r", "g", "b")): - 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] - self.assertAlmostEqualMany(method(*vals1), vals2, 2) - - def testHSVtoRGB(self): - self._testGeneric(hsv_to_rgb, "h s_hsv v".split()) - - def testHSVAtoRGBA(self): - self._testGeneric(hsva_to_rgba, "h s_hsv v alpha".split()) - - def testHSLtoRGB(self): - self._testGeneric(hsl_to_rgb, "h s_hsl l".split()) - - def testHSLAtoRGBA(self): - self._testGeneric(hsla_to_rgba, "h s_hsl l alpha".split()) - - 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()) - - 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()) - - -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.)) - - 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.)) - - 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.)) - - -def suite(): - color_suite = unittest.makeSuite(ColorTests) - 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/igraph/test/conversion.py b/igraph/test/conversion.py deleted file mode 100644 index 1a4601d00..000000000 --- a/igraph/test/conversion.py +++ /dev/null @@ -1,102 +0,0 @@ -import unittest -from igraph import * - -class DirectedUndirectedTests(unittest.TestCase): - def testToUndirected(self): - 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)]) - - 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)]) - - graph2 = graph.copy() - 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]) - - 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.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]) - - def testToDirected(self): - 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)] - ) - - -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) - - # 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] - ])) - - -def suite(): - direction_suite = unittest.makeSuite(DirectedUndirectedTests) - representation_suite = unittest.makeSuite(GraphRepresentationTests) - return unittest.TestSuite([direction_suite, - representation_suite]) - -def test(): - runner = unittest.TextTestRunner() - runner.run(suite()) - -if __name__ == "__main__": - test() - diff --git a/igraph/test/decomposition.py b/igraph/test/decomposition.py deleted file mode 100644 index dce67402b..000000000 --- a/igraph/test/decomposition.py +++ /dev/null @@ -1,416 +0,0 @@ -import random -import unittest -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()) - - 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.vs["id"] == vs) - - def testSubgraphEdges(self): - g = Graph.Lattice([10, 10], circular=False, mutual=False) - g.es["id"] = range(g.ecount()) - - es = [0, 1, 2, 5, 20, 21, 22, 24, 38, 40] - sg = g.subgraph_edges(es) - exp = Graph.Lattice([3, 3], circular=False, mutual=False) - exp.delete_edges([7, 8]) - - self.assertTrue(sg.isomorphic(exp)) - self.assertTrue(sg.es["id"] == es) - - -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]) - 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)]) - - -class ClusteringTests(unittest.TestCase): - def setUp(self): - self.cl = Clustering([0,0,0,1,1,2,1,1,4,4]) - - def testClusteringIndex(self): - self.assertTrue(self.cl[0] == [0, 1, 2]) - self.assertTrue(self.cl[1] == [3, 4, 6, 7]) - self.assertTrue(self.cl[2] == [5]) - self.assertTrue(self.cl[3] == []) - self.assertTrue(self.cl[4] == [8, 9]) - - 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]) - - def testClusteringSizes(self): - self.assertTrue(self.cl.sizes() == [3, 4, 1, 0, 2]) - self.assertTrue(self.cl.sizes(2, 4, 1) == [1, 2, 4]) - self.assertTrue(self.cl.size(2) == 1) - - def testClusteringHistogram(self): - self.assertTrue(isinstance(self.cl.size_histogram(), Histogram)) - - -class VertexClusteringTests(unittest.TestCase): - def setUp(self): - self.graph = Graph.Full(10) - self.graph.vs["string"] = list("aaabbcccab") - self.graph.vs["int"] = [17, 41, 23, 25, 64, 33, 3, 24, 47, 15] - - def testFromStringAttribute(self): - cl = VertexClustering.FromAttribute(self.graph, "string") - self.assertTrue(cl.membership == [0,0,0,1,1,2,2,2,0,1]) - - def testFromIntAttribute(self): - cl = VertexClustering.FromAttribute(self.graph, "int") - self.assertTrue(cl.membership == list(range(10))) - cl = VertexClustering.FromAttribute(self.graph, "int", 15) - self.assertTrue(cl.membership == [0, 1, 0, 0, 2, 1, 3, 0, 4, 0]) - cl = VertexClustering.FromAttribute(self.graph, "int", [10, 20, 30]) - self.assertTrue(cl.membership == [0, 1, 2, 2, 1, 1, 3, 2, 1, 0]) - - 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]))) - clg = cl.cluster_graph(dict(string="concat", int=max)) - - self.assertTrue(sorted(clg.get_edgelist()) == [(0, 2), (1, 2)]) - self.assertTrue(not clg.is_directed()) - self.assertTrue(clg.vs["string"] == ["aaa", "bbc", "ccab"]) - 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(not clg.is_directed()) - self.assertTrue(clg.vs["string"] == ["aaa", "bbc", "ccab"]) - self.assertTrue(clg.vs["int"] == [41, 64, 47]) - - -class CoverTests(unittest.TestCase): - def setUp(self): - 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]) - self.assertTrue(self.cl[1] == [3, 4, 5, 6, 9]) - self.assertTrue(self.cl[2] == []) - self.assertTrue(self.cl[3] == [8, 9]) - - def testCoverLength(self): - self.assertTrue(len(self.cl) == 4) - - def testCoverSizes(self): - self.assertTrue(self.cl.sizes() == [4, 5, 0, 2]) - self.assertTrue(self.cl.sizes(1, 3, 0) == [5, 2, 4]) - self.assertTrue(self.cl.size(1) == 5) - self.assertTrue(self.cl.size(2) == 0) - - def testCoverHistogram(self): - self.assertTrue(isinstance(self.cl.size_histogram(), Histogram)) - - def testCoverConstructorWithN(self): - self.assertTrue(self.cl.n == 10) - cl = Cover(self.cl, n = 15) - self.assertTrue(cl.n == 15) - cl = Cover(self.cl, n = 1) - self.assertTrue(cl.n == 10) - - -class CommunityTests(unittest.TestCase): - def reindexMembership(self, cl): - idgen = UniqueIdGenerator() - return [idgen[i] for i in cl.membership] - - 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.assertEqual(cl.membership, [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)]) - weights = [1, 1, 1, 1, 1, 1, 10, 10] - cl = g.community_fastgreedy(weights).as_clustering() - self.assertEqual(cl.membership, [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.assertEqual(cl.membership, [0, 0, 0, 0, 1, 1]) - self.assertAlmostEqual(cl.q, 0.1796, places=3) - - # Disconnected graph - g = Graph.Full(4) + Graph.Full(4) + Graph.Full(3) + Graph.Full(2) - cl = g.community_fastgreedy().as_clustering() - self.assertEqual(cl.membership, [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3]) - - # Empty graph - g = Graph(20) - cl = g.community_fastgreedy().as_clustering() - self.assertEqual(cl.membership, range(g.vcount())) - - def testEdgeBetweenness(self): - # Full graph, no weights - g = Graph.Full(5) - cl = g.community_edge_betweenness().as_clustering() - self.assertEqual(cl.membership, [0]*5) - - # Full graph with weights - g.es["weight"] = 1 - g[0,1] = g[1,2] = g[2,0] = g[3,4] = 10 - cl = g.community_edge_betweenness(weights="weight").as_clustering() - self.assertEqual(cl.membership, [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.assertTrue(cl.membership == [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.assertTrue(cl.membership == [0,0,0,0,0,1,1,1,1,1]) - self.assertAlmostEqual(cl.q, 0.4523, places=3) - - def testInfomap(self): - g = Graph.Famous("zachary") - cl = g.community_infomap() - self.assertAlmostEqual(cl.codelength, 4.60605, places=3) - self.assertAlmostEqual(cl.q, 0.40203, places=3) - self.assertTrue(cl.membership == [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 xrange(g.vcount())] - e_weights = [random.randint(1, 5) for _ in xrange(g.ecount())] - cl = g.community_infomap(edge_weights=e_weights) - cl = g.community_infomap(vertex_weights=v_weights) - cl = g.community_infomap(edge_weights=e_weights, vertex_weights=v_weights) - - def testLabelPropagation(self): - # Nothing to test there really, since the algorithm - # is pretty nondeterministic. We just do a quick smoke - # test. - g = Graph.GRG(100, 0.2) - cl = g.community_label_propagation() - 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.assertTrue(cl.membership == [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)] - cls = g.community_multilevel(return_levels=True) - self.assertTrue(len(cls) == 2) - self.assertTrue(cls[0].membership == [0,0,0,1,0,0,1,1,2,2,2,3,2,3,2,2]) - self.assertTrue(cls[1].membership == [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) - - def testOptimalModularity(self): - try: - g = Graph.Famous("bull") - - cl = g.community_optimal_modularity() - self.assertTrue(len(cl) == 2) - self.assertTrue(cl.membership == [0, 0, 1, 0, 1]) - self.assertAlmostEqual(cl.q, 0.08, places=7) - - ws = [i % 5 for i in xrange(g.ecount())] - cl = g.community_optimal_modularity(weights=ws) - 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.assertTrue(cl.membership == [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 xrange(g.ecount())] - cl = g.community_optimal_modularity(weights=ws) - self.assertAlmostEqual(cl.q, g.modularity(cl.membership, weights=ws), - places=7) - - except NotImplementedError: - # Well, meh - pass - - def testSpinglass(self): - g = Graph.Full(5) + Graph.Full(5) + Graph.Full(5) - g += [(0,5), (5,10), (10, 0)] - - # Spinglass community detection is a bit unstable, so run it three times - ok = False - for i in xrange(3): - cl = g.community_spinglass() - 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)] - cl = g.community_walktrap().as_clustering() - self.assertTrue(cl.membership == [0,0,0,0,0,1,1,1,1,1,2,2,2,2,2]) - cl = g.community_walktrap(steps=3).as_clustering() - self.assertTrue(cl.membership == [0,0,0,0,0,1,1,1,1,1,2,2,2,2,2]) - - -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()))) - - def testCohesiveBlocks1(self): - # Taken from the igraph R manual - g = Graph.Full(4) + Graph(2) + [(3, 4), (4, 5), (4, 2)] - g *= 3 - g += [(0, 6), (1, 7), (0, 12), (4, 0), (4, 1)] - - cbs = g.cohesive_blocks() - self.genericTests(cbs) - self.assertEqual(sorted(list(cbs)), - [range(0, 5), range(18), [0, 1, 2, 3, 4, 6, 7, 8, 9, 10], - range(6, 10), range(12, 16), 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.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") - - cbs = g.cohesive_blocks() - self.genericTests(cbs) - - expected_blocks = [range(7), range(23), range(7)+range(16, 23), - 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)]) - - def testCohesiveBlockingErrors(self): - g = Graph.GRG(100, 0.2) - g.to_directed() - self.assertRaises(InternalError, g.cohesive_blocks) - - -class ComparisonTests(unittest.TestCase): - def setUp(self): - self.clusterings = [ - ([1, 1, 1, 2, 2, 2], [2, 2, 2, 1, 1, 1]), - ([1, 1, 1, 2, 2, 2], [1, 1, 2, 2, 3, 3]), - ([1, 1, 1, 1, 1, 1], [1, 2, 3, 5, 6, 7]), - ([1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3], - [3, 1, 2, 1, 3, 1, 3, 1, 2, 1, 4, 2]) - ] - - def _testMethod(self, method, expected): - for clusters, result in zip(self.clusterings, expected): - self.assertAlmostEqual(compare_communities(method=method, *clusters), - result, places=3) - - def testCompareVI(self): - expected = [0, 0.8675, math.log(6)] - self._testMethod(None, expected) - self._testMethod("vi", expected) - - def testCompareNMI(self): - expected = [1, 0.5158, 0] - self._testMethod("nmi", expected) - - def testCompareSplitJoin(self): - expected = [0, 3, 5, 11] - self._testMethod("split", 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)) - - def testCompareRand(self): - expected = [1, 2/3., 0, 0.590909] - self._testMethod("rand", expected) - - def testCompareAdjustedRand(self): - expected = [1, 0.242424, 0, -0.04700353] - self._testMethod("adjusted_rand", expected) - - 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) - -def suite(): - decomposition_suite = unittest.makeSuite(DecompositionTests) - clustering_suite = unittest.makeSuite(ClusteringTests) - vertex_clustering_suite = unittest.makeSuite(VertexClusteringTests) - cover_suite = unittest.makeSuite(CoverTests) - 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]) - -def test(): - runner = unittest.TextTestRunner() - runner.run(suite()) - -if __name__ == "__main__": - test() - diff --git a/igraph/test/foreign.py b/igraph/test/foreign.py deleted file mode 100644 index 520c885fa..000000000 --- a/igraph/test/foreign.py +++ /dev/null @@ -1,238 +0,0 @@ -from __future__ import with_statement - -import io -import unittest -import warnings - -from igraph import * -from igraph.test.utils import temporary_file - - -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()) - - 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) - - -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/igraph/test/generators.py b/igraph/test/generators.py deleted file mode 100644 index 9d282745f..000000000 --- a/igraph/test/generators.py +++ /dev/null @@ -1,176 +0,0 @@ -import unittest -from igraph import * - -class GeneratorTests(unittest.TestCase): - def testStar(self): - 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)] - 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)] - 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)] - self.assertTrue(not g.is_directed()) - self.assertTrue(sorted(g.get_edgelist()) == el) - - def testFamous(self): - g=Graph.Famous("tutte") - self.assertTrue(g.vcount() == 46 and g.ecount() == 69) - self.assertRaises(InternalError, Graph.Famous, "unknown") - - def testFormula(self): - tests = [ - (None, [], []), - ("", [""], []), - ("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"], - [(0,1), (2,3), (4,5), (6,7)] - ), - ("A:B:C:D -- A:B:C:D", - ["A", "B", "C", "D"], - [(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", - ["Alice", "Bob", "Cecil", "Daniel", "Eugene", "Gordon"], - [(0,1),(1,2),(0,2),(2,3),(2,4),(2,5)] - ), - ("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)] - ), - ("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)] - ), - ("Alice -- Bob -- Daniel, Cecil:Gordon, Helen", - ["Alice", "Bob", "Daniel", "Cecil", "Gordon", "Helen"], - [(0,1),(1,2)] - ), - ('"+" -- "-", "*" -- "/", "%%" -- "%/%"', - ["+", "-", "*", "/", "%%", "%/%"], - [(0,1),(2,3),(4,5)] - ) - ] - for formula, names, edges in tests: - g = Graph.Formula(formula) - self.assertEqual(g.vs["name"], names) - self.assertEqual(g.get_edgelist(), sorted(edges)) - - def testFull(self): - 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]) - - def testFullCitation(self): - 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 xrange(19) for y in xrange(x+1, 20)]) - - 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 xrange(1, 20) for y in xrange(x)]) - - self.assertRaises(InternalError, Graph.Full_Citation, -2) - - 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) - - def testKautz(self): - 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) - - 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) - # 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) - - def testSBM(self): - pref_matrix = [[0.5, 0, 0], [0, 0, 0.5], [0, 0.5, 0]] - n = 60 - types = [20, 20, 20] - g = Graph.SBM(n, pref_matrix, types) - - # 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.assertTrue(not any(e.source // 20 == e.target // 20 for e in g2.es)) - - # Check loops argument - g = Graph.SBM(n, pref_matrix, types, loops=True) - self.assertFalse(g.is_simple()) - self.assertTrue(sum(g.is_loop()) > 0) - - # Check directedness - g = Graph.SBM(n, pref_matrix, types, directed=True) - self.assertTrue(g.is_directed()) - self.assertTrue(sum(g.is_mutual()) < g.ecount()) - self.assertTrue(sum(g.is_loop()) == 0) - - # Check error conditions - self.assertRaises(InternalError, 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) - - def testWeightedAdjacency(self): - mat = [[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]) - - -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/igraph/test/isomorphism.py b/igraph/test/isomorphism.py deleted file mode 100644 index 7bcde77ba..000000000 --- a/igraph/test/isomorphism.py +++ /dev/null @@ -1,273 +0,0 @@ -import unittest -from igraph import * -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)]) - - # Test the isomorphy 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.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])) - - # 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(not g1.isomorphic_vf2(g2, "color", "color")) - - # 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(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] - self.assertTrue(g1.isomorphic_vf2(g2, node_compat_fn=node_compat)) - 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)) - - 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.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]] - self.assertTrue(sorted(maps) == expected_maps) - - maps[:] = [] - g3 = Graph.Full(4) - 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]] - 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)]) - self.assertTrue(g.count_automorphisms_vf2() == 4) - - # Some more tests with colors - g3 = Graph.Full(4) - 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) - - # Test VF2 with node/edge compatibility function - 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] - self.assertTrue(g3.count_isomorphisms_vf2(node_compat_fn=node_compat) == 2) - 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)]) - 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]] - 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) - -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)] - - self.assertTrue(g.subisomorphic_lad(g2)) - self.assertFalse(g2.subisomorphic_lad(g)) - - # Test 'induced' - self.assertFalse(g3.subisomorphic_lad(g, induced=True)) - self.assertTrue(g3.subisomorphic_lad(g, induced=False)) - self.assertTrue(g3.subisomorphic_lad(g)) - self.assertTrue(g3.subisomorphic_lad(g2, induced=True)) - self.assertTrue(g3.subisomorphic_lad(g2, induced=False)) - 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]] - 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]] - self.assertTrue(not g.subisomorphic_lad(g2, domains=domains)) - - # Corner cases - empty = Graph() - self.assertTrue(g.subisomorphic_lad(empty)) - 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)] - - 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 \ - 6743 7436 7458 7634 7854 8547 8745" - all_subiso = sorted([int(x) for x in item] for item in all_subiso.split()) - - self.assertEqual(all_subiso, sorted(g.get_subisomorphisms_lad(g2))) - self.assertEqual([], sorted(g2.get_subisomorphisms_lad(g))) - - # 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()) - all_subiso_extra = sorted(all_subiso + induced_subiso) - 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]] - self.assertEqual([], sorted(g.get_subisomorphisms_lad(g2, domains=domains))) - - # Corner cases - empty = Graph() - self.assertEqual([], g.get_subisomorphisms_lad(empty)) - 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)]) - 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] - self.assertTrue(g.subisomorphic_vf2(g2, node_compat_fn=node_compat)) - 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) - 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) - 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) - - # 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) - -class PermutationTests(unittest.TestCase): - def testCanonicalPermutation(self): - # Simple case: two ring graphs - g1 = Graph(4, [(0, 1), (1, 2), (2, 3), (3, 0)]) - g2 = Graph(4, [(0, 1), (1, 3), (3, 2), (2, 0)]) - - cp = g1.canonical_permutation() - g3 = g1.permute_vertices(cp) - - cp = g2.canonical_permutation() - 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 = range(10) - shuffle(perm) - g2 = g.permute_vertices(perm) - g3 = g.permute_vertices(g.canonical_permutation()) - g4 = g2.permute_vertices(g2.canonical_permutation()) - - self.assertTrue(g3.vcount() == g4.vcount()) - 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)]) - _, _, 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]) - -def test(): - runner = unittest.TextTestRunner() - runner.run(suite()) - -if __name__ == "__main__": - test() - diff --git a/igraph/test/iterators.py b/igraph/test/iterators.py deleted file mode 100644 index a229e4383..000000000 --- a/igraph/test/iterators.py +++ /dev/null @@ -1,25 +0,0 @@ -import unittest -from igraph import * - -class IteratorTests(unittest.TestCase): - def testBFS(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,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)]) - - -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/igraph/test/layouts.py b/igraph/test/layouts.py deleted file mode 100644 index bcb7ae828..000000000 --- a/igraph/test/layouts.py +++ /dev/null @@ -1,298 +0,0 @@ -import unittest -from igraph import Graph, Layout, BoundingBox - - -class LayoutTests(unittest.TestCase): - def testConstructor(self): - 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) - self.assertEqual(layout.dim, 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)]) - self.assertEqual(len(layout), 4) - self.assertEqual(layout[1], [0, 1, 0]) - self.assertEqual(layout[3], [2, 1, 3]) - - row = layout[2] - row[2] = 1 - self.assertEqual(layout[2], [1, 0, 1]) - - del layout[1] - self.assertEqual(len(layout), 3) - - def testScaling(self): - 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]]) - - 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]]) - - 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.assertRaises(ValueError, layout.scale, 2, 3) - - def testTranslation(self): - 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, 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)]) - 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.) - - 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([]) - self.assertRaises(ValueError, layout.boundaries) - layout = Layout([], dim=3) - 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([]) - self.assertEqual(layout.bounding_box(), BoundingBox(0,0,0,0)) - - def testCenter(self): - 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.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) - 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)) - 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([]) - 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.]] - for idx in xrange(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 - layout = Layout([(1, 2), (3, 4)]) - layout.transform(tr, 2, -1) - self.assertEqual(layout.coords, [[3, 1], [5, 3]]) - - -class LayoutAlgorithmTests(unittest.TestCase): - def testAuto(self): - def layout_test(graph, test_with_dims=(2, 3)): - lo = graph.layout("auto") - self.assertTrue(isinstance(lo, Layout)) - self.assertEqual(len(lo[0]), 2) - for dim in test_with_dims: - lo = graph.layout("auto", dim=dim) - self.assertTrue(isinstance(lo, Layout)) - self.assertEqual(len(lo[0]), dim) - return lo - - g = Graph.Barabasi(10) - layout_test(g) - - g = Graph.GRG(101, 0.2) - del g.vs["x"] - del g.vs["y"] - layout_test(g) - - g = Graph.Full(10) * 2 - layout_test(g) - - g["layout"] = "graphopt" - layout_test(g, test_with_dims=()) - - g.vs["x"] = range(20) - g.vs["y"] = 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], - zip(range(20), range(20, 40))) - - g.vs["z"] = range(40, 60) - lo = layout_test(g) - self.assertEqual([tuple(item) for item in lo], - zip(range(20), range(20, 40), range(40, 60))) - - def testCircle(self): - def test_is_proper_circular_layout(graph, layout): - xs, ys = 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) - - g = Graph.Ring(8) - layout = g.layout("circle") - test_is_proper_circular_layout(g, g.layout("circle")) - - order = [0, 2, 4, 6, 1, 3, 5, 7] - ordered_layout = g.layout("circle", order=order) - test_is_proper_circular_layout(g, g.layout("circle")) - for v, w in enumerate(order): - self.assertAlmostEquals(layout[v][0], ordered_layout[w][0]) - self.assertAlmostEquals(layout[v][1], ordered_layout[w][1]) - - def testDavidsonHarel(self): - # Quick smoke testing only - g = Graph.Barabasi(100) - lo = g.layout("dh") - self.assertTrue(isinstance(lo, Layout)) - - def testFruchtermanReingold(self): - g = Graph.Barabasi(100) - - lo = g.layout("fr") - self.assertTrue(isinstance(lo, Layout)) - - lo = g.layout("fr", miny=range(100)) - self.assertTrue(isinstance(lo, Layout)) - self.assertTrue(all(lo[i][1] >= i for i in xrange(100))) - - lo = g.layout("fr", miny=range(100), maxy=range(100)) - self.assertTrue(isinstance(lo, Layout)) - self.assertTrue(all(lo[i][1] == i for i in xrange(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) - self.assertTrue(bbox.bottom <= 3) - self.assertTrue(bbox.left >= 4) - self.assertTrue(bbox.right <= 6) - - 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) - self.assertTrue(isinstance(lo, Layout)) - self.assertTrue(all(lo[i][1] >= i for i in xrange(100))) - - def testKamadaKawai(self): - g = Graph.Barabasi(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) - self.assertTrue(bbox.bottom <= 3) - self.assertTrue(bbox.left >= 4) - self.assertTrue(bbox.right <= 6) - - def testMDS(self): - g = Graph.Tree(10, 2) - lo = g.layout("mds") - self.assertTrue(isinstance(lo, Layout)) - - dists = g.shortest_paths() - lo = g.layout("mds", dists) - self.assertTrue(isinstance(lo, Layout)) - - g += Graph.Tree(10, 2) - lo = g.layout("mds") - self.assertTrue(isinstance(lo, Layout)) - - def testReingoldTilford(self): - g = Graph.Barabasi(100) - lo = g.layout("rt") - ys = [coord[1] for coord in lo] - root = ys.index(0.0) - 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) - lo = g.layout("rt", root=[0, 100], rootlevel=[2, 10]) - self.assertEqual(lo[100][1]-lo[0][1], 8) - - def testBipartite(self): - g = Graph.Full_Bipartite(3, 2) - - lo = g.layout("bipartite") - ys = [coord[1] for coord in lo] - self.assertEqual([1, 1, 1, 0, 0], ys) - - lo = g.layout("bipartite", vgap=3) - ys = [coord[1] for coord in lo] - 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)) - - def testDRL(self): - # Regression test for bug #1091891 - g = Graph.Ring(10, circular=False) + 1 - lo = g.layout("drl") - self.assertTrue(isinstance(lo, Layout)) - - -def suite(): - layout_suite = unittest.makeSuite(LayoutTests) - layout_algorithm_suite = unittest.makeSuite(LayoutAlgorithmTests) - return unittest.TestSuite([layout_suite, layout_algorithm_suite]) - - -def test(): - runner = unittest.TextTestRunner() - runner.run(suite()) - - -if __name__ == "__main__": - test() diff --git a/igraph/test/operators.py b/igraph/test/operators.py deleted file mode 100644 index 78ce419cd..000000000 --- a/igraph/test/operators.py +++ /dev/null @@ -1,213 +0,0 @@ -import unittest -from igraph import * -from igraph.test.utils import skipIf - -try: - import numpy as np -except ImportError: - np = None - -class OperatorTests(unittest.TestCase): - 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]) - - def testIntersection(self): - g = Graph.Tree(7, 2) & Graph.Lattice([7]) - self.assertTrue(g.get_edgelist() == [(0, 1)]) - - def testUnion(self): - g = Graph.Tree(7, 2) | Graph.Lattice([7]) - self.assertTrue(g.vcount() == 7 and g.ecount() == 12) - - def testInPlaceAddition(self): - g = Graph.Full(3) - orig = g - - # Adding vertices - g += 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]) - - # 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]) - - # 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) - - # Adding two more vertices - g += ["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 - # could be moved to the end - self.assertTrue(id(g) == id(orig)) - - # 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]) - - # 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]) - - 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]) - 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]) - 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]) - 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) - 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]) - - def testInPlaceSubtraction(self): - g = Graph.Full(8) - orig = g - - # 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]) - - # 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]) - - # Deleting two vertices - g -= [4, 5] - 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]) - - # 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]) - - # Did we really use the original graph so far? - self.assertTrue(id(g) == id(orig)) - - # 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]) - - def testNonzero(self): - self.assertTrue(Graph(1)) - self.assertFalse(Graph(0)) - - def testLength(self): - self.assertRaises(TypeError, len, Graph(15)) - self.assertTrue(len(Graph(15).vs) == 15) - 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)] - g = Graph(el) - g.es["weight"] = [1, 2, 3, 4, 5, 6, 7] - - g2 = g.copy() - g2.simplify() - self.assertTrue(g2.vcount() == g.vcount()) - self.assertTrue(g2.ecount() == 3) - - g2 = g.copy() - g2.simplify(loops=False) - self.assertTrue(g2.vcount() == g.vcount()) - self.assertTrue(g2.ecount() == 4) - - g2 = g.copy() - g2.simplify(multiple=False) - self.assertTrue(g2.vcount() == g.vcount()) - self.assertTrue(g2.ecount() == g.ecount() - 1) - - def testContractVertices(self): - g = Graph.Full(4) + Graph.Full(4) + [(0, 5), (1, 4)] - - g2 = g.copy() - 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)]) - - 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)]) - - g2 = Graph(10) - g2.contract_vertices([0, 0, 1, 1, 2, 2, 3, 3, 4, 4]) - self.assertEqual(g2.vcount(), 5) - self.assertEqual(g2.ecount(), 0) - - @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() - 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)]) - - -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/igraph/test/spectral.py b/igraph/test/spectral.py deleted file mode 100644 index f2d75f441..000000000 --- a/igraph/test/spectral.py +++ /dev/null @@ -1,45 +0,0 @@ -# vim:set ts=4 sw=4 sts=4 et: -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 testLaplacian(self): - 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.assertAlmostEqualMatrix(g.laplacian("weight", True), mx0) - - 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]]) - -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/igraph/test/structural.py b/igraph/test/structural.py deleted file mode 100644 index 500fd9d27..000000000 --- a/igraph/test/structural.py +++ /dev/null @@ -1,543 +0,0 @@ -from __future__ import division - -import unittest - -from igraph import * -from igraph.compat import isnan - -class SimplePropertiesTests(unittest.TestCase): - 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) - 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) - - 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.g.diameter() == 2) - self.assertTrue(self.gdir.diameter(False) == 2) - self.assertTrue(self.gdir.diameter() == 3) - self.assertTrue(self.tree.diameter() == 5) - - 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)) - - d = self.tree.get_diameter() - self.assertTrue(d[0] == 13 or d[-1] == 13) - - weights = [1, 1, 1, 5, 1, 5, 1, 1, 1, 1, 1, 1, 5] - self.assertTrue(self.tree.diameter(weights=weights) == 15) - - d = self.tree.farthest_points(weights=weights) - 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.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(Graph().eccentricity(), []) - - def testRadius(self): - self.assertEqual(self.gfull.radius(), 1) - self.assertEqual(self.gempty.radius(), 0) - self.assertEqual(self.g.radius(), 1) - self.assertEqual(self.gdir.radius(), 1) - self.assertEqual(self.tree.radius(), 3) - self.assertTrue(isnan(Graph().radius())) - - def testTransitivity(self): - self.assertTrue(self.gfull.transitivity_undirected() == 1.0) - self.assertTrue(self.tree.transitivity_undirected() == 0.0) - 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()) - - 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]) - - 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) - - 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) - - def testModularity(self): - 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 - self.assertAlmostEqual(g.modularity(cl, ws), 0.4523, places=3) - ws = [2]*21 - self.assertAlmostEqual(g.modularity(cl, ws), 0.4523, places=3) - 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) - 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) - tree = Graph.Tree(10, 3) - - def testKnn(self): - knn, knnk = self.gfull.knn() - self.assertTrue(knn == [9.] * 10) - self.assertAlmostEqual(knnk[8], 9.0, places=6) - - # knn works for simple graphs only -- self.g is not simple - self.assertRaises(InternalError, self.g.knn) - - # Okay, simplify it and then go on - g = self.g.copy() - 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) - self.assertEqual(len(knnk), 3) - self.assertAlmostEqual(knnk[1], 3, places=6) - self.assertAlmostEqual(knnk[2], 7/3., places=6) - - def testDegree(self): - self.assertTrue(self.gfull.degree() == [9] * 10) - self.assertTrue(self.gempty.degree() == [0] * 10) - self.assertTrue(self.g.degree(loops=False) == [3, 3, 2, 2]) - self.assertTrue(self.g.degree() == [5, 3, 2, 2]) - self.assertTrue(self.gdir.degree(mode=IN) == [1, 2, 2, 2]) - self.assertTrue(self.gdir.degree(mode=OUT) == [3, 2, 1, 1]) - self.assertTrue(self.gdir.degree(mode=ALL) == [4, 4, 3, 3]) - vs = self.gdir.vs.select(0, 2) - self.assertTrue(self.gdir.degree(vs, mode=ALL) == [4, 3]) - self.assertTrue(self.gdir.degree(self.gdir.vs[1], mode=ALL) == 4) - - def testMaxDegree(self): - self.assertTrue(self.gfull.maxdegree() == 9) - self.assertTrue(self.gempty.maxdegree() == 0) - self.assertTrue(self.g.maxdegree() == 3) - self.assertTrue(self.g.maxdegree(loops=True) == 5) - self.assertTrue(self.g.maxdegree([1, 2], loops=True) == 3) - self.assertTrue(self.gdir.maxdegree(mode=IN) == 2) - self.assertTrue(self.gdir.maxdegree(mode=OUT) == 3) - self.assertTrue(self.gdir.maxdegree(mode=ALL) == 4) - - def testStrength(self): - # Turn off warnings about calling strength without weights - import warnings - warnings.filterwarnings("ignore", "No edge weights for strength calculation", \ - RuntimeWarning) - - # No weights - self.assertTrue(self.gfull.strength() == [9] * 10) - self.assertTrue(self.gempty.strength() == [0] * 10) - self.assertTrue(self.g.degree(loops=False) == [3, 3, 2, 2]) - 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) == [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]) - 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) - - - -class LocalTransitivityTests(unittest.TestCase): - def testLocalTransitivityFull(self): - trans = Graph.Full(10).transitivity_local_undirected() - self.assertTrue(trans == [1.0]*10) - - def testLocalTransitivityTree(self): - trans = Graph.Tree(10, 3).transitivity_local_undirected() - self.assertTrue(trans[0:3] == [0.0, 0.0, 0.0]) - - def testLocalTransitivityHalf(self): - g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) - trans = g.transitivity_local_undirected() - trans = [round(x, 3) for x in trans] - self.assertTrue(trans == [0.667, 0.667, 1.0, 1.0]) - - def testLocalTransitivityPartial(self): - g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) - 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)]) - - def testBiconnectedComponents(self): - s = self.g1.biconnected_components() - self.assertTrue(len(s) == 1 and s[0]==range(10)) - s, ap = self.g1.biconnected_components(True) - self.assertTrue(len(s) == 1 and s[0]==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]) - 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]) - - def testArticulationPoints(self): - self.assertTrue(self.g1.articulation_points() == []) - 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.]) - 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.]) - 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]) - - def testEdgeBetweennessCentrality(self): - g = Graph.Star(5) - self.assertTrue(g.edge_betweenness() == [4., 4., 4., 4.]) - 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.]) - 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]) - - def testClosenessCentrality(self): - g = Graph.Star(5) - cl = g.closeness() - cl2 = [1., 0.57142, 0.57142, 0.57142, 0.57142] - for idx in xrange(g.vcount()): - self.assertAlmostEqual(cl[idx], cl2[idx], places=3) - - g = Graph.Star(5) - cl = g.closeness(cutoff=1) - cl2 = [1., 0.25, 0.25, 0.25, 0.25] - for idx in xrange(g.vcount()): - self.assertAlmostEqual(cl[idx], cl2[idx], places=3) - - weights = [1] * 4 - - g = Graph.Star(5) - cl = g.closeness(weights=weights) - cl2 = [1., 0.57142, 0.57142, 0.57142, 0.57142] - for idx in xrange(g.vcount()): - self.assertAlmostEqual(cl[idx], cl2[idx], places=3) - - g = Graph.Star(5) - cl = g.closeness(cutoff=1, weights=weights) - cl2 = [1., 0.25, 0.25, 0.25, 0.25] - for idx in xrange(g.vcount()): - self.assertAlmostEqual(cl[idx], cl2[idx], places=3) - - def testPageRank(self): - g = Graph.Star(11) - cent = g.pagerank() - self.assertTrue(cent.index(max(cent)) == 0) - self.assertAlmostEqual(max(cent), 0.4668, places=3) - - 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.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) - - def testEigenvectorCentrality(self): - g = Graph.Star(11) - cent = g.evcent() - self.assertTrue(cent.index(max(cent)) == 0) - 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] - self.assertTrue(cent.index(max(cent)) == 0) - self.assertAlmostEqual(cent[1]/cent[0], 0.3162, places=3) - self.assertAlmostEqual(ev, 3.162, places=3) - - 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] - - 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] - - 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]) - - -class NeighborhoodTests(unittest.TestCase): - def testNeighborhood(self): - g = Graph.Ring(10, circular=False) - self.assertTrue(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(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(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]) - - -class MiscTests(unittest.TestCase): - 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 - - def testTopologicalSorting(self): - g = Graph(5, [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)], directed=True) - self.assertTrue(g.topological_sorting() == [0, 4, 1, 2, 3]) - self.assertTrue(g.topological_sorting(IN) == [3, 4, 2, 1, 0]) - g.to_undirected() - self.assertRaises(InternalError, g.topological_sorting) - - def testIsDAG(self): - g = Graph(5, [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)], directed=True) - self.assertTrue(g.is_dag()) - g.to_undirected() - self.assertFalse(g.is_dag()) - g = Graph.Barabasi(1000, 2, directed=True) - self.assertTrue(g.is_dag()) - g = Graph.GRG(100, 0.2) - self.assertFalse(g.is_dag()) - g = Graph.Ring(10, directed=True, mutual=False) - self.assertFalse(g.is_dag()) - - 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)]) - - g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)], directed=True) - el = g.linegraph().get_edgelist() - el.sort() - self.assertTrue(el == [(0, 2), (0, 4)]) - - -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.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], - [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]) - - def testGetShortestPaths(self): - 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) - sps = g.get_shortest_paths(0, output="vpath") - expected = [[0], [0, 1], [0, 2], [0, 1, 3]] - self.assertTrue(sps == expected) - sps = g.get_shortest_paths(0, output="epath") - expected = [[], [0], [1], [0, 2]] - self.assertTrue(sps == expected) - 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) - - sps = sorted(g.get_all_shortest_paths(0, 0)) - expected = [[0]] - self.assertEqual(expected, sps) - - sps = sorted(g.get_all_shortest_paths(0, 5)) - expected = [[0, 1, 2, 4, 5], [0, 1, 3, 4, 5]] - self.assertEqual(expected, sps) - - sps = sorted(g.get_all_shortest_paths(1, 4)) - expected = [[1, 2, 4], [1, 3, 4]] - self.assertEqual(expected, sps) - - 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]] - 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]] - 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.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) - - g = Graph.Lattice([4, 4], circular=False) - g.es["weight"] = 1 - 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)) - self.assertEqual(4, sum(1 for path in sps if path[-1] == 12)) - self.assertEqual(12, sum(1 for path in sps if path[-1] == 15)) - - 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) - - 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) - 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) - 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) - - g = Graph.Lattice([4, 4], circular=False) - g = Graph([(min(u, v), max(u, v)) for u, v in g.get_edgelist()], directed=True) - sps = sorted(g.get_all_simple_paths(0, 15)) - self.assertEqual(20, len(sps)) - for path in sps: - self.assertEqual(0, path[0]) - self.assertEqual(15, path[-1]) - curr = path[0] - for next in path[1:]: - self.assertTrue(g.are_connected(curr, next)) - curr = next - - def testPathLengthHist(self): - g = Graph.Tree(15, 2) - h = g.path_length_hist() - self.assertTrue(h.unconnected == 0L) - 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() - h = g.path_length_hist() - self.assertTrue(h.unconnected == 40) - h = g.path_length_hist(False) - self.assertTrue(h.unconnected == 20) - -def suite(): - simple_suite = unittest.makeSuite(SimplePropertiesTests) - degree_suite = unittest.makeSuite(DegreeTests) - local_transitivity_suite = unittest.makeSuite(LocalTransitivityTests) - biconnected_suite = unittest.makeSuite(BiconnectedComponentTests) - centrality_suite = unittest.makeSuite(CentralityTests) - neighborhood_suite = unittest.makeSuite(NeighborhoodTests) - path_suite = unittest.makeSuite(PathTests) - misc_suite = unittest.makeSuite(MiscTests) - return unittest.TestSuite([simple_suite, - degree_suite, - local_transitivity_suite, - biconnected_suite, - centrality_suite, - neighborhood_suite, - path_suite, - misc_suite]) - -def test(): - runner = unittest.TextTestRunner() - runner.run(suite()) - -if __name__ == "__main__": - test() - diff --git a/igraph/test/utils.py b/igraph/test/utils.py deleted file mode 100644 index 1a01d78ca..000000000 --- a/igraph/test/utils.py +++ /dev/null @@ -1,72 +0,0 @@ -"""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, types.ClassType)): - @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 - - -@contextmanager -def temporary_file(content=None, mode=None): - tmpf, tmpfname = tempfile.mkstemp() - os.close(tmpf) - - if mode is None: - if content is None: - mode = "rb" - else: - mode = "wb" - - tmpf = open(tmpfname, mode) - if content is not None: - if isinstance(content, unicode): - tmpf.write(dedent(content).encode("utf8")) - else: - tmpf.write(content) - - tmpf.close() - yield tmpfname - os.unlink(tmpfname) - - -is_pypy = (platform.python_implementation() == "PyPy") diff --git a/igraph/test/walks.py b/igraph/test/walks.py deleted file mode 100644 index 5f41ef1a3..000000000 --- a/igraph/test/walks.py +++ /dev/null @@ -1,69 +0,0 @@ -import random -import unittest -from igraph import Graph, InternalError - - -class RandomWalkTests(unittest.TestCase): - def validate_walk(self, g, walk, start, length, mode="out"): - prev = None - for vertex in walk: - if prev is not None: - self.assertTrue(vertex in g.neighbors(prev, mode=mode)) - else: - self.assertEqual(start, vertex) - prev = vertex - - def testRandomWalkUndirected(self): - g = Graph.GRG(100, 0.2) - for i in xrange(100): - 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) - - def testRandomWalkDirectedOut(self): - g = Graph.Tree(121, 3, mode="out") - mode = "out" - for i in xrange(100): - start = 0 - length = random.randint(0, 4) - walk = g.random_walk(start, length, mode) - self.validate_walk(g, walk, start, length, mode) - - def testRandomWalkDirectedIn(self): - g = Graph.Tree(121, 3, mode="out") - mode = "in" - for i in xrange(100): - 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) - - def testRandomWalkDirectedAll(self): - g = Graph.Tree(121, 3, mode="out") - mode = "all" - for i in xrange(100): - 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) - - def testRandomWalkStuck(self): - g = Graph.Ring(10, circular=False, directed=True) - walk = g.random_walk(5, 20) - self.assertEqual([5, 6, 7, 8, 9], walk) - self.assertRaises(InternalError, g.random_walk, 5, 20, stuck="error") - - -def suite(): - random_walk_suite = unittest.makeSuite(RandomWalkTests) - return unittest.TestSuite([random_walk_suite]) - - -def test(): - runner = unittest.TextTestRunner() - runner.run(suite()) - - -if __name__ == "__main__": - test() diff --git a/igraph/vendor/__init__.py b/igraph/vendor/__init__.py deleted file mode 100644 index 915ca8ab1..000000000 --- a/igraph/vendor/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -This package contains third party libraries that igraph depends on and that -are small enough to be distributed with igraph itself. - -The primary entry point of this module is ``vendor_import``, a function that -first tries to import a particular library using the standard Python mechanism -and falls back to the version of the library provided within ``igraph.vendor`` -if the standard Python import fails. - -The libraries contained within igraph are as follows: - - - `texttable`, a library to print ASCII tables, by Gerome Fournier. - See . -""" - -__license__ = "GPL" - -__all__ = ["vendor_import"] -__docformat__ = "restructuredtext en" - -def vendor_import(module_name): - """Tries to import a module name ``module_name`` using the standard Python - `import` statement and return the imported module. If the import fails, - tries to import a module of the same name from within ``igraph.vendor`` - and return that module instead. - """ - - parts = module_name.split(".") - - try: - result = __import__(module_name, level=0) - except ImportError: - result = __import__("igraph.vendor.%s" % module_name, level=0) - parts[0:0] = ["igraph", "vendor"] - - parts.pop(0) - while parts: - result = getattr(result, parts.pop(0)) - - return result diff --git a/igraph/vendor/texttable.py b/igraph/vendor/texttable.py deleted file mode 100644 index 086ff43c3..000000000 --- a/igraph/vendor/texttable.py +++ /dev/null @@ -1,586 +0,0 @@ -#!/usr/bin/env python -# -# texttable - module for creating simple ASCII tables -# Copyright (C) 2003-2011 Gerome Fournier -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -"""module for creating simple ASCII tables - - -Example: - - table = Texttable() - table.set_cols_align(["l", "r", "c"]) - table.set_cols_valign(["t", "m", "b"]) - table.add_rows([ ["Name", "Age", "Nickname"], - ["Mr\\nXavier\\nHuon", 32, "Xav'"], - ["Mr\\nBaptiste\\nClement", 1, "Baby"] ]) - print table.draw() + "\\n" - - table = Texttable() - table.set_deco(Texttable.HEADER) - table.set_cols_dtype(['t', # text - 'f', # float (decimal) - 'e', # float (exponent) - 'i', # integer - 'a']) # automatic - table.set_cols_align(["l", "r", "r", "r", "l"]) - table.add_rows([["text", "float", "exp", "int", "auto"], - ["abcd", "67", 654, 89, 128.001], - ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], - ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], - ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) - print table.draw() - -Result: - - +----------+-----+----------+ - | Name | Age | Nickname | - +==========+=====+==========+ - | Mr | | | - | Xavier | 32 | | - | Huon | | Xav' | - +----------+-----+----------+ - | Mr | | | - | Baptiste | 1 | | - | Clement | | Baby | - +----------+-----+----------+ - - text float exp int auto - =========================================== - abcd 67.000 6.540e+02 89 128.001 - efgh 67.543 6.540e-01 90 1.280e+22 - ijkl 0.000 5.000e-78 89 0.000 - mnop 0.023 5.000e+78 92 1.280e+22 -""" - -__all__ = ["Texttable", "ArraySizeError"] - -__author__ = 'Gerome Fournier ' -__license__ = 'GPL' -__version__ = '0.8.1' -__credits__ = """\ -Jeff Kowalczyk: - - textwrap improved import - - comment concerning header output - -Anonymous: - - add_rows method, for adding rows in one go - -Sergey Simonenko: - - redefined len() function to deal with non-ASCII characters - -Roger Lew: - - columns datatype specifications - -Brian Peterson: - - better handling of unicode errors -""" - -import sys -import string - -try: - if sys.version >= '2.3': - import textwrap - elif sys.version >= '2.2': - from optparse import textwrap - else: - from optik import textwrap -except ImportError: - sys.stderr.write("Can't import textwrap module!\n") - raise - -def len(iterable): - """Redefining len here so it will be able to work with non-ASCII characters - """ - if not isinstance(iterable, str): - return iterable.__len__() - - try: - return len(unicode(iterable, 'utf')) - except: - return iterable.__len__() - -class ArraySizeError(Exception): - """Exception raised when specified rows don't fit the required size - """ - - def __init__(self, msg): - self.msg = msg - Exception.__init__(self, msg, '') - - def __str__(self): - return self.msg - -class Texttable: - - BORDER = 1 - HEADER = 1 << 1 - HLINES = 1 << 2 - VLINES = 1 << 3 - - def __init__(self, max_width=80): - """Constructor - - - max_width is an integer, specifying the maximum width of the table - - if set to 0, size is unlimited, therefore cells won't be wrapped - """ - - if max_width <= 0: - max_width = False - self._max_width = max_width - self._precision = 3 - - self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | \ - Texttable.HEADER - self.set_chars(['-', '|', '+', '=']) - self.reset() - - def reset(self): - """Reset the instance - - - reset rows and header - """ - - self._hline_string = None - self._row_size = None - self._header = [] - self._rows = [] - - def set_chars(self, array): - """Set the characters used to draw lines between rows and columns - - - the array should contain 4 fields: - - [horizontal, vertical, corner, header] - - - default is set to: - - ['-', '|', '+', '='] - """ - - if len(array) != 4: - raise ArraySizeError, "array should contain 4 characters" - array = [ x[:1] for x in [ str(s) for s in array ] ] - (self._char_horiz, self._char_vert, - self._char_corner, self._char_header) = array - - def set_deco(self, deco): - """Set the table decoration - - - 'deco' can be a combinaison of: - - Texttable.BORDER: Border around the table - Texttable.HEADER: Horizontal line below the header - Texttable.HLINES: Horizontal lines between rows - Texttable.VLINES: Vertical lines between columns - - All of them are enabled by default - - - example: - - Texttable.BORDER | Texttable.HEADER - """ - - self._deco = deco - - def set_cols_align(self, array): - """Set the desired columns alignment - - - the elements of the array should be either "l", "c" or "r": - - * "l": column flushed left - * "c": column centered - * "r": column flushed right - """ - - self._check_row_size(array) - self._align = array - - def set_cols_valign(self, array): - """Set the desired columns vertical alignment - - - the elements of the array should be either "t", "m" or "b": - - * "t": column aligned on the top of the cell - * "m": column aligned on the middle of the cell - * "b": column aligned on the bottom of the cell - """ - - self._check_row_size(array) - self._valign = array - - def set_cols_dtype(self, array): - """Set the desired columns datatype for the cols. - - - the elements of the array should be either "a", "t", "f", "e" or "i": - - * "a": automatic (try to use the most appropriate datatype) - * "t": treat as text - * "f": treat as float in decimal format - * "e": treat as float in exponential format - * "i": treat as int - - - by default, automatic datatyping is used for each column - """ - - self._check_row_size(array) - self._dtype = array - - def set_cols_width(self, array): - """Set the desired columns width - - - the elements of the array should be integers, specifying the - width of each column. For example: - - [10, 20, 5] - """ - - self._check_row_size(array) - try: - array = map(int, array) - if reduce(min, array) <= 0: - raise ValueError - except ValueError: - sys.stderr.write("Wrong argument in column width specification\n") - raise - self._width = array - - def set_precision(self, width): - """Set the desired precision for float/exponential formats - - - width must be an integer >= 0 - - - default value is set to 3 - """ - - if not type(width) is int or width < 0: - raise ValueError('width must be an integer greater then 0') - self._precision = width - - def header(self, array): - """Specify the header of the table - """ - - self._check_row_size(array) - self._header = map(str, array) - - def add_row(self, array): - """Add a row in the rows stack - - - cells can contain newlines and tabs - """ - - self._check_row_size(array) - - if not hasattr(self, "_dtype"): - self._dtype = ["a"] * self._row_size - - cells = [] - for i,x in enumerate(array): - cells.append(self._str(i,x)) - self._rows.append(cells) - - def add_rows(self, rows, header=True): - """Add several rows in the rows stack - - - The 'rows' argument can be either an iterator returning arrays, - or a by-dimensional array - - 'header' specifies if the first row should be used as the header - of the table - """ - - # nb: don't use 'iter' on by-dimensional arrays, to get a - # usable code for python 2.1 - if header: - if hasattr(rows, '__iter__') and hasattr(rows, 'next'): - self.header(rows.next()) - else: - self.header(rows[0]) - rows = rows[1:] - for row in rows: - self.add_row(row) - - def draw(self): - """Draw the table - - - the table is returned as a whole string - """ - - if not self._header and not self._rows: - return - self._compute_cols_width() - self._check_align() - out = "" - if self._has_border(): - out += self._hline() - if self._header: - out += self._draw_line(self._header, isheader=True) - if self._has_header(): - out += self._hline_header() - length = 0 - for row in self._rows: - length += 1 - out += self._draw_line(row) - if self._has_hlines() and length < len(self._rows): - out += self._hline() - if self._has_border(): - out += self._hline() - return out[:-1] - - def _str(self, i, x): - """Handles string formatting of cell data - - i - index of the cell datatype in self._dtype - x - cell data to format - """ - try: - f = float(x) - except: - return str(x) - - n = self._precision - dtype = self._dtype[i] - - if dtype == 'i': - return str(int(round(f))) - elif dtype == 'f': - return '%.*f' % (n, f) - elif dtype == 'e': - return '%.*e' % (n, f) - elif dtype == 't': - return str(x) - else: - if f - round(f) == 0: - if abs(f) > 1e8: - return '%.*e' % (n, f) - else: - return str(int(round(f))) - else: - if abs(f) > 1e8: - return '%.*e' % (n, f) - else: - return '%.*f' % (n, f) - - def _check_row_size(self, array): - """Check that the specified array fits the previous rows size - """ - - if not self._row_size: - self._row_size = len(array) - elif self._row_size != len(array): - raise ArraySizeError, "array should contain %d elements" \ - % self._row_size - - def _has_vlines(self): - """Return a boolean, if vlines are required or not - """ - - return self._deco & Texttable.VLINES > 0 - - def _has_hlines(self): - """Return a boolean, if hlines are required or not - """ - - return self._deco & Texttable.HLINES > 0 - - def _has_border(self): - """Return a boolean, if border is required or not - """ - - return self._deco & Texttable.BORDER > 0 - - def _has_header(self): - """Return a boolean, if header line is required or not - """ - - return self._deco & Texttable.HEADER > 0 - - def _hline_header(self): - """Print header's horizontal line - """ - - return self._build_hline(True) - - def _hline(self): - """Print an horizontal line - """ - - if not self._hline_string: - self._hline_string = self._build_hline() - return self._hline_string - - def _build_hline(self, is_header=False): - """Return a string used to separated rows or separate header from - rows - """ - horiz = self._char_horiz - if (is_header): - horiz = self._char_header - # compute cell separator - s = "%s%s%s" % (horiz, [horiz, self._char_corner][self._has_vlines()], - horiz) - # build the line - l = string.join([horiz * n for n in self._width], s) - # add border if needed - if self._has_border(): - l = "%s%s%s%s%s\n" % (self._char_corner, horiz, l, horiz, - self._char_corner) - else: - l += "\n" - return l - - def _len_cell(self, cell): - """Return the width of the cell - - Special characters are taken into account to return the width of the - cell, such like newlines and tabs - """ - - cell_lines = cell.split('\n') - maxi = 0 - for line in cell_lines: - length = 0 - parts = line.split('\t') - for part, i in zip(parts, range(1, len(parts) + 1)): - length = length + len(part) - if i < len(parts): - length = (length/8 + 1) * 8 - maxi = max(maxi, length) - return maxi - - def _compute_cols_width(self): - """Return an array with the width of each column - - If a specific width has been specified, exit. If the total of the - columns width exceed the table desired width, another width will be - computed to fit, and cells will be wrapped. - """ - - if hasattr(self, "_width"): - return - maxi = [] - if self._header: - maxi = [ self._len_cell(x) for x in self._header ] - for row in self._rows: - for cell,i in zip(row, range(len(row))): - try: - maxi[i] = max(maxi[i], self._len_cell(cell)) - except (TypeError, IndexError): - maxi.append(self._len_cell(cell)) - items = len(maxi) - length = reduce(lambda x,y: x+y, maxi) - if self._max_width and length + items * 3 + 1 > self._max_width: - maxi = [(self._max_width - items * 3 -1) / items \ - for n in range(items)] - self._width = maxi - - def _check_align(self): - """Check if alignment has been specified, set default one if not - """ - - if not hasattr(self, "_align"): - self._align = ["l"] * self._row_size - if not hasattr(self, "_valign"): - self._valign = ["t"] * self._row_size - - def _draw_line(self, line, isheader=False): - """Draw a line - - Loop over a single cell length, over all the cells - """ - - line = self._splitit(line, isheader) - space = " " - out = "" - for i in range(len(line[0])): - if self._has_border(): - out += "%s " % self._char_vert - length = 0 - for cell, width, align in zip(line, self._width, self._align): - length += 1 - cell_line = cell[i] - fill = width - len(cell_line) - if isheader: - align = "c" - if align == "r": - out += "%s " % (fill * space + cell_line) - elif align == "c": - out += "%s " % (fill/2 * space + cell_line \ - + (fill/2 + fill%2) * space) - else: - out += "%s " % (cell_line + fill * space) - if length < len(line): - out += "%s " % [space, self._char_vert][self._has_vlines()] - out += "%s\n" % ['', self._char_vert][self._has_border()] - return out - - def _splitit(self, line, isheader): - """Split each element of line to fit the column width - - Each element is turned into a list, result of the wrapping of the - string to the desired width - """ - - line_wrapped = [] - for cell, width in zip(line, self._width): - array = [] - for c in cell.split('\n'): - try: - c = unicode(c, 'utf') - except UnicodeDecodeError, strerror: - sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (c, strerror)) - c = unicode(c, 'utf', 'replace') - array.extend(textwrap.wrap(c, width)) - line_wrapped.append(array) - max_cell_lines = reduce(max, map(len, line_wrapped)) - for cell, valign in zip(line_wrapped, self._valign): - if isheader: - valign = "t" - if valign == "m": - missing = max_cell_lines - len(cell) - cell[:0] = [""] * (missing / 2) - cell.extend([""] * (missing / 2 + missing % 2)) - elif valign == "b": - cell[:0] = [""] * (max_cell_lines - len(cell)) - else: - cell.extend([""] * (max_cell_lines - len(cell))) - return line_wrapped - -if __name__ == '__main__': - table = Texttable() - table.set_cols_align(["l", "r", "c"]) - table.set_cols_valign(["t", "m", "b"]) - table.add_rows([ ["Name", "Age", "Nickname"], - ["Mr\nXavier\nHuon", 32, "Xav'"], - ["Mr\nBaptiste\nClement", 1, "Baby"] ]) - print table.draw() + "\n" - - table = Texttable() - table.set_deco(Texttable.HEADER) - table.set_cols_dtype(['t', # text - 'f', # float (decimal) - 'e', # float (exponent) - 'i', # integer - 'a']) # automatic - table.set_cols_align(["l", "r", "r", "r", "l"]) - table.add_rows([["text", "float", "exp", "int", "auto"], - ["abcd", "67", 654, 89, 128.001], - ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], - ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], - ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) - print table.draw() diff --git a/pylint.rc b/pylint.rc deleted file mode 100644 index 4c50648f0..000000000 --- a/pylint.rc +++ /dev/null @@ -1,310 +0,0 @@ -# lint Python modules using external checkers. -# -# This is the main checker controlling the other ones and the reports -# generation. It is itself both a raw checker and an astng checker in order -# to: -# * handle message activation / deactivation at the module level -# * handle some basic but necessary stats'data (number of classes, methods...) -# -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Profiled execution. -profile=no - -# Add to the black list. It should be a base name, not a -# path. You may set this option multiple times. -ignore=.bzr - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - - -[MESSAGES CONTROL] - -# Enable only checker(s) with the given id(s). This option conflicts with the -# disable-checker option -#enable-checker= - -# Enable all checker(s) except those with the given id(s). This option -# conflicts with the enable-checker option -#disable-checker= - -# Enable all messages in the listed categories (IRCWEF). -#enable-msg-cat= - -# Disable all messages in the listed categories (IRCWEF). -disable-msg-cat=I - -# Enable the message(s) with the given id(s). -#enable-msg= - -# Disable the message(s) with the given id(s). -disable-msg=W0704 - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html -output-format=text - -# Include message's id in output -include-ids=yes - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (R0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (R0004). -comment=no - -# Enable the report(s) with the given id(s). -#enable-report= - -# Disable the report(s) with the given id(s). -#disable-report= - - -# checks for : -# * doc strings -# * modules / classes / functions / methods / arguments / variables name -# * number of arguments, local variables, branches, returns and statements in -# functions, methods -# * required module attributes -# * dangerous default values as arguments -# * redefinition of function / method / class -# * uses of the global statement -# -[BASIC] - -# Required attributes for module, separated by a comma -required-attributes= - -# Regular expression which should only match functions or classes name which do -# not require a docstring -no-docstring-rgx=__.*__ - -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct list comprehension / -# generator expression variable names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,l,m,n,x,y,v,e,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,apply,input - - -# try to find bugs in the code using type inference -# -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). -ignored-classes=SQLObject - -# When zope mode is activated, add a predefined set of Zope acquired attributes -# to generated-members. -zope=no - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E0201 when accessed. -generated-members=REQUEST,acl_users,aq_parent - - -# checks for -# * unused variables / imports -# * undefined variables -# * redefinition of variable from builtins or from an outer scope -# * use of variable before assignment -# -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching names used for dummy variables (i.e. not used). -dummy-variables-rgx=_|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - - -# checks for : -# * methods without self as first argument -# * overridden methods signature -# * access only to existent members via self -# * attributes not defined in the __init__ method -# * supported interfaces implementation -# * unreachable code -# -[CLASSES] - -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - - -# checks for sign of poor/misdesign: -# * number of methods, attributes, local variables... -# * size, complexity of functions, methods -# -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branchs=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - - -# checks for -# * external modules dependencies -# * relative / wildcard imports -# * cyclic imports -# * uses of deprecated modules -# -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report R0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report R0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report R0402 must -# not be disabled) -int-import-graph= - - -# checks for : -# * unauthorized constructions -# * strict indentation -# * line length -# * use of <> instead of != -# -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=80 - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - - -# checks for: -# * warning notes in the code like FIXME, XXX -# * PEP 263: source code with non ascii character but no encoding declaration -# -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -# checks for similarities and duplicated code. This computation may be -# memory / CPU intensive, so you should disable it if you experiments some -# problems. -# -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..db932f633 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = [ + # pin setuptools: + # https://github.com/airspeed-velocity/asv/pull/1426#issuecomment-2290658198 + # Most likely cause: + # https://github.com/pypa/distutils/issues/283 + # Workaround based on this commit: + # https://github.com/harfbuzz/uharfbuzz/commit/9b607bd06fb17fcb4abe3eab5c4f342ad08309d7 + "setuptools>=64,<72.2.0; platform_python_implementation == 'PyPy'", + "setuptools>=64; platform_python_implementation != 'PyPy'", + "cmake>=3.18" +] +build-backend = "setuptools.build_meta" + +[tool.ruff] +lint.ignore = ["B905", "C901", "E402", "E501"] +lint.select = ["B", "C", "E", "F", "W"] diff --git a/test/doctests.py b/scripts/doctests.py similarity index 62% rename from test/doctests.py rename to scripts/doctests.py index 031a19655..685d3046d 100755 --- a/test/doctests.py +++ b/scripts/doctests.py @@ -9,10 +9,18 @@ def run_doctests(): import igraph + num_failed = 0 - modules = [igraph, igraph.clustering, igraph.cut, igraph.datatypes, - igraph.drawing.utils, igraph.formula, igraph.remote.nexus, - igraph.statistics, igraph.utils] + modules = [ + igraph, + igraph.clustering, + igraph.cut, + igraph.datatypes, + igraph.drawing.utils, + igraph.formula, + igraph.statistics, + igraph.utils, + ] for module in modules: num_failed += doctest.testmod(module)[0] return num_failed == 0 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/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/fix_pyodide_build.py b/scripts/fix_pyodide_build.py new file mode 100644 index 000000000..9e239f973 --- /dev/null +++ b/scripts/fix_pyodide_build.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +import pyodide_build + +from pathlib import Path +from urllib.request import urlretrieve + +target_dir = ( + Path(pyodide_build.__file__).parent / "tools" / "cmake" / "Modules" / "Platform" +) +target_dir.mkdir(exist_ok=True, parents=True) + +target_file = target_dir / "Emscripten.cmake" +if not target_file.is_file(): + url = "https://raw.githubusercontent.com/pyodide/pyodide-build/main/pyodide_build/tools/cmake/Modules/Platform/Emscripten.cmake" + urlretrieve(url, str(target_file)) diff --git a/scripts/igraph b/scripts/igraph deleted file mode 100755 index e88e35721..000000000 --- a/scripts/igraph +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python -"""Small script to execute the igraph command line interface""" -from igraph.app.shell import main -main() diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 7b58af6d4..9309987f6 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -1,65 +1,141 @@ -#!/bin/sh +#!/bin/bash # -# Creates documentation for igraph's Python interface using epydoc +# Creates the API documentation for igraph's Python interface # -# Usage: ./mkdoc.sh [--sync] [directory] +# Usage: ./mkdoc.sh (makes API and tutorial docs) +# ./mkdoc.sh -d (makes a Dash docset based on standalone docs, requires doc2dash) +# +# Add -c to ensure that the documentation is built from scratch and no cached +# assets from previous builds are used. +# +# Make sure we bail out on build errors +set -e + +DOC2DASH=0 +LINKCHECK=0 +CLEAN=0 + +while getopts ":cdl" OPTION; do + case $OPTION in + c) + CLEAN=1 + ;; + d) + DOC2DASH=1 + ;; + l) + LINKCHECK=1 + ;; + \?) + echo "Usage: $0 [-cdl]" + echo "" + echo "-c: clean and force a full rebuild of the documentation" + echo "-d: generate Dash docset with doc2dash" + echo "-l: check the generated documentation for broken links" + exit 1 + ;; + esac +done +shift $((OPTIND - 1)) -SCRIPTS_FOLDER=`dirname $0` +SCRIPTS_FOLDER=$(dirname $0) cd ${SCRIPTS_FOLDER}/.. -ROOT_FOLDER=`pwd` -DOC_API_FOLDER=${ROOT_FOLDER}/doc/api -CONFIG=${ROOT_FOLDER}/scripts/epydoc.cfg +ROOT_FOLDER=$(pwd) +DOC_SOURCE_FOLDER=${ROOT_FOLDER}/doc/source +DOC_HTML_FOLDER=${ROOT_FOLDER}/doc/html +DOC_LINKCHECK_FOLDER=${ROOT_FOLDER}/doc/linkcheck +SCRIPTS_FOLDER=${ROOT_FOLDER}/scripts 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..." - exit 1 -fi -PWD=`pwd` +# Create a virtual environment +if [ ! -d ".venv" ]; then + echo "Creating virtualenv..." + ${PYTHON:-python3} -m venv .venv -SYNC=0 -if [ x$1 = x--sync ]; then - SYNC=1 - shift -fi -if [ x$1 != x ]; then - cd $1 || exit 1 + # Install documentation dependencies into the venv. + # doc2dash is optional; it will be installed when -d is given + .venv/bin/pip install -q -U pip wheel sphinx==7.4.7 matplotlib pandas scipy pydoctor sphinx-rtd-theme iplotx +else + # Upgrade pip in the virtualenv + echo "Upgrading pip in virtualenv..." + .venv/bin/pip install -q -U pip wheel fi -echo "Checking symlinked _igraph.so in ${ROOT_FOLDER}/igraph..." -if [ ! -e ${ROOT_FOLDER}/igraph/_igraph.so -o ! -L ${ROOT_FOLDER}/igraph/_igraph.so ]; then - rm -f ${ROOT_FOLDER}/igraph/_igraph.so - cd ${ROOT_FOLDER}/igraph - ln -s ../build/lib*/igraph/_igraph.so . - cd ${ROOT_FOLDER} +# Make sure that documentation dependencies are up-to-date in the virtualenv +echo "Making sure that all dependencies are up-to-date..." +.venv/bin/pip install -q -U sphinx==7.4.7 pydoctor sphinx-gallery sphinxcontrib-jquery sphinx-rtd-theme iplotx +if [ x$DOC2DASH = x1 ]; then + .venv/bin/pip install -U doc2dash fi -echo "Removing existing documentation..." -rm -rf html +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 "Generating HTML documentation..." -${EPYDOC} --html -v -o ${DOC_API_FOLDER}/html --config ${CONFIG} +echo "Installing igraph in virtualenv..." +rm -f dist/*.whl && .venv/bin/pip wheel -q -w dist . && .venv/bin/pip install -q --force-reinstall dist/*.whl -PDF=0 -which latex >/dev/null && PDF=1 +echo "Patching modularized Graph methods" +.venv/bin/python3 ${SCRIPTS_FOLDER}/patch_modularized_graph_methods.py -if [ $PDF -eq 1 ]; then - echo "Generating PDF documentation..." -${EPYDOC} --pdf -v -o ${DOC_API_FOLDER}/pdf --config ${CONFIG} +echo "Clean previous docs" +rm -rf "${DOC_HTML_FOLDER}" + +if [ "x$CLEAN" = "x1" ]; then + # This is generated by sphinx-gallery + rm -rf "${DOC_SOURCE_FOLDER}/tutorials" fi -if [ $SYNC -eq 1 ]; then - echo "Syncing documentation to web" - cp ${DOC_API_FOLDER}/pdf/igraph.pdf ${DOC_API_FOLDER}/html - rsync --delete -avz ${DOC_API_FOLDER}/html/ csardi@igraph.org:2222:www/doc/python/ - rm ${DOC_API_FOLDER}/html/igraph.pdf +if [ "x$LINKCHECK" = "x1" ]; then + echo "Check for broken links" + .venv/bin/python -m sphinx \ + -T \ + -b linkcheck \ + -Dtemplates_path='' \ + -Dhtml_theme='alabaster' \ + ${DOC_SOURCE_FOLDER} ${DOC_LINKCHECK_FOLDER} fi -cd "$PWD" +echo "Generating HTML documentation..." +.venv/bin/pip install -q -U sphinx-rtd-theme +.venv/bin/python -m sphinx -T -b html ${DOC_SOURCE_FOLDER} ${DOC_HTML_FOLDER} + +echo "HTML documentation generated in ${DOC_HTML_FOLDER}" + +# doc2dash +if [ "x$DOC2DASH" = "x1" ]; then + PWD=$(pwd) + # Output folder of sphinx (before Jekyll if requested) + DOC_API_FOLDER=${ROOT_FOLDER}/doc/html/api + DOC2DASH=.venv/bin/doc2dash + DASH_FOLDER=${ROOT_FOLDER}/doc/dash + if [ "x$DOC2DASH" != x ]; then + echo "Generating Dash docset..." + "$DOC2DASH" \ + --online-redirect-url "https://python.igraph.org/en/latest/api/" \ + --name "python-igraph" \ + -d "${DASH_FOLDER}" \ + -f \ + -j \ + -I "index.html" \ + --icon ${ROOT_FOLDER}/doc/source/icon.png \ + --icon-2x ${ROOT_FOLDER}/doc/source/icon@2x.png \ + "${DOC_API_FOLDER}" + DASH_READY=1 + else + echo "WARNING: doc2dash not installed, skipping Dash docset generation." + DASH_READY=0 + fi + + echo "" + if [ "x${DASH_READY}" = x1 ]; then + echo "Dash docset generated in ${DASH_FOLDER}/python-igraph.docset" + fi + + cd "$PWD" +fi diff --git a/scripts/patch_modularized_graph_methods.py b/scripts/patch_modularized_graph_methods.py new file mode 100644 index 000000000..081ab73a5 --- /dev/null +++ b/scripts/patch_modularized_graph_methods.py @@ -0,0 +1,88 @@ +import os +import inspect + +import igraph + + +# FIXME: there must be a better way to do this +auxiliary_imports = [ + ("typing", "*"), + ("igraph.io.files", "_identify_format"), + ("igraph.community", "_optimal_cluster_count_from_merges_and_modularity"), +] + + +def main(): + # 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 = {} + underscore_functions = set() + for mname, method in methods: + # Source code of the function that uses the method + source = inspect.getsourcelines(method)[0] + + # Find function name for this modularized method + fname = source[0][source[0].find("def ") + 4 : source[0].find("(")] + + # FIXME: this also swaps in methods that are already there. While + # that should be fine, we could check + + # Make new source code, which is the same but with the name swapped + newsource = [source[0].replace(fname, mname)] + source[1:] + methodsources[mname] = newsource + + # Prepare to delete the import for underscore functions + if fname.startswith("_"): + underscore_functions.add(fname) + + newmodule = igraph.__file__ + ".new" + with open(newmodule, "wt") as fout: + # FIXME: whitelisting all cases is not great, try to improve + for origin, value in auxiliary_imports: + fout.write(f"from {origin} import {value}\n") + + with open(igraph.__file__, "rt") as f: + # Swap in the method sources + for line in f: + mtype = None + + for mname in methodsources: + # Class methods (constructors) + if " " + mname + " = classmethod(_" in line: + mtype = "class" + break + + # Instance methods (excluding factories e.g. 3d layouts) + if (" " + mname + " = _" in line) and ("(" not in line): + mtype = "instance" + break + + else: + fout.write(line) + continue + + # Method found, substitute and remove from dict + fout.write("\n") + if mtype == "class": + fout.write(" @classmethod\n") + for mline in methodsources[mname]: + # Correct indentation + fout.write(" " + mline) + + del methodsources[mname] + + # 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) + + +if __name__ == "__main__": + main() diff --git a/scripts/release-osx.sh b/scripts/release-osx.sh deleted file mode 100755 index 65efecd75..000000000 --- a/scripts/release-osx.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/bin/bash -# Creates the OS X installer package and puts it in a disk image - -FATLIB=../igraph/fatbuild/libigraph.dylib -PYTHON_VERSIONS="2.6 2.7" - -function check_universal { - if [ `file $1 | grep -c "binary with 2 architectures"` -lt 1 ]; then - echo "$1 is not a universal binary" - exit 2 - fi -} - -function get_dependent_libraries { - local LIBS=`otool -L $1 | awk 'NR >= 2 { print }' | cut -f 2 | cut -d ' ' -f 1` - echo "$LIBS" -} - -function check_library_paths { - local LIB - for LIB in $2; do - DIR=`dirname $LIB` - if [ x$DIR != x/usr/lib -a x$DIR != x/usr/local/lib ]; then - echo "$1 links to disallowed library: $LIB" - exit 3 - fi - done -} - -function check_mandatory_library_linkage { - local LIB - for LIB in $2; do - if [ x$LIB = x$3 ]; then - return - fi - done - echo "$1 does not link to required library: $3" - exit 4 -} - -function check_universal { - if [ `file $1 | grep -c "binary with 2 architectures"` -lt 1 ]; then - echo "$1 is not a universal binary" - exit 2 - fi -} - -function get_dependent_libraries { - local LIBS=`otool -L $1 | awk 'NR >= 2 { print }' | cut -f 2 | cut -d ' ' -f 1` - echo "$LIBS" -} - -function check_library_paths { - local LIB - for LIB in $2; do - DIR=`dirname $LIB` - if [ x$DIR != x/usr/lib -a x$DIR != x/usr/local/lib ]; then - echo "$1 links to disallowed library: $LIB" - exit 3 - fi - done -} - -function check_mandatory_library_linkage { - local LIB - for LIB in $2; do - if [ x$LIB = x$3 ]; then - return - fi - done - echo "$1 does not link to required library: $3" - exit 4 -} - -# Check whether we are running the script on Mac OS X -which hdiutil >/dev/null || ( echo "This script must be run on OS X"; exit 1 ) - -# Find the directory with setup.py -CWD=`pwd` -while [ ! -f setup.py ]; do cd ..; done - -# Extract the version number from setup.py -VERSION=`cat setup.py | grep "VERSION =" | cut -d '=' -f 2 | tr -d "' "` -VERSION_UNDERSCORE=`echo ${VERSION} | tr '-' '_'` - -# Ensure that the igraph library we are linking to is a fat binary -if [ ! -f ${FATLIB} ]; then - pushd ../igraph && tools/fatbuild.sh && popd - if [ ! -f ${FATLIB} ]; then - echo "Failed to build fat igraph library: ${FATLIB}" - exit 1 - fi -fi - -# Ensure that we are really linking to a fat binary and that it refers -# to libxml2 and libz -check_universal ${FATLIB} -LIBS=$(get_dependent_libraries ${FATLIB}) -check_library_paths ${FATLIB} "${LIBS}" -check_mandatory_library_linkage ${FATLIB} "${LIBS}" /usr/lib/libxml2.2.dylib -# check_mandatory_library_linkage ${FATLIB} "${LIBS}" /usr/lib/libz.1.dylib - -# Clean up the previous build directory -rm -rf build/ igraphcore/ - -# Set up ARCHFLAGS to ensure that we build a multi-arch Python extension -export ARCHFLAGS="-arch i386 -arch x86_64" - -# For each Python version, build the .mpkg and the .dmg -for PYVER in $PYTHON_VERSIONS; do - PYTHON=/usr/bin/python$PYVER - $PYTHON setup.py build_ext --no-download --no-wait --no-pkg-config -I ../igraph/include:../igraph/fatbuild/x86/include -L `dirname $FATLIB` || exit 3 - $PYTHON setup.py bdist_mpkg --no-download --no-wait --no-pkg-config || exit 4 - - # Ensure that the built library is really universal - LIB=build/lib.macosx-*-${PYVER}/igraph/_igraph.so - check_universal ${LIB} - DEPS=$(get_dependent_libraries ${LIB}) - check_library_paths ${LIB} "${DEPS}" - check_mandatory_library_linkage ${LIB} "${DEPS}" /usr/local/lib/libigraph.0.dylib - - for MACVER in 10.5 10.6 10.7 10.8 10.9; do - MPKG="dist/python_igraph-${VERSION_UNDERSCORE}-py${PYVER}-macosx${MACVER}.mpkg" - if [ -f $MPKG ]; then - break - fi - done - - DMG=dist/`basename $MPKG .mpkg`.dmg - rm -f ${DMG} - echo "hdiutil create -volname 'python-igraph ${VERSION}' -layout NONE -srcfolder $MPKG $DMG" - hdiutil create -volname "python-igraph ${VERSION}" -layout NONE -srcfolder $MPKG $DMG - rm -rf ${MPKG} -done - -cd $CWD diff --git a/scripts/rtd_prebuild.sh b/scripts/rtd_prebuild.sh new file mode 100755 index 000000000..461cf2cc4 --- /dev/null +++ b/scripts/rtd_prebuild.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +echo "Compile and install igraph into venv. This might take a few minutes..." +/home/docs/checkouts/readthedocs.org/user_builds/igraph/envs/${READTHEDOCS_VERSION}/bin/pip wheel -q -w dist . +/home/docs/checkouts/readthedocs.org/user_builds/igraph/envs/${READTHEDOCS_VERSION}/bin/pip install -q --force-reinstall dist/*.whl + +echo "Modularize pure Python modules" +/home/docs/checkouts/readthedocs.org/user_builds/igraph/envs/${READTHEDOCS_VERSION}/bin/python3 scripts/patch_modularized_graph_methods.py + +echo "NOTE: Patch pydoctor to trigger build-finished before RTD extension" +# see https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.connect +# see also https://github.com/readthedocs/readthedocs.org/pull/4054 - might or might not be exactly what we are seeing here +sed -i 's/on_build_finished)/on_build_finished, priority=490)/' /home/docs/checkouts/readthedocs.org/user_builds/igraph/envs/${READTHEDOCS_VERSION}/lib/python3.11/site-packages/pydoctor/sphinx_ext/build_apidocs.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) - diff --git a/setup.py b/setup.py index eba742649..c919d3507 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!usr/bin/env python import os import platform @@ -6,238 +6,95 @@ ########################################################################### -# Global version number. Keep the format of the next line intact. -VERSION = '0.7.1.post6' - # Check Python's version info and exit early if it is too old -if sys.version_info < (2, 5): - print("This module requires Python >= 2.5") +if sys.version_info < (3, 9): + print("This module requires Python >= 3.9") sys.exit(0) -# Check whether we are running inside tox -- if so, we will use a non-logging -# URL to download the C core of igraph to avoid inflating download counts -TESTING_IN_TOX = "TESTING_IN_TOX" in os.environ - -# Check whether we are running in a CI environment like Travis -- if so, -# we will download the master tarball of igraph when needed -TESTING_IN_CI = "CONTINUOUS_INTEGRATION" in os.environ - -# Check whether we are compiling for PyPy. Headers will not be installed -# for PyPy. -IS_PYPY = platform.python_implementation() == "PyPy" - ########################################################################### -## Here be ugly workarounds. These must be run before setuptools -## is imported. - -class Workaround(object): - """Base class for platform-specific workarounds and hacks that are - needed to get the Python interface compile with as little user - intervention as possible.""" - - def required(self): - """Returns ``True`` if the workaround is required on the platform - of the user and ``False`` otherwise.""" - raise NotImplementedError - - def hack(self): - """Installs the workaround. This method will get called if and only - if the ``required()`` method returns ``True``. - """ - pass - - def update_buildcfg(self, cfg): - """Allows the workaround to update the build configuration of the - igraph extension. This method will get called if and only if the - ``required()`` method returns ``True``. - """ - pass - - def _extend_compiler_customization(self, func): - """Helper function that extends ``distutils.sysconfig.customize_compiler`` - and ``setuptools.command.build_ext.customize_compiler`` with new, - user-defined code at the end.""" - from distutils import sysconfig - old_func = sysconfig.customize_compiler - def replaced(*args, **kwds): - old_func(*args, **kwds) - return func(*args, **kwds) - self._replace_compiler_customization_distutils(replaced) - self._replace_compiler_customization_setuptools(replaced) - - def _replace_compiler_customization_distutils(self, new_func): - from distutils import ccompiler, sysconfig - sysconfig.customize_compiler = new_func - ccompiler.customize_compiler = new_func - - def _replace_compiler_customization_setuptools(self, new_func): - if "setuptools.command.build_ext" in sys.modules: - sys.modules["setuptools.command.build_ext"].customize_compiler = new_func - - -class OSXClangAndSystemPythonWorkaround(Workaround): - """Removes ``-mno-fused-madd`` from the arguments used to compile - Python extensions if the user is running OS X.""" - - @staticmethod - def remove_compiler_args(compiler): - while "-mno-fused-madd" in compiler.compiler: - compiler.compiler.remove("-mno-fused-madd") - while "-mno-fused-madd" in compiler.compiler_so: - compiler.compiler_so.remove("-mno-fused-madd") - while "-mno-fused-madd" in compiler.linker_so: - compiler.linker_so.remove("-mno-fused-madd") - - def required(self): - return sys.platform.startswith("darwin") - - def hack(self): - self._extend_compiler_customization(self.remove_compiler_args) - - -class OSXAnacondaPythonIconvWorkaround(Workaround): - """Anaconda Python contains a file named libxml2.la which refers to - /usr/lib/libiconv.la -- but such a file does not exist in OS X. This - hack ensures that we link to libxml2 from OS X itself and not from - Anaconda Python (after all, this is what would have happened if the - C core of igraph was compiled independently).""" - - def required(self): - from distutils.spawn import find_executable - if not sys.platform.startswith("darwin"): - return False - if "Anaconda" not in sys.version: - return False - self.xml2_config_path = find_executable("xml2-config") - if not self.xml2_config_path: - return False - xml2_config_path_abs = os.path.abspath(self.xml2_config_path) - return xml2_config_path_abs.startswith(os.path.abspath(sys.prefix)) - - def hack(self): - path = os.environ["PATH"].split(os.pathsep) - dir_to_remove = os.path.dirname(self.xml2_config_path) - if dir_to_remove in path: - path.remove(dir_to_remove) - os.environ["PATH"] = os.pathsep.join(path) - - def update_buildcfg(self, cfg): - anaconda_libdir = os.path.join(sys.prefix, "lib") - cfg.extra_link_args.append("-Wl,-rpath,%s" % anaconda_libdir) - cfg.post_build_hooks.append(self.fix_install_name) - - def fix_install_name(self, cfg): - """Fixes the install name of the libxml2 library in _igraph.so - to ensure that it loads libxml2 from Anaconda Python.""" - for outputfile in cfg.get_outputs(): - lines, retcode = get_output(["otool", "-L", outputfile]) - if retcode: - raise OSError("otool -L %s failed with error code: %s" % (outputfile, retcode)) - - for line in lines.split("\n"): - if "libxml2" in line: - libname = line.strip().split(" ")[0] - subprocess.call(["install_name_tool", "-change", - libname, "@rpath/" + os.path.basename(libname), - outputfile]) - - -class ContinuousIntegrationSetup(Workaround): - """Prepares the build configuration for a CI environment like Travis.""" - - def required(self): - return TESTING_IN_CI - - def update_buildcfg(self, cfg): - cfg.c_core_url = "https://github.com/igraph/igraph/archive/master.tar.gz" - - -class WorkaroundSet(object): - def __init__(self, workaround_classes): - self.each = [cls() for cls in workaround_classes] - self.executed = [] - - def execute(self): - for workaround in self.each: - if workaround.required(): - workaround.hack() - self.executed.append(workaround) - - -workarounds = WorkaroundSet([ - OSXClangAndSystemPythonWorkaround, - OSXAnacondaPythonIconvWorkaround, - ContinuousIntegrationSetup -]) -workarounds.execute() -########################################################################### +from setuptools import find_packages, setup, Command, Extension try: - from setuptools import setup - from setuptools.command.build_ext import build_ext - build_py = None + from setuptools.command.bdist_wheel import bdist_wheel except ImportError: - from distutils.core import setup - try: - from distutils.command.build_py import build_py_2to3 as build_py - except ImportError: - from distutils.command.build_py import build_py + bdist_wheel = None -import atexit -import distutils.ccompiler import glob +import shlex import shutil import subprocess -import sys -import tarfile -import tempfile +import sysconfig +from contextlib import contextmanager +from pathlib import Path from select import select - -try: - from urllib import urlretrieve - from urllib2 import Request, urlopen, URLError -except: - # Maybe Python 3? - from urllib.request import Request, urlopen, urlretrieve - from urllib.error import URLError - -from distutils.core import Extension -from distutils.util import get_platform +from shutil import which +from time import sleep +from typing import List, Iterable, Iterator, Optional, Tuple, TypeVar, Union ########################################################################### -LIBIGRAPH_FALLBACK_INCLUDE_DIRS = ['/usr/include/igraph', '/usr/local/include/igraph'] -LIBIGRAPH_FALLBACK_LIBRARIES = ['igraph'] +LIBIGRAPH_FALLBACK_INCLUDE_DIRS = ["/usr/include/igraph", "/usr/local/include/igraph"] +LIBIGRAPH_FALLBACK_LIBRARIES = ["igraph"] LIBIGRAPH_FALLBACK_LIBRARY_DIRS = [] +# Check whether we are compiling for PyPy or wasm with emscripten. Headers will +# not be installed in these cases, or when the SKIP_HEADER_INSTALL envvar is +# set explicitly. +SKIP_HEADER_INSTALL = ( + platform.python_implementation() == "PyPy" + or (sysconfig.get_config_var("HOST_GNU_TYPE") or "").endswith("emscripten") + or "SKIP_HEADER_INSTALL" in os.environ +) + ########################################################################### -def cleanup_tmpdir(dirname): - """Removes the given temporary directory if it exists.""" - if dirname is not None and os.path.exists(dirname): - shutil.rmtree(dirname) -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) +T = TypeVar("T") + + +def is_envvar_on(name: str) -> bool: + """Returns whether the given environment variable is set to a truthy value + such as '1', 'on' or 'true'. + """ + value = os.environ.get(name, "") + return value and str(value).lower() in ("1", "on", "true") + + +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 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): +def building_with_emscripten() -> bool: + """Returns True when building with Emscripten to WebAssembly""" + return (sysconfig.get_config_var("HOST_GNU_TYPE") or "").endswith("emscripten") + + +def building_with_sanitizers() -> bool: + """Returns True when the IGRAPH_USE_SANITIZERS envvar is set, indicating that + we want to build the Python interface with AddressSanitizer and LeakSanitizer + enabled. Currently works on Linux only and the primary use-case is to be able + to test igraph with sanitizers in CI. + """ + return platform.system() == "Linux" and is_envvar_on("IGRAPH_USE_SANITIZERS") + + +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 fail(message: str, code: int = 1) -> None: + """Fails the build with the given error message and exit code.""" + print(message) + sys.exit(code) + + +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 @@ -245,8 +102,16 @@ def find_static_library(library_name, library_path): variants = ["lib{0}.a", "{0}.a", "{0}.lib", "lib{0}.lib"] if is_unix_like(): - extra_libdirs = ["/usr/local/lib64", "/usr/local/lib", - "/usr/lib64", "/usr/lib", "/lib64", "/lib"] + extra_libdirs = [ + "/opt/homebrew/lib", # for newer Homebrew installations on macOS + "/usr/local/lib64", + "/usr/local/lib", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib64", + "/usr/lib", + "/lib64", + "/lib", + ] else: extra_libdirs = [] @@ -260,29 +125,18 @@ def find_static_library(library_name, library_path): if os.path.isfile(full_path): return full_path -def find_temporary_directory(): - """Finds a suitable temporary directory where the installer can download the - C core of igraph if needed and returns its full path.""" - script_file = sys.modules[__name__].__file__ - if not script_file.endswith("setup.py"): - # We are probably running within an easy_install sandbox. Luckily this - # provides a temporary directory for us so we can use that - result = tempfile.gettempdir() - else: - # Use a temporary directory next to setup.py. We cannot blindly use - # the default (given by tempfile.tempdir) because it might be on a - # RAM disk that has not enough space - result = os.path.join(os.path.dirname(script_file), "tmp") - return os.path.abspath(result) -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) @@ -291,385 +145,590 @@ 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 http_url_exists(url): - """Returns whether the given HTTP URL 'exists' in the sense that it is returning - an HTTP error code or not. A URL is considered to exist if it does not return - an HTTP error code.""" - class HEADRequest(Request): - def get_method(self): - return "HEAD" - try: - response = urlopen(HEADRequest(url)) - return True - except URLError: - return False -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`` """ platform = platform or sys.platform platform = platform.lower() - return platform.startswith("linux") or platform.startswith("darwin") or \ - platform.startswith("cygwin") - -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 os.name == 'nt' 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 - all_msvc_dirs = glob.glob(os.path.join('..', '..', '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 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") - else: - print("Using MSVC build dir as a fallback: %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 ( + platform.startswith("linux") + or platform.startswith("darwin") + or platform.startswith("cygwin") + ) + + +def wait_for_keypress(seconds: float) -> None: + """Wait for a keypress or until the given number of seconds have passed, + whichever happens first. + """ + while seconds > 0: + if seconds > 1: + plural = "s" + else: + plural = "" + + sys.stdout.write( + "\rContinuing in %2d second%s; press Enter to continue " + "immediately. " % (seconds, plural) + ) + sys.stdout.flush() + + if platform.system() == "Windows": + from msvcrt import kbhit # type: ignore + + for _ in range(10): + if kbhit(): + seconds = 0 + break + sleep(0.1) + else: + rlist, _, _ = select([sys.stdin], [], [], 1) + if rlist: + sys.stdin.readline() + seconds = 0 + break -def version_variants(version): - """Given an igraph version number, returns a list of possible version - number variants to try when looking for a suitable nightly build of the - C core to download from igraph.org.""" + seconds -= 1 - result = [version] + sys.stdout.write("\r" + " " * 65 + "\r") - # Strip any release tags - version, _, _ = version.partition(".post") - result.append(version) - # Add trailing ".0" as needed to ensure that we have at least - # major.minor.patch - parts = version.split(".") - while len(parts) < 3: - parts.append("0") - result.append(".".join(parts)) +@contextmanager +def working_directory(dir: Union[str, Path]) -> Iterator[None]: + cwd = os.getcwd() + os.chdir(dir) + try: + yield + finally: + os.chdir(cwd) - return result ########################################################################### -class IgraphCCoreBuilder(object): + +class IgraphCCoreCMakeBuilder: """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 CMake as the + build tool. This is the case from igraph 0.9. + """ - def __init__(self, versions_to_try, remote_url=None, - show_progress_bar=True, tmproot=None): - self.versions_to_try = versions_to_try - self.remote_url = remote_url - self.show_progress_bar = show_progress_bar - self.tmproot = tmproot - self._tmpdir = None + 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. + + 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 + """ + 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( + "igraph uses CMake as the build system. You need to install CMake " + "before compiling igraph." + ) + return False - if self.tmproot is None: - self.tmproot = find_temporary_directory() + build_to_source_folder = os.path.relpath(source_folder, build_folder) - @property - def tmpdir(self): - """The temporary directory in which igraph is downloaded and extracted.""" - if self._tmpdir is None: - create_dir_unless_exists(self.tmproot) - self._tmpdir = tempfile.mkdtemp(prefix="igraph.", dir=self.tmproot) - atexit.register(cleanup_tmpdir, self._tmpdir) - return self._tmpdir - - def download_and_compile(self): - """Downloads and compiles the C core of igraph.""" - - def _progress_hook(count, block_size, total_size): - if total_size < 0: - sys.stdout.write("\rDownloading %s... please wait." % local_file) - else: - percentage = count * block_size * 100.0 / total_size - percentage = min(percentage, 100.0) - sys.stdout.write("\rDownloading %s... %.2f%%" % (local_file, percentage)) - sys.stdout.flush() - - # Determine the remote URL if needed - if self.remote_url is None: - self.version, remote_url = self.find_first_version() - if not self.version: - print("Version %s of the C core of igraph is not found among the " - "nightly builds." % self.versions_to_try[0]) - print("Use the --c-core-version switch to try a different version.") - print("") - return False - local_file = "igraph-%s.tar.gz" % self.version - else: - remote_url = self.remote_url - _, _, local_file = remote_url.rpartition("/") - - print("Using temporary directory: %s" % self.tmpdir) + print("Configuring build...") + args = [cmake] + cmake_build_mode = "Release" - # Now determine the full path where the C core will be downloaded - local_file_full_path = os.path.join(self.tmpdir, local_file) + # Build to wasm requires invocation of the Emscripten SDK + if building_with_emscripten(): + emcmake = which("emcmake") + if not emcmake: + print( + "You need to install emcmake from the Emscripten SDK before " + "compiling igraph." + ) + return False + args.insert(0, emcmake) + args.append("-DIGRAPH_WARNINGS_AS_ERRORS:BOOL=OFF") + args.append("-DIGRAPH_GRAPHML_SUPPORT:BOOL=OFF") + + # Build the Python interface with vendored libraries + for deps in "ARPACK BLAS GLPK GMP LAPACK PLFIT".split(): + args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") + + # Use link-time optinization if available + args.append("-DIGRAPH_ENABLE_LTO=AUTO") + + # -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") + + # No need to build tests + args.append("-DBUILD_TESTING=OFF") + + # Do not treat compilation warnings as errors in case someone is trying + # to "pip install" igraph in an environment for which we don't provide + # wheels and the compiler complains about harmless things + args.append("-DIGRAPH_WARNINGS_AS_ERRORS=OFF") + + # Set install directory during config step instead of install step in order + # to avoid having the architecture name in the LIBPATH (e.g. lib/x86_64-linux-gnu) + args.append("-DCMAKE_INSTALL_PREFIX=" + str(install_folder)) + + # On macOS, compile the C core with the same macOS deployment target as + # the one that was used to compile Python itself + if sysconfig.get_config_var("MACOSX_DEPLOYMENT_TARGET"): + args.append( + "-DCMAKE_OSX_DEPLOYMENT_TARGET=" + + sysconfig.get_config_var("MACOSX_DEPLOYMENT_TARGET") + ) + + # Compile the C core with sanitizers if needed + if building_with_sanitizers(): + args.append("-DUSE_SANITIZER=Address;Undefined") + args.append("-DFLEX_KEEP_LINE_NUMBERS=ON") + cmake_build_mode = "Debug" + + # 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"], + posix=not building_on_windows_msvc(), + ) + ) + + # Finally, add the source folder path + args.append(str(build_to_source_folder)) + + retcode = subprocess.call(args) + if retcode: + return False - # Download the C core - if self.show_progress_bar: - urlretrieve(remote_url, local_file_full_path, reporthook=_progress_hook) - print("") - else: - print("Downloading %s... " % local_file) - urlretrieve(remote_url, local_file_full_path) - - # Extract it in the temporary directory - print("Extracting %s..." % local_file) - if local_file.lower().endswith(".tar.gz"): - archive = tarfile.open(local_file_full_path, "r:gz") - elif local_file.lower().endswith(".tar.bz2"): - archive = tarfile.open(local_file_full_path, "r:bz2") - else: - print("Cannot extract unknown archive format: %s." % ext) - print("") + print("Running build...") + # We are _not_ using a parallel build; this is intentional, see igraph/igraph#1755 + retcode = subprocess.call([cmake, "--build", ".", "--config", cmake_build_mode]) + if retcode: return False - archive.extractall(self.tmpdir) - - # Determine the name of the build directory - self.builddir = None - for name in os.listdir(self.tmpdir): - full_path = os.path.join(self.tmpdir, name) - if name.startswith("igraph") and os.path.isdir(full_path): - self.builddir = full_path - break - if not self.builddir: - print("Downloaded tarball did not contain a directory whose name " - "started with igraph; giving up build.") + print("Installing build...") + retcode = subprocess.call( + [ + cmake, + "--install", + ".", + "--config", + cmake_build_mode, + ] + ) + if retcode: return False - # Try to compile - cwd = os.getcwd() - try: - os.chdir(self.builddir) - - # 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 on OS X 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) - os.rename("ltmain.sh.new", "ltmain.sh") - - print("Configuring igraph...") - retcode = subprocess.call("CFLAGS=-fPIC CXXFLAGS=-fPIC ./configure --disable-tls --disable-gmp", - shell=True) - if retcode: - return False + for candidate in install_folder.rglob("igraph.pc"): + return self._parse_pkgconfig_file(candidate) - print("Building igraph...") - retcode = subprocess.call("make", shell=True) - if retcode: - return False + raise RuntimeError( + "no igraph.pc was found in the installation folder of igraph" + ) - libraries = [] - for line in open(os.path.join(self.builddir, "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")) + 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") + ) + # Remap known library names in Requires and Requires.private with + # prior knowledge -- we don't want to rebuild pkg-config in Python + if line.startswith("Requires: ") or line.startswith( + "Requires.private: " + ): + for word in line.strip().split(): + if word.startswith("libxml-"): + libraries.append("xml2") if not libraries: # Educated guess libraries = ["igraph"] - finally: - os.chdir(cwd) - - # Compilation succeeded; copy everything into igraphcore - create_dir_unless_exists("igraphcore") - ensure_dir_does_not_exist("igraphcore", "include") - ensure_dir_does_not_exist("igraphcore", "lib") - shutil.copytree(os.path.join(self.builddir, "include"), - os.path.join("igraphcore", "include")) - shutil.copytree(os.path.join(self.builddir, "src", ".libs"), - os.path.join("igraphcore", "lib")) - f = open(os.path.join("igraphcore", "build.cfg"), "w") - f.write(repr(libraries)) - f.close() + return libraries - return True - def find_first_version(self): - """Finds the first version of igraph that exists in the nightly build - repo from the version numbers provided in ``self.versions_to_try``.""" - for version in self.versions_to_try: - remote_url = self.get_download_url(version=version) - if http_url_exists(remote_url): - return version, remote_url - return None, None - - def get_download_url(self, version): - if TESTING_IN_TOX: - # Make sure that tox unit tests are not counted as real - # igraph downloads - return "http://igraph.org/nightly/steal/c/igraph-%s.tar.gz" % version - else: - return "http://igraph.org/nightly/get/c/igraph-%s.tar.gz" % version - - def run(self): - return self.download_and_compile() +########################################################################### -class BuildConfiguration(object): +class BuildConfiguration: def __init__(self): - global VERSION - self.c_core_versions = version_variants(VERSION) - self.c_core_url = None self.include_dirs = [] self.library_dirs = [] self.runtime_library_dirs = [] self.libraries = [] self.extra_compile_args = [] self.extra_link_args = [] + self.define_macros = [] self.extra_objects = [] - self.show_progress_bar = True self.static_extension = False - self.download_igraph_if_needed = True - self.use_pkgconfig = True + self.use_pkgconfig = False + self.use_sanitizers = False + self.c_core_built = False + self.allow_educated_guess = True self._has_pkgconfig = None self.excluded_include_dirs = [] self.excluded_library_dirs = [] - self.pre_build_hooks = [] - self.post_build_hooks = [] - self.wait = True + 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"]) - self._has_pkgconfig = (exit_code == 0) + _, 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_ext(self): + def build_c_core(self) -> Command: + """Returns a class representing a custom setup.py command that builds + the C core of igraph. + + This is used in CI environments where we want to build the C core of + igraph once and then build the Python interface for various Python + versions without having to recompile the C core all the time. + + If is also used as a custom building block of `build_ext`. + """ + + buildcfg = self + + class build_c_core(Command): + description = "Compile the C core of igraph only" + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + buildcfg.c_core_built = buildcfg.compile_igraph_from_vendor_source() + + return build_c_core + + @property + def build_ext(self) -> Command: """Returns a class that can be used as a replacement for the - ``build_ext`` command in ``distutils`` and that will download and - compile the C core of igraph if needed.""" - try: - from setuptools.command.build_ext import build_ext - except ImportError: - from distutils.command.build_ext import build_ext - from distutils.sysconfig import get_python_inc + ``build_ext`` command in ``setuptools`` and that will compile the C core + of igraph before compiling the Python extension. + """ + from setuptools.command.build_ext import build_ext 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) + fail("You will need the Python headers to compile this extension.") - # Print a warning if pkg-config is not available or does not know about igraph + # 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: + fail( + "Cannot find the C core of igraph on this system using pkg-config." + ) else: - detected = False - - # Check whether we have already compiled igraph in a previous run. - # If so, it should be found in igraphcore/include and - # igraphcore/lib - if os.path.exists("igraphcore"): - buildcfg.use_built_igraph() - detected = True - - # Download and compile igraph if the user did not disable it and - # we do not know the libraries from pkg-config yet - if not detected: - if buildcfg.download_igraph_if_needed and is_unix_like(): - detected = buildcfg.download_and_compile_igraph() - if detected: - buildcfg.use_built_igraph() - else: - sys.exit(1) - - # Fall back to an educated guess if everything else failed - if not detected: - buildcfg.use_educated_guess() + # Build the C core from the vendored igraph source + self.run_command("build_c_core") + if not buildcfg.c_core_built: + # Fall back to an educated guess if everything else failed + if not detected: + if buildcfg.allow_educated_guess: + buildcfg.use_educated_guess() + else: + fail("Cannot build the C core of igraph.") + + # 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 + ) + + # 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 build configuration based on environment variables + if "IGRAPH_STATIC_EXTENSION" in os.environ: + buildcfg.static_extension = is_envvar_on("IGRAPH_STATIC_EXTENSION") + buildcfg.use_sanitizers = building_with_sanitizers() # 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 # -fPIC if buildcfg.static_extension: - buildcfg.replace_static_libraries(exclusions=["m"]) + if buildcfg.static_extension == "only_igraph": + buildcfg.replace_static_libraries(only=["igraph"]) + else: + buildcfg.replace_static_libraries(exclusions=["m"]) + + # Add sanitizer flags + if buildcfg.use_sanitizers: + buildcfg.extra_link_args += [ + "-fsanitize=address", + "-fsanitize=undefined", + ] + buildcfg.extra_compile_args += [ + "-g", + "-Og", + "-fno-omit-frame-pointer", + "-fdiagnostics-color", + ] + + # 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) + + # 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() - ext = first(extension for extension in self.extensions - if extension.name == "igraph._igraph") + # Find the igraph extension and configure it with the settings + # of this build configuration + ext = first( + extension + for extension in self.extensions + if extension.name == "igraph._igraph" + ) buildcfg.configure(ext) - # Run any pre-build hooks - for hook in buildcfg.pre_build_hooks: - hook(self) - # Run the original build_ext command build_ext.run(self) - # Run any post-build hooks - for hook in buildcfg.post_build_hooks: - hook(self) - return custom_build_ext - def configure(self, ext): + @property + def sdist(self): + """Returns a class that can be used as a replacement for the + ``sdist`` command in ``setuptools`` and that will clean up + ``vendor/source/igraph`` before running the original ``sdist`` + command. + """ + from setuptools.command.sdist import sdist + + def is_git_repo(folder) -> bool: + return (Path(folder) / ".git").exists() + + 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") + + class custom_sdist(sdist): + def run(self): + 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 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): + with working_directory(igraph_source_repo): + version = ( + subprocess.check_output("git describe", shell=True) + .decode("utf-8") + .strip() + ) + + # If we still don't have a version number, try to parse it from + # include/igraph_version.h + if not version: + 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 the Python interface of igraph" + ) + + with version_header.open("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] + + 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): + # 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 + cleanup_git_repo(igraph_source_repo) + + # Copy the generated parser sources from the build folder + parser_dir = igraph_build_dir / "src" / "io" / "parsers" + if parser_dir.is_dir(): + shutil.copytree( + parser_dir, igraph_source_repo / "src" / "io" / "parsers" + ) + else: + raise RuntimeError( + "You need to build the C core of igraph first before " + "generating a source tarball of the Python interface" + ) + + # Add a version file to the tarball + version_file.write_text(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 + + 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. + """ + 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 + + if (source_folder / "CMakeLists.txt").exists(): + igraph_builder = IgraphCCoreCMakeBuilder() + else: + print("Cannot find vendored igraph source in {0}".format(source_folder)) + print("") + return False + + print("We are going to build the C core of igraph.") + print(" Source folder: {0}".format(source_folder)) + print(" Build folder: {0}".format(build_folder)) + print(" Install folder: {0}".format(install_folder)) + print("") + + source_folder = source_folder.resolve() + build_folder = build_folder.resolve() + install_folder = install_folder.resolve() + + Path(build_folder).mkdir(parents=True, exist_ok=True) + + libraries = igraph_builder.compile_in( + source_folder=source_folder, + build_folder=build_folder, + install_folder=install_folder, + ) + + if libraries is False: + fail("Build failed for the C core of igraph.") + + assert not isinstance(libraries, bool) + + igraph_builder.create_build_config_file(install_folder, libraries) + + self.use_vendored_igraph() + return True + + 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) - ext.library_dirs = exclude_from_list(self.library_dirs, self.excluded_library_dirs) + ext.include_dirs = exclude_from_list( + self.include_dirs, self.excluded_include_dirs + ) + ext.library_dirs = exclude_from_list( + self.library_dirs, self.excluded_library_dirs + ) ext.runtime_library_dirs = self.runtime_library_dirs ext.libraries = self.libraries ext.extra_compile_args = self.extra_compile_args ext.extra_link_args = self.extra_link_args 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: - print("Cannot find the C core of igraph on this system using pkg-config.") return False cmd = ["pkg-config", "igraph", "--cflags", "--libs"] @@ -685,26 +744,11 @@ def detect_from_pkgconfig(self): self.include_dirs = [opt[2:] for opt in opts if opt.startswith("-I")] return True - def download_and_compile_igraph(self): - """Downloads and compiles the C core of igraph.""" - print("We will now try to download and compile the C core from scratch.") - print("Version number of the C core: %s" % self.c_core_versions[0]) - if len(self.c_core_versions) > 1: - print("We will also try: %s" % ", ".join(self.c_core_versions[1:])) - print("") - - igraph_builder = IgraphCCoreBuilder(self.c_core_versions, self.c_core_url, - show_progress_bar=self.show_progress_bar) - if not igraph_builder.run(): - print("Could not download and compile the C core of igraph.") - print("") - return False - else: - 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: + if self.static_extension == "only_igraph": + build_type = "dynamic extension with vendored igraph source" + elif self.static_extension: build_type = "static extension" else: build_type = "dynamic extension" @@ -723,7 +767,8 @@ def print_build_info(self): def process_args_from_command_line(self): """Preprocesses the command line options before they are passed to - setup.py and sets up the build configuration.""" + setup.py and sets up the build configuration. + """ # Yes, this is ugly, but we don't want to interfere with setup.py's own # option handling opts_to_remove = [] @@ -733,77 +778,107 @@ def process_args_from_command_line(self): if option == "--static": opts_to_remove.append(idx) self.static_extension = True - elif option == "--no-download": - opts_to_remove.append(idx) - self.download_igraph_if_needed = False elif option == "--no-pkg-config": opts_to_remove.append(idx) self.use_pkgconfig = False - elif option == "--no-progress-bar": - opts_to_remove.append(idx) - self.show_progress_bar = False elif option == "--no-wait": opts_to_remove.append(idx) self.wait = False - elif option.startswith("--c-core-version"): - opts_to_remove.append(idx) - if option == "--c-core-version": - value = sys.argv[idx+1] - opts_to_remove.append(idx+1) - else: - value = option.split("=", 1)[1] - self.c_core_versions = [value] - elif option.startswith("--c-core-url"): + elif option == "--use-pkg-config": opts_to_remove.append(idx) - if option == "--c-core-url": - value = sys.argv[idx+1] - opts_to_remove.append(idx+1) - else: - value = option.split("=", 1)[1] - self.c_core_url = value + self.use_pkgconfig = True for idx in reversed(opts_to_remove): - sys.argv[idx:(idx+1)] = [] + sys.argv[idx : (idx + 1)] = [] + + def process_environment_variables(self): + """Processes environment variables that serve as replacements for the + command line options. This is typically useful in CI environments where + it is easier to set up a few environment variables permanently than to + pass the same options to ``setup.py build`` and ``setup.py install`` + at the same time. + """ + + def process_envvar(name, attr, value): + name = "IGRAPH_" + name.upper() + if name in os.environ: + value = str(os.environ[name]).lower() + if value in ("on", "true", "yes"): + value = True + elif value in ("off", "false", "no"): + value = False + else: + try: + value = bool(int(value)) + except Exception: + return + + setattr(self, attr, value) + + process_envvar("static", "static_extension", True) + process_envvar("no_pkg_config", "use_pkgconfig", False) + process_envvar("no_wait", "wait", False) + process_envvar("use_pkg_config", "use_pkgconfig", True) + process_envvar("use_sanitizers", "use_sanitizers", False) - def replace_static_libraries(self, exclusions=None): + 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.""" - if "stdc++" not in self.libraries: - self.libraries.append("stdc++") - if exclusions is None: exclusions = [] for library_name in set(self.libraries) - set(exclusions): + if only is not None and library_name not in only: + continue + 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_built_igraph(self): - """Assumes that igraph is built already in ``igraphcore`` and sets up + 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.""" - buildcfg.include_dirs = [os.path.join("igraphcore", "include")] - buildcfg.library_dirs = [os.path.join("igraphcore", "lib")] - buildcfg.static_extension = True + building_on_windows = building_on_windows_msvc() + + 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) + ) - buildcfg_file = os.path.join("igraphcore", "build.cfg") - if os.path.exists(buildcfg_file): - buildcfg.libraries = eval(open(buildcfg_file).read()) + if not buildcfg.static_extension: + buildcfg.static_extension = "only_igraph" + if building_on_windows: + buildcfg.define_macros.append(("IGRAPH_STATIC", "1")) - def use_educated_guess(self): + buildcfg_file = vendor_dir / "build.cfg" + if buildcfg_file.exists(): + buildcfg.libraries = eval(buildcfg_file.open("r").read()) + + def use_educated_guess(self) -> None: """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 print("WARNING: we were not able to detect where igraph is installed on") print("your machine (if it is installed at all). We will use the fallback") - print("library and include pathss hardcoded in setup.py and hope that the") + print("library and include paths hardcoded in setup.py and hope that the") print("C core of igraph is installed there.") print("") print("If the compilation fails and you are sure that igraph is installed") @@ -814,24 +889,8 @@ def use_educated_guess(self): print("- LIBIGRAPH_FALLBACK_LIBRARY_DIRS") print("") - seconds_remaining = 10 if self.wait else 0 - while seconds_remaining > 0: - if seconds_remaining > 1: - plural = "s" - else: - plural = "" - - sys.stdout.write("\rContinuing in %2d second%s; press Enter to continue " - "immediately. " % (seconds_remaining, plural)) - sys.stdout.flush() - - rlist, _, _ = select([sys.stdin], [], [], 1) - if rlist: - sys.stdin.readline() - break - - seconds_remaining -= 1 - sys.stdout.write("\r" + " "*65 + "\r") + if self.wait: + wait_for_keypress(seconds=10) self.libraries = LIBIGRAPH_FALLBACK_LIBRARIES[:] if self.static_extension: @@ -839,101 +898,184 @@ def use_educated_guess(self): self.include_dirs = LIBIGRAPH_FALLBACK_INCLUDE_DIRS[:] self.library_dirs = LIBIGRAPH_FALLBACK_LIBRARY_DIRS[:] + +########################################################################### + +if bdist_wheel is not None: + + class bdist_wheel_abi3(bdist_wheel): + def get_tag(self): + python, abi, plat = super().get_tag() + if python.startswith("cp"): + # on CPython, our wheels are abi3 and compatible back to 3.9 + return "cp39", "abi3", plat + + return python, abi, plat + +else: + bdist_wheel_abi3 = None + +# We are going to build an abi3 wheel if we are at least on CPython 3.9. +should_build_abi3_wheel = ( + bdist_wheel_abi3 + and platform.python_implementation() == "CPython" + and sys.version_info >= (3, 9) +) + ########################################################################### +# Import version number from version.py so we only need to change it in +# one place when a new release is created +__version__: str = "" +exec(open("src/igraph/version.py").read()) + # Process command line options buildcfg = BuildConfiguration() +buildcfg.process_environment_variables() buildcfg.process_args_from_command_line() -for workaround in workarounds.executed: - workaround.update_buildcfg(buildcfg) # Define the extension -sources=glob.glob(os.path.join('src', '*.c')) -igraph_extension = Extension('igraph._igraph', sources) - # library_dirs=library_dirs, - # libraries=libraries, - # include_dirs=include_dirs, - # extra_objects=extra_objects, - # extra_link_args=extra_link_args +sources = glob.glob(os.path.join("src", "_igraph", "*.c")) +sources.append(os.path.join("src", "_igraph", "force_cpp_linker.cpp")) +macros = [] +if should_build_abi3_wheel: + macros.append(("Py_LIMITED_API", "0x03090000")) +igraph_extension = Extension( + "igraph._igraph", + sources=sources, + py_limited_api=should_build_abi3_wheel, + define_macros=macros, +) description = """Python interface to the igraph high performance graph library, primarily aimed at complex network research and analysis. Graph plotting functionality is provided by the Cairo library, so make sure you install the Python bindings of Cairo if you want to generate -publication-quality graph plots. - -See the `Cairo homepage `_ for details. - -From release 0.5, the C core of the igraph library is **not** included -in the Python distribution - you must compile and install the C core -separately. Windows installers already contain a compiled igraph DLL, -so they should work out of the box. Linux users should refer to the -`igraph homepage `_ for -compilation instructions (but check your distribution first, maybe -there are pre-compiled packages available). OS X users may -benefit from the disk images in the Python Package Index. - -Unofficial installers for 64-bit Windows machines and/or different Python -versions can also be found `here `_. -Many thanks to the maintainers of this page! +publication-quality graph plots. You can try either `pycairo +`_ or `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/igraphmodule_api.h'] if not IS_PYPY else [] - -options = dict( - name = 'python-igraph', - version = VERSION, - url = 'http://pypi.python.org/pypi/python-igraph', - - description = 'High performance graph data structures and algorithms', - long_description = description, - license = 'GNU General Public License (GPL)', - - author = 'Tamas Nepusz', - author_email = 'tamas@cs.rhul.ac.uk', - - ext_modules = [igraph_extension], - package_dir = {'igraph': 'igraph'}, - packages = ['igraph', 'igraph.test', 'igraph.app', 'igraph.drawing', - 'igraph.remote', 'igraph.vendor'], - scripts = ['scripts/igraph'], - test_suite = "igraph.test.suite", - - headers = headers, - - platforms = 'ALL', - keywords = ['graph', 'network', 'mathematics', 'math', 'graph theory', 'discrete mathematics'], - classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'Operating System :: OS Independent', - 'Programming Language :: C', - 'Programming Language :: Python', - 'Topic :: Scientific/Engineering', - 'Topic :: Scientific/Engineering :: Information Analysis', - 'Topic :: Scientific/Engineering :: Mathematics', - 'Topic :: Scientific/Engineering :: Physics', - 'Topic :: Scientific/Engineering :: Bio-Informatics', - 'Topic :: Software Development :: Libraries :: Python Modules' +headers = ["src/_igraph/igraphmodule_api.h"] if not SKIP_HEADER_INSTALL else [] + +cmdclass = { + "build_c_core": buildcfg.build_c_core, # used by CI + "build_ext": buildcfg.build_ext, + "sdist": buildcfg.sdist, +} + +if should_build_abi3_wheel: + cmdclass["bdist_wheel"] = bdist_wheel_abi3 + +options = { + "name": "igraph", + "version": __version__, + "url": "https://igraph.org/python", + "description": "High performance graph data structures and algorithms", + "long_description": description, + "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", + "Changelog": "https://github.com/igraph/python-igraph/blob/main/CHANGELOG.md", + "CI": "https://github.com/igraph/python-igraph/actions", + "Documentation": "https://python.igraph.org", + "Source Code": "https://github.com/igraph/python-igraph", + }, + "ext_modules": [igraph_extension], + "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": find_packages(where="src"), + "install_requires": ["texttable>=1.6.2"], + "entry_points": {"console_scripts": ["igraph=igraph.app.shell:main"]}, + "extras_require": { + # Dependencies needed for plotting with Cairo + "cairo": ["cairocffi>=1.2.0"], + # Dependencies needed for plotting with Matplotlib + "matplotlib": ["matplotlib>=3.6.0; platform_python_implementation != 'PyPy'"], + # Dependencies needed for plotting with Plotly + "plotly": ["plotly>=5.3.0"], + # Compatibility alias to 'cairo' for python-igraph <= 0.9.6 + "plotting": ["cairocffi>=1.2.0"], + # Dependencies needed for testing only + "test": [ + "cairocffi>=1.2.0", + "networkx>=2.5", + "pytest>=7.0.1", + "pytest-timeout>=2.1.0", + "numpy>=1.19.0; platform_python_implementation != 'PyPy'", + "pandas>=1.1.0; platform_python_implementation != 'PyPy'", + "scipy>=1.5.0; platform_python_implementation != 'PyPy'", + "matplotlib>=3.6.0; platform_python_implementation != 'PyPy'", + "plotly>=5.3.0", + "Pillow>=9; platform_python_implementation != 'PyPy'", + ], + # Dependencies needed for testing on Windows ARM64; only those that are either + # pure Python or have Windows ARM64 wheels as we don't want to compile wheels + # in CI + "test-win-arm64": [ + "cairocffi>=1.2.0", + "networkx>=2.5", + "pytest>=7.0.1", + "pytest-timeout>=2.1.0", + ], + # Dependencies needed for testing on musllinux; only those that are either + # pure Python or have musllinux wheels as we don't want to compile wheels + # in CI + "test-musl": [ + "cairocffi>=1.2.0", + "networkx>=2.5", + "pytest>=7.0.1", + "pytest-timeout>=2.1.0", + ], + # Dependencies needed for building the documentation + "doc": [ + "Sphinx>=7.0.0", + "sphinx-rtd-theme>=1.3.0", + "sphinx-gallery>=0.14.0", + "pydoctor>=23.4.0", + ], + }, + "python_requires": ">=3.9", + "headers": headers, + "platforms": "ALL", + "keywords": [ + "graph", + "network", + "mathematics", + "math", + "graph theory", + "discrete mathematics", ], - - cmdclass = { - "build_ext": buildcfg.build_ext - } -) - -if "macosx" in get_platform() and "bdist_mpkg" in sys.argv: - # OS X specific stuff to build the .mpkg installer - options["data_files"] = [ \ - ('/usr/local/lib', [os.path.join('..', 'igraph', 'fatbuild', 'libigraph.0.dylib')]) - ] - -if sys.version_info > (3, 0): - if build_py is None: - options["use_2to3"] = True - else: - options["cmdclass"]["build_py"] = build_py + "classifiers": [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Operating System :: OS Independent", + "Programming Language :: C", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Physics", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + "cmdclass": cmdclass, +} setup(**options) diff --git a/src/arpackobject.c b/src/_igraph/arpackobject.c similarity index 50% rename from src/arpackobject.c rename to src/_igraph/arpackobject.c index 96eefa340..69d8841ab 100644 --- a/src/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -1,114 +1,108 @@ /* vim:set ts=4 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2007-2012 Tamas Nepusz - + Copyright (C) 2007-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "arpackobject.h" -#include "graphobject.h" +#include "convert.h" #include "error.h" -#include "py2compat.h" +#include "graphobject.h" +#include "pyhelpers.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); - if (self) { - 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; } /** * \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);*/ - PyObject_Del((PyObject*)self); +static void igraphmodule_ARPACKOptions_dealloc(igraphmodule_ARPACKOptionsObject* self) { + RC_DEALLOC("ARPACKOptions", self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_ARPACKOptionsType); } /** \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; 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); + result=PyFloat_FromDouble(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); } @@ -118,20 +112,25 @@ 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) { + igraph_int_t igraph_int; + if (value == 0) { PyErr_SetString(PyExc_TypeError, "attribute can not be deleted"); return -1; } if (strcmp(attrname, "maxiter") == 0 || strcmp(attrname, "mxiter") == 0) { - if (PyInt_Check(value)) { - long int n=PyInt_AsLong(value); - if (n>0) - self->params.mxiter=(igraph_integer_t)n; - else { + if (PyLong_Check(value)) { + 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,10 +139,13 @@ 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)) { + 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); + self->params.tol = PyFloat_AsDouble(value); } else { PyErr_SetString(PyExc_ValueError, "integer or float expected"); return -1; @@ -173,66 +175,15 @@ igraph_arpack_options_t *igraphmodule_ARPACKOptions_get( /** \ingroup python_interface_arpack * \brief Formats an \c igraph.ARPACKOptions object in a * human-consumable format. - * + * * \return the formatted textual representation as a \c PyObject */ -PyObject* igraphmodule_ARPACKOptions_str( - igraphmodule_ARPACKOptionsObject *self) { - PyObject *s; - - s=PyString_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() -> list\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" @@ -258,24 +209,27 @@ 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 */ -}; - +); + +int igraphmodule_ARPACKOptions_register_type() { + PyType_Slot slots[] = { + { Py_tp_init, igraphmodule_ARPACKOptions_init }, + { 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/arpackobject.h b/src/_igraph/arpackobject.h similarity index 69% rename from src/arpackobject.h rename to src/_igraph/arpackobject.h index b9d663d30..92b685112 100644 --- a/src/arpackobject.h +++ b/src/_igraph/arpackobject.h @@ -1,29 +1,30 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_ARPACKOBJECT_H -#define PYTHON_ARPACKOBJECT_H +#ifndef IGRAPHMODULE_ARPACKOBJECT_H +#define IGRAPHMODULE_ARPACKOBJECT_H + +#include "preamble.h" -#include #include #include "graphobject.h" @@ -31,7 +32,6 @@ * \ingroup python_interface * \defgroup python_interface_arpack ARPACK parameters object */ -extern PyTypeObject igraphmodule_ARPACKOptionsType; /** * \ingroup python_interface_arpack @@ -43,13 +43,11 @@ 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(void); -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/attributes.c b/src/_igraph/attributes.c similarity index 51% rename from src/attributes.c rename to src/_igraph/attributes.c index aca566de6..1bc39a0eb 100644 --- a/src/attributes.c +++ b/src/_igraph/attributes.c @@ -1,52 +1,80 @@ /* vim:set ts=2 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include #include "attributes.h" #include "common.h" #include "convert.h" -#include "py2compat.h" +#include "platform.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 || (o != 0 && PyNumber_Check(o)); +} + +static INLINE int PyObject_allowed_in_string_attribute(PyObject* o) { + return o == Py_None || (o != 0 && PyBaseString_Check(o)); +} + +static INLINE void igraphmodule_i_attribute_struct_invalidate_vertex_name_index( + igraphmodule_i_attribute_struct *attrs +); + int igraphmodule_i_attribute_struct_init(igraphmodule_i_attribute_struct *attrs) { - int i; - for (i=0; i<3; i++) { + int i, j; + + for (i = 0; i < 3; i++) { attrs->attrs[i] = PyDict_New(); - if (PyErr_Occurred()) + if (PyErr_Occurred()) { + for (j = 0; j < i; j++) { + RC_DEALLOC("dict", attrs->attrs[j]); + Py_DECREF(attrs->attrs[j]); + attrs->attrs[j] = NULL; + } return 1; + } + RC_ALLOC("dict", attrs->attrs[i]); } - attrs->vertex_name_index = 0; + + attrs->vertex_name_index = NULL; + return 0; } void igraphmodule_i_attribute_struct_destroy(igraphmodule_i_attribute_struct *attrs) { int i; - for (i=0; i<3; i++) { + + for (i = 0; i < 3; i++) { if (attrs->attrs[i]) { RC_DEALLOC("dict", attrs->attrs[i]); Py_DECREF(attrs->attrs[i]); + attrs->attrs[i] = NULL; } } + if (attrs->vertex_name_index) { RC_DEALLOC("dict", attrs->vertex_name_index); Py_DECREF(attrs->vertex_name_index); @@ -57,29 +85,57 @@ int igraphmodule_i_attribute_struct_index_vertex_names( igraphmodule_i_attribute_struct *attrs, igraph_bool_t force) { Py_ssize_t n = 0; PyObject *name_list, *key, *value; + igraph_bool_t success = false; - if (attrs->vertex_name_index && !force) + if (attrs->vertex_name_index && !force) { return 0; + } if (attrs->vertex_name_index == 0) { attrs->vertex_name_index = PyDict_New(); if (attrs->vertex_name_index == 0) { - return 1; + goto cleanup; } - } else - PyDict_Clear(attrs->vertex_name_index); + } - name_list = PyDict_GetItemString(attrs->attrs[1], "name"); - if (name_list == 0) - return 0; /* no name attribute */ + PyDict_Clear(attrs->vertex_name_index); + + name_list = PyDict_GetItemString(attrs->attrs[ATTRHASH_IDX_VERTEX], "name"); + if (name_list == 0) { + success = true; + goto cleanup; + } 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 */ - if (value == 0) - return 1; - PyDict_SetItem(attrs->vertex_name_index, key, value); + key = PyList_GetItem(name_list, n); /* we don't own a reference to key */ + if (key == 0) { + goto cleanup; + } + + value = PyLong_FromLong(n); /* we do own a reference to value */ + if (value == 0) { + goto cleanup; + } + + if (PyDict_SetItem(attrs->vertex_name_index, key, value)) { + /* probably unhashable vertex name. If the error is a TypeError, convert + * it to a more readable error message */ + if (PyErr_Occurred() && PyErr_ExceptionMatches(PyExc_TypeError)) { + 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 + ); + } + + /* Drop reference to value because we still own it */ + Py_DECREF(value); + + goto cleanup; + } + /* PyDict_SetItem did an INCREF for both the key and a value, therefore we * have to drop our reference on value */ Py_DECREF(value); @@ -87,16 +143,24 @@ int igraphmodule_i_attribute_struct_index_vertex_names( n--; } - return 0; -} + success = true; -void igraphmodule_i_attribute_struct_invalidate_vertex_name_index( - igraphmodule_i_attribute_struct *attrs) { - if (attrs->vertex_name_index == 0) - return; +cleanup: + if (!success) { + igraphmodule_i_attribute_struct_invalidate_vertex_name_index(attrs); + } - Py_DECREF(attrs->vertex_name_index); - attrs->vertex_name_index = 0; + return success ? 0 : 1; +} + +static void igraphmodule_i_attribute_struct_invalidate_vertex_name_index( + igraphmodule_i_attribute_struct *attrs +) { + if (attrs->vertex_name_index) { + RC_DEALLOC("dict", attrs->vertex_name_index); + Py_DECREF(attrs->vertex_name_index); + attrs->vertex_name_index = NULL; + } } void igraphmodule_invalidate_vertex_name_index(igraph_t *graph) { @@ -107,10 +171,21 @@ void igraphmodule_index_vertex_names(igraph_t *graph, igraph_bool_t force) { igraphmodule_i_attribute_struct_index_vertex_names(ATTR_STRUCT(graph), force); } -int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_integer_t* vid) { +int igraphmodule_PyObject_matches_attribute_record(PyObject* object, igraph_attribute_record_t* record) { + if (record == 0) { + return 0; + } + + if (PyUnicode_Check(object)) { + return PyUnicode_IsEqualToASCIIString(object, record->name); + } + + return 0; +} + +int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_int_t* vid) { igraphmodule_i_attribute_struct* attrs = ATTR_STRUCT(graph); PyObject* o_vid = NULL; - int tmp; if (graph) { attrs = ATTR_STRUCT(graph); @@ -120,29 +195,18 @@ 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; } - - if (PyInt_AsInt(o_vid, &tmp)) + + if (igraphmodule_PyObject_to_integer_t(o_vid, vid)) { return 1; - - *vid = tmp; + } return 0; } @@ -193,15 +257,19 @@ igraph_bool_t igraphmodule_has_edge_attribute(const igraph_t *graph, const char* * attribute exists already (no exception set). The returned * reference is borrowed. */ -PyObject* igraphmodule_create_edge_attribute(const igraph_t* graph, +PyObject* igraphmodule_i_create_edge_attribute(const igraph_t* graph, const char* name) { PyObject *dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; PyObject *values; Py_ssize_t i, n; - if (dict == 0) { + if (dict == NULL) { dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE] = PyDict_New(); + if (dict == NULL) { + return NULL; + } } + if (PyDict_GetItemString(dict, name)) return 0; @@ -212,7 +280,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)) { @@ -260,220 +332,281 @@ PyObject* igraphmodule_get_edge_attribute_values(const igraph_t* graph, PyObject* igraphmodule_create_or_get_edge_attribute_values(const igraph_t* graph, const char* name) { PyObject *dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE], *result; - if (dict == 0) + if (dict == NULL) { return 0; + } + result = PyDict_GetItemString(dict, name); - if (result != 0) + if (result != NULL) { return result; - return igraphmodule_create_edge_attribute(graph, name); + } + + return igraphmodule_i_create_edge_attribute(graph, name); } /* Attribute handlers for the Python interface */ -/* Initialization */ -static int igraphmodule_i_attribute_init(igraph_t *graph, igraph_vector_ptr_t *attr) { +/* Initialization */ +static igraph_error_t igraphmodule_i_attribute_init( + igraph_t *graph, const igraph_attribute_record_list_t *attr +) { igraphmodule_i_attribute_struct* attrs; - long int i, n; - - attrs=(igraphmodule_i_attribute_struct*)calloc(1, sizeof(igraphmodule_i_attribute_struct)); - if (!attrs) + igraph_int_t i, n; + + 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); + } + IGRAPH_FINALLY(free, attrs); + 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; + IGRAPH_FINALLY(igraphmodule_i_attribute_struct_destroy, attrs); /* See if we have graph attributes */ if (attr) { - PyObject *dict=attrs->attrs[0], *value; - char *s; - n = igraph_vector_ptr_size(attr); - for (i=0; iattrs[0], *value; + const char *s; + + n = igraph_attribute_record_list_size(attr); + for (i = 0; i < n; i++) { + igraph_attribute_record_t *attr_rec = igraph_attribute_record_list_get_ptr(attr, i); + switch (attr_rec->type) { case IGRAPH_ATTRIBUTE_NUMERIC: - value=PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[0]); + value = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[0]); + if (!value) { + PyErr_PrintEx(0); + } break; case IGRAPH_ATTRIBUTE_STRING: - igraph_strvector_get((igraph_strvector_t*)attr_rec->value, 0, &s); - if (s == 0) - value=PyString_FromString(""); - else - value=PyString_FromString(s); + s = igraph_strvector_get(attr_rec->value.as_strvector, 0); + value = PyUnicode_FromString(s ? s : ""); + if (!value) { + PyErr_PrintEx(0); + } break; case IGRAPH_ATTRIBUTE_BOOLEAN: - value=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[0] ? Py_True : Py_False; + value = VECTOR(*attr_rec->value.as_vector_bool)[0] ? Py_True : Py_False; Py_INCREF(value); break; default: IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); - value=0; + value = NULL; break; } + if (value) { if (PyDict_SetItemString(dict, attr_rec->name, value)) { Py_DECREF(value); - igraphmodule_i_attribute_struct_destroy(attrs); - free(graph->attr); graph->attr = 0; - IGRAPH_ERROR("failed to add attributes to graph attribute hash", - IGRAPH_FAILURE); + value = NULL; /* set value to NULL to indicate an error */ + } else { + Py_DECREF(value); } - Py_DECREF(value); - value=0; + } + + if (!value) { + /* there was an error above, bail out */ + IGRAPH_ERROR("failed to add attributes to graph attribute hash", IGRAPH_FAILURE); } } } + graph->attr = (void*)attrs; + IGRAPH_FINALLY_CLEAN(2); + return IGRAPH_SUCCESS; } /* Destruction */ static void igraphmodule_i_attribute_destroy(igraph_t *graph) { igraphmodule_i_attribute_struct* attrs; - + /* printf("Destroying attribute table\n"); */ if (graph->attr) { - attrs=(igraphmodule_i_attribute_struct*)graph->attr; + attrs = (igraphmodule_i_attribute_struct*)graph->attr; + graph->attr = NULL; igraphmodule_i_attribute_struct_destroy(attrs); free(attrs); } } /* Copying */ -static int igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t *from, +static igraph_error_t igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t *from, igraph_bool_t ga, igraph_bool_t va, igraph_bool_t ea) { 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, list_len; + if (from->attr) { - fromattrs=ATTR_STRUCT(from); - /* what to do with the original value of toattrs? */ - toattrs=(igraphmodule_i_attribute_struct*)calloc(1, sizeof(igraphmodule_i_attribute_struct)); - if (!toattrs) + fromattrs = ATTR_STRUCT(from); + + toattrs = (igraphmodule_i_attribute_struct*) calloc(1, sizeof(igraphmodule_i_attribute_struct)); + if (!toattrs) { IGRAPH_ERROR("not enough memory to allocate attribute hashes", IGRAPH_ENOMEM); + } + IGRAPH_FINALLY(free, toattrs); + if (igraphmodule_i_attribute_struct_init(toattrs)) { PyErr_PrintEx(0); - free(toattrs); IGRAPH_ERROR("not enough memory to allocate attribute hashes", IGRAPH_ENOMEM); } - to->attr=toattrs; + IGRAPH_FINALLY(igraphmodule_i_attribute_struct_destroy, toattrs); - for (i=0; i<3; i++) { - if (!copy_attrs[i]) + for (i = 0; i < 3; i++) { + if (!copy_attrs[i]) { continue; + } if (!PyDict_Check(fromattrs->attrs[i])) { - toattrs->attrs[i]=fromattrs->attrs[i]; - Py_XINCREF(fromattrs->attrs[i]); - continue; + IGRAPH_ERRORF("expected dict in attribute hash at index %d", IGRAPH_EINVAL, i); } - - pos = 0; - 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; jattrs[i]); /* we already had a pre-constructed dict there */ + toattrs->attrs[i] = PyDict_Copy(fromattrs->attrs[i]); + if (!toattrs->attrs[i]) { + PyErr_PrintEx(0); + IGRAPH_ERROR("cannot copy attribute hashes", IGRAPH_FAILURE); + } + } else { + /* vertex and edge attributes have to be copied in a way that values + * are also copied */ + pos = 0; + while (PyDict_Next(fromattrs->attrs[i], &pos, &key, &value)) { + /* value is only borrowed, so copy it */ + if (!PyList_Check(value)) { + IGRAPH_ERRORF("expected list in attribute hash at index %d", IGRAPH_EINVAL, i); + } + + list_len = PyList_Size(value); + newval = PyList_New(list_len); + for (j = 0; j < list_len; j++) { + o = PyList_GetItem(value, j); Py_INCREF(o); PyList_SetItem(newval, j, o); } - } else { - newval=value; - Py_INCREF(newval); + + if (!newval) { + PyErr_PrintEx(0); + IGRAPH_ERROR("cannot copy attribute hashes", IGRAPH_FAILURE); + } + + if (PyDict_SetItem(toattrs->attrs[i], key, newval)) { + PyErr_PrintEx(0); + Py_DECREF(newval); + IGRAPH_ERROR("cannot copy attribute hashes", IGRAPH_FAILURE); + } + + Py_DECREF(newval); /* compensate for PyDict_SetItem */ } - PyDict_SetItem(toattrs->attrs[i], key, newval); - Py_DECREF(newval); /* compensate for PyDict_SetItem */ } } + + to->attr = toattrs; + IGRAPH_FINALLY_CLEAN(2); } + return IGRAPH_SUCCESS; } /* Adding vertices */ -static int igraphmodule_i_attribute_add_vertices(igraph_t *graph, long int nv, igraph_vector_ptr_t *attr) { +static igraph_error_t igraphmodule_i_attribute_add_vertices( + igraph_t *graph, igraph_int_t nv, const igraph_attribute_record_list_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_int_t i, j, k, num_attr_entries; igraph_attribute_record_t *attr_rec; - igraph_bool_t *added_attrs=0; + igraph_vector_bool_t added_attrs; Py_ssize_t pos = 0; - if (!graph->attr) return IGRAPH_SUCCESS; - if (nv<0) return IGRAPH_SUCCESS; + if (!graph->attr) { + 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) - IGRAPH_ERROR("can't add vertex attributes", IGRAPH_ENOMEM); - IGRAPH_FINALLY(free, added_attrs); + if (nv < 0) { + return IGRAPH_SUCCESS; } - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; - if (!PyDict_Check(dict)) + num_attr_entries = attr ? igraph_attribute_record_list_size(attr) : 0; + IGRAPH_VECTOR_BOOL_INIT_FINALLY(&added_attrs, num_attr_entries); + + 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 (!PyString_Check(key)) - IGRAPH_ERROR("vertex attribute hash key is not a string", IGRAPH_EINVAL); - 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; - if (attr) { - j=igraph_vector_ptr_size(attr); - for (i=0; iname)) { - added_attrs[i]=1; - break; - } - attr_rec=0; + attr_rec = NULL; + for (i = 0; i < num_attr_entries; i++) { + attr_rec = igraph_attribute_record_list_get_ptr(attr, i); + if (igraphmodule_PyObject_matches_attribute_record(key, attr_rec)) { + VECTOR(added_attrs)[i] = 1; + break; } + attr_rec = NULL; } + /* If we have specific values for the given attribute, attr_rec contains * the appropriate vector. If not, it is null. */ if (attr_rec) { - 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(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyString_FromString(s); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); + 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(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); - o=0; + o = Py_None; + Py_INCREF(o); break; } + if (o) { - if (PyList_Append(value, o) == -1) - IGRAPH_ERROR("can't extend a vertex attribute hash member", IGRAPH_FAILURE); - else Py_DECREF(o); + if (PyList_Append(value, o)) { + Py_DECREF(o); /* append failed */ + o = NULL; /* indicate error */ + } else { + Py_DECREF(o); /* drop reference, the list has it now */ + } + } + + if (!o) { + PyErr_PrintEx(0); + IGRAPH_ERROR("can't extend a vertex attribute hash member", IGRAPH_FAILURE); } } /* 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(*attr_rec->value.as_vector)[i]); + break; + case IGRAPH_ATTRIBUTE_STRING: + s = igraph_strvector_get(attr_rec->value.as_strvector, i); + o = PyUnicode_FromString(s); + break; + case IGRAPH_ATTRIBUTE_BOOLEAN: + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; + Py_INCREF(o); + break; + default: + IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); + o = Py_None; + Py_INCREF(o); + break; } - for (i=0; itype) { - case IGRAPH_ATTRIBUTE_NUMERIC: - 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=PyString_FromString(s); - break; - case IGRAPH_ATTRIBUTE_BOOLEAN: - 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; - break; + if (o) { + if (PyList_SetItem(value, i + j, o)) { + Py_DECREF(o); /* append failed */ + o = NULL; /* indicate error */ + } else { + /* reference stolen by the list */ } - if (o) PyList_SET_ITEM(value, i+j, o); } - /* Invalidate the vertex name index if needed */ - if (!strcmp(attr_rec->name, "name")) - igraphmodule_i_attribute_struct_invalidate_vertex_name_index(ATTR_STRUCT(graph)); + if (!o) { + PyErr_PrintEx(0); + IGRAPH_ERROR("can't extend a vertex attribute hash member", IGRAPH_FAILURE); + } + } - PyDict_SetItemString(dict, attr_rec->name, value); - Py_DECREF(value); /* compensate for PyDict_SetItemString */ + /* Invalidate the vertex name index if needed */ + if (!strcmp(attr_rec->name, "name")) { + igraphmodule_i_attribute_struct_invalidate_vertex_name_index(ATTR_STRUCT(graph)); } - free(added_attrs); - IGRAPH_FINALLY_CLEAN(1); + + PyDict_SetItemString(dict, attr_rec->name, value); + Py_DECREF(value); /* compensate for PyDict_SetItemString */ } + igraph_vector_bool_destroy(&added_attrs); + IGRAPH_FINALLY_CLEAN(1); + return IGRAPH_SUCCESS; } /* Permuting vertices */ -static int igraphmodule_i_attribute_permute_vertices(const igraph_t *graph, - igraph_t *newgraph, const igraph_vector_t *idx) { - long int n, i; +static igraph_error_t igraphmodule_i_attribute_permute_vertices(const igraph_t *graph, + igraph_t *newgraph, const igraph_vector_int_t *idx) { + igraph_int_t i, n; PyObject *key, *value, *dict, *newdict, *newlist, *o; - Py_ssize_t pos=0; - - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; - if (!PyDict_Check(dict)) return 1; + Py_ssize_t pos = 0; - newdict=PyDict_New(); - if (!newdict) return 1; + dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; + if (!PyDict_Check(dict)) { + IGRAPH_ERROR("vertex attribute hash type mismatch", IGRAPH_EINVAL); + } - n=igraph_vector_size(idx); - pos=0; + newdict = PyDict_New(); + if (!newdict) { + IGRAPH_ERROR("cannot allocate new dict for vertex permutation", IGRAPH_ENOMEM); + } + + 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 (attr) { - added_attrs = (igraph_bool_t*)calloc((size_t)igraph_vector_ptr_size(attr), - sizeof(igraph_bool_t)); - if (!added_attrs) - IGRAPH_ERROR("can't add vertex attributes", IGRAPH_ENOMEM); - IGRAPH_FINALLY(free, added_attrs); + if (!graph->attr) { + return IGRAPH_SUCCESS; + } + + ne = igraph_vector_int_size(edges) / 2; + if (ne < 0) { + return IGRAPH_SUCCESS; } - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; - if (!PyDict_Check(dict)) + num_attr_entries = attr ? igraph_attribute_record_list_size(attr) : 0; + IGRAPH_VECTOR_BOOL_INIT_FINALLY(&added_attrs, num_attr_entries); + + 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 (!PyString_Check(key)) - IGRAPH_ERROR("edge attribute hash key is not a string", IGRAPH_EINVAL); - 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; - if (attr) { - j=igraph_vector_ptr_size(attr); - for (i=0; iname)) { - added_attrs[i]=1; - break; - } - attr_rec=0; + attr_rec = NULL; + for (i = 0; i < num_attr_entries; i++) { + attr_rec = igraph_attribute_record_list_get_ptr(attr, i); + if (igraphmodule_PyObject_matches_attribute_record(key, attr_rec)) { + VECTOR(added_attrs)[i] = 1; + break; } + attr_rec = NULL; } + /* If we have specific values for the given attribute, attr_rec contains * the appropriate vector. If not, it is null. */ if (attr_rec) { - 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(*attr_rec->value.as_vector)[i]); break; case IGRAPH_ATTRIBUTE_STRING: - igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyString_FromString(s); + s = igraph_strvector_get(attr_rec->value.as_strvector, i); + 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(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); - o=0; + o = Py_None; + Py_INCREF(o); break; } + if (o) { - if (PyList_Append(value, o) == -1) - IGRAPH_ERROR("can't extend an edge attribute hash member", IGRAPH_FAILURE); - else Py_DECREF(o); + if (PyList_Append(value, o)) { + Py_DECREF(o); /* append failed */ + o = NULL; /* indicate error */ + } else { + Py_DECREF(o); /* drop reference, the list has it now */ + } + } + + if (!o) { + PyErr_PrintEx(0); + IGRAPH_ERROR("can't extend an edge attribute hash member", IGRAPH_FAILURE); } } } else { - for (i=0; itype) { - case IGRAPH_ATTRIBUTE_NUMERIC: - 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=PyString_FromString(s); - break; - case IGRAPH_ATTRIBUTE_BOOLEAN: - 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; - break; - } - if (o) PyList_SET_ITEM(value, i+j, o); - } + value = PyList_New(j + ne); + if (!value) { + IGRAPH_ERROR("can't add attributes", IGRAPH_ENOMEM); + } - PyDict_SetItemString(dict, attr_rec->name, value); - Py_DECREF(value); /* compensate for PyDict_SetItemString */ + for (i = 0; i < j; i++) { + Py_INCREF(Py_None); + PyList_SetItem(value, i, Py_None); } - free(added_attrs); - IGRAPH_FINALLY_CLEAN(1); - } - return IGRAPH_SUCCESS; -} + for (i = 0; i < ne; i++) { + const char *s; + PyObject *o; + switch (attr_rec->type) { + case IGRAPH_ATTRIBUTE_NUMERIC: + o = PyFloat_FromDouble((double)VECTOR(*attr_rec->value.as_vector)[i]); + break; + case IGRAPH_ATTRIBUTE_STRING: + s = igraph_strvector_get(attr_rec->value.as_strvector, i); + o = PyUnicode_FromString(s); + break; + case IGRAPH_ATTRIBUTE_BOOLEAN: + o = VECTOR(*attr_rec->value.as_vector_bool)[i] ? Py_True : Py_False; + Py_INCREF(o); + break; + default: + IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); + o = Py_None; + Py_INCREF(o); + break; + } -/* 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; iname, value); + Py_DECREF(value); /* compensate for PyDict_SetItemString */ } - - pos=0; - while (PyDict_Next(dict, &pos, &key, &value)) { - n=PySequence_Size(value); - if (PySequence_DelSlice(value, n-ndeleted, n) == -1) return; - } - - return; + + igraph_vector_bool_destroy(&added_attrs); + IGRAPH_FINALLY_CLEAN(1); + + return IGRAPH_SUCCESS; } -*/ /* Permuting edges */ -static int igraphmodule_i_attribute_permute_edges(const igraph_t *graph, - igraph_t *newgraph, const igraph_vector_t *idx) { - long int n, i; +static igraph_error_t igraphmodule_i_attribute_permute_edges(const igraph_t *graph, + igraph_t *newgraph, const igraph_vector_int_t *idx) { + igraph_int_t i, n; PyObject *key, *value, *dict, *newdict, *newlist, *o; Py_ssize_t pos=0; - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; - if (!PyDict_Check(dict)) return 1; + dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; + if (!PyDict_Check(dict)) { + IGRAPH_ERROR("edge attribute hash type mismatch", IGRAPH_EINVAL); + } - newdict=PyDict_New(); - if (!newdict) return 1; + newdict = PyDict_New(); + if (!newdict) { + IGRAPH_ERROR("cannot allocate new dict for edge permutation", IGRAPH_ENOMEM); + } - n=igraph_vector_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; i 0 ? PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[0]) : Py_None; + if (item == 0) { + Py_DECREF(res); + return 0; + } - item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[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; @@ -966,8 +1163,8 @@ static PyObject* igraphmodule_i_ac_first(PyObject* values, * merged vertices/edges. */ static PyObject* igraphmodule_i_ac_random(PyObject* values, - const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + const igraph_vector_int_list_t *merges) { + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item, *num; PyObject *random_module = PyImport_ImportModule("random"); PyObject *random_func; @@ -983,23 +1180,38 @@ 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_list_get_ptr(merges, i); + igraph_int_t n = igraph_vector_int_size(v); - if (n == 0) - continue; + if (n > 0) { + num = PyObject_CallObject(random_func, 0); + if (num == 0) { + Py_DECREF(random_func); + Py_DECREF(res); + return 0; + } - num = PyObject_CallObject(random_func, 0); - if (num == 0) { + item = PyList_GetItem( + values, VECTOR(*v)[(igraph_int_t)(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); + if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ + Py_DECREF(item); 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_INCREF(item); - Py_DECREF(num); - PyList_SET_ITEM(res, i, item); /* reference to item stolen */ } Py_DECREF(random_func); @@ -1014,21 +1226,28 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, * vertices/edges. */ static PyObject* igraphmodule_i_ac_last(PyObject* values, - const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + const igraph_vector_int_list_t *merges) { + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; 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_list_get_ptr(merges, i); + igraph_int_t n = igraph_vector_int_size(v); - if (n == 0) - continue; + item = (n > 0) ? PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[n-1]) : Py_None; + if (item == 0) { + Py_DECREF(res); + return 0; + } - item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[n-1]); 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; @@ -1041,18 +1260,23 @@ static PyObject* igraphmodule_i_ac_last(PyObject* values, * the merged vertices/edges. */ static PyObject* igraphmodule_i_ac_mean(PyObject* values, - const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + const igraph_vector_int_list_t *merges) { + igraph_int_t i, len = igraph_vector_int_list_size(merges); PyObject *res, *item; 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_list_get_ptr(merges, i); igraph_real_t num = 0.0, mean = 0.0; - long int j, n = igraph_vector_size(v); + igraph_int_t j, n = igraph_vector_int_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); @@ -1064,7 +1288,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; @@ -1077,46 +1306,88 @@ static PyObject* igraphmodule_i_ac_mean(PyObject* values, * the merged vertices/edges. */ static PyObject* igraphmodule_i_ac_median(PyObject* values, - const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + const igraph_vector_int_list_t *merges) { + igraph_int_t i, len = igraph_vector_int_list_size(merges); 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); + igraph_vector_int_t *v = igraph_vector_int_list_get_ptr(merges, i); + igraph_int_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]); + 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 % 2 == 1) { - item = PyList_GET_ITEM(list, n / 2); + + if (n == 0) { + item = Py_None; + Py_INCREF(item); + } else if (n % 2 == 1) { + 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; @@ -1136,35 +1407,39 @@ static void igraphmodule_i_free_attribute_combination_records( * igraphmodule_i_attribute_combine_vertices and * igraphmodule_i_attribute_combine_edges */ -static int igraphmodule_i_attribute_combine_dicts(PyObject *dict, - PyObject *newdict, const igraph_vector_ptr_t *merges, +static igraph_error_t igraphmodule_i_attribute_combine_dicts(PyObject *dict, + PyObject *newdict, const igraph_vector_int_list_t *merges, const igraph_attribute_combination_t *comb) { PyObject *key, *value; Py_ssize_t pos; igraph_attribute_combination_record_t* todo; Py_ssize_t i, n; - if (!PyDict_Check(dict) || !PyDict_Check(newdict)) return 1; + + if (!PyDict_Check(dict) || !PyDict_Check(newdict)) { + IGRAPH_ERROR("dict or newdict are corrupted", IGRAPH_FAILURE); + } /* Allocate memory for the attribute_combination_records */ n = PyDict_Size(dict); todo = (igraph_attribute_combination_record_t*)calloc( - n+1, sizeof(igraph_attribute_combination_record_t) + n + 1, sizeof(igraph_attribute_combination_record_t) ); if (todo == 0) { IGRAPH_ERROR("cannot allocate memory for attribute combination", IGRAPH_ENOMEM); } - for (i = 0; i < n+1; i++) + for (i = 0; i < n + 1; i++) { todo[i].name = 0; /* sentinel elements */ + } IGRAPH_FINALLY(igraphmodule_i_free_attribute_combination_records, todo); /* 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); - if (todo[i].name == 0) - IGRAPH_ERROR("PyString_CopyAsString failed", IGRAPH_FAILURE); - igraph_attribute_combination_query(comb, todo[i].name, - &todo[i].type, &todo[i].func); + todo[i].name = PyUnicode_CopyAsString(key); + if (todo[i].name == 0) { + IGRAPH_ERROR("PyUnicode_CopyAsString failed", IGRAPH_FAILURE); + } + IGRAPH_CHECK(igraph_attribute_combination_query(comb, todo[i].name, &todo[i].type, &todo[i].func)); i++; } @@ -1177,7 +1452,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); @@ -1231,7 +1506,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); @@ -1264,19 +1539,19 @@ static int igraphmodule_i_attribute_combine_dicts(PyObject *dict, igraphmodule_i_free_attribute_combination_records(todo); IGRAPH_FINALLY_CLEAN(1); - return 0; + return IGRAPH_SUCCESS; } /* Combining vertices */ -static int igraphmodule_i_attribute_combine_vertices(const igraph_t *graph, - igraph_t *newgraph, const igraph_vector_ptr_t *merges, +static igraph_error_t igraphmodule_i_attribute_combine_vertices(const igraph_t *graph, + igraph_t *newgraph, const igraph_vector_int_list_t *merges, const igraph_attribute_combination_t *comb) { PyObject *dict, *newdict; - int result; + igraph_error_t 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, @@ -1289,101 +1564,109 @@ static int igraphmodule_i_attribute_combine_vertices(const igraph_t *graph, } /* Combining edges */ -static int igraphmodule_i_attribute_combine_edges(const igraph_t *graph, - igraph_t *newgraph, const igraph_vector_ptr_t *merges, +static igraph_error_t igraphmodule_i_attribute_combine_edges(const igraph_t *graph, + igraph_t *newgraph, const igraph_vector_int_list_t *merges, const igraph_attribute_combination_t *comb) { PyObject *dict, *newdict; /* Get the attribute dicts */ - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; - newdict=ATTR_STRUCT_DICT(newgraph)[ATTRHASH_IDX_EDGE]; + dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; + newdict = ATTR_STRUCT_DICT(newgraph)[ATTRHASH_IDX_EDGE]; return igraphmodule_i_attribute_combine_dicts(dict, newdict, merges, comb); } /* 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) { +static igraph_error_t igraphmodule_i_attribute_get_info(const igraph_t *graph, + igraph_strvector_t *gnames, + igraph_vector_int_t *gtypes, + igraph_strvector_t *vnames, + igraph_vector_int_t *vtypes, + igraph_strvector_t *enames, + igraph_vector_int_t *etypes) { igraph_strvector_t *names[3] = { gnames, vnames, enames }; - igraph_vector_t *types[3] = { gtypes, vtypes, etypes }; - int retval; - long int i, j, k, l, m; - - for (i=0; i<3; i++) { + igraph_vector_int_t *types[3] = { gtypes, vtypes, etypes }; + int i, retval; + Py_ssize_t 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; - PyObject *o=0; - keys=PyDict_Keys(dict); - if (!keys) IGRAPH_ERROR("Internal error in PyDict_Keys", IGRAPH_FAILURE); - + + keys = PyDict_Keys(dict); + if (!keys) { + IGRAPH_ERROR("Internal error in PyDict_Keys", IGRAPH_FAILURE); + } + if (n) { - retval = igraphmodule_PyList_to_strvector_t(keys, n); - if (retval) - return retval; + retval = igraphmodule_PyList_to_existing_strvector_t(keys, n); + if (retval) { + IGRAPH_ERROR("Cannot convert Python list to existing igraph_strvector_t", IGRAPH_FAILURE); + } } + if (t) { - k=PyList_Size(keys); - igraph_vector_resize(t, k); - for (j=0; j0) { - - for (i=0; i 0) { + PyObject *item; + for (i = 0; i < j && is_numeric; i++) { + item = PyList_GetItem(o, i); + if (!PyObject_allowed_in_numeric_attribute(item)) { + is_numeric = 0; + } } - for (i=0; iob_type) : 0; - if (type_str != 0) { - PyErr_Format(PyExc_TypeError, "igraph supports string attribute names only, got %s", - PyString_AS_STRING(type_str)); - Py_DECREF(type_str); + type_obj = Py_TYPE(obj); + 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"); } return 0; } - diff --git a/src/attributes.h b/src/_igraph/attributes.h similarity index 77% rename from src/attributes.h rename to src/_igraph/attributes.h index 5e68f2a2b..d02d55239 100644 --- a/src/attributes.h +++ b/src/_igraph/attributes.h @@ -1,21 +1,21 @@ /* vim:set ts=2 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @@ -23,7 +23,8 @@ #ifndef PY_IGRAPH_ATTRIBUTES_H #define PY_IGRAPH_ATTRIBUTES_H -#include +#include "preamble.h" + #include #include #include @@ -43,37 +44,37 @@ typedef struct { #define ATTR_STRUCT_DICT(graph) ((igraphmodule_i_attribute_struct*)((graph)->attr))->attrs #define ATTR_NAME_INDEX(graph) ((igraphmodule_i_attribute_struct*)((graph)->attr))->vertex_name_index -int igraphmodule_i_attribute_get_type(const igraph_t *graph, +igraph_error_t igraphmodule_i_attribute_get_type(const igraph_t *graph, igraph_attribute_type_t *type, igraph_attribute_elemtype_t elemtype, const char *name); -int igraphmodule_i_get_numeric_graph_attr(const igraph_t *graph, +igraph_error_t igraphmodule_i_get_numeric_graph_attr(const igraph_t *graph, const char *name, igraph_vector_t *value); -int igraphmodule_i_get_numeric_vertex_attr(const igraph_t *graph, +igraph_error_t igraphmodule_i_get_numeric_vertex_attr(const igraph_t *graph, const char *name, igraph_vs_t vs, igraph_vector_t *value); -int igraphmodule_i_get_numeric_edge_attr(const igraph_t *graph, +igraph_error_t igraphmodule_i_get_numeric_edge_attr(const igraph_t *graph, const char *name, igraph_es_t es, igraph_vector_t *value); -int igraphmodule_i_get_string_graph_attr(const igraph_t *graph, +igraph_error_t igraphmodule_i_get_string_graph_attr(const igraph_t *graph, const char *name, igraph_strvector_t *value); -int igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, +igraph_error_t igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, const char *name, igraph_vs_t vs, igraph_strvector_t *value); -int igraphmodule_i_get_string_edge_attr(const igraph_t *graph, +igraph_error_t igraphmodule_i_get_string_edge_attr(const igraph_t *graph, const char *name, igraph_es_t es, igraph_strvector_t *value); -int igraphmodule_i_get_boolean_graph_attr(const igraph_t *graph, +igraph_error_t igraphmodule_i_get_boolean_graph_attr(const igraph_t *graph, const char *name, igraph_vector_bool_t *value); -int igraphmodule_i_get_boolean_vertex_attr(const igraph_t *graph, +igraph_error_t igraphmodule_i_get_boolean_vertex_attr(const igraph_t *graph, const char *name, igraph_vs_t vs, igraph_vector_bool_t *value); -int igraphmodule_i_get_boolean_edge_attr(const igraph_t *graph, +igraph_error_t igraphmodule_i_get_boolean_edge_attr(const igraph_t *graph, const char *name, igraph_es_t es, igraph_vector_bool_t *value); @@ -82,10 +83,8 @@ int igraphmodule_attribute_name_check(PyObject* obj); void igraphmodule_initialize_attribute_handler(void); void igraphmodule_index_vertex_names(igraph_t *graph, igraph_bool_t force); void igraphmodule_invalidate_vertex_name_index(igraph_t *graph); -int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_integer_t* id); +int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_int_t* id); -PyObject* igraphmodule_create_edge_attribute(const igraph_t* graph, - const char* name); PyObject* igraphmodule_create_or_get_edge_attribute_values(const igraph_t* graph, const char* name); PyObject* igraphmodule_get_edge_attribute_values(const igraph_t* graph, @@ -96,4 +95,3 @@ igraph_bool_t igraphmodule_has_vertex_attribute(const igraph_t *graph, const cha igraph_bool_t igraphmodule_has_edge_attribute(const igraph_t *graph, const char* name); #endif - diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c new file mode 100644 index 000000000..c8b814f88 --- /dev/null +++ b/src/_igraph/bfsiter.c @@ -0,0 +1,237 @@ +/* -*- mode: C -*- */ +/* + IGraph library. + Copyright (C) 2006-2023 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 "bfsiter.h" +#include "common.h" +#include "convert.h" +#include "error.h" +#include "pyhelpers.h" +#include "vertexobject.h" + +/** + * \ingroup python_interface + * \defgroup python_interface_bfsiter BFS iterator object + */ + +PyTypeObject* igraphmodule_BFSIterType; + +/** + * \ingroup python_interface_bfsiter + * \brief Allocate a new BFS 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_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { + igraphmodule_BFSIterObject* self; + igraph_int_t no_of_nodes, r; + + self = (igraphmodule_BFSIterObject*) PyType_GenericNew(igraphmodule_BFSIterType, 0, 0); + if (!self) { + return NULL; + } + + Py_INCREF(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"); + return NULL; + } + + no_of_nodes = igraph_vcount(&g->g); + 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(&self->queue, 100)) { + PyErr_SetString(PyExc_MemoryError, "out of memory"); + return NULL; + } + + if (igraph_vector_int_init(&self->neis, 0)) { + PyErr_SetString(PyExc_MemoryError, "out of memory"); + igraph_dqueue_int_destroy(&self->queue); + return NULL; + } + + if (PyLong_Check(root)) { + if (igraphmodule_PyObject_to_integer_t(root, &r)) { + 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(&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; + } + self->visited[r] = 1; + + if (!igraph_is_directed(&g->g)) { + mode=IGRAPH_ALL; + } + + self->mode = mode; + self->advanced = advanced; + + RC_ALLOC("BFSIter", self); + + return (PyObject*)self; +} + +/** + * \ingroup python_interface_bfsiter + * \brief Support for cyclic garbage collection in Python + * + * This is necessary because the \c igraph.BFSIter object contains several + * other \c PyObject pointers and they might point back to itself. + */ +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)); + return 0; +} + +/** + * \ingroup python_interface_bfsiter + * \brief Clears the iterator's subobject (before deallocation) + */ +int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { + PyObject_GC_UnTrack(self); + + Py_CLEAR(self->gref); + + igraph_dqueue_int_destroy(&self->queue); + igraph_vector_int_destroy(&self->neis); + + free(self->visited); + self->visited=0; + + return 0; +} + +/** + * \ingroup python_interface_bfsiter + * \brief Deallocates a Python representation of a given BFS iterator object + */ +static void igraphmodule_BFSIter_dealloc(igraphmodule_BFSIterObject* self) { + RC_DEALLOC("BFSIter", self); + + igraphmodule_BFSIter_clear(self); + + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_BFSIterType); +} + +static PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { + Py_INCREF(self); + return (PyObject*)self; +} + +static PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) { + if (!igraph_dqueue_int_empty(&self->queue)) { + igraph_int_t vid = igraph_dqueue_int_pop(&self->queue); + igraph_int_t dist = igraph_dqueue_int_pop(&self->queue); + igraph_int_t parent = igraph_dqueue_int_pop(&self->queue); + igraph_int_t i, n; + + if (igraph_neighbors(self->graph, &self->neis, vid, self->mode, /* loops = */ 0, /* multiple = */ 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + n = igraph_vector_int_size(&self->neis); + for (i = 0; i < n; i++) { + igraph_int_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; + } + } + } + + if (self->advanced) { + PyObject *vertexobj, *parentobj; + vertexobj = igraphmodule_Vertex_New(self->gref, vid); + if (!vertexobj) + return NULL; + if (parent >= 0) { + parentobj = igraphmodule_Vertex_New(self->gref, parent); + if (!parentobj) + return NULL; + } else { + Py_INCREF(Py_None); + parentobj=Py_None; + } + return Py_BuildValue("NnN", vertexobj, (Py_ssize_t)dist, parentobj); + } else { + return igraphmodule_Vertex_New(self->gref, vid); + } + } else { + return NULL; + } +} + +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/bfsiter.h b/src/_igraph/bfsiter.h similarity index 63% rename from src/bfsiter.h rename to src/_igraph/bfsiter.h index 94ddb6113..2e6fd5e3f 100644 --- a/src/bfsiter.h +++ b/src/_igraph/bfsiter.h @@ -1,53 +1,54 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_BFSITER_H -#define PYTHON_BFSITER_H +#ifndef IGRAPHMODULE_BFSITER_H +#define IGRAPHMODULE_BFSITER_H + +#include "preamble.h" -#include #include "graphobject.h" /** * \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_t queue; - igraph_vector_t neis; + igraph_dqueue_int_t queue; + igraph_vector_int_t neis; igraph_t *graph; char *visited; igraph_neimode_t mode; 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; + +int igraphmodule_BFSIter_register_type(void); -extern PyTypeObject igraphmodule_BFSIterType; +PyObject* igraphmodule_BFSIter_new( + igraphmodule_GraphObject *g, PyObject *o, igraph_neimode_t mode, + igraph_bool_t advanced +); #endif diff --git a/src/common.c b/src/_igraph/common.c similarity index 68% rename from src/common.c rename to src/_igraph/common.c index 25299a34d..a099ef030 100644 --- a/src/common.c +++ b/src/_igraph/common.c @@ -1,43 +1,44 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "common.h" +#include "pyhelpers.h" #include "structmember.h" /** * \ingroup python_interface * \brief Handler function for all unimplemented \c igraph.Graph methods - * + * * This function is called whenever an unimplemented \c igraph.Graph method * is called ("unimplemented" meaning that there is a method name in the * method table of \c igraph.Graph , but there isn't any working implementation * either because the underlying \c igraph API might be subject to change * or because the calling format from Python is not decided yet (or maybe * because of laziness or lack of time ;)) - * + * * All of the parameters are ignored, they are here just to make the * function satisfy the requirements of \c PyCFunction, thus allowing it * to be included in a method table. - * + * * \return NULL */ PyObject* igraphmodule_unimplemented(PyObject* self, PyObject* args, PyObject* kwds) @@ -45,27 +46,3 @@ PyObject* igraphmodule_unimplemented(PyObject* self, PyObject* args, PyObject* k PyErr_SetString(PyExc_NotImplementedError, "This method is unimplemented."); return NULL; } - -/** - * \ingroup python_interface - * \brief Resolves a weak reference to an \c igraph.Graph - * \return the \c igraph.Graph object or NULL if the weak reference is dead. - * Sets an exception in the latter case. - */ -PyObject* igraphmodule_resolve_graph_weakref(PyObject* ref) { - PyObject *o; - -#ifndef PYPY_VERSION - /* PyWeakref_Check is not implemented in PyPy yet */ - if (!PyWeakref_Check(ref)) { - PyErr_SetString(PyExc_TypeError, "weak reference expected"); - return NULL; - } -#endif /* PYPY_VERSION */ - o=PyWeakref_GetObject(ref); - if (o == Py_None) { - PyErr_SetString(PyExc_TypeError, "underlying graph has already been destroyed"); - return NULL; - } - return o; -} diff --git a/src/common.h b/src/_igraph/common.h similarity index 65% rename from src/common.h rename to src/_igraph/common.h index ca301faf0..fda85c948 100644 --- a/src/common.h +++ b/src/_igraph/common.h @@ -1,29 +1,31 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_COMMON_H -#define PYTHON_COMMON_H +#ifndef IGRAPHMODULE_COMMON_H +#define IGRAPHMODULE_COMMON_H + +#include -#include +#include "preamble.h" #ifdef RC_DEBUG # define RC_ALLOC(T, P) fprintf(stderr, "[ alloc ] " T " @ %p\n", P) @@ -41,38 +43,12 @@ # 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 PyObject* igraphmodule_unimplemented(PyObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_resolve_graph_weakref(PyObject* ref); #endif diff --git a/src/convert.c b/src/_igraph/convert.c similarity index 51% rename from src/convert.c rename to src/_igraph/convert.c index 7c4274bbd..a36d0558d 100644 --- a/src/convert.c +++ b/src/_igraph/convert.c @@ -1,95 +1,157 @@ /* vim:set ts=2 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -/************************ Miscellaneous functions *************************/ +/************************ Conversion functions *************************/ -#include #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 #endif /** - * \brief Converts a Python integer to a C int + * \brief Converts a Python long to a C int * - * This is similar to PyInt_AsLong, but it checks for overflow first and throws - * an exception if necessary. + * This is similar to PyLong_AsLong, but it checks for overflow first and + * throws an exception if necessary. This variant is needed for enum conversions + * because we assume that enums fit into an int. + * + * Note that Python 3.13 also provides a PyLong_AsInt() function, hence we need + * a different name for this function. The difference is that PyLong_AsInt() + * needs an extra call to PyErr_Occurred() to disambiguate in case of errors. * * Returns -1 if there was an error, 0 otherwise. */ -int PyInt_AsInt(PyObject* obj, int* result) { - long dummy = PyInt_AsLong(obj); +int PyLong_AsInt_OutArg(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"); + PyErr_SetString(PyExc_OverflowError, "long 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"); + PyErr_SetString(PyExc_OverflowError, "long integer too large for conversion to C int"); return -1; } - *result = (int)dummy; + *result = dummy; return 0; } /** - * \brief Converts a Python long to a C int + * \ingroup python_interface_conversion + * \brief Converts a Python object to a corresponding igraph enum. * - * This is similar to PyLong_AsLong, but it checks for overflow first and - * throws an exception if necessary. + * 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. * - * Returns -1 if there was an error, 0 otherwise. + * \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 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"); +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) + return 0; + + if (PyLong_Check(o)) + return PyLong_AsInt_OutArg(o, result); + + s = PyUnicode_CopyAsString(o); + if (s == 0) { + PyErr_SetString(PyExc_TypeError, "int, long or string expected"); return -1; } - if (dummy > INT_MAX) { - PyErr_SetString(PyExc_OverflowError, - "long integer too large for conversion to C int"); + + /* 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; + } + + /* 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; + } + + table++; + } + + free(s); + + if (best_unique) { + PY_IGRAPH_DEPRECATED( + "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; } - *result = (int)dummy; - return 0; } /** * \ingroup python_interface_conversion - * \brief Converts a Python object to a corresponding igraph enum. + * \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 @@ -103,63 +165,77 @@ int PyLong_AsInt(PyObject* obj, int* result) { * \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(PyObject *o, +int igraphmodule_PyObject_to_enum_strict(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) + + if (o == 0 || o == Py_None) { return 0; - if (PyInt_Check(o)) - return PyInt_AsInt(o, result); - if (PyLong_Check(o)) - return PyLong_AsInt(o, result); - s = PyString_CopyAsString(o); + } + + if (PyLong_Check(o)) { + return PyLong_AsInt_OutArg(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); - best = 0; best_unique = 0; best_result = -1; - /* Search for matches */ + } + + /* Search for exact 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++; + if (strcmp(s, table->name) == 0) { + *result = table->value; + free(s); + return 0; + } + table++; } + free(s); - if (best_unique) { *result = best_result; return 0; } PyErr_SetObject(PyExc_ValueError, o); + return -1; } +#define TRANSLATE_ENUM_WITH(translation_table) \ + int result_int = *result, retval; \ + retval = igraphmodule_PyObject_to_enum(o, translation_table, &result_int); \ + if (retval == 0) { \ + *result = result_int; \ + } \ + return retval; + +#define TRANSLATE_ENUM_STRICTLY_WITH(translation_table) \ + int result_int = *result, retval; \ + retval = igraphmodule_PyObject_to_enum_strict(o, translation_table, &result_int); \ + if (retval == 0) { \ + *result = result_int; \ + } \ + return retval; + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_neimode_t */ -int igraphmodule_PyObject_to_neimode_t(PyObject *o, - igraph_neimode_t *result) { +int igraphmodule_PyObject_to_neimode_t(PyObject *o, igraph_neimode_t *result) { static igraphmodule_enum_translation_table_entry_t neimode_tt[] = { {"in", IGRAPH_IN}, {"out", IGRAPH_OUT}, {"all", IGRAPH_ALL}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, neimode_tt, (int*)result); + TRANSLATE_ENUM_WITH(neimode_tt); } /** @@ -188,7 +264,7 @@ int igraphmodule_PyObject_to_add_weights_t(PyObject *o, return 0; } - return igraphmodule_PyObject_to_enum(o, add_weights_tt, (int*)result); + TRANSLATE_ENUM_WITH(add_weights_tt); } /** @@ -204,11 +280,12 @@ 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} }; - - return igraphmodule_PyObject_to_enum(o, adjacency_tt, (int*)result); + TRANSLATE_ENUM_WITH(adjacency_tt); } int igraphmodule_PyObject_to_attribute_combination_type_t(PyObject* o, @@ -216,6 +293,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}, @@ -224,6 +302,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} }; @@ -238,11 +317,11 @@ int igraphmodule_PyObject_to_attribute_combination_type_t(PyObject* o, return 0; } - return igraphmodule_PyObject_to_enum(o, attribute_combination_type_tt, (int*)result); + TRANSLATE_ENUM_WITH(attribute_combination_type_tt); } -int igraphmodule_PyObject_to_eigen_algorithm_t(PyObject *object, - igraph_eigen_algorithm_t *a) { +int igraphmodule_PyObject_to_eigen_algorithm_t(PyObject *o, + igraph_eigen_algorithm_t *result) { static igraphmodule_enum_translation_table_entry_t eigen_algorithm_tt[] = { {"auto", IGRAPH_EIGEN_AUTO}, @@ -254,19 +333,19 @@ int igraphmodule_PyObject_to_eigen_algorithm_t(PyObject *object, {0,0} }; - if (object == Py_None) { - *a = IGRAPH_EIGEN_ARPACK; + if (o == Py_None) { + *result = IGRAPH_EIGEN_ARPACK; return 0; - } else { - return igraphmodule_PyObject_to_enum(object, eigen_algorithm_tt, - (int*)a); } + + TRANSLATE_ENUM_WITH(eigen_algorithm_tt); } -int igraphmodule_PyObject_to_eigen_which_t(PyObject *object, - igraph_eigen_which_t *w) { +int igraphmodule_PyObject_to_eigen_which_t(PyObject *o, + igraph_eigen_which_t *result) { PyObject *key, *value; Py_ssize_t pos = 0; + igraph_eigen_which_t *w = result; static igraphmodule_enum_translation_table_entry_t eigen_which_position_tt[] = { @@ -295,20 +374,19 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *object, w->pos = IGRAPH_EIGEN_LM; w->howmany = 1; w->il = w->iu = -1; - w->vl = IGRAPH_NEGINFINITY; + w->vl = -IGRAPH_INFINITY; w->vu = IGRAPH_INFINITY; w->vestimate = 0; w->balance = IGRAPH_LAPACK_DGEEVX_BALANCE_NONE; - if (object != Py_None && !PyDict_Check(object)) { + if (o != Py_None && !PyDict_Check(o)) { PyErr_SetString(PyExc_TypeError, "Python dictionary expected"); return -1; } - if (object != Py_None) { - while (PyDict_Next(object, &pos, &key, &value)) { + if (o != Py_None) { + while (PyDict_Next(o, &pos, &key, &value)) { char *kv; -#ifdef IGRAPH_PYTHON3 PyObject *temp_bytes; if (!PyUnicode_Check(key)) { PyErr_SetString(PyExc_TypeError, "Dict key must be string"); @@ -319,47 +397,58 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *object, /* Exception set already by PyUnicode_AsEncodedString */ return -1; } - 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"); + kv = PyBytes_AsString(temp_bytes); + if (kv == 0) { + /* Exception set already by PyBytes_AsString */ return -1; } - kv=PyString_AsString(key); -#endif + 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, - (int*) &w->pos); + int w_pos_int = w->pos; + if (igraphmodule_PyObject_to_enum(value, eigen_which_position_tt, &w_pos_int)) { + return -1; + } + w->pos = w_pos_int; } else if (!strcasecmp(kv, "howmany")) { - w->howmany = (int) PyInt_AsLong(value); + if (PyLong_AsInt_OutArg(value, &w->howmany)) { + return -1; + } } else if (!strcasecmp(kv, "il")) { - w->il = (int) PyInt_AsLong(value); + if (PyLong_AsInt_OutArg(value, &w->il)) { + return -1; + } } else if (!strcasecmp(kv, "iu")) { - w->iu = (int) PyInt_AsLong(value); + if (PyLong_AsInt_OutArg(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) PyInt_AsLong(value); + if (PyLong_AsInt_OutArg(value, &w->vestimate)) { + return -1; + } } else if (!strcasecmp(kv, "balance")) { - igraphmodule_PyObject_to_enum(value, lapack_dgeevc_balance_tt, - (int*) &w->balance); + int w_balance_as_int = w->balance; + if (igraphmodule_PyObject_to_enum(value, lapack_dgeevc_balance_tt, &w_balance_as_int)) { + return -1; + } + w->balance = w_balance_as_int; } 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; @@ -377,8 +466,7 @@ int igraphmodule_PyObject_to_barabasi_algorithm_t(PyObject *o, {"psumtree_multiple", IGRAPH_BARABASI_PSUMTREE_MULTIPLE}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, barabasi_algorithm_tt, (int*)result); + TRANSLATE_ENUM_WITH(barabasi_algorithm_tt); } /** @@ -392,8 +480,7 @@ int igraphmodule_PyObject_to_connectedness_t(PyObject *o, {"strong", IGRAPH_STRONG}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, connectedness_tt, (int*)result); + TRANSLATE_ENUM_WITH(connectedness_tt); } /** @@ -410,8 +497,7 @@ int igraphmodule_PyObject_to_vconn_nei_t(PyObject *o, {"ignore", IGRAPH_VCONN_NEI_IGNORE}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, vconn_nei_tt, (int*)result); + TRANSLATE_ENUM_WITH(vconn_nei_tt); } /** @@ -429,8 +515,34 @@ int igraphmodule_PyObject_to_bliss_sh_t(PyObject *o, {"fsm", IGRAPH_BLISS_FSM}, {0,0} }; + TRANSLATE_ENUM_WITH(bliss_sh_tt); +} + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_chung_lu_t + */ +int igraphmodule_PyObject_to_chung_lu_t(PyObject *o, igraph_chung_lu_t *result) { + static igraphmodule_enum_translation_table_entry_t chung_lu_tt[] = { + {"original", IGRAPH_CHUNG_LU_ORIGINAL}, + {"maxent", IGRAPH_CHUNG_LU_MAXENT}, + {"nr", IGRAPH_CHUNG_LU_NR}, + {0,0} + }; + TRANSLATE_ENUM_WITH(chung_lu_tt); +} - return igraphmodule_PyObject_to_enum(o, bliss_sh_tt, (int*)result); +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_coloring_greedy_t + */ +int igraphmodule_PyObject_to_coloring_greedy_t(PyObject *o, igraph_coloring_greedy_t *result) { + static igraphmodule_enum_translation_table_entry_t coloring_greedy_tt[] = { + {"colored_neighbors", IGRAPH_COLORING_GREEDY_COLORED_NEIGHBORS}, + {"dsatur", IGRAPH_COLORING_GREEDY_DSATUR}, + {0,0} + }; + TRANSLATE_ENUM_WITH(coloring_greedy_tt); } /** @@ -445,12 +557,12 @@ 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} }; - - return igraphmodule_PyObject_to_enum(o, commcmp_tt, (int*)result); + TRANSLATE_ENUM_WITH(commcmp_tt); } /** @@ -460,14 +572,20 @@ int igraphmodule_PyObject_to_community_comparison_t(PyObject *o, int igraphmodule_PyObject_to_degseq_t(PyObject *o, igraph_degseq_t *result) { static igraphmodule_enum_translation_table_entry_t degseq_tt[] = { - {"simple", IGRAPH_DEGSEQ_SIMPLE}, - {"no_multiple", IGRAPH_DEGSEQ_SIMPLE_NO_MULTIPLE}, - {"vl", IGRAPH_DEGSEQ_VL}, + /* legacy names before 0.10 */ + {"simple", IGRAPH_DEGSEQ_CONFIGURATION}, + {"no_multiple", IGRAPH_DEGSEQ_FAST_HEUR_SIMPLE}, {"viger-latapy", IGRAPH_DEGSEQ_VL}, + /* up-to-date names as of igraph 0.10 */ + {"configuration", IGRAPH_DEGSEQ_CONFIGURATION}, + {"vl", IGRAPH_DEGSEQ_VL}, + {"viger_latapy", IGRAPH_DEGSEQ_VL}, + {"fast_heur_simple", IGRAPH_DEGSEQ_FAST_HEUR_SIMPLE}, + {"configuration_simple", IGRAPH_DEGSEQ_CONFIGURATION_SIMPLE}, + {"edge_switching_simple", IGRAPH_DEGSEQ_EDGE_SWITCHING_SIMPLE}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, degseq_tt, (int*)result); + TRANSLATE_ENUM_WITH(degseq_tt); } /** @@ -482,10 +600,66 @@ int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, {"exact", IGRAPH_FAS_EXACT_IP}, {"exact_ip", IGRAPH_FAS_EXACT_IP}, {"ip", IGRAPH_FAS_EXACT_IP}, + {"ip_ti", IGRAPH_FAS_EXACT_IP_TI}, + {"ip_cg", IGRAPH_FAS_EXACT_IP_CG}, + {0,0} + }; + TRANSLATE_ENUM_WITH(fas_algorithm_tt); +} + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_fvs_algorithm_t + */ +int igraphmodule_PyObject_to_fvs_algorithm_t(PyObject *o, + igraph_fvs_algorithm_t *result) { + static igraphmodule_enum_translation_table_entry_t fvs_algorithm_tt[] = { + {"ip", IGRAPH_FVS_EXACT_IP}, + {0,0} + }; + TRANSLATE_ENUM_WITH(fvs_algorithm_tt); +} + +/** + * \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} + }; + TRANSLATE_ENUM_WITH(get_adjacency_tt); +} + +/** + * \brief Converts a Python object to an igraph \c igraph_laplacian_normalization_t + */ +int igraphmodule_PyObject_to_laplacian_normalization_t( + PyObject *o, igraph_laplacian_normalization_t *result +) { + static igraphmodule_enum_translation_table_entry_t laplacian_normalization_tt[] = { + {"unnormalized", IGRAPH_LAPLACIAN_UNNORMALIZED}, + {"symmetric", IGRAPH_LAPLACIAN_SYMMETRIC}, + {"left", IGRAPH_LAPLACIAN_LEFT}, + {"right", IGRAPH_LAPLACIAN_RIGHT}, {0,0} }; - return igraphmodule_PyObject_to_enum(o, fas_algorithm_tt, (int*)result); + if (o == Py_True) { + *result = IGRAPH_LAPLACIAN_SYMMETRIC; + return 0; + } + + if (o == Py_False) { + *result = IGRAPH_LAPLACIAN_UNNORMALIZED; + return 0; + } + + TRANSLATE_ENUM_WITH(laplacian_normalization_tt); } /** @@ -502,12 +676,68 @@ int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *re if (o == Py_True) { *result = IGRAPH_LAYOUT_GRID; return 0; - } else if (o == Py_False) { + } + + if (o == Py_False) { *result = IGRAPH_LAYOUT_NOGRID; return 0; - } else { - return igraphmodule_PyObject_to_enum(o, layout_grid_tt, (int*)result); } + + TRANSLATE_ENUM_WITH(layout_grid_tt); +} + +/** + * \brief Converts a Python object to an igraph \c igraph_loops_t + */ +int igraphmodule_PyObject_to_loops_t(PyObject *o, igraph_loops_t *result) { + static igraphmodule_enum_translation_table_entry_t loops_tt[] = { + {"ignore", IGRAPH_NO_LOOPS}, + {"once", IGRAPH_LOOPS_ONCE}, + {"twice", IGRAPH_LOOPS_TWICE}, + {0,0} + }; + + if (o == Py_True) { + *result = IGRAPH_LOOPS_TWICE; + return 0; + } + + if (o == Py_False) { + *result = IGRAPH_NO_LOOPS; + return 0; + } + + TRANSLATE_ENUM_WITH(loops_tt); +} + +/** + * \brief Converts a Python object to an igraph \c igraph_lpa_variant_t + */ +int igraphmodule_PyObject_to_lpa_variant_t(PyObject *o, igraph_lpa_variant_t *result) { + static igraphmodule_enum_translation_table_entry_t lpa_variant_tt[] = { + {"dominance", IGRAPH_LPA_DOMINANCE}, + {"retention", IGRAPH_LPA_RETENTION}, + {"fast", IGRAPH_LPA_FAST}, + {0,0} + }; + + TRANSLATE_ENUM_WITH(lpa_variant_tt); +} + +/** + * \brief Converts a Python object to an igraph \c igraph_mst_algorithm_t + */ +int igraphmodule_PyObject_to_mst_algorithm_t(PyObject *o, igraph_mst_algorithm_t *result) { + static igraphmodule_enum_translation_table_entry_t mst_algorithm_tt[] = { + {"auto", IGRAPH_MST_AUTOMATIC}, + {"automatic", IGRAPH_MST_AUTOMATIC}, + {"unweighted", IGRAPH_MST_UNWEIGHTED}, + {"prim", IGRAPH_MST_PRIM}, + {"kruskal", IGRAPH_MST_KRUSKAL}, + {0,0} + }; + + TRANSLATE_ENUM_WITH(mst_algorithm_tt); } /** @@ -521,8 +751,7 @@ int igraphmodule_PyObject_to_random_walk_stuck_t(PyObject *o, {"error", IGRAPH_RANDOM_WALK_STUCK_ERROR}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, random_walk_stuck_tt, (int*)result); + TRANSLATE_ENUM_WITH(random_walk_stuck_tt); } /** @@ -534,22 +763,21 @@ int igraphmodule_PyObject_to_reciprocity_t(PyObject *o, igraph_reciprocity_t *re {"ratio", IGRAPH_RECIPROCITY_RATIO}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, reciprocity_tt, (int*)result); + TRANSLATE_ENUM_WITH(reciprocity_tt); } /** - * \brief Converts a Python object to an igraph \c igraph_rewiring_t + * \brief Converts a Python object to an igraph \c igraphmodule_shortest_path_algorithm_t */ -int igraphmodule_PyObject_to_rewiring_t(PyObject *o, igraph_rewiring_t *result) { - static igraphmodule_enum_translation_table_entry_t rewiring_tt[] = { - {"simple", IGRAPH_REWIRING_SIMPLE}, - {"simple_loops", IGRAPH_REWIRING_SIMPLE_LOOPS}, - {"loops", IGRAPH_REWIRING_SIMPLE_LOOPS}, +int igraphmodule_PyObject_to_shortest_path_algorithm_t(PyObject *o, igraphmodule_shortest_path_algorithm_t *result) { + static igraphmodule_enum_translation_table_entry_t shortest_path_algorithm_tt[] = { + {"auto", IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO}, + {"dijkstra", IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA}, + {"bellman_ford", IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD}, + {"johnson", IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_JOHNSON}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, rewiring_tt, (int*)result); + TRANSLATE_ENUM_WITH(shortest_path_algorithm_tt); } /** @@ -561,8 +789,7 @@ int igraphmodule_PyObject_to_spinglass_implementation_t(PyObject *o, igraph_spin {"negative", IGRAPH_SPINCOMM_IMP_NEG}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, spinglass_implementation_tt, (int*)result); + TRANSLATE_ENUM_WITH(spinglass_implementation_tt); } /** @@ -574,8 +801,7 @@ int igraphmodule_PyObject_to_spincomm_update_t(PyObject *o, igraph_spincomm_upda {"config", IGRAPH_SPINCOMM_UPDATE_CONFIG}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, spincomm_update_tt, (int*)result); + TRANSLATE_ENUM_WITH(spincomm_update_tt); } /** @@ -591,8 +817,7 @@ int igraphmodule_PyObject_to_star_mode_t(PyObject *o, {"undirected", IGRAPH_STAR_UNDIRECTED}, {0,0} }; - - return igraphmodule_PyObject_to_enum(o, star_mode_tt, (int*)result); + TRANSLATE_ENUM_WITH(star_mode_tt); } /** @@ -609,8 +834,34 @@ int igraphmodule_PyObject_to_subgraph_implementation_t(PyObject *o, {"new", IGRAPH_SUBGRAPH_CREATE_FROM_SCRATCH}, {0,0} }; + TRANSLATE_ENUM_WITH(subgraph_impl_tt); +} + +/** + * \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; + } + + if (o == Py_False) { + *result = IGRAPH_TO_DIRECTED_ARBITRARY; + return 0; + } - return igraphmodule_PyObject_to_enum(o, subgraph_impl_tt, (int*)result); + TRANSLATE_ENUM_WITH(to_directed_tt); } /** @@ -629,12 +880,14 @@ int igraphmodule_PyObject_to_to_undirected_t(PyObject *o, if (o == Py_True) { *result = IGRAPH_TO_UNDIRECTED_COLLAPSE; return 0; - } else if (o == Py_False) { + } + + if (o == Py_False) { *result = IGRAPH_TO_UNDIRECTED_EACH; return 0; } - return igraphmodule_PyObject_to_enum(o, to_undirected_tt, (int*)result); + TRANSLATE_ENUM_WITH(to_undirected_tt); } /** @@ -650,7 +903,7 @@ int igraphmodule_PyObject_to_transitivity_mode_t(PyObject *o, {0,0} }; - return igraphmodule_PyObject_to_enum(o, transitivity_mode_tt, (int*)result); + TRANSLATE_ENUM_WITH(transitivity_mode_tt); } /** @@ -670,7 +923,7 @@ int igraphmodule_PyObject_to_tree_mode_t(PyObject *o, {0,0} }; - return igraphmodule_PyObject_to_enum(o, tree_mode_tt, (int*)result); + TRANSLATE_ENUM_WITH(tree_mode_tt); } /** @@ -688,9 +941,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; } @@ -699,102 +951,169 @@ int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result) { } /** - * \brief Converts a Python object to an igraph \c igraph_integer_t + * \brief Converts a PyLong to an igraph \c igraph_int_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, igraph_int_t* v) { + if (IGRAPH_INTEGER_SIZE == 64) { + /* 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 */ + long long int dummy = PyLong_AsLongLong(obj); + if (PyErr_Occurred()) { + return 1; + } + *v = dummy; + } else { + /* 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; +} + +/** + * \brief Converts a Python object to an igraph \c igraph_int_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) { - int retval, num; +int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_int_t *v) { + int retval; + igraph_int_t num; if (object == NULL) { } else if (PyLong_Check(object)) { - retval = PyLong_AsInt(object, &num); - if (retval) - return retval; - *v = num; - return 0; -#ifdef IGRAPH_PYTHON3 - } 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; - } -#else - } else if (PyInt_Check(object)) { - retval = PyInt_AsInt(object, &num); + retval = PyLong_to_integer_t(object, &num); if (retval) return retval; *v = num; return 0; } else if (PyNumber_Check(object)) { - PyObject *i = PyNumber_Int(object); + /* try to recast as PyLong */ + PyObject *i = PyNumber_Long(object); if (i == NULL) return 1; - retval = PyInt_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; *v = num; return 0; } -#endif PyErr_BadArgument(); return 1; } /** - * \brief Converts a Python object to an igraph \c igraph_real_t + * \brief Converts a Python object to an igraph \c igraph_int_t when it is + * used as a limit on the number of results for some function. + * + * This is different from \ref igraphmodule_PyObject_to_integer_t such that it + * converts None and positive infinity to \c IGRAPH_UNLIMITED, and it does not + * accept negative values. * * 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_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 */ - - if (object == NULL) { - } else if (PyLong_Check(object)) { - 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; - 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); - Py_DECREF(i); - *v = (igraph_real_t)d; - return 0; - } - PyErr_BadArgument(); - return 1; +int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_int_t *v) { + int retval; + igraph_int_t num; + + if (object != NULL) { + if (object == Py_None) { + *v = IGRAPH_UNLIMITED; + return 0; + } + + if (PyNumber_Check(object)) { + PyObject *flt = PyNumber_Float(object); + if (flt == NULL) { + return 1; + } + + if (PyFloat_AsDouble(flt) == IGRAPH_INFINITY) { + Py_DECREF(flt); + *v = IGRAPH_UNLIMITED; + return 0; + } + + Py_DECREF(flt); + } + } + + retval = igraphmodule_PyObject_to_integer_t(object, &num); + if (retval) { + return retval; + } + + if (num < 0) { + PyErr_SetString(PyExc_ValueError, "expected non-negative integer, None or infinity"); + return 1; + } + + *v = num; + return 0; +} + +/** + * \brief Converts a Python object to an igraph \c igraph_real_t + * + * Raises suitable Python exceptions when needed. + * + * \param object the Python object to be converted; \c NULL is accepted but + * will keep the input value of v + * \param v the result is returned here + * \return 0 if everything was OK, 1 otherwise + */ +int igraphmodule_PyObject_to_real_t(PyObject *object, igraph_real_t *v) { + igraph_real_t value; + + if (object == NULL) { + return 0; + } else if (PyLong_Check(object)) { + value = PyLong_AsDouble(object); + } else if (PyFloat_Check(object)) { + value = PyFloat_AsDouble(object); + } else if (PyNumber_Check(object)) { + PyObject *i = PyNumber_Float(object); + if (i == NULL) { + return 1; + } + value = PyFloat_AsDouble(i); + Py_DECREF(i); + } else { + PyErr_BadArgument(); + return 1; + } + + if (PyErr_Occurred()) { + return 1; + } else { + *v = value; + return 0; + } } /** @@ -802,7 +1121,7 @@ int igraphmodule_PyObject_to_real_t(PyObject *object, igraph_real_t *v) { * \brief Converts a Python object to an igraph \c igraph_vector_t * The incoming \c igraph_vector_t should be uninitialized. Raises suitable * Python exceptions when needed. - * + * * \param list the Python list to be converted * \param v the \c igraph_vector_t containing the result * \param need_non_negative if true, checks whether all elements are non-negative @@ -812,7 +1131,7 @@ int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph PyObject *item, *it; Py_ssize_t size_hint; int ok; - igraph_integer_t number; + igraph_int_t number; if (PyBaseString_Check(list)) { /* It is highly unlikely that a string (although it is a sequence) will @@ -851,7 +1170,7 @@ int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph it = PyObject_GetIter(list); if (it) { while ((item = PyIter_Next(it)) != 0) { - ok = 1; + ok = true; if (igraphmodule_PyObject_to_integer_t(item, &number)) { PyErr_SetString(PyExc_ValueError, "iterable must yield integers"); @@ -864,7 +1183,7 @@ int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph } Py_DECREF(item); - + if (!ok) { igraph_vector_destroy(v); Py_DECREF(it); @@ -881,6 +1200,7 @@ int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph Py_DECREF(it); } else { /* list is not iterable; maybe it's a single number? */ + PyErr_Clear(); if (igraphmodule_PyObject_to_integer_t(list, &number)) { PyErr_SetString(PyExc_TypeError, "sequence or iterable expected"); igraph_vector_destroy(v); @@ -891,7 +1211,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; + } } } @@ -904,7 +1228,7 @@ int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph * \brief Converts a Python list of floats to an igraph \c igraph_vector_t * The incoming \c igraph_vector_t should be uninitialized. Raises suitable * Python exceptions when needed. - * + * * \param list the Python list to be converted * \param v the \c igraph_vector_t containing the result * \return 0 if everything was OK, 1 otherwise @@ -952,7 +1276,7 @@ int igraphmodule_PyObject_float_to_vector_t(PyObject *list, igraph_vector_t *v) it = PyObject_GetIter(list); if (it) { while ((item = PyIter_Next(it)) != 0) { - ok = 1; + ok = true; if (igraphmodule_PyObject_to_real_t(item, &number)) { PyErr_SetString(PyExc_ValueError, "iterable must yield numbers"); @@ -960,7 +1284,7 @@ int igraphmodule_PyObject_float_to_vector_t(PyObject *list, igraph_vector_t *v) } Py_DECREF(item); - + if (!ok) { igraph_vector_destroy(v); Py_DECREF(it); @@ -977,6 +1301,7 @@ int igraphmodule_PyObject_float_to_vector_t(PyObject *list, igraph_vector_t *v) Py_DECREF(it); } else { /* list is not iterable; maybe it's a single number? */ + PyErr_Clear(); if (igraphmodule_PyObject_to_real_t(list, &number)) { PyErr_SetString(PyExc_TypeError, "sequence or iterable expected"); igraph_vector_destroy(v); @@ -1004,10 +1329,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_int_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 @@ -1018,32 +1343,29 @@ 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 = false; } else { - PyObject *item2 = PyNumber_Int(item); - if (item2 == 0) { - PyErr_SetString(PyExc_TypeError, "can't convert a list item to integer"); - ok = 0; - } else { - ok = (PyInt_AsInt(item, &value) == 0); - Py_DECREF(item2); - } + ok = (igraphmodule_PyObject_to_integer_t(item, &value) == 0); } - + if (ok == 0) { igraph_vector_int_destroy(v); Py_DECREF(item); Py_DECREF(it); return 1; } + if (igraph_vector_int_push_back(v, value)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(v); @@ -1051,163 +1373,55 @@ 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>=1; - list=PyList_New(n); - - /* populate the list with data */ - for (i=0, j=0; ibuf, 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; + } + + Py_DECREF(unfolded_list); +#endif + } + + return 0; + } + it = PyObject_GetIter(list); if (!it) return 1; - igraph_vector_init(v, 0); + igraph_vector_int_init(v, 0); + if (list_is_owned) { + *list_is_owned = 1; + } + while ((item = PyIter_Next(it)) != 0) { - ok = 1; + ok = true; if (!PySequence_Check(item) || PySequence_Size(item) != 2) { PyErr_SetString(PyExc_TypeError, "iterable must return pairs of integers or strings"); - ok=0; + ok = false; } 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); @@ -1510,18 +1943,18 @@ int igraphmodule_PyObject_to_edgelist(PyObject *list, igraph_vector_t *v, 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; + ok = false; } - if (ok && igraph_vector_push_back(v, idx2)) { + if (ok && igraph_vector_int_push_back(v, idx2)) { igraphmodule_handle_igraph_error(); - ok = 0; + ok = false; } } if (!ok) { - igraph_vector_destroy(v); + igraph_vector_int_destroy(v); Py_DECREF(it); return 1; } @@ -1543,7 +1976,7 @@ int igraphmodule_PyObject_to_edgelist(PyObject *list, igraph_vector_t *v, * object as an attribute name and returns the attribute values corresponding * to the name as an \c igraph_vector_t, or returns a null pointer if the attribute * does not exist. - * + * * Note that if the function returned a pointer to an \c igraph_vector_t, * it is the caller's responsibility to destroy the object and free its * pointer after having finished using it. @@ -1559,15 +1992,20 @@ 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 (PyString_Check(o)) { + } + + 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; - char *name = PyString_CopyAsString(o); + igraph_int_t n; + char *name = PyUnicode_CopyAsString(o); if (attr_type == ATTRIBUTE_TYPE_VERTEX) { et = IGRAPH_ATTRIBUTE_VERTEX; @@ -1590,12 +2028,24 @@ 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; } - igraph_vector_init(result, n); + if (igraph_vector_init(result, 0)) { + igraphmodule_handle_igraph_error(); + free(name); + free(result); + return 1; + } + if (igraph_vector_reserve(result, n)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(result); + free(name); + free(result); + return 1; + } if (attr_type == ATTRIBUTE_TYPE_VERTEX) { if (igraphmodule_i_get_numeric_vertex_attr(&self->g, name, igraph_vss_all(), result)) { @@ -1619,7 +2069,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; } @@ -1640,10 +2090,9 @@ int igraphmodule_attrib_to_vector_t(PyObject *o, igraphmodule_GraphObject *self, * \ingroup python_interface_conversion * \brief Converts an attribute name or a sequence to a vector_int_t * - * Similar to igraphmodule_attrib_to_vector_t and - * igraphmodule_attrib_to_vector_long_t. Make sure you fix bugs - * in all three places (if any). - * + * Similar to igraphmodule_attrib_to_vector_t. Make sure you fix bugs + * in all two places (if any). + * * Note that if the function returned a pointer to an \c igraph_vector_int_t, * it is the caller's responsibility to destroy the object and free its * pointer after having finished using it. @@ -1666,114 +2115,60 @@ int igraphmodule_attrib_to_vector_int_t(PyObject *o, igraphmodule_GraphObject *s if (o == Py_None) return 0; - if (PyString_Check(o)) { + if (PyUnicode_Check(o)) { igraph_vector_t* dummy = 0; - long int i, n; + igraph_int_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, name, igraph_vss_all(), result)) { @@ -1881,13 +2288,18 @@ 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; i= 0; i--) { + /* Remove the last graph from the list and take ownership of it temporarily */ + if (igraph_graph_list_remove(v, i, &g)) { + igraphmodule_handle_igraph_error(); + Py_DECREF(list); + return NULL; + } + + /* Transfer ownership of the graph to Python */ + o = (igraphmodule_GraphObject*) igraphmodule_Graph_subclass_from_igraph_t(type, &g); + if (o == NULL) { + igraph_destroy(&g); + Py_DECREF(list); + return NULL; + } + + /* Put the graph into the result list; the list will take ownership */ + if (PyList_SetItem(list, i, (PyObject *) o)) { + Py_DECREF(o); + Py_DECREF(list); + return NULL; + } + } + + if (!igraph_graph_list_empty(v)) { + PyErr_SetString(PyExc_RuntimeError, "expected empty graph list after conversion"); + Py_DECREF(list); + return NULL; + } + + return list; +} + + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python list of lists to an \c igraph_matrix_t + * + * \param o the Python object representing the list of lists + * \param m the address of an uninitialized \c igraph_matrix_t + * \param arg_name name of the argument we are attempting to convert, if + * applicable. May be used in error messages. + * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. + */ +int igraphmodule_PyObject_to_matrix_t( + PyObject* o, igraph_matrix_t *m, const char *arg_name +) { + return igraphmodule_PyObject_to_matrix_t_with_minimum_column_count(o, m, 0, arg_name); +} + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python list of lists to an \c igraph_matrix_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_t + * \param num_cols the minimum number of columns in the matrix + * \param arg_name name of the argument we are attempting to convert, if + * applicable. May be used in error messages. + * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. + */ +int igraphmodule_PyObject_to_matrix_t_with_minimum_column_count( + PyObject *o, igraph_matrix_t *m, int min_cols, const char *arg_name +) { + Py_ssize_t nr, nc, n, i, j; + PyObject *row, *item; + igraph_real_t value; + + /* calculate the matrix dimensions */ + if (!PySequence_Check(o) || PyUnicode_Check(o)) { + if (arg_name) { + PyErr_Format(PyExc_TypeError, "matrix expected in '%s'", arg_name); + } else { + PyErr_SetString(PyExc_TypeError, "matrix expected"); + } + return 1; + } + + nr = PySequence_Size(o); + if (nr < 0) { + return 1; + } + + 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); + if (arg_name) { + PyErr_Format(PyExc_TypeError, "matrix expected in '%s'", arg_name); + } else { + PyErr_SetString(PyExc_TypeError, "matrix expected"); + } + return 1; + } + n = PySequence_Size(row); + Py_DECREF(row); + if (n < 0) { + return 1; + } + if (n > nc) { + nc = n; + } + } + + if (igraph_matrix_init(m, nr, nc)) { + igraphmodule_handle_igraph_error(); + return 1; + } + + 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 (!item) { + igraph_matrix_destroy(m); + return 1; + } + if (igraphmodule_PyObject_to_real_t(item, &value)) { + igraph_matrix_destroy(m); + Py_DECREF(item); + return 1; + } + Py_DECREF(item); + MATRIX(*m, i, j) = value; + } + Py_DECREF(row); + } + + 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 + * \param arg_name name of the argument we are attempting to convert, if + * applicable. May be used in error messages. + * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. + */ +int igraphmodule_PyObject_to_matrix_int_t( + PyObject* o, igraph_matrix_int_t *m, const char* arg_name +) { + return igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count(o, m, 0, arg_name); +} + +/** + * \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 + * \param arg_name name of the argument we are attempting to convert, if + * applicable. May be used in error messages. + * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. + */ +int igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count( + PyObject *o, igraph_matrix_int_t *m, int min_cols, const char* arg_name +) { Py_ssize_t nr, nc, n, i, j; PyObject *row, *item; - int was_warned=0; + igraph_int_t value; /* calculate the matrix dimensions */ - if (!PySequence_Check(o) || PyString_Check(o)) { - PyErr_SetString(PyExc_TypeError, "matrix expected (list of sequences)"); + if (!PySequence_Check(o) || PyUnicode_Check(o)) { + if (arg_name) { + PyErr_Format(PyExc_TypeError, "integer matrix expected in '%s'", arg_name); + } else { + PyErr_SetString(PyExc_TypeError, "integer matrix expected"); + } return 1; } nr = PySequence_Size(o); - nc = 0; - 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)"); + if (arg_name) { + PyErr_Format(PyExc_TypeError, "integer matrix expected in '%s'", arg_name); + } else { + PyErr_SetString(PyExc_TypeError, "integer matrix expected"); + } return 1; } - n=PySequence_Size(row); + n = PySequence_Size(row); Py_DECREF(row); - if (n>nc) nc=n; - } - - igraph_matrix_init(m, nr, nc); - for (i=0; i nc) { + nc = n; + } + } + + if (igraph_matrix_int_init(m, nr, nc)) { + igraphmodule_handle_igraph_error(); + return 1; + } + + 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 (!item) { + igraph_matrix_int_destroy(m); + return 1; + } + if (igraphmodule_PyObject_to_integer_t(item, &value)) { + igraph_matrix_int_destroy(m); + Py_DECREF(item); + return 1; } Py_DECREF(item); + MATRIX(*m, i, j) = value; } Py_DECREF(row); } @@ -2097,7 +2955,7 @@ int igraphmodule_PyList_to_matrix_t(PyObject* o, igraph_matrix_t *m) { * \ingroup python_interface_conversion * \brief Converts a Python list of lists to an \c igraph_vector_ptr_t * containing \c igraph_vector_t items. - * + * * The returned vector will have an item destructor that destroys the * contained vectors, so it is important to call \c igraph_vector_ptr_destroy_all * on it instead of \c igraph_vector_ptr_destroy when the vector is no longer @@ -2112,7 +2970,7 @@ int igraphmodule_PyObject_to_vector_ptr_t(PyObject* list, igraph_vector_ptr_t* v PyObject *it, *item; igraph_vector_t *subvec; - if (PyString_Check(list)) { + if (PyUnicode_Check(list)) { PyErr_SetString(PyExc_TypeError, "expected iterable (but not string)"); return 1; } @@ -2130,7 +2988,7 @@ int igraphmodule_PyObject_to_vector_ptr_t(PyObject* list, igraph_vector_ptr_t* v IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(vec, igraph_vector_destroy); while ((item = PyIter_Next(it)) != 0) { - subvec = igraph_Calloc(1, igraph_vector_t); + subvec = IGRAPH_CALLOC(1, igraph_vector_t); if (subvec == 0) { Py_DECREF(item); Py_DECREF(it); @@ -2142,6 +3000,7 @@ int igraphmodule_PyObject_to_vector_ptr_t(PyObject* list, igraph_vector_ptr_t* v Py_DECREF(item); Py_DECREF(it); igraph_vector_destroy(subvec); + free(subvec); igraph_vector_ptr_destroy_all(vec); return 1; } @@ -2151,10 +3010,81 @@ int igraphmodule_PyObject_to_vector_ptr_t(PyObject* list, igraph_vector_ptr_t* v if (igraph_vector_ptr_push_back(vec, subvec)) { Py_DECREF(it); igraph_vector_destroy(subvec); + free(subvec); + igraph_vector_ptr_destroy_all(vec); + return 1; + } + + /* ownership of 'subvec' taken by 'vec' here */ + } + + Py_DECREF(it); + return 0; +} + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python list of lists to an \c igraph_vector_ptr_t + * containing \c igraph_vector_int_t items. + * + * The returned vector will have an item destructor that destroys the + * contained vectors, so it is important to call \c igraph_vector_ptr_destroy_all + * on it instead of \c igraph_vector_ptr_destroy when the vector is no longer + * needed. + * + * \param o the Python object representing the list of lists + * \param m the address of an uninitialized \c igraph_vector_ptr_t + * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. + */ +int igraphmodule_PyObject_to_vector_int_ptr_t(PyObject* list, igraph_vector_ptr_t* vec) { + PyObject *it, *item; + igraph_vector_int_t *subvec; + + if (PyUnicode_Check(list)) { + PyErr_SetString(PyExc_TypeError, "expected iterable (but not string)"); + return 1; + } + + it = PyObject_GetIter(list); + if (!it) { + return 1; + } + + if (igraph_vector_ptr_init(vec, 0)) { + igraphmodule_handle_igraph_error(); + Py_DECREF(it); + return 1; + } + + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(vec, igraph_vector_int_destroy); + while ((item = PyIter_Next(it)) != 0) { + subvec = IGRAPH_CALLOC(1, igraph_vector_int_t); + if (subvec == 0) { + Py_DECREF(item); + Py_DECREF(it); + PyErr_NoMemory(); + return 1; + } + + if (igraphmodule_PyObject_to_vector_int_t(item, subvec)) { + Py_DECREF(item); + Py_DECREF(it); + igraph_vector_int_destroy(subvec); + free(subvec); + igraph_vector_ptr_destroy_all(vec); + return 1; + } + + Py_DECREF(item); + + if (igraph_vector_ptr_push_back(vec, subvec)) { + Py_DECREF(it); + igraph_vector_int_destroy(subvec); + free(subvec); igraph_vector_ptr_destroy_all(vec); return 1; } - + /* ownership of 'subvec' taken by 'vec' here */ } @@ -2162,42 +3092,159 @@ int igraphmodule_PyObject_to_vector_ptr_t(PyObject* list, igraph_vector_ptr_t* v return 0; } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python list of lists to an \c igraph_vector_list_t + * (containing \c igraph_vector_t items). + * + * \param o the Python object representing the list of lists + * \param m the address of an uninitialized \c igraph_vector_list_t + * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. + */ +int igraphmodule_PyObject_to_vector_list_t(PyObject* list, igraph_vector_list_t* veclist) { + PyObject *it, *item; + igraph_vector_t vec; + + if (PyUnicode_Check(list)) { + PyErr_SetString(PyExc_TypeError, "expected iterable (but not string)"); + return 1; + } + + it = PyObject_GetIter(list); + if (!it) { + return 1; + } + + if (igraph_vector_list_init(veclist, 0)) { + igraphmodule_handle_igraph_error(); + Py_DECREF(it); + return 1; + } + + while ((item = PyIter_Next(it)) != 0) { + if (igraphmodule_PyObject_to_vector_t(item, &vec, 0)) { + Py_DECREF(item); + Py_DECREF(it); + igraph_vector_destroy(&vec); + igraph_vector_list_destroy(veclist); + return 1; + } + + Py_DECREF(item); + + if (igraph_vector_list_push_back(veclist, &vec)) { + Py_DECREF(it); + igraph_vector_destroy(&vec); + igraph_vector_list_destroy(veclist); + return 1; + } + + /* ownership of 'vec' taken by 'veclist' here */ + } + + Py_DECREF(it); + return 0; +} + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python list of lists to an \c igraph_vector_int_list_t + * (containing \c igraph_vector_int_t items). + * + * \param o the Python object representing the list of lists + * \param m the address of an uninitialized \c igraph_vector_int_list_t + * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. + */ +int igraphmodule_PyObject_to_vector_int_list_t(PyObject* list, igraph_vector_int_list_t* veclist) { + PyObject *it, *item; + igraph_vector_int_t vec; + + if (PyUnicode_Check(list)) { + PyErr_SetString(PyExc_TypeError, "expected iterable (but not string)"); + return 1; + } + + it = PyObject_GetIter(list); + if (!it) { + return 1; + } + + if (igraph_vector_int_list_init(veclist, 0)) { + igraphmodule_handle_igraph_error(); + Py_DECREF(it); + return 1; + } + + while ((item = PyIter_Next(it)) != 0) { + if (igraphmodule_PyObject_to_vector_int_t(item, &vec)) { + Py_DECREF(item); + Py_DECREF(it); + igraph_vector_int_destroy(&vec); + igraph_vector_int_list_destroy(veclist); + return 1; + } + + Py_DECREF(item); + + if (igraph_vector_int_list_push_back(veclist, &vec)) { + Py_DECREF(it); + igraph_vector_int_destroy(&vec); + igraph_vector_int_list_destroy(veclist); + return 1; + } + + /* ownership of 'vec' taken by 'veclist' here */ + } + + Py_DECREF(it); + return 0; +} + + /** * \ingroup python_interface_conversion * \brief Converts an \c igraph_strvector_t to a Python string list - * + * * \param v the \c igraph_strvector_t containing the vector to be converted * \return the Python string list as a \c PyObject*, or \c NULL if an error occurred */ PyObject* igraphmodule_strvector_t_to_PyList(igraph_strvector_t *v) { - PyObject* list; + PyObject *list, *item; Py_ssize_t n, i; - char* ptr; - - n=igraph_strvector_size(v); - if (n<0) + const char* ptr; + + n = igraph_strvector_size(v); + if (n < 0) { return igraphmodule_handle_igraph_error(); - - // create a new Python list - list=PyList_New(n); + } + + /* create a new Python list */ + list = PyList_New(n); + if (!list) { + return NULL; + } + /* populate the list with data */ - for (i=0; ig); Py_DECREF(t); - } - + } + + 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 - * + * * \param o the Python object * \param vid the vertex ID will be stored here * \param graph the graph that will be used to interpret vertex names @@ -2303,72 +3403,139 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(PyObject *it, * if we don't need name lookups. * \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 (PyInt_Check(o)) { - /* Single vertex ID */ - if (PyInt_AsInt(o, &tmp)) - return 1; - *vid = tmp; +int igraphmodule_PyObject_to_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph) { + if (o == 0) { + PyErr_SetString(PyExc_TypeError, "only non-negative integers, strings or igraph.Vertex objects can be converted to vertex IDs"); + return 1; } else if (PyLong_Check(o)) { /* Single vertex ID */ - if (PyLong_AsInt(o, &tmp)) + if (igraphmodule_PyObject_to_integer_t(o, vid)) { return 1; - *vid = tmp; + } } 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)) { + } + } 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); - } else if (PyIndex_Check(o)) { + } else { /* Other numeric type that can be converted to an index */ PyObject* num = PyNumber_Index(o); if (num) { - if (PyInt_Check(num)) { - retval = PyInt_AsInt(num, &tmp); - if (retval) { + if (PyLong_Check(num)) { + if (igraphmodule_PyObject_to_integer_t(num, vid)) { Py_DECREF(num); return 1; } - *vid = tmp; - } else if (PyLong_Check(num)) { - retval = PyLong_AsInt(num, &tmp); - if (retval) { - Py_DECREF(num); - return 1; - } - *vid = tmp; } 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 + } else { + PyErr_SetString(PyExc_TypeError, "only non-negative integers, 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) { - PyErr_Format(PyExc_ValueError, "vertex IDs must be positive, got: %ld", (long)(*vid)); + PyErr_Format(PyExc_ValueError, "vertex IDs must be non-negative, got: %" IGRAPH_PRId, *vid); return 1; } return 0; } +/** + * \ingroup python_interface_conversion + * \brief Tries to interpret a Python object as a single vertex ID, leaving + * the input vertex ID unmodified if the Python object is NULL or None + * + * \param o the Python object + * \param vid the vertex ID will be stored here + * \param graph the graph that will be used to interpret vertex names + * if a string was given in o. It may also be a null pointer + * if we don't need name lookups. + * \return 0 if everything was OK, 1 otherwise + */ +int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph) { + if (o == 0 || o == Py_None) { + return 0; + } else { + return igraphmodule_PyObject_to_vid(o, vid, graph); + } +} + +/** + * \ingroup python_interface_conversion + * \brief Tries to interpret a Python object as a list of vertex IDs. + * + * \param o the Python object + * \param result the vertex IDs will be stored here + * \param graph the graph that will be used to interpret vertex names + * if a string was given in o. It may also be a null pointer + * if we don't need name lookups. + * \return 0 if everything was OK, 1 otherwise + */ +int igraphmodule_PyObject_to_vid_list(PyObject* o, igraph_vector_int_t* result, igraph_t* graph) { + PyObject *iterator; + PyObject *item; + igraph_int_t vid; + + if (PyBaseString_Check(o)) { + /* exclude strings; they are iterable but cannot yield meaningful vertex IDs */ + PyErr_SetString(PyExc_TypeError, "cannot convert string to a list of vertex IDs"); + return 1; + } + + iterator = PyObject_GetIter(o); + if (iterator == NULL) { + PyErr_SetString(PyExc_TypeError, "conversion to vertex sequence failed"); + return 1; + } + + if (igraph_vector_int_init(result, 0)) { + Py_DECREF(iterator); + igraphmodule_handle_igraph_error(); + return 1; + } + + while ((item = PyIter_Next(iterator))) { + vid = -1; + + if (igraphmodule_PyObject_to_vid(item, &vid, graph)) { + Py_DECREF(item); + break; + } + + Py_DECREF(item); + + if (igraph_vector_int_push_back(result, vid)) { + igraphmodule_handle_igraph_error(); + /* no need to destroy 'result' here; will be done outside the loop due + * to PyErr_Occurred */ + break; + } + } + + Py_DECREF(iterator); + + if (PyErr_Occurred()) { + igraph_vector_int_destroy(result); + return 1; + } + + return 0; +} + /** * \ingroup python_interface_conversion * \brief Tries to interpret a Python object as a vertex selector - * + * * \param o the Python object * \param vs the \c igraph_vs_t which will contain the result * \param graph an \c igraph_t object which will be used to interpret vertex @@ -2381,9 +3548,9 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, - igraph_t *graph, igraph_bool_t *return_single, igraph_integer_t *single_vid) { - igraph_integer_t vid; - igraph_vector_t vector; + igraph_t *graph, igraph_bool_t *return_single, igraph_int_t *single_vid) { + igraph_int_t vid; + igraph_vector_int_t vector; if (o == 0 || o == Py_None) { /* Returns a vertex sequence for all vertices */ @@ -2393,15 +3560,19 @@ 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; + if (igraph_vs_copy(vs, &vso->vs)) { igraphmodule_handle_igraph_error(); return 1; } - if (return_single) + + if (return_single) { *return_single = 0; + } + return 0; } @@ -2410,30 +3581,33 @@ 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) { igraph_vs_all(vs); } else { - IGRAPH_CHECK(igraph_vector_init(&vector, slicelength)); - IGRAPH_FINALLY(igraph_vector_destroy, &vector); + if (igraph_vector_int_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_int_destroy(&vector); + return 1; + } - igraph_vector_destroy(&vector); - IGRAPH_FINALLY_CLEAN(1); + igraph_vector_int_destroy(&vector); } - if (return_single) + if (return_single) { *return_single = 0; + } return 0; } @@ -2442,9 +3616,6 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, /* Object cannot be converted to a single vertex ID, * assume it is a sequence or iterable */ - PyObject *iterator; - PyObject *item; - if (PyBaseString_Check(o)) { /* Special case: strings and unicode objects are sequences, but they * will not yield valid vertex IDs */ @@ -2454,47 +3625,28 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, /* Clear the exception set by igraphmodule_PyObject_to_vid */ PyErr_Clear(); - iterator = PyObject_GetIter(o); - - if (iterator == NULL) { - PyErr_SetString(PyExc_TypeError, "conversion to vertex sequence failed"); + if (igraphmodule_PyObject_to_vid_list(o, &vector, graph)) { return 1; } - IGRAPH_CHECK(igraph_vector_init(&vector, 0)); - IGRAPH_FINALLY(igraph_vector_destroy, &vector); - IGRAPH_CHECK(igraph_vector_reserve(&vector, 20)); - - while ((item = PyIter_Next(iterator))) { - vid = -1; - - if (igraphmodule_PyObject_to_vid(item, &vid, graph)) - break; - - Py_DECREF(item); - igraph_vector_push_back(&vector, vid); - } - Py_DECREF(iterator); - - if (PyErr_Occurred()) { - igraph_vector_destroy(&vector); - IGRAPH_FINALLY_CLEAN(1); + if (igraph_vs_vector_copy(vs, &vector)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&vector); return 1; } - IGRAPH_CHECK(igraph_vs_vector_copy(vs, &vector)); - igraph_vector_destroy(&vector); - IGRAPH_FINALLY_CLEAN(1); + igraph_vector_int_destroy(&vector); - if (return_single) + if (return_single) { *return_single = 0; - + } + return 0; } /* The object can be converted into a single vertex ID */ if (return_single) - *return_single = 1; + *return_single = true; if (single_vid) *single_vid = vid; @@ -2506,7 +3658,7 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, /** * \ingroup python_interface_conversion * \brief Tries to interpret a Python object as a single edge ID - * + * * \param o the Python object * \param eid the edge ID will be stored here * \param graph the graph that will be used to interpret vertex names and @@ -2514,94 +3666,100 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, * if we don't want to handle tuples. * \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; - igraph_integer_t vid1, vid2; +int igraphmodule_PyObject_to_eid(PyObject *o, igraph_int_t *eid, igraph_t *graph) { + int retval; + igraph_int_t vid1, vid2; - if (o == Py_None || o == 0) { - *eid = 0; - } else if (PyInt_Check(o)) { - /* Single edge ID */ - if (PyInt_AsInt(o, &tmp)) - return 1; - *eid = tmp; + if (!o) { + PyErr_SetString(PyExc_TypeError, + "only non-negative integers, 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 (PyLong_AsInt(o, &tmp)) + if (igraphmodule_PyObject_to_integer_t(o, eid)) { return 1; - *eid = tmp; - } 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_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 (PyInt_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); - return 1; - } - *eid = tmp; - } else { - PyErr_SetString(PyExc_TypeError, "PyNumber_Index returned invalid type"); - Py_DECREF(num); - return 1; - } - Py_DECREF(num); - } else - return 1; + *eid = igraphmodule_Edge_get_index_as_igraph_integer(eo); } 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); - if (!o2) + if (!o2) { + return 1; + } + + if (igraphmodule_PyObject_to_vid(o1, &vid1, graph)) { + return 1; + } + if (igraphmodule_PyObject_to_vid(o2, &vid2, graph)) { return 1; + } - if (igraphmodule_PyObject_to_vid(o1, &vid1, graph)) + retval = igraph_get_eid(graph, eid, vid1, vid2, 1, 0); + if (retval == IGRAPH_EINVVID) { + PyErr_Format( + PyExc_ValueError, + "no edge from vertex #%" IGRAPH_PRId " to #%" IGRAPH_PRId "; no such vertex ID", + vid1, vid2 + ); return 1; - if (igraphmodule_PyObject_to_vid(o2, &vid2, graph)) + } else if (retval) { + igraphmodule_handle_igraph_error(); return 1; + } - igraph_get_eid(graph, eid, vid1, vid2, 1, 0); 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 { - 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 non-negative integers, igraph.Edge objects or tuples of vertex IDs can be " + "converted to edge IDs"); + return 1; + } } if (*eid < 0) { - PyErr_Format(PyExc_ValueError, "edge IDs must be positive, got: %ld", (long)(*eid)); + PyErr_Format(PyExc_ValueError, "edge IDs must be non-negative, got: %" IGRAPH_PRId, *eid); return 1; } return 0; } - /** * \ingroup python_interface_conversion * \brief Tries to interpret a Python object as an edge selector - * + * * \param o the Python object * \param vs the \c igraph_es_t which will contain the result * \param graph an \c igraph_t object which will be used to interpret vertex @@ -2612,8 +3770,8 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g */ int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, igraph_bool_t *return_single) { - igraph_integer_t eid; - igraph_vector_t vector; + igraph_int_t eid; + igraph_vector_int_t vector; if (o == 0 || o == Py_None) { /* Returns an edge sequence for all edges */ @@ -2623,7 +3781,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)) { @@ -2648,49 +3806,59 @@ int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, iterator = PyObject_GetIter(o); if (iterator == NULL) { - PyErr_SetString(PyExc_TypeError, "conversion to edge sequene failed"); + PyErr_SetString(PyExc_TypeError, "conversion to edge sequence failed"); 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_int_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_int_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); + igraph_vector_int_destroy(&vector); return 1; } - - if (igraph_vector_size(&vector) > 0) { + + if (igraph_vector_int_size(&vector) > 0) { igraph_es_vector_copy(es, &vector); } else { igraph_es_none(es); } - igraph_vector_destroy(&vector); - IGRAPH_FINALLY_CLEAN(1); + igraph_vector_int_destroy(&vector); - if (return_single) + if (return_single) { *return_single = 0; - + } + return 0; } /* The object can be converted into a single edge ID */ - if (return_single) - *return_single = 1; + if (return_single) { + *return_single = true; + } + /* if (single_eid) *single_eid = eid; @@ -2704,7 +3872,7 @@ int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, /** * \ingroup python_interface_conversion * \brief Tries to interpret a Python object as a numeric attribute value list - * + * * \param o the Python object * \param v the \c igraph_vector_t which will contain the result * \param g a \c igraphmodule_GraphObject object or \c NULL - used when the @@ -2723,17 +3891,26 @@ int igraphmodule_PyObject_to_attribute_values(PyObject *o, igraphmodule_GraphObject* g, int type, igraph_real_t def) { PyObject* list = o; - long i, n; + Py_ssize_t i, n; + + if (o == NULL) { + return 1; + } - if (o==NULL) return 1; - if (o == Py_None) { - 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 (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; itype == IGRAPH_ATTRIBUTE_COMBINE_FUNCTION) { - result->func = value; + result->func = (void*) value; } else { result->func = 0; } if (name == Py_None) result->name = 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; @@ -2897,7 +4099,7 @@ int igraphmodule_i_PyObject_pair_to_attribute_combination_record_t( * \param object the Python object to be converted * \param result the result is returned here. It must be an uninitialized * \c igraph_attribute_combination_t object, it will be initialized accordingly. - * It is the responsibility of the caller to + * It is the responsibility of the caller to * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_attribute_combination_t(PyObject* object, @@ -2913,6 +4115,8 @@ int igraphmodule_PyObject_to_attribute_combination_t(PyObject* object, return 0; } + rec.type = IGRAPH_ATTRIBUTE_COMBINE_IGNORE; + if (PyDict_Check(object)) { /* a full-fledged dict was passed */ PyObject *key, *value; @@ -2924,9 +4128,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 */ @@ -2936,9 +4138,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; @@ -2948,15 +4148,68 @@ int igraphmodule_PyObject_to_attribute_combination_t(PyObject* object, * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_pagerank_algo_t */ -int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, - igraph_pagerank_algo_t *result) { +int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t *result) { 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} }; + TRANSLATE_ENUM_WITH(pagerank_algo_tt); +} + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_metric_t + */ +int igraphmodule_PyObject_to_metric_t(PyObject *o, igraph_metric_t *result) { + static igraphmodule_enum_translation_table_entry_t metric_tt[] = { + {"euclidean", IGRAPH_METRIC_EUCLIDEAN}, + {"l2", IGRAPH_METRIC_L2}, /* alias to the previous */ + {"manhattan", IGRAPH_METRIC_MANHATTAN}, + {"l1", IGRAPH_METRIC_L1}, /* alias to the previous */ + {0,0} + }; + TRANSLATE_ENUM_WITH(metric_tt); +} + +/** + * \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", IGRAPH_SIMPLE_SW}, + {"loops", IGRAPH_LOOPS_SW}, + {"multi", IGRAPH_MULTI_SW}, + {"all", IGRAPH_LOOPS_SW | IGRAPH_MULTI_SW}, + {0,0} + }; + TRANSLATE_ENUM_STRICTLY_WITH(edge_type_sw_tt); +} - 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_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} + }; + TRANSLATE_ENUM_STRICTLY_WITH(realize_degseq_tt); } +/** + * \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} + }; + TRANSLATE_ENUM_STRICTLY_WITH(random_tree_tt); +} diff --git a/src/convert.h b/src/_igraph/convert.h similarity index 53% rename from src/convert.h rename to src/_igraph/convert.h index f163b17b6..39fbf5619 100644 --- a/src/convert.h +++ b/src/_igraph/convert.h @@ -1,53 +1,63 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -/************************ Miscellaneous functions *************************/ +/************************ Conversion functions *************************/ /** \defgroup python_interface_conversion Converting between Python and igraph data types * \ingroup python_interface */ -#ifndef PYTHON_CONVERT_H -#define PYTHON_CONVERT_H +#ifndef IGRAPHMODULE_CONVERT_H +#define IGRAPHMODULE_CONVERT_H + +#include "preamble.h" -#include #include #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; int value; } igraphmodule_enum_translation_table_entry_t; -int PyInt_AsInt(PyObject* obj, int* result); -int PyLong_AsInt(PyObject* obj, int* result); +typedef enum { + IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO = 0, + IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA = 1, + IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD = 2, + IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_JOHNSON = 3, +} igraphmodule_shortest_path_algorithm_t; /* Conversion from PyObject to enum types */ 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, @@ -55,21 +65,34 @@ int igraphmodule_PyObject_to_attribute_combination_type_t(PyObject* o, int igraphmodule_PyObject_to_barabasi_algorithm_t(PyObject *o, igraph_barabasi_algorithm_t *result); int igraphmodule_PyObject_to_bliss_sh_t(PyObject *o, igraph_bliss_sh_t *result); +int igraphmodule_PyObject_to_chung_lu_t(PyObject *o, igraph_chung_lu_t *result); +int igraphmodule_PyObject_to_coloring_greedy_t(PyObject *o, igraph_coloring_greedy_t *result); int igraphmodule_PyObject_to_community_comparison_t(PyObject *obj, - igraph_community_comparison_t *result); + igraph_community_comparison_t *result); 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_fvs_algorithm_t(PyObject *o, igraph_fvs_algorithm_t *result); +int igraphmodule_PyObject_to_get_adjacency_t(PyObject *o, igraph_get_adjacency_t *result); +int igraphmodule_PyObject_to_laplacian_normalization_t(PyObject *o, igraph_laplacian_normalization_t *result); int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *result); +int igraphmodule_PyObject_to_lpa_variant_t(PyObject *o, igraph_lpa_variant_t *result); +int igraphmodule_PyObject_to_loops_t(PyObject *o, igraph_loops_t *result); +int igraphmodule_PyObject_to_metric_t(PyObject *o, igraph_metric_t *result); +int igraphmodule_PyObject_to_mst_algorithm_t(PyObject *o, igraph_mst_algorithm_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_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); +int igraphmodule_PyObject_to_shortest_path_algorithm_t(PyObject *o, igraphmodule_shortest_path_algorithm_t *result); int igraphmodule_PyObject_to_spinglass_implementation_t(PyObject *o, igraph_spinglass_implementation_t *result); 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); @@ -77,31 +100,52 @@ int igraphmodule_PyObject_to_vconn_nei_t(PyObject *o, igraph_vconn_nei_t *result /* Conversion from PyObject to igraph types */ -int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v); +int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_int_t *v); int igraphmodule_PyObject_to_real_t(PyObject *object, igraph_real_t *v); int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result); +int igraphmodule_PyObject_to_max_results_t(PyObject *object, igraph_int_t *v); + int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph_bool_t need_non_negative); int igraphmodule_PyObject_float_to_vector_t(PyObject *list, igraph_vector_t *v); int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v); -int igraphmodule_PyObject_to_vector_long_t(PyObject *list, igraph_vector_long_t *v); int igraphmodule_PyObject_to_vector_bool_t(PyObject *list, igraph_vector_bool_t *v); int igraphmodule_PyObject_to_vector_ptr_t(PyObject *list, igraph_vector_ptr_t *v, igraph_bool_t need_non_negative); +int igraphmodule_PyObject_to_vector_int_ptr_t(PyObject *list, igraph_vector_ptr_t *v); +int igraphmodule_PyObject_to_vector_list_t(PyObject *list, igraph_vector_list_t *v); +int igraphmodule_PyObject_to_vector_int_list_t(PyObject *list, igraph_vector_int_list_t *v); + +int igraphmodule_PyObject_to_edgelist( + PyObject *list, igraph_vector_int_t *v, igraph_t *graph, + igraph_bool_t *list_is_owned +); + +int igraphmodule_PyObject_to_matrix_t( + PyObject *o, igraph_matrix_t *m, const char *arg_name); +int igraphmodule_PyObject_to_matrix_t_with_minimum_column_count( + PyObject *o, igraph_matrix_t *m, int min_cols, const char *arg_name); +int igraphmodule_PyObject_to_matrix_int_t( + PyObject *o, igraph_matrix_int_t *m, const char *arg_name); +int igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count( + PyObject *o, igraph_matrix_int_t *m, int min_cols, const char *arg_name); -int igraphmodule_PyObject_to_edgelist(PyObject *list, igraph_vector_t *v, igraph_t *graph); - -int igraphmodule_PyList_to_matrix_t(PyObject *o, igraph_matrix_t *m); PyObject* igraphmodule_strvector_t_to_PyList(igraph_strvector_t *v); int igraphmodule_PyList_to_strvector_t(PyObject* v, igraph_strvector_t *result); +int igraphmodule_PyList_to_existing_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_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, - igraph_integer_t *single_vid); -int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *graph); +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_int_t *vid, igraph_t *graph); +int igraphmodule_PyObject_to_optional_vid(PyObject *o, igraph_int_t *vid, igraph_t *graph); +int igraphmodule_PyObject_to_vid_list(PyObject *o, igraph_vector_int_t *vids, igraph_t *graph); +int igraphmodule_PyObject_to_vs_t( + PyObject *o, igraph_vs_t *vs, igraph_t *graph, + igraph_bool_t *return_single, igraph_int_t *single_vid +); +int igraphmodule_PyObject_to_eid(PyObject *o, igraph_int_t *eid, igraph_t *graph); int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, igraph_bool_t *return_single); int igraphmodule_PyObject_to_attribute_values(PyObject *o, @@ -110,13 +154,14 @@ int igraphmodule_PyObject_to_attribute_values(PyObject *o, int type, igraph_real_t def); int igraphmodule_PyObject_to_drl_options_t(PyObject *obj, - igraph_layout_drl_options_t *options); + igraph_layout_drl_options_t *options); int igraphmodule_PyObject_to_attribute_combination_t(PyObject* object, igraph_attribute_combination_t *type); int igraphmodule_PyObject_to_eigen_algorithm_t(PyObject *object, igraph_eigen_algorithm_t *a); int igraphmodule_PyObject_to_eigen_which_t(PyObject *object, igraph_eigen_which_t *w); +int igraphmodule_PyObject_to_vpath_or_epath(PyObject *object, igraph_bool_t *use_edges); /* Conversion from attributes to igraph types */ @@ -124,24 +169,33 @@ int igraphmodule_attrib_to_vector_t(PyObject *o, igraphmodule_GraphObject *self, igraph_vector_t **vptr, int attr_type); int igraphmodule_attrib_to_vector_int_t(PyObject *o, igraphmodule_GraphObject *self, igraph_vector_int_t **vptr, int attr_type); -int igraphmodule_attrib_to_vector_long_t(PyObject *o, igraphmodule_GraphObject *self, - igraph_vector_long_t **vptr, int attr_type); int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject *self, igraph_vector_bool_t **vptr, int attr_type); /* Conversion from igraph types to PyObjects */ +PyObject* igraphmodule_integer_t_to_PyObject(igraph_int_t value); +PyObject* igraphmodule_real_t_to_PyObject(igraph_real_t value, igraphmodule_conv_t type); + 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_t_to_PyList(const igraph_vector_t *v, igraphmodule_conv_t type); +PyObject* igraphmodule_vector_t_to_PyTuple(const igraph_vector_t *v, igraphmodule_conv_t type); +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_of_fixed_length_tuples( + const igraph_vector_int_t *v, Py_ssize_t tuple_len); +PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_int_t nanvalue); PyObject* igraphmodule_vector_ptr_t_to_PyList(const igraph_vector_ptr_t *v, igraphmodule_conv_t type); +PyObject* igraphmodule_vector_int_ptr_t_to_PyList(const igraph_vector_ptr_t *v); +PyObject* igraphmodule_vector_list_t_to_PyList(const igraph_vector_list_t *v); +PyObject* igraphmodule_vector_int_list_t_to_PyList(const igraph_vector_int_list_t *v); +PyObject* igraphmodule_vector_int_list_t_to_PyList_of_tuples(const igraph_vector_int_list_t *v); +PyObject* igraphmodule_graph_list_t_to_PyList(igraph_graph_list_t *v, PyTypeObject *type); PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v); -PyObject* igraphmodule_vector_long_t_to_PyList(const igraph_vector_long_t *v); PyObject* igraphmodule_matrix_t_to_PyList(const igraph_matrix_t *m, igraphmodule_conv_t type); +PyObject* igraphmodule_matrix_int_t_to_PyList(const igraph_matrix_int_t *m); +PyObject* igraphmodule_matrix_list_t_to_PyList(const igraph_matrix_list_t *m); #endif diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c new file mode 100644 index 000000000..08a7fedea --- /dev/null +++ b/src/_igraph/dfsiter.c @@ -0,0 +1,271 @@ +/* -*- 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 "convert.h" +#include "common.h" +#include "dfsiter.h" +#include "error.h" +#include "pyhelpers.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* self; + igraph_int_t no_of_nodes, r; + + self = (igraphmodule_DFSIterObject*) PyType_GenericNew(igraphmodule_DFSIterType, 0, 0); + if (!self) { + return NULL; + } + + Py_INCREF(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"); + return NULL; + } + + no_of_nodes = igraph_vcount(&g->g); + 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(&self->stack, 100)) { + PyErr_SetString(PyExc_MemoryError, "out of memory"); + return NULL; + } + + if (igraph_vector_int_init(&self->neis, 0)) { + PyErr_SetString(PyExc_MemoryError, "out of memory"); + igraph_stack_int_destroy(&self->stack); + return NULL; + } + + if (PyLong_Check(root)) { + if (igraphmodule_PyObject_to_integer_t(root, &r)) { + igraph_stack_int_destroy(&self->stack); + igraph_vector_int_destroy(&self->neis); + return NULL; + } + } else { + r = ((igraphmodule_VertexObject*)root)->idx; + } + + /* push the root onto the stack */ + 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; + } + self->visited[r] = 1; + + if (!igraph_is_directed(&g->g)) { + mode = IGRAPH_ALL; + } + + self->mode = mode; + self->advanced = advanced; + + RC_ALLOC("DFSIter", self); + + return (PyObject*)self; +} + +/** + * \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. + */ +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)); + return 0; +} + +/** + * \ingroup python_interface_dfsiter + * \brief Clears the iterator's subobject (before deallocation) + */ +static int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { + PyObject_GC_UnTrack(self); + + Py_CLEAR(self->gref); + + igraph_stack_int_destroy(&self->stack); + igraph_vector_int_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 + */ +static void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self) { + RC_DEALLOC("DFSIter", self); + igraphmodule_DFSIter_clear(self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_DFSIterType); +} + +static PyObject* igraphmodule_DFSIter_iter(igraphmodule_DFSIterObject* self) { + Py_INCREF(self); + return (PyObject*)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_int_t parent_out, dist_out, vid_out; + igraph_bool_t any = false; + + /* nothing on the stack, end of iterator */ + 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_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_int_t parent = igraph_stack_int_pop(&self->stack); + igraph_int_t dist = igraph_stack_int_pop(&self->stack); + igraph_int_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_int_t i, n; + + /* 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, /* loops = */ 0, /* multiple = */ 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + n = igraph_vector_int_size(&self->neis); + for (i = 0; i < n; i++) { + igraph_int_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_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; + } + break; + } + } + /* no new neighbors, end of subtree */ + if (!any) { + igraph_stack_int_pop(&self->stack); + igraph_stack_int_pop(&self->stack); + igraph_stack_int_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("NnN", vertexobj, (Py_ssize_t) dist_out, parentobj); + } else { + return vertexobj; + } +} + +PyDoc_STRVAR( + igraphmodule_DFSIter_doc, + "igraph DFS iterator object" +); + +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 new file mode 100644 index 000000000..c996fd744 --- /dev/null +++ b/src/_igraph/dfsiter.h @@ -0,0 +1,54 @@ +/* -*- 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 IGRAPHMODULE_DFSITER_H +#define IGRAPHMODULE_DFSITER_H + +#include "preamble.h" + +#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_int_t stack; + igraph_vector_int_t neis; + igraph_t *graph; + char *visited; + igraph_neimode_t mode; + igraph_bool_t advanced; +} igraphmodule_DFSIterObject; + +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/edgeobject.c b/src/_igraph/edgeobject.c similarity index 70% rename from src/edgeobject.c rename to src/_igraph/edgeobject.c index ab0d71b77..0f5e54171 100644 --- a/src/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -1,33 +1,33 @@ /* -*- mode: C -*- */ /* vim: set ts=2 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "attributes.h" +#include "convert.h" #include "edgeobject.h" #include "error.h" #include "graphobject.h" #include "pyhelpers.h" -#include "py2compat.h" #include "vertexobject.h" /** @@ -35,17 +35,16 @@ * \defgroup python_interface_edge Edge object */ -PyTypeObject igraphmodule_EdgeType; +PyTypeObject* igraphmodule_EdgeType; + +PyObject* igraphmodule_Edge_attributes(igraphmodule_EdgeObject* self, PyObject* _null); /** * \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; } /** @@ -55,7 +54,7 @@ int igraphmodule_Edge_Check(PyObject* obj) { * exception and returns zero if the edge object is invalid. */ int igraphmodule_Edge_Validate(PyObject* obj) { - igraph_integer_t n; + igraph_int_t n; igraphmodule_EdgeObject *self; igraphmodule_GraphObject *graph; @@ -92,36 +91,43 @@ int igraphmodule_Edge_Validate(PyObject* obj) { * \brief Allocates a new Python edge object * \param gref weak reference of the \c igraph.Graph being referenced by the edge * \param idx the index of the edge - * + * * \warning \c igraph references its edges by indices, so if * you delete some edges from the graph, the edge indices will * change. Since the \c igraph.Edge objects do not follow these * changes, your existing edge objects will point to elsewhere * (or they might even get invalidated). */ -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(gref); - self->gref=gref; - self->idx=idx; - self->hash=-1; - } - return (PyObject*)self; +PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_int_t idx) { + return PyObject_CallFunction((PyObject*) 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 */ -int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self) { - PyObject *tmp; +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_int_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; - tmp=(PyObject*)self->gref; - self->gref=NULL; - Py_XDECREF(tmp); + 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; } @@ -130,64 +136,45 @@ 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) { - igraphmodule_Edge_clear(self); - +static void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self) { RC_DEALLOC("Edge", self); - - PyObject_Del((PyObject*)self); + Py_CLEAR(self->gref); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_EdgeType); } /** \ingroup python_interface_edge * \brief Formats an \c igraph.Edge object as a string - * + * * \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; -#ifndef IGRAPH_PYTHON3 - PyObject *grepr, *drepr; -#endif - attrs = igraphmodule_Edge_attributes(self); + attrs = igraphmodule_Edge_attributes(self, NULL); if (attrs == 0) return NULL; -#ifdef IGRAPH_PYTHON3 - 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); -#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; +static 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 = igraphmodule_integer_t_to_PyObject(self->idx); if (index_o == 0) return -1; @@ -215,7 +202,7 @@ Py_hash_t 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; @@ -258,43 +245,46 @@ 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); +PyObject* igraphmodule_Edge_attribute_names(igraphmodule_EdgeObject* self, PyObject* Py_UNUSED(_null)) { + return self->gref ? igraphmodule_Graph_edge_attributes(self->gref, NULL) : NULL; } /** \ingroup python_interface_edge * \brief Returns a dict with attribute names and values */ -PyObject* igraphmodule_Edge_attributes(igraphmodule_EdgeObject* self) { +PyObject* igraphmodule_Edge_attributes(igraphmodule_EdgeObject* self, PyObject* Py_UNUSED(_null)) { 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, NULL); if (!names) { Py_DECREF(dict); return NULL; } n = PyList_Size(names); - for (i=0; igref; PyObject* result; int r; - + if (!igraphmodule_Edge_Validate((PyObject*)self)) return -1; @@ -382,7 +380,7 @@ int igraphmodule_Edge_set_attribute(igraphmodule_EdgeObject* self, PyObject* k, if (v==NULL) // we are deleting attribute return PyDict_DelItem(((PyObject**)o->g.attr)[2], k); - + result=PyDict_GetItem(((PyObject**)o->g.attr)[2], k); if (result) { /* result is a list, so set the element with index self->idx */ @@ -398,28 +396,28 @@ int igraphmodule_Edge_set_attribute(igraphmodule_EdgeObject* self, PyObject* k, if (r == -1) { Py_DECREF(v); } return r; } - + /* 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_ecount(&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)[2], k, result) == -1) { @@ -429,7 +427,7 @@ int igraphmodule_Edge_set_attribute(igraphmodule_EdgeObject* self, PyObject* k, Py_DECREF(result); /* compensating for PyDict_SetItem */ return 0; } - + return -1; } @@ -439,15 +437,17 @@ int igraphmodule_Edge_set_attribute(igraphmodule_EdgeObject* self, PyObject* k, */ PyObject* igraphmodule_Edge_get_from(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_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 PyInt_FromLong((long int)from); + + return igraphmodule_integer_t_to_PyObject(from); } /** @@ -456,7 +456,7 @@ PyObject* igraphmodule_Edge_get_from(igraphmodule_EdgeObject* self, void* closur */ PyObject* igraphmodule_Edge_get_source_vertex(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) return NULL; @@ -474,15 +474,17 @@ PyObject* igraphmodule_Edge_get_source_vertex(igraphmodule_EdgeObject* self, voi */ 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)) + igraph_int_t from, to; + + 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 PyInt_FromLong((long)to); + + return igraphmodule_integer_t_to_PyObject(to); } /** @@ -491,7 +493,7 @@ PyObject* igraphmodule_Edge_get_to(igraphmodule_EdgeObject* self, void* closure) */ PyObject* igraphmodule_Edge_get_target_vertex(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; if (!igraphmodule_Edge_Validate((PyObject*)self)) return NULL; @@ -508,40 +510,50 @@ 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 igraphmodule_integer_t_to_PyObject(self->idx); } /** * \ingroup python_interface_edge - * Returns the edge index as an igraph_integer_t + * Returns the edge index as an igraph_int_t */ -igraph_integer_t igraphmodule_Edge_get_index_igraph_integer(igraphmodule_EdgeObject* self) { +igraph_int_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 */ PyObject* igraphmodule_Edge_get_tuple(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; - - if (!igraphmodule_Edge_Validate((PyObject*)self)) + igraph_int_t from, to; + PyObject *from_o, *to_o, *result; + + 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; } /** @@ -550,7 +562,7 @@ PyObject* igraphmodule_Edge_get_tuple(igraphmodule_EdgeObject* self, void* closu */ PyObject* igraphmodule_Edge_get_vertex_tuple(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; - igraph_integer_t from, to; + igraph_int_t from, to; PyObject *from_o, *to_o; if (!igraphmodule_Edge_Validate((PyObject*)self)) @@ -585,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; \ - 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); \ - 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); \ @@ -613,16 +632,18 @@ 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" \ + METHODNAME "(*args, **kwds)\n--\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" \ + METHODNAME "(*args, **kwds)\n--\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 @@ -631,17 +652,17 @@ GRAPH_PROXY_METHOD(is_mutual, "is_mutual"); PyMethodDef igraphmodule_Edge_methods[] = { {"attributes", (PyCFunction)igraphmodule_Edge_attributes, METH_NOARGS, - "attributes() -> dict\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" - "Returns the list of edge attribute names\n" + METH_NOARGS, + "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" + "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" @@ -692,46 +713,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,16 +722,45 @@ 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" - " 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 -}; - + " >>> print(e[\"weight\"]) #doctest: +SKIP\n" + " 2\n" + "\n" + "@ivar source: Source vertex index of this edge\n" + "@ivar source_vertex: Source vertex of this edge\n" + "@ivar target: Target vertex index of this edge\n" + "@ivar target_vertex: Target vertex of this edge\n" + "@ivar tuple: Source and target vertex index of this edge as a tuple\n" + "@ivar vertex_tuple: Source and target vertex of this edge as a tuple\n" + "@ivar index: Index of this edge\n" + "@ivar graph: The graph the edge belongs to\t" +); + +int igraphmodule_Edge_register_type() { + PyType_Slot slots[] = { + { Py_tp_init, igraphmodule_Edge_init }, + { 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/edgeobject.h b/src/_igraph/edgeobject.h similarity index 52% rename from src/edgeobject.h rename to src/_igraph/edgeobject.h index 837e145e1..039a6e20f 100644 --- a/src/edgeobject.h +++ b/src/_igraph/edgeobject.h @@ -1,31 +1,31 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_EDGEOBJECT_H -#define PYTHON_EDGEOBJECT_H +#ifndef IGRAPHMODULE_EDGEOBJECT_H +#define IGRAPHMODULE_EDGEOBJECT_H + +#include "preamble.h" -#include #include "graphobject.h" -#include "py2compat.h" /** * \ingroup python_interface_edge @@ -34,25 +34,16 @@ typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; - igraph_integer_t idx; - Py_hash_t hash; + igraph_int_t idx; + long hash; } igraphmodule_EdgeObject; -int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self); -void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self); - -int igraphmodule_Edge_Check(PyObject *obj); -int igraphmodule_Edge_Validate(PyObject *obj); +extern PyTypeObject* igraphmodule_EdgeType; -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_igraph_integer(igraphmodule_EdgeObject* self); -long igraphmodule_Edge_get_index_long(igraphmodule_EdgeObject* self); -PyObject* igraphmodule_Edge_update_attributes(PyObject* self, PyObject* args, - PyObject* kwds); +int igraphmodule_Edge_register_type(void); -extern PyTypeObject igraphmodule_EdgeType; +int igraphmodule_Edge_Check(PyObject* obj); +PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_int_t idx); +igraph_int_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self); #endif diff --git a/src/edgeseqobject.c b/src/_igraph/edgeseqobject.c similarity index 57% rename from src/edgeseqobject.c rename to src/_igraph/edgeseqobject.c index deeaef86f..13f4364f9 100644 --- a/src/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -1,23 +1,23 @@ /* -*- mode: C -*- */ /* vim: set ts=2 sts=2 sw=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @@ -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) @@ -38,28 +37,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; } /** @@ -71,76 +58,87 @@ 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_t v; - if (igraph_vector_copy(&v, o->es.data.vecptr)) { + igraph_vector_int_t v; + if (igraph_vector_int_init_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; } 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; } - /** * \ingroup python_interface_edgeseq * \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 */ 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); + igraph_int_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_t v; - igraph_integer_t n = igraph_ecount(&((igraphmodule_GraphObject*)g)->g); - if (igraphmodule_PyObject_to_vector_t(esobj, &v, 1)) + /* We selected multiple edges */ + igraph_vector_int_t v; + igraph_int_t n = igraph_ecount(&((igraphmodule_GraphObject*)g)->g); + 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; @@ -155,32 +153,36 @@ 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_CLEAR(self->gref); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_EdgeSeqType); } /** * \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; + igraph_int_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 (int)result; + + return result; } /** @@ -190,27 +192,31 @@ int igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject* self) { PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, Py_ssize_t i) { igraph_t *g; - igraph_integer_t idx = -1; - - if (!self->gref) return NULL; - g=&GET_GRAPH(self); + igraph_int_t idx = -1; + + if (!self->gref) { + return NULL; + } + + g = &GET_GRAPH(self); + switch (igraph_es_type(&self->es)) { case IGRAPH_ES_ALL: if (i < 0) { i = igraph_ecount(g) + i; } if (i >= 0 && i < igraph_ecount(g)) { - idx = (igraph_integer_t)i; + idx = i; } break; 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)) { - idx = (igraph_integer_t)VECTOR(*self->es.data.vecptr)[i]; + if (i >= 0 && i < igraph_vector_int_size(self->es.data.vecptr)) { + idx = VECTOR(*self->es.data.vecptr)[i]; } break; @@ -220,19 +226,28 @@ PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, } break; - case IGRAPH_ES_SEQ: + case IGRAPH_ES_RANGE: if (i < 0) { - i = self->es.data.seq.to - self->es.data.seq.from + i; + i = self->es.data.range.end - self->es.data.range.start + 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; + if (i >= 0 && i < self->es.data.range.end - self->es.data.range.start) { + idx = self->es.data.range.start + i; } 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"); return NULL; @@ -244,8 +259,8 @@ PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, /** \ingroup python_interface_edgeseq * \brief Returns the list of attribute names */ -PyObject* igraphmodule_EdgeSeq_attribute_names(igraphmodule_EdgeSeqObject* self) { - return igraphmodule_Graph_edge_attributes(self->gref); +PyObject* igraphmodule_EdgeSeq_attribute_names(igraphmodule_EdgeSeqObject* self, PyObject* Py_UNUSED(_null)) { + return igraphmodule_Graph_edge_attributes(self->gref, NULL); } /** \ingroup python_interface_edgeseq @@ -254,58 +269,100 @@ 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)) + 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); + n = igraph_vector_int_size(self->es.data.vecptr); result = PyList_New(n); - if (!result) return 0; + if (!result) { + return 0; + } + + for (i = 0; i < n; i++) { + item = PyList_GetItem(values, VECTOR(*self->es.data.vecptr)[i]); + if (!item) { + Py_DECREF(result); + return 0; + } - for (i=0; ies.data.vecptr)[i]); Py_INCREF(item); - PyList_SET_ITEM(result, i, item); + + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } } break; - case IGRAPH_ES_SEQ: - n = self->es.data.seq.to - self->es.data.seq.from; + case IGRAPH_ES_RANGE: + n = self->es.data.range.end - self->es.data.range.start; result = PyList_New(n); - if (!result) return 0; + if (!result) { + return 0; + } + + for (i = 0; i < n; i++) { + item = PyList_GetItem(values, self->es.data.range.start + i); + if (!item) { + Py_DECREF(result); + return 0; + } - for (i=0; ies.data.seq.from+i); 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,36 +373,54 @@ PyObject* igraphmodule_EdgeSeq_get_attribute_values(igraphmodule_EdgeSeqObject* return result; } -PyObject* igraphmodule_EdgeSeq_is_all(igraphmodule_EdgeSeqObject* self) { - if (igraph_es_is_all(&self->es)) +PyObject* igraphmodule_EdgeSeq_is_all(igraphmodule_EdgeSeqObject* self, PyObject* Py_UNUSED(_null)) { + 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); } @@ -356,40 +431,51 @@ 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; - long i, j, n, no_of_edges; - + igraph_vector_int_t es; + Py_ssize_t i, j, n; + igraph_int_t no_of_edges; + 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; } - 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; 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_EdgeSeq_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_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; @@ -399,27 +485,41 @@ 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, 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 = 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 */ 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_int_t n2 = igraph_ecount(&gr->g); list = PyList_New(n2); - if (list == 0) { igraph_vector_destroy(&es); return -1; } - for (i=0; ies); + igraph_vector_int_t v, v2; + 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; ies); igraph_es_none(&result->es); /* We can simply bail out here */ - return (PyObject*)result; + return (PyObject*) result; } else if (PyCallable_Check(item)) { /* Call the callable for every edge in the current sequence to * determine what's up */ - igraph_bool_t was_excluded = 0; - igraph_vector_t v; + igraph_bool_t was_excluded = false; + igraph_vector_int_t v; - if (igraph_vector_init(&v, 0)) { + if (igraph_vector_int_init(&v, 0)) { igraphmodule_handle_igraph_error(); return 0; } m = PySequence_Size((PyObject*)result); - for (j=0; jes); 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); - } else if (PyInt_Check(item)) { + 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)) { - igraphmodule_handle_igraph_error(); - return 0; - } - if (igraph_vector_init(&v2, 0)) { - igraph_vector_destroy(&v); + if (igraph_vector_int_init(&v, 0)) { igraphmodule_handle_igraph_error(); return 0; } - if (igraph_es_as_vector(&gr->g, self->es, &v2)) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); - igraphmodule_handle_igraph_error(); - return 0; + + if (!working_on_whole_graph) { + /* Extract the current vertex sequence into a vector */ + 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_int_destroy(&v); + igraph_vector_int_destroy(&v2); + igraphmodule_handle_igraph_error(); + return 0; + } + m = igraph_vector_int_size(&v2); + } else { + /* v2 left uninitialized, we are not going to use it as it would + * simply contain integers from 0 to ecount(g)-1 */ + m = igraph_ecount(&gr->g); } - m = igraph_vector_size(&v2); - for (; i= m || idx < 0) { + Py_DECREF(result); PyErr_SetString(PyExc_ValueError, "edge index out of range"); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } return NULL; } - if (igraph_vector_push_back(&v, VECTOR(v2)[idx])) { + if (igraph_vector_int_push_back(&v, working_on_whole_graph ? idx : VECTOR(v2)[idx])) { Py_DECREF(result); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } return NULL; } } - igraph_vector_destroy(&v2); + + if (!working_on_whole_graph) { + 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; - + PyObject *iter = NULL, *item2 = NULL; + /* 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); - igraphmodule_handle_igraph_error(); - return 0; - } - if (igraph_es_as_vector(&gr->g, self->es, &v2)) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); - igraphmodule_handle_igraph_error(); - return 0; + if (!working_on_whole_graph) { + /* Extract the current vertex sequence into a vector */ + 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_int_destroy(&v); + igraph_vector_int_destroy(&v2); + igraphmodule_handle_igraph_error(); + return 0; + } + m = igraph_vector_int_size(&v2); + } else { + /* v2 left uninitialized, we are not going to use it as it would + * simply contain integers from 0 to ecount(g)-1 */ + m = igraph_ecount(&gr->g); } - m = igraph_vector_size(&v2); /* Create an appropriate iterator */ if (PySlice_Check(item)) { @@ -708,10 +883,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, m, &start, &stop, &step, &sl) == 0); if (ok) { range = igraphmodule_PyRange_create(start, stop, step); ok = (range != 0); @@ -722,8 +894,10 @@ 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); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } PyErr_SetString(PyExc_TypeError, "error while converting slice to iterator"); Py_DECREF(result); return 0; @@ -735,54 +909,65 @@ 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); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } PyErr_SetString(PyExc_TypeError, "invalid edge filter among positional arguments"); Py_DECREF(result); return 0; } /* Do the iteration */ - while ((item2=PyIter_Next(iter)) != 0) { - if (PyInt_Check(item2)) { - long idx = PyInt_AsLong(item2); + 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); - if (idx >= m || idx < 0) { + } else { + Py_DECREF(item2); + if (igraph_idx >= m || igraph_idx < 0) { 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); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } return NULL; } - if (igraph_vector_push_back(&v, VECTOR(v2)[idx])) { + if (igraph_vector_int_push_back(&v, working_on_whole_graph ? igraph_idx : VECTOR(v2)[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); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } return NULL; } - } else { - /* We simply ignore elements that we don't know */ - Py_DECREF(item2); } } + /* Deallocate stuff */ - igraph_vector_destroy(&v2); + if (!working_on_whole_graph) { + 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; } + 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); } } @@ -797,74 +982,41 @@ 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()\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)\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)\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()\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)\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(*args, **kwds)\n--\n\n" "For internal use only.\n" }, {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 @@ -877,26 +1029,26 @@ PyObject* igraphmodule_EdgeSeq_get_graph(igraphmodule_EdgeSeqObject* self, /** * \ingroup python_interface_edgeseq - * Returns the indices of the edges in this edge sequence + * Returns the indices of the edges in this edge sequence */ 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; } @@ -914,58 +1066,47 @@ 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.core.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 */ - "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 */ +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" +); + +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/edgeseqobject.h b/src/_igraph/edgeseqobject.h similarity index 53% rename from src/edgeseqobject.h rename to src/_igraph/edgeseqobject.h index 7a410b9e1..d806c23f0 100644 --- a/src/edgeseqobject.h +++ b/src/_igraph/edgeseqobject.h @@ -1,29 +1,30 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_EDGESEQOBJECT_H -#define PYTHON_EDGESEQOBJECT_H +#ifndef IGRAPHMODULE_EDGESEQOBJECT_H +#define IGRAPHMODULE_EDGESEQOBJECT_H + +#include "preamble.h" -#include #include "graphobject.h" /** @@ -38,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); - -int 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; -extern PyTypeObject igraphmodule_EdgeSeqType; +int igraphmodule_EdgeSeq_Check(PyObject* obj); +int igraphmodule_EdgeSeq_register_type(void); #endif diff --git a/src/error.c b/src/_igraph/error.c similarity index 63% rename from src/error.c rename to src/_igraph/error.c index 3f677b282..b6940a27e 100644 --- a/src/error.c +++ b/src/_igraph/error.c @@ -1,26 +1,28 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "error.h" +#include "pyhelpers.h" + #include /** \ingroup python_interface_errors @@ -31,21 +33,22 @@ PyObject* igraphmodule_InternalError; /** * \ingroup python_interface_errors * \brief Generic error handler for internal \c igraph errors. - * + * * Since now \c igraph supports error handler functions, a special * function called \c igraphmodule_igraph_error_hook is responsible * for providing a meaningful error message. If it fails (or it isn't * even called), this function will provide a default error message. - * + * * \return Always returns \c NULL, and all callers are advised to pass this * \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,10 +59,23 @@ 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) { char buf[4096]; - sprintf(buf, "%s at %s:%i", reason, file, line); - PyErr_Warn(PyExc_RuntimeWarning, buf); + char end; + size_t len = strlen(reason); + const char* separator = " "; + + if (len == 0) { + separator = ""; + } else { + end = reason[len - 1]; + if (end != '.' && end != '?' && end != '!') { + separator = ". "; + } + } + + snprintf(buf, sizeof(buf), "%s%sLocation: %s:%i", reason, separator, file, line); + PY_IGRAPH_WARN(buf); } /** @@ -67,18 +83,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, igraph_error_t 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; + + /* 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 = "."; + } + } - sprintf(buf, "Error at %s:%i: %s, %s", file, line, reason, - igraph_strerror(igraph_errno)); + 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 */ diff --git a/src/error.h b/src/_igraph/error.h similarity index 86% rename from src/error.h rename to src/_igraph/error.h index 8d54e728c..1265f2918 100644 --- a/src/error.h +++ b/src/_igraph/error.h @@ -1,29 +1,30 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_ERROR_H -#define PYTHON_ERROR_H +#ifndef IGRAPHMODULE_ERROR_H +#define IGRAPHMODULE_ERROR_H + +#include "preamble.h" -#include #include /** \defgroup python_interface_errors Error handling @@ -31,9 +32,9 @@ PyObject* igraphmodule_handle_igraph_error(void); void igraphmodule_igraph_warning_hook(const char *reason, const char *file, - int line, int igraph_errno); + int line); void igraphmodule_igraph_error_hook(const char *reason, const char *file, - int line, int igraph_errno); + int line, igraph_error_t igraph_errno); extern PyObject* igraphmodule_InternalError; diff --git a/src/_igraph/filehandle.c b/src/_igraph/filehandle.c new file mode 100644 index 000000000..b5add4010 --- /dev/null +++ b/src/_igraph/filehandle.c @@ -0,0 +1,190 @@ +/* -*- mode: C -*- */ +/* + IGraph library. + Copyright (C) 2010-2023 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 "filehandle.h" +#include "pyhelpers.h" + +#ifndef PYPY_VERSION +static int igraphmodule_i_filehandle_init_cpython_3(igraphmodule_filehandle_t* handle, + PyObject* object, char* mode) { + int fp; + + if (object == 0 || PyLong_Check(object)) { + PyErr_SetString(PyExc_TypeError, "string or file-like object expected"); + 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 + * string now and mark that we opened the file ourselves (so we need + * to close it when igraphmodule_filehandle_destroy is invoked). */ + 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 */ + 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); + } + + /* At this stage, handle->object is something we can handle. + * We have to call PyObject_AsFileDescriptor instead + * and then fdopen() it to get the corresponding FILE* object. + */ + fp = PyObject_AsFileDescriptor(handle->object); + if (fp == -1) { + igraphmodule_filehandle_destroy(handle); + /* 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); + /* This already called Py_DECREF(handle->object), no need to call it */ + PyErr_SetString(PyExc_RuntimeError, "fdopen() failed unexpectedly"); + return 1; + } + + return 0; +} +#else /* PYPY_VERSION */ +static int igraphmodule_i_filehandle_init_pypy_3(igraphmodule_filehandle_t* handle, + PyObject* object, char* mode) { + int fp; + + if (object == 0 || PyLong_Check(object)) { + PyErr_SetString(PyExc_TypeError, "string or file-like object expected"); + 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 + * string now and mark that we opened the file ourselves (so we need + * to close it when igraphmodule_filehandle_destroy is invoked). */ + 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 */ + 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); + } + + /* At this stage, handle->object is something we can handle. + * We have to call PyObject_AsFileDescriptor instead + * and then fdopen() it to get the corresponding FILE* object. + */ + fp = PyObject_AsFileDescriptor(handle->object); + if (fp == -1) { + igraphmodule_filehandle_destroy(handle); + /* 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); + /* This already called Py_DECREF(handle->object), no need to call it */ + PyErr_SetString(PyExc_RuntimeError, "fdopen() failed unexpectedly"); + return 1; + } + + return 0; +} +#endif + +/** + * \ingroup python_interface_filehandle + * \brief Constructs a new file handle object from a Python object. + * + * \return 0 if everything was OK, 1 otherwise. An appropriate Python + * exception is raised in this case. + */ +int igraphmodule_filehandle_init(igraphmodule_filehandle_t* handle, + PyObject* object, char* mode) { +#ifdef PYPY_VERSION + return igraphmodule_i_filehandle_init_pypy_3(handle, object, mode); +#else + return igraphmodule_i_filehandle_init_cpython_3(handle, object, mode); +#endif +} + +/** + * \ingroup python_interface_filehandle + * \brief Destroys the file handle object. + */ +void igraphmodule_filehandle_destroy(igraphmodule_filehandle_t* handle) { + PyObject *exc_type = 0, *exc_value = 0, *exc_traceback = 0; + + if (handle->fp != 0) { + fflush(handle->fp); + if (handle->need_close && !handle->object) { + fclose(handle->fp); + } + handle->fp = 0; + } + + if (handle->object != 0) { + /* 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 (igraphmodule_PyFile_Close(handle->object)) { + PyErr_WriteUnraisable(Py_None); + } + } + Py_DECREF(handle->object); + PyErr_Restore(exc_type, exc_value, exc_traceback); + exc_type = exc_value = exc_traceback = 0; + handle->object = 0; + } + + handle->need_close = 0; +} + +/** + * \ingroup python_interface_filehandle + * \brief Returns the file encapsulated by the given \c igraphmodule_filehandle_t. + */ +FILE* igraphmodule_filehandle_get(const igraphmodule_filehandle_t* handle) { + return handle->fp; +} diff --git a/src/filehandle.h b/src/_igraph/filehandle.h similarity index 89% rename from src/filehandle.h rename to src/_igraph/filehandle.h index 68bf3c3be..f374a59a8 100644 --- a/src/filehandle.h +++ b/src/_igraph/filehandle.h @@ -1,29 +1,30 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2010-2012 Tamas Nepusz - + Copyright (C) 2010-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_FILEHANDLE_H -#define PYTHON_FILEHANDLE_H +#ifndef IGRAPHMODULE_FILEHANDLE_H +#define IGRAPHMODULE_FILEHANDLE_H + +#include "preamble.h" -#include #include /** diff --git a/src/_igraph/force_cpp_linker.cpp b/src/_igraph/force_cpp_linker.cpp new file mode 100644 index 000000000..45d31ee9e --- /dev/null +++ b/src/_igraph/force_cpp_linker.cpp @@ -0,0 +1,3 @@ +/* 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 */ diff --git a/src/graphobject.c b/src/_igraph/graphobject.c similarity index 60% rename from src/graphobject.c rename to src/_igraph/graphobject.c index 9d0976584..b6c14a340 100644 --- a/src/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1,21 +1,21 @@ /* vim:set ts=4 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @@ -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" @@ -31,28 +32,24 @@ #include "graphobject.h" #include "indexing.h" #include "memory.h" -#include "py2compat.h" #include "pyhelpers.h" #include "vertexseqobject.h" #include -PyTypeObject igraphmodule_GraphType; +PyTypeObject* igraphmodule_GraphType; -#define CREATE_GRAPH(py_graph, c_graph) { \ - py_graph = (igraphmodule_GraphObject *) Py_TYPE(self)->tp_alloc(Py_TYPE(self), 0); \ - if (py_graph != NULL) { \ - igraphmodule_Graph_init_internal(py_graph); \ - py_graph->g = (c_graph); \ - } \ - RC_ALLOC("Graph", py_graph); \ -} #define CREATE_GRAPH_FROM_TYPE(py_graph, c_graph, py_type) { \ - py_graph = (igraphmodule_GraphObject *) py_type->tp_alloc(py_type, 0); \ - if (py_graph != NULL) { \ - igraphmodule_Graph_init_internal(py_graph); \ - py_graph->g = (c_graph); \ - } \ - RC_ALLOC("Graph", py_graph); \ + py_graph = (igraphmodule_GraphObject*) igraphmodule_Graph_subclass_from_igraph_t( \ + py_type, &c_graph \ + ); \ + if (py_graph == NULL) { igraph_destroy(&c_graph); } \ +} + +#define CREATE_GRAPH(py_graph, c_graph) { \ + py_graph = (igraphmodule_GraphObject*) igraphmodule_Graph_subclass_from_igraph_t( \ + Py_TYPE(self), &c_graph \ + ); \ + if (py_graph == NULL) { igraph_destroy(&c_graph); } \ } /********************************************************************** @@ -62,39 +59,27 @@ 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 - * + * Most of the parameters are processed by \c igraphmodule_Graph_init ; the + * responsibility of \c igraphmodule_Graph_new is only to ensure that the + * \c igraph_t structure is initialized so the user has no chance for messing + * around with an uninitialized structure. + * * \return the new \c igraph.Graph object or NULL if an error occurred. - * + * * \sa igraphmodule_Graph_init * \sa igraph_empty */ @@ -103,14 +88,18 @@ PyObject *igraphmodule_Graph_new(PyTypeObject * type, PyObject * args, { igraphmodule_GraphObject *self; - self = (igraphmodule_GraphObject *) type->tp_alloc(type, 0); + self = (igraphmodule_GraphObject *) ((allocfunc)PyType_GetSlot(type, Py_tp_alloc))(type, 0); RC_ALLOC("Graph", self); - /* don't need it, the constructor will do it */ - /*if (self != NULL) { - igraph_empty(&self->g, 1, 0); - } */ - igraphmodule_Graph_init_internal(self); + /* We need to ensure that self->g is a valid igraph_t pointer in case the + * user somehow manages to sneak in a call to a method of the Graph instance + * between __new__() and __init__() (e.g,. by overriding __init__()). We + * will replace it later with the "proper" graph instance in __init__() if + * needed. */ + if (igraph_empty(&self->g, 0, IGRAPH_UNDIRECTED)) { + igraphmodule_handle_igraph_error(); + return NULL; + } return (PyObject *) self; } @@ -134,31 +123,25 @@ int igraphmodule_Graph_clear(igraphmodule_GraphObject * self) /** * \ingroup python_interface_graph * \brief Support for cyclic garbage collection in Python - * + * * This is necessary because the \c igraph.Graph object contains several * other \c PyObject pointers and they might point back to itself. */ 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]); } } + Py_VISIT(Py_TYPE(self)); + return 0; } @@ -170,13 +153,16 @@ 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); - if (PyCallable_Check(self->destructor)) { + if (self->destructor != NULL && PyCallable_Check(self->destructor)) { r = PyObject_CallObject(self->destructor, NULL); if (r) { Py_DECREF(r); @@ -185,64 +171,181 @@ 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, igraphmodule_GraphType); } /** * \ingroup python_interface_graph * \brief Initializes a new \c igraph object in Python - * + * * This function is called whenever a new \c igraph.Graph object is initialized in * Python (note that initializing is not equal to creating: an object might * be created but not initialized when it is being recovered from a serialized * state). - * + * * 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 */ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "n", "edges", "directed", NULL }; - long int n = 0; - PyObject *edges = NULL, *dir = Py_False; - igraph_vector_t edges_vector; + static char *kwlist[] = { "n", "edges", "directed", "__ptr", NULL }; + PyObject *edges = NULL, *dir = Py_False, *ptr_o = 0; + void* ptr = 0; + Py_ssize_t n = 0; + igraph_vector_int_t edges_vector; + igraph_int_t vcount; + igraph_bool_t edges_vector_owned = false; + int retval = 0; + + self->destructor = NULL; + self->weakreflist = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nOOO!", kwlist, + &n, &edges, &dir, + &PyCapsule_Type, &ptr_o)) + return -1; + + /* Safety check: if ptr is not null, it means that we have been explicitly + * given a pointer to an igraph_t for which we must take ownership. + * This means that n should be zero and edges should not be specified */ + if (ptr_o && (n != 0 || edges != NULL)) { + PyErr_SetString(PyExc_ValueError, "neither n nor edges should be given " + "in the call to Graph.__init__() when the graph is " + "pre-initialized with a C pointer"); + return -1; + } - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lO!O", kwlist, - &n, &PyList_Type, &edges, &dir)) + 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. Since we already created + * one in igraphmodule_Graph_new(), we need to destroy that first */ + ptr = PyCapsule_GetPointer(ptr_o, "__igraph_t"); + if (ptr == 0) { + PyErr_SetString(PyExc_ValueError, "pointer should not be null"); + } else { + igraph_destroy(&self->g); + self->g = *(igraph_t*)ptr; + } + } else { + vcount = 0; + + if (edges) { + /* Caller specified an edge list, so we use igraph_add_vertices() and + * igraph_add_edges() as needed. But first, we have to convert the Python + * list to a igraph_vector_t */ + if (igraphmodule_PyObject_to_edgelist(edges, &edges_vector, 0, &edges_vector_owned)) { + igraphmodule_handle_igraph_error(); + return -1; + } + + if (igraph_vector_int_size(&edges_vector) > 0) { + vcount = igraph_vector_int_max(&edges_vector) + 1; + } + } + + if (vcount < n) { + vcount = n; + } - if (edges && PyList_Check(edges)) { - /* Caller specified an edge list, so we use igraph_create */ - /* We have to convert the Python list to a igraph_vector_t */ - if (igraphmodule_PyObject_to_edgelist(edges, &edges_vector, 0)) { + /* We already have an undirected graph in &self->g. Make it directed first + * if needed */ + if (PyObject_IsTrue(dir) && igraph_to_directed(&self->g, IGRAPH_TO_DIRECTED_ARBITRARY) != IGRAPH_SUCCESS) { igraphmodule_handle_igraph_error(); - return -1; + retval = -1; + goto cleanup; } - if (igraph_create - (&self->g, &edges_vector, (igraph_integer_t) n, PyObject_IsTrue(dir))) { + /* Add the vertices first */ + if (vcount > 0 && igraph_add_vertices(&self->g, vcount, 0) != IGRAPH_SUCCESS) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&edges_vector); - return -1; + retval = -1; + goto cleanup; } - igraph_vector_destroy(&edges_vector); - } - else { - /* No edge list was specified, let's use igraph_empty */ - if (igraph_empty(&self->g, (igraph_integer_t) n, PyObject_IsTrue(dir))) { + /* Then the edges */ + if (edges && igraph_add_edges(&self->g, &edges_vector, 0) != IGRAPH_SUCCESS) { igraphmodule_handle_igraph_error(); - return -1; + retval = -1; + goto cleanup; } } - return 0; +cleanup: + if (edges_vector_owned) { + igraph_vector_int_destroy(&edges_vector); + } + + return retval; +} + +/** \ingroup python_interface_graph + * \brief Creates an \c igraph.Graph subtype from an existing \c igraph_t + * + * 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, 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 +) { + PyObject* result_o; + PyObject* capsule; + PyObject* args; + PyObject* kwds; + + if (!PyType_IsSubtype(type, igraphmodule_GraphType)) { + PyErr_SetString(PyExc_TypeError, "igraph._igraph.GraphBase expected"); + return 0; + } + + capsule = PyCapsule_New(graph, "__igraph_t", 0); + if (capsule == 0) { + return 0; + } + + args = PyTuple_New(0); + if (args == 0) { + Py_DECREF(capsule); + return 0; + } + + kwds = PyDict_New(); + if (kwds == 0) { + Py_DECREF(args); + Py_DECREF(capsule); + return 0; + } + + if (PyDict_SetItemString(kwds, "__ptr", capsule)) { + Py_DECREF(kwds); + Py_DECREF(args); + Py_DECREF(capsule); + return 0; + } + + /* kwds now holds a reference to the capsule so we can release it */ + Py_DECREF(capsule); + + /* Call the type */ + result_o = PyObject_Call((PyObject*) type, args, kwds); + + /* Release args and kwds */ + Py_DECREF(args); + Py_DECREF(kwds); + + return result_o; } /** \ingroup python_interface_graph @@ -254,41 +357,41 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, * See \c api.h for more details. */ PyObject* igraphmodule_Graph_from_igraph_t(igraph_t *graph) { - igraphmodule_GraphObject* result; - PyTypeObject* type = &igraphmodule_GraphType; - - CREATE_GRAPH_FROM_TYPE(result, *graph, type); - - return (PyObject*)result; + return igraphmodule_Graph_subclass_from_igraph_t( + igraphmodule_GraphType, graph + ); } /** \ingroup python_interface_graph * \brief Formats an \c igraph.Graph object in a human-readable format. - * + * * This function is rather simple now, it returns the number of vertices * and edges in a string. - * + * * \return the formatted textual representation as a \c PyObject */ PyObject *igraphmodule_Graph_str(igraphmodule_GraphObject * self) { - if (igraph_is_directed(&self->g)) - return PyString_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)", - (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 - * \brief Creates an exact deep copy of the graph + * \brief Creates a copy of the graph * \return the copy of the graph */ -PyObject *igraphmodule_Graph_copy(igraphmodule_GraphObject * self) +PyObject *igraphmodule_Graph_copy(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { - igraphmodule_GraphObject *result; + igraphmodule_GraphObject *result_o; igraph_t g; if (igraph_copy(&g, &self->g)) { @@ -296,9 +399,9 @@ PyObject *igraphmodule_Graph_copy(igraphmodule_GraphObject * self) return NULL; } - CREATE_GRAPH(result, g); + CREATE_GRAPH(result_o, g); - return (PyObject *) result; + return (PyObject *) result_o; } /********************************************************************** @@ -310,11 +413,9 @@ PyObject *igraphmodule_Graph_copy(igraphmodule_GraphObject * self) * \return the number of vertices as a \c PyObject * \sa igraph_vcount */ -PyObject *igraphmodule_Graph_vcount(igraphmodule_GraphObject * self) +PyObject *igraphmodule_Graph_vcount(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { - 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 @@ -322,30 +423,9 @@ PyObject *igraphmodule_Graph_vcount(igraphmodule_GraphObject * self) * \return the number of edges as a \c PyObject * \sa igraph_ecount */ -PyObject *igraphmodule_Graph_ecount(igraphmodule_GraphObject * self) -{ - PyObject *result; - result = Py_BuildValue("l", (long)igraph_ecount(&self->g)); - return result; -} - -/** \ingroup python_interface_graph - * \brief Checks whether an \c igraph.Graph object is a DAG. - * \return \c True if the graph is directed, \c False otherwise. - * \sa igraph_is_dag - */ -PyObject *igraphmodule_Graph_is_dag(igraphmodule_GraphObject * self) +PyObject *igraphmodule_Graph_ecount(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { - igraph_bool_t res; - - if (igraph_is_dag(&self->g, &res)) { - igraphmodule_handle_igraph_error(); - return NULL; - } - - if (res) - Py_RETURN_TRUE; - Py_RETURN_FALSE; + return igraphmodule_integer_t_to_PyObject(igraph_ecount(&self->g)); } /** \ingroup python_interface_graph @@ -353,7 +433,7 @@ PyObject *igraphmodule_Graph_is_dag(igraphmodule_GraphObject * self) * \return \c True if the graph is directed, \c False otherwise. * \sa igraph_is_directed */ -PyObject *igraphmodule_Graph_is_directed(igraphmodule_GraphObject * self) +PyObject *igraphmodule_Graph_is_directed(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { if (igraph_is_directed(&self->g)) Py_RETURN_TRUE; @@ -370,34 +450,34 @@ PyObject *igraphmodule_Graph_is_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; + igraph_bool_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &matching_o, &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; + 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; + if (matching != 0) { igraph_vector_int_destroy(matching); free(matching); } + return NULL; } - if (igraph_is_matching(&self->g, types, matching, &result)) { - if (matching != 0) { igraph_vector_long_destroy(matching); free(matching); } + if (igraph_is_matching(&self->g, types, matching, &res)) { + 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) + if (res) Py_RETURN_TRUE; Py_RETURN_FALSE; } @@ -412,34 +492,34 @@ 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; + igraph_bool_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &matching_o, &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; + 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; + 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 (igraph_is_maximal_matching(&self->g, types, matching, &res)) { + 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) + if (res) Py_RETURN_TRUE; Py_RETURN_FALSE; } @@ -450,10 +530,10 @@ PyObject *igraphmodule_Graph_is_maximal_matching(igraphmodule_GraphObject* self, * \return \c True if the graph is simple, \c False otherwise. * \sa igraph_is_simple */ -PyObject *igraphmodule_Graph_is_simple(igraphmodule_GraphObject *self) { +PyObject *igraphmodule_Graph_is_simple(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { igraph_bool_t res; - if (igraph_is_simple(&self->g, &res)) { + if (igraph_is_simple(&self->g, &res, IGRAPH_DIRECTED)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -463,6 +543,129 @@ PyObject *igraphmodule_Graph_is_simple(igraphmodule_GraphObject *self) { Py_RETURN_FALSE; } + +/** \ingroup python_interface_graph + * \brief Checks whether an \c igraph.Graph object is a complete graph. + * \return \c True if the graph is complete, \c False otherwise. + * \sa igraph_is_complete + */ +PyObject *igraphmodule_Graph_is_complete(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { + igraph_bool_t res; + + if (igraph_is_complete(&self->g, &res)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (res) + Py_RETURN_TRUE; + Py_RETURN_FALSE; +} + + +/** \ingroup python_interface_graph + * \brief Checks whether a given vertex set forms a clique + */ +PyObject *igraphmodule_Graph_is_clique(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + PyObject *list = Py_None; + PyObject *directed = Py_False; + igraph_bool_t res; + igraph_vs_t vs; + + static char *kwlist[] = { "vertices", "directed", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &list, &directed)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, NULL, NULL)) { + return NULL; + } + + if (igraph_is_clique(&self->g, vs, PyObject_IsTrue(directed), &res)) { + igraphmodule_handle_igraph_error(); + igraph_vs_destroy(&vs); + return NULL; + } + + igraph_vs_destroy(&vs); + + if (res) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + + +/** \ingroup python_interface_graph + * \brief Checks whether a the given vertices form an independent set + */ +PyObject *igraphmodule_Graph_is_independent_vertex_set(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + PyObject *list = Py_None; + igraph_bool_t res; + igraph_vs_t vs; + + static char *kwlist[] = { "vertices", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &list)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, NULL, NULL)) { + return NULL; + } + + if (igraph_is_independent_vertex_set(&self->g, vs, &res)) { + igraphmodule_handle_igraph_error(); + igraph_vs_destroy(&vs); + return NULL; + } + + igraph_vs_destroy(&vs); + + if (res) + Py_RETURN_TRUE; + else + 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 res; + + 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, &res, /* root = */ 0, mode)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (res) { + 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 @@ -470,12 +673,15 @@ PyObject *igraphmodule_Graph_is_simple(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; } @@ -486,18 +692,30 @@ PyObject *igraphmodule_Graph_add_vertices(igraphmodule_GraphObject * self, /** \ingroup python_interface_graph * \brief Removes vertices from an \c igraph.Graph * \return the modified \c igraph.Graph object - * + * * \todo Need more error checking on vertex IDs. (igraph fails when an * invalid vertex ID is given) * \sa igraph_delete_vertices */ 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 (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, 0, 0)) return NULL; + if (!PyArg_ParseTuple(args, "|O", &list)) return NULL; + + /* no arguments means delete all. */ + + /* Py_None used to mean 'all', but not any more */ + if (list == Py_None) { + PyErr_SetString(PyExc_ValueError, "expected number of vertices to delete, got None"); + 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(); @@ -512,7 +730,7 @@ PyObject *igraphmodule_Graph_delete_vertices(igraphmodule_GraphObject * self, /** \ingroup python_interface_graph * \brief Adds edges to an \c igraph.Graph * \return the extended \c igraph.Graph object - * + * * \todo Need more error checking on vertex IDs. (igraph fails when an * invalid vertex ID is given) * \sa igraph_add_edges @@ -521,29 +739,35 @@ 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 = false; if (!PyArg_ParseTuple(args, "O", &list)) return NULL; - if (igraphmodule_PyObject_to_edgelist(list, &v, &self->g)) + if (igraphmodule_PyObject_to_edgelist(list, &v, &self->g, &v_owned)) return NULL; /* do the hard work :) */ if (igraph_add_edges(&self->g, &v, 0)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); + if (v_owned) { + igraph_vector_int_destroy(&v); + } return NULL; } - igraph_vector_destroy(&v); + if (v_owned) { + igraph_vector_int_destroy(&v); + } + Py_RETURN_NONE; } /** \ingroup python_interface_graph * \brief Deletes edges from an \c igraph.Graph * \return the extended \c igraph.Graph object - * + * * \todo Need more error checking on vertex IDs. (igraph fails when an * invalid vertex ID is given) * \sa igraph_delete_edges @@ -551,13 +775,21 @@ 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 means "do nothing" since igraph 0.10 */ + if (list == Py_None) { + Py_RETURN_NONE; + } + + /* this already converts no arguments and Py_None to all edges */ if (igraphmodule_PyObject_to_es_t(list, &es, &self->g, 0)) { /* something bad happened during conversion, return immediately */ return NULL; @@ -574,7 +806,7 @@ PyObject *igraphmodule_Graph_delete_edges(igraphmodule_GraphObject * self, } /********************************************************************** - * tructural properties * + * Structural properties * **********************************************************************/ /** \ingroup python_interface_graph @@ -587,50 +819,46 @@ 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_t result; + igraph_vector_int_t res; igraph_vs_t vs; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; - 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)) + 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; } - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_int_init(&res, 0)) { igraph_vs_destroy(&vs); return NULL; } - if (igraph_degree(&self->g, &result, vs, + if (igraph_degree(&self->g, &res, vs, dmode, PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&res); return NULL; } - if (!return_single) - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - else - list = PyInt_FromLong((long int)VECTOR(result)[0]); + if (!return_single) { + list = igraphmodule_vector_int_t_to_PyList(&res); + } else { + list = igraphmodule_integer_t_to_PyObject(VECTOR(res)[0]); + } - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&res); igraph_vs_destroy(&vs); return list; @@ -645,10 +873,10 @@ PyObject *igraphmodule_Graph_diversity(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { PyObject *list = Py_None; PyObject *weights_o = Py_None; - igraph_vector_t result, *weights = 0; + igraph_vector_t res, *weights = 0; igraph_vs_t vs; - igraph_bool_t return_single = 0; - igraph_integer_t no_of_nodes; + igraph_bool_t return_single = false; + igraph_int_t no_of_nodes; static char *kwlist[] = { "vertices", "weights", NULL }; @@ -661,15 +889,15 @@ PyObject *igraphmodule_Graph_diversity(igraphmodule_GraphObject * self, return NULL; } - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_init(&res, 0)) { igraph_vs_destroy(&vs); return NULL; } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vs_destroy(&vs); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return NULL; } @@ -679,21 +907,21 @@ PyObject *igraphmodule_Graph_diversity(igraphmodule_GraphObject * self, if (igraph_vs_size(&self->g, &vs, &no_of_nodes)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return NULL; } - if (igraph_vector_resize(&result, no_of_nodes)) { + if (igraph_vector_resize(&res, no_of_nodes)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return NULL; } - igraph_vector_fill(&result, 1.0); + igraph_vector_fill(&res, 1.0); } else { - if (igraph_diversity(&self->g, weights, &result, vs)) { + if (igraph_diversity(&self->g, weights, &res, vs)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); igraph_vector_destroy(weights); free(weights); return NULL; } @@ -702,11 +930,11 @@ PyObject *igraphmodule_Graph_diversity(igraphmodule_GraphObject * self, if (!return_single) - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_FLOAT); + list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); else - list = PyFloat_FromDouble(VECTOR(result)[0]); + list = PyFloat_FromDouble(VECTOR(res)[0]); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); igraph_vs_destroy(&vs); return list; @@ -722,52 +950,45 @@ 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; - igraph_vector_t result, *weights = 0; + igraph_vector_t res, *weights = 0; igraph_vs_t vs; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; - 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(); return NULL; } - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_init(&res, 0)) { igraph_vs_destroy(&vs); return NULL; } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vs_destroy(&vs); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return NULL; } - if (igraph_strength(&self->g, &result, vs, dmode, + if (igraph_strength(&self->g, &res, vs, dmode, PyObject_IsTrue(loops), weights)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; } @@ -775,11 +996,11 @@ PyObject *igraphmodule_Graph_strength(igraphmodule_GraphObject * self, if (weights) { igraph_vector_destroy(weights); free(weights); } if (!return_single) - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_FLOAT); + list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); else - list = PyFloat_FromDouble(VECTOR(result)[0]); + list = PyFloat_FromDouble(VECTOR(res)[0]); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); igraph_vs_destroy(&vs); return list; @@ -792,20 +1013,55 @@ PyObject *igraphmodule_Graph_strength(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_density(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) +{ + char *kwlist[] = { "loops", "weights", NULL }; + igraph_real_t res; + PyObject *loops = Py_False, *weights_o = Py_None; + igraph_vector_t *weights = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &loops, &weights_o)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + + if (igraph_density(&self->g, weights, &res, PyObject_IsTrue(loops))) { + igraphmodule_handle_igraph_error(); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + return NULL; + } + + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); +} + +/** \ingroup python_interface_graph + * \brief Calculates the mean degree + * \return the mean degree + * \sa igraph_mean_degree + */ +PyObject *igraphmodule_Graph_mean_degree(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) { char *kwlist[] = { "loops", NULL }; - igraph_real_t result; - PyObject *loops = Py_False; + igraph_real_t res; + PyObject *loops = Py_True; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &loops)) return NULL; - if (igraph_density(&self->g, &result, PyObject_IsTrue(loops))) { + if (igraph_mean_degree(&self->g, &res, PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); return NULL; } - return Py_BuildValue("d", (double)result); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -818,33 +1074,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_int_t res; igraph_vs_t vs; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; - 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, &res, vs, dmode, PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); return NULL; @@ -852,20 +1102,20 @@ PyObject *igraphmodule_Graph_maxdegree(igraphmodule_GraphObject * self, igraph_vs_destroy(&vs); - return PyInt_FromLong((long)result); + return igraphmodule_integer_t_to_PyObject(res); } /** \ingroup python_interface_graph - * \brief Checks whether an edge is a loop edge + * \brief Checks whether an edge is a loop edge * \return a boolean or a list of booleans * \sa igraph_is_loop */ PyObject *igraphmodule_Graph_is_loop(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { PyObject *list = Py_None; - igraph_vector_bool_t result; + igraph_vector_bool_t res; igraph_es_t es; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; static char *kwlist[] = { "edges", NULL }; @@ -877,43 +1127,43 @@ PyObject *igraphmodule_Graph_is_loop(igraphmodule_GraphObject *self, return NULL; } - if (igraph_vector_bool_init(&result, 0)) { + if (igraph_vector_bool_init(&res, 0)) { igraphmodule_handle_igraph_error(); igraph_es_destroy(&es); return NULL; } - if (igraph_is_loop(&self->g, &result, es)) { + if (igraph_is_loop(&self->g, &res, es)) { igraphmodule_handle_igraph_error(); igraph_es_destroy(&es); - igraph_vector_bool_destroy(&result); + igraph_vector_bool_destroy(&res); return NULL; } if (!return_single) - list = igraphmodule_vector_bool_t_to_PyList(&result); + list = igraphmodule_vector_bool_t_to_PyList(&res); else { - list = (VECTOR(result)[0]) ? Py_True : Py_False; + list = (VECTOR(res)[0]) ? Py_True : Py_False; Py_INCREF(list); } - igraph_vector_bool_destroy(&result); + igraph_vector_bool_destroy(&res); igraph_es_destroy(&es); return list; } /** \ingroup python_interface_graph - * \brief Checks whether an edge is a multiple edge + * \brief Checks whether an edge is a multiple edge * \return a boolean or a list of booleans * \sa igraph_is_multiple */ PyObject *igraphmodule_Graph_is_multiple(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { PyObject *list = Py_None; - igraph_vector_bool_t result; + igraph_vector_bool_t res; igraph_es_t es; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; static char *kwlist[] = { "edges", NULL }; @@ -925,47 +1175,48 @@ PyObject *igraphmodule_Graph_is_multiple(igraphmodule_GraphObject *self, return NULL; } - if (igraph_vector_bool_init(&result, 0)) { + if (igraph_vector_bool_init(&res, 0)) { igraphmodule_handle_igraph_error(); igraph_es_destroy(&es); return NULL; } - if (igraph_is_multiple(&self->g, &result, es)) { + if (igraph_is_multiple(&self->g, &res, es)) { igraphmodule_handle_igraph_error(); igraph_es_destroy(&es); - igraph_vector_bool_destroy(&result); + igraph_vector_bool_destroy(&res); return NULL; } if (!return_single) - list = igraphmodule_vector_bool_t_to_PyList(&result); + list = igraphmodule_vector_bool_t_to_PyList(&res); else { - list = (VECTOR(result)[0]) ? Py_True : Py_False; + list = (VECTOR(res)[0]) ? Py_True : Py_False; Py_INCREF(list); } - igraph_vector_bool_destroy(&result); + igraph_vector_bool_destroy(&res); igraph_es_destroy(&es); return list; } /** \ingroup python_interface_graph - * \brief Checks whether an edge is mutual + * \brief Checks whether an edge is mutual * \return a boolean or a list of booleans * \sa igraph_is_mutual */ PyObject *igraphmodule_Graph_is_mutual(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { PyObject *list = Py_None; - igraph_vector_bool_t result; + igraph_vector_bool_t res; igraph_es_t es; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; + PyObject* loops_o = Py_True; - static char *kwlist[] = { "edges", NULL }; + static char *kwlist[] = { "edges", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &list)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &list, &loops_o)) return NULL; if (igraphmodule_PyObject_to_es_t(list, &es, &self->g, &return_single)) { @@ -973,27 +1224,27 @@ PyObject *igraphmodule_Graph_is_mutual(igraphmodule_GraphObject *self, return NULL; } - if (igraph_vector_bool_init(&result, 0)) { + if (igraph_vector_bool_init(&res, 0)) { igraphmodule_handle_igraph_error(); igraph_es_destroy(&es); return NULL; } - if (igraph_is_mutual(&self->g, &result, es)) { + if (igraph_is_mutual(&self->g, &res, es, PyObject_IsTrue(loops_o))) { igraphmodule_handle_igraph_error(); igraph_es_destroy(&es); - igraph_vector_bool_destroy(&result); + igraph_vector_bool_destroy(&res); return NULL; } if (!return_single) - list = igraphmodule_vector_bool_t_to_PyList(&result); + list = igraphmodule_vector_bool_t_to_PyList(&res); else { - list = (VECTOR(result)[0]) ? Py_True : Py_False; + list = (VECTOR(res)[0]) ? Py_True : Py_False; Py_INCREF(list); } - igraph_vector_bool_destroy(&result); + igraph_vector_bool_destroy(&res); igraph_es_destroy(&es); return list; @@ -1004,7 +1255,7 @@ PyObject *igraphmodule_Graph_is_mutual(igraphmodule_GraphObject *self, * \return \c True if the graph has multiple edges, \c False otherwise. * \sa igraph_has_multiple */ -PyObject *igraphmodule_Graph_has_multiple(igraphmodule_GraphObject *self) { +PyObject *igraphmodule_Graph_has_multiple(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { igraph_bool_t res; if (igraph_has_multiple(&self->g, &res)) { @@ -1018,16 +1269,16 @@ PyObject *igraphmodule_Graph_has_multiple(igraphmodule_GraphObject *self) { } /** \ingroup python_interface_graph - * \brief Checks the multiplicity of the edges + * \brief Checks the multiplicity of the edges * \return the edge multiplicities as a Python list * \sa igraph_count_multiple */ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { PyObject *list = Py_None; - igraph_vector_t result; + igraph_vector_int_t res; igraph_es_t es; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; static char *kwlist[] = { "edges", NULL }; @@ -1039,24 +1290,25 @@ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, return NULL; } - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_int_init(&res, 0)) { igraph_es_destroy(&es); return NULL; } - if (igraph_count_multiple(&self->g, &result, es)) { + if (igraph_count_multiple(&self->g, &res, es)) { igraphmodule_handle_igraph_error(); igraph_es_destroy(&es); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&res); return NULL; } - if (!return_single) - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - else - list = PyInt_FromLong((long int)VECTOR(result)[0]); + if (!return_single) { + list = igraphmodule_vector_int_t_to_PyList(&res); + } else { + list = igraphmodule_integer_t_to_PyObject(VECTOR(res)[0]); + } - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&res); igraph_es_destroy(&es); return list; @@ -1069,46 +1321,52 @@ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, * second argument may be passed as well, meaning the type of neighbors to * be returned (\c OUT for successors, \c IN for predecessors or \c ALL * for both of them). This argument is ignored for undirected graphs. - * + * * \return the neighbor list as a Python list object * \sa igraph_neighbors */ 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, *loops_o = Py_True, *multiple_o = Py_True; igraph_neimode_t dmode = IGRAPH_ALL; - igraph_integer_t idx; - igraph_vector_t result; + igraph_loops_t loops = IGRAPH_LOOPS; + igraph_bool_t multiple = 1; + igraph_int_t idx; + igraph_vector_int_t res; - static char *kwlist[] = { "vertex", "mode", "type", NULL }; + static char *kwlist[] = { "vertex", "mode", "loops", "multiple", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, - &index_o, &dmode_o, &dtype_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &index_o, &dmode_o, &loops_o, &multiple_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_loops_t(loops_o, &loops)) { return NULL; + } - if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) + multiple = PyObject_IsTrue(multiple_o); + + if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) { return NULL; + } - if (igraph_vector_init(&result, 1)) - return igraphmodule_handle_igraph_error(); + if (igraph_vector_int_init(&res, 1)) { + igraphmodule_handle_igraph_error(); + return NULL; + } - if (igraph_neighbors(&self->g, &result, idx, dmode)) { + if (igraph_neighbors(&self->g, &res, idx, dmode, loops, multiple)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&res); return NULL; } - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + list = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); return list; } @@ -1120,44 +1378,49 @@ PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, * A second argument may be passed as well, meaning the type of neighbors to * be returned (\c OUT for successors, \c IN for predecessors or \c ALL * for both of them). This argument is ignored for undirected graphs. - * + * * \return the adjacency list as a Python list object * \sa igraph_incident */ 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, *loops_o = Py_True; igraph_neimode_t dmode = IGRAPH_OUT; - igraph_integer_t idx; - igraph_vector_t result; + igraph_loops_t loops = IGRAPH_LOOPS; + igraph_int_t idx; + igraph_vector_int_t res; - static char *kwlist[] = { "vertex", "mode", "type", NULL }; + static char *kwlist[] = { "vertex", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, - &index_o, &dmode_o, &dtype_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &index_o, &dmode_o, &loops_o)) + return NULL; + + if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { 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_loops_t(loops_o, &loops)) { + 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(&res, 1)) { + igraphmodule_handle_igraph_error(); return NULL; + } - igraph_vector_init(&result, 1); - if (igraph_incident(&self->g, &result, idx, dmode)) { + if (igraph_incident(&self->g, &res, idx, dmode, loops)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&res); return NULL; } - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + list = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); return list; } @@ -1171,7 +1434,7 @@ PyObject *igraphmodule_Graph_reciprocity(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { char *kwlist[] = { "ignore_loops", "mode", NULL }; - igraph_real_t result; + igraph_real_t res; igraph_reciprocity_t mode = IGRAPH_RECIPROCITY_DEFAULT; PyObject *ignore_loops = Py_True, *mode_o = Py_None; @@ -1181,86 +1444,12 @@ PyObject *igraphmodule_Graph_reciprocity(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_reciprocity_t(mode_o, &mode)) return NULL; - if (igraph_reciprocity(&self->g, &result, PyObject_IsTrue(ignore_loops), mode)) { - igraphmodule_handle_igraph_error(); - return NULL; - } - - return Py_BuildValue("d", (double)result); -} - -/** \ingroup python_interface_graph - * \brief The successors of a given vertex in an \c igraph.Graph - * This method accepts a single vertex ID as a parameter, and returns the - * successors of the given vertex in the form of an integer list. It - * is equivalent to calling \c igraph.Graph.neighbors with \c type=OUT - * - * \return the successor list as a Python list object - * \sa igraph_neighbors - */ -PyObject *igraphmodule_Graph_successors(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) -{ - PyObject *list, *index_o; - igraph_integer_t idx; - igraph_vector_t result; - - static char *kwlist[] = { "vertex", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &index_o)) - return NULL; - - if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) - return NULL; - - igraph_vector_init(&result, 1); - if (igraph_neighbors(&self->g, &result, idx, IGRAPH_OUT)) { - igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); - return NULL; - } - - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); - - return list; -} - -/** \ingroup python_interface_graph - * \brief The predecessors of a given vertex in an \c igraph.Graph - * This method accepts a single vertex ID as a parameter, and returns the - * predecessors of the given vertex in the form of an integer list. It - * is equivalent to calling \c igraph.Graph.neighbors with \c type=IN - * - * \return the predecessor list as a Python list object - * \sa igraph_neighbors - */ -PyObject *igraphmodule_Graph_predecessors(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) -{ - PyObject *list, *index_o; - igraph_integer_t idx; - igraph_vector_t result; - - static char *kwlist[] = { "vertex", NULL }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &index_o)) - return NULL; - - if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) - return NULL; - - igraph_vector_init(&result, 1); - if (igraph_neighbors(&self->g, &result, idx, IGRAPH_IN)) { + if (igraph_reciprocity(&self->g, &res, PyObject_IsTrue(ignore_loops), mode)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); return NULL; } - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); - - return list; + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -1292,17 +1481,38 @@ PyObject *igraphmodule_Graph_is_connected(igraphmodule_GraphObject * self, Py_RETURN_FALSE; } +/** \ingroup python_interface_graph + * \brief Decides whether a graph is biconnected. + * \return Py_True if the graph is biconnected, Py_False otherwise + * \sa igraph_is_biconnected + */ +PyObject *igraphmodule_Graph_is_biconnected(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) +{ + igraph_bool_t res; + + if (igraph_is_biconnected(&self->g, &res)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (res) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + /** \ingroup python_interface_graph * \brief Decides whether there is an edge from a given vertex to an other one. * \return Py_True if the vertices are directly connected, Py_False otherwise - * \sa igraph_are_connected + * \sa igraph_are_adjacent */ -PyObject *igraphmodule_Graph_are_connected(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) +PyObject *igraphmodule_Graph_are_adjacent(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) { static char *kwlist[] = { "v1", "v2", NULL }; PyObject *v1, *v2; - igraph_integer_t idx1, idx2; + igraph_int_t idx1, idx2; igraph_bool_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &v1, &v2)) @@ -1314,7 +1524,7 @@ PyObject *igraphmodule_Graph_are_connected(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(v2, &idx2, &self->g)) return NULL; - if (igraph_are_connected(&self->g, idx1, idx2, &res)) + if (igraph_are_adjacent(&self->g, idx1, idx2, &res)) return igraphmodule_handle_igraph_error(); if (res) @@ -1333,8 +1543,8 @@ PyObject *igraphmodule_Graph_get_eid(igraphmodule_GraphObject * self, PyObject *v1, *v2; PyObject *directed = Py_True; PyObject *error = Py_True; - igraph_integer_t idx1, idx2; - igraph_integer_t result; + igraph_int_t idx1, idx2; + igraph_int_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", kwlist, &v1, &v2, &directed, &error)) @@ -1346,11 +1556,11 @@ PyObject *igraphmodule_Graph_get_eid(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(v2, &idx2, &self->g)) return NULL; - if (igraph_get_eid(&self->g, &result, idx1, idx2, + if (igraph_get_eid(&self->g, &res, idx1, idx2, PyObject_IsTrue(directed), PyObject_IsTrue(error))) return igraphmodule_handle_igraph_error(); - return Py_BuildValue("l", (long)result); + return igraphmodule_integer_t_to_PyObject(res); } /** \ingroup python_interface_graph @@ -1360,53 +1570,41 @@ PyObject *igraphmodule_Graph_get_eid(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_get_eids(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "pairs", "path", "directed", "error", NULL }; - PyObject *pairs_o = Py_None, *path_o = Py_None; + static char *kwlist[] = { "pairs", "directed", "error", NULL }; + PyObject *pairs_o = Py_None; PyObject *directed = Py_True; PyObject *error = Py_True; - PyObject *result = NULL; - igraph_vector_t pairs, path, res; + PyObject *result_o = NULL; + igraph_vector_int_t pairs, res; + igraph_bool_t pairs_owned = false; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, - &pairs_o, &path_o, &directed, - &error)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, + &pairs_o, &directed, &error)) return NULL; - if (igraph_vector_init(&res, 0)) + if (igraph_vector_int_init(&res, 1)) return igraphmodule_handle_igraph_error(); - if (pairs_o != Py_None) { - if (igraphmodule_PyObject_to_edgelist(pairs_o, &pairs, &self->g)) { - igraph_vector_destroy(&res); - return NULL; - } + if (igraphmodule_PyObject_to_edgelist(pairs_o, &pairs, &self->g, &pairs_owned)) { + 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 (pairs_o != Py_None) igraph_vector_destroy(&pairs); - return NULL; + if (igraph_get_eids(&self->g, &res, &pairs, PyObject_IsTrue(directed), PyObject_IsTrue(error))) { + if (pairs_owned) { + igraph_vector_int_destroy(&pairs); } - } - - if (igraph_get_eids(&self->g, &res, - pairs_o == Py_None ? 0 : &pairs, - path_o == Py_None ? 0 : &path, - PyObject_IsTrue(directed), - PyObject_IsTrue(error))) { - if (pairs_o != Py_None) igraph_vector_destroy(&pairs); - if (path_o != Py_None) igraph_vector_destroy(&path); - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); return igraphmodule_handle_igraph_error(); } - if (pairs_o != Py_None) igraph_vector_destroy(&pairs); - if (path_o != Py_None) igraph_vector_destroy(&path); + if (pairs_owned) { + igraph_vector_int_destroy(&pairs); + } - result = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&res); - return result; + result_o = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); + return result_o; } /** \ingroup python_interface_graph @@ -1417,7 +1615,7 @@ PyObject *igraphmodule_Graph_get_eids(igraphmodule_GraphObject * self, * in unconnected graphs: it is \c True if the longest geodesic * within a component should be returned and \c False if the number of * vertices should be returned. They both have a default value of \c False. - * + * * \return the diameter as a Python integer * \sa igraph_diameter */ @@ -1427,6 +1625,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 @@ -1438,27 +1637,26 @@ 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) { - igraph_real_t i; - if (igraph_diameter_dijkstra(&self->g, weights, &i, 0, 0, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); + if ( + igraph_diameter(&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(); + if (weights) { igraph_vector_destroy(weights); free(weights); - return NULL; } + return NULL; + } + + if (weights) { igraph_vector_destroy(weights); free(weights); - return PyFloat_FromDouble((double)i); - } else { - igraph_integer_t i; - if (igraph_diameter(&self->g, &i, 0, 0, 0, PyObject_IsTrue(dir), - PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); - return NULL; - } - return PyInt_FromLong((long)i); } + + return igraphmodule_real_t_to_PyObject(diameter, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -1468,10 +1666,10 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *dir = Py_True, *vcount_if_unconnected = Py_True, *result; + PyObject *dir = Py_True, *vcount_if_unconnected = Py_True, *result_o; 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 }; @@ -1481,29 +1679,36 @@ 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) { - if (igraph_diameter_dijkstra(&self->g, weights, 0, 0, 0, &res, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); + if (igraph_vector_int_init(&res, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if ( + igraph_diameter(&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(); + if (weights) { igraph_vector_destroy(weights); free(weights); - igraph_vector_destroy(&res); - return NULL; } + igraph_vector_int_destroy(&res); + return NULL; + } + + if (weights) { igraph_vector_destroy(weights); free(weights); - } else { - if (igraph_diameter(&self->g, 0, 0, 0, &res, PyObject_IsTrue(dir), - PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); - return NULL; - } } - result = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&res); - return result; + result_o = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); + + return result_o; } /** \ingroup python_interface_graph @@ -1516,8 +1721,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_int_t from, to; + igraph_real_t len; static char *kwlist[] = { "directed", "unconn", "weights", NULL }; @@ -1527,29 +1732,31 @@ 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_real, &from, &to, 0, - PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); + if ( + igraph_diameter( + &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(); + if (weights) { igraph_vector_destroy(weights); free(weights); - return NULL; } + return NULL; + } + + if (weights) { 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); - } else { - if (igraph_diameter(&self->g, &len, &from, &to, 0, PyObject_IsTrue(dir), - PyObject_IsTrue(vcount_if_unconnected))) { - igraphmodule_handle_igraph_error(); - 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 (from >= 0) { + 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); } } @@ -1562,67 +1769,74 @@ PyObject *igraphmodule_Graph_girth(igraphmodule_GraphObject *self, { PyObject *sc = Py_False; static char *kwlist[] = { "return_shortest_circle", NULL }; - igraph_integer_t girth; - igraph_vector_t vids; + igraph_real_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_init(&vids, 0); if (igraph_girth(&self->g, &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 PyInt_FromLong((long)girth); + + return igraphmodule_real_t_to_PyObject(girth, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** * \ingroup python_interface_graph - * \brief Calculates the convergence degree of the edges in a graph + * \brief Calculates the convergence degree of the edges in a graph */ -PyObject *igraphmodule_Graph_convergence_degree(igraphmodule_GraphObject *self) { - igraph_vector_t result; - PyObject *o; +PyObject *igraphmodule_Graph_convergence_degree(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { + igraph_vector_t res; + PyObject *o; - igraph_vector_init(&result, 0); - if (igraph_convergence_degree(&self->g, &result, 0, 0)) { + igraph_vector_init(&res, 0); + if (igraph_convergence_degree(&self->g, &res, 0, 0)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return NULL; } - o=igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_FLOAT); - igraph_vector_destroy(&result); + o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); + igraph_vector_destroy(&res); + return o; } /** * \ingroup python_interface_graph - * \brief Calculates the sizes of the convergence fields in a graph + * \brief Calculates the sizes of the convergence fields in a graph */ -PyObject *igraphmodule_Graph_convergence_field_size(igraphmodule_GraphObject *self) { +PyObject *igraphmodule_Graph_convergence_field_size(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { igraph_vector_t ins, outs; PyObject *o1, *o2; 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); - o2=igraphmodule_vector_t_to_PyList(&outs, IGRAPHMODULE_TYPE_INT); + o1 = igraphmodule_vector_t_to_PyList(&ins, IGRAPHMODULE_TYPE_INT); + o2 = igraphmodule_vector_t_to_PyList(&outs, IGRAPHMODULE_TYPE_INT); igraph_vector_destroy(&ins); igraph_vector_destroy(&outs); return Py_BuildValue("NN", o1, o2); @@ -1665,14 +1879,14 @@ 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); return NULL; } - if (igraph_avg_nearest_neighbor_degree(&self->g, vids, &knn, &knnk, weights)) { + if (igraph_avg_nearest_neighbor_degree(&self->g, vids, IGRAPH_ALL, IGRAPH_ALL, &knn, &knnk, weights)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vids); igraph_vector_destroy(&knn); @@ -1709,32 +1923,71 @@ PyObject *igraphmodule_Graph_knn(igraphmodule_GraphObject *self, /** \ingroup python_interface_graph * \brief Calculates the radius of an \c igraph.Graph - * + * * \return the radius as a Python integer * \sa igraph_radius */ PyObject *igraphmodule_Graph_radius(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *mode_o = Py_None; + PyObject *mode_o = Py_None, *weights_o = Py_None; igraph_neimode_t mode = IGRAPH_OUT; igraph_real_t radius; + igraph_vector_t *weights; - static char *kwlist[] = { "mode", NULL }; + static char *kwlist[] = { "mode", "weights", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, - &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &mode_o, &weights_o)) { return NULL; + } - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { + return NULL; + } + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + + if (igraph_radius(&self->g, weights, &radius, mode)) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + + return igraphmodule_real_t_to_PyObject(radius, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); +} + +/** \ingroup python_interface_graph + * \brief Converts a tree graph into a Prüfer sequence + * \return the Prüfer sequence as a Python object + * \sa igraph_to_prufer + */ +PyObject *igraphmodule_Graph_to_prufer(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) +{ + igraph_vector_int_t res; + PyObject *list; + + if (igraph_vector_int_init(&res, 0)) { return NULL; + } - if (igraph_radius(&self->g, &radius, mode)) { + if (igraph_to_prufer(&self->g, &res)) { igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&res); return NULL; } - return PyFloat_FromDouble((double)radius); + list = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); + + return list; } /********************************************************************** @@ -1751,23 +2004,27 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, igraphmodule_GraphObject *self; igraph_t g; igraph_matrix_t m; - PyObject *matrix, *mode_o = Py_None; + PyObject *matrix_o, *mode_o = Py_None, *loops_o = Py_None; igraph_adjacency_t mode = IGRAPH_ADJ_DIRECTED; + igraph_loops_t loops = IGRAPH_LOOPS_ONCE; - static char *kwlist[] = { "matrix", "mode", NULL }; + static char *kwlist[] = { "matrix", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, - &PyList_Type, &matrix, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, + &matrix_o, &mode_o, &loops_o)) return NULL; - if (igraphmodule_PyObject_to_adjacency_t(mode_o, &mode)) return NULL; - if (igraphmodule_PyList_to_matrix_t(matrix, &m)) { - PyErr_SetString(PyExc_TypeError, - "Error while converting adjacency matrix"); + if (igraphmodule_PyObject_to_adjacency_t(mode_o, &mode)) + return NULL; + + if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) + return NULL; + + if (igraphmodule_PyObject_to_matrix_t(matrix_o, &m, "matrix")) { return NULL; } - if (igraph_adjacency(&g, &m, mode)) { + if (igraph_adjacency(&g, &m, mode, loops)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&m); return NULL; @@ -1780,6 +2037,7 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, return (PyObject *) self; } + /** \ingroup python_interface_graph * \brief Generates a graph from the Graph Atlas * \return a reference to the newly generated Python igraph object @@ -1787,14 +2045,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; } @@ -1805,11 +2064,11 @@ PyObject *igraphmodule_Graph_Atlas(PyTypeObject * type, PyObject * args) } /** \ingroup python_interface_graph - * \brief Generates a graph based on the Barabasi-Albert model + * \brief Generates a graph based on the Barabási-Albert model * 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). - * + * * \return a reference to the newly generated Python igraph object * \sa igraph_barabasi_game */ @@ -1818,9 +2077,11 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, m = 1; + Py_ssize_t n; float power = 1.0f, zero_appeal = 1.0f; - igraph_vector_t outseq; + igraph_int_t m = 1; + igraph_vector_int_t outseq; + igraph_bool_t has_outseq = false; igraph_t *start_from = 0; igraph_barabasi_algorithm_t algo = IGRAPH_BARABASI_PSUMTREE; PyObject *m_obj = 0, *outpref = Py_False, *directed = Py_False; @@ -1831,56 +2092,51 @@ 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)) 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)) 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_init(&outseq, 0); 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); - igraph_vector_init(&outseq, 0); - } else if (PyList_Check(m_obj)) { - if (igraphmodule_PyObject_to_vector_t(m_obj, &outseq, 1)) { - /* something bad happened during conversion */ - return NULL; - } - } else { - PyErr_SetString(PyExc_TypeError, "m must be an integer or a list of integers"); + } else if (PyLong_Check(m_obj)) { + if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { return NULL; } + } else if (PySequence_Check(m_obj)) { + if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { + return NULL; + } + has_outseq = true; + } else { + PyErr_SetString(PyExc_TypeError, "m must be an integer or a sequence of integers"); + return NULL; } - if (igraph_barabasi_game(&g, (igraph_integer_t) n, - (igraph_real_t) power, - (igraph_integer_t) m, - &outseq, PyObject_IsTrue(outpref), - (igraph_real_t) zero_appeal, + if (igraph_barabasi_game(&g, n, power, m, + has_outseq ? &outseq : NULL, PyObject_IsTrue(outpref), + zero_appeal, PyObject_IsTrue(directed), algo, start_from)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } return NULL; } - igraph_vector_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -1898,7 +2154,8 @@ 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 = false; PyObject *types_o, *edges_o, *directed = Py_False; static char *kwlist[] = { "types", "edges", "directed", NULL }; @@ -1910,21 +2167,76 @@ PyObject *igraphmodule_Graph_Bipartite(PyTypeObject * type, if (igraphmodule_PyObject_to_vector_bool_t(types_o, &types)) return NULL; - if (igraphmodule_PyObject_to_edgelist(edges_o, &edges, 0)) { + if (igraphmodule_PyObject_to_edgelist(edges_o, &edges, 0, &edges_owned)) { igraph_vector_bool_destroy(&types); return NULL; } if (igraph_create_bipartite(&g, &types, &edges, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&edges); + if (edges_owned) { + igraph_vector_int_destroy(&edges); + } igraph_vector_bool_destroy(&types); return NULL; } - igraph_vector_destroy(&edges); + if (edges_owned) { + igraph_vector_int_destroy(&edges); + } igraph_vector_bool_destroy(&types); - + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + +/** \ingroup python_interface_graph + * \brief Generates a Chung-Lu random graph + * 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). + * + * \return a reference to the newly generated Python igraph object + * \sa igraph_chung_lu_game + */ +PyObject *igraphmodule_Graph_Chung_Lu(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + igraphmodule_GraphObject *self; + igraph_t g; + igraph_vector_t outw, inw; + igraph_chung_lu_t var = IGRAPH_CHUNG_LU_ORIGINAL; + igraph_bool_t has_inw = false; + PyObject *weight_out = NULL, *weight_in = NULL, *loops = Py_True, *variant = NULL; + + static char *kwlist[] = { "out", "in_", "loops", "variant", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, + &weight_out, &weight_in, &loops, &variant)) + return NULL; + + if (igraphmodule_PyObject_to_chung_lu_t(variant, &var)) return NULL; + if (igraphmodule_PyObject_to_vector_t(weight_out, &outw, /* need_non_negative */ true)) return NULL; + if (weight_in) { + if (igraphmodule_PyObject_to_vector_t(weight_in, &inw, /* need_non_negative */ true)) { + igraph_vector_destroy(&outw); + return NULL; + } + has_inw=true; + } + + if (igraph_chung_lu_game(&g, &outw, has_inw ? &inw : NULL, PyObject_IsTrue(loops), var)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&outw); + if (has_inw) + igraph_vector_destroy(&inw); + return NULL; + } + + igraph_vector_destroy(&outw); + if (has_inw) + igraph_vector_destroy(&inw); + CREATE_GRAPH_FROM_TYPE(self, g, type); return (PyObject *) self; @@ -1936,15 +2248,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; } @@ -1959,7 +2274,7 @@ PyObject *igraphmodule_Graph_De_Bruijn(PyTypeObject *type, PyObject *args, * 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). - * + * * \return a reference to the newly generated Python igraph object * \sa igraph_degree_sequence_game */ @@ -1968,24 +2283,22 @@ PyObject *igraphmodule_Graph_Degree_Sequence(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - igraph_vector_t outseq, inseq; - igraph_degseq_t meth = IGRAPH_DEGSEQ_SIMPLE; - igraph_bool_t has_inseq = 0; + igraph_vector_int_t outseq, inseq; + igraph_degseq_t meth = IGRAPH_DEGSEQ_CONFIGURATION; + igraph_bool_t has_inseq = false; 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, - &PyList_Type, &indeg, - &method)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, + &outdeg, &indeg, &method)) 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; @@ -1993,15 +2306,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); @@ -2018,19 +2331,23 @@ 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; + PyObject *loops = Py_False, *directed = Py_False, *edge_labeled = Py_False; + int retval; - static char *kwlist[] = { "n", "p", "m", "directed", "loops", NULL }; + static char *kwlist[] = { "n", "p", "m", "directed", "loops", "edge_labeled", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|dlOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|dnOOO", kwlist, &n, &p, &m, &directed, - &loops)) + &loops, + &edge_labeled)) 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."); @@ -2042,12 +2359,23 @@ PyObject *igraphmodule_Graph_Erdos_Renyi(PyTypeObject * type, return NULL; } - t = (m == -1) ? IGRAPH_ERDOS_RENYI_GNP : IGRAPH_ERDOS_RENYI_GNM; + if (m == -1) { + /* GNP model */ + retval = igraph_erdos_renyi_game_gnp( + &g, n, p, PyObject_IsTrue(directed), + PyObject_IsTrue(loops) ? IGRAPH_LOOPS_SW : IGRAPH_SIMPLE_SW, + PyObject_IsTrue(edge_labeled) + ); + } else { + /* GNM model */ + retval = igraph_erdos_renyi_game_gnm( + &g, n, m, PyObject_IsTrue(directed), + PyObject_IsTrue(loops) ? IGRAPH_LOOPS_SW : IGRAPH_SIMPLE_SW, + PyObject_IsTrue(edge_labeled) + ); + } - if (igraph_erdos_renyi_game(&g, t, (igraph_integer_t) n, - (igraph_real_t) (m == -1 ? p : m), - PyObject_IsTrue(directed), - PyObject_IsTrue(loops))) { + if (retval) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2067,7 +2395,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; @@ -2075,9 +2403,8 @@ 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, - &n, &k, &PyList_Type, &type_dist, - &PyList_Type, &pref_matrix, &directed)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnOO|O", kwlist, + &n, &k, &type_dist, &pref_matrix, &directed)) return NULL; if (n <= 0 || k <= 0) { @@ -2085,34 +2412,36 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, "Number of vertices and the amount of connection trials per step must be positive."); return NULL; } - types = PyList_Size(type_dist); - if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm)) { - PyErr_SetString(PyExc_TypeError, - "Error while converting preference matrix"); + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE(k, "connection trials per set"); + + if (igraphmodule_PyObject_to_vector_t(type_dist, &td, 1)) { + PyErr_SetString(PyExc_ValueError, + "Error while converting type distribution vector"); + return NULL; + } + + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + igraph_vector_destroy(&td); return NULL; } + + types = igraph_vector_size(&td); + if (igraph_matrix_nrow(&pm) != igraph_matrix_ncol(&pm) || igraph_matrix_nrow(&pm) != types) { PyErr_SetString(PyExc_ValueError, "Preference matrix must have exactly the same rows and columns as the number of types"); - igraph_matrix_destroy(&pm); - return NULL; - } - if (igraphmodule_PyObject_to_vector_t(type_dist, &td, 1)) { - PyErr_SetString(PyExc_ValueError, - "Error while converting type distribution vector"); + igraph_vector_destroy(&td); igraph_matrix_destroy(&pm); return NULL; } - if (igraph_establishment_game(&g, (igraph_integer_t) n, - (igraph_integer_t) types, - (igraph_integer_t) k, &td, &pm, - PyObject_IsTrue(directed))) { + 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); + igraph_matrix_destroy(&pm); return NULL; } @@ -2161,20 +2490,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, "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))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2195,22 +2524,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, "vertex count"); - 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; } @@ -2231,41 +2555,45 @@ 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; + 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); + 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; + return NULL; } CREATE_GRAPH_FROM_TYPE(self, g, type); + if (self == NULL) { + igraph_vector_bool_destroy(&vertex_types); + return NULL; + } vertex_types_o = igraphmodule_vector_bool_t_to_PyList(&vertex_types); igraph_vector_bool_destroy(&vertex_types); - if (vertex_types_o == 0) return NULL; + if (vertex_types_o == 0) { + return NULL; + } + return Py_BuildValue("NN", (PyObject *) self, vertex_types_o); } @@ -2279,16 +2607,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; } @@ -2308,7 +2637,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; @@ -2316,7 +2645,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; @@ -2329,8 +2658,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); @@ -2344,6 +2674,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) { @@ -2353,6 +2684,12 @@ PyObject *igraphmodule_Graph_GRG(PyTypeObject * type, } CREATE_GRAPH_FROM_TYPE(self, g, type); + if (self == NULL) { + Py_DECREF(o_xs); + Py_DECREF(o_ys); + return NULL; + } + return Py_BuildValue("NNN", (PyObject*)self, o_xs, o_ys); } @@ -2364,33 +2701,97 @@ 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)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|OO", kwlist, &n, &m, &directed, &citation)) { return NULL; + } - if (n < 0) { - PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); + 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, n, m, PyObject_IsTrue(directed), PyObject_IsTrue(citation))) { + igraphmodule_handle_igraph_error(); return NULL; } - if (m < 0) { - PyErr_SetString(PyExc_ValueError, - "Number of new edges per iteration must be positive."); + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + +/** \ingroup python_interface_graph + * \brief Generates a regular hexagonal lattice + * \return a reference to the newly generated Python igraph object + * \sa igraph_hexagonal_lattice + */ +PyObject *igraphmodule_Graph_Hexagonal_Lattice(PyTypeObject * type, + PyObject * args, PyObject * kwds) +{ + igraph_vector_int_t dimvector; + igraph_bool_t directed; + igraph_bool_t mutual; + PyObject *o_directed = Py_False, *o_mutual = Py_True; + PyObject *o_dimvector = Py_None; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "dim", "directed", "mutual", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, + &o_dimvector, &o_directed, &o_mutual)) + return NULL; + + directed = PyObject_IsTrue(o_directed); + mutual = PyObject_IsTrue(o_mutual); + + if (igraphmodule_PyObject_to_vector_int_t(o_dimvector, &dimvector)) + return NULL; + + if (igraph_hexagonal_lattice(&g, &dimvector, directed, mutual)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&dimvector); + return NULL; + } + + igraph_vector_int_destroy(&dimvector); + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + + +/** \ingroup python_interface_graph + * \brief Generates hypercube graph + * \return a reference to the newly generated Python igraph object + * \sa igraph_hypercube + */ +PyObject *igraphmodule_Graph_Hypercube(PyTypeObject * type, + PyObject * args, PyObject * kwds) +{ + Py_ssize_t n; + igraph_bool_t directed; + PyObject *o_directed = Py_False; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "n", "directed", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|O", kwlist, &n, &o_directed)) { return NULL; } - if (igraph_growing_random_game(&g, (igraph_integer_t) n, - (igraph_integer_t) m, - (directed == Py_True), - (citation == Py_True))) { + CHECK_SSIZE_T_RANGE(n, "vertex count"); + + directed = PyObject_IsTrue(o_directed); + + if (igraph_hypercube(&g, n, directed)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2401,12 +2802,12 @@ PyObject *igraphmodule_Graph_Growing_Random(PyTypeObject * type, } /** \ingroup python_interface_graph - * \brief Generates a bipartite graph from an incidence matrix + * \brief Generates a bipartite graph from a bipartite adjacency matrix * \return a reference to the newly generated Python igraph object - * \sa igraph_incidence + * \sa igraph_biadjacency */ -PyObject *igraphmodule_Graph_Incidence(PyTypeObject * type, - PyObject * args, PyObject * kwds) { +PyObject *igraphmodule_Graph_Biadjacency(PyTypeObject * type, + PyObject * args, PyObject * kwds) { igraphmodule_GraphObject *self; igraph_matrix_t matrix; igraph_vector_bool_t vertex_types; @@ -2417,34 +2818,35 @@ 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)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &matrix_o, + &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); - PyErr_SetString(PyExc_TypeError, - "Error while converting incidence matrix"); + if (igraphmodule_PyObject_to_matrix_t(matrix_o, &matrix, "matrix")) { + igraph_vector_bool_destroy(&vertex_types); 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; + if (igraph_biadjacency(&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; } igraph_matrix_destroy(&matrix); CREATE_GRAPH_FROM_TYPE(self, g, type); + if (self == NULL) { + return NULL; + } vertex_types_o = igraphmodule_vector_bool_t_to_PyList(&vertex_types); igraph_vector_bool_destroy(&vertex_types); @@ -2453,36 +2855,29 @@ 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). - * + * * \return a reference to the newly generated Python igraph object * \sa igraph_isoclass_create */ 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", "class", "directed", NULL }; + 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; - if (n < 3 || n > 4) { - PyErr_SetString(PyExc_ValueError, - "Only graphs with 3 or 4 vertices are supported"); - 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; } @@ -2498,15 +2893,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; + } - if (igraph_kautz(&g, (igraph_integer_t) m, (igraph_integer_t) n)) { + CHECK_SSIZE_T_RANGE(m, "m"); + CHECK_SSIZE_T_RANGE(n, "n"); + + if (igraph_kautz(&g, m, n)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2526,17 +2925,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; } @@ -2547,18 +2948,18 @@ PyObject *igraphmodule_Graph_K_Regular(PyTypeObject * type, } /** \ingroup python_interface_graph - * \brief Generates a regular lattice + * \brief Generates a regular square lattice * \return a reference to the newly generated Python igraph object - * \sa igraph_lattice + * \sa igraph_square_lattice */ PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, PyObject * args, PyObject * kwds) { - igraph_vector_t dimvector; - long int nei = 1; + igraph_vector_int_t dimvector; + igraph_vector_bool_t circular; + Py_ssize_t nei = 1; igraph_bool_t directed; igraph_bool_t mutual; - igraph_bool_t circular; PyObject *o_directed = Py_False, *o_mutual = Py_True, *o_circular = Py_True; PyObject *o_dimvector = Py_None; igraphmodule_GraphObject *self; @@ -2566,26 +2967,42 @@ PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, static char *kwlist[] = { "dim", "nei", "directed", "mutual", "circular", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|lOOO", kwlist, - &PyList_Type, &o_dimvector, - &nei, &o_directed, &o_mutual, &o_circular)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|nOOO", kwlist, + &o_dimvector, &nei, &o_directed, &o_mutual, &o_circular)) return NULL; directed = PyObject_IsTrue(o_directed); 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)) { + if (PyBool_Check(o_circular) || PyNumber_Check(o_circular) || PyBaseString_Check(o_circular)) { + if (igraph_vector_bool_init(&circular, igraph_vector_int_size(&dimvector))) { + igraph_vector_int_destroy(&dimvector); + igraphmodule_handle_igraph_error(); + return NULL; + } + + igraph_vector_bool_fill(&circular, PyObject_IsTrue(o_circular)); + } else { + if (igraphmodule_PyObject_to_vector_bool_t(o_circular, &circular)) { + igraph_vector_int_destroy(&dimvector); + return NULL; + } + } + + CHECK_SSIZE_T_RANGE_POSITIVE(nei, "number of neighbors"); + + if (igraph_square_lattice(&g, &dimvector, nei, directed, mutual, &circular)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&dimvector); + igraph_vector_bool_destroy(&circular); + igraph_vector_int_destroy(&dimvector); return NULL; } - igraph_vector_destroy(&dimvector); + igraph_vector_bool_destroy(&circular); + igraph_vector_int_destroy(&dimvector); CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -2599,28 +3016,31 @@ PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, */ PyObject *igraphmodule_Graph_LCF(PyTypeObject *type, PyObject *args, PyObject *kwds) { - igraph_vector_t shifts; - long int repeats, n; - PyObject *o_shifts; + igraph_vector_int_t 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_t(o_shifts, &shifts, 0)) + 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)) { - igraph_vector_destroy(&shifts); + if (igraph_lcf(&g, n, &shifts, repeats)) { + 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); @@ -2628,54 +3048,174 @@ PyObject *igraphmodule_Graph_LCF(PyTypeObject *type, } /** \ingroup python_interface_graph - * \brief Generates a graph based on vertex types and connection preferences + * \brief Generates a graph with a specified degree sequence * \return a reference to the newly generated Python igraph object - * \sa igraph_preference_game + * \sa igraph_realize_degree_sequence */ -PyObject *igraphmodule_Graph_Preference(PyTypeObject * type, - PyObject * args, PyObject * kwds) -{ +PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, + PyObject *args, PyObject *kwds) { + + 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; + PyObject *edge_types_o = Py_None, *method_o = Py_None; igraphmodule_GraphObject *self; igraph_t g; - long n, types; - PyObject *type_dist, *pref_matrix; - PyObject *directed = Py_False; - PyObject *loops = Py_False; - igraph_matrix_t pm; - igraph_vector_t td; - igraph_vector_t type_vec; - PyObject *type_vec_o; - PyObject *attribute_key = Py_None; - igraph_bool_t store_attribs; - char *kwlist[] = - { "n", "type_dist", "pref_matrix", "attribute", "directed", "loops", -NULL }; + 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; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "lO!O!|OOO", kwlist, - &n, &PyList_Type, &type_dist, - &PyList_Type, &pref_matrix, - &attribute_key, &directed, &loops)) + /* allowed edge types */ + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) return NULL; - types = PyList_Size(type_dist); + /* methods */ + if (igraphmodule_PyObject_to_realize_degseq_t(method_o, &method)) + return NULL; - if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm)) return NULL; - if (igraphmodule_PyObject_float_to_vector_t(type_dist, &td)) { + /* Outdegree vector */ + if (igraphmodule_PyObject_to_vector_int_t(outdeg_o, &outdeg)) + return NULL; + + /* Indegree vector, Py_None means undirected graph */ + if (indeg_o != Py_None) { + if (igraphmodule_PyObject_to_vector_int_t(indeg_o, &indeg)) { + igraph_vector_int_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_int_destroy(&outdeg); + if (indegp != 0) { + igraph_vector_int_destroy(indegp); + } + igraphmodule_handle_igraph_error(); + return NULL; + } + + igraph_vector_int_destroy(&outdeg); + if (indegp != 0) { + igraph_vector_int_destroy(indegp); + } + + CREATE_GRAPH_FROM_TYPE(self, g, 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_bipartite_degree_sequence + */ +PyObject *igraphmodule_Graph_Realize_Bipartite_Degree_Sequence(PyTypeObject *type, + PyObject *args, PyObject *kwds) { + + igraph_vector_int_t degrees1, degrees2; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; + igraph_realize_degseq_t method = IGRAPH_REALIZE_DEGSEQ_SMALLEST; + PyObject *degrees1_o, *degrees2_o; + PyObject *edge_types_o = Py_None, *method_o = Py_None; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "degrees1", "degrees2", "allowed_edge_types", "method", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", kwlist, + °rees1_o, °rees2_o, &edge_types_o, &method_o)) + return NULL; + + /* allowed edge types */ + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + + /* methods */ + if (igraphmodule_PyObject_to_realize_degseq_t(method_o, &method)) + return NULL; + + /* First degree vector */ + if (igraphmodule_PyObject_to_vector_int_t(degrees1_o, °rees1)) + return NULL; + + /* Second degree vector */ + if (igraphmodule_PyObject_to_vector_int_t(degrees2_o, °rees2)) { + igraph_vector_int_destroy(°rees1); + return NULL; + } + + if (igraph_realize_bipartite_degree_sequence(&g, °rees1, °rees2, allowed_edge_types, method)) { + igraph_vector_int_destroy(°rees1); + igraph_vector_int_destroy(°rees2); + igraphmodule_handle_igraph_error(); + return NULL; + } + + igraph_vector_int_destroy(°rees1); + igraph_vector_int_destroy(°rees2); + + 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 + * \sa igraph_preference_game + */ +PyObject *igraphmodule_Graph_Preference(PyTypeObject * type, + PyObject * args, PyObject * kwds) +{ + igraphmodule_GraphObject *self; + igraph_t g; + Py_ssize_t n, types; + PyObject *type_dist, *pref_matrix; + PyObject *directed = Py_False; + PyObject *loops = Py_False; + igraph_matrix_t pm; + igraph_vector_t td; + igraph_vector_int_t type_vec; + PyObject *type_vec_o; + PyObject *attribute_key = Py_None; + igraph_bool_t store_attribs; + + char *kwlist[] = + { "n", "type_dist", "pref_matrix", "attribute", "directed", "loops", +NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOO|OOO", kwlist, + &n, &type_dist, &pref_matrix, + &attribute_key, &directed, &loops)) + return NULL; + + CHECK_SSIZE_T_RANGE(n, "vertex count"); + types = PyList_Size(type_dist); + + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + return NULL; + } + if (igraphmodule_PyObject_float_to_vector_t(type_dist, &td)) { igraph_matrix_destroy(&pm); return NULL; } store_attribs = (attribute_key && attribute_key != Py_None); - if (store_attribs && igraph_vector_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))) { @@ -2683,18 +3223,18 @@ NULL }; igraph_matrix_destroy(&pm); igraph_vector_destroy(&td); if (store_attribs) - igraph_vector_destroy(&type_vec); + igraph_vector_int_destroy(&type_vec); return NULL; } CREATE_GRAPH_FROM_TYPE(self, g, type); - if (store_attribs) { - type_vec_o = igraphmodule_vector_t_to_PyList(&type_vec, IGRAPHMODULE_TYPE_INT); + if (self != NULL && store_attribs) { + type_vec_o = igraphmodule_vector_int_t_to_PyList(&type_vec); if (type_vec_o == 0) { igraph_matrix_destroy(&pm); igraph_vector_destroy(&td); - igraph_vector_destroy(&type_vec); + igraph_vector_int_destroy(&type_vec); Py_DECREF(self); return NULL; } @@ -2704,14 +3244,14 @@ NULL }; Py_DECREF(type_vec_o); igraph_matrix_destroy(&pm); igraph_vector_destroy(&td); - igraph_vector_destroy(&type_vec); + igraph_vector_int_destroy(&type_vec); Py_DECREF(self); return NULL; } } Py_DECREF(type_vec_o); - igraph_vector_destroy(&type_vec); + igraph_vector_int_destroy(&type_vec); } igraph_matrix_destroy(&pm); @@ -2730,12 +3270,12 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, types; + Py_ssize_t n, in_types, out_types; PyObject *type_dist_matrix, *pref_matrix; PyObject *loops = Py_False; igraph_matrix_t pm; igraph_matrix_t td; - igraph_vector_t in_type_vec, out_type_vec; + igraph_vector_int_t in_type_vec, out_type_vec; PyObject *type_vec_o; PyObject *attribute_key = Py_None; igraph_bool_t store_attribs; @@ -2743,44 +3283,49 @@ 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, - &n, &PyList_Type, &type_dist_matrix, - &PyList_Type, &pref_matrix, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOO|OO", kwlist, + &n, &type_dist_matrix, + &pref_matrix, &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)) { + CHECK_SSIZE_T_RANGE(n, "vertex count"); + + if (igraphmodule_PyObject_to_matrix_t(pref_matrix, &pm, "pref_matrix")) { + return NULL; + } + if (igraphmodule_PyObject_to_matrix_t(type_dist_matrix, &td, "type_dist_matrix")) { 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)) { + 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_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_destroy(&in_type_vec); + igraph_vector_int_destroy(&in_type_vec); igraphmodule_handle_igraph_error(); return NULL; } } - if (igraph_asymmetric_preference_game(&g, (igraph_integer_t) n, - (igraph_integer_t) 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))) { igraphmodule_handle_igraph_error(); - 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); igraph_matrix_destroy(&td); return NULL; @@ -2788,14 +3333,14 @@ 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, + if (self != NULL && store_attribs) { + type_vec_o = igraphmodule_vector_int_t_pair_to_PyList(&in_type_vec, &out_type_vec); if (type_vec_o == NULL) { + 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; } @@ -2803,18 +3348,18 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, if (PyDict_SetItem(ATTR_STRUCT_DICT(&self->g)[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); @@ -2822,6 +3367,43 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, return (PyObject *) self; } + +/** \ingroup python_interface_graph + * \brief Generates a tree graph based on a Prufer sequence + * \return a reference to the newly generated Python igraph object + * \sa igraph_from_prufer + */ +PyObject *igraphmodule_Graph_Prufer( + PyTypeObject * type, PyObject * args, PyObject * kwds +) { + igraphmodule_GraphObject *self; + igraph_t g; + PyObject *seq_o; + igraph_vector_int_t seq; + + static char *kwlist[] = { "seq", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &seq_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vector_int_t(seq_o, &seq)) { + return NULL; + } + + if (igraph_from_prufer(&g, &seq)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&seq); + return NULL; + } + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + igraph_vector_int_destroy(&seq); + + return (PyObject *) self; +} + /** \ingroup python_interface_graph * \brief Generates a bipartite graph based on the Erdos-Renyi model * \return a reference to the newly generated Python igraph object @@ -2832,20 +3414,28 @@ 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; - PyObject *directed_o = Py_False, *neimode_o = NULL; + PyObject *directed_o = Py_False, *neimode_o = NULL, *edge_labeled_o = Py_False; + PyObject *edge_types_o = Py_None; igraph_vector_bool_t vertex_types; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; PyObject *vertex_types_o; + igraph_error_t retval; - static char *kwlist[] = { "n1", "n2", "p", "m", "directed", "neimode", NULL }; + static char *kwlist[] = { + "n1", "n2", "p", "m", "directed", "neimode", "allowed_edge_types", "edge_labeled", NULL + }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|dlOO", kwlist, - &n1, &n2, &p, &m, &directed_o, &neimode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|dnOOOO", kwlist, + &n1, &n2, &p, &m, &directed_o, &neimode_o, + &edge_types_o, &edge_labeled_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."); @@ -2857,37 +3447,51 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, return NULL; } - t = (m == -1) ? IGRAPH_ERDOS_RENYI_GNP : IGRAPH_ERDOS_RENYI_GNM; - if (igraphmodule_PyObject_to_neimode_t(neimode_o, &neimode)) return NULL; + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + if (igraph_vector_bool_init(&vertex_types, n1+n2)) { - igraphmodule_handle_igraph_error(); - return NULL; + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (m == -1) { + /* GNP model */ + retval = igraph_bipartite_game_gnp( + &g, &vertex_types, n1, n2, p, PyObject_IsTrue(directed_o), neimode, + allowed_edge_types, PyObject_IsTrue(edge_labeled_o) + ); + } else { + /* GNM model */ + retval = igraph_bipartite_game_gnm( + &g, &vertex_types, n1, n2, m, PyObject_IsTrue(directed_o), neimode, + allowed_edge_types, PyObject_IsTrue(edge_labeled_o) + ); } - 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); + if (retval) { igraphmodule_handle_igraph_error(); return NULL; } CREATE_GRAPH_FROM_TYPE(self, g, type); + if (self == NULL) { + return NULL; + } 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); } /** \ingroup python_interface_graph - * \brief Generates a graph based on sort of a "windowed" Barabasi-Albert model + * \brief Generates a graph based on sort of a "windowed" Barabási-Albert model * \return a reference to the newly generated Python igraph object * \sa igraph_recent_degree_game */ @@ -2896,50 +3500,52 @@ PyObject *igraphmodule_Graph_Recent_Degree(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, m = 0, window = 0; + Py_ssize_t n, window = 0; float power = 0.0f, zero_appeal = 0.0f; - igraph_vector_t outseq; + igraph_int_t m = 0; + igraph_vector_int_t outseq; + igraph_bool_t has_outseq = false; PyObject *m_obj, *outpref = Py_False, *directed = Py_False; char *kwlist[] = { "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 (PyInt_Check(m_obj)) { - m = PyInt_AsLong(m_obj); - igraph_vector_init(&outseq, 0); - } - else if (PyList_Check(m_obj)) { - if (igraphmodule_PyObject_to_vector_t(m_obj, &outseq, 1)) { + if (PyLong_Check(m_obj)) { + if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { + return NULL; + } + } else if (PyList_Check(m_obj)) { + if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { // something bad happened during conversion return NULL; } + has_outseq = true; } - if (igraph_recent_degree_game(&g, (igraph_integer_t) n, - (igraph_real_t) power, - (igraph_integer_t) window, - (igraph_integer_t) m, &outseq, + if (igraph_recent_degree_game(&g, n, power, window, m, has_outseq ? &outseq : NULL, PyObject_IsTrue(outpref), - (igraph_real_t) zero_appeal, + zero_appeal, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } return NULL; } - igraph_vector_destroy(&outseq); + if (has_outseq) { + igraph_vector_int_destroy(&outseq); + } CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -2954,26 +3560,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; } @@ -2993,30 +3593,33 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long int n; - PyObject *block_sizes_o, *pref_matrix_o; + PyObject *block_sizes_o, *pref_matrix_o, *edge_types_o = Py_None; PyObject *directed_o = Py_False; - PyObject *loops_o = Py_False; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; igraph_matrix_t pref_matrix; igraph_vector_int_t block_sizes; - static char *kwlist[] = { "n", "pref_matrix", "block_sizes", "directed", - "loops", NULL }; + static char *kwlist[] = { "pref_matrix", "block_sizes", "directed", "allowed_edge_types", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OO", kwlist, + &pref_matrix_o, + &block_sizes_o, + &directed_o, &edge_types_o)) + return NULL; + + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "lO!O!|OO", kwlist, - &n, &PyList_Type, &pref_matrix_o, - &PyList_Type, &block_sizes_o, - &directed_o, &loops_o)) + if (igraphmodule_PyObject_to_matrix_t(pref_matrix_o, &pref_matrix, "pref_matrix")) { return NULL; + } - 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, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), allowed_edge_types)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&pref_matrix); igraph_vector_int_destroy(&block_sizes); @@ -3027,6 +3630,7 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, igraph_vector_int_destroy(&block_sizes); CREATE_GRAPH_FROM_TYPE(self, g, type); + return (PyObject *) self; } @@ -3038,7 +3642,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; @@ -3046,28 +3650,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; } @@ -3087,20 +3685,23 @@ 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; + PyObject *edge_types_o = Py_None; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; igraph_vector_t fitness_out, fitness_in; static char *kwlist[] = { "m", "fitness_out", "fitness_in", - "loops", "multiple", "fitness", NULL }; + "allowed_edge_types", "fitness", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|OOOOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OOOO", kwlist, &m, &fitness_out_o, &fitness_in_o, - &loops, &multiple, &fitness_o)) + &edge_types_o, &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" */ @@ -3112,6 +3713,9 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, return NULL; } + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + if (igraphmodule_PyObject_float_to_vector_t(fitness_out_o, &fitness_out)) return NULL; @@ -3122,9 +3726,9 @@ 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))) { + allowed_edge_types)) { igraph_vector_destroy(&fitness_out); if (fitness_in_o != Py_None) igraph_vector_destroy(&fitness_in); @@ -3137,6 +3741,7 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, igraph_vector_destroy(&fitness_in); CREATE_GRAPH_FROM_TYPE(self, g, type); + return (PyObject *) self; } @@ -3150,20 +3755,27 @@ 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 *edge_types_o = Py_None; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; PyObject *finite_size_correction = Py_True; static char *kwlist[] = { "n", "m", "exponent_out", "exponent_in", - "loops", "multiple", "finite_size_correction", "exponent", NULL }; + "allowed_edge_types", "finite_size_correction", "exponent", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|ffOOOf", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|ffOOf", kwlist, &n, &m, &exponent_out, &exponent_in, - &loops, &multiple, &finite_size_correction, + &edge_types_o, &finite_size_correction, &exponent)) return NULL; + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE(m, "edge count"); + + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; + /* This trickery allows us to use "exponent" or "exponent_out" as * keyword argument, with "exponent_out" taking precedence over * "exponent" */ @@ -3175,15 +3787,15 @@ 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, - PyObject_IsTrue(loops), PyObject_IsTrue(multiple), + if (igraph_static_power_law_game(&g, n, m, exponent_out, exponent_in, + allowed_edge_types, PyObject_IsTrue(finite_size_correction))) { igraphmodule_handle_igraph_error(); return NULL; } CREATE_GRAPH_FROM_TYPE(self, g, type); + return (PyObject *) self; } @@ -3195,34 +3807,77 @@ 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 (igraphmodule_PyObject_to_tree_mode_t(tree_mode_o, &mode)) { + if (igraph_kary_tree(&g, n, children, mode)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + CREATE_GRAPH_FROM_TYPE(self, g, 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: + * - 'prufer' aka sample Prüfer 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 + */ +PyObject *igraphmodule_Graph_Tree_Game(PyTypeObject * type, + PyObject * args, PyObject * kwds) +{ + 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, "n|OO", kwlist, + &n, &directed, &tree_method_o)) + return NULL; + + CHECK_SSIZE_T_RANGE(n, "vertex count"); + + if (igraphmodule_PyObject_to_random_tree_t(tree_method_o, &tree_method)) { return NULL; } - if (igraph_tree(&g, (igraph_integer_t) n, (igraph_integer_t) children, mode)) { + if (igraph_tree_game(&g, n, PyObject_IsTrue(directed), tree_method)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -3232,6 +3887,47 @@ PyObject *igraphmodule_Graph_Tree(PyTypeObject * type, return (PyObject *) self; } +/** \ingroup python_interface_graph + * \brief Generates a regular triangular lattice + * \return a reference to the newly generated Python igraph object + * \sa igraph_triangular_lattice + */ +PyObject *igraphmodule_Graph_Triangular_Lattice(PyTypeObject * type, + PyObject * args, PyObject * kwds) +{ + igraph_vector_int_t dimvector; + igraph_bool_t directed; + igraph_bool_t mutual; + PyObject *o_directed = Py_False, *o_mutual = Py_True; + PyObject *o_dimvector = Py_None; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "dim", "directed", "mutual", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, + &o_dimvector, &o_directed, &o_mutual)) + return NULL; + + directed = PyObject_IsTrue(o_directed); + mutual = PyObject_IsTrue(o_mutual); + + if (igraphmodule_PyObject_to_vector_int_t(o_dimvector, &dimvector)) + return NULL; + + if (igraph_triangular_lattice(&g, &dimvector, directed, mutual)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&dimvector); + return NULL; + } + + igraph_vector_int_destroy(&dimvector); + + 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 @@ -3240,22 +3936,27 @@ PyObject *igraphmodule_Graph_Tree(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; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; + PyObject* edge_types_o = Py_None; igraphmodule_GraphObject *self; igraph_t g; - static char *kwlist[] = { "dim", "size", "nei", "p", "loops", "multiple", NULL }; + static char *kwlist[] = { "dim", "size", "nei", "p", "allowed_edge_types", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnnd|O", kwlist, + &dim, &size, &nei, &p, &edge_types_o)) + return NULL; + + CHECK_SSIZE_T_RANGE(dim, "dimensionality"); + CHECK_SSIZE_T_RANGE(size, "size"); + CHECK_SSIZE_T_RANGE(nei, "number of neighbors"); - if (!PyArg_ParseTupleAndKeywords(args, kwds, "llld|OO", kwlist, - &dim, &size, &nei, &p, &loops, &multiple)) + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) 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))) { + if (igraph_watts_strogatz_game(&g, dim, size, nei, p, allowed_edge_types)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -3275,54 +3976,62 @@ PyObject *igraphmodule_Graph_Weighted_Adjacency(PyTypeObject * type, igraphmodule_GraphObject *self; igraph_t g; igraph_matrix_t m; - PyObject *matrix, *mode_o = Py_None, *attr_o = Py_None, *s = 0; - PyObject *loops = Py_True; - char* attr = 0; + PyObject *matrix, *mode_o = Py_None; + PyObject *loops_o = Py_None, *weights_o; igraph_adjacency_t mode = IGRAPH_ADJ_DIRECTED; + igraph_loops_t loops = IGRAPH_LOOPS_ONCE; + igraph_vector_t weights; - static char *kwlist[] = { "matrix", "mode", "attr", "loops", NULL }; + static char *kwlist[] = { "matrix", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOO", kwlist, - &PyList_Type, &matrix, &mode_o, &attr_o, - &loops)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, + &matrix, &mode_o, &loops_o)) return NULL; if (igraphmodule_PyObject_to_adjacency_t(mode_o, &mode)) return NULL; - if (attr_o != Py_None) { - s = PyObject_Str(attr_o); - if (s) { - attr = PyString_CopyAsString(s); - if (attr == 0) - return NULL; - } else return NULL; + /* mapping of Py_True is different from what igraphmodule_PyObject_to_loops_t + * assumes so we handle it separately */ + if (loops_o == Py_True) { + loops = IGRAPH_LOOPS_ONCE; + } else if (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) + return NULL; + + if (igraphmodule_PyObject_to_matrix_t(matrix, &m, "matrix")) { + return NULL; } - if (igraphmodule_PyList_to_matrix_t(matrix, &m)) { - if (attr != 0) - free(attr); - PyErr_SetString(PyExc_TypeError, - "Error while converting adjacency matrix"); + if (igraph_vector_init(&weights, 0)) { + igraphmodule_handle_igraph_error(); + igraph_matrix_destroy(&m); return NULL; } - if (igraph_weighted_adjacency(&g, &m, mode, attr ? attr : "weight", - PyObject_IsTrue(loops))) { + if (igraph_weighted_adjacency(&g, &m, mode, &weights, loops)) { igraphmodule_handle_igraph_error(); - if (attr != 0) - free(attr); igraph_matrix_destroy(&m); + igraph_vector_destroy(&weights); return NULL; } - if (attr != 0) - free(attr); igraph_matrix_destroy(&m); CREATE_GRAPH_FROM_TYPE(self, g, type); + if (self == NULL) { + return NULL; + } - return (PyObject *) self; + weights_o = igraphmodule_vector_t_to_PyList(&weights, IGRAPHMODULE_TYPE_FLOAT); + if (!weights_o) { + Py_DECREF(self); + igraph_vector_destroy(&weights); + return NULL; + } + + igraph_vector_destroy(&weights); + + return Py_BuildValue("NN", (PyObject *) self, weights_o); } /********************************************************************** @@ -3334,23 +4043,23 @@ PyObject *igraphmodule_Graph_Weighted_Adjacency(PyTypeObject * type, * \return the list of articulation points in a PyObject * \sa igraph_articulation_points */ -PyObject *igraphmodule_Graph_articulation_points(igraphmodule_GraphObject *self) { - igraph_vector_t res; +PyObject *igraphmodule_Graph_articulation_points(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)) { + igraph_vector_int_t res; PyObject *o; - if (igraph_vector_init(&res, 0)) { - igraphmodule_handle_igraph_error(); - return NULL; + 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; } @@ -3360,22 +4069,33 @@ PyObject *igraphmodule_Graph_articulation_points(igraphmodule_GraphObject *self) */ PyObject *igraphmodule_Graph_assortativity_nominal(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "types", "directed", NULL }; - PyObject *types_o = Py_None, *directed = Py_True; + static char *kwlist[] = { "types", "directed", "normalized", "weights", NULL }; + PyObject *types_o = Py_None, *weights_o = Py_None, *directed = Py_True, *normalized = Py_True; igraph_real_t res; - int ret; - igraph_vector_t *types = 0; + igraph_error_t ret; + igraph_vector_int_t *types = 0; + igraph_vector_t *weights = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &types_o, &directed)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &types_o, &directed, &normalized, &weights_o)) 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 (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + if (types) { igraph_vector_int_destroy(types); free(types); } + return NULL; + } + + ret = igraph_assortativity_nominal(&self->g, weights, types, &res, PyObject_IsTrue(directed), + PyObject_IsTrue(normalized)); if (types) { - igraph_vector_destroy(types); free(types); + igraph_vector_int_destroy(types); free(types); + } + + if (weights) { + igraph_vector_destroy(weights); free(weights); } if (ret) { @@ -3383,7 +4103,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 @@ -3392,33 +4112,43 @@ PyObject *igraphmodule_Graph_assortativity_nominal(igraphmodule_GraphObject *sel */ PyObject *igraphmodule_Graph_assortativity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "types1", "types2", "directed", NULL }; - PyObject *types1_o = Py_None, *types2_o = Py_None, *directed = Py_True; + static char *kwlist[] = { "types1", "types2", "directed", "normalized", "weights", NULL }; + PyObject *types1_o = Py_None, *types2_o = Py_None, *directed = Py_True, *normalized = Py_True; + PyObject *weights_o = Py_None; igraph_real_t res; - int ret; - igraph_vector_t *types1 = 0, *types2 = 0; + igraph_error_t ret; + igraph_vector_t *types1 = 0, *types2 = 0, *weights = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &types1_o, &types2_o, &directed)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOO", kwlist, &types1_o, &types2_o, &directed, &normalized, &weights_o)) return NULL; - if (igraphmodule_attrib_to_vector_t(types1_o, self, &types1, ATTRIBUTE_TYPE_VERTEX)) + if (igraphmodule_attrib_to_vector_t(types1_o, self, &types1, ATTRIBUTE_TYPE_VERTEX)) { return NULL; + } + if (igraphmodule_attrib_to_vector_t(types2_o, self, &types2, ATTRIBUTE_TYPE_VERTEX)) { if (types1) { igraph_vector_destroy(types1); free(types1); } return NULL; } - ret = igraph_assortativity(&self->g, types1, types2, &res, PyObject_IsTrue(directed)); + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + if (types1) { igraph_vector_destroy(types1); free(types1); } + if (types2) { igraph_vector_destroy(types2); free(types2); } + return NULL; + } + + ret = igraph_assortativity(&self->g, weights, types1, types2, &res, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); if (types1) { igraph_vector_destroy(types1); free(types1); } if (types2) { igraph_vector_destroy(types2); free(types2); } + if (weights) { igraph_vector_destroy(weights); free(weights); } if (ret) { igraphmodule_handle_igraph_error(); return NULL; } - return Py_BuildValue("d", (double)(res)); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -3439,7 +4169,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 @@ -3458,18 +4188,20 @@ PyObject *igraphmodule_Graph_authority_score( igraph_real_t value; igraph_vector_t res, *weights = 0; + /* scale is deprecated but kept for backward compatibility reasons */ + 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), + if (igraph_hub_and_authority_scores(&self->g, NULL, &res, &value, weights, igraphmodule_ARPACKOptions_get(arpack_options))) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -3479,12 +4211,12 @@ PyObject *igraphmodule_Graph_authority_score( if (weights) { igraph_vector_destroy(weights); free(weights); } - res_o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); + res_o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&res); 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(); @@ -3505,21 +4237,30 @@ PyObject *igraphmodule_Graph_average_path_length(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - char *kwlist[] = { "directed", "unconn", NULL }; + static char *kwlist[] = { "directed", "unconn", "weights", NULL }; + PyObject *weights_o = Py_None; PyObject *directed = Py_True, *unconn = Py_True; igraph_real_t res; + igraph_vector_t *weights = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O!O!", kwlist, - &PyBool_Type, &directed, - &PyBool_Type, &unconn)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &directed, &unconn, &weights_o)) return NULL; - if (igraph_average_path_length(&self->g, &res, (directed == Py_True), - (unconn == Py_True))) { + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, + ATTRIBUTE_TYPE_EDGE)) return NULL; + + if (igraph_average_path_length(&self->g, weights, &res, 0, PyObject_IsTrue(directed), PyObject_IsTrue(unconn))) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraphmodule_handle_igraph_error(); return NULL; } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + return PyFloat_FromDouble(res); } @@ -3532,26 +4273,47 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "vertices", "directed", "cutoff", "weights", - "nobigint", NULL }; + "sources", "targets", "normalized", NULL }; PyObject *directed = Py_True; + PyObject *normalized = Py_False; PyObject *vobj = Py_None, *list; PyObject *cutoff = Py_None; PyObject *weights_o = Py_None; - PyObject *nobigint = Py_True; + PyObject *sources_o = Py_None; + PyObject *targets_o = Py_None; igraph_vector_t res, *weights = 0; - igraph_bool_t return_single = 0; - igraph_vs_t vs; + igraph_bool_t return_single = false; + igraph_bool_t is_subsetted = false; + igraph_vs_t vs, sources, targets; + igraph_error_t retval; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOO", kwlist, &vobj, &directed, &cutoff, &weights_o, - &nobigint)) { + &sources_o, &targets_o, &normalized)) { return NULL; } 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(sources_o, &sources, &self->g, NULL, NULL)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraphmodule_PyObject_to_vs_t(targets_o, &targets, &self->g, NULL, NULL)) { + igraph_vs_destroy(&sources); + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraphmodule_handle_igraph_error(); + return NULL; + } + + is_subsetted = !igraph_vs_is_all(&sources) || !igraph_vs_is_all(&targets); if (igraphmodule_PyObject_to_vs_t(vobj, &vs, &self->g, &return_single, 0)) { + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); if (weights) { igraph_vector_destroy(weights); free(weights); } igraphmodule_handle_igraph_error(); return NULL; @@ -3559,31 +4321,57 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, if (igraph_vector_init(&res, 0)) { igraph_vs_destroy(&vs); + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); if (weights) { igraph_vector_destroy(weights); free(weights); } return igraphmodule_handle_igraph_error(); } if (cutoff == Py_None) { - if (igraph_betweenness(&self->g, &res, vs, PyObject_IsTrue(directed), - weights, PyObject_IsTrue(nobigint))) { + if (is_subsetted) { + retval = igraph_betweenness_subset( + &self->g, weights, &res, sources, targets, vs, PyObject_IsTrue(directed), PyObject_IsTrue(normalized) + ); + } else { + retval = igraph_betweenness(&self->g, weights, &res, vs, PyObject_IsTrue(directed), PyObject_IsTrue(normalized)); + } + + if (retval) { igraph_vs_destroy(&vs); + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); 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_Int(cutoff); + PyObject *cutoff_num = PyNumber_Float(cutoff); if (cutoff_num == NULL) { igraph_vs_destroy(&vs); + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); + igraph_vector_destroy(&res); + if (weights) { igraph_vector_destroy(weights); free(weights); } + return NULL; + } + + if (is_subsetted) { + igraph_vs_destroy(&vs); + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } + Py_DECREF(cutoff_num); + PyErr_SetString(PyExc_ValueError, "subsetting and cutoffs may not be used at the same time"); return NULL; } - if (igraph_betweenness_estimate(&self->g, &res, vs, PyObject_IsTrue(directed), - (igraph_integer_t)PyInt_AsLong(cutoff_num), weights, - PyObject_IsTrue(nobigint))) { + + if (igraph_betweenness_cutoff(&self->g, weights, &res, vs, PyObject_IsTrue(directed), + PyObject_IsTrue(normalized), PyFloat_AsDouble(cutoff_num))) { igraph_vs_destroy(&vs); + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } Py_DECREF(cutoff_num); @@ -3594,6 +4382,8 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, } else { PyErr_SetString(PyExc_TypeError, "cutoff value must be None or integer"); igraph_vs_destroy(&vs); + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; @@ -3604,8 +4394,10 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, else list = PyFloat_FromDouble(VECTOR(res)[0]); - igraph_vector_destroy(&res); igraph_vs_destroy(&vs); + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); + igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } return list; @@ -3623,7 +4415,7 @@ PyObject *igraphmodule_Graph_bibcoupling(igraphmodule_GraphObject * self, PyObject *vobj = NULL, *list; igraph_matrix_t res; igraph_vs_t vs; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &vobj)) return NULL; @@ -3660,50 +4452,49 @@ PyObject *igraphmodule_Graph_bibcoupling(igraphmodule_GraphObject * self, * \sa igraph_biconnected_components */ PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *self, - PyObject *args, PyObject *kwds) { - igraph_vector_ptr_t components; - igraph_vector_t points; + PyObject *args, PyObject *kwds) { + igraph_vector_int_list_t components; + igraph_vector_int_t points; igraph_bool_t return_articulation_points; - igraph_integer_t no; - PyObject *result, *aps=Py_False; + igraph_int_t no; + PyObject *result_o, *aps=Py_False; static char* kwlist[] = {"return_articulation_points", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &aps)) return NULL; return_articulation_points = PyObject_IsTrue(aps); - if (igraph_vector_ptr_init(&components, 0)) { - igraphmodule_handle_igraph_error(); - return NULL; + if (igraph_vector_int_list_init(&components, 0)) { + 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_int_init(&points, 0)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_list_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); - if (return_articulation_points) igraph_vector_destroy(&points); + igraph_vector_int_list_destroy(&components); + 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); - igraph_vector_ptr_destroy_all(&components); + result_o = igraphmodule_vector_int_list_t_to_PyList(&components); + igraph_vector_int_list_destroy(&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); - return Py_BuildValue("NN", result, result2); /* references stolen */ + PyObject *result2; + igraph_vector_int_sort(&points); + result2 = igraphmodule_vector_int_t_to_PyList(&points); + igraph_vector_int_destroy(&points); + return Py_BuildValue("NN", result_o, result2); /* references stolen */ } - - return result; + + return result_o; } /** \ingroup python_interface_graph @@ -3712,24 +4503,34 @@ 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; - 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; - 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; + 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; @@ -3738,14 +4539,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; @@ -3754,16 +4555,15 @@ 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)) { - igraph_vector_destroy(&multiplicities[0]); - igraph_vector_destroy(&multiplicities[1]); + probe1)) { + 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; } } 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; @@ -3774,30 +4574,38 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel if (p_g1) { CREATE_GRAPH(result1, g1); + if (result1 == NULL) { + return NULL; + } } + if (p_g2) { CREATE_GRAPH(result2, g2); + if (result2 == NULL) { + Py_XDECREF(result1); + return NULL; + } } 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) { @@ -3824,10 +4632,10 @@ 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; + igraph_int_t vcount1, vcount2, ecount1, ecount2; static char* kwlist[] = {"types", NULL}; @@ -3835,10 +4643,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; @@ -3846,7 +4654,111 @@ 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 + * \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, PyObject* Py_UNUSED(_null)) { + igraph_vector_int_t res; + PyObject *o; + 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_int_destroy(&res); + return NULL; + } + + igraph_vector_int_sort(&res); + o = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); + + 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_of_fixed_length_tuples(&edges, 2); + igraph_vector_int_destroy(&edges); + + return res_o; } /** \ingroup python_interface_graph @@ -3858,12 +4770,12 @@ 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; igraph_neimode_t mode = IGRAPH_ALL; - int return_single = 0; + igraph_bool_t return_single = false; igraph_vs_t vs; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, &vobj, @@ -3882,15 +4794,15 @@ 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; } if (cutoff == Py_None) { - if (igraph_closeness(&self->g, &res, vs, mode, weights, - PyObject_IsTrue(normalized_o))) { + if (igraph_closeness(&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); } @@ -3898,14 +4810,14 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, return NULL; } } else if (PyNumber_Check(cutoff)) { - PyObject *cutoff_num = PyNumber_Int(cutoff); + 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_estimate(&self->g, &res, vs, mode, - (igraph_integer_t)PyInt_AsLong(cutoff_num), weights, - PyObject_IsTrue(normalized_o))) { + if (igraph_closeness_cutoff(&self->g, &res, NULL, NULL, vs, mode, weights, + PyObject_IsTrue(normalized_o), PyFloat_AsDouble(cutoff_num))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -3930,17 +4842,97 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, } /** \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 - * \sa igraph_clusters + * \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_clusters(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) +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; + igraph_bool_t return_single = false; + 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, 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); + if (weights) { igraph_vector_destroy(weights); free(weights); } + return NULL; + } + if (igraph_harmonic_centrality_cutoff(&self->g, &res, vs, mode, weights, + PyObject_IsTrue(normalized_o), PyFloat_AsDouble(cutoff_num))) { + 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 component ID for every vertex in the graph + * \sa igraph_connected_components + */ +PyObject *igraphmodule_Graph_connected_components( + igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds +) { static char *kwlist[] = { "mode", NULL }; igraph_connectedness_t mode = IGRAPH_STRONG; - igraph_vector_t res1, res2; - igraph_integer_t no; + igraph_vector_int_t res1, res2; + igraph_int_t no; PyObject *list, *mode_o = Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) @@ -3949,19 +4941,28 @@ 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); + if (igraph_vector_int_init(&res1, igraph_vcount(&self->g))) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_vector_int_init(&res2, 10)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&res1); + return NULL; + } - if (igraph_clusters(&self->g, &res1, &res2, &no, mode)) { + if (igraph_connected_components(&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; } @@ -3974,15 +4975,15 @@ PyObject *igraphmodule_Graph_constraint(igraphmodule_GraphObject * self, { static char *kwlist[] = { "vertices", "weights", NULL }; PyObject *vids_obj = Py_None, *weight_obj = Py_None, *list; - igraph_vector_t result, weights; + igraph_vector_t res, weights; igraph_vs_t vids; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; if (!PyArg_ParseTupleAndKeywords (args, kwds, "|OO", kwlist, &vids_obj, &weight_obj)) return NULL; - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_init(&res, 0)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -3990,32 +4991,33 @@ PyObject *igraphmodule_Graph_constraint(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_attribute_values(weight_obj, &weights, self, ATTRHASH_IDX_EDGE, 1.0)) { - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return NULL; } if (igraphmodule_PyObject_to_vs_t(vids_obj, &vids, &self->g, &return_single, 0)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); igraph_vector_destroy(&weights); return NULL; } - if (igraph_constraint(&self->g, &result, vids, &weights)) { + if (igraph_constraint(&self->g, &res, vids, &weights)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vids); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); igraph_vector_destroy(&weights); return NULL; } - if (!return_single) - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_FLOAT); - else - list = PyFloat_FromDouble((double)VECTOR(result)[0]); + if (!return_single) { + list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); + } else { + list = igraphmodule_real_t_to_PyObject(VECTOR(res)[0], IGRAPHMODULE_TYPE_FLOAT); + } igraph_vs_destroy(&vids); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); igraph_vector_destroy(&weights); return list; @@ -4032,7 +5034,7 @@ PyObject *igraphmodule_Graph_cocitation(igraphmodule_GraphObject * self, char *kwlist[] = { "vertices", NULL }; PyObject *vobj = NULL, *list = NULL; igraph_matrix_t res; - int return_single = 0; + igraph_bool_t return_single = false; igraph_vs_t vs; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &vobj)) @@ -4070,33 +5072,33 @@ 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_vector_int_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)) { + 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; } @@ -4112,40 +5114,51 @@ PyObject *igraphmodule_Graph_decompose(igraphmodule_GraphObject * self, char *kwlist[] = { "mode", "maxcompno", "minelements", NULL }; igraph_connectedness_t mode = IGRAPH_STRONG; PyObject *list, *mode_o = Py_None; - igraphmodule_GraphObject *o; - long maxcompno = -1, minelements = -1, n, i; - igraph_vector_ptr_t components; - igraph_t *g; + Py_ssize_t maxcompno = -1, minelements = -1; + igraph_graph_list_t components; - 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; - igraph_vector_ptr_init(&components, 3); + /* Prepare the components */ + if (igraph_graph_list_init(&components, 0)) { + PyErr_SetString(PyExc_MemoryError, ""); + return NULL; + }; + + /* Decompose in C */ if (igraph_decompose(&self->g, &components, mode, maxcompno, minelements)) { - igraph_vector_ptr_destroy(&components); + igraph_graph_list_destroy(&components); igraphmodule_handle_igraph_error(); return NULL; } - /* We have to create a Python igraph object for every graph returned */ - n = igraph_vector_ptr_size(&components); - list = PyList_New(n); - 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 - * - * 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 */ - free(g); + /* We have to create a Python igraph object for every graph returned. During + * the conversion, the graph list will be emptied as the Python list we return + * from the conversion function takes ownership of all the graphs */ + list = igraphmodule_graph_list_t_to_PyList(&components, Py_TYPE(self)); + if (!list) { + igraph_graph_list_destroy(&components); + return 0; } - igraph_vector_ptr_destroy(&components); + igraph_graph_list_destroy(&components); return list; } @@ -4157,18 +5170,21 @@ PyObject *igraphmodule_Graph_decompose(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_eccentricity(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds) { - static char *kwlist[] = { "vertices", "mode", NULL }; - PyObject *vobj = Py_None, *list = NULL, *mode_o = Py_None; + static char *kwlist[] = { "vertices", "mode", "weights", NULL }; + PyObject *vobj = Py_None, *list = NULL, *mode_o = Py_None, *weights_o = Py_None; igraph_vector_t res; igraph_neimode_t mode = IGRAPH_OUT; - int return_single = 0; + igraph_bool_t return_single = false; igraph_vs_t vs; + igraph_vector_t* weights; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &vobj, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &vobj, &mode_o, &weights_o)) { 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_vs_t(vobj, &vs, &self->g, &return_single, 0)) { igraphmodule_handle_igraph_error(); @@ -4180,17 +5196,31 @@ PyObject *igraphmodule_Graph_eccentricity(igraphmodule_GraphObject* self, return igraphmodule_handle_igraph_error(); } - if (igraph_eccentricity(&self->g, &res, vs, mode)) { + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + igraph_vs_destroy(&vs); + igraph_vector_destroy(&res); + return NULL; + } + + if (igraph_eccentricity(&self->g, weights, &res, vs, mode)) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } igraph_vs_destroy(&vs); igraph_vector_destroy(&res); igraphmodule_handle_igraph_error(); return NULL; } - if (!return_single) - list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); - else + 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); @@ -4213,7 +5243,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; } @@ -4261,39 +5291,96 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "directed", "cutoff", "weights", NULL }; + static char *kwlist[] = { "directed", "cutoff", "weights", "sources", "targets", "normalized", NULL }; igraph_vector_t res, *weights = 0; - PyObject *list, *directed = Py_True, *cutoff = Py_None; + PyObject *list, *directed = Py_True, *cutoff = Py_None, *normalized = Py_False; PyObject *weights_o = Py_None; + PyObject *sources_o = Py_None; + PyObject *targets_o = Py_None; + igraph_vs_t sources; + igraph_vs_t targets; + igraph_error_t retval; + igraph_bool_t is_subsetted = false; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, - &directed, &cutoff, &weights_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOO", kwlist, + &directed, &cutoff, &weights_o, &sources_o, &targets_o, &normalized)) return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - igraph_vector_init(&res, igraph_ecount(&self->g)); + if (igraphmodule_PyObject_to_vs_t(sources_o, &sources, &self->g, NULL, NULL)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraphmodule_PyObject_to_vs_t(targets_o, &targets, &self->g, NULL, NULL)) { + igraph_vs_destroy(&sources); + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraphmodule_handle_igraph_error(); + return NULL; + } + + is_subsetted = !igraph_vs_is_all(&sources) || !igraph_vs_is_all(&targets); + + if (igraph_vector_init(&res, igraph_ecount(&self->g))) { + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraphmodule_handle_igraph_error(); + } if (cutoff == Py_None) { - if (igraph_edge_betweenness(&self->g, &res, PyObject_IsTrue(directed), weights)) { - igraphmodule_handle_igraph_error(); + if (is_subsetted) { + retval = igraph_edge_betweenness_subset( + &self->g, weights, &res, + sources, targets, igraph_ess_all(IGRAPH_EDGEORDER_ID), + PyObject_IsTrue(directed), PyObject_IsTrue(normalized) + ); + } else { + retval = igraph_edge_betweenness( + &self->g, weights, &res, igraph_ess_all(IGRAPH_EDGEORDER_ID), + PyObject_IsTrue(directed), PyObject_IsTrue(normalized) + ); + } + if (retval) { + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_vector_destroy(&res); + igraphmodule_handle_igraph_error(); return NULL; } } else if (PyNumber_Check(cutoff)) { - PyObject *cutoff_num = PyNumber_Int(cutoff); + PyObject *cutoff_num = PyNumber_Float(cutoff); if (!cutoff_num) { + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); 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_integer_t)PyInt_AsLong(cutoff_num), weights)) { - igraphmodule_handle_igraph_error(); + + if (is_subsetted) { + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraph_vector_destroy(&res); + Py_DECREF(cutoff_num); + PyErr_SetString(PyExc_ValueError, "subsetting and cutoffs may not be used at the same time"); + return NULL; + } + + if (igraph_edge_betweenness_cutoff( + &self->g, weights, &res, igraph_ess_all(IGRAPH_EDGEORDER_ID), + PyObject_IsTrue(directed), PyObject_IsTrue(normalized), PyFloat_AsDouble(cutoff_num) + )) { igraph_vector_destroy(&res); + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); if (weights) { igraph_vector_destroy(weights); free(weights); } Py_DECREF(cutoff_num); + igraphmodule_handle_igraph_error(); return NULL; } Py_DECREF(cutoff_num); @@ -4303,10 +5390,13 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, return NULL; } + igraph_vs_destroy(&targets); + igraph_vs_destroy(&sources); if (weights) { igraph_vector_destroy(weights); free(weights); } list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&res); + return list; } @@ -4318,33 +5408,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; - igraph_integer_t res; + PyObject *checks = Py_True, *source_o = Py_None, *target_o = Py_None; + igraph_int_t source = -1, target = -1; + igraph_int_t res; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &source_o, &target_o, &checks)) + return NULL; + + if (igraphmodule_PyObject_to_optional_vid(source_o, &source, &self->g)) { + return NULL; + } - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|llO", kwlist, - &source, &target, &checks)) + if (igraphmodule_PyObject_to_optional_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 @@ -4366,9 +5460,11 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( igraph_real_t value; igraph_vector_t *weights=0, res; + /* scale is deprecated but kept for backward compatibility reasons */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO!O", kwlist, &directed_o, &scale_o, &weights_o, - &igraphmodule_ARPACKOptionsType, + igraphmodule_ARPACKOptionsType, &arpack_options, &return_eigenvalue)) return NULL; @@ -4382,7 +5478,7 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; if (igraph_eigenvector_centrality(&self->g, &res, &value, - PyObject_IsTrue(directed_o), PyObject_IsTrue(scale_o), + PyObject_IsTrue(directed_o), weights, igraphmodule_ARPACKOptions_get(arpack_options))) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4391,13 +5487,13 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( } if (weights) { igraph_vector_destroy(weights); free(weights); } - - res_o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); + + res_o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&res); 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(); @@ -4417,7 +5513,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 res; igraph_fas_algorithm_t algo = IGRAPH_FAS_APPROX_EADES; PyObject *weights_o = Py_None, *result_o = NULL, *algo_o = NULL; @@ -4431,25 +5527,272 @@ PyObject *igraphmodule_Graph_feedback_arc_set( ATTRIBUTE_TYPE_EDGE)) return NULL; - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_int_init(&res, 0)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + } + + if (igraph_feedback_arc_set(&self->g, &res, weights, algo)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraph_vector_int_destroy(&res); + return NULL; + } + + if (weights) { igraph_vector_destroy(weights); free(weights); } + + result_o = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); + + return result_o; +} + + +/** \ingroup python_interface_graph + * \brief Calculates a feedback vertex set for a graph + * \return a list containing the indices in the chosen feedback vertex set + * \sa igraph_feedback_vertex_set + */ +PyObject *igraphmodule_Graph_feedback_vertex_set( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = { "weights", "method", NULL }; + igraph_vector_t* weights = 0; + igraph_vector_int_t res; + igraph_fvs_algorithm_t algo = IGRAPH_FVS_EXACT_IP; + PyObject *weights_o = Py_None, *result_o = NULL, *algo_o = NULL; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &weights_o, &algo_o)) + return NULL; + + if (igraphmodule_PyObject_to_fvs_algorithm_t(algo_o, &algo)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, + ATTRIBUTE_TYPE_VERTEX)) + return NULL; + + if (igraph_vector_int_init(&res, 0)) { if (weights) { igraph_vector_destroy(weights); free(weights); } } - if (igraph_feedback_arc_set(&self->g, &result, weights, algo)) { + if (igraph_feedback_vertex_set(&self->g, &res, weights, algo)) { if (weights) { igraph_vector_destroy(weights); free(weights); } - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&res); 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(&res); + igraph_vector_int_destroy(&res); return result_o; } +/** \ingroup python_interface_graph + * \brief Calculates a single shortest path between a source and a target vertex + * \return a list containing a single shortest path from the source to the target + * \sa igraph_get_shortest_path + */ +PyObject *igraphmodule_Graph_get_shortest_path( + igraphmodule_GraphObject *self, PyObject *args, PyObject * kwds +) { + static char *kwlist[] = { "v", "to", "weights", "mode", "output", "algorithm", NULL }; + igraph_vector_t *weights=0; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_int_t from, to; + PyObject *list, *mode_o=Py_None, *weights_o=Py_None, + *output_o=Py_None, *from_o = Py_None, *to_o=Py_None, + *algorithm_o=Py_None; + igraph_vector_int_t vec; + igraph_bool_t use_edges = false; + igraph_error_t retval; + igraphmodule_shortest_path_algorithm_t algorithm = IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OOO!O", kwlist, &from_o, + &to_o, &weights_o, &mode_o, &PyUnicode_Type, &output_o, &algorithm_o)) + return NULL; + + if (igraphmodule_PyObject_to_vpath_or_epath(output_o, &use_edges)) + return NULL; + + if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) + return NULL; + + if (igraphmodule_PyObject_to_vid(to_o, &to, &self->g)) + return NULL; + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + return NULL; + + if (igraphmodule_PyObject_to_shortest_path_algorithm_t(algorithm_o, &algorithm)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, + ATTRIBUTE_TYPE_EDGE)) return NULL; + + if (igraph_vector_int_init(&vec, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* Call the C function */ + switch (algorithm) { + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO: + retval = igraph_get_shortest_path( + &self->g, weights, use_edges ? NULL : &vec, use_edges ? &vec : NULL, from, to, mode + ); + break; + + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: + retval = igraph_get_shortest_path_dijkstra( + &self->g, use_edges ? NULL : &vec, use_edges ? &vec : NULL, from, to, weights, mode + ); + break; + + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD: + retval = igraph_get_shortest_path_bellman_ford( + &self->g, use_edges ? NULL : &vec, use_edges ? &vec : NULL, from, to, weights, mode + ); + break; + + default: + retval = IGRAPH_UNIMPLEMENTED; + PyErr_SetString(PyExc_ValueError, "Algorithm not supported"); + } + + if (retval) { + igraph_vector_int_destroy(&vec); + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* We don't need these anymore, the result is in vec */ + if (weights) { igraph_vector_destroy(weights); free(weights); } + + /* Convert to Python list of paths */ + list = igraphmodule_vector_int_t_to_PyList(&vec); + igraph_vector_int_destroy(&vec); + + return list ? list : NULL; +} + +typedef struct { + PyObject* func; + PyObject* graph; +} igraphmodule_i_Graph_get_shortest_path_astar_callback_data_t; + +igraph_error_t igraphmodule_i_Graph_get_shortest_path_astar_callback( + igraph_real_t *result, igraph_int_t from, igraph_int_t to, + void *extra +) { + igraphmodule_i_Graph_get_shortest_path_astar_callback_data_t* data = + (igraphmodule_i_Graph_get_shortest_path_astar_callback_data_t*)extra; + PyObject* from_o; + PyObject* to_o; + PyObject* result_o; + + from_o = igraphmodule_integer_t_to_PyObject(from); + if (from_o == NULL) { + /* Error in conversion, return 1 */ + return IGRAPH_FAILURE; + } + + to_o = igraphmodule_integer_t_to_PyObject(to); + if (to_o == NULL) { + /* Error in conversion, return 1 */ + Py_DECREF(from_o); + return IGRAPH_FAILURE; + } + + result_o = PyObject_CallFunction(data->func, "OOO", data->graph, from_o, to_o); + Py_DECREF(from_o); + Py_DECREF(to_o); + + if (result_o == NULL) { + /* Error in callback, return 1 */ + return IGRAPH_FAILURE; + } + + if (igraphmodule_PyObject_to_real_t(result_o, result)) { + /* Error in conversion, return 1 */ + Py_DECREF(result_o); + return IGRAPH_FAILURE; + } + + Py_DECREF(result_o); + return IGRAPH_SUCCESS; +} + +/** \ingroup python_interface_graph + * \brief Calculates a single shortest path between a source and a target vertex using the A-star algorithm + * \return a list containing a single shortest path from the source to the target + * \sa igraph_get_shortest_path_astar + */ +PyObject *igraphmodule_Graph_get_shortest_path_astar( + igraphmodule_GraphObject *self, PyObject *args, PyObject * kwds +) { + static char *kwlist[] = { "v", "to", "heuristics", "weights", "mode", "output", NULL }; + igraph_vector_t *weights=0; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_int_t from, to; + PyObject *list, *mode_o=Py_None, *weights_o=Py_None, + *output_o=Py_None, *from_o = Py_None, *to_o=Py_None, + *heuristics_o; + igraph_vector_int_t vec; + igraph_bool_t use_edges = false; + igraphmodule_i_Graph_get_shortest_path_astar_callback_data_t extra; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO|OOO!", kwlist, &from_o, + &to_o, &heuristics_o, &weights_o, &mode_o, &PyUnicode_Type, &output_o)) + return NULL; + + if (igraphmodule_PyObject_to_vpath_or_epath(output_o, &use_edges)) + return NULL; + + if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) + return NULL; + + if (igraphmodule_PyObject_to_vid(to_o, &to, &self->g)) + return NULL; + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, + ATTRIBUTE_TYPE_EDGE)) return NULL; + + if (igraph_vector_int_init(&vec, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + extra.func = heuristics_o; + extra.graph = (PyObject*) self; + + /* Call the C function */ + if (igraph_get_shortest_path_astar(&self->g, use_edges ? 0 : &vec, + use_edges ? &vec : 0, from, to, weights, mode, + igraphmodule_i_Graph_get_shortest_path_astar_callback, + &extra + )) { + igraph_vector_int_destroy(&vec); + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* We don't need these anymore, the result is in vec */ + if (weights) { igraph_vector_destroy(weights); free(weights); } + + /* Convert to Python list of paths */ + list = igraphmodule_vector_int_t_to_PyList(&vec); + igraph_vector_int_destroy(&vec); + + return list ? list : NULL; +} + + /** \ingroup python_interface_graph * \brief Calculates the shortest paths from/to a given node in the graph * \return a list containing shortest paths from/to the given node @@ -4459,36 +5802,35 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "v", "to", "weights", "mode", "output", NULL }; - igraph_vector_t *res, *weights=0; + static char *kwlist[] = { "v", "to", "weights", "mode", "output", "algorithm", NULL }; + igraph_vector_t *weights = NULL; igraph_neimode_t mode = IGRAPH_OUT; - long int i, j; - igraph_integer_t from, no_of_target_nodes; + igraphmodule_shortest_path_algorithm_t algorithm = IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO; + igraph_int_t from, no_of_target_nodes; igraph_vs_t to; - PyObject *list, *item, *mode_o=Py_None, *weights_o=Py_None, - *output_o=Py_None, *from_o = Py_None, *to_o=Py_None; - 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)) - return NULL; - - if (output_o == 0 || output_o == Py_None || - PyString_IsEqualToASCIIString(output_o, "vpath")) { - use_edges = 0; - } else if (PyString_IsEqualToASCIIString(output_o, "epath")) { - use_edges = 1; - } else { - PyErr_SetString(PyExc_ValueError, "output argument must be \"vpath\" or \"epath\""); + PyObject *list, *mode_o=Py_None, *weights_o=Py_None, + *output_o=Py_None, *from_o = Py_None, *to_o=Py_None, + *algorithm_o=Py_None; + igraph_vector_int_list_t veclist; + igraph_bool_t use_edges = false; + igraph_error_t retval; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOO!O", kwlist, &from_o, + &to_o, &weights_o, &mode_o, &PyUnicode_Type, &output_o, &algorithm_o)) + return NULL; + + if (igraphmodule_PyObject_to_vpath_or_epath(output_o, &use_edges)) return NULL; - } if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) return NULL; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - + + if (igraphmodule_PyObject_to_shortest_path_algorithm_t(algorithm_o, &algorithm)) + return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; @@ -4497,6 +5839,8 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * return NULL; } + /* The starting point is a single vertex, but the end is a list + * of vertices, so we need one shortest path per target vertex */ if (igraph_vs_size(&self->g, &to, &no_of_target_nodes)) { if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_vs_destroy(&to); @@ -4504,74 +5848,59 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * return NULL; } - ptrvec = (igraph_vector_ptr_t *) calloc(1, sizeof(igraph_vector_ptr_t)); - if (!ptrvec) { - PyErr_SetString(PyExc_MemoryError, ""); + /* Initialize the vector_int_list itself, size is managed internally + * by the C core function */ + if (igraph_vector_int_list_init(&veclist, 0)) { if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_vs_destroy(&to); + igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_vector_ptr_init(ptrvec, no_of_target_nodes)) { - PyErr_SetString(PyExc_MemoryError, ""); - free(ptrvec); - if (weights) { igraph_vector_destroy(weights); free(weights); } - igraph_vs_destroy(&to); - return NULL; - } + /* Call the C function */ + switch (algorithm) { + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO: + retval = igraph_get_shortest_paths( + &self->g, weights, use_edges ? NULL : &veclist, use_edges ? &veclist : NULL, + from, to, mode, NULL, NULL + ); + break; - res = (igraph_vector_t *) calloc(no_of_target_nodes, sizeof(igraph_vector_t)); - if (!res) { - PyErr_SetString(PyExc_MemoryError, ""); - igraph_vector_ptr_destroy(ptrvec); free(ptrvec); - if (weights) { igraph_vector_destroy(weights); free(weights); } - igraph_vs_destroy(&to); - return NULL; - } + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: + retval = igraph_get_shortest_paths_dijkstra( + &self->g, use_edges ? NULL : &veclist, use_edges ? &veclist : NULL, + from, to, weights, mode, NULL, NULL + ); + break; - for (i = 0; i < no_of_target_nodes; i++) { - VECTOR(*ptrvec)[i] = &res[i]; - igraph_vector_init(&res[i], 0); + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD: + retval = igraph_get_shortest_paths_bellman_ford( + &self->g, use_edges ? NULL : &veclist, use_edges ? &veclist : NULL, + from, to, weights, mode, NULL, NULL + ); + break; + + default: + retval = IGRAPH_UNIMPLEMENTED; + PyErr_SetString(PyExc_ValueError, "Algorithm not supported"); } - 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]); - free(res); - igraph_vector_ptr_destroy(ptrvec); free(ptrvec); + if (retval) { + igraph_vector_int_list_destroy(&veclist); if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_vs_destroy(&to); + igraphmodule_handle_igraph_error(); return NULL; } - igraph_vector_ptr_destroy(ptrvec); free(ptrvec); + /* We don't need these anymore, the result is in veclist */ if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_vs_destroy(&to); - list = PyList_New(no_of_target_nodes); - if (!list) { - for (j = 0; j < no_of_target_nodes; j++) igraph_vector_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); - 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]); - free(res); - return NULL; - } - } - - for (j = 0; j < no_of_target_nodes; j++) igraph_vector_destroy(&res[j]); - free(res); - return list; + /* Convert to Python list of paths */ + list = igraphmodule_vector_int_list_t_to_PyList(&veclist); + igraph_vector_int_list_destroy(&veclist); + return list ? list : NULL; } /** \ingroup python_interface_graph @@ -4584,13 +5913,12 @@ PyObject *igraphmodule_Graph_get_all_shortest_paths(igraphmodule_GraphObject * PyObject * kwds) { static char *kwlist[] = { "v", "to", "weights", "mode", NULL }; - igraph_vector_ptr_t res; + igraph_vector_int_list_t res; igraph_vector_t *weights = 0; igraph_neimode_t mode = IGRAPH_OUT; - long int i, j; - igraph_integer_t from; + igraph_int_t from; igraph_vs_t to; - PyObject *list, *item, *from_o, *mode_o=Py_None, *to_o=Py_None, *weights_o=Py_None; + PyObject *list, *from_o, *mode_o=Py_None, *to_o=Py_None, *weights_o=Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &from_o, &to_o, &weights_o, &mode_o)) @@ -4611,17 +5939,19 @@ PyObject *igraphmodule_Graph_get_all_shortest_paths(igraphmodule_GraphObject * return NULL; } - if (igraph_vector_ptr_init(&res, 1)) { + if (igraph_vector_int_list_init(&res, 0)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&to); if (weights) { igraph_vector_destroy(weights); free(weights); } 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); + igraph_vector_int_list_destroy(&res); igraph_vs_destroy(&to); if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; @@ -4630,35 +5960,76 @@ 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); + list = igraphmodule_vector_int_list_t_to_PyList(&res); + igraph_vector_int_list_destroy(&res); + return list ? list : NULL; +} +/** \ingroup python_interface_graph + * \brief Calculates the k-shortest paths from/to a given node in the graph + * \return a list containing the k-shortest paths from/to the given node + * \sa TODO I don't know what to write here : igraph_get_shortest_paths + */ +PyObject *igraphmodule_Graph_get_k_shortest_paths( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + static char *kwlist[] = { "v", "to", "k", "weights", "mode", "output", NULL }; + igraph_vector_int_list_t res; + igraph_vector_t *weights = 0; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_int_t from; + igraph_int_t to; + igraph_int_t k = 1; + PyObject *list, *from_o, *to_o; + PyObject *output_o = Py_None, *mode_o = Py_None, *weights_o = Py_None, *k_o = NULL; + igraph_bool_t use_edges = false; - j = igraph_vector_ptr_size(&res); - list = PyList_New(j); - if (!list) { - igraph_vector_ptr_destroy_all(&res); + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|OOOO", kwlist, &from_o, + &to_o, &k_o, &weights_o, &mode_o, &output_o)) + return NULL; + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + return NULL; + + if (k_o != NULL && igraphmodule_PyObject_to_integer_t(k_o, &k)) + return NULL; + + if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) + return NULL; + + if (igraphmodule_PyObject_to_vid(to_o, &to, &self->g)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) + return NULL; + + if (igraphmodule_PyObject_to_vpath_or_epath(output_o, &use_edges)) + return NULL; + + if (igraph_vector_int_list_init(&res, 0)) { + igraphmodule_handle_igraph_error(); + if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; } - for (i = 0; i < j; i++) { - item = - igraphmodule_vector_t_to_PyList((igraph_vector_t *) - igraph_vector_ptr_e(&res, i), - IGRAPHMODULE_TYPE_INT); - if (!item) { - Py_DECREF(list); - igraph_vector_ptr_destroy_all(&res); - return NULL; - } - if (PyList_SetItem(list, i, item)) { - Py_DECREF(list); - Py_DECREF(item); - igraph_vector_ptr_destroy_all(&res); - return NULL; - } + if (igraph_get_k_shortest_paths(&self->g, + weights, + /* vertices, edges */ + use_edges ? 0 : &res, + use_edges ? &res : 0, + k, from, to, mode) + ) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_list_destroy(&res); + if (weights) { igraph_vector_destroy(weights); free(weights); } + return NULL; } - igraph_vector_ptr_destroy_all(&res); - return list; + if (weights) { igraph_vector_destroy(weights); free(weights); } + + list = igraphmodule_vector_int_list_t_to_PyList(&res); + igraph_vector_int_list_destroy(&res); + + return list ? list : NULL; } /** \ingroup python_interface_graph @@ -4673,42 +6044,56 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "v", "to", "mode", NULL }; - igraph_vector_int_t res; + static char *kwlist[] = { "v", "to", "minlen", "maxlen", "mode", "max_results", NULL }; + igraph_vector_int_list_t res; igraph_neimode_t mode = IGRAPH_OUT; - igraph_integer_t from; + igraph_int_t from; igraph_vs_t to; - PyObject *list, *from_o, *mode_o=Py_None, *to_o=Py_None; + igraph_int_t minlen, maxlen, max_results = IGRAPH_UNLIMITED; + PyObject *list, *from_o, *mode_o = Py_None, *to_o = Py_None, *max_results_o = Py_None; + PyObject *minlen_o = Py_None, *maxlen_o = Py_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &from_o, - &to_o, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOOO", kwlist, &from_o, + &to_o, &minlen_o, &maxlen_o, &mode_o, &max_results_o)) return NULL; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) + if (igraphmodule_PyObject_to_integer_t(minlen_o, &minlen)) return NULL; - if (igraphmodule_PyObject_to_vs_t(to_o, &to, &self->g, 0, 0)) + if (igraphmodule_PyObject_to_integer_t(maxlen_o, &maxlen)) return NULL; - if (igraph_vector_int_init(&res, 0)) { + if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) + return NULL; + + if (igraphmodule_PyObject_to_vs_t(to_o, &to, &self->g, 0, 0)) + return NULL; + + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) + return NULL; + + if (igraph_vector_int_list_init(&res, 0)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&to); return NULL; } - if (igraph_get_all_simple_paths(&self->g, &res, from, to, mode)) { + if (igraph_get_all_simple_paths(&self->g, &res, from, to, mode, minlen, maxlen, max_results)) { igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&res); + igraph_vector_int_list_destroy(&res); igraph_vs_destroy(&to); return NULL; } igraph_vs_destroy(&to); - list = igraphmodule_vector_int_t_to_PyList(&res); + list = igraphmodule_vector_int_list_t_to_PyList(&res); + + igraph_vector_int_list_destroy(&res); + return list; } @@ -4728,8 +6113,10 @@ PyObject *igraphmodule_Graph_hub_score( igraph_real_t value; igraph_vector_t res, *weights = 0; + /* scale is deprecated but kept for backward compatibility reasons */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO!O", kwlist, &weights_o, - &scale_o, &igraphmodule_ARPACKOptionsType, + &scale_o, igraphmodule_ARPACKOptionsType, &arpack_options, &return_eigenvalue)) return NULL; @@ -4739,7 +6126,7 @@ PyObject *igraphmodule_Graph_hub_score( ATTRIBUTE_TYPE_EDGE)) return NULL; arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; - if (igraph_hub_score(&self->g, &res, &value, PyObject_IsTrue(scale_o), + if (igraph_hub_and_authority_scores(&self->g, &res, NULL, &value, weights, igraphmodule_ARPACKOptions_get(arpack_options))) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4749,12 +6136,12 @@ PyObject *igraphmodule_Graph_hub_score( if (weights) { igraph_vector_destroy(weights); free(weights); } - res_o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); + res_o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&res); 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(); @@ -4765,6 +6152,72 @@ 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); + } + + if (res) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} /** \ingroup python_interface_graph * \brief Returns the line graph of the graph @@ -4773,16 +6226,61 @@ PyObject *igraphmodule_Graph_hub_score( */ PyObject *igraphmodule_Graph_linegraph(igraphmodule_GraphObject * self) { igraph_t lg; - igraphmodule_GraphObject *result; + igraphmodule_GraphObject *result_o; if (igraph_linegraph(&self->g, &lg)) { igraphmodule_handle_igraph_error(); return NULL; } - CREATE_GRAPH(result, lg); + CREATE_GRAPH(result_o, lg); + + return (PyObject *) result_o; +} + +/** + * \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, PyObject* Py_UNUSED(_null)) { + 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 (PyObject *) result; + return Py_BuildValue("(NN)", alpha_o, alpham1_o); } /** @@ -4796,48 +6294,49 @@ PyObject *igraphmodule_Graph_neighborhood(igraphmodule_GraphObject *self, static char *kwlist[] = { "vertices", "order", "mode", "mindist", NULL }; PyObject *vobj = Py_None; PyObject *mode_o = 0; - PyObject *result; - long int order = 1; - int mindist = 0; + PyObject *result_o; + Py_ssize_t order = 1, mindist = 0; igraph_neimode_t mode = IGRAPH_ALL; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; igraph_vs_t vs; - igraph_vector_ptr_t res; + igraph_vector_int_list_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(); } - if (igraph_vector_ptr_init(&res, 0)) { + if (igraph_vector_int_list_init(&res, 0)) { igraph_vs_destroy(&vs); 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) - 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); + if (!return_single) { + result_o = igraphmodule_vector_int_list_t_to_PyList(&res); + } else { + result_o = igraphmodule_vector_int_t_to_PyList(igraph_vector_int_list_get_ptr(&res, 0)); + } - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&res, igraph_vector_destroy); - igraph_vector_ptr_destroy_all(&res); + igraph_vector_int_list_destroy(&res); - return result; + return result_o; } /** @@ -4851,46 +6350,49 @@ PyObject *igraphmodule_Graph_neighborhood_size(igraphmodule_GraphObject *self, static char *kwlist[] = { "vertices", "order", "mode", "mindist", NULL }; PyObject *vobj = Py_None; PyObject *mode_o = 0; - PyObject *result; - long int order = 1; - int mindist = 0; + PyObject *result_o; + Py_ssize_t order = 1, mindist = 0; igraph_neimode_t mode = IGRAPH_ALL; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; igraph_vs_t vs; - igraph_vector_t res; + 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(); } - if (igraph_vector_init(&res, 0)) { + if (igraph_vector_int_init(&res, 0)) { igraph_vs_destroy(&vs); 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(); } igraph_vs_destroy(&vs); - if (!return_single) - result = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); - else - result = PyInt_FromLong((long)VECTOR(res)[0]); + if (!return_single) { + result_o = igraphmodule_vector_int_t_to_PyList(&res); + } else { + result_o = igraphmodule_integer_t_to_PyObject(VECTOR(res)[0]); + } - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); - return result; + return result_o; } /** \ingroup python_interface_graph @@ -4903,7 +6405,7 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel { static char *kwlist[] = { "vertices", "directed", "damping", "reset", "reset_vertices", "weights", - "arpack_options", "implementation", "niter", "eps", NULL }; + "arpack_options", "implementation", NULL }; PyObject *directed = Py_True; PyObject *vobj = Py_None, *wobj = Py_None, *robj = Py_None, *rvsobj = Py_None; PyObject *list; @@ -4913,21 +6415,18 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel igraph_vector_t res; igraph_vector_t *reset = 0; igraph_vector_t weights; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; igraph_vs_t vs, reset_vs; igraph_pagerank_algo_t algo=IGRAPH_PAGERANK_ALGO_PRPACK; PyObject *algo_o = Py_None; - long niter=1000; - float eps=0.001f; - igraph_pagerank_power_options_t popts; void *opts; - int retval; + igraph_error_t retval; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdOOOO!Olf", kwlist, &vobj, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdOOOO!O", kwlist, &vobj, &directed, &damping, &robj, - &rvsobj, &wobj, - &igraphmodule_ARPACKOptionsType, - &arpack_options_o, &algo_o, &niter, &eps)) + &rvsobj, &wobj, + igraphmodule_ARPACKOptionsType, + &arpack_options_o, &algo_o)) return NULL; @@ -4977,22 +6476,18 @@ 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; } if (rvsobj != Py_None) - retval = igraph_personalized_pagerank_vs(&self->g, algo, &res, 0, vs, - PyObject_IsTrue(directed), damping, reset_vs, &weights, opts); + retval = igraph_personalized_pagerank_vs(&self->g, &weights, &res, 0, reset_vs, + damping, PyObject_IsTrue(directed), vs, algo, opts); else - retval = igraph_personalized_pagerank(&self->g, algo, &res, 0, vs, - PyObject_IsTrue(directed), damping, reset, &weights, opts); + retval = igraph_personalized_pagerank(&self->g, &weights, &res, 0, reset, + damping, PyObject_IsTrue(directed), vs, algo, opts); if (retval) { igraphmodule_handle_igraph_error(); @@ -5025,7 +6520,7 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel PyObject *igraphmodule_Graph_path_length_hist(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "directed", NULL }; - PyObject *directed = Py_True, *result; + PyObject *directed = Py_True, *result_o; igraph_real_t unconn; igraph_vector_t res; @@ -5039,14 +6534,14 @@ PyObject *igraphmodule_Graph_path_length_hist(igraphmodule_GraphObject *self, igraph_vector_destroy(&res); return igraphmodule_handle_igraph_error(); } - - result=igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); + + result_o=igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); igraph_vector_destroy(&res); - return Py_BuildValue("Nd", result, (double)unconn); + return Py_BuildValue("Nd", result_o, (double)unconn); } /** \ingroup python_interface_graph - * \brief Permutes the vertices of the graph + * \brief Permutes the vertices of the graph * \return the new graph as a new igraph object * \sa igraph_permute_vertices */ @@ -5054,27 +6549,27 @@ PyObject *igraphmodule_Graph_permute_vertices(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "permutation", NULL }; igraph_t pg; - igraph_vector_t perm; - igraphmodule_GraphObject *result; + igraph_vector_int_t perm; + igraphmodule_GraphObject *result_o; PyObject *list; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!", kwlist, &PyList_Type, &list)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &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); + CREATE_GRAPH(result_o, pg); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -5085,18 +6580,26 @@ PyObject *igraphmodule_Graph_permute_vertices(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "n", "mode", NULL }; - long int n = 1000; - PyObject *mode_o = Py_None; - igraph_rewiring_t mode = IGRAPH_REWIRING_SIMPLE; + static char *kwlist[] = { "n", "allowed_edge_types", NULL }; + PyObject *n_o = Py_None, *allowed_edge_types_o = Py_None; + igraph_int_t n = 10 * igraph_ecount(&self->g); /* TODO overflow check */ + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lO", kwlist, &n, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &n_o, &allowed_edge_types_o)) { return NULL; + } + + if (n_o != Py_None) { + if (igraphmodule_PyObject_to_integer_t(n_o, &n)) { + return NULL; + } + } - if (igraphmodule_PyObject_to_rewiring_t(mode_o, &mode)) + if (igraphmodule_PyObject_to_edge_type_sw_t(allowed_edge_types_o, &allowed_edge_types)) { return NULL; + } - if (igraph_rewire(&self->g, (igraph_integer_t) n, mode)) { + if (igraph_rewire(&self->g, n, allowed_edge_types, /* rewiring_stats = */ NULL)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -5112,16 +6615,20 @@ PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_rewire_edges(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "prob", "loops", "multiple", NULL }; + static char *kwlist[] = { "prob", "allowed_edge_types", NULL }; double prob; - PyObject *loops_o = Py_False, *multiple_o = Py_False; + PyObject *edge_types_o = Py_None; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "d|O", kwlist, + &prob, &edge_types_o)) + return NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "d|OO", kwlist, - &prob, &loops_o, &multiple_o)) + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) { return NULL; + } - if (igraph_rewire_edges(&self->g, prob, PyObject_IsTrue(loops_o), - PyObject_IsTrue(multiple_o))) { + if (igraph_rewire_edges(&self->g, prob, allowed_edge_types)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -5130,28 +6637,37 @@ PyObject *igraphmodule_Graph_rewire_edges(igraphmodule_GraphObject * self, } /** \ingroup python_interface_graph - * \brief Calculates shortest paths in a graph. + * \brief Calculates shortest path lengths in a graph. * \return the shortest path lengths for the given vertices * \sa igraph_shortest_paths, igraph_shortest_paths_dijkstra, * igraph_shortest_paths_bellman_ford, igraph_shortest_paths_johnson */ -PyObject *igraphmodule_Graph_shortest_paths(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) -{ - static char *kwlist[] = { "source", "target", "weights", "mode", NULL }; +PyObject *igraphmodule_Graph_distances( + igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds +) { + static char *kwlist[] = { "source", "target", "weights", "mode", "algorithm", NULL }; PyObject *from_o = NULL, *to_o = NULL, *mode_o = NULL, *weights_o = Py_None; + PyObject *algorithm_o = NULL; PyObject *list = NULL; igraph_matrix_t res; - igraph_vector_t *weights=0; + igraph_vector_t *weights = NULL; igraph_neimode_t mode = IGRAPH_OUT; - int return_single_from = 0, return_single_to = 0, e = 0; + igraphmodule_shortest_path_algorithm_t algorithm = IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO; + igraph_bool_t return_single_from = false, return_single_to = false; + igraph_error_t retval = IGRAPH_SUCCESS; igraph_vs_t from_vs, to_vs; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, - &from_o, &to_o, &weights_o, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, + &from_o, &to_o, &weights_o, &mode_o, &algorithm_o)) + return NULL; + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + return NULL; + + if (igraphmodule_PyObject_to_shortest_path_algorithm_t(algorithm_o, &algorithm)) return NULL; - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return 0; if (igraphmodule_PyObject_to_vs_t(from_o, &from_vs, &self->g, &return_single_from, 0)) { igraphmodule_handle_igraph_error(); return NULL; @@ -5175,45 +6691,41 @@ PyObject *igraphmodule_Graph_shortest_paths(igraphmodule_GraphObject * self, return igraphmodule_handle_igraph_error(); } - /* Select the most suitable algorithm */ - if (weights) { - if (igraph_vector_min(weights) > 0) { - /* Only positive weights, use Dijkstra's algorithm */ - e = igraph_shortest_paths_dijkstra(&self->g, &res, from_vs, to_vs, weights, mode); - } else { - /* There are negative weights. For a small number of sources, use Bellman-Ford. - * Otherwise, use Johnson's algorithm */ - igraph_integer_t vs_size; - e = igraph_vs_size(&self->g, &from_vs, &vs_size); - if (!e) { - if (vs_size <= 100 || mode != IGRAPH_OUT) { - e = igraph_shortest_paths_bellman_ford(&self->g, &res, from_vs, to_vs, weights, mode); - } else { - e = igraph_shortest_paths_johnson(&self->g, &res, from_vs, to_vs, weights); - } - } - } - } else { - /* No weights, use a simple BFS */ - e = igraph_shortest_paths(&self->g, &res, from_vs, to_vs, mode); + /* Call the C function */ + switch (algorithm) { + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_AUTO: + retval = igraph_distances(&self->g, weights, &res, from_vs, to_vs, mode); + break; + + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_DIJKSTRA: + retval = igraph_distances_dijkstra(&self->g, &res, from_vs, to_vs, weights, mode); + break; + + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_BELLMAN_FORD: + retval = igraph_distances_bellman_ford(&self->g, &res, from_vs, to_vs, weights, mode); + break; + + case IGRAPHMODULE_SHORTEST_PATH_ALGORITHM_JOHNSON: + retval = igraph_distances_johnson(&self->g, &res, from_vs, to_vs, weights, mode); + break; + + default: + retval = IGRAPH_UNIMPLEMENTED; + PyErr_SetString(PyExc_ValueError, "Algorithm not supported"); } - if (e) { - if (weights) igraph_vector_destroy(weights); - igraph_matrix_destroy(&res); - igraph_vs_destroy(&from_vs); - igraph_vs_destroy(&to_vs); + if (retval) { igraphmodule_handle_igraph_error(); - return NULL; + goto cleanup; } if (weights) { - igraph_vector_destroy(weights); list = igraphmodule_matrix_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); } else { list = igraphmodule_matrix_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); } +cleanup: if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_matrix_destroy(&res); @@ -5251,38 +6763,49 @@ PyObject *igraphmodule_Graph_similarity_jaccard(igraphmodule_GraphObject * self, if (pairs_o == Py_None) { /* Case #1: vertices, returning matrix */ igraph_matrix_t res; - igraph_vs_t vs; - int return_single = 0; + igraph_vs_t vs_from; + igraph_vs_t vs_to; + igraph_bool_t return_single = false; + + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_from, &self->g, &return_single, 0)) + return NULL; - if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs, &self->g, &return_single, 0)) + /* TODO(ntamas): support separate vs_from and vs_to arguments */ + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_to, &self->g, &return_single, 0)) { + igraph_vs_destroy(&vs_from); return NULL; + } if (igraph_matrix_init(&res, 0, 0)) { - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); return igraphmodule_handle_igraph_error(); } - if (igraph_similarity_jaccard(&self->g, &res, vs, mode, PyObject_IsTrue(loops))) { + if (igraph_similarity_jaccard(&self->g, &res, vs_from, vs_to, mode, PyObject_IsTrue(loops))) { igraph_matrix_destroy(&res); - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); list = igraphmodule_matrix_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); 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)) + 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; } @@ -5290,12 +6813,16 @@ PyObject *igraphmodule_Graph_similarity_jaccard(igraphmodule_GraphObject * self, if (igraph_similarity_jaccard_pairs(&self->g, &res, &edges, mode, PyObject_IsTrue(loops))) { igraph_vector_destroy(&res); - igraph_vector_destroy(&edges); + if (edges_owned) { + igraph_vector_int_destroy(&edges); + } igraphmodule_handle_igraph_error(); return NULL; } - igraph_vector_destroy(&edges); + if (edges_owned) { + igraph_vector_int_destroy(&edges); + } list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&res); @@ -5332,38 +6859,51 @@ PyObject *igraphmodule_Graph_similarity_dice(igraphmodule_GraphObject * self, if (pairs_o == Py_None) { /* Case #1: vertices, returning matrix */ igraph_matrix_t res; - igraph_vs_t vs; - int return_single = 0; + igraph_vs_t vs_from; + igraph_vs_t vs_to; + igraph_bool_t return_single = false; - if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs, &self->g, &return_single, 0)) + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_from, &self->g, &return_single, 0)) return NULL; + /* TODO(ntamas): support separate vs_from and vs_to arguments */ + if (igraphmodule_PyObject_to_vs_t(vertices_o, &vs_to, &self->g, &return_single, 0)) { + igraph_vs_destroy(&vs_from); + return NULL; + } + if (igraph_matrix_init(&res, 0, 0)) { - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); return igraphmodule_handle_igraph_error(); } - if (igraph_similarity_dice(&self->g, &res, vs, mode, PyObject_IsTrue(loops))) { + if (igraph_similarity_dice(&self->g, &res, vs_from, vs_to, mode, PyObject_IsTrue(loops))) { igraph_matrix_destroy(&res); - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vs_destroy(&vs); + igraph_vs_destroy(&vs_from); + igraph_vs_destroy(&vs_to); list = igraphmodule_matrix_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); 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)) + 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)) { + if (edges_owned) { + igraph_vector_int_destroy(&edges); + } igraphmodule_handle_igraph_error(); return NULL; } @@ -5371,12 +6911,16 @@ PyObject *igraphmodule_Graph_similarity_dice(igraphmodule_GraphObject * self, if (igraph_similarity_dice_pairs(&self->g, &res, &edges, mode, PyObject_IsTrue(loops))) { igraph_vector_destroy(&res); - igraph_vector_destroy(&edges); + if (edges_owned) { + igraph_vector_int_destroy(&edges); + } igraphmodule_handle_igraph_error(); return NULL; } - igraph_vector_destroy(&edges); + if (edges_owned) { + igraph_vector_int_destroy(&edges); + } list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&res); @@ -5397,14 +6941,14 @@ PyObject *igraphmodule_Graph_similarity_inverse_log_weighted( PyObject *vobj = NULL, *list = NULL, *mode_o = Py_None; igraph_matrix_t res; igraph_neimode_t mode = IGRAPH_ALL; - int return_single = 0; + igraph_bool_t return_single = false; igraph_vs_t vs; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &vobj, &mode_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)) return NULL; + if (igraphmodule_PyObject_to_vs_t(vobj, &vs, &self->g, &return_single, 0)) return NULL; if (igraph_matrix_init(&res, 0, 0)) { igraph_vs_destroy(&vs); @@ -5436,35 +6980,44 @@ PyObject *igraphmodule_Graph_similarity_inverse_log_weighted( PyObject *igraphmodule_Graph_spanning_tree(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "weights", NULL }; + static char *kwlist[] = { "weights", "method", NULL }; igraph_vector_t* ws = 0; - igraph_vector_t res; - PyObject *weights_o = Py_None, *result = NULL; + igraph_vector_int_t res; + igraph_mst_algorithm_t method = IGRAPH_MST_AUTOMATIC; + PyObject *weights_o = Py_None, *result_o = NULL, *method_o = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &weights_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &weights_o, &method_o)) return NULL; - if (igraph_vector_init(&res, 0)) { + if (igraphmodule_PyObject_to_mst_algorithm_t(method_o, &method)) + return NULL; + + 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); + if (igraph_minimum_spanning_tree(&self->g, &res, ws, method)) { + if (ws != 0) { + igraph_vector_destroy(ws); free(ws); + } + 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); - return result; + if (ws != 0) { + igraph_vector_destroy(ws); free(ws); + } + + result_o = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); + return result_o; } /** \ingroup python_interface_graph @@ -5508,9 +7061,9 @@ 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; + igraph_int_t from; PyObject *list = NULL, *mode_o = Py_None, *from_o = Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &from_o, &mode_o)) @@ -5522,15 +7075,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; } @@ -5546,7 +7099,7 @@ PyObject *igraphmodule_Graph_induced_subgraph(igraphmodule_GraphObject * self, static char *kwlist[] = { "vertices", "implementation", NULL }; igraph_vs_t vs; igraph_t sg; - igraphmodule_GraphObject *result; + igraphmodule_GraphObject *result_o; PyObject *list, *impl_o = Py_None; igraph_subgraph_implementation_t impl = IGRAPH_SUBGRAPH_AUTO; @@ -5567,9 +7120,9 @@ PyObject *igraphmodule_Graph_induced_subgraph(igraphmodule_GraphObject * self, igraph_vs_destroy(&vs); - CREATE_GRAPH(result, sg); + CREATE_GRAPH(result_o, sg); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -5583,7 +7136,7 @@ PyObject *igraphmodule_Graph_subgraph_edges(igraphmodule_GraphObject * self, static char *kwlist[] = { "edges", "delete_vertices", NULL }; igraph_es_t es; igraph_t sg; - igraphmodule_GraphObject *result; + igraphmodule_GraphObject *result_o; PyObject *list, *delete_vertices = Py_True; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &list, &delete_vertices)) @@ -5592,17 +7145,17 @@ PyObject *igraphmodule_Graph_subgraph_edges(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_es_t(list, &es, &self->g, 0)) return NULL; - if (igraph_subgraph_edges(&self->g, &sg, es, PyObject_IsTrue(delete_vertices))) { + if (igraph_subgraph_from_edges(&self->g, &sg, es, PyObject_IsTrue(delete_vertices))) { igraphmodule_handle_igraph_error(); igraph_es_destroy(&es); return NULL; } - CREATE_GRAPH(result, sg); + CREATE_GRAPH(result_o, sg); igraph_es_destroy(&es); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -5616,7 +7169,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)) @@ -5631,8 +7184,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 @@ -5645,7 +7197,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)) @@ -5659,8 +7211,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 @@ -5676,12 +7227,12 @@ PyObject static char *kwlist[] = { "vertices", "mode", "weights", NULL }; PyObject *vobj = NULL, *mode_o = Py_None, *list = NULL; PyObject *weights_o = Py_None; - igraph_vector_t result; + igraph_vector_t res; igraph_vector_t *weights = 0; - igraph_bool_t return_single = 0; + igraph_bool_t return_single = false; 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; @@ -5694,22 +7245,22 @@ PyObject return NULL; } - if (igraph_vector_init(&result, 0)) { + 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)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vs_destroy(&vs); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return NULL; } if (weights == 0) { - retval = igraph_transitivity_local_undirected(&self->g, &result, vs, mode); + retval = igraph_transitivity_local_undirected(&self->g, &res, vs, mode); } else { - retval = igraph_transitivity_barrat(&self->g, &result, vs, weights, mode); + retval = igraph_transitivity_barrat(&self->g, &res, vs, weights, mode); } igraph_vs_destroy(&vs); @@ -5719,16 +7270,16 @@ PyObject if (retval) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return NULL; } if (!return_single) - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_FLOAT); + list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); else - list = PyFloat_FromDouble(VECTOR(result)[0]); + list = PyFloat_FromDouble(VECTOR(res)[0]); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return list; } @@ -5746,15 +7297,15 @@ 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 res; 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; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (igraph_vector_init(&result, 0)) + if (igraph_vector_int_init(&res, 0)) return igraphmodule_handle_igraph_error(); if (!PyObject_IsTrue(warnings_o)) { @@ -5762,7 +7313,7 @@ PyObject *igraphmodule_Graph_topological_sorting(igraphmodule_GraphObject * old_handler = igraph_set_warning_handler(igraph_warning_handler_ignore); } - retval = igraph_topological_sorting(&self->g, &result, mode); + retval = igraph_topological_sorting(&self->g, &res, mode); if (!PyObject_IsTrue(warnings_o)) { /* Restore the warning handler */ @@ -5771,12 +7322,12 @@ PyObject *igraphmodule_Graph_topological_sorting(igraphmodule_GraphObject * if (retval) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&res); return NULL; } - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + list = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); return list; } @@ -5789,37 +7340,41 @@ 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, result; - igraph_integer_t res; + PyObject *checks = Py_True, *neis = Py_None, *source_o = Py_None, *target_o = Py_None; + igraph_int_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 (igraphmodule_PyObject_to_optional_vid(source_o, &source, &self->g)) { return NULL; + } + + if (igraphmodule_PyObject_to_optional_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; + 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; } } 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); - - result = (long)res; - return Py_BuildValue("l", result); + return igraphmodule_integer_t_to_PyObject(res); } /********************************************************************** @@ -5835,7 +7390,7 @@ PyObject *igraphmodule_Graph_is_bipartite(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { PyObject *types_o, *return_types_o = Py_False; igraph_vector_bool_t types; - igraph_bool_t return_types = 0, result; + igraph_bool_t return_types = false, res; static char *kwlist[] = { "return_types", NULL }; @@ -5849,13 +7404,13 @@ PyObject *igraphmodule_Graph_is_bipartite(igraphmodule_GraphObject *self, return NULL; } - if (igraph_is_bipartite(&self->g, &result, &types)) { + if (igraph_is_bipartite(&self->g, &res, &types)) { igraph_vector_bool_destroy(&types); igraphmodule_handle_igraph_error(); return NULL; } - if (result) { + if (res) { types_o = igraphmodule_vector_bool_t_to_PyList(&types); if (!types_o) { igraph_vector_bool_destroy(&types); @@ -5869,12 +7424,12 @@ PyObject *igraphmodule_Graph_is_bipartite(igraphmodule_GraphObject *self, return Py_BuildValue("OO", Py_False, Py_None); } } else { - if (igraph_is_bipartite(&self->g, &result, 0)) { + if (igraph_is_bipartite(&self->g, &res, 0)) { igraphmodule_handle_igraph_error(); return NULL; } - if (result) + if (res) Py_RETURN_TRUE; else Py_RETURN_FALSE; @@ -5882,7 +7437,7 @@ PyObject *igraphmodule_Graph_is_bipartite(igraphmodule_GraphObject *self, } /********************************************************************** - * Motifs, dyad and triad census * + * Motifs, triangles, dyad and triad census * **********************************************************************/ /** \ingroup python_interface_graph @@ -5890,16 +7445,58 @@ PyObject *igraphmodule_Graph_is_bipartite(igraphmodule_GraphObject *self, * \return the dyad census as a 3-tuple * \sa igraph_dyad_census */ -PyObject *igraphmodule_Graph_dyad_census(igraphmodule_GraphObject *self) { - igraph_integer_t mut, asym, nul; - PyObject *list; +PyObject *igraphmodule_Graph_dyad_census(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { + igraph_real_t mut, asym, nul; + PyObject *mut_o, *asym_o, *nul_o; 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; + mut_o = igraphmodule_real_t_to_PyObject(mut, IGRAPHMODULE_TYPE_INT); + if (!mut_o) { + return NULL; + } + + asym_o = igraphmodule_real_t_to_PyObject(asym, IGRAPHMODULE_TYPE_INT); + if (!asym_o) { + Py_DECREF(mut_o); + return NULL; + } + + nul_o = igraphmodule_real_t_to_PyObject(nul, IGRAPHMODULE_TYPE_INT); + if (!nul_o) { + Py_DECREF(mut_o); + Py_DECREF(asym_o); + return NULL; + } + + return Py_BuildValue("NNN", mut_o, asym_o, nul_o); +} + +/** \ingroup python_interface_graph + * \brief Lists the triangles of the graph + * \return the triangles of the graph as a list of triplets with vertex IDs + * \sa igraph_list_triangles + */ +PyObject *igraphmodule_Graph_list_triangles(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { + igraph_vector_int_t res; + PyObject *res_o; + + if (igraph_vector_int_init(&res, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_list_triangles(&self->g, &res)) { + return igraphmodule_handle_igraph_error(); + } + + res_o = igraphmodule_vector_int_t_to_PyList_of_fixed_length_tuples(&res, 3); + + igraph_vector_int_destroy(&res); + + return res_o; } typedef struct { @@ -5907,52 +7504,54 @@ 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, + const igraph_vector_int_t *vids, igraph_int_t 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; + PyObject* result_o; 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); + result_o = PyObject_CallFunction(data->func, "OOn", data->graph, vector, (Py_ssize_t) isoclass); Py_DECREF(vector); - if (result == NULL) { + if (result_o == NULL) { /* Error in callback, return 1 */ - return 1; + return IGRAPH_FAILURE; } - retval = PyObject_IsTrue(result); - Py_DECREF(result); + retval = PyObject_IsTrue(result_o); + Py_DECREF(result_o); - return retval; + return retval ? IGRAPH_STOP : IGRAPH_SUCCESS; } /** \ingroup python_interface_graph - * \brief Counts the motifs of the graph sorted by isomorphism classes + * \brief Counts the motifs of the graph sorted by isomorphism classes * \return the number of motifs found for each isomorphism class * \sa igraph_motifs_randesu */ PyObject *igraphmodule_Graph_motifs_randesu(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - igraph_vector_t result, cut_prob; - long int size=3; + igraph_vector_t res, cut_prob; + 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(); @@ -5964,27 +7563,27 @@ PyObject *igraphmodule_Graph_motifs_randesu(igraphmodule_GraphObject *self, } if (callback == Py_None) { - if (igraph_vector_init(&result, 1)) { + if (igraph_vector_init(&res, 1)) { 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, &res, size, &cut_prob)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); igraph_vector_destroy(&cut_prob); return NULL; } igraph_vector_destroy(&cut_prob); - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(&res); return list; } else if (PyCallable_Check(callback)) { 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); @@ -6009,14 +7608,16 @@ PyObject *igraphmodule_Graph_motifs_randesu(igraphmodule_GraphObject *self, 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; + igraph_real_t res; + 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(); @@ -6027,14 +7628,14 @@ 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, &res, size, &cut_prob)) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&cut_prob); return NULL; } igraph_vector_destroy(&cut_prob); - return PyInt_FromLong((long)result); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -6045,16 +7646,18 @@ PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { igraph_vector_t cut_prob; - igraph_integer_t result; - long size=3; + igraph_real_t res; + 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; @@ -6071,32 +7674,37 @@ 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); - if (igraph_motifs_randesu_estimate(&self->g, &result, (igraph_integer_t) size, - &cut_prob, (igraph_integer_t) ns, 0)) { + igraph_int_t ns; + if (igraphmodule_PyObject_to_integer_t(sample, &ns)) { + igraph_vector_destroy(&cut_prob); + return NULL; + } + if (igraph_motifs_randesu_estimate(&self->g, &res, size, &cut_prob, ns, 0)) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&cut_prob); return NULL; } } 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, + if (igraph_motifs_randesu_estimate(&self->g, &res, 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); - return PyInt_FromLong((long)result); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -6104,25 +7712,246 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s * \return the triad census as a list * \sa igraph_triad_census */ -PyObject *igraphmodule_Graph_triad_census(igraphmodule_GraphObject *self) { - igraph_vector_t result; +PyObject *igraphmodule_Graph_triad_census(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { + igraph_vector_t res; PyObject *list; - if (igraph_vector_init(&result, 16)) { + if (igraph_vector_init(&res, 16)) { return igraphmodule_handle_igraph_error(); } - if (igraph_triad_census(&self->g, &result)) { + if (igraph_triad_census(&self->g, &res)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_destroy(&res); return NULL; } - list = igraphmodule_vector_t_to_PyTuple(&result); - igraph_vector_destroy(&result); + list = igraphmodule_vector_t_to_PyTuple(&res, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(&res); return list; } +/********************************************************************** + * Cycles and cycle bases * + **********************************************************************/ + +PyObject *igraphmodule_Graph_is_acyclic(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { + igraph_bool_t res; + + if (igraph_is_acyclic(&self->g, &res)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (res) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + +PyObject *igraphmodule_Graph_is_dag(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { + igraph_bool_t res; + + if (igraph_is_dag(&self->g, &res)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (res) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + +PyObject *igraphmodule_Graph_fundamental_cycles( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + PyObject *cutoff_o = Py_None; + PyObject *start_vid_o = Py_None; + PyObject *weights_o = Py_None; + PyObject *result_o; + igraph_int_t cutoff = -1, start_vid = -1; + igraph_vector_int_list_t result; + igraph_vector_t *weights = 0; + + static char *kwlist[] = { "start_vid", "cutoff", "weights", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &start_vid_o, &cutoff_o, &weights_o)) + return NULL; + + if (igraphmodule_PyObject_to_optional_vid(start_vid_o, &start_vid, &self->g)) + return NULL; + + if (cutoff_o != Py_None && igraphmodule_PyObject_to_integer_t(cutoff_o, &cutoff)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + + if (igraph_vector_int_list_init(&result, 0)) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_fundamental_cycles(&self->g, weights, &result, start_vid, cutoff)) { + igraph_vector_int_list_destroy(&result); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&result); + igraph_vector_int_list_destroy(&result); + + return result_o; +} + +PyObject *igraphmodule_Graph_minimum_cycle_basis( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + PyObject *cutoff_o = Py_None; + PyObject *complete_o = Py_True; + PyObject *use_cycle_order_o = Py_True; + PyObject *weights_o = Py_None; + PyObject *result_o; + igraph_int_t cutoff = -1; + igraph_vector_int_list_t result; + igraph_vector_t *weights; + + static char *kwlist[] = { "cutoff", "complete", "use_cycle_order", "weights", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &cutoff_o, &complete_o, &use_cycle_order_o, &weights_o)) + return NULL; + + if (cutoff_o != Py_None && igraphmodule_PyObject_to_integer_t(cutoff_o, &cutoff)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } + + if (igraph_vector_int_list_init(&result, 0)) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_minimum_cycle_basis( + &self->g, weights, &result, cutoff, PyObject_IsTrue(complete_o), + PyObject_IsTrue(use_cycle_order_o) + )) { + igraph_vector_int_list_destroy(&result); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&result); + igraph_vector_int_list_destroy(&result); + + return result_o; +} + + +PyObject *igraphmodule_Graph_simple_cycles( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + PyObject *mode_o = Py_None; + PyObject *output_o = Py_None; + PyObject *min_cycle_length_o = Py_None; + PyObject *max_cycle_length_o = Py_None; + PyObject *max_results_o = Py_None; + + // argument defaults: no cycle limits + igraph_int_t mode = IGRAPH_OUT; + igraph_int_t min_cycle_length = -1; + igraph_int_t max_cycle_length = -1; + igraph_int_t max_results = IGRAPH_UNLIMITED; + igraph_bool_t use_edges = false; + + static char *kwlist[] = { "mode", "min", "max", "output", "max_results", NULL }; + + if ( + !PyArg_ParseTupleAndKeywords( + args, kwds, "|OOOOO", kwlist, + &mode_o, &min_cycle_length_o, &max_cycle_length_o, &output_o, &max_results_o + ) + ) + return NULL; + + if (mode_o != Py_None && igraphmodule_PyObject_to_integer_t(mode_o, &mode)) + return NULL; + + if (min_cycle_length_o != Py_None && igraphmodule_PyObject_to_integer_t(min_cycle_length_o, &min_cycle_length)) + return NULL; + + if (max_cycle_length_o != Py_None && igraphmodule_PyObject_to_integer_t(max_cycle_length_o, &max_cycle_length)) + return NULL; + + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) + return NULL; + + if (igraphmodule_PyObject_to_vpath_or_epath(output_o, &use_edges)) + return NULL; + + igraph_vector_int_list_t vertices; + if (igraph_vector_int_list_init(&vertices, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + igraph_vector_int_list_t edges; + if (igraph_vector_int_list_init(&edges, 0)) { + igraph_vector_int_list_destroy(&vertices); + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_simple_cycles( + &self->g, + use_edges ? NULL : &vertices, + use_edges ? &edges : NULL, + mode, min_cycle_length, max_cycle_length, + max_results + )) { + igraph_vector_int_list_destroy(&vertices); + igraph_vector_int_list_destroy(&edges); + igraphmodule_handle_igraph_error(); + return NULL; + } + + PyObject *result_o; + + if (use_edges) { + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&edges); + } else { + result_o = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&vertices); + } + igraph_vector_int_list_destroy(&edges); + igraph_vector_int_list_destroy(&vertices); + + return result_o; +} + /********************************************************************** * Graph layout algorithms * **********************************************************************/ @@ -6136,16 +7965,17 @@ PyObject *igraphmodule_Graph_layout_circle(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { igraph_matrix_t m; - int ret; - long dim = 2; - PyObject *result; + igraph_error_t ret; + Py_ssize_t dim = 2; + PyObject *result_o; 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; @@ -6180,11 +8010,11 @@ PyObject *igraphmodule_Graph_layout_circle(igraphmodule_GraphObject * self, return NULL; } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6196,14 +8026,15 @@ PyObject *igraphmodule_Graph_layout_random(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { igraph_matrix_t m; - int ret; - long dim = 2; - PyObject *result; + igraph_error_t ret; + Py_ssize_t dim = 2; + PyObject *result_o; 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; @@ -6225,9 +8056,9 @@ PyObject *igraphmodule_Graph_layout_random(igraphmodule_GraphObject * self, return NULL; } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6235,37 +8066,44 @@ 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; - PyObject *result; - long int width = 0, height = 0, dim = 2; - int ret; + PyObject *result_o; + 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(); @@ -6273,10 +8111,10 @@ PyObject *igraphmodule_Graph_layout_grid(igraphmodule_GraphObject* self, return NULL; } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6284,14 +8122,14 @@ 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 }; igraph_matrix_t m; - PyObject *result, *order_o = Py_None, *center_o = Py_None; - igraph_integer_t center = 0; - igraph_vector_t* order = 0; + PyObject *result_o, *order_o = Py_None, *center_o = Py_None; + igraph_int_t center = 0; + igraph_vector_int_t* order = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, ¢er_o, &order_o)) @@ -6302,17 +8140,18 @@ PyObject *igraphmodule_Graph_layout_star(igraphmodule_GraphObject* self, return NULL; } - if (igraphmodule_PyObject_to_vid(center_o, ¢er, &self->g)) + if (igraphmodule_PyObject_to_optional_vid(center_o, ¢er, &self->g)) { 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(); @@ -6322,16 +8161,16 @@ 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(); return NULL; } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6345,19 +8184,23 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * { static char *kwlist[] = { "maxiter", "epsilon", "kkconst", "seed", "minx", "maxx", - "miny", "maxy", "minz", "maxz", "dim", NULL }; + "miny", "maxy", "minz", "maxz", "dim", "weights", NULL }; igraph_matrix_t m; - igraph_bool_t use_seed=0; - int ret; - long int niter = 1000, dim = 2; - double kkconst, epsilon = 0.0; - PyObject *result, *seed_o=Py_None; + igraph_bool_t use_seed = false; + igraph_error_t ret; + igraph_int_t maxiter; + Py_ssize_t dim = 2; + igraph_real_t kkconst; + double epsilon = 0.0; + PyObject *result_o, *maxiter_o=Py_None, *seed_o=Py_None, *kkconst_o=Py_None; PyObject *minx_o=Py_None, *maxx_o=Py_None; PyObject *miny_o=Py_None, *maxy_o=Py_None; PyObject *minz_o=Py_None, *maxz_o=Py_None; + PyObject *weights_o=Py_None; igraph_vector_t *minx=0, *maxx=0; igraph_vector_t *miny=0, *maxy=0; igraph_vector_t *minz=0, *maxz=0; + igraph_vector_t *weights=0; #define DESTROY_VECTORS { \ if (minx) { igraph_vector_destroy(minx); free(minx); } \ @@ -6366,31 +8209,52 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * if (maxy) { igraph_vector_destroy(maxy); free(maxy); } \ if (minz) { igraph_vector_destroy(minz); free(minz); } \ if (maxz) { igraph_vector_destroy(maxz); free(maxz); } \ + if (weights) { igraph_vector_destroy(weights); free(weights); } \ } kkconst = igraph_vcount(&self->g); + maxiter = 50 * igraph_vcount(&self->g); - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lddOOOOOOOl", kwlist, - &niter, &epsilon, - &kkconst, &seed_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OdOOOOOOOOnO", kwlist, + &maxiter_o, &epsilon, + &kkconst_o, &seed_o, &minx_o, &maxx_o, &miny_o, &maxy_o, - &minz_o, &maxz_o, &dim)) + &minz_o, &maxz_o, &dim, &weights_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; } + /* Convert number of iterations */ + if (maxiter_o != 0 && maxiter_o != Py_None) { + if (igraphmodule_PyObject_to_integer_t(maxiter_o, &maxiter)) { + return NULL; + } + } + CHECK_SSIZE_T_RANGE_POSITIVE(maxiter, "number of iterations"); + + /* Convert Kamada-Kawai constant */ + if (kkconst_o != 0 && kkconst_o != Py_None) { + if (igraphmodule_PyObject_to_real_t(kkconst_o, &kkconst)) { + return NULL; + } + } + + /* Handle seed */ 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; - if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) return NULL; + use_seed = 1; + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { + return NULL; + } } /* Convert minimum and maximum x-y-z values */ @@ -6432,14 +8296,23 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * return NULL; } } - if (dim == 2) + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + igraph_matrix_destroy(&m); + DESTROY_VECTORS; + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (dim == 2) { ret = igraph_layout_kamada_kawai - (&self->g, &m, use_seed, (igraph_integer_t) niter, epsilon, kkconst, - /*weights=*/ 0, /*bounds*/ minx, maxx, miny, maxy); - else + (&self->g, &m, use_seed, maxiter, epsilon, kkconst, + weights, /*bounds*/ minx, maxx, miny, maxy); + } else { ret = igraph_layout_kamada_kawai_3d - (&self->g, &m, use_seed, (igraph_integer_t) niter, epsilon, kkconst, - /*weights=*/ 0, /*bounds*/ minx, maxx, miny, maxy, minz, maxz); + (&self->g, &m, use_seed, maxiter, epsilon, kkconst, + weights, /*bounds*/ minx, maxx, miny, maxy, minz, maxz); + } DESTROY_VECTORS; @@ -6451,9 +8324,22 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * return NULL; } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + /* Align layout, but only if no bounding box was specified. */ + if (minx == NULL && maxx == NULL && + miny == NULL && maxy == NULL && + minz == NULL && maxz == NULL && + igraph_vcount(&self->g) <= 1000) { + ret = igraph_layout_align(&self->g, &m); + if (ret) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6469,9 +8355,8 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel "weight_border", "weight_edge_lengths", "weight_edge_crossings", "weight_node_edge_dist", NULL }; igraph_matrix_t m; - igraph_bool_t use_seed=0; - long int maxiter=10; - long int fineiter=-1; + igraph_bool_t use_seed = false; + Py_ssize_t maxiter = 10, fineiter = -1; double cool_fact=0.75; double weight_node_dist=1.0; double weight_border=0.0; @@ -6479,16 +8364,16 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel double weight_edge_crossings=-1; double weight_node_edge_dist=-1; igraph_real_t density; - PyObject *result; + PyObject *result_o; 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, &weight_node_edge_dist)) - return NULL; + return NULL; /* Provide default parameters based on the properties of the graph */ if (fineiter < 0) { @@ -6499,7 +8384,7 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel } if (weight_edge_lengths < 0 || weight_edge_crossings < 0 || weight_node_edge_dist < 0) { - if (igraph_density(&self->g, &density, 0)) { + if (igraph_density(&self->g, 0, &density, 0)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -6527,14 +8412,14 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel return NULL; } } else { - if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) { - return NULL; - } - use_seed=1; + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { + return NULL; + } + use_seed = 1; } 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) { @@ -6543,9 +8428,19 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel return NULL; } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + /* Align layout */ + if (igraph_vcount(&self->g)) { + retval = igraph_layout_align(&self->g, &m); + if (retval) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6559,19 +8454,19 @@ PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, static char *kwlist[] = { "weights", "seed", "fixed", "options", "dim", NULL }; igraph_matrix_t m; - igraph_bool_t use_seed=0; + igraph_bool_t use_seed = false; igraph_vector_t *weights=0; - igraph_vector_bool_t *fixed=0; 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; + PyObject *result_o; + PyObject *wobj=Py_None, *fixed_o = 0, *seed_o=Py_None, *options_o=Py_None; + 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; + 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; @@ -6580,64 +8475,52 @@ PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, if (igraphmodule_PyObject_to_drl_options_t(options_o, &options)) return NULL; - if (igraph_layout_drl_options_init(&options, IGRAPH_LAYOUT_DRL_DEFAULT)) { - igraphmodule_handle_igraph_error(); - return NULL; - } - - 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; - } + if (fixed_o != 0) { + /* Apparently the "fixed" argument does not do anything in the DrL + * implementation so we throw a warning if the user tries to use it */ + 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." + ); } 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); } 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 (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { + 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); } 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); } 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); } if (retval) { igraph_matrix_destroy(&m); if (weights) { igraph_vector_destroy(weights); free(weights); } - if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } igraphmodule_handle_igraph_error(); return NULL; } if (weights) { igraph_vector_destroy(weights); free(weights); } - if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6655,16 +8538,16 @@ PyObject "seed", "minx", "maxx", "miny", "maxy", "minz", "maxz", "dim", "grid", NULL }; igraph_matrix_t m; - igraph_bool_t use_seed=0; + igraph_bool_t use_seed = false; igraph_vector_t *weights=0; igraph_vector_t *minx=0, *maxx=0; 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 *result_o; PyObject *wobj=Py_None, *seed_o=Py_None; PyObject *minx_o=Py_None, *maxx_o=Py_None; PyObject *miny_o=Py_None, *maxy_o=Py_None; @@ -6683,13 +8566,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; @@ -6705,8 +8590,10 @@ PyObject return NULL; } } else { - if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) return NULL; - use_seed=1; + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { + return NULL; + } + use_seed = 1; } /* Convert the weight parameter to a vector */ @@ -6757,11 +8644,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); } @@ -6773,12 +8660,25 @@ PyObject return NULL; } + /* Align layout, but only if no bounding box was specified. */ + if (minx == NULL && maxx == NULL && + miny == NULL && maxy == NULL && + minz == NULL && maxz == NULL && + igraph_vcount(&self->g) <= 1000) { + ret = igraph_layout_align(&self->g, &m); + if (ret) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + #undef DESTROY_VECTORS - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6793,31 +8693,33 @@ 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; - PyObject *result, *seed_o = Py_None; - igraph_bool_t use_seed=0; + double spring_constant = 1, max_sa_movement = 5, spring_length = 0; + PyObject *result_o, *seed_o = Py_None; + igraph_bool_t use_seed = false; - 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; - if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) - return NULL; + use_seed = 1; + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { + 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); @@ -6825,9 +8727,18 @@ PyObject *igraphmodule_Graph_layout_graphopt(igraphmodule_GraphObject *self, return NULL; } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6842,9 +8753,9 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, { "maxiter", "maxdelta", "area", "coolexp", "repulserad", "cellsize", "root", NULL }; igraph_matrix_t m; - PyObject *result, *root_o = Py_None; - long int maxiter = 150; - igraph_integer_t proot = -1; + PyObject *result_o, *root_o = Py_None; + Py_ssize_t maxiter = 150; + igraph_int_t proot = -1; double maxdelta, area, coolexp, repulserad, cellsize; maxdelta = igraph_vcount(&self->g); @@ -6853,11 +8764,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) @@ -6865,24 +8778,34 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, if (cellsize <= 0) cellsize = sqrt(sqrt(area)); - if (igraphmodule_PyObject_to_vid(root_o, &proot, &self->g)) + if (igraphmodule_PyObject_to_optional_vid(root_o, &proot, &self->g)) { return NULL; + } if (igraph_matrix_init(&m, 1, 1)) { igraphmodule_handle_igraph_error(); 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(); return NULL; } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6897,24 +8820,28 @@ 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; - igraphmodule_ARPACKOptionsObject *arpack_options; + Py_ssize_t dim = 2; PyObject *dist_o = Py_None; PyObject *arpack_options_o = igraphmodule_arpack_options_default; - PyObject *result; + PyObject *result_o; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlO!", kwlist, &dist_o, - &dim, &igraphmodule_ARPACKOptionsType, + /* arpack_options_o is now unused but we kept here for sake of backwards + * compatibility */ + + 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) { PyErr_NoMemory(); return NULL; } - if (igraphmodule_PyList_to_matrix_t(dist_o, dist)) { + if (igraphmodule_PyObject_to_matrix_t(dist_o, dist, "dist")) { free(dist); return NULL; } @@ -6928,9 +8855,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); } @@ -6943,9 +8868,18 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, igraph_matrix_destroy(dist); free(dist); } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -6960,11 +8894,11 @@ 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; + PyObject *result_o; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &mode_o, &roots_o, &rootlevels_o)) @@ -6975,36 +8909,39 @@ 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_vid_list(roots_o, roots_p, &self->g)) { + 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); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -7018,11 +8955,11 @@ 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; + PyObject *result_o; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &mode_o, &roots_o, &rootlevels_o)) @@ -7032,19 +8969,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; } @@ -7052,17 +8989,17 @@ 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); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -7073,78 +9010,200 @@ PyObject *igraphmodule_Graph_layout_reingold_tilford_circular( PyObject *igraphmodule_Graph_layout_sugiyama( igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "layers", "weights", "hgap", "vgap", "maxiter", - "return_extended_graph", NULL }; + static char *kwlist[] = { "layers", "weights", "hgap", "vgap", "maxiter", 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_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; - PyObject *return_extended_graph = Py_False; - PyObject *result; - igraphmodule_GraphObject *graph_o; + Py_ssize_t maxiter = 100; + PyObject *layers_o = Py_None, *weights_o = Py_None; + PyObject *layout_o, *routing_o; + igraph_matrix_list_t routing; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOddlO", kwlist, - &layers_o, &weights_o, &hgap, &vgap, &maxiter, &return_extended_graph)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOddn", kwlist, + &layers_o, &weights_o, &hgap, &vgap, &maxiter)) return NULL; - if (igraph_vector_init(&extd_to_orig_eids, 0)) { + CHECK_SSIZE_T_RANGE_POSITIVE(maxiter, "maximum number of iterations"); + + if (igraph_matrix_list_init(&routing, 0)) { igraphmodule_handle_igraph_error(); return NULL; } if (igraph_matrix_init(&m, 1, 1)) { - igraph_vector_destroy(&extd_to_orig_eids); + igraph_matrix_list_destroy(&routing); igraphmodule_handle_igraph_error(); return NULL; } - if (igraphmodule_attrib_to_vector_t(layers_o, self, &layers, - ATTRIBUTE_TYPE_VERTEX)) { - igraph_vector_destroy(&extd_to_orig_eids); + if (igraphmodule_attrib_to_vector_int_t(layers_o, self, &layers, + ATTRIBUTE_TYPE_VERTEX)) { + igraph_matrix_list_destroy(&routing); 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); + ATTRIBUTE_TYPE_EDGE)) { + if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } + igraph_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); return NULL; } - if (igraph_layout_sugiyama(&self->g, &m, - (PyObject_IsTrue(return_extended_graph) ? &extd_graph : 0), - (PyObject_IsTrue(return_extended_graph) ? &extd_to_orig_eids : 0), + if (igraph_layout_sugiyama(&self->g, &m, &routing, 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_matrix_list_destroy(&routing); igraph_matrix_destroy(&m); igraphmodule_handle_igraph_error(); return NULL; } - if (layers != 0) { igraph_vector_destroy(layers); free(layers); } - if (weights != 0) { igraph_vector_destroy(weights); free(weights); } + layout_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + if (layout_o == NULL) { + igraph_matrix_list_destroy(&routing); + igraph_matrix_destroy(&m); + return NULL; + } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - 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); - result = Py_BuildValue("NNN", result, graph_o, extd_to_orig_eids_o); + routing_o = igraphmodule_matrix_list_t_to_PyList(&routing); + if (routing_o == NULL) { + igraph_matrix_list_destroy(&routing); + return NULL; } - igraph_vector_destroy(&extd_to_orig_eids); - return (PyObject *) result; + igraph_matrix_list_destroy(&routing); + + return Py_BuildValue("NN", layout_o, routing_o); } +/** \ingroup python_interface_graph + * \brief Uniform Manifold Approximation and Projection (UMAP) + * \return the calculated coordinates as a Python list of lists + * \sa igraph_layout_umap + */ +PyObject *igraphmodule_Graph_layout_umap( + igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) +{ + static char *kwlist[] = + { "dist", "weights", "dim", "seed", "min_dist", "epochs", NULL }; + igraph_matrix_t m; + igraph_vector_t *dist = 0; + Py_ssize_t dim = 2; + double min_dist = 0.01; + Py_ssize_t epochs = 500; + PyObject *dist_o = Py_None, *weights_o = Py_None; + PyObject *seed_o = Py_None; + PyObject *result_o; + igraph_bool_t use_seed = false; + igraph_bool_t distances_are_weights = false; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOnOdn", kwlist, &dist_o, + &weights_o, &dim, &seed_o, &min_dist, &epochs)) + 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(epochs, "number of epochs"); + + /* Check whether distances and weights are both set, which is not allowed */ + if ((dist_o != Py_None) && (weights_o != Py_None)) { + PyErr_SetString(PyExc_ValueError, "dist and weights cannot be both set"); + return NULL; + } + + /* Precomputed starting layout */ + 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; + if (igraphmodule_PyObject_to_matrix_t(seed_o, &m, "seed")) { + return NULL; + } + } + + /* Initialize distances or weights */ + if (dist_o != Py_None) { + dist = (igraph_vector_t*)malloc(sizeof(igraph_vector_t)); + if (!dist) { + igraph_matrix_destroy(&m); + PyErr_NoMemory(); + return NULL; + } + if (igraphmodule_PyObject_to_vector_t(dist_o, dist, 0)) { + igraph_matrix_destroy(&m); + free(dist); + return NULL; + } + } else if (weights_o != Py_None) { + distances_are_weights = true; + /* they are actually weights, but let's keep them into the same variable + * for simplicity */ + dist = (igraph_vector_t*)malloc(sizeof(igraph_vector_t)); + if (!dist) { + igraph_matrix_destroy(&m); + PyErr_NoMemory(); + return NULL; + } + if (igraphmodule_PyObject_to_vector_t(weights_o, dist, 0)) { + igraph_matrix_destroy(&m); + free(dist); + return NULL; + } + } + + if (dim == 2) { + if (igraph_layout_umap(&self->g, &m, + use_seed, + dist, + (igraph_real_t)min_dist, + (igraph_int_t)epochs, + distances_are_weights)) { + if (dist) { + igraph_vector_destroy(dist); free(dist); + } + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } else { + if (igraph_layout_umap_3d(&self->g, &m, + use_seed, + dist, + (igraph_real_t)min_dist, + (igraph_int_t)epochs, + distances_are_weights)) { + if (dist) { + igraph_vector_destroy(dist); free(dist); + } + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + + if (dist) { + igraph_vector_destroy(dist); free(dist); + } + + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + igraph_matrix_destroy(&m); + return (PyObject *) result_o; +} + + /** \ingroup python_interface_graph * \brief Places the vertices of a bipartite graph according to a simple two-layer * Sugiyama layout. @@ -7158,27 +9217,29 @@ 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; + PyObject *result_o; - 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; } if (types_o == Py_None) { - types_o = PyString_FromString("type"); + types_o = PyUnicode_FromString("type"); } else { Py_INCREF(types_o); } 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; @@ -7194,9 +9255,9 @@ PyObject *igraphmodule_Graph_layout_bipartite( if (types != 0) { igraph_vector_bool_destroy(types); free(types); } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); - return (PyObject *) result; + return (PyObject *) result_o; } /********************************************************************** @@ -7211,20 +9272,18 @@ PyObject *igraphmodule_Graph_layout_bipartite( 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; + static char *kwlist[] = { "type", "loops", NULL }; + igraph_get_adjacency_t mode = IGRAPH_GET_ADJACENCY_BOTH; igraph_matrix_t m; - PyObject *result, *eids = Py_False; + igraph_loops_t loops = IGRAPH_LOOPS_TWICE; + PyObject *result_o, *mode_o = Py_None, *loops_o = Py_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iO", kwlist, &t, &eids)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &mode_o, &loops_o)) 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 (igraphmodule_PyObject_to_loops_t(loops_o, &loops)) return NULL; if (igraph_matrix_init (&m, igraph_vcount(&self->g), igraph_vcount(&self->g))) { @@ -7232,74 +9291,85 @@ 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, /* weights = */ 0, loops)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&m); return NULL; } - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_INT); + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_INT); igraph_matrix_destroy(&m); - return result; + return result_o; } /** \ingroup python_interface_graph - * \brief Returns the incidence matrix of a bipartite graph. - * \return the incidence matrix as a Python list of lists - * \sa igraph_get_incidence + * \brief Returns the bipartite adjacency matrix of a bipartite graph. + * \return the bipartite adjacency matrix as a Python list of lists + * \sa igraph_get_biadjacency */ -PyObject *igraphmodule_Graph_get_incidence(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) +PyObject *igraphmodule_Graph_get_biadjacency(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "types", NULL }; + static char *kwlist[] = { "types", "weights", 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; + igraph_vector_t *weights = 0; + PyObject *matrix_o, *row_ids_o, *col_ids_o, *types_o, *weights_o = Py_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &types_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &types_o)) return NULL; - if (igraph_vector_init(&row_ids, 0)) - return NULL; + if (igraph_vector_int_init(&row_ids, 0)) + return NULL; - if (igraph_vector_init(&col_ids, 0)) { - igraph_vector_destroy(&row_ids); - return NULL; + 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); - return NULL; + igraph_vector_int_destroy(&row_ids); + igraph_vector_int_destroy(&col_ids); + return NULL; + } + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + 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_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); } + if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; } - if (igraph_get_incidence(&self->g, types, &matrix, &row_ids, &col_ids)) { + if (igraph_get_biadjacency(&self->g, types, weights, &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); } + if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_matrix_destroy(&matrix); return NULL; } - + if (types) { igraph_vector_bool_destroy(types); free(types); } + if (weights) { igraph_vector_destroy(weights); free(weights); } 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); - igraph_vector_destroy(&row_ids); - col_ids_o = igraphmodule_vector_t_to_PyList(&col_ids, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&col_ids); + row_ids_o = igraphmodule_vector_int_t_to_PyList(&row_ids); + igraph_vector_int_destroy(&row_ids); + 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); } @@ -7312,19 +9382,28 @@ PyObject *igraphmodule_Graph_get_incidence(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_laplacian(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "weights", "normalized", NULL }; + static char *kwlist[] = { "weights", "normalized", "mode", NULL }; igraph_matrix_t m; - PyObject *result; + PyObject *result_o; PyObject *weights_o = Py_None; - PyObject *normalized = Py_False; + PyObject *normalized_o = Py_False; + PyObject *mode_o = Py_None; + igraph_laplacian_normalization_t normalize = IGRAPH_LAPLACIAN_UNNORMALIZED; + igraph_neimode_t mode = IGRAPH_OUT; igraph_vector_t *weights = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, - &weights_o, &normalized)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, + &weights_o, &normalized_o, &mode_o)) return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + ATTRIBUTE_TYPE_EDGE)) return NULL; + + if (igraphmodule_PyObject_to_laplacian_normalization_t(normalized_o, &normalize)) + return NULL; + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + return NULL; if (igraph_matrix_init (&m, igraph_vcount(&self->g), igraph_vcount(&self->g))) { @@ -7333,25 +9412,19 @@ PyObject *igraphmodule_Graph_laplacian(igraphmodule_GraphObject * self, return NULL; } - if (igraph_laplacian(&self->g, &m, /*sparseres=*/ 0, - PyObject_IsTrue(normalized), weights)) { + if (igraph_get_laplacian(&self->g, &m, mode, normalize, weights)) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_matrix_destroy(&m); return NULL; } - if (PyObject_IsTrue(normalized) || weights) { - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); - } - else { - result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_INT); - } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_matrix_destroy(&m); - return result; + return result_o; } /** \ingroup python_interface_graph @@ -7360,22 +9433,22 @@ PyObject *igraphmodule_Graph_laplacian(igraphmodule_GraphObject * self, * \sa igraph_get_edgelist */ PyObject *igraphmodule_Graph_get_edgelist(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) + PyObject* Py_UNUSED(_null)) { - igraph_vector_t edgelist; - PyObject *result; + igraph_vector_int_t edgelist; + PyObject *result_o; - 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_o = igraphmodule_vector_int_t_to_PyList_of_fixed_length_tuples(&edgelist, 2); + igraph_vector_int_destroy(&edgelist); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -7399,7 +9472,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); @@ -7422,18 +9495,38 @@ 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; + 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)) { + return NULL; + } + } + if (igraph_to_directed(&self->g, mode)) { igraphmodule_handle_igraph_error(); return NULL; } + Py_RETURN_NONE; } @@ -7444,14 +9537,14 @@ PyObject *igraphmodule_Graph_to_directed(igraphmodule_GraphObject * self, /** \ingroup python_interface_graph * \brief Reads a DIMACS file and creates a graph from it. * \return the graph - * \sa igraph_read_graph_dimacs + * \sa igraph_read_graph_dimacs_flow */ PyObject *igraphmodule_Graph_Read_DIMACS(PyTypeObject * type, PyObject * args, PyObject * kwds) { igraphmodule_GraphObject *self; igraphmodule_filehandle_t fobj; - igraph_integer_t source = 0, target = 0; + igraph_int_t source = 0, target = 0; igraph_vector_t capacity; igraph_t g; PyObject *fname = NULL, *directed = Py_False, *capacity_obj; @@ -7471,9 +9564,10 @@ PyObject *igraphmodule_Graph_Read_DIMACS(PyTypeObject * type, return NULL; } - if (igraph_read_graph_dimacs(&g, igraphmodule_filehandle_get(&fobj), - 0, 0, &source, &target, - &capacity, PyObject_IsTrue(directed))) { + if (igraph_read_graph_dimacs_flow( + &g, igraphmodule_filehandle_get(&fobj), 0, 0, &source, &target, + &capacity, PyObject_IsTrue(directed) + )) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&capacity); igraphmodule_filehandle_destroy(&fobj); @@ -7488,9 +9582,13 @@ PyObject *igraphmodule_Graph_Read_DIMACS(PyTypeObject * type, return NULL; CREATE_GRAPH_FROM_TYPE(self, g, type); + if (self == NULL) { + Py_DECREF(capacity_obj); + return NULL; + } - 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 @@ -7632,7 +9730,6 @@ PyObject *igraphmodule_Graph_Read_Lgl(PyTypeObject * type, PyObject * args, PyDict_GetItemString(kwds, "directed") == NULL) { if (PyErr_Occurred()) return NULL; - PY_IGRAPH_WARN("Graph.Read_Lgl creates directed networks by default from igraph 0.6. To get rid of this warning, specify directed=... explicitly. This warning will be removed from igraph 0.7."); } if (igraphmodule_filehandle_init(&fobj, fname, "r")) @@ -7678,7 +9775,7 @@ PyObject *igraphmodule_Graph_Read_Pajek(PyTypeObject * type, PyObject * args, igraphmodule_filehandle_destroy(&fobj); return NULL; } - + igraphmodule_filehandle_destroy(&fobj); CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -7745,10 +9842,10 @@ PyObject *igraphmodule_Graph_Read_GraphDB(PyTypeObject * type, igraphmodule_filehandle_destroy(&fobj); return NULL; } - + igraphmodule_filehandle_destroy(&fobj); CREATE_GRAPH_FROM_TYPE(self, g, type); - + return (PyObject *) self; } @@ -7762,41 +9859,43 @@ 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; } - + igraphmodule_filehandle_destroy(&fobj); CREATE_GRAPH_FROM_TYPE(self, g, type); - + return (PyObject *) self; } /** \ingroup python_interface_graph * \brief Writes the graph as a DIMACS file * \return none - * \sa igraph_write_graph_dimacs + * \sa igraph_write_graph_dimacs_flow */ 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_int_t source, target; igraphmodule_filehandle_t fobj; igraph_vector_t* capacity = 0; @@ -7804,15 +9903,24 @@ PyObject *igraphmodule_Graph_write_dimacs(igraphmodule_GraphObject * self, "f", "source", "target", "capacity", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oll|O", kwlist, &fname, - &source, &target, &capacity_obj)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO|O", kwlist, &fname, + &source_o, &target_o, &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 = PyString_FromString("capacity"); + capacity_obj = PyUnicode_FromString("capacity"); } else { Py_INCREF(capacity_obj); } @@ -7825,7 +9933,7 @@ PyObject *igraphmodule_Graph_write_dimacs(igraphmodule_GraphObject * self, Py_DECREF(capacity_obj); - if (igraph_write_graph_dimacs(&self->g, igraphmodule_filehandle_get(&fobj), + if (igraph_write_graph_dimacs_flow(&self->g, igraphmodule_filehandle_get(&fobj), source, target, capacity)) { igraphmodule_handle_igraph_error(); if (capacity) { @@ -7940,7 +10048,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) { @@ -7951,7 +10059,7 @@ PyObject *igraphmodule_Graph_write_gml(igraphmodule_GraphObject * self, } if (igraph_write_graph_gml(&self->g, igraphmodule_filehandle_get(&fobj), - idvecptr, creator_str)) { + IGRAPH_WRITE_GML_DEFAULT_SW, idvecptr, creator_str)) { if (idvecptr) { igraph_vector_destroy(idvecptr); } if (creator_str) free(creator_str); @@ -8069,18 +10177,19 @@ PyObject *igraphmodule_Graph_write_pajek(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_write_graphml(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *fname = NULL; - static char *kwlist[] = { "f", NULL }; + PyObject *fname = NULL, *prefixattr_o = Py_True; + static char *kwlist[] = { "f", "prefixattr", NULL }; igraphmodule_filehandle_t fobj; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &fname)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &fname, &prefixattr_o)) return NULL; if (igraphmodule_filehandle_init(&fobj, fname, "w")) return NULL; - if (igraph_write_graph_graphml(&self->g, igraphmodule_filehandle_get(&fobj), - /*prefixattr=*/ 1)) { + if (igraph_write_graph_graphml( + &self->g, igraphmodule_filehandle_get(&fobj), PyObject_IsTrue(prefixattr_o) + )) { igraphmodule_handle_igraph_error(); igraphmodule_filehandle_destroy(&fobj); return NULL; @@ -8107,7 +10216,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")) @@ -8128,84 +10237,177 @@ PyObject *igraphmodule_Graph_write_leda(igraphmodule_GraphObject * self, * Routines related to graph isomorphism * **********************************************************************/ +/** + * \ingroup python_interface_graph + * \brief Calculates the automorphism group generators of a graph using BLISS + * \sa igraph_automorphism_group + */ +PyObject *igraphmodule_Graph_automorphism_group( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = { "sh", "color", NULL }; + PyObject *sh_o = Py_None; + PyObject *color_o = Py_None; + PyObject *list; + igraph_bliss_sh_t sh = IGRAPH_BLISS_FL; + igraph_vector_int_list_t generators; + igraph_vector_int_t *color = 0; + igraph_error_t retval; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &sh_o, &color_o)) + return NULL; + + if (igraphmodule_PyObject_to_bliss_sh_t(sh_o, &sh)) + return NULL; + + if (igraph_vector_int_list_init(&generators, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, + ATTRIBUTE_TYPE_VERTEX)) return NULL; + + retval = igraph_automorphism_group_bliss(&self->g, color, &generators, sh, 0); + + if (color) { igraph_vector_int_destroy(color); free(color); } + + if (retval) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_list_destroy(&generators); + return NULL; + } + + list = igraphmodule_vector_int_list_t_to_PyList(&generators); + + igraph_vector_int_list_destroy(&generators); + + return list; +} + /** * \ingroup python_interface_graph * \brief Calculates the canonical permutation of a graph using BLISS * \sa igraph_canonical_permutation */ PyObject *igraphmodule_Graph_canonical_permutation( - igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "sh", NULL }; + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = { "sh", "color", NULL }; PyObject *sh_o = Py_None; + PyObject *color_o = Py_None; PyObject *list; - igraph_bliss_sh_t sh = IGRAPH_BLISS_FM; - igraph_vector_t labeling; + igraph_bliss_sh_t sh = IGRAPH_BLISS_FL; + igraph_vector_int_t labeling, *color = 0; + igraph_error_t retval; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &sh_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &sh_o, &color_o)) 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; + if (igraph_vector_int_init(&labeling, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; } - if (igraph_canonical_permutation(&self->g, 0, &labeling, sh, 0)) { - igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&labeling); - return NULL; + if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, + ATTRIBUTE_TYPE_VERTEX)) return NULL; + + retval = igraph_canonical_permutation(&self->g, color, &labeling); + + if (color) { igraph_vector_int_destroy(color); free(color); } + + if (retval) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&labeling); + return NULL; } - list = igraphmodule_vector_t_to_PyList(&labeling, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&labeling); + list = igraphmodule_vector_int_t_to_PyList(&labeling); + + igraph_vector_int_destroy(&labeling); + return list; } +/** + * \ingroup python_interface_graph + * \brief Calculates the number of automorphisms of a graph using BLISS + * \sa igraph_count_automorphisms + */ +PyObject *igraphmodule_Graph_count_automorphisms( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = { "sh", "color", NULL }; + PyObject *sh_o = Py_None; + PyObject *color_o = Py_None; + PyObject *result; + igraph_bliss_sh_t sh = IGRAPH_BLISS_FL; + igraph_vector_int_t *color = 0; + igraph_error_t retval; + igraph_bliss_info_t info; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &sh_o, &color_o)) + return NULL; + + if (igraphmodule_PyObject_to_bliss_sh_t(sh_o, &sh)) + return NULL; + + if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, + ATTRIBUTE_TYPE_VERTEX)) return NULL; + + retval = igraph_count_automorphisms_bliss(&self->g, color, sh, &info); + + if (color) { igraph_vector_int_destroy(color); free(color); } + + if (retval) { + igraphmodule_handle_igraph_error(); + igraph_free(info.group_size); + return NULL; + } + + result = PyLong_FromString(info.group_size, NULL, 10); + igraph_free(info.group_size); + if (!result) { + return NULL; + } + + return result; +} + /** \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, PyObject * args, PyObject * kwds) { - Py_ssize_t n; - igraph_integer_t isoclass = 0; + igraph_int_t isoclass = 0; PyObject *vids = 0; char *kwlist[] = { "vertices", NULL }; if (!PyArg_ParseTupleAndKeywords - (args, kwds, "|O!", kwlist, &PyList_Type, &vids)) + (args, kwds, "|O", kwlist, &vids)) return NULL; - n = vids ? PyList_Size(vids) : igraph_vcount(&self->g); - if (n < 3 || n > 4) { - PyErr_SetString(PyExc_ValueError, - "Graph or subgraph must have 3 or 4 vertices."); - return NULL; - } - if (vids) { - igraph_vector_t vidsvec; - if (igraphmodule_PyObject_to_vector_t(vids, &vidsvec, 1)) { - PyErr_SetString(PyExc_ValueError, - "Error while converting PyList to igraph_vector_t"); + igraph_vs_t vs; + if (igraphmodule_PyObject_to_vs_t(vids, &vs, &self->g, NULL, NULL)) { return NULL; } - if (igraph_isoclass_subgraph(&self->g, &vidsvec, &isoclass)) { + if (igraph_isoclass_subgraph(&self->g, vs, &isoclass)) { + igraph_vs_destroy(&vs); igraphmodule_handle_igraph_error(); return NULL; } - } - else { + igraph_vs_destroy(&vs); + } else { if (igraph_isoclass(&self->g, &isoclass)) { igraphmodule_handle_igraph_error(); return NULL; } } - return PyInt_FromLong((long)isoclass); + return igraphmodule_integer_t_to_PyObject(isoclass); } /** \ingroup python_interface_graph @@ -8216,22 +10418,22 @@ PyObject *igraphmodule_Graph_isoclass(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_isomorphic(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - igraph_bool_t result = 0; + igraph_bool_t res = false; PyObject *o = Py_None; igraphmodule_GraphObject *other; 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; - if (igraph_isomorphic(&self->g, &other->g, &result)) { + if (igraph_isomorphic(&self->g, &other->g, &res)) { igraphmodule_handle_igraph_error(); return NULL; } - if (result) Py_RETURN_TRUE; + if (res) Py_RETURN_TRUE; Py_RETURN_FALSE; } @@ -8247,19 +10449,22 @@ PyObject *igraphmodule_Graph_isomorphic(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - igraph_bool_t result = 0; + igraph_bool_t res = false; PyObject *o=Py_None, *return1=Py_False, *return2=Py_False; 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_bliss_sh_t sh1=IGRAPH_BLISS_FM, sh2=IGRAPH_BLISS_FM; + 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; + igraph_error_t retval; static char *kwlist[] = { "other", "return_mapping_12", - "return_mapping_21", "sh1", "sh2", NULL }; + "return_mapping_21", "sh1", "sh2", "color1", "color2", NULL }; /* TODO: convert igraph_bliss_info_t when needed */ if (!PyArg_ParseTupleAndKeywords - (args, kwds, "|O!OOOO", kwlist, &igraphmodule_GraphType, &o, - &return1, &return2, &sho1, &sho2)) + (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; sh2 = sh1; @@ -8269,48 +10474,59 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, "be equal to sh1"); } sh2 = sh1; + + if (igraphmodule_attrib_to_vector_int_t(color1_o, self, &color1, + ATTRIBUTE_TYPE_VERTEX)) return NULL; + if (igraphmodule_attrib_to_vector_int_t(color2_o, self, &color2, + 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_int_init(&mapping_12, 0); + map12 = &mapping_12; } if (PyObject_IsTrue(return2)) { - igraph_vector_init(&mapping_21, 0); - map21 = &mapping_21; + igraph_vector_int_init(&mapping_21, 0); + map21 = &mapping_21; } - if (igraph_isomorphic_bliss(&self->g, &other->g, 0, 0, &result, map12, map21, - sh1, 0, 0)) { + retval = igraph_isomorphic_bliss(&self->g, &other->g, color1, color2, + &res, map12, map21, sh1, 0, 0); + + if (color1) { igraph_vector_int_destroy(color1); free(color1); } + if (color2) { igraph_vector_int_destroy(color2); free(color2); } + + if (retval) { igraphmodule_handle_igraph_error(); return NULL; } if (!map12 && !map21) { - if (result) Py_RETURN_TRUE; + if (res) 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 = res ? Py_True : Py_False; + Py_INCREF(iso); + if (map12) { + m1 = igraphmodule_vector_int_t_to_PyList(map12); + igraph_vector_int_destroy(map12); + if (!m1) { + Py_DECREF(iso); + if (map21) igraph_vector_int_destroy(map21); + return NULL; + } + } else { m1 = Py_None; Py_INCREF(m1); } + if (map21) { + m2 = igraphmodule_vector_int_t_to_PyList(map21); + igraph_vector_int_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); } } @@ -8323,91 +10539,91 @@ typedef struct { PyObject* graph2; } 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, +igraph_error_t igraphmodule_i_Graph_isomorphic_vf2_callback_fn( + 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; igraph_bool_t retval; PyObject *map12_o, *map21_o; - PyObject *result; + PyObject *result_o; - 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 */ + /* Error in conversion, return an error code */ PyErr_WriteUnraisable(data->callback_fn); - return 0; + return IGRAPH_FAILURE; } - 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 */ + /* Error in conversion, return an error code */ PyErr_WriteUnraisable(data->callback_fn); - Py_DECREF(map21_o); - return 0; + Py_DECREF(map12_o); + return IGRAPH_FAILURE; } - result = PyObject_CallFunction(data->callback_fn, "OOOO", data->graph1, data->graph2, + result_o = PyObject_CallFunction(data->callback_fn, "OOOO", data->graph1, data->graph2, map12_o, map21_o); Py_DECREF(map12_o); Py_DECREF(map21_o); - if (result == NULL) { - /* Error in callback, return 0 */ + if (result_o == NULL) { + /* Error in callback, return an error code */ PyErr_WriteUnraisable(data->callback_fn); - return 0; + return IGRAPH_FAILURE; } - retval = PyObject_IsTrue(result); - Py_DECREF(result); + retval = PyObject_IsTrue(result_o); + Py_DECREF(result_o); - return retval; + return retval ? IGRAPH_SUCCESS : IGRAPH_STOP; } igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn( const igraph_t *graph1, const igraph_t *graph2, - const igraph_integer_t cand1, const igraph_integer_t cand2, + const igraph_int_t cand1, const igraph_int_t cand2, void* extra) { igraphmodule_i_Graph_isomorphic_vf2_callback_data_t* data = (igraphmodule_i_Graph_isomorphic_vf2_callback_data_t*)extra; igraph_bool_t retval; - PyObject *result; + PyObject *result_o; - result = PyObject_CallFunction(data->node_compat_fn, "OOll", - data->graph1, data->graph2, (long)cand1, (long)cand2); + result_o = PyObject_CallFunction(data->node_compat_fn, "OOnn", + data->graph1, data->graph2, (Py_ssize_t)cand1, (Py_ssize_t)cand2); - if (result == NULL) { + if (result_o == NULL) { /* Error in callback, return 0 */ PyErr_WriteUnraisable(data->node_compat_fn); return 0; } - retval = PyObject_IsTrue(result); - Py_DECREF(result); + retval = PyObject_IsTrue(result_o); + Py_DECREF(result_o); return retval; } igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn( const igraph_t *graph1, const igraph_t *graph2, - const igraph_integer_t cand1, const igraph_integer_t cand2, + const igraph_int_t cand1, const igraph_int_t cand2, void* extra) { igraphmodule_i_Graph_isomorphic_vf2_callback_data_t* data = (igraphmodule_i_Graph_isomorphic_vf2_callback_data_t*)extra; igraph_bool_t retval; - PyObject *result; + PyObject *result_o; - result = PyObject_CallFunction(data->edge_compat_fn, "OOll", - data->graph1, data->graph2, (long)cand1, (long)cand2); + result_o = PyObject_CallFunction(data->edge_compat_fn, "OOnn", + data->graph1, data->graph2, (Py_ssize_t)cand1, (Py_ssize_t)cand2); - if (result == NULL) { + if (result_o == NULL) { /* Error in callback, return 0 */ PyErr_WriteUnraisable(data->edge_compat_fn); return 0; } - retval = PyObject_IsTrue(result); - Py_DECREF(result); + retval = PyObject_IsTrue(result_o); + Py_DECREF(result_o); return retval; } @@ -8424,19 +10640,18 @@ igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn( PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - igraph_bool_t result = 0; + igraph_bool_t res = false; PyObject *o=Py_None, *return1=Py_False, *return2=Py_False; PyObject *color1_o=Py_None, *color2_o=Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; 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; - int retval; + igraph_error_t retval; static char *kwlist[] = { "other", "color1", "color2", "edge_color1", "edge_color2", @@ -8445,7 +10660,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; @@ -8471,20 +10686,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); } @@ -8492,12 +10707,12 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, } if (PyObject_IsTrue(return1)) { - igraph_vector_init(&mapping_12, 0); - map12 = &mapping_12; + igraph_vector_int_init(&mapping_12, 0); + map12 = &mapping_12; } if (PyObject_IsTrue(return2)) { - igraph_vector_init(&mapping_21, 0); - map21 = &mapping_21; + igraph_vector_int_init(&mapping_21, 0); + map21 = &mapping_21; } callback_data.graph1 = (PyObject*)self; @@ -8508,12 +10723,12 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, if (callback_data.callback_fn == 0) { retval = igraph_isomorphic_vf2(&self->g, &other->g, - color1, color2, edge_color1, edge_color2, &result, map12, map21, + color1, color2, edge_color1, edge_color2, &res, map12, map21, node_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn, edge_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn, &callback_data); } else { - retval = igraph_isomorphic_function_vf2(&self->g, &other->g, + retval = igraph_get_isomorphisms_vf2_callback(&self->g, &other->g, color1, color2, edge_color1, edge_color2, map12, map21, igraphmodule_i_Graph_isomorphic_vf2_callback_fn, node_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn, @@ -8532,33 +10747,33 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, } if (!map12 && !map21) { - if (result) Py_RETURN_TRUE; + if (res) 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); - } -} - -/** \ingroup python_interface_graph - * \brief Counts the number of isomorphisms of two given graphs - * + PyObject *m1, *m2; + if (map12) { + m1 = igraphmodule_vector_int_t_to_PyList(map12); + igraph_vector_int_destroy(map12); + if (!m1) { + if (map21) igraph_vector_int_destroy(map21); + return NULL; + } + } else { m1 = Py_None; Py_INCREF(m1); } + if (map21) { + m2 = igraphmodule_vector_int_t_to_PyList(map21); + igraph_vector_int_destroy(map21); + if (!m2) { + Py_DECREF(m1); + return NULL; + } + } else { m2 = Py_None; Py_INCREF(m2); } + return Py_BuildValue("ONN", res ? Py_True : Py_False, m1, m2); + } +} + +/** \ingroup python_interface_graph + * \brief Counts the number of isomorphisms of two given graphs + * * The actual code is almost the same as igraphmodule_Graph_count_subisomorphisms. * Make sure you correct bugs in both interfaces if applicable! * @@ -8566,7 +10781,7 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_count_isomorphisms_vf2(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - igraph_integer_t result = 0; + igraph_int_t res = 0; PyObject *o = Py_None; PyObject *color1_o=Py_None, *color2_o=Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; @@ -8580,7 +10795,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; @@ -8601,20 +10816,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); } @@ -8628,7 +10843,7 @@ PyObject *igraphmodule_Graph_count_isomorphisms_vf2(igraphmodule_GraphObject *se callback_data.edge_compat_fn = edge_compat_fn == Py_None ? 0 : edge_compat_fn; if (igraph_count_isomorphisms_vf2(&self->g, &other->g, - color1, color2, edge_color1, edge_color2, &result, + color1, color2, edge_color1, edge_color2, &res, node_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn, edge_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn, &callback_data)) { @@ -8645,11 +10860,11 @@ 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(res); } /** \ingroup python_interface_graph - * \brief Returns all isomorphisms of two given graphs + * \brief Returns all isomorphisms of two given graphs * * The actual code is almost the same as igraphmodule_Graph_get_subisomorphisms. * Make sure you correct bugs in both interfaces if applicable! @@ -8658,12 +10873,12 @@ PyObject *igraphmodule_Graph_count_isomorphisms_vf2(igraphmodule_GraphObject *se */ PyObject *igraphmodule_Graph_get_isomorphisms_vf2(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - igraph_vector_ptr_t result; + igraph_vector_int_list_t res; PyObject *o = Py_None; PyObject *color1_o = Py_None, *color2_o = Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; PyObject *node_compat_fn=Py_None, *edge_compat_fn=Py_None; - PyObject *res; + PyObject *result_o; igraphmodule_GraphObject *other; igraph_vector_int_t *color1=0, *color2=0; igraph_vector_int_t *edge_color1=0, *edge_color2=0; @@ -8673,7 +10888,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; @@ -8694,27 +10909,27 @@ 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); } return NULL; } - if (igraph_vector_ptr_init(&result, 0)) { + if (igraph_vector_int_list_init(&res, 0)) { 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); } @@ -8729,7 +10944,7 @@ PyObject *igraphmodule_Graph_get_isomorphisms_vf2(igraphmodule_GraphObject *self callback_data.edge_compat_fn = edge_compat_fn == Py_None ? 0 : edge_compat_fn; if (igraph_get_isomorphisms_vf2(&self->g, &other->g, - color1, color2, edge_color1, edge_color2, &result, + color1, color2, edge_color1, edge_color2, &res, node_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn, edge_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn, &callback_data)) { @@ -8738,7 +10953,7 @@ PyObject *igraphmodule_Graph_get_isomorphisms_vf2(igraphmodule_GraphObject *self if (color2) { igraph_vector_int_destroy(color2); free(color2); } if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } if (edge_color2) { igraph_vector_int_destroy(edge_color2); free(edge_color2); } - igraph_vector_ptr_destroy(&result); + igraph_vector_int_list_destroy(&res); return NULL; } @@ -8747,12 +10962,10 @@ 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); + result_o = igraphmodule_vector_int_list_t_to_PyList(&res); + igraph_vector_int_list_destroy(&res); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&result, igraph_vector_destroy); - igraph_vector_ptr_destroy_all(&result); - - return res; + return result_o; } /** \ingroup python_interface_graph @@ -8764,18 +10977,18 @@ PyObject *igraphmodule_Graph_get_isomorphisms_vf2(igraphmodule_GraphObject *self PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - igraph_bool_t result = 0; + igraph_bool_t res = false; PyObject *o, *return1=Py_False, *return2=Py_False; PyObject *color1_o=Py_None, *color2_o=Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; 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; - int retval; + igraph_error_t retval; static char *kwlist[] = { "other", "color1", "color2", "edge_color1", "edge_color2", "return_mapping_12", "return_mapping_21", @@ -8783,7 +10996,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; @@ -8806,20 +11019,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); } @@ -8827,12 +11040,12 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, } if (PyObject_IsTrue(return1)) { - igraph_vector_init(&mapping_12, 0); - map12 = &mapping_12; + igraph_vector_int_init(&mapping_12, 0); + map12 = &mapping_12; } if (PyObject_IsTrue(return2)) { - igraph_vector_init(&mapping_21, 0); - map21 = &mapping_21; + igraph_vector_int_init(&mapping_21, 0); + map21 = &mapping_21; } callback_data.graph1 = (PyObject*)self; @@ -8843,12 +11056,12 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, if (callback_data.callback_fn == 0) { retval = igraph_subisomorphic_vf2(&self->g, &other->g, - color1, color2, edge_color1, edge_color2, &result, map12, map21, + color1, color2, edge_color1, edge_color2, &res, map12, map21, node_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn, edge_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn, &callback_data); } else { - retval = igraph_subisomorphic_function_vf2(&self->g, &other->g, + retval = igraph_get_subisomorphisms_vf2_callback(&self->g, &other->g, color1, color2, edge_color1, edge_color2, map12, map21, igraphmodule_i_Graph_isomorphic_vf2_callback_fn, node_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn, @@ -8867,37 +11080,37 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, } if (!map12 && !map21) { - if (result) + if (res) 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_int_t_to_PyList(map12); + igraph_vector_int_destroy(map12); + if (!m1) { + 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); - if (!m2) { - Py_DECREF(m1); - return NULL; - } - } else { + if (map21) { + m2 = igraphmodule_vector_int_t_to_PyList(map21); + igraph_vector_int_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", res ? Py_True : Py_False, m1, m2); } } /** \ingroup python_interface_graph - * \brief Counts the number of subisomorphisms of two given graphs + * \brief Counts the number of subisomorphisms of two given graphs * * The actual code is almost the same as igraphmodule_Graph_count_isomorphisms. * Make sure you correct bugs in both interfaces if applicable! @@ -8906,7 +11119,7 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_count_subisomorphisms_vf2(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - igraph_integer_t result = 0; + igraph_int_t res = 0; PyObject *o = Py_None; PyObject *color1_o = Py_None, *color2_o = Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; @@ -8920,7 +11133,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; @@ -8938,20 +11151,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); } @@ -8965,7 +11178,7 @@ PyObject *igraphmodule_Graph_count_subisomorphisms_vf2(igraphmodule_GraphObject callback_data.edge_compat_fn = edge_compat_fn == Py_None ? 0 : edge_compat_fn; if (igraph_count_subisomorphisms_vf2(&self->g, &other->g, color1, color2, - edge_color1, edge_color2, &result, + edge_color1, edge_color2, &res, node_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn, edge_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn, &callback_data)) { @@ -8982,11 +11195,11 @@ 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(res); } /** \ingroup python_interface_graph - * \brief Returns all subisomorphisms of two given graphs + * \brief Returns all subisomorphisms of two given graphs * * The actual code is almost the same as igraphmodule_Graph_get_isomorphisms. * Make sure you correct bugs in both interfaces if applicable! @@ -8995,12 +11208,12 @@ PyObject *igraphmodule_Graph_count_subisomorphisms_vf2(igraphmodule_GraphObject */ PyObject *igraphmodule_Graph_get_subisomorphisms_vf2(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - igraph_vector_ptr_t result; + igraph_vector_int_list_t res; PyObject *o; PyObject *color1_o=Py_None, *color2_o=Py_None; PyObject *edge_color1_o=Py_None, *edge_color2_o=Py_None; PyObject *node_compat_fn=Py_None, *edge_compat_fn=Py_None; - PyObject *res; + PyObject *result_o; igraphmodule_GraphObject *other; igraph_vector_int_t *color1=0, *color2=0; igraph_vector_int_t *edge_color1=0, *edge_color2=0; @@ -9010,12 +11223,12 @@ 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; - if (igraph_vector_ptr_init(&result, 0)) { + if (igraph_vector_int_list_init(&res, 0)) { return igraphmodule_handle_igraph_error(); } @@ -9032,20 +11245,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); } @@ -9059,7 +11272,7 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_vf2(igraphmodule_GraphObject *s callback_data.edge_compat_fn = edge_compat_fn == Py_None ? 0 : edge_compat_fn; if (igraph_get_subisomorphisms_vf2(&self->g, &other->g, color1, color2, - edge_color1, edge_color2, &result, + edge_color1, edge_color2, &res, node_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn, edge_compat_fn == Py_None ? 0 : igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn, &callback_data)) { @@ -9068,7 +11281,7 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_vf2(igraphmodule_GraphObject *s if (color2) { igraph_vector_int_destroy(color2); free(color2); } if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } if (edge_color2) { igraph_vector_int_destroy(edge_color2); free(edge_color2); } - igraph_vector_ptr_destroy(&result); + igraph_vector_int_list_destroy(&res); return NULL; } @@ -9077,12 +11290,10 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_vf2(igraphmodule_GraphObject *s 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); - - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&result, igraph_vector_destroy); - igraph_vector_ptr_destroy_all(&result); + result_o = igraphmodule_vector_int_list_t_to_PyList(&res); + igraph_vector_int_list_destroy(&res); - return res; + return result_o; } /** \ingroup python_interface_graph @@ -9094,62 +11305,64 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_vf2(igraphmodule_GraphObject *s PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - igraph_bool_t result = 0; + igraph_bool_t res = false; PyObject *o, *return_mapping=Py_False, *domains_o=Py_None, *induced=Py_False; - float time_limit = 0; igraphmodule_GraphObject *other; - igraph_vector_ptr_t domains; - igraph_vector_ptr_t* p_domains = 0; - igraph_vector_t mapping, *map=0; + igraph_vector_int_list_t domains; + igraph_vector_int_list_t* p_domains = 0; + igraph_vector_int_t mapping, *map=0; - static char *kwlist[] = { "pattern", "domains", "induced", "time_limit", - "return_mapping", NULL }; + static char *kwlist[] = { + "pattern", "domains", "induced", "return_mapping", NULL + }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOfO", kwlist, - &igraphmodule_GraphType, &o, &domains_o, &induced, - &time_limit, &return_mapping)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOO", kwlist, + igraphmodule_GraphType, &o, &domains_o, &induced, + &return_mapping)) return NULL; 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_list_t(domains_o, &domains)) return NULL; p_domains = &domains; } if (PyObject_IsTrue(return_mapping)) { - if (igraph_vector_init(&mapping, 0)) { - if (p_domains) - igraph_vector_ptr_destroy_all(p_domains); + if (igraph_vector_int_init(&mapping, 0)) { + if (p_domains) { + igraph_vector_int_list_destroy(p_domains); + } igraphmodule_handle_igraph_error(); return NULL; } - map = &mapping; + map = &mapping; } - if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, &result, - map, 0, PyObject_IsTrue(induced), (int)time_limit)) { - if (p_domains) - igraph_vector_ptr_destroy_all(p_domains); + if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, &res, + map, 0, PyObject_IsTrue(induced))) { + if (p_domains) { + igraph_vector_int_list_destroy(p_domains); + } igraphmodule_handle_igraph_error(); return NULL; } if (p_domains) - igraph_vector_ptr_destroy_all(p_domains); + igraph_vector_int_list_destroy(p_domains); if (!map) { - if (result) + if (res) Py_RETURN_TRUE; Py_RETURN_FALSE; } else { - 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); + PyObject *m = igraphmodule_vector_int_t_to_PyList(map); + igraph_vector_int_destroy(map); + if (!m) + return NULL; + return Py_BuildValue("ON", res ? Py_True : Py_False, m); } } @@ -9162,51 +11375,51 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_get_subisomorphisms_lad( igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *o, *domains_o=Py_None, *induced=Py_False, *result; - float time_limit = 0; + PyObject *o, *domains_o=Py_None, *induced=Py_False, *result_o; igraphmodule_GraphObject *other; - igraph_vector_ptr_t domains; - igraph_vector_ptr_t* p_domains = 0; - igraph_vector_ptr_t mappings; + igraph_vector_int_list_t domains; + igraph_vector_int_list_t* p_domains = 0; + igraph_vector_int_list_t mappings; - static char *kwlist[] = { "pattern", "domains", "induced", "time_limit", NULL }; + static char *kwlist[] = { "pattern", "domains", "induced", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOf", kwlist, - &igraphmodule_GraphType, &o, &domains_o, &induced, &time_limit)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OO", kwlist, + igraphmodule_GraphType, &o, &domains_o, &induced)) return NULL; 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_list_t(domains_o, &domains)) return NULL; p_domains = &domains; } - if (igraph_vector_ptr_init(&mappings, 0)) { + if (igraph_vector_int_list_init(&mappings, 0)) { igraphmodule_handle_igraph_error(); if (p_domains) - igraph_vector_ptr_destroy_all(p_domains); + igraph_vector_int_list_destroy(p_domains); return NULL; } - if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, 0, 0, &mappings, - PyObject_IsTrue(induced), (int)time_limit)) { + if (igraph_subisomorphic_lad( + &other->g, &self->g, p_domains, 0, 0, &mappings, PyObject_IsTrue(induced) + )) { igraphmodule_handle_igraph_error(); - igraph_vector_ptr_destroy_all(&mappings); + igraph_vector_int_list_destroy(&mappings); if (p_domains) - igraph_vector_ptr_destroy_all(p_domains); + igraph_vector_int_list_destroy(p_domains); return NULL; } if (p_domains) - igraph_vector_ptr_destroy_all(p_domains); + igraph_vector_int_list_destroy(p_domains); - result = igraphmodule_vector_ptr_t_to_PyList(&mappings, IGRAPHMODULE_TYPE_INT); - igraph_vector_ptr_destroy_all(&mappings); + result_o = igraphmodule_vector_int_list_t_to_PyList(&mappings); + igraph_vector_int_list_destroy(&mappings); - return result; + return result_o; } /********************************************************************** @@ -9223,7 +11436,7 @@ Py_ssize_t igraphmodule_Graph_attribute_count(igraphmodule_GraphObject * self) /** \ingroup python_interface_graph * \brief Handles the subscript operator on the graph. - * + * * When the subscript is a string, returns the corresponding value of the * given attribute in the graph. When the subscript is a tuple of length * 2, retrieves the adjacency matrix representation of the graph between @@ -9232,18 +11445,25 @@ Py_ssize_t igraphmodule_Graph_attribute_count(igraphmodule_GraphObject * self) PyObject *igraphmodule_Graph_mp_subscript(igraphmodule_GraphObject * self, PyObject * s) { - PyObject *result = 0; + PyObject *result_o = 0; 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; @@ -9253,10 +11473,10 @@ PyObject *igraphmodule_Graph_mp_subscript(igraphmodule_GraphObject * self, } /* Ordinary attribute retrieval */ - result = PyDict_GetItem(ATTR_STRUCT_DICT(&self->g)[ATTRHASH_IDX_GRAPH], s); - if (result) { - Py_INCREF(result); - return result; + result_o = PyDict_GetItem(ATTR_STRUCT_DICT(&self->g)[ATTRHASH_IDX_GRAPH], s); + if (result_o) { + Py_INCREF(result_o); + return result_o; } /* result is NULL, check whether there was an error */ @@ -9268,7 +11488,7 @@ PyObject *igraphmodule_Graph_mp_subscript(igraphmodule_GraphObject * self, /** \ingroup python_interface_graph * \brief Handles the subscript assignment operator on the graph. - * + * * If k is a string, sets the value of the corresponding attribute of the graph. * If k is a tuple of length 2, sets part of the adjacency matrix. * @@ -9282,20 +11502,27 @@ int igraphmodule_Graph_mp_assign_subscript(igraphmodule_GraphObject * self, if (PyTuple_Check(k) && PyTuple_Size(k) >= 2) { /* Adjacency matrix representation */ PyObject *ri, *ci, *attr; - + if (v == NULL) { PyErr_SetString(PyExc_NotImplementedError, "cannot delete parts " "of the adjacency matrix of a graph"); 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; @@ -9316,7 +11543,7 @@ int igraphmodule_Graph_mp_assign_subscript(igraphmodule_GraphObject * self, /** \ingroup python_interface_graph * \brief Returns the attribute list of the graph */ -PyObject *igraphmodule_Graph_attributes(igraphmodule_GraphObject * self) +PyObject *igraphmodule_Graph_attributes(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { return PyDict_Keys(ATTR_STRUCT_DICT(&self->g)[ATTRHASH_IDX_GRAPH]); } @@ -9324,8 +11551,7 @@ PyObject *igraphmodule_Graph_attributes(igraphmodule_GraphObject * self) /** \ingroup python_interface_graph * \brief Returns the attribute list of the graph's vertices */ -PyObject *igraphmodule_Graph_vertex_attributes(igraphmodule_GraphObject * - self) +PyObject *igraphmodule_Graph_vertex_attributes(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { return PyDict_Keys(ATTR_STRUCT_DICT(&self->g)[ATTRHASH_IDX_VERTEX]); } @@ -9333,211 +11559,26 @@ PyObject *igraphmodule_Graph_vertex_attributes(igraphmodule_GraphObject * /** \ingroup python_interface_graph * \brief Returns the attribute list of the graph's edges */ -PyObject *igraphmodule_Graph_edge_attributes(igraphmodule_GraphObject * self) +PyObject *igraphmodule_Graph_edge_attributes(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { return PyDict_Keys(ATTR_STRUCT_DICT(&self->g)[ATTRHASH_IDX_EDGE]); } /********************************************************************** - * 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) */ PyObject *igraphmodule_Graph_difference(igraphmodule_GraphObject * self, PyObject * other) { - igraphmodule_GraphObject *o, *result; + igraphmodule_GraphObject *o, *result_o; igraph_t g; - if (!PyObject_TypeCheck(other, &igraphmodule_GraphType)) { + if (!PyObject_TypeCheck(other, igraphmodule_GraphType)) { Py_INCREF(Py_NotImplemented); return Py_NotImplemented; } @@ -9551,22 +11592,23 @@ PyObject *igraphmodule_Graph_difference(igraphmodule_GraphObject * self, /* 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); + CREATE_GRAPH(result_o, g); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph * \brief Creates the complementer of a graph */ PyObject *igraphmodule_Graph_complementer(igraphmodule_GraphObject * self, - PyObject * args) + PyObject * args, PyObject * kwds) { - igraphmodule_GraphObject *result; + static char *kwlist[] = { "loops", NULL }; + igraphmodule_GraphObject *result_o; 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(); @@ -9576,9 +11618,9 @@ PyObject *igraphmodule_Graph_complementer(igraphmodule_GraphObject * self, /* 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); + CREATE_GRAPH(result_o, g); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -9586,7 +11628,7 @@ PyObject *igraphmodule_Graph_complementer(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_complementer_op(igraphmodule_GraphObject * self) { - igraphmodule_GraphObject *result; + igraphmodule_GraphObject *result_o; igraph_t g; if (igraph_complementer(&g, &self->g, 0)) { @@ -9597,9 +11639,9 @@ PyObject *igraphmodule_Graph_complementer_op(igraphmodule_GraphObject * self) /* 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); + CREATE_GRAPH(result_o, g); - return (PyObject *) result; + return (PyObject *) result_o; } /** \ingroup python_interface_graph @@ -9608,17 +11650,17 @@ PyObject *igraphmodule_Graph_complementer_op(igraphmodule_GraphObject * self) PyObject *igraphmodule_Graph_compose(igraphmodule_GraphObject * self, PyObject * other) { - igraphmodule_GraphObject *o, *result; + igraphmodule_GraphObject *o, *result_o; igraph_t g; - if (!PyObject_TypeCheck(other, &igraphmodule_GraphType)) { + if (!PyObject_TypeCheck(other, igraphmodule_GraphType)) { Py_INCREF(Py_NotImplemented); return Py_NotImplemented; } o = (igraphmodule_GraphObject *) other; - if (igraph_compose(&g, &self->g, &o->g, /*edge_map1=*/ 0, - /*edge_map2=*/ 0)) { + if (igraph_compose(&g, &self->g, &o->g, /*edge_map1=*/ 0, + /*edge_map2=*/ 0)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -9626,9 +11668,45 @@ PyObject *igraphmodule_Graph_compose(igraphmodule_GraphObject * self, /* 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); + CREATE_GRAPH(result_o, g); + + return (PyObject *) result_o; +} + +/** \ingroup python_interface_graph + * \brief Reverses some edges in an \c igraph.Graph + * \return the modified \c igraph.Graph object + * \sa igraph_reverse_edges + */ +PyObject *igraphmodule_Graph_reverse_edges(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + PyObject *list = 0; + igraph_es_t es; + static char *kwlist[] = { "edges", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &list)) + return NULL; + + /* no arguments means reverse all; Py_None means reverse _nothing_ */ + if (list == Py_None) { + Py_RETURN_NONE; + } + + /* this one converts no arguments to all edges */ + if (igraphmodule_PyObject_to_es_t(list, &es, &self->g, 0)) { + /* something bad happened during conversion, return immediately */ + return NULL; + } + + if (igraph_reverse_edges(&self->g, es)) { + igraphmodule_handle_igraph_error(); + igraph_es_destroy(&es); + return NULL; + } - return (PyObject *) result; + igraph_es_destroy(&es); + Py_RETURN_NONE; } /********************************************************************** @@ -9642,47 +11720,59 @@ 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_o, *mode_o = Py_None, *vid_o; + igraph_int_t vid; 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)) + 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_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_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_i_bfs - (&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_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 */ + result_o = Py_BuildValue("NNN", l1, l2, l3); /* references stolen */ } else { if (l1) { Py_DECREF(l1); } if (l2) { Py_DECREF(l2); } if (l3) { Py_DECREF(l3); } - result = NULL; + result_o = NULL; } - igraph_vector_destroy(&vids); - igraph_vector_destroy(&layers); - igraph_vector_destroy(&parents); - return result; + + igraph_vector_int_destroy(&vids); + igraph_vector_int_destroy(&layers); + igraph_vector_int_destroy(&parents); + + return result_o; } /** \ingroup python_interface_graph @@ -9713,8 +11803,8 @@ 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_t result; + igraph_vector_int_t mapping, vids; + igraph_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &roots_o, &mode_o)) return NULL; @@ -9722,48 +11812,121 @@ 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); + if (igraph_unfold_tree(&self->g, &res, mode, &vids, &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); + igraph_destroy(&res); return NULL; } - CREATE_GRAPH(result_o, result); + CREATE_GRAPH(result_o, res); + if (!result_o) { + Py_DECREF(mapping_o); + return NULL; + } 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 * + **********************************************************************/ + +/** \ingroup python_interface_graph + * \brief Calculates the dominator tree for the graph + */ +PyObject *igraphmodule_Graph_dominator(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + static char *kwlist[] = { "vid", "mode", NULL }; + PyObject *list = Py_None, *mode_o = Py_None, *root_o; + igraph_int_t root; + igraph_vector_int_t dom; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_error_t res; + + 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; + } + + if (igraph_vector_int_init(&dom, 0)) { + return NULL; + } + + res = igraph_dominator_tree(&self->g, root, &dom, NULL, NULL, mode); + + 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; +} + /********************************************************************** * Maximum flows * **********************************************************************/ @@ -9775,33 +11938,36 @@ 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_real_t res; + igraph_int_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)) { + if (igraph_maxflow_value(&self->g, &res, v1, v2, &capacity_vector, + &stats)) { igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } igraph_vector_destroy(&capacity_vector); - return Py_BuildValue("d", (double)result); + + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -9811,20 +11977,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, cut, partition; + igraph_real_t res; + igraph_int_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)) @@ -9835,25 +12005,25 @@ 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(); } - if (igraph_maxflow(&self->g, &result, &flow, &cut, &partition, 0, - v1, v2, &capacity_vector, &stats)) { + if (igraph_maxflow(&self->g, &res, &flow, &cut, &partition, 0, + 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(); } @@ -9861,28 +12031,28 @@ PyObject *igraphmodule_Graph_maxflow(igraphmodule_GraphObject * self, flow_o = igraphmodule_vector_t_to_PyList(&flow, IGRAPHMODULE_TYPE_FLOAT); 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; - return Py_BuildValue("dOOO", (double)result, flow_o, cut_o, partition_o); + return Py_BuildValue("dNNN", (double)res, flow_o, cut_o, partition_o); } /********************************************************************** @@ -9896,8 +12066,8 @@ PyObject *igraphmodule_Graph_all_st_cuts(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", NULL }; - igraph_integer_t source, target; - igraph_vector_ptr_t cuts, partition1s; + igraph_int_t source, target; + igraph_vector_int_list_t cuts, partition1s; PyObject *source_o, *target_o; PyObject *cuts_o, *partition1s_o; @@ -9910,33 +12080,30 @@ PyObject *igraphmodule_Graph_all_st_cuts(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(target_o, &target, &self->g)) return NULL; - if (igraph_vector_ptr_init(&partition1s, 0)) { - return igraphmodule_handle_igraph_error(); + if (igraph_vector_int_list_init(&partition1s, 0)) { + return igraphmodule_handle_igraph_error(); } - if (igraph_vector_ptr_init(&cuts, 0)) { - igraph_vector_ptr_destroy(&partition1s); - return igraphmodule_handle_igraph_error(); + if (igraph_vector_int_list_init(&cuts, 0)) { + igraph_vector_int_list_destroy(&partition1s); + return igraphmodule_handle_igraph_error(); } if (igraph_all_st_cuts(&self->g, &cuts, &partition1s, source, target)) { - igraph_vector_ptr_destroy(&cuts); - igraph_vector_ptr_destroy(&partition1s); + igraph_vector_int_list_destroy(&cuts); + igraph_vector_int_list_destroy(&partition1s); 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); - - cuts_o = igraphmodule_vector_ptr_t_to_PyList(&cuts, IGRAPHMODULE_TYPE_INT); - igraph_vector_ptr_destroy_all(&cuts); + cuts_o = igraphmodule_vector_int_list_t_to_PyList(&cuts); + igraph_vector_int_list_destroy(&cuts); if (cuts_o == NULL) { - igraph_vector_ptr_destroy_all(&partition1s); + igraph_vector_int_list_destroy(&partition1s); return NULL; } - partition1s_o = igraphmodule_vector_ptr_t_to_PyList(&partition1s, IGRAPHMODULE_TYPE_INT); - igraph_vector_ptr_destroy_all(&partition1s); + partition1s_o = igraphmodule_vector_int_list_t_to_PyList(&partition1s); + igraph_vector_int_list_destroy(&partition1s); if (partition1s_o == NULL) return NULL; @@ -9950,9 +12117,9 @@ PyObject *igraphmodule_Graph_all_st_mincuts(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - igraph_integer_t source, target; + igraph_int_t source, target; igraph_real_t value; - igraph_vector_ptr_t cuts, partition1s; + igraph_vector_int_list_t cuts, partition1s; igraph_vector_t capacity_vector; PyObject *source_o, *target_o, *capacity_o = Py_None; PyObject *cuts_o, *partition1s_o; @@ -9966,44 +12133,41 @@ PyObject *igraphmodule_Graph_all_st_mincuts(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(target_o, &target, &self->g)) return NULL; - if (igraph_vector_ptr_init(&partition1s, 0)) { - return igraphmodule_handle_igraph_error(); + if (igraph_vector_int_list_init(&partition1s, 0)) { + return igraphmodule_handle_igraph_error(); } - if (igraph_vector_ptr_init(&cuts, 0)) { - igraph_vector_ptr_destroy(&partition1s); - return igraphmodule_handle_igraph_error(); + if (igraph_vector_int_list_init(&cuts, 0)) { + igraph_vector_int_list_destroy(&partition1s); + return igraphmodule_handle_igraph_error(); } if (igraphmodule_PyObject_to_attribute_values(capacity_o, &capacity_vector, self, ATTRHASH_IDX_EDGE, 1.0)) { - igraph_vector_ptr_destroy(&cuts); - igraph_vector_ptr_destroy(&partition1s); + igraph_vector_int_list_destroy(&cuts); + igraph_vector_int_list_destroy(&partition1s); return igraphmodule_handle_igraph_error(); } if (igraph_all_st_mincuts(&self->g, &value, &cuts, &partition1s, source, target, &capacity_vector)) { - igraph_vector_ptr_destroy(&cuts); - igraph_vector_ptr_destroy(&partition1s); + igraph_vector_int_list_destroy(&cuts); + igraph_vector_int_list_destroy(&partition1s); igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } 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); - - cuts_o = igraphmodule_vector_ptr_t_to_PyList(&cuts, IGRAPHMODULE_TYPE_INT); - igraph_vector_ptr_destroy_all(&cuts); + cuts_o = igraphmodule_vector_int_list_t_to_PyList(&cuts); + igraph_vector_int_list_destroy(&cuts); if (cuts_o == NULL) { - igraph_vector_ptr_destroy_all(&partition1s); + igraph_vector_int_list_destroy(&partition1s); return NULL; } - partition1s_o = igraphmodule_vector_ptr_t_to_PyList(&partition1s, IGRAPHMODULE_TYPE_INT); - igraph_vector_ptr_destroy_all(&partition1s); + partition1s_o = igraphmodule_vector_int_list_t_to_PyList(&partition1s); + igraph_vector_int_list_destroy(&partition1s); if (partition1s_o == NULL) return NULL; @@ -10017,15 +12181,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_real_t res, mincut; + igraph_int_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, @@ -10033,55 +12195,61 @@ 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)) { + if (igraph_mincut_value(&self->g, &res, &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; + res = -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(); } - if (result < 0 || result > mincut) - result = mincut; + if (res < 0 || res > mincut) + res = mincut; } - if (result < 0) - result = 0.0; - } - else if (v2 == -1) { + if (res < 0) { + res = 0.0; + } + } else if (v2 == -1) { n = igraph_vcount(&self->g); - result = -1; + res = -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) - result = mincut; + if (res < 0.0 || res > mincut) { + res = mincut; + } } - if (result < 0) - result = 0.0; - } - else { - if (igraph_st_mincut_value(&self->g, &result, v1, v2, &capacity_vector)) { + if (res < 0) + res = 0.0; + } else { + if (igraph_st_mincut_value(&self->g, &res, v1, v2, &capacity_vector)) { igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } } igraph_vector_destroy(&capacity_vector); - return Py_BuildValue("d", (double)result); + + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -10091,13 +12259,13 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - PyObject *capacity_object = Py_None, *cut_o, *part_o, *part2_o, *result; + PyObject *capacity_object = Py_None, *cut_o, *part_o, *part2_o, *result_o; 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_t partition, partition2, cut; - igraph_integer_t source = -1, target = -1; + igraph_vector_int_t partition, partition2, cut; + igraph_int_t source = -1, target = -1; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &source_o, &target_o, &capacity_object)) @@ -10113,20 +12281,20 @@ 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(); + 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(); + 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(); + return igraphmodule_handle_igraph_error(); } if (source == -1 && target == -1) { @@ -10140,11 +12308,11 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, retval = igraph_st_mincut(&self->g, &value, &cut, &partition, &partition2, source, target, &capacity_vector); } - + 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(); @@ -10153,32 +12321,32 @@ 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); - return 0; + 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); + Py_DECREF(cut_o); return 0; } - result = Py_BuildValue("dNNN", (double)value, cut_o, part_o, part2_o); - return result; + result_o = Py_BuildValue("dNNN", (double)value, cut_o, part_o, part2_o); + return result_o; } /** \ingroup python_interface_graph @@ -10225,8 +12393,7 @@ PyObject *igraphmodule_Graph_gomory_hu_tree(igraphmodule_GraphObject * self, CREATE_GRAPH(tree_o, tree); if (!tree_o) { - igraph_destroy(&tree); - return 0; + return NULL; } return Py_BuildValue("NN", tree_o, flow_o); @@ -10239,12 +12406,12 @@ PyObject *igraphmodule_Graph_st_mincut(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - igraph_integer_t source, target; - PyObject *cut_o, *part_o, *part2_o, *result; + igraph_int_t source, target; + PyObject *cut_o, *part_o, *part2_o, *result_o; 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)) @@ -10260,59 +12427,59 @@ 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); return NULL; } - result = Py_BuildValue("dNNN", (double)value, cut_o, part_o, part2_o); - return result; + result_o = Py_BuildValue("dNNN", (double)value, cut_o, part_o, part2_o); + return result_o; } /********************************************************************** @@ -10323,24 +12490,23 @@ PyObject *igraphmodule_Graph_st_mincut(igraphmodule_GraphObject * self, * \brief Returns all minimal s-t separators of a graph */ PyObject *igraphmodule_Graph_all_minimal_st_separators( - igraphmodule_GraphObject * self) { + igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { PyObject* result_o; - igraph_vector_ptr_t result; + igraph_vector_int_list_t res; - if (igraph_vector_ptr_init(&result, 0)) { + if (igraph_vector_int_list_init(&res, 0)) { igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_all_minimal_st_separators(&self->g, &result)) { + if (igraph_all_minimal_st_separators(&self->g, &res)) { igraphmodule_handle_igraph_error(); - igraph_vector_ptr_destroy(&result); + igraph_vector_int_list_destroy(&res); return NULL; } - result_o = igraphmodule_vector_ptr_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&result, igraph_vector_destroy); - igraph_vector_ptr_destroy_all(&result); + result_o = igraphmodule_vector_int_list_t_to_PyList(&res); + igraph_vector_int_list_destroy(&res); return result_o; } @@ -10352,7 +12518,7 @@ PyObject *igraphmodule_Graph_is_separator(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { PyObject* list = Py_None; - igraph_bool_t result; + igraph_bool_t res; igraph_vs_t vs; static char *kwlist[] = { "vertices", NULL }; @@ -10364,7 +12530,7 @@ PyObject *igraphmodule_Graph_is_separator(igraphmodule_GraphObject * self, return NULL; } - if (igraph_is_separator(&self->g, vs, &result)) { + if (igraph_is_separator(&self->g, vs, &res)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); return NULL; @@ -10372,7 +12538,7 @@ PyObject *igraphmodule_Graph_is_separator(igraphmodule_GraphObject * self, igraph_vs_destroy(&vs); - if (result) + if (res) Py_RETURN_TRUE; else Py_RETURN_FALSE; @@ -10385,7 +12551,7 @@ PyObject *igraphmodule_Graph_is_minimal_separator(igraphmodule_GraphObject * sel PyObject * args, PyObject * kwds) { PyObject* list = Py_None; - igraph_bool_t result; + igraph_bool_t res; igraph_vs_t vs; static char *kwlist[] = { "vertices", NULL }; @@ -10397,7 +12563,7 @@ PyObject *igraphmodule_Graph_is_minimal_separator(igraphmodule_GraphObject * sel return NULL; } - if (igraph_is_minimal_separator(&self->g, vs, &result)) { + if (igraph_is_minimal_separator(&self->g, vs, &res)) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); return NULL; @@ -10405,7 +12571,7 @@ PyObject *igraphmodule_Graph_is_minimal_separator(igraphmodule_GraphObject * sel igraph_vs_destroy(&vs); - if (result) + if (res) Py_RETURN_TRUE; else Py_RETURN_FALSE; @@ -10415,24 +12581,23 @@ PyObject *igraphmodule_Graph_is_minimal_separator(igraphmodule_GraphObject * sel * \brief Returns the minimum size separators of the graph */ PyObject *igraphmodule_Graph_minimum_size_separators( - igraphmodule_GraphObject * self) { + igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { PyObject* result_o; - igraph_vector_ptr_t result; + igraph_vector_int_list_t res; - if (igraph_vector_ptr_init(&result, 0)) { + if (igraph_vector_int_list_init(&res, 0)) { igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_minimum_size_separators(&self->g, &result)) { + if (igraph_minimum_size_separators(&self->g, &res)) { igraphmodule_handle_igraph_error(); - igraph_vector_ptr_destroy(&result); + igraph_vector_int_list_destroy(&res); return NULL; } - result_o = igraphmodule_vector_ptr_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&result, igraph_vector_destroy); - igraph_vector_ptr_destroy_all(&result); + result_o = igraphmodule_vector_int_list_t_to_PyList(&res); + igraph_vector_int_list_destroy(&res); return result_o; } @@ -10444,57 +12609,55 @@ PyObject *igraphmodule_Graph_minimum_size_separators( /** \ingroup python_interface_graph * \brief Calculates the cohesive block structure of a graph */ -PyObject *igraphmodule_Graph_cohesive_blocks(igraphmodule_GraphObject *self, - PyObject *args, PyObject *kwds) { +PyObject *igraphmodule_Graph_cohesive_blocks(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { PyObject *blocks_o, *cohesion_o, *parents_o, *result_o; - igraph_vector_ptr_t blocks; - igraph_vector_t cohesion, parents; + igraph_vector_int_list_t blocks; + igraph_vector_int_t cohesion, parents; - if (igraph_vector_ptr_init(&blocks, 0)) { + if (igraph_vector_int_list_init(&blocks, 0)) { igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_vector_init(&cohesion, 0)) { - igraph_vector_ptr_destroy(&blocks); + if (igraph_vector_int_init(&cohesion, 0)) { + igraph_vector_int_list_destroy(&blocks); igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_vector_init(&parents, 0)) { - igraph_vector_ptr_destroy(&blocks); - igraph_vector_destroy(&cohesion); + if (igraph_vector_int_init(&parents, 0)) { + igraph_vector_int_list_destroy(&blocks); + igraph_vector_int_destroy(&cohesion); igraphmodule_handle_igraph_error(); return NULL; } if (igraph_cohesive_blocks(&self->g, &blocks, &cohesion, &parents, 0)) { - igraph_vector_ptr_destroy(&blocks); - igraph_vector_destroy(&cohesion); - igraph_vector_destroy(&parents); + igraph_vector_int_list_destroy(&blocks); + igraph_vector_int_destroy(&cohesion); + igraph_vector_int_destroy(&parents); igraphmodule_handle_igraph_error(); return NULL; } - blocks_o = igraphmodule_vector_ptr_t_to_PyList(&blocks, IGRAPHMODULE_TYPE_INT); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&blocks, igraph_vector_destroy); - igraph_vector_ptr_destroy_all(&blocks); + blocks_o = igraphmodule_vector_int_list_t_to_PyList(&blocks); + igraph_vector_int_list_destroy(&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); @@ -10512,6 +12675,43 @@ PyObject *igraphmodule_Graph_cohesive_blocks(igraphmodule_GraphObject *self, return result_o; } +/********************************************************************** + * Coloring * + **********************************************************************/ + +PyObject *igraphmodule_Graph_vertex_coloring_greedy( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + static char *kwlist[] = { "method", NULL }; + igraph_vector_int_t result; + igraph_coloring_greedy_t heuristics = IGRAPH_COLORING_GREEDY_COLORED_NEIGHBORS; + PyObject *heuristics_o = Py_None; + PyObject *result_o; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &heuristics_o)) + return NULL; + + if (igraphmodule_PyObject_to_coloring_greedy_t(heuristics_o, &heuristics)) { + return NULL; + } + + if (igraph_vector_int_init(&result, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_vertex_coloring_greedy(&self->g, &result, heuristics)) { + igraph_vector_int_destroy(&result); + igraphmodule_handle_igraph_error(); + return NULL; + } + + result_o = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); + + return result_o; +} + /********************************************************************** * Cliques and independent sets * **********************************************************************/ @@ -10522,94 +12722,70 @@ PyObject *igraphmodule_Graph_cohesive_blocks(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "min", "max", NULL }; - PyObject *list, *item; - long int min_size = 0, max_size = 0; - long int i, j, n; - igraph_vector_ptr_t result; + static char *kwlist[] = { "min", "max", "max_results", NULL }; + PyObject *list; + PyObject *max_results_o = Py_None; + Py_ssize_t min_size = 0, max_size = 0; + igraph_vector_int_list_t res; + igraph_int_t max_results; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ll", kwlist, - &min_size, &max_size)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, + &min_size, &max_size, &max_results_o)) return NULL; - if (igraph_vector_ptr_init(&result, 0)) { - PyErr_SetString(PyExc_MemoryError, ""); - return NULL; + if (min_size >= 0) { + CHECK_SSIZE_T_RANGE(min_size, "minimum size"); + } else { + min_size = -1; } - if (igraph_cliques(&self->g, &result, (igraph_integer_t) min_size, - (igraph_integer_t) max_size)) { - igraph_vector_ptr_destroy(&result); - return igraphmodule_handle_igraph_error(); + if (max_size >= 0) { + CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + } else { + max_size = -1; } - n = (long)igraph_vector_ptr_size(&result); - list = PyList_New(n); - if (!list) + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { 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); - if (!item) { - for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); - igraph_vector_ptr_destroy_all(&result); - Py_DECREF(list); - return NULL; - } - else { - PyList_SET_ITEM(list, i, item); - } - igraph_vector_destroy(vec); + if (igraph_vector_int_list_init(&res, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; } - igraph_vector_ptr_destroy_all(&result); - return list; + if (igraph_cliques(&self->g, &res, min_size, max_size, max_results)) { + igraph_vector_int_list_destroy(&res); + return igraphmodule_handle_igraph_error(); + } + + + list = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&res); + igraph_vector_int_list_destroy(&res); + return list ? list : NULL; } /** \ingroup python_interface_graph * \brief Find all largest cliques in a graph */ -PyObject *igraphmodule_Graph_largest_cliques(igraphmodule_GraphObject * self) +PyObject *igraphmodule_Graph_largest_cliques(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { - PyObject *list, *item; - long int i, j, n; - igraph_vector_ptr_t result; + PyObject *list; + igraph_vector_int_list_t res; - if (igraph_vector_ptr_init(&result, 0)) { + if (igraph_vector_int_list_init(&res, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_largest_cliques(&self->g, &result)) { - igraph_vector_ptr_destroy(&result); + if (igraph_largest_cliques(&self->g, &res)) { + igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } - n = (long)igraph_vector_ptr_size(&result); - list = PyList_New(n); - if (!list) - 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); - if (!item) { - for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); - igraph_vector_ptr_destroy_all(&result); - Py_DECREF(list); - return NULL; - } - else { - PyList_SET_ITEM(list, i, item); - } - igraph_vector_destroy(vec); - } - igraph_vector_ptr_destroy_all(&result); - - return list; + list = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&res); + igraph_vector_int_list_destroy(&res); + return list ? list : NULL; } /** \ingroup python_interface_graph @@ -10621,7 +12797,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 res; double eps = -1; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|Od", kwlist, &types_o, @@ -10632,24 +12808,24 @@ 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)) { + if (igraph_vector_int_init(&res, 0)) { if (types != 0) { igraph_vector_bool_destroy(types); free(types); } if (weights != 0) { igraph_vector_destroy(weights); free(weights); } igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_maximum_bipartite_matching(&self->g, types, 0, 0, &result, weights, eps)) { + if (igraph_maximum_bipartite_matching(&self->g, types, 0, 0, &res, 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(&res); igraphmodule_handle_igraph_error(); return NULL; } @@ -10657,8 +12833,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(&res); + igraph_vector_int_destroy(&res); return result_o; } @@ -10668,60 +12844,44 @@ PyObject *igraphmodule_Graph_maximum_bipartite_matching(igraphmodule_GraphObject */ 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; - igraph_vector_ptr_t result; + static char* kwlist[] = { "min", "max", "file", "max_results", NULL }; + PyObject *list, *file = Py_None, *max_results_o = Py_None; + Py_ssize_t min = 0, max = 0; + igraph_vector_int_list_t res; igraphmodule_filehandle_t filehandle; + igraph_int_t max_results = IGRAPH_UNLIMITED; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|llO", kwlist, &i, &j, &file)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnOO", kwlist, &min, &max, &file, &max_results_o)) return NULL; - min = (igraph_integer_t) i; - max = (igraph_integer_t) j; + CHECK_SSIZE_T_RANGE(min, "minimum size"); + CHECK_SSIZE_T_RANGE(max, "maximum size"); + + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { + return NULL; + } if (file == Py_None) { - if (igraph_vector_ptr_init(&result, 0)) { + if (igraph_vector_int_list_init(&res, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_maximal_cliques(&self->g, &result, min, max)) { - igraph_vector_ptr_destroy(&result); + if (igraph_maximal_cliques(&self->g, &res, min, max, max_results)) { + igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } - n = (Py_ssize_t)igraph_vector_ptr_size(&result); - list = PyList_New(n); - if (!list) - 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); - if (!item) { - for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); - igraph_vector_ptr_destroy_all(&result); - Py_DECREF(list); - return NULL; - } - else { - PyList_SET_ITEM(list, i, item); - } - igraph_vector_destroy(vec); - } - igraph_vector_ptr_destroy_all(&result); + list = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&res); + igraph_vector_int_list_destroy(&res); + return list ? list : NULL; - return list; } else { if (igraphmodule_filehandle_init(&filehandle, file, "w")) { return igraphmodule_handle_igraph_error(); } if (igraph_maximal_cliques_file(&self->g, - igraphmodule_filehandle_get(&filehandle), min, max)) { + igraphmodule_filehandle_get(&filehandle), min, max, max_results)) { igraphmodule_filehandle_destroy(&filehandle); return igraphmodule_handle_igraph_error(); } @@ -10733,16 +12893,15 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, /** \ingroup python_interface_graph * \brief Returns the clique number of the graph */ -PyObject *igraphmodule_Graph_clique_number(igraphmodule_GraphObject * self) +PyObject *igraphmodule_Graph_clique_number(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { - PyObject *result; - igraph_integer_t i; + igraph_int_t i; - if (igraph_clique_number(&self->g, &i)) + if (igraph_clique_number(&self->g, &i)) { return igraphmodule_handle_igraph_error(); + } - result = PyInt_FromLong((long)i); - return result; + return igraphmodule_integer_t_to_PyObject(i); } /** \ingroup python_interface_graph @@ -10752,158 +12911,130 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "min", "max", NULL }; - PyObject *list, *item; - long int min_size = 0, max_size = 0; - long int i, j, n; - igraph_vector_ptr_t result; + static char *kwlist[] = { "min", "max", "max_results", NULL }; + PyObject *list, *max_results_o = Py_None; + Py_ssize_t min_size = 0, max_size = 0; + igraph_vector_int_list_t res; + igraph_int_t max_results = IGRAPH_UNLIMITED; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ll", kwlist, - &min_size, &max_size)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, + &min_size, &max_size, &max_results_o)) return NULL; - if (igraph_vector_ptr_init(&result, 0)) { - PyErr_SetString(PyExc_MemoryError, ""); - return NULL; + if (min_size >= 0) { + CHECK_SSIZE_T_RANGE(min_size, "minimum size"); + } else { + min_size = -1; } - if (igraph_independent_vertex_sets(&self->g, &result, - (igraph_integer_t) min_size, (igraph_integer_t) max_size)) { - igraph_vector_ptr_destroy(&result); - return igraphmodule_handle_igraph_error(); + if (max_size >= 0) { + CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + } else { + max_size = -1; } - n = (long)igraph_vector_ptr_size(&result); - list = PyList_New(n); - if (!list) + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { 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); - if (!item) { - for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); - igraph_vector_ptr_destroy_all(&result); - Py_DECREF(list); - return NULL; - } - else { - PyList_SET_ITEM(list, i, item); - } - igraph_vector_destroy(vec); + if (igraph_vector_int_list_init(&res, 0)) { + PyErr_SetString(PyExc_MemoryError, ""); + return NULL; } - igraph_vector_ptr_destroy_all(&result); - return list; + if (igraph_independent_vertex_sets(&self->g, &res, min_size, max_size, max_results)) { + igraph_vector_int_list_destroy(&res); + return igraphmodule_handle_igraph_error(); + } + + list = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&res); + igraph_vector_int_list_destroy(&res); + return list ? list : NULL; } /** \ingroup python_interface_graph * \brief Find all largest independent_vertex_sets in a graph */ -PyObject - *igraphmodule_Graph_largest_independent_vertex_sets(igraphmodule_GraphObject - * self) -{ - PyObject *list, *item; - long int i, j, n; - igraph_vector_ptr_t result; +PyObject *igraphmodule_Graph_largest_independent_vertex_sets( + igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null) +) { + PyObject *list; + igraph_vector_int_list_t res; - if (igraph_vector_ptr_init(&result, 0)) { + if (igraph_vector_int_list_init(&res, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_largest_independent_vertex_sets(&self->g, &result)) { - igraph_vector_ptr_destroy(&result); + if (igraph_largest_independent_vertex_sets(&self->g, &res)) { + igraph_vector_int_list_destroy(&res); return igraphmodule_handle_igraph_error(); } - n = (long)igraph_vector_ptr_size(&result); - list = PyList_New(n); - if (!list) - 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); - if (!item) { - for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); - igraph_vector_ptr_destroy_all(&result); - Py_DECREF(list); - return NULL; - } - else { - PyList_SET_ITEM(list, i, item); - } - igraph_vector_destroy(vec); - } - igraph_vector_ptr_destroy_all(&result); - - return list; + list = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&res); + igraph_vector_int_list_destroy(&res); + return list ? list : NULL; } /** \ingroup python_interface_graph * \brief Find all maximal independent vertex sets in a graph */ -PyObject - *igraphmodule_Graph_maximal_independent_vertex_sets(igraphmodule_GraphObject - * self) -{ - PyObject *list, *item; - long int i, j, n; - igraph_vector_ptr_t result; +PyObject *igraphmodule_Graph_maximal_independent_vertex_sets( + igraphmodule_GraphObject *self, PyObject* args, PyObject *kwds +) { + static char *kwlist[] = { "min", "max", "max_results", NULL }; + PyObject *list, *max_results_o = Py_None; + Py_ssize_t min_size = 0, max_size = 0; + igraph_vector_int_list_t res; + igraph_int_t max_results = IGRAPH_UNLIMITED; - if (igraph_vector_ptr_init(&result, 0)) { - PyErr_SetString(PyExc_MemoryError, ""); + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, &min_size, &max_size, &max_results_o)) return NULL; + + if (min_size >= 0) { + CHECK_SSIZE_T_RANGE(min_size, "minimum size"); + } else { + min_size = -1; } - if (igraph_maximal_independent_vertex_sets(&self->g, &result)) { - igraph_vector_ptr_destroy(&result); - return igraphmodule_handle_igraph_error(); + if (max_size >= 0) { + CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + } else { + max_size = -1; } - n = (long)igraph_vector_ptr_size(&result); - list = PyList_New(n); - if (!list) + if (igraphmodule_PyObject_to_max_results_t(max_results_o, &max_results)) { 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); - if (!item) { - for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); - igraph_vector_ptr_destroy_all(&result); - Py_DECREF(list); - return NULL; - } - else { - PyList_SET_ITEM(list, i, item); - } - igraph_vector_destroy(vec); + if (igraph_vector_int_list_init(&res, 0)) { + PyErr_SetString(PyExc_MemoryError, ""); + return NULL; } - igraph_vector_ptr_destroy_all(&result); - return list; + if (igraph_maximal_independent_vertex_sets(&self->g, &res, min_size, max_size, max_results)) { + igraph_vector_int_list_destroy(&res); + return igraphmodule_handle_igraph_error(); + } + + list = igraphmodule_vector_int_list_t_to_PyList_of_tuples(&res); + igraph_vector_int_list_destroy(&res); + return list ? list : NULL; } /** \ingroup python_interface_graph * \brief Returns the independence number of the graph */ -PyObject *igraphmodule_Graph_independence_number(igraphmodule_GraphObject * - self) -{ - PyObject *result; - igraph_integer_t i; +PyObject *igraphmodule_Graph_independence_number( + igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null) +) { + igraph_int_t i; - if (igraph_independence_number(&self->g, &i)) + if (igraph_independence_number(&self->g, &i)) { return igraphmodule_handle_igraph_error(); + } - result = PyInt_FromLong((long)i); - return result; + return igraphmodule_integer_t_to_PyObject(i); } /********************************************************************** @@ -10919,7 +13050,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 res; PyObject *o, *mode_o = Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) @@ -10927,16 +13058,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(&res, igraph_vcount(&self->g))) return igraphmodule_handle_igraph_error(); - if (igraph_coreness(&self->g, &result, mode)) { - igraph_vector_destroy(&result); + if (igraph_coreness(&self->g, &res, mode)) { + igraph_vector_int_destroy(&res); 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(&res); + igraph_vector_int_destroy(&res); return o; } @@ -10950,30 +13081,84 @@ PyObject *igraphmodule_Graph_coreness(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_modularity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"membership", "weights", 0}; - igraph_vector_t membership; + static char *kwlist[] = {"membership", "weights", "resolution", "directed", 0}; + igraph_vector_int_t membership; igraph_vector_t *weights=0; + double resolution = 1; 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)) + 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, &modularity, weights)) { - igraph_vector_destroy(&membership); - if (weights) { igraph_vector_destroy(weights); free(weights); } + + if (igraph_modularity(&self->g, &membership, weights, resolution, PyObject_IsTrue(directed), &modularity)) { + igraph_vector_int_destroy(&membership); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } return NULL; } - igraph_vector_destroy(&membership); - if (weights) { igraph_vector_destroy(weights); free(weights); } - return Py_BuildValue("d", (double)modularity); + + igraph_vector_int_destroy(&membership); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + + return igraphmodule_real_t_to_PyObject(modularity, IGRAPHMODULE_TYPE_FLOAT); +} + +/** + * Modularity matrix calculation + */ +PyObject *igraphmodule_Graph_modularity_matrix(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"weights", "resolution", "directed", 0}; + igraph_vector_t *weights=0; + double resolution = 1; + igraph_matrix_t result; + PyObject *wvec = Py_None; + PyObject *directed = Py_True; + PyObject *result_o; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OdO", kwlist, &wvec, &resolution, &directed)) + return NULL; + + if (igraphmodule_attrib_to_vector_t(wvec, self, &weights, ATTRIBUTE_TYPE_EDGE)) + return NULL; + + if (igraph_matrix_init(&result, 0, 0)) { + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + return NULL; + } + + if (igraph_modularity_matrix(&self->g, weights, resolution, &result, PyObject_IsTrue(directed))) { + igraph_matrix_destroy(&result); + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + return NULL; + } + + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + + result_o = igraphmodule_matrix_t_to_PyList(&result, IGRAPHMODULE_TYPE_FLOAT); + + igraph_matrix_destroy(&result); + + return result_o; } /** @@ -10984,62 +13169,66 @@ 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; + igraph_vector_t *weights = NULL; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &directed, &weights_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &directed, &weights_o)) { return NULL; + } - if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { + return NULL; + } - if (igraph_matrix_init(&merges, 0, 0)) { - if (weights != 0) { + if (igraph_matrix_int_init(&merges, 0, 0)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } return igraphmodule_handle_igraph_error(); } if (igraph_vector_init(&q, 0)) { - igraph_matrix_destroy(&merges); - if (weights != 0) { + igraph_matrix_int_destroy(&merges); + if (weights) { igraph_vector_destroy(weights); free(weights); } return igraphmodule_handle_igraph_error(); } if (igraph_community_edge_betweenness(&self->g, - /* removed_edges = */ 0, - /* edge_betweenness = */ 0, + /* removed_edges = */ NULL, + /* edge_betweenness = */ NULL, /* merges = */ &merges, - /* bridges = */ 0, + /* bridges = */ NULL, /* modularity = */ &q, - /* membership = */ 0, + /* membership = */ NULL, PyObject_IsTrue(directed), - weights)) { - igraphmodule_handle_igraph_error(); - if (weights != 0) { + weights, + /* lengths = */ NULL)) { + + igraph_vector_destroy(&q); + igraph_matrix_int_destroy(&merges); + if (weights) { igraph_vector_destroy(weights); free(weights); } - igraph_matrix_destroy(&merges); - igraph_vector_destroy(&q); - return NULL; + + return igraphmodule_handle_igraph_error();; } - if (weights != 0) { + if (weights) { igraph_vector_destroy(weights); free(weights); } - qs=igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); + 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); @@ -11056,46 +13245,48 @@ 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_ssize_t 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_matrix_int_t m; igraph_real_t q; igraphmodule_ARPACKOptionsObject *arpack_options; PyObject *arpack_options_o = igraphmodule_arpack_options_default; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lOO!", kwlist, &n, &weights_obj, - &igraphmodule_ARPACKOptionsType, &arpack_options_o)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nOO!", kwlist, &n, &weights_obj, + igraphmodule_ARPACKOptionsType, &arpack_options_o)) { return NULL; } - if (igraph_vector_init(&members, 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_init(&m, 0, 0)) { + if (igraph_matrix_int_init(&m, 0, 0)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&members); + igraph_vector_int_destroy(&membership); 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)) { - igraph_matrix_destroy(&m); - igraph_vector_destroy(&members); + 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, &members, (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_destroy(&m); - igraph_vector_destroy(&members); + igraph_matrix_int_destroy(&m); + igraph_vector_int_destroy(&membership); if (weights != 0) { igraph_vector_destroy(weights); free(weights); } @@ -11106,15 +13297,15 @@ 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); + 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; @@ -11131,7 +13322,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)) { @@ -11141,14 +13332,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) { @@ -11158,12 +13349,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_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); @@ -11183,44 +13374,48 @@ 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_t membership; + + igraph_vector_int_t membership; PyObject *res = Py_False; 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; } - if (igraph_vector_init(&membership, igraph_vcount(&self->g))) { - igraphmodule_handle_igraph_error(); + 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; } 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); } return NULL; } - - if (igraph_community_infomap(/*in */ &self->g, - /*e_weight=*/ e_ws, /*v_weight=*/ v_ws, - /*nb_trials=*/nb_trials, - /*out*/ &membership, &codelength)) { - igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&membership); + + if (igraph_community_infomap( + /*in */ &self->g, /*e_weight=*/ e_ws, /*v_weight=*/ v_ws, + /*nb_trials=*/nb_trials, /*is_regularized=*/0, + /*regularization_strength=*/ 0, + /*out*/ &membership, &codelength) + ) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&membership); if (e_ws) { igraph_vector_destroy(e_ws); free(e_ws); @@ -11231,23 +13426,23 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, } return NULL; } - + if (e_ws) { igraph_vector_destroy(e_ws); free(e_ws); } - + if (v_ws) { igraph_vector_destroy(v_ws); 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; - + return NULL; + return Py_BuildValue("Nd", res, (double)codelength); } @@ -11258,16 +13453,23 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_community_label_propagation( igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "weights", "initial", "fixed", NULL }; + static char *kwlist[] = { "weights", "initial", "fixed", "variant", NULL }; PyObject *weights_o = Py_None, *initial_o = Py_None, *fixed_o = Py_None; - PyObject *result; - igraph_vector_t membership, *ws = 0, *initial = 0; + PyObject *variant_o = NULL; + PyObject *result_o; + igraph_vector_int_t membership, *initial = 0; + igraph_vector_t *ws = 0; igraph_vector_bool_t fixed; + igraph_lpa_variant_t variant = IGRAPH_LPA_DOMINANCE; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &weights_o, &initial_o, &fixed_o)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, &weights_o, &initial_o, &fixed_o, &variant_o)) { return NULL; } + if (igraphmodule_PyObject_to_lpa_variant_t(variant_o, &variant)) { + return NULL; + } + if (fixed_o != Py_None) { if (igraphmodule_PyObject_to_vector_bool_t(fixed_o, &fixed)) return NULL; @@ -11278,30 +13480,30 @@ 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; } - 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)) { + IGRAPH_OUT, ws, initial, (fixed_o != Py_None ? &fixed : 0), variant)) { 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); + 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_t_to_PyList(&membership, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&membership); + result_o=igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); - return result; + return result_o; } /** @@ -11310,30 +13512,32 @@ 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; + igraph_matrix_int_t memberships; + igraph_vector_int_t membership; + igraph_vector_t modularity; + double resolution = 1; 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; } if (igraphmodule_attrib_to_vector_t(weights, self, &ws, ATTRIBUTE_TYPE_EDGE)) return NULL; - igraph_matrix_init(&memberships, 0, 0); - igraph_vector_init(&membership, 0); + igraph_matrix_int_init(&memberships, 0, 0); + igraph_vector_int_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); + igraph_vector_int_destroy(&membership); igraph_vector_destroy(&modularity); - igraph_matrix_destroy(&memberships); + igraph_matrix_int_destroy(&memberships); return igraphmodule_handle_igraph_error(); } @@ -11342,246 +13546,681 @@ 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_matrix_destroy(&memberships); + igraph_vector_int_destroy(&membership); + 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 { res=Py_BuildValue("NN", mss, qs); /* steals references */ } - } else { - res=igraphmodule_vector_t_to_PyList(&membership, IGRAPHMODULE_TYPE_INT); - } + } else { + res=igraphmodule_vector_int_t_to_PyList(&membership); + } + + igraph_vector_int_destroy(&membership); + igraph_matrix_int_destroy(&memberships); + + return res; +} + +/** + * Optimal modularity by integer programming + */ +PyObject *igraphmodule_Graph_community_optimal_modularity( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"weights", "resolution", NULL}; + PyObject *weights_o = Py_None; + igraph_real_t modularity; + igraph_vector_int_t membership; + igraph_vector_t* weights = 0; + double resolution = 1; + PyObject *res; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Od", kwlist, + &weights_o, &resolution)) + return NULL; + + 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_int_destroy(&membership); + return NULL; + } + + if (igraph_community_optimal_modularity(&self->g, weights, resolution, &modularity, + &membership)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&membership); + if (weights != 0) { + igraph_vector_destroy(weights); free(weights); + } + return NULL; + } + + if (weights != 0) { + igraph_vector_destroy(weights); free(weights); + } + + res = igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); + + if (!res) + return NULL; + + return Py_BuildValue("Nd", res, (double)modularity); +} + +/** + * Spinglass community detection method of Reichardt & Bornholdt + */ +PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"weights", "spins", "parupdate", + "start_temp", "stop_temp", "cool_fact", "update_rule", + "gamma", "implementation", "lambda_", NULL}; + PyObject *weights_o = Py_None; + PyObject *parupdate_o = Py_False; + PyObject *update_rule_o = Py_None; + PyObject *impl_o = Py_None; + PyObject *res; + + Py_ssize_t spins = 25; + double start_temp = 1.0; + double stop_temp = 0.01; + double cool_fact = 0.99; + igraph_spinglass_implementation_t impl = IGRAPH_SPINCOMM_IMP_ORIG; + igraph_spincomm_update_t update_rule = IGRAPH_SPINCOMM_UPDATE_CONFIG; + double gamma = 1; + double lambda = 1; + igraph_vector_t *weights = 0; + igraph_vector_int_t membership; + + 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; + } + + if (igraphmodule_PyObject_to_spinglass_implementation_t(impl_o, &impl)) { + return NULL; + } + + 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_int_destroy(&membership); + return NULL; + } + + if (igraph_community_spinglass(&self->g, weights, + 0, 0, &membership, 0, spins, + PyObject_IsTrue(parupdate_o), + start_temp, stop_temp, cool_fact, + update_rule, gamma, impl, lambda)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&membership); + if (weights != 0) { + igraph_vector_destroy(weights); + free(weights); + } + return NULL; + } + + if (weights != 0) { + igraph_vector_destroy(weights); + free(weights); + } + + res = igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); + + return res; +} + +/** + * Walktrap community detection of Latapy & Pons + */ +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_int_t merges; + Py_ssize_t steps = 4; + igraph_vector_t q, *ws=0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|On", kwlist, &weights, &steps)) + return NULL; + + 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); + + if (igraph_community_walktrap(&self->g, ws, steps, &merges, &q, 0)) { + if (ws) { + igraph_vector_destroy(ws); free(ws); + } + igraph_vector_destroy(&q); + igraph_matrix_int_destroy(&merges); + return igraphmodule_handle_igraph_error(); + } + if (ws) { + igraph_vector_destroy(ws); free(ws); + } + + qs = igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); + igraph_vector_destroy(&q); + if (!qs) { + igraph_matrix_int_destroy(&merges); + return NULL; + } + + ms = igraphmodule_matrix_int_t_to_PyList(&merges); + igraph_matrix_int_destroy(&merges); + + if (ms == NULL) { + Py_DECREF(qs); + return NULL; + } + + res=Py_BuildValue("NN", ms, qs); /* steals references */ + + return res; +} + +/** + * Leiden community detection method of Traag, Waltman & van Eck + */ +PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + + static char *kwlist[] = {"edge_weights", "node_weights", "node_in_weights", "resolution", + "normalize_resolution", "beta", "initial_membership", "n_iterations", NULL}; + + PyObject *edge_weights_o = Py_None; + PyObject *node_weights_o = Py_None; + PyObject *node_in_weights_o = Py_None; + PyObject *initial_membership_o = Py_None; + PyObject *normalize_resolution = Py_False; + PyObject *res = Py_None; + + int error = 0; + Py_ssize_t n_iterations = 2; + double resolution = 1.0; + double beta = 0.01; + igraph_vector_t *edge_weights = NULL, *node_weights = NULL, *node_in_weights = NULL; + igraph_vector_int_t *membership = NULL; + igraph_bool_t start = true; + igraph_int_t nb_clusters = 0; + igraph_real_t quality = 0.0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOdOdOn", kwlist, + &edge_weights_o, &node_weights_o, &node_in_weights_o, &resolution, &normalize_resolution, &beta, &initial_membership_o, &n_iterations)) + return NULL; + + 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, + ATTRIBUTE_TYPE_EDGE)) { + igraphmodule_handle_igraph_error(); + error = -1; + } + + /* Get node weights */ + if (!error && igraphmodule_attrib_to_vector_t(node_weights_o, self, &node_weights, + ATTRIBUTE_TYPE_VERTEX)) { + igraphmodule_handle_igraph_error(); + error = -1; + } + + /* Get node in-weights (directed case) */ + if (!error && igraphmodule_attrib_to_vector_t(node_in_weights_o, self, &node_in_weights, + ATTRIBUTE_TYPE_VERTEX)) { + igraphmodule_handle_igraph_error(); + error = -1; + } + + /* Get initial membership */ + if (!error && igraphmodule_attrib_to_vector_int_t(initial_membership_o, self, &membership, + ATTRIBUTE_TYPE_VERTEX)) { + igraphmodule_handle_igraph_error(); + error = -1; + } + + if (!error && membership == 0) { + start = 0; + membership = (igraph_vector_int_t*)calloc(1, sizeof(igraph_vector_int_t)); + if (membership == 0) { + PyErr_NoMemory(); + error = -1; + } else if (igraph_vector_int_init(membership, 0)) { + igraphmodule_handle_igraph_error(); + error = -1; + } + } + + if (!error && PyObject_IsTrue(normalize_resolution)) + { + /* If we need to normalize the resolution parameter, + * we will need to have node weights. */ + if (node_weights == 0) + { + node_weights = (igraph_vector_t*)calloc(1, sizeof(igraph_vector_t)); + if (node_weights == 0) { + PyErr_NoMemory(); + error = -1; + } else if (igraph_vector_init(node_weights, 0)) { + igraphmodule_handle_igraph_error(); + error = -1; + } else if (igraph_strength( + &self->g, node_weights, igraph_vss_all(), + igraph_is_directed(&self->g) ? IGRAPH_OUT : IGRAPH_ALL, + IGRAPH_NO_LOOPS, edge_weights + )) { + igraphmodule_handle_igraph_error(); + error = -1; + } + } + resolution /= igraph_vector_sum(node_weights); + } + + /* Run actual Leiden algorithm for several iterations. */ + if (!error) { + error = igraph_community_leiden(&self->g, + edge_weights, node_weights, node_in_weights, + resolution, beta, + start, n_iterations, membership, + &nb_clusters, &quality); + } + + if (edge_weights != 0) { + igraph_vector_destroy(edge_weights); + free(edge_weights); + } + if (node_weights != 0) { + igraph_vector_destroy(node_weights); + free(node_weights); + } + if (node_in_weights != 0) { + igraph_vector_destroy(node_in_weights); + free(node_in_weights); + } + + if (!error && membership != 0) { + res = igraphmodule_vector_int_t_to_PyList(membership); + } + + if (membership != 0) { + igraph_vector_int_destroy(membership); + free(membership); + } + + return error ? NULL : Py_BuildValue("Nd", res, (double) quality); +} + + /** + * Fluid communities + */ +PyObject *igraphmodule_Graph_community_fluid_communities(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"no_of_communities", NULL}; + Py_ssize_t no_of_communities; + igraph_vector_int_t membership; + PyObject *result; + + // Parse the Python integer argument + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n", kwlist, &no_of_communities)) { + return NULL; + } + + if (igraph_vector_int_init(&membership, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_community_fluid_communities(&self->g, no_of_communities, &membership)) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&membership); + return NULL; + } - igraph_vector_destroy(&membership); - igraph_matrix_destroy(&memberships); + result = igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); - return res; + return result; } /** - * Optimal modularity by integer programming + * Voronoi clustering */ -PyObject *igraphmodule_Graph_community_optimal_modularity( - igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"weights", NULL}; - PyObject *weights_o = Py_None; - igraph_real_t modularity; - igraph_vector_t membership; - igraph_vector_t* weights = 0; - PyObject *res; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, - &weights_o)) - return NULL; +PyObject *igraphmodule_Graph_community_voronoi(igraphmodule_GraphObject *self, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"lengths", "weights", "mode", "radius", NULL}; + PyObject *lengths_o = Py_None, *weights_o = Py_None; + PyObject *mode_o = Py_None; + PyObject *radius_o = Py_None; + igraph_vector_t *lengths_v = NULL; + igraph_vector_t *weights_v = NULL; + igraph_vector_int_t membership_v, generators_v; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_real_t radius = -1.0; /* negative means auto-optimize */ + igraph_real_t modularity = IGRAPH_NAN; + PyObject *membership_o, *generators_o, *result_o; - if (igraph_vector_init(&membership, igraph_vcount(&self->g))) { - igraphmodule_handle_igraph_error(); + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, + &lengths_o, &weights_o, &mode_o, &radius_o)) { return NULL; } - if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { - igraph_vector_destroy(&membership); + /* Handle mode parameter */ + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { return NULL; } - if (igraph_community_optimal_modularity(&self->g, &modularity, - &membership, weights)) { - igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&membership); - if (weights != 0) { - igraph_vector_destroy(weights); free(weights); + /* Handle radius parameter */ + if (radius_o != Py_None) { + if (igraphmodule_PyObject_to_real_t(radius_o, &radius)) { + return NULL; } - return NULL; } - if (weights != 0) { - igraph_vector_destroy(weights); free(weights); + /* Handle lengths parameter */ + if (igraphmodule_attrib_to_vector_t(lengths_o, self, &lengths_v, ATTRIBUTE_TYPE_EDGE)) { + return NULL; } - res = igraphmodule_vector_t_to_PyList(&membership, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&membership); - - if (!res) - return NULL; - - return Py_BuildValue("Nd", res, (double)modularity); -} - -/** - * Spinglass community detection method of Reichardt & Bornholdt - */ -PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, - PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"weights", "spins", "parupdate", - "start_temp", "stop_temp", "cool_fact", "update_rule", - "gamma", "implementation", "lambda_", NULL}; - PyObject *weights_o = Py_None; - PyObject *parupdate_o = Py_False; - PyObject *update_rule_o = Py_None; - PyObject *impl_o = Py_None; - PyObject *res; - - long int spins = 25; - double start_temp = 1.0; - double stop_temp = 0.01; - double cool_fact = 0.99; - igraph_spinglass_implementation_t impl = IGRAPH_SPINCOMM_IMP_ORIG; - igraph_spincomm_update_t update_rule = IGRAPH_SPINCOMM_UPDATE_CONFIG; - double gamma = 1; - double lambda = 1; - igraph_vector_t *weights = 0, membership; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlOdddOdOd", kwlist, - &weights_o, &spins, &parupdate_o, &start_temp, &stop_temp, - &cool_fact, &update_rule_o, &gamma, &impl_o, &lambda)) + /* Handle weights parameter */ + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights_v, ATTRIBUTE_TYPE_EDGE)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } return NULL; + } - if (igraphmodule_PyObject_to_spincomm_update_t(update_rule_o, &update_rule)) { + /* Initialize result vectors */ + if (igraph_vector_int_init(&membership_v, 0)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraphmodule_handle_igraph_error(); return NULL; } - if (igraphmodule_PyObject_to_spinglass_implementation_t(impl_o, &impl)) { + if (igraph_vector_int_init(&generators_v, 0)) { + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraph_vector_int_destroy(&membership_v); + igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_vector_init(&membership, igraph_vcount(&self->g))) { - igraphmodule_handle_igraph_error(); + /* Call the C function - pass NULL for None parameters */ + if (igraph_community_voronoi(&self->g, &membership_v, &generators_v, + &modularity, + lengths_v, + weights_v, + mode, radius)) { + + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); + } + igraph_vector_int_destroy(&membership_v); + igraph_vector_int_destroy(&generators_v); + igraphmodule_handle_igraph_error(); return NULL; } - if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { - igraph_vector_destroy(&membership); - return NULL; + /* Clean up input vectors */ + if (lengths_v != NULL) { + igraph_vector_destroy(lengths_v); free(lengths_v); + } + if (weights_v != NULL) { + igraph_vector_destroy(weights_v); free(weights_v); } - if (igraph_community_spinglass(&self->g, weights, - 0, 0, &membership, 0, (igraph_integer_t) spins, - PyObject_IsTrue(parupdate_o), - start_temp, stop_temp, cool_fact, - update_rule, gamma, impl, lambda)) { - igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&membership); - if (weights != 0) { - igraph_vector_destroy(weights); - free(weights); - } + /* Convert results to Python objects */ + membership_o = igraphmodule_vector_int_t_to_PyList(&membership_v); + igraph_vector_int_destroy(&membership_v); + if (!membership_o) { + igraph_vector_int_destroy(&generators_v); return NULL; } - if (weights != 0) { - igraph_vector_destroy(weights); - free(weights); + generators_o = igraphmodule_vector_int_t_to_PyList(&generators_v); + igraph_vector_int_destroy(&generators_v); + if (!generators_o) { + Py_DECREF(membership_o); + return NULL; } - res = igraphmodule_vector_t_to_PyList(&membership, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&membership); + /* Return tuple with membership, generators, and modularity */ + result_o = Py_BuildValue("(NNd)", membership_o, generators_o, modularity); - return res; + return result_o; } +/********************************************************************** + * Random walks * + **********************************************************************/ + /** - * Walktrap community detection of Latapy & Pons + * Simple random walk of a given length */ -PyObject *igraphmodule_Graph_community_walktrap(igraphmodule_GraphObject * self, +PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "weights", "steps", NULL }; - PyObject *ms, *qs, *res, *weights = Py_None; - igraph_matrix_t merges; - int steps=4; - igraph_vector_t q, *ws=0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oi", kwlist, &weights, - &steps)) - return NULL; + static char *kwlist[] = { "start", "steps", "mode", "stuck", "weights", "return_type", NULL }; + PyObject *start_o, *mode_o = Py_None, *stuck_o = Py_None, *weights_o = Py_None, *return_type_o = Py_None; + igraph_int_t start; + Py_ssize_t steps = 10; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_random_walk_stuck_t stuck = IGRAPH_RANDOM_WALK_STUCK_RETURN; + igraph_vector_t *weights=0; + int return_type = 1; /* the default is "vertices" */ + igraph_vector_int_t vertices, edges; + PyObject *resv, *rese; + static igraphmodule_enum_translation_table_entry_t return_type_tt[] = { + {"vertices", 1}, + {"edges", 2}, + {"both", 3}, + {0,0} + }; - if (igraphmodule_attrib_to_vector_t(weights, self, &ws, ATTRIBUTE_TYPE_EDGE)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OnOOOO", kwlist, &start_o, + &steps, &mode_o, &stuck_o, &weights_o, &return_type_o)) return NULL; - igraph_matrix_init(&merges, 0, 0); - igraph_vector_init(&q, 0); + CHECK_SSIZE_T_RANGE(steps, "number of steps"); - if (igraph_community_walktrap(&self->g, ws, steps, &merges, &q, 0)) { - if (ws) { - igraph_vector_destroy(ws); free(ws); - } - igraph_vector_destroy(&q); - igraph_matrix_destroy(&merges); - return igraphmodule_handle_igraph_error(); + if (igraphmodule_PyObject_to_vid(start_o, &start, &self->g)) { + return NULL; } - if (ws) { - igraph_vector_destroy(ws); free(ws); + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { + return NULL; } - qs = igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); - igraph_vector_destroy(&q); - if (!qs) { - igraph_matrix_destroy(&merges); + if (igraphmodule_PyObject_to_random_walk_stuck_t(stuck_o, &stuck)) { return NULL; } - ms = igraphmodule_matrix_t_to_PyList(&merges, IGRAPHMODULE_TYPE_INT); - igraph_matrix_destroy(&merges); + /* Figure out return type */ + if (return_type_o != Py_None) { + if (igraphmodule_PyObject_to_enum_strict(return_type_o, return_type_tt, &return_type)) { + return NULL; + } + if (return_type == 0){ + PyErr_SetString(PyExc_ValueError, + "return_type must be \"vertices\", \"edges\", or \"both\"."); + return NULL; + } + } - if (ms == NULL) { - Py_DECREF(qs); - return NULL; + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, + ATTRIBUTE_TYPE_EDGE)) { + return NULL; } - res=Py_BuildValue("NN", ms, qs); /* steals references */ + /* Only return vertices */ + if (return_type == 1) { + if (igraph_vector_int_init(&vertices, 0)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + return igraphmodule_handle_igraph_error(); + } - return res; + if (igraph_random_walk(&self->g, + weights, + &vertices, NULL, + start, mode, steps, stuck)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraph_vector_int_destroy(&vertices); + return igraphmodule_handle_igraph_error(); + } + + if (weights) { igraph_vector_destroy(weights); free(weights); } + resv = igraphmodule_vector_int_t_to_PyList(&vertices); + igraph_vector_int_destroy(&vertices); + return resv; + + /* only return edges */ + } else if (return_type == 2) { + if (igraph_vector_int_init(&edges, 0)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + return igraphmodule_handle_igraph_error(); + } + + if (igraph_random_walk(&self->g, + weights, + NULL, &edges, + start, mode, steps, stuck)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraph_vector_int_destroy(&edges); + return igraphmodule_handle_igraph_error(); + } + + if (weights) { igraph_vector_destroy(weights); free(weights); } + rese = igraphmodule_vector_int_t_to_PyList(&edges); + igraph_vector_int_destroy(&edges); + return rese; + + /* return both vertices and edges, as a dict */ + } else { + if (igraph_vector_int_init(&vertices, 0)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + return igraphmodule_handle_igraph_error(); + } + if (igraph_vector_int_init(&edges, 0)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraph_vector_int_destroy(&vertices); + return igraphmodule_handle_igraph_error(); + } + + if (igraph_random_walk(&self->g, + weights, + &vertices, &edges, + start, mode, steps, stuck)) { + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraph_vector_int_destroy(&vertices); + igraph_vector_int_destroy(&edges); + return igraphmodule_handle_igraph_error(); + } + + if (weights) { igraph_vector_destroy(weights); free(weights); } + + resv = igraphmodule_vector_int_t_to_PyList(&vertices); + igraph_vector_int_destroy(&vertices); + if (resv == NULL) { + igraph_vector_int_destroy(&edges); + return resv; + } + + rese = igraphmodule_vector_int_t_to_PyList(&edges); + igraph_vector_int_destroy(&edges); + if (rese == NULL) { + return rese; + } + + return Py_BuildValue("{s:O,s:O}", + "vertices", resv, + "edges", rese); /* steals references */ + } } /********************************************************************** - * Random walks * + * Spatial graphs * **********************************************************************/ -/** - * Simple random walk of a given length - */ -PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) { - 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; - igraph_neimode_t mode = IGRAPH_OUT; - igraph_random_walk_stuck_t stuck = IGRAPH_RANDOM_WALK_STUCK_RETURN; - igraph_vector_t walk; +PyObject *igraphmodule_Graph_Nearest_Neighbor_Graph(PyTypeObject *type, + PyObject *args, PyObject *kwds) { + static char *kwlist[] = {"points", "k", "r", "metric", "directed", NULL}; + PyObject *points_o = Py_None, *metric_o = Py_None, *directed_o = Py_False; + double r = -1; + Py_ssize_t k = 1; + igraph_matrix_t points; + igraphmodule_GraphObject *self; + igraph_t graph; + igraph_metric_t metric = IGRAPH_METRIC_EUCLIDEAN; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OiOO", kwlist, &start_o, - &steps, &mode_o, &stuck_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ndOO", kwlist, + &points_o, &k, &r, &metric_o, &directed_o)) { return NULL; + } - if (igraphmodule_PyObject_to_vid(start_o, &start, &self->g)) - return NULL; - - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) - return NULL; - - if (igraphmodule_PyObject_to_random_walk_stuck_t(stuck_o, &stuck)) - return NULL; + if (igraphmodule_PyObject_to_metric_t(metric_o, &metric)) { + return NULL; + } - if (igraph_vector_init(&walk, steps)) - return igraphmodule_handle_igraph_error(); + if (igraphmodule_PyObject_to_matrix_t(points_o, &points, "points")) { + return NULL; + } - if (igraph_random_walk(&self->g, &walk, start, mode, steps, stuck)) { - igraph_vector_destroy(&walk); - return igraphmodule_handle_igraph_error(); + if (igraph_nearest_neighbor_graph(&graph, &points, metric, k, r, PyObject_IsTrue(directed_o))) { + igraph_matrix_destroy(&points); + return igraphmodule_handle_igraph_error(); } - res = igraphmodule_vector_t_to_PyList(&walk, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&walk); + igraph_matrix_destroy(&points); - return res; + CREATE_GRAPH_FROM_TYPE(self, graph, type); + + return (PyObject *) self; } /********************************************************************** @@ -11591,33 +14230,26 @@ 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) + +PyObject *igraphmodule_Graph___invalidate_cache__(igraphmodule_GraphObject *self) { - return PyCObject_FromVoidPtr((void *)&self->g, 0); + igraph_invalidate_cache(&self->g); + Py_RETURN_NONE; } -#endif /** \ingroup python_interface_internal * \brief Returns the pointer of the encapsulated igraph graph as an ordinary * Python integer. This allows us to use igraph graphs with the Python ctypes * module without any additional conversions. */ -PyObject *igraphmodule_Graph__raw_pointer(igraphmodule_GraphObject *self) { - return PyInt_FromLong((long int)&self->g); +PyObject *igraphmodule_Graph__raw_pointer(igraphmodule_GraphObject *self, PyObject* Py_UNUSED(_null)) { + return PyLong_FromVoidPtr(&self->g); } /** \ingroup python_interface_internal @@ -11630,7 +14262,7 @@ PyObject *igraphmodule_Graph___register_destructor__(igraphmodule_GraphObject PyObject * kwds) { char *kwlist[] = { "destructor", NULL }; - PyObject *destructor = NULL, *result; + PyObject *destructor = NULL, *result_o; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &destructor)) return NULL; @@ -11640,14 +14272,14 @@ PyObject *igraphmodule_Graph___register_destructor__(igraphmodule_GraphObject return NULL; } - result = self->destructor; + result_o = self->destructor; self->destructor = destructor; Py_INCREF(self->destructor); - if (!result) + if (!result_o) Py_RETURN_NONE; - return result; + return result_o; } /** \ingroup python_interface @@ -11666,61 +14298,96 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_vcount {"vcount", (PyCFunction) igraphmodule_Graph_vcount, METH_NOARGS, - "vcount()\n\n" - "Counts the number of vertices.\n" - "@return: the number of vertices in the graph.\n" "@rtype: integer"}, + "vcount()\n--\n\n" + "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" - "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" - "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" - "@rtype: boolean"}, + "ecount()\n--\n\n" + "Counts the number of edges.\n\n" + "@return: the number of edges in the graph.\n" + "@rtype: integer\n"}, // interface to igraph_is_directed {"is_directed", (PyCFunction) igraphmodule_Graph_is_directed, METH_NOARGS, - "is_directed()\n\n" - "Checks whether the graph is directed.\n" + "is_directed()\n--\n\n" + "Checks whether the graph is directed.\n\n" "@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" + "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"}, + /* interface to igraph_is_complete */ + {"is_complete", (PyCFunction) igraphmodule_Graph_is_complete, + METH_NOARGS, + "is_complete()\n--\n\n" + "Checks whether the graph is complete, i.e. whether there is at least one\n" + "connection between all distinct pairs of vertices. In directed graphs,\n" + "ordered pairs are considered.\n\n" + "@return: C{True} if it is complete, C{False} otherwise.\n" + "@rtype: boolean"}, + + {"is_clique", (PyCFunction) igraphmodule_Graph_is_clique, + METH_VARARGS | METH_KEYWORDS, + "is_clique(vertices=None, directed=False)\n--\n\n" + "Decides whether a set of vertices is a clique, i.e. a fully connected subgraph.\n\n" + "@param vertices: a list of vertex IDs.\n" + "@param directed: whether to require mutual connections between vertex pairs\n" + " in directed graphs.\n" + "@return: C{True} is the given vertex set is a clique, C{False} if not.\n"}, + + {"is_independent_vertex_set", (PyCFunction) igraphmodule_Graph_is_independent_vertex_set, + METH_VARARGS | METH_KEYWORDS, + "is_independent_vertex_set(vertices=None)\n--\n\n" + "Decides whether no two vertices within a set are adjacent.\n\n" + "@param vertices: a list of vertex IDs.\n" + "@return: C{True} is the given vertices form an independent set, C{False} if not.\n"}, + + /* 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, - "add_vertices(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"}, /* interface to igraph_delete_vertices */ {"delete_vertices", (PyCFunction) igraphmodule_Graph_delete_vertices, METH_VARARGS, - "delete_vertices(vs)\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.\n"}, + " to be deleted. No argument deletes all vertices.\n"}, /* interface to igraph_add_edges */ {"add_edges", (PyCFunction) igraphmodule_Graph_add_edges, METH_VARARGS, - "add_edges(es)\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" @@ -11729,17 +14396,18 @@ 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)\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" "@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, 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" @@ -11747,14 +14415,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" + "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" @@ -11763,18 +14432,19 @@ 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" - " even an edge attribute name.\n" + " even an edge attribute name. ``None`` means to treat the graph as\n" + " unweighted, falling back to ordinary degree calculations.\n" }, /* 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)\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" @@ -11783,7 +14453,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)\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" @@ -11798,7 +14468,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()\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" @@ -11807,7 +14477,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, loops=True)\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" @@ -11821,12 +14491,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "of edges do not matter.\n\n" "@param edges: edge indices which we want to check. If C{None}, all\n" " edges are checked.\n" + "@param loops: specifies whether loop edges should be treated as mutual\n" + " in a directed graph.\n" "@return: a list of booleans, one for every edge given\n"}, /* 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)\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" @@ -11835,29 +14507,26 @@ 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\", loops=\"twice\", multiple=True)\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" - " graphs."}, - - {"successors", (PyCFunction) igraphmodule_Graph_successors, - METH_VARARGS | METH_KEYWORDS, - "successors(vertex)\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" - "Returns the predecessors of a given vertex.\n\n" - "Equivalent to calling the L{Graph.neighbors} method with type=L{IN}."}, + "@param mode: whether to return only successors (C{\"out\"}),\n" + " predecessors (C{\"in\"}) or both (C{\"all\"}). Ignored for undirected\n" + " graphs." + "@param loops: whether to return loops in I{undirected} graphs once\n" + " (C{\"once\"}), twice (C{\"twice\"}) or not at all (C{\"ignore\"}). C{False}\n" + " is accepted as an alias to C{\"ignore\"} and C{True} is accepted as an\n" + " alias to C{\"twice\"}. For directed graphs, C{\"twice\"} is equivalent\n" + " to C{\"once\"} (except when C{mode} is C{\"all\"} because the graph is\n" + " then treated as undirected).\n" + "@param multiple: whether to return endpoints of multiple edges as many\n" + " times as their multiplicities." + }, /* 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)\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" @@ -11872,23 +14541,14 @@ 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, 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" "The method does not consider multiple edges; if there are multiple\n" "edges between a pair of vertices, only the ID of one of the edges\n" "is returned.\n\n" "@param pairs: a list of integer pairs. Each integer pair is considered\n" " as a source-target vertex pair; the corresponding edge is looked up\n" " in the graph and the edge ID is returned for each pair.\n" - "@param path: a list of vertex IDs. The list is considered as a\n" - " continuous path from the first vertex to the last, passing\n" - " through the intermediate vertices. The corresponding edge IDs\n" - " between the first and the second, the second and the third and\n" - " so on are looked up in the graph and the edge IDs are returned.\n" - " If both C{path} and C{pairs} are given, the two lists are\n" - " concatenated.\n" "@param directed: whether edge directions should be considered in\n" " directed graphs. The default is C{True}. Ignored for undirected\n" " graphs.\n" @@ -11900,12 +14560,19 @@ 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" - " predecessors (L{IN}) or both (L{ALL}). Ignored for undirected\n" - " graphs."}, + "@param mode: whether to return only successors (C{\"out\"}),\n" + " predecessors (C{\"in\"}) or both (C{\"all\"}). Ignored for undirected\n" + " graphs." + "@param loops: whether to return loops in I{undirected} graphs once\n" + " (C{\"once\"}), twice (C{\"twice\"}) or not at all (C{\"ignore\"}). C{False}\n" + " is accepted as an alias to C{\"ignore\"} and C{True} is accepted as an\n" + " alias to C{\"twice\"}. For directed graphs, C{\"twice\"} is equivalent\n" + " to C{\"once\"} (except when C{mode} is C{\"all\"} because the graph is\n" + " then treated as undirected).\n" + }, ////////////////////// // GRAPH GENERATORS // @@ -11914,33 +14581,40 @@ 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=\"directed\", loops=\"once\")\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 specifies the number of edges between two vertices.\n" + " - C{\"undirected\"} - the graph will be undirected and a matrix\n" + " element specifies the number of edges between two vertices. The\n" + " input matrix must be symmetric.\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 loops: specifies how the diagonal of the matrix should be handled:\n" + "\n" + " - C{\"ignore\"} - ignore loop edges in the diagonal\n" + " - C{\"once\"} - treat the diagonal entries as loop edge counts\n" + " - C{\"twice\"} - treat the diagonal entries as I{twice} the number\n" + " of loop edges\n" }, /* interface to igraph_asymmetric_preference_game */ {"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" + "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" @@ -11959,8 +14633,10 @@ 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)\n--\n\n" "Generates a graph from the Graph Atlas.\n\n" + "B{Reference}: Ronald C. Read and Robin J. Wilson: I{An Atlas of Graphs}.\n" + "Oxford University Press, 1998.\n\n" "@param idx: The index of the graph to be generated.\n" " Indices start from zero, graphs are listed:\n\n" " 1. in increasing order of number of vertices;\n" @@ -11969,16 +14645,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " 3. for fixed numbers of vertices and edges, in increasing order\n" " of the degree sequence, for example 111223 < 112222;\n" " 4. for fixed degree sequence, in increasing number of automorphisms.\n\n" - "@newfield ref: Reference\n" - "@ref: I{An Atlas of Graphs} by Ronald C. Read and Robin J. Wilson,\n" - " Oxford University Press, 1998."}, + }, // interface to igraph_barabasi_game {"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" - "Generates a graph based on the Barabasi-Albert model.\n\n" + " zero_appeal=1, implementation=\"psumtree\", start_from=None)\n--\n\n" + "Generates a graph based on the Barabási-Albert model.\n\n" + "B{Reference}: Barabási, A-L and Albert, R. 1999. Emergence of scaling\n" + "in random networks. I{Science}, 286 509-512.\n\n" "@param n: the number of vertices\n" "@param m: either the number of outgoing edges generated for\n" " each vertex or a list containing the number of outgoing\n" @@ -12010,23 +14686,21 @@ 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" - " in random networks. Science, 286 509-512."}, + }, /* 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" @@ -12034,7 +14708,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "from vertex M{v} to vertex M{w} if the string of M{v} can be transformed into\n" "the string of M{w} by removing its first letter and appending a letter to it.\n" "\n" - "Please note that the graph will have M{m^n} vertices and even more edges,\n" + "Please note that the graph will have M{m^n} vertices and even more edges,\n" "so probably you don't want to supply too big numbers for M{m} and M{n}.\n\n" "@param m: the size of the alphabet\n" "@param n: the length of the strings\n" @@ -12043,7 +14717,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" @@ -12059,33 +14733,37 @@ 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" - "Generates a graph based on the Erdos-Renyi model.\n\n" + "Erdos_Renyi(n, p, m, directed=False, loops=False, edge_labeled=False)\n--\n\n" + "Generates a graph based on the Erdős-Rényi model.\n\n" "@param n: the number of vertices.\n" "@param p: the probability of edges. If given, C{m} must be missing.\n" "@param m: the number of edges. If given, C{p} must be missing.\n" "@param directed: whether to generate a directed graph.\n" - "@param loops: whether self-loops are allowed.\n"}, + "@param loops: whether self-loops are allowed.\n" + "@param edge_labeled: whether to sample uniformly from the set of\n" + " I{ordered} edge lists. Use C{False} to recover the classic\n" + " Erdős-Rényi model.\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\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{https://igraph.org/c/doc}.\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, 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 growin graph model. In every time step, a new\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" "more than one if M{ambs>1}) and starts a simulated forest fire at its\n" "ambassador(s). The fire spreads through the edges. The spreading probability\n" @@ -12103,7 +14781,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" @@ -12114,7 +14792,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" @@ -12123,21 +14801,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" @@ -12145,17 +14823,39 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param citation: whether the new edges should originate from the most\n" " recently added vertex.\n"}, - /* interface to igraph_incidence */ - {"_Incidence", (PyCFunction) igraphmodule_Graph_Incidence, + /* interface to igraph_hexagonal_lattice */ + {"Hexagonal_Lattice", (PyCFunction) igraphmodule_Graph_Hexagonal_Lattice, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Hexagonal_Lattice(dim, directed=False, mutual=True)\n--\n\n" + "Generates a regular hexagonal lattice.\n\n" + "@param dim: list with the dimensions of the lattice\n" + "@param directed: whether to create a directed graph.\n" + "@param mutual: whether to create all connections as mutual\n" + " in case of a directed graph.\n"}, + + /* interface to igraph_hypercube */ + {"Hypercube", (PyCFunction) igraphmodule_Graph_Hypercube, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "_Incidence(matrix, directed=False, mode=ALL, multiple=False)\n\n" + "Hypercube(n, directed=False)\n--\n\n" + "Generates an n-dimensional hypercube graph.\n\n" + "The hypercube graph M{Q_n} has M{2^n} vertices and M{2^{n-1} n} edges.\n" + "Two vertices are connected when the binary representations of their vertex\n" + "IDs differ in precisely one bit.\n" + "@param n: the dimension of the hypercube graph\n" + "@param directed: whether to create a directed graph; edges will point\n" + " from lower index vertices towards higher index ones."}, + + /* interface to igraph_biadjacency */ + {"_Biadjacency", (PyCFunction) igraphmodule_Graph_Biadjacency, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "_Biadjacency(matrix, directed=False, mode=\"all\", multiple=False)\n--\n\n" "Internal function, undocumented.\n\n" - "@see: Graph.Incidence()\n\n"}, + "@see: Graph.Biadjacency()\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" @@ -12171,7 +14871,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" @@ -12186,9 +14886,9 @@ 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" + "This is practically the non-growing 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" @@ -12202,17 +14902,27 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param directed: whether to generate a directed graph.\n" "@param loops: whether loop edges are allowed.\n"}, + /* interface to igraph_from_prufer */ + {"Prufer", (PyCFunction) igraphmodule_Graph_Prufer, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Prufer(seq)\n--\n\n" + "Generates a tree from its Prüfer sequence.\n\n" + "A Prüfer sequence is a unique sequence of integers associated with a\n" + "labelled tree. A tree on M{n} vertices can be represented by a sequence\n" + "of M{n-2} integers, each between M{0} and M{n-1} (inclusive).\n\n" + "@param seq: the Prüfer sequence as an iterable of integers\n"}, + /* 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\", allowed_edge_types=\"simple\", edge_labeled=False)\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" @@ -12233,65 +14943,157 @@ 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" - "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" + "SBM(pref_matrix, block_sizes, directed=False, allowed_edge_types=\"simple\")\n--\n\n" + "Generates a graph based on a stochastic block model.\n\n" + "Every vertex is assigned to a vertex type according to the given block\n" + "sizes, which also determine the total vertex count. Vertices of the same\n" "type will be assigned consecutive vertex IDs. Finally, every\n" "vertex pair is evaluated and an edge is created between them with a\n" "probability depending on the types of the vertices involved. The\n" "probabilities are taken from the preference matrix.\n\n" - "@param n: the number of vertices in the graph\n" - "@param pref_matrix: matrix giving the connection probabilities for\n" - " different vertex types.\n" + "@param pref_matrix: matrix giving the connection probabilities (or expected\n" + " edge multiplicities for multigraphs) between different vertex types.\n" "@param block_sizes: list giving the number of vertices in each block; must\n" " sum up to I{n}.\n" "@param directed: whether to generate a directed graph.\n" - "@param loops: whether loop edges are allowed.\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"}, // 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" " either \"in\", \"out\", \"mutual\" or \"undirected\"\n" "@param center: Vertex ID for the central vertex in the star.\n"}, - // interface to igraph_lattice + // interface to igraph_square_lattice {"Lattice", (PyCFunction) igraphmodule_Graph_Lattice, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Lattice(dim, nei=1, directed=False, mutual=True, circular=True)\n\n" - "Generates a regular lattice.\n\n" + "Lattice(dim, nei=1, directed=False, mutual=True, circular=True)\n--\n\n" + "Generates a regular square 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" " two vertices will be connected.\n" "@param directed: whether to create a directed graph.\n" "@param mutual: whether to create all connections as mutual\n" " in case of a directed graph.\n" - "@param circular: whether the generated lattice is periodic.\n"}, + "@param circular: whether the generated lattice is periodic. May also be an\n" + " iterable; in this case, the iterator is assumed to yield booleans that\n" + " specify whether the lattice is periodic along each dimension.\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{https://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" + }, + + {"Realize_Degree_Sequence", (PyCFunction) igraphmodule_Graph_Realize_Degree_Sequence, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "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" + "B{References}\n\n" + " - V. Havel, Poznámka o existenci konečných grafů (A remark on the\n" + " existence of finite graphs), Časopis pro pěstování matematiky 80,\n" + " 477-480 (1955). U{http://eudml.org/doc/19050}\n" + " - S. L. Hakimi, On Realizability of a Set of Integers as Degrees of the\n" + " Vertices of a Linear Graph, I{Journal of the SIAM} 10, 3 (1962).\n" + " U{https://www.jstor.org/stable/2098770}\n" + " - D. J. Kleitman and D. L. Wang, Algorithms for Constructing Graphs and\n" + " Digraphs with Given Valences and Factors, I{Discrete Mathematics} 6, 1 (1973).\n" + " U{https://doi.org/10.1016/0012-365X%2873%2990037-X}\n" + " - Sz. Horvát and C. D. Modes, Connectedness matters: construction and\n" + " exact random sampling of connected networks (2021).\n" + " U{https://doi.org/10.1088/2632-072X/abced5}\n" + "\n" + "@param out: the degree sequence of an undirected graph (if in_=None),\n" + " or the out-degree sequence of a directed graph.\n" + "@param in_: 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" + "\n" + " In the undirected case, C{smallest} is guaranteed to produce a connected graph.\n" + " See Horvát and Modes (2021) for details.\n" + }, + + {"Realize_Bipartite_Degree_Sequence", (PyCFunction) igraphmodule_Graph_Realize_Bipartite_Degree_Sequence, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Realize_Bipartite_Degree_Sequence(degrees1, degrees2, allowed_edge_types=\"simple\", method=\"smallest\")\n--\n\n" + "Generates a bipartite graph from the degree sequences of its partitions.\n" + "\n" + "This method implements a Havel-Hakimi style graph construction for biparite\n" + "graphs. In each step, the algorithm picks two vertices in a deterministic\n" + "manner and connects them. The way the vertices are picked is defined by the\n" + "C{method} parameter. The allowed edge types (i.e. whether multi-edges are allowed)\n" + "are specified in the C{allowed_edge_types} parameter. Self-loops are never created,\n" + "since a graph with self-loops is not bipartite.\n" + "\n" + "@param degrees1: the degrees of the first partition.\n" + "@param degrees2: the degrees of the second partition.\n" + "@param allowed_edge_types: controls whether multi-edges are allowed\n" + " during the generation process. Possible values are:\n" + "\n" + " - C{\"simple\"}: simple graphs (no multi-edges)\n" + " - C{\"multi\"}: multi-edges 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" + "\n" + " The smallest C{smallest} method is guaranteed to produce a connected graph,\n" + " if one exists." + }, + // 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" @@ -12301,7 +15103,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, allowed_edge_types=\"simple\")\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" @@ -12316,8 +15118,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param fitness_in: a numeric vector with non-negative entries, one for each\n" " vertex. These values represent the in-fitness scores for directed graphs.\n" " For undirected graphs, this argument must be C{None}.\n" - "@param loops: whether loop edges are allowed.\n" - "@param multiple: whether multiple edges are allowed.\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" "@return: a directed or undirected graph with the prescribed power-law\n" " degree distributions.\n" }, @@ -12325,9 +15135,15 @@ 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, allowed_edge_types=\"simple\", " + "finite_size_correction=True)\n--\n\n" "Generates a non-growing graph with prescribed power-law degree distributions.\n\n" + "B{References}\n\n" + " - Goh K-I, Kahng B, Kim D: Universal behaviour of load distribution\n" + " in scale-free networks. I{Phys Rev Lett} 87(27):278701, 2001.\n" + " - Cho YS, Kim JS, Park J, Kahng B, Kim D: Percolation transitions in\n" + " scale-free networks under the Achlioptas process. I{Phys Rev Lett}\n" + " 103:135702, 2009.\n\n" "@param n: the number of vertices in the graph\n" "@param m: the number of edges in the graph\n" "@param exponent_out: the exponent of the out-degree distribution, which\n" @@ -12338,128 +15154,270 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param exponent_in: the exponent of the in-degree distribution, which\n" " must be between 2 and infinity (inclusive) It can also be negative, in\n" " which case an undirected graph will be generated.\n" - "@param loops: whether loop edges are allowed.\n" - "@param multiple: whether multiple edges are allowed.\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 finite_size_correction: whether to apply a finite-size correction\n" " to the generated fitness values for exponents less than 3. See the\n" " paper of Cho et al for more details.\n" "@return: a directed or undirected graph with the prescribed power-law\n" " degree distributions.\n" - "\n" - "@newfield ref: Reference\n" - "@ref: Goh K-I, Kahng B, Kim D: Universal behaviour of load distribution\n" - " in scale-free networks. Phys Rev Lett 87(27):278701, 2001.\n" - "@ref: Cho YS, Kim JS, Park J, Kahng B, Kim D: Percolation transitions in\n" - " scale-free networks under the Achlioptas process. Phys Rev Lett\n" - " 103:135702, 2009.\n" }, // 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, mode=\"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" + "@param mode: 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_chung_lu_game */ + {"Chung_Lu", (PyCFunction) igraphmodule_Graph_Chung_Lu, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Chung_Lu(out, in_=None, loops=True, variant=\"original\")\n--\n\n" + "Generates a Chung-Lu random graph.\n\n" + "In the original Chung-Lu model, each pair of vertices M{i} and M{j} is connected\n" + "with independent probability M{p_{ij} = w_i w_j / S}, where M{w_i} is a weight\n" + "associated with vertex M{i} and M{S = \\sum_k w_k} is the sum of weights.\n" + "In the directed variant, vertices have both out-weights, M{w^\\text{out}},\n" + "and in-weights, M{w^\\text{in}}, with equal sums,\n" + "M{S = \\sum_k w^\\text{out}_k = \\sum_k w^\\text{in}_k}. The connection\n" + "probability between M{i} and M{j} is M{p_{ij} = w^\\text{out}_i w^\\text{in}_j / S}.\n\n" + "This model is commonly used to create random graphs with a fixed I{expected}\n" + "degree sequence. The expected degree of vertex M{i} is approximately equal\n" + "to the weight M{w_i}. Specifically, if the graph is directed and self-loops\n" + "are allowed, then the expected out- and in-degrees are precisely M{w^\\text{out}}\n" + "and M{w^\\text{in}}. If self-loops are disallowed, then the expected out-\n" + "and in-degrees are M{w^\\text{out} (S - w^\\text{in}) / S} and\n" + "M{w^\\text{in} (S - w^\\text{out}) / S}, respectively. If the graph is\n" + "undirected, then the expected degrees with and without self-loops are\n" + "M{w (S + w) / S} and M{w (S - w) / S}, respectively.\n\n" + "A limitation of the original Chung-Lu model is that when some of the\n" + "weights are large, the formula for M{p_{ij}} yields values larger than 1.\n" + "Chung and Lu's original paper excludes the use of such weights. When\n" + "M{p_{ij} > 1}, this function simply issues a warning and creates\n" + "a connection between M{i} and M{j}. However, in this case the expected degrees\n" + "will no longer relate to the weights in the manner stated above. Thus the\n" + "original Chung-Lu model cannot produce certain (large) expected degrees.\n\n" + "The overcome this limitation, this function implements additional variants of\n" + "the model, with modified expressions for the connection probability M{p_{ij}}\n" + "between vertices M{i} and M{j}. Let M{q_{ij} = w_i w_j / S}, or\n" + "M{q_{ij} = w^out_i w^in_j / S} in the directed case. All model\n" + "variants become equivalent in the limit of sparse graphs where M{q_{ij}}\n" + "approaches zero. In the original Chung-Lu model, selectable by setting\n" + "C{variant} to C{\"original\"}, M{p_{ij} = min(q_{ij}, 1)}.\n" + "The C{\"maxent\"} variant, sometimes referred to as the generalized\n" + "random graph, uses M{p_{ij} = q_{ij} / (1 + q_{ij})}, and is equivalent\n" + "to a maximum entropy model (i.e. exponential random graph model) with\n" + "a constraint on expected degrees, see Park and Newman (2004), Section B,\n" + "setting M{exp(-\\Theta_{ij}) = w_i w_j / S}. This model is also\n" + "discussed by Britton, Deijfen and Martin-Löf (2006). By virtue of being\n" + "a degree-constrained maximum entropy model, it generates graphs having\n" + "the same degree sequence with the same probability.\n" + "A third variant can be requested with C{\"nr\"}, and uses\n" + "M{p_{ij} = 1 - exp(-q_{ij})}. This is the underlying simple graph\n" + "of a multigraph model introduced by Norros and Reittu (2006).\n" + "For a discussion of these three model variants, see Section 16.4 of\n" + "Bollobás, Janson, Riordan (2007), as well as Van Der Hofstad (2013).\n\n" + "B{References:}\n\n" + " - Chung F and Lu L: Connected components in a random graph with given degree sequences.\n" + " I{Annals of Combinatorics} 6, 125-145 (2002) U{https://doi.org/10.1007/PL00012580}\n" + " - Miller JC and Hagberg A: Efficient Generation of Networks with Given Expected Degrees (2011)\n" + " U{https://doi.org/10.1007/978-3-642-21286-4_10}\n" + " - Park J and Newman MEJ: Statistical mechanics of networks.\n" + " I{Physical Review E} 70, 066117 (2004). U{https://doi.org/10.1103/PhysRevE.70.066117}\n" + " - Britton T, Deijfen M, Martin-Löf A: Generating Simple Random Graphs with Prescribed Degree Distribution.\n" + " I{J Stat Phys} 124, 1377–1397 (2006). U{https://doi.org/10.1007/s10955-006-9168-x}\n" + " - Norros I and Reittu H: On a conditionally Poissonian graph process.\n" + " I{Advances in Applied Probability} 38, 59–75 (2006).\n" + " U{https://doi.org/10.1239/aap/1143936140}\n" + " - Bollobás B, Janson S, Riordan O: The phase transition in inhomogeneous random graphs.\n" + " I{Random Struct Algorithms} 31, 3–122 (2007). U{https://doi.org/10.1002/rsa.20168}\n" + " - Van Der Hofstad R: Critical behavior in inhomogeneous random graphs.\n" + " I{Random Struct Algorithms} 42, 480–508 (2013). U{https://doi.org/10.1002/rsa.20450}\n\n" + "@param out: the vertex weights (or out-weights). In sparse graphs\n" + " these will be approximately equal to the expected (out-)degrees.\n" + "@param in_: the vertex in-weights, approximately equal to the expected\n" + " in-degrees of the graph. If omitted, the generated graph will be\n" + " undirected.\n" + "@param loops: whether to allow the generation of self-loops.\n" + "@param variant: the model variant to be used. Let M{q_{ij}=w_i w_j / S},\n" + " where M{S = \\sum_k w_k}. The following variants are available:\n" + " \n" + " - C{\"original\"} -- the original Chung-Lu model with\n" + " M{p_{ij} = min(1, q_{ij})}.\n" + " - C{\"maxent\"} -- maximum entropy model with fixed expected degrees\n" + " M{p_{ij} = q_{ij} / (1 + q_{ij})}\n" + " - C{\"nr\"} -- Norros and Reittu's model, M{p_{ij} = 1 - exp(-q_{ij})}\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" + "Degree_Sequence(out, in_=None, method=\"configuration\")\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" - " - C{\"simple\"} -- simple generator that sometimes generates\n" - " loop edges and multiple edges. The generated graph is not\n" - " guaranteed to be connected.\n" - " - C{\"no_multiple\"} -- similar to C{\"simple\"} but avoids the\n" - " generation of multiple and loop edges at the expense of increased\n" + " - C{\"configuration\"} -- simple generator that implements the stub-matching\n" + " configuration model. It may generate self-loops and multiple edges.\n" + " This method does not sample multigraphs uniformly, but it can be\n" + " used to implement uniform sampling for simple graphs by rejecting\n" + " any result that is non-simple (i.e. contains loops or multi-edges).\n" + " - C{\"fast_heur_simple\"} -- similar to C{\"configuration\"} but avoids\n" + " the generation of multiple and loop edges at the expense of increased\n" " time complexity. The method will re-start the generation every time\n" " it gets stuck in a configuration where it is not possible to insert\n" " any more edges without creating loops or multiple edges, and there\n" " is no upper bound on the number of iterations, but it will succeed\n" " eventually if the input degree sequence is graphical and throw an\n" " exception if the input degree sequence is not graphical.\n" + " This method does not sample simple graphs uniformly.\n" + " - C{\"configuration_simple\"} -- similar to C{\"configuration\"} but\n" + " rejects generated graphs if they are not simple. This method samples\n" + " simple graphs uniformly.\n" + " - C{\"edge_switching_simple\"} -- an MCMC sampler based on degree-preserving\n" + " edge switches. It generates simple undirected or directed graphs. The\n" + " algorithm uses L{Graph.Realize_Degree_Sequence()} to construct an\n" + " initial graph, then rewires it using L{Graph.rewire()}.\n" " - C{\"vl\"} -- a more sophisticated generator that can sample\n" - " undirected, connected simple graphs uniformly. It uses\n" - " Monte-Carlo methods to randomize the graphs.\n" + " undirected, connected simple graphs approximately uniformly. It uses\n" + " edge-switching Monte-Carlo methods to randomize the graphs.\n" " This generator should be favoured if undirected and connected\n" " 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 */ {"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" - "@param n: the number of vertices in the graph (3 or 4)\n" - "@param class: the isomorphy class\n" + "Isoclass(n, cls, directed=False)\n--\n\n" + "Generates a graph with a given isomorphism class.\n\n" + "Currently we support directed graphs of size 3 and 4, and undirected graphs\n" + "of size 3, 4, 5 or 6. Use the L{isoclass()} instance method to find the\n" + "isomorphism class of a given graph.\n\n" + "@param n: the number of vertices in the graph\n" + "@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=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: the generation method to be used. One of the following:\n" + " \n" + " - C{\"prufer\"} -- samples Prüfer 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_triangular_lattice */ + {"Triangular_Lattice", (PyCFunction) igraphmodule_Graph_Triangular_Lattice, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Triangular_Lattice(dim, directed=False, mutual=True)\n--\n\n" + "Generates a regular triangular lattice.\n\n" + "@param dim: list with the dimensions of the lattice\n" + "@param directed: whether to create a directed graph.\n" + "@param mutual: whether to create all connections as mutual\n" + " in case of a directed graph.\n"}, + /* 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, allowed_edge_types=\"simple\")\n--\n\n" + "This function generates networks with the small-world property based on a\n" + "variant of the Watts-Strogatz model. The network is obtained by first creating\n" + "a periodic undirected lattice, then rewiring both endpoints of each edge with\n" + "probability I{p}, while avoiding the creation of multi-edges.\n\n" + "This process differs from the original model of Watts and Strogatz (see\n" + "reference) in that it rewires I{both} endpoints of edges. Thus in the limit\n" + "of C{p=1}, we obtain a G(n,m) random graph with the same number of vertices\n" + "and edges as the original lattice. In comparison, the original Watts-Strogatz\n" + "model only rewires a single endpoint of each edge, thus the network does not\n" + "become fully random even for p=1.\n\n" + "For appropriate choices of I{p}, both models exhibit the property of\n" + "simultaneously having short path lengths and high clustering.\n\n" + "B{Reference}: Duncan J Watts and Steven H Strogatz: Collective dynamics of\n" + "small world networks, I{Nature} 393, 440-442, 1998\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" " two vertices will be connected.\n" "@param p: rewiring probability\n\n" - "@param loops: specifies whether loop edges are allowed\n" - "@param multiple: specifies whether multiple edges are allowed\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" "@see: L{Lattice()}, L{rewire()}, L{rewire_edges()} if more flexibility is\n" " needed\n" - "@newfield ref: Reference\n" - "@ref: Duncan J Watts and Steven H Strogatz: I{Collective dynamics of\n" - " small world networks}, Nature 393, 440-442, 1998\n"}, + }, /* interface to igraph_weighted_adjacency */ - {"Weighted_Adjacency", (PyCFunction) igraphmodule_Graph_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=\"directed\", loops=\"once\")\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" - " of the adjacency matrix will be ignored.\n" + "@param loops: specifies how to handle loop edges. When C{False} or C{\"ignore\"},\n" + " the diagonal of the adjacency matrix will be ignored. When C{True} or\n" + " C{\"once\"}, the diagonal is assumed to contain the weight of the\n" + " corresponding loop edge. When C{\"twice\"}, the diagonal is assumed to\n" + " contain I{twice} the weight of the corresponding loop edge.\n" + "@return: a pair consisting of the graph itself and its edge weight vector\n" }, ///////////////////////////////////// // STRUCTURAL PROPERTIES OF GRAPHS // ///////////////////////////////////// - // interface to igraph_are_connected - {"are_connected", (PyCFunction) igraphmodule_Graph_are_connected, + // interface to igraph_are_adjacent + {"are_adjacent", (PyCFunction) igraphmodule_Graph_are_adjacent, METH_VARARGS | METH_KEYWORDS, - "are_connected(v1, v2)\n\n" + "are_adjacent(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" @@ -12469,7 +15427,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" @@ -12478,37 +15436,41 @@ 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, normalized=True, weights=None)\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" "connectivity patterns of the vertices and the pattern expected from the\n" - "disribution of the vertex types.\n\n" + "distribution of the vertex types.\n\n" "See equation (21) in Newman MEJ: Mixing patterns in networks, Phys Rev E\n" "67:026126 (2003) for the proper definition. The actual calculation is\n" "performed using equation (26) in the same paper for directed graphs, and\n" "equation (4) in Newman MEJ: Assortative mixing in networks, Phys Rev Lett\n" "89:208701 (2002) for undirected graphs.\n\n" + "B{References}\n\n" + " - Newman MEJ: Mixing patterns in networks, I{Phys Rev E} 67:026126, 2003.\n" + " - Newman MEJ: Assortative mixing in networks, I{Phys Rev Lett} 89:208701, 2002.\n\n" "@param types1: vertex types in a list or the name of a vertex attribute\n" " holding vertex types. Types are ideally denoted by numeric values.\n" "@param types2: in directed assortativity calculations, each vertex can\n" " have an out-type and an in-type. In this case, I{types1} contains the\n" " out-types and this parameter contains the in-types in a list or the\n" " name of a vertex attribute. If C{None}, it is assumed to be equal\n" - " to I{types1}.\n\n" + " to I{types1}.\n" "@param directed: whether to consider edge directions or not.\n" + "@param normalized: whether to compute the normalized covariance, i.e.\n" + " Pearson correlation. Supply True here to compute the standard\n" + " assortativity.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the assortativity coefficient\n\n" - "@newfield ref: Reference\n" - "@ref: Newman MEJ: Mixing patterns in networks, Phys Rev E 67:026126, 2003.\n" - "@ref: Newman MEJ: Assortative mixing in networks, Phys Rev Lett 89:208701,\n" - " 2002.\n" "@see: L{assortativity_degree()} when the types are the vertex degrees\n" }, /* 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" @@ -12521,7 +15483,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, normalized=True, weights=None)\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" @@ -12532,19 +15494,24 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "asymptotically zero.\n\n" "See equation (2) in Newman MEJ: Mixing patterns in networks, Phys Rev E\n" "67:026126 (2003) for the proper definition.\n\n" + "B{Reference}: Newman MEJ: Mixing patterns in networks, I{Phys Rev E}\n" + "67:026126, 2003.\n\n" "@param types: vertex types in a list or the name of a vertex attribute\n" " holding vertex types. Types should be denoted by numeric values.\n" "@param directed: whether to consider edge directions or not.\n" + "@param normalized: whether to compute the (usual) normalized assortativity.\n" + " The unnormalized version is identical to modularity. Supply True here to\n" + " compute the standard assortativity.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the assortativity coefficient\n\n" - "@newfield ref: Reference\n" - "@ref: Newman MEJ: Mixing patterns in networks, Phys Rev E 67:026126, 2003.\n" }, /* interface to igraph_average_path_length */ {"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, weights=None)\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" @@ -12552,17 +15519,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " the average of the geodesic lengths in the components is\n" " calculated. Otherwise for all unconnected vertex pairs,\n" " a path length equal to the number of vertices is used.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the average path length in the graph\n"}, /* 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" - "@param scale: whether to normalize the scores so that the largest one\n" - " is 1.\n" "@param arpack_options: an L{ARPACKOptions} object used to fine-tune\n" " the ARPACK eigenvector calculation. If omitted, the module-level\n" " variable called C{arpack_options} is used.\n" @@ -12572,11 +15539,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@see: hub_score()\n" }, - /* interface to igraph_betweenness[_estimate] */ + /* interface to igraph_betweenness, igraph_betweenness_cutoff and igraph_betweenness_subset */ {"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, sources=None, targets=None)\n--\n\n" "Calculates or estimates the betweenness of vertices in a graph.\n\n" + "Also supports calculating betweenness with shortest path length cutoffs or\n" + "considering shortest paths only from certain source vertices or to certain\n" + "target vertices.\n\n" "Keyword arguments:\n" "@param vertices: the vertices for which the betweennesses must be returned.\n" " If C{None}, assumes all of the vertices in the graph.\n" @@ -12587,19 +15557,19 @@ 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"}, + "@param sources: the set of source vertices to consider when calculating\n" + " shortest paths.\n" + "@param targets: the set of target vertices to consider when calculating\n" + " shortest paths.\n" + "@return: the (possibly cutoff-limited) betweenness of the given vertices in a list\n"}, /* 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" "@param return_articulation_points: whether to return the articulation\n" " points as well\n" "@return: a list of lists containing edge indices making up spanning trees\n" @@ -12610,27 +15580,63 @@ 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\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_is_chordal with alternative arguments */ + {"chordal_completion", (PyCFunction)igraphmodule_Graph_chordal_completion, + METH_VARARGS | METH_KEYWORDS, + "chordal_completion(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, - "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" + "The closeness centrality 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" @@ -12638,9 +15644,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" @@ -12654,26 +15660,60 @@ 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_clusters */ - {"clusters", (PyCFunction) igraphmodule_Graph_clusters, + /* 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, " + "normalized=True)\n--\n\n" + "Calculates the harmonic centralities of given vertices in a graph.\n\n" + "The harmonic centrality 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" + "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 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 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" + "@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" + " result is the sum of inverse path lengths to other vertices.\n" + "@return: the calculated harmonic centralities in a list\n"}, + + /* interface to igraph_connected_components */ + {"connected_components", (PyCFunction) igraphmodule_Graph_connected_components, METH_VARARGS | METH_KEYWORDS, - "clusters(mode=STRONG)\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" + "connected_components(mode=\"strong\")\n--\n\n" + "Calculates the (strong or weak) connected components 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" "@return: the component index for every node in the graph.\n"}, {"copy", (PyCFunction) igraphmodule_Graph_copy, METH_NOARGS, - "copy()\n\n" "Creates an exact deep copy of the graph."}, + "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" + "be shared between the old and new graph. You can use `deepcopy()`\n" + "from the `copy` module if you need a truly deep copy of the graph.\n" + }, {"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" + "@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" @@ -12684,7 +15724,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" @@ -12702,14 +15742,14 @@ 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, 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" @@ -12729,18 +15769,28 @@ 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, weights=None)\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" " 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."}, + "@param weights: weights associated to the edges. Can be an attribute name\n" + " as well. If C{None}, every edge will have the same weight.\n" + "@return: the (weighted or unweighted) density of the graph."}, + + /* interface to igraph_mean_degree */ + {"mean_degree", (PyCFunction) igraphmodule_Graph_mean_degree, + METH_VARARGS | METH_KEYWORDS, + "mean_degree(loops=True)\n--\n\n" + "Calculates the mean degree of the graph.\n\n" + "@param loops: whether to consider self-loops during the calculation\n" + "@return: the mean degree of the graph."}, /* 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" @@ -12753,7 +15803,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" @@ -12768,7 +15818,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" @@ -12788,45 +15838,50 @@ 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" "The measure is defined for undirected graphs only; edge directions are\n" "ignored.\n\n" + "B{Reference}: Eagle N, Macy M and Claxton R: Network diversity and economic\n" + "development, I{Science} 328, 1029-1031, 2010.\n\n" "@param vertices: the vertices for which the diversity indices must\n" " be returned. If C{None}, uses all of the vertices in the graph.\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" "@return: the calculated diversity indices in a list, or a single number if\n" " a single vertex was supplied.\n" - "@newfield ref: Reference\n" - "@ref: Eagle N, Macy M and Claxton R: Network diversity and economic\n" - " development, Science 328, 1029--1031, 2010." }, /* 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\", weights=None)\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" + "@param weights: a list containing the edge weights. It can also be\n" + " an attribute name (edge weights are retrieved from the given\n" + " attribute) or C{None} (all edges have equal weight).\n" "@return: the calculated eccentricities in a list, or a single number if\n" " a single vertex was supplied.\n"}, - /* interface to igraph_edge_betweenness[_estimate] */ + /* interface to igraph_edge_betweenness, igraph_edge_betweenness_cutoff and igraph_edge_betweenness_subset */ {"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, sources=None, targets=None)\n--\n\n" "Calculates or estimates the edge betweennesses in a graph.\n\n" + "Also supports calculating edge betweenness with shortest path length cutoffs or\n" + "considering shortest paths only from certain source vertices or to certain\n" + "target vertices.\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" " length are considered, effectively resulting in an estimation of the\n" @@ -12834,17 +15889,21 @@ 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 sources: the set of source vertices to consider when calculating\n" + " shortest paths.\n" + "@param targets: the set of target vertices to consider when calculating\n" + " shortest paths.\n" "@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, + "eigen_adjacency(algorithm=None, which=None, arpack_options=None)\n--\n\n" }, /* 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" @@ -12870,12 +15929,26 @@ 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" + "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" - " one will always be 1.\n" "@param weights: edge weights given as a list or an edge attribute. If\n" " C{None}, all edges have equal weight.\n" "@param return_eigenvalue: whether to return the actual largest\n" @@ -12890,7 +15963,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" @@ -12899,6 +15972,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "set. Note that the task is trivial for an undirected graph as it is enough\n" "to find a spanning tree and then remove all the edges not in the spanning\n" "tree. Of course it is more complicated for directed graphs.\n\n" + "B{Reference}: Eades P, Lin X and Smyth WF: A fast and effective heuristic for the\n" + "feedback arc set problem. In: I{Proc Inf Process Lett} 319-323, 1993.\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name. When given, the algorithm will strive to\n" " remove lightweight edges in order to minimize the total weight of the\n" @@ -12907,18 +15982,63 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " breaking heuristic of Eades, Lin and Smyth, which is linear in the number\n" " of edges but not necessarily optimal; however, it guarantees that the\n" " number of edges to be removed is smaller than |E|/2 - |V|/6. C{\"ip\"} uses\n" - " an integer programming formulation which is guaranteed to yield an optimal\n" - " result, but is too slow for large graphs.\n" + " the most efficient available integer programming formulation which is guaranteed\n" + " to yield an optimal result. Specific integer programming formulations can be\n" + " selected using C{\"ip_ti\"} (using triangle inequalities) and C{\"ip_cg\"}\n" + " (a minimum set cover formulation using incremental constraint generation).\n" + " Note that the minimum feedback arc set problem is NP-hard, therefore all methods\n" + " that obtain exact optimal solutions are infeasibly slow on large graphs.\n" "@return: the IDs of the edges to be removed, in a list.\n\n" - "@newfield ref: Reference\n" - "@ref: Eades P, Lin X and Smyth WF: A fast and effective heuristic for the\n" - " feedback arc set problem. In: Proc Inf Process Lett 319-323, 1993.\n" }, - // interface to igraph_get_shortest_paths + /* interface to igraph_feedback_vertex_set */ + {"feedback_vertex_set", (PyCFunction) igraphmodule_Graph_feedback_vertex_set, + METH_VARARGS | METH_KEYWORDS, + "feedback_vertex_set(weights=None, method=\"ip\")\n--\n\n" + "Calculates a minimum feedback vertex set.\n\n" + "A feedback vertex set is a set of edges whose removal makes the graph acyclic.\n" + "Finding a minimum feedback vertex set is an NP-hard problem both in directed\n" + "and undirected graphs.\n\n" + "@param weights: vertex weights to be used. Can be a sequence or iterable or\n" + " even a vertex attribute name. When given, the algorithm will strive to\n" + " remove lightweight vertices in order to minimize the total weight of the\n" + " feedback vertex set.\n" + "@param method: the algorithm to use. C{\"ip\"} uses an exact integer programming\n" + " approach, and is currently the only available method.\n" + "@return: the IDs of the vertices to be removed, in a list.\n\n" + }, + + /* interface to igraph_get_shortest_path */ + {"get_shortest_path", (PyCFunction) igraphmodule_Graph_get_shortest_path, + METH_VARARGS | METH_KEYWORDS, + "get_shortest_path(v, to, weights=None, mode=\"out\", output=\"vpath\", algorithm=\"auto\")\n--\n\n" + "Calculates the shortest path from a source vertex to a target vertex in a graph.\n\n" + "This function only returns a single shortest path. Consider using L{get_shortest_paths()}\n" + "to find all shortest paths between a source and one or more target vertices.\n\n" + "@param v: the source vertex of the path\n" + "@param to: the target vertex of the path\n" + "@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. C{\"out\"} means to\n" + " calculate paths from source to target, following edges according to\n" + " their natural direction. C{\"in\"} means to calculate paths from target\n" + " to source, flipping the direction of each edge on-the-fly. C{\"all\"}\n" + " means to ignore edge directions.\n" + "@param output: determines what should be returned. If this is\n" + " C{\"vpath\"}, a list of vertex IDs will be returned. If this is\n" + " C{\"epath\"}, edge IDs are returned instead of vertex IDs.\n" + "@param algorithm: the shortest path algorithm to use. C{\"auto\"} selects an\n" + " algorithm automatically based on whether the graph has negative weights\n" + " or not. C{\"dijkstra\"} uses Dijkstra's algorithm. C{\"bellman_ford\"}\n" + " uses the Bellman-Ford algorithm. Ignored for unweighted graphs.\n" + "@return: see the documentation of the C{output} parameter.\n" + "@see: L{get_shortest_paths()}\n"}, + + /* 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\", algorithm=\"auto\")\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" @@ -12928,22 +16048,26 @@ 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" + "@param algorithm: the shortest path algorithm to use. C{\"auto\"} selects an\n" + " algorithm automatically based on whether the graph has negative weights\n" + " or not. C{\"dijkstra\"} uses Dijkstra's algorithm. C{\"bellman_ford\"}\n" + " uses the Bellman-Ford algorithm. Ignored for unweighted graphs.\n" "@return: see the documentation of the C{output} parameter.\n"}, /* interface to igraph_get_all_shortest_paths */ {"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" @@ -12953,18 +16077,71 @@ 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_k_shortest_paths */ + {"get_k_shortest_paths", + (PyCFunction) igraphmodule_Graph_get_k_shortest_paths, + METH_VARARGS | METH_KEYWORDS, + "get_k_shortest_paths(v, to, k=1, weights=None, mode=\"out\", output=\"vpath\")\n--\n\n" + "Calculates the k shortest paths from/to a given node in a graph.\n\n" + "@param v: the ID or name of the vertex from which the paths are calculated.\n" + "@param to: the ID or name of the vertex to which the paths are calculated.\n" + "@param k: the desired number of shortest path\n" + "@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. 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=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: the k shortest paths from the given source node to the given target node\n" + " in a list of vertex or edge IDs (depending on the value of the C{output}\n" + " argument). Note that in case of mode=C{\"in\"},\n" " the vertices in a path are returned in reversed order!"}, + /* interface to igraph_get_shortest_path_astar */ + {"get_shortest_path_astar", (PyCFunction) igraphmodule_Graph_get_shortest_path_astar, + METH_VARARGS | METH_KEYWORDS, + "get_shortest_path_astar(v, to, heuristics, weights=None, mode=\"out\", output=\"vpath\")\n--\n\n" + "Calculates the shortest path from a source vertex to a target vertex in a\n" + "graph using the A-Star algorithm and a heuristic function.\n\n" + "@param v: the source vertex of the path\n" + "@param to: the target vertex of the path\n" + "@param heuristics: a function that will be called with the graph and two\n" + " vertices, and must return an estimate of the cost of the path from the\n" + " first vertex to the second vertex. The A-Star algorithm is guaranteed to\n" + " return an optimal solution if the heuristic is I{admissible}, i.e. if it\n" + " does never overestimate the cost of the shortest path from the given\n" + " source vertex to the given target vertex.\n" + "@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. C{\"out\"} means to\n" + " calculate paths from source to target, following edges according to\n" + " their natural direction. C{\"in\"} means to calculate paths from target\n" + " to source, flipping the direction of each edge on-the-fly. C{\"all\"}\n" + " means to ignore edge directions.\n" + "@param output: determines what should be returned. If this is\n" + " C{\"vpath\"}, a list of vertex IDs will be returned. If this is\n" + " C{\"epath\"}, edge IDs are returned instead of vertex IDs.\n" + "@return: see the documentation of the C{output} parameter.\n"}, + /* 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, mode=OUT)\n\n" + "_get_all_simple_paths(v, to=None, minlen=0, maxlen=-1, mode=\"out\", max_results=None)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.get_all_simple_paths()\n\n" }, @@ -12972,7 +16149,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" @@ -12984,26 +16161,24 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_convergence_degree */ {"convergence_degree", (PyCFunction)igraphmodule_Graph_convergence_degree, METH_NOARGS, - "convergence_degree()\n\n" - "Undocumented (yet)." + "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" - "Undocumented (yet)." + "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" - "@param scale: whether to normalize the scores so that the largest one\n" - " is 1.\n" "@param arpack_options: an L{ARPACKOptions} object used to fine-tune\n" " the ARPACK eigenvector calculation. If omitted, the module-level\n" " variable called C{arpack_options} is used.\n" @@ -13016,7 +16191,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" @@ -13037,7 +16212,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)\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" @@ -13053,10 +16228,30 @@ 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, - "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" @@ -13075,15 +16270,29 @@ 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\")\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"}, + /* interface to igraph_is_biconnected */ + {"is_biconnected", (PyCFunction) igraphmodule_Graph_is_biconnected, + METH_NOARGS, + "is_biconnected()\n--\n\n" + "Decides whether the graph is biconnected.\n\n" + "A graph is biconnected if it stays connected after the removal of\n" + "any single vertex.\n\n" + "Note that there are different conventions in use about whether to\n" + "consider a graph consisting of two connected vertices to be biconnected.\n" + "igraph does consider it biconnected.\n\n" + "@return: C{True} if it is biconnected, C{False} otherwise.\n" + "@rtype: boolean" + }, + /* 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" @@ -13092,13 +16301,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "The line graph of a directed graph is slightly different: two vertices\n" "are connected by a directed edge iff the target of the first vertex's\n" "corresponding edge is the same as the source of the second vertex's\n" - "corresponding edge.\n" + "corresponding edge.\n\n" + "Edge M{i} in the original graph will map to vertex M{i} of the line graph.\n" }, /* 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" @@ -13107,23 +16317,41 @@ 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 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, - "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" - "than I{mindist] steps are excluded.\n\n" + "than I{mindist} steps are excluded.\n\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 order: the order of the neighborhood, i.e. the maximum number of\n" - " steps to take from the seed vertex.\n" + " steps to take from the seed vertex. Negative values are interpreted as\n" + " an infinite order, i.e. no limit on the number of steps.\n" "@param mode: specifies how to take into account the direction of\n" " the edges if a directed graph is analyzed. C{\"out\"} means that\n" " only the outgoing edges are followed, so all vertices reachable\n" @@ -13143,15 +16371,16 @@ 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" - "than I{mindist] steps are excluded.\n\n" + "than I{mindist} steps are excluded.\n\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 order: the order of the neighborhood, i.e. the maximum number of\n" - " steps to take from the seed vertex.\n" + " steps to take from the seed vertex. Negative values are interpreted as\n" + " an infinite order, i.e. no limit on the number of steps.\n" "@param mode: specifies how to take into account the direction of\n" " the edges if a directed graph is analyzed. C{\"out\"} means that\n" " only the outgoing edges are followed, so all vertices reachable\n" @@ -13173,8 +16402,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "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" + " arpack_options=None, implementation=\"prpack\")\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" @@ -13184,8 +16412,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " C{None} means all of the vertices.\n" "@param directed: whether to consider directed paths.\n" "@param damping: the damping factor.\n" - " M{1-damping} is the PageRank value for vertices with no\n" - " incoming links.\n" "@param reset: the distribution over the vertices to be used when resetting\n" " the random walk. Can be a sequence, an iterable or a vertex attribute\n" " name as long as they return a list of floats whose length is equal to\n" @@ -13209,24 +16435,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " implementation in igraph 0.7\n\n" " - C{\"arpack\"}: use the ARPACK library. This implementation\n" " was used from version 0.5, until version 0.7.\n\n" - " - C{\"power\"}: use a simple power method. This is the\n" - " implementation that was used before igraph version 0.5.\n\n" - "@param niter: The number of iterations to use in the power method\n" - " implementation. It is ignored in the other implementations.\n" - "@param eps: The power method implementation will consider the\n" - " calculation as complete if the difference of PageRank values between\n" - " iterations change less than this value for every node. It is \n" - " ignored by the other implementations.\n" "@return: a list with the personalized PageRank values of the specified\n" " vertices.\n"}, /* 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" + "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" "@param directed: whether to consider directed paths\n" "@return: a tuple. The first item of the tuple is a list of path lengths,\n" " the M{i}th element of the list contains the number of paths with length\n" @@ -13237,11 +16455,11 @@ 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" - "in the new graph. No validity checks are performed on the permutation\n" + "Vertex M{k} of the new graph will belong to vertex M{permutation[k]}\n" + "in the original graph. No validity checks are performed on the permutation\n" "vector.\n\n" "@return: the new graph\n" }, @@ -13249,7 +16467,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\", weights=None)\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" @@ -13258,14 +16476,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " edge directions, C{IN} considers paths that follow the opposite\n" " edge directions, C{ALL} ignores edge directions. The argument is\n" " ignored for undirected graphs.\n" + "@param weights: a list containing the edge weights. It can also be\n" + " an attribute name (edge weights are retrieved from the given\n" + " attribute) or C{None} (all edges have equal weight).\n" "@return: the radius\n" - "@see: L{Graph.eccentricity()}" + "@see: L{eccentricity()}" }, /* 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\")\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" @@ -13287,42 +16508,43 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, /* interface to igraph_rewire */ - {"rewire", (PyCFunction) igraphmodule_Graph_rewire, + {"_rewire", (PyCFunction) igraphmodule_Graph_rewire, METH_VARARGS | METH_KEYWORDS, - "rewire(n=1000, mode=\"simple\")\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" - "use the L{copy} method before.\n\n" - "@param n: the number of rewiring trials.\n" - "@param mode: the rewiring algorithm to use. It can either be C{\"simple\"} or\n" - " C{\"loops\"}; the former does not create or destroy loop edges while the\n" - " latter does.\n"}, + "_rewire(n=None, allowed_edge_types=\"simple\")\n--\n\n" + "Internal function, undocumented.\n\n" + "@see: Graph.rewire()\n\n"}, /* 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, allowed_edge_types=\"simple\")\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" "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" - "use the L{copy} method before.\n\n" + "use the L{cop y} method before.\n\n" "@param prob: rewiring probability\n" - "@param loops: whether the algorithm is allowed to create loop edges\n" - "@param multiple: whether the algorithm is allowed to create multiple\n" - " edges.\n"}, - - /* interface to igraph_shortest_paths */ - {"shortest_paths", (PyCFunction) igraphmodule_Graph_shortest_paths, + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the rewiring 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"}, + + /* interface to igraph_distances */ + {"distances", (PyCFunction) igraphmodule_Graph_distances, METH_VARARGS | METH_KEYWORDS, - "shortest_paths(source=None, target=None, weights=None, mode=OUT)\n\n" + "distances(source=None, target=None, weights=None, mode=\"out\", algorithm=\"auto\")\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" - "used when all the weights are positive. Otherwise, the Bellman-Ford\n" - "algorithm is used if the number of requested source vertices is larger\n" + "used when all the weights are non-negative. Otherwise, the Bellman-Ford\n" + "algorithm is used if the number of requested source vertices is smaller\n" "than 100 and Johnson's algorithm is used otherwise.\n\n" "@param source: a list containing the source vertex IDs which should be\n" " included in the result. If C{None}, all vertices will be considered.\n" @@ -13332,15 +16554,20 @@ 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" + "@param algorithm: the shortest path algorithm to use. C{\"auto\"} selects an\n" + " algorithm automatically based on whether the graph has negative weights\n" + " or not. C{\"dijkstra\"} uses Dijkstra's algorithm. C{\"bellman_ford\"}\n" + " uses the Bellman-Ford algorithm. C{\"johnson\"} uses Johnson's\n" + " algorithm. Ignored for unweighted graphs.\n" "@return: the shortest path lengths for given vertices in a matrix\n"}, /* 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" @@ -13392,29 +16619,29 @@ 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, method=\"auto\")\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" - " 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 */ {"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" @@ -13427,20 +16654,29 @@ 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" - "@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 */ + {"to_prufer", + (PyCFunction) igraphmodule_Graph_to_prufer, + METH_NOARGS, + "to_prufer()\n--\n\n" + "Converts a tree graph into a Prüfer sequence.\n\n" + "@return: the Prüfer sequence as a list" + }, + // interface to igraph_transitivity_undirected {"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" @@ -13451,14 +16687,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Note that this measure is different from the local transitivity\n" "measure (see L{transitivity_local_undirected()}) as it calculates\n" "a single value for the whole graph.\n\n" + "B{Reference}: S. Wasserman and K. Faust: I{Social Network Analysis: Methods\n" + "and Applications}. Cambridge: Cambridge University Press, 1994.\n\n" "@param mode: if C{TRANSITIVITY_ZERO} or C{\"zero\"}, the result will\n" " be zero if the graph does not have any triplets. If C{\"nan\"} or\n" " C{TRANSITIVITY_NAN}, the result will be C{NaN} (not a number).\n" "@return: the transitivity\n" "@see: L{transitivity_local_undirected()}, L{transitivity_avglocal_undirected()}\n" - "@newfield ref: Reference\n" - "@ref: S. Wasserman and K. Faust: I{Social Network Analysis: Methods and\n" - " Applications}. Cambridge: Cambridge University Press, 1994." }, /* interface to igraph_transitivity_local_undirected and @@ -13466,7 +16701,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" @@ -13478,6 +16713,12 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "The traditional local transitivity measure applies for unweighted graphs\n" "only. When the C{weights} argument is given, this function calculates\n" "the weighted local transitivity proposed by Barrat et al (see references).\n\n" + "B{References}:\n\n" + " - D. J. Watts and S. Strogatz: Collective dynamics of\n" + " small-world networks. I{Nature} 393(6884):440-442, 1998.\n" + " - Barrat A, Barthelemy M, Pastor-Satorras R and Vespignani A:\n" + " The architecture of complex weighted networks. I{PNAS} 101, 3747 (2004).\n" + " U{https://arxiv.org/abs/cond-mat/0311416}.\n\n" "@param vertices: a list containing the vertex IDs which should be\n" " included in the result. C{None} means all of the vertices.\n" "@param mode: defines how to treat vertices with degree less than two.\n" @@ -13488,19 +16729,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " even an edge attribute name.\n" "@return: the transitivities for the given vertices in a list\n" "@see: L{transitivity_undirected()}, L{transitivity_avglocal_undirected()}\n" - "@newfield ref: Reference\n" - "@ref: Watts DJ and Strogatz S: I{Collective dynamics of small-world\n" - " networks}. Nature 393(6884):440-442, 1998.\n" - "@ref: Barrat A, Barthelemy M, Pastor-Satorras R and Vespignani A:\n" - " I{The architecture of complex weighted networks}. PNAS 101, 3747 (2004).\n" - " U{http://arxiv.org/abs/cond-mat/0311416}." }, /* interface to igraph_transitivity_avglocal_undirected */ {"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" @@ -13512,24 +16747,23 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Note that this measure is different from the global transitivity measure\n" "(see L{transitivity_undirected()}) as it simply takes the average local\n" "transitivity across the whole network.\n\n" + "B{Reference}: D. J. Watts and S. Strogatz: Collective dynamics of\n" + "small-world networks. I{Nature} 393(6884):440-442, 1998.\n\n" "@param mode: defines how to treat vertices with degree less than two.\n" " If C{TRANSITIVITT_ZERO} or C{\"zero\"}, these vertices will have\n" " zero transitivity. If C{TRANSITIVITY_NAN} or C{\"nan\"}, these\n" " vertices will be excluded from the average.\n" "@see: L{transitivity_undirected()}, L{transitivity_local_undirected()}\n" - "@newfield ref: Reference\n" - "@ref: D. J. Watts and S. Strogatz: I{Collective dynamics of small-world\n" - " networks}. Nature 393(6884):440-442, 1998." }, /* 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" - " 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" @@ -13541,7 +16775,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" @@ -13562,8 +16796,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " graph, therefore it is advised to set this to C{True}. The parameter\n" " is ignored if the connectivity between two given vertices is computed.\n" "@param neighbors: tells igraph what to do when the two vertices are\n" - " connected. C{\"error\"} raises an exception, C{\"infinity\"} returns\n" - " infinity, C{\"ignore\"} ignores the edge.\n" + " connected. C{\"error\"} raises an exception, C{\"negative\"} returns\n" + " a negative value, C{\"number_of_nodes\"} or C{\"nodes\"} returns the\n" + " number of nodes, or C{\"ignore\"} ignores the edge.\n" "@return: the vertex connectivity\n" }, @@ -13574,7 +16809,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" @@ -13582,7 +16817,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" @@ -13590,7 +16825,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=\"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" @@ -13602,7 +16837,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" @@ -13617,16 +16852,25 @@ 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=\"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" "of their common neighbors.\n\n" + "Note that the presence of loop edges may yield counter-intuitive\n" + "results. A node with a loop edge is considered to be a neighbor of itself\n" + "I{twice} (because there are two edge stems incident on the node). Adding a\n" + "loop edge to a node may decrease its similarity to other nodes, but it may\n" + "also I{increase} it. For instance, if nodes A and B are connected but share\n" + "no common neighbors, their similarity is zero. However, if a loop edge is\n" + "added to B, then B itself becomes a common neighbor of A and B and thus the\n" + "similarity of A and B will be increased. Consider removing loop edges\n" + "explicitly before invoking this function using L{Graph.simplify()}.\n\n" "@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" @@ -13634,7 +16878,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=\"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" @@ -13645,7 +16889,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" @@ -13657,51 +16901,50 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " if C{pairs} is not C{None}.\n" }, - /******************/ - /* MOTIF COUNTING */ - /******************/ + /*****************************/ + /* MOTIF COUNTING, TRIANGLES */ + /*****************************/ {"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" "the graph) is characteristic for different types of networks and\n" "network function is related to the motifs in the graph.\n\n" - "This function is able to find the different motifs of size three\n" - "and four (ie. the number of different subgraphs with three and four\n" - "vertices) in the network.\n\n" + "Currently we support motifs of size 3 and 4 for directed graphs, and\n" + "motifs of size 3, 4, 5 or 6 for undirected graphs.\n\n" "In a big network the total number of motifs can be very large, so\n" "it takes a lot of time to find all of them. In such cases, a sampling\n" "method can be used. This function is capable of doing sampling via\n" "the I{cut_prob} argument. This argument gives the probability that\n" "a branch of the motif search tree will not be explored.\n\n" - "@newfield ref: Reference\n" - "@ref: S. Wernicke and F. Rasche: FANMOD: a tool for fast network\n" - " motif detection, Bioinformatics 22(9), 1152--1153, 2006.\n\n" - "@param size: the size of the motifs (3 or 4).\n" + "B{Reference}: S. Wernicke and F. Rasche: FANMOD: a tool for fast network\n" + "motif detection, I{Bioinformatics} 22(9), 1152--1153, 2006.\n\n" + "@param size: the size of the motifs\n" "@param cut_prob: the cut probabilities for different levels of the search\n" " tree. This must be a list of length I{size} or C{None} to find all\n" " 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" - " motif (see L{Graph.isoclass()}). The search will stop when the callback\n" + " itself, the list of vertices in the motif and the isomorphism class of the\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" }, {"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" "assigning isomorphism classes to them.\n\n" - "@newfield ref: Reference\n" - "@ref: S. Wernicke and F. Rasche: FANMOD: a tool for fast network\n" - " motif detection, Bioinformatics 22(9), 1152--1153, 2006.\n\n" - "@param size: the size of the motifs (3 or 4).\n" + "Currently we support motifs of size 3 and 4 for directed graphs, and\n" + "motifs of size 3, 4, 5 or 6 for undirected graphs.\n\n" + "B{Reference}: S. Wernicke and F. Rasche: FANMOD: a tool for fast network\n" + "motif detection, I{Bioinformatics} 22(9), 1152--1153, 2006.\n\n" + "@param size: the size of the motifs\n" "@param cut_prob: the cut probabilities for different levels of the search\n" " tree. This must be a list of length I{size} or C{None} to find all\n" " motifs.\n" @@ -13710,16 +16953,17 @@ 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=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" "assigning isomorphism classes to them by extrapolating from a random\n" "sample of vertices.\n\n" - "@newfield ref: Reference\n" - "@ref: S. Wernicke and F. Rasche: FANMOD: a tool for fast network\n" - " motif detection, Bioinformatics 22(9), 1152--1153, 2006.\n\n" - "@param size: the size of the motifs (3 or 4).\n" + "Currently we support motifs of size 3 and 4 for directed graphs, and\n" + "motifs of size 3, 4, 5 or 6 for undirected graphs.\n\n" + "B{Reference}: S. Wernicke and F. Rasche: FANMOD: a tool for fast network\n" + "motif detection, I{Bioinformatics} 22(9), 1152--1153, 2006.\n\n" + "@param size: the size of the motifs\n" "@param cut_prob: the cut probabilities for different levels of the search\n" " tree. This must be a list of length I{size} or C{None} to find all\n" " motifs.\n" @@ -13729,31 +16973,115 @@ 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" "I{b} and also from I{b} to I{a}; asymmetric, there is an edge\n" "either from I{a} to I{b} or from I{b} to I{a} but not the other way\n" "and null, no edges between I{a} and I{b}.\n\n" - "@attention: this function has a more convenient interface in class\n" - " L{Graph} which wraps the result in a L{DyadCensus} object.\n" - " It is advised to use that.\n\n" + "Attention: this function has a more convenient interface in class\n" + "L{Graph}, which wraps the result in a L{DyadCensus} object.\n" + "It is advised to use that.\n\n" "@return: the number of mutual, asymmetric and null connections in a\n" " 3-tuple." }, {"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" "these are listed in the documentation of the C interface of igraph.\n" "\n" - "@attention: this function has a more convenient interface in class\n" - " L{Graph} which wraps the result in a L{TriadCensus} object.\n" - " It is advised to use that. The name of the triplet classes are\n" - " also documented there.\n\n" + "Attention: this function has a more convenient interface in class\n" + "L{Graph}, which wraps the result in a L{TriadCensus} object.\n" + "It is advised to use that. The name of the triplet classes are\n" + "also documented there.\n\n" + }, + {"list_triangles", (PyCFunction) igraphmodule_Graph_list_triangles, + METH_NOARGS, + "list_triangles()\n--\n\n" + "Lists the triangles of the graph\n\n" + "@return: the list of triangles in the graph; each triangle is represented\n" + " by a tuple of length 3, containing the corresponding vertex IDs." + }, + + /***********************/ + /* CYCLES, CYCLE BASES */ + /***********************/ + + {"is_acyclic", (PyCFunction) igraphmodule_Graph_is_acyclic, + METH_NOARGS, + "is_acyclic()\n--\n\n" + "Returns whether the graph is acyclic (i.e. contains no cycles).\n\n" + "@return: C{True} if the graph is acyclic, C{False} otherwise.\n" + "@rtype: boolean" + }, + + {"is_dag", (PyCFunction) igraphmodule_Graph_is_dag, + METH_NOARGS, + "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" + "@rtype: boolean" + }, + + {"fundamental_cycles", (PyCFunction) igraphmodule_Graph_fundamental_cycles, + METH_VARARGS | METH_KEYWORDS, + "fundamental_cycles(start_vid=None, cutoff=None, weights=None)\n--\n\n" + "Finds a single fundamental cycle basis of the graph\n\n" + "@param start_vid: when C{None} or negative, a complete fundamental cycle basis is\n" + " returned. When it is a vertex or a vertex ID, the fundamental cycles\n" + " associated with the BFS tree rooted in that vertex will be returned,\n" + " only for the weakly connected component containing that vertex\n" + "@param cutoff: when C{None} or negative, a complete cycle basis is returned. Otherwise\n" + " the BFS is stopped after this many steps, so the result will effectively\n" + " include cycles of length M{2 * cutoff + 1} or shorter only.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" + "@return: the cycle basis as a list of tuples containing edge IDs" + }, + {"minimum_cycle_basis", (PyCFunction) igraphmodule_Graph_minimum_cycle_basis, + METH_VARARGS | METH_KEYWORDS, + "minimum_cycle_basis(cutoff=None, complete=True, use_cycle_order=True, weights=None)\n--\n\n" + "Computes a minimum cycle basis of the graph\n\n" + "@param cutoff: when C{None} or negative, a complete minimum cycle basis is returned.\n" + " Otherwise only those cycles in the result will be part of some minimum\n" + " cycle basis that are of length M{2 * cutoff + 1} or shorter. Cycles\n" + " longer than this limit may not be of the smallest possible size. This\n" + " parameter effectively limits the depth of the BFS tree when computing\n" + " candidate cycles and may speed up the computation substantially.\n" + "@param complete: used only when a cutoff is specified, and in this case it\n" + " specifies whether a complete basis is returned (C{True}) or the result\n" + " will be limited to cycles of length M{2 * cutoff + 1} or shorter only.\n" + " This limits computation time, but the result may not span the entire\n" + " cycle space.\n" + "@param use_cycle_order: if C{True}, every cycle is returned in natural\n" + " order: the edge IDs will appear ordered along the cycle. If C{False},\n" + " no guarantees are given about the ordering of edge IDs within cycles.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" + "@return: the cycle basis as a list of tuples containing edge IDs" + }, + {"simple_cycles", (PyCFunction) igraphmodule_Graph_simple_cycles, + METH_VARARGS | METH_KEYWORDS, + "simple_cycles(mode=None, min=-1, max=-1, output=\"vpath\")\n--\n\n" + "Finds simple cycles in a graph\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" + "@param min: the minimum number of vertices in a cycle\n" + " for it to be returned.\n" + "@param max: the maximum number of vertices in a cycle\n" + " for it to be considered.\n" + "@param output: determines what should be returned. If this is\n" + " C{\"vpath\"}, a list of tuples of vertex IDs will be returned. If this is\n" + " C{\"epath\"}, edge IDs are returned instead of vertex IDs.\n" + "@return: see the documentation of the C{output} parameter.\n" }, /********************/ @@ -13764,7 +17092,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" @@ -13783,7 +17111,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" @@ -13794,7 +17122,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" @@ -13809,7 +17137,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" @@ -13822,21 +17150,23 @@ 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=None, epsilon=0, kkconst=None, seed=None, " + "minx=None, maxx=None, miny=None, maxy=None, minz=None, maxz=None, dim=2, " + "weights=None)\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" "Information Processing Letters, 31/1, 7--15, 1989.\n\n" - "@param maxiter: the maximum number of iterations to perform.\n" - "@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 maxiter: the maximum number of iterations to perform. C{None} selects\n" + " a reasonable default based on the number of vertices.\n" + "@param seed: when C{None}, uses a circular layout as a starting point for the\n" + " algorithm when no bounds are given, or a random layout when bounds are\n" + " specified for the coordinated. When the argument is a matrix (list of\n" + " lists), it uses the given matrix as the initial layout.\n" "@param epsilon: quit if the energy of the system changes less than\n" " epsilon. See the original paper for details.\n" "@param kkconst: the Kamada-Kawai vertex attraction constant.\n" - " C{None} means the square of the number of vertices.\n" + " C{None} means the number of vertices.\n" "@param minx: if not C{None}, it must be a vector with exactly as many\n" " elements as there are vertices in the graph. Each element is a\n" " minimum constraint on the X value of the vertex in the layout.\n" @@ -13849,6 +17179,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " for 3D layouts (C{dim}=3).\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" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" "@return: the calculated layout." }, @@ -13856,9 +17188,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" @@ -13896,7 +17228,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" @@ -13908,11 +17240,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" @@ -13956,9 +17287,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" @@ -13998,7 +17329,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" @@ -14006,7 +17338,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "forces among the vertices and then the physical system is simulated\n" "until it reaches an equilibrium or the maximal number of iterations is\n" "reached.\n\n" - "See U{http://www.schmuhl.org/graphopt/} for the original graphopt.\n\n" + "See U{https://web.archive.org/web/20220611030748/http://www.schmuhl.org/graphopt/}\n" + "and U{https://sourceforge.net/projects/graphopt/} for the original graphopt.\n\n" "@param niter: the number of iterations to perform. Should be a couple\n" " of hundred in general.\n\n" "@param node_charge: the charge of the vertices, used to calculate electric\n" @@ -14024,7 +17357,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" @@ -14050,7 +17384,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\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" @@ -14062,6 +17396,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "For unconnected graphs, the method will decompose the graph into\n" "weakly connected components and then lay out the components\n" "individually using the appropriate parts of the distance matrix.\n\n" + "B{Reference}: Cox & Cox: Multidimensional Scaling (1994), Chapman and\n" + "Hall, London.\n\n" "@param dist: the distance matrix. It must be symmetric and the\n" " symmetry is not checked -- results are unspecified when a\n" " non-symmetric distance matrix is used. If this parameter is\n" @@ -14073,21 +17409,20 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param arpack_options: an L{ARPACKOptions} object used to fine-tune\n" " the ARPACK eigenvector calculation. If omitted, the module-level\n" " variable called C{arpack_options} is used.\n" - "@return: the calculated layout.\n\n" - "@newfield ref: Reference\n" - "@ref: Cox & Cox: Multidimensional Scaling (1994), Chapman and\n" - " Hall, London.\n" + "@return: the calculated layout.\n" }, /* interface to igraph_layout_reingold_tilford */ {"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\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" "search is executed first to obtain a possible spanning tree.\n\n" + "B{Reference}: EM Reingold, JS Tilford: Tidier Drawings of Trees. I{IEEE\n" + "Transactions on Software Engineering} 7:22, 223-228, 1981.\n\n" "@param mode: specifies which edges to consider when builing the tree.\n" " If it is C{OUT} then only the outgoing, if it is C{IN} then only the\n" " incoming edges of a parent are considered. If it is C{ALL} then all\n" @@ -14095,39 +17430,41 @@ 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" "@return: the calculated layout.\n\n" "@see: layout_reingold_tilford_circular\n" - "@newfield ref: Reference\n" - "@ref: EM Reingold, JS Tilford: I{Tidier Drawings of Trees.}\n" - "IEEE Transactions on Software Engineering 7:22, 223-228, 1981."}, + }, /* interface to igraph_layout_reingold_tilford_circular */ {"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\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" "See L{layout_reingold_tilford} for the explanation of the parameters.\n\n" + "B{Reference}: EM Reingold, JS Tilford: Tidier Drawings of Trees. I{IEEE\n" + "Transactions on Software Engineering} 7:22, 223-228, 1981.\n\n" "@return: the calculated layout.\n\n" "@see: layout_reingold_tilford\n" - "@newfield ref: Reference\n" - "@ref: EM Reingold, JS Tilford: I{Tidier Drawings of Trees.}\n" - "IEEE Transactions on Software Engineering 7:22, 223-228, 1981."}, + }, /* 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\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" @@ -14137,18 +17474,60 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"_layout_sugiyama", (PyCFunction) igraphmodule_Graph_layout_sugiyama, METH_VARARGS | METH_KEYWORDS, + "_layout_sugiyama()\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.layout_sugiyama()\n\n"}, + /* interface to igraph_layout_umap */ + {"layout_umap", + (PyCFunction) igraphmodule_Graph_layout_umap, + METH_VARARGS | METH_KEYWORDS, + "layout_umap(dist=None, weights=None, dim=2, seed=None, min_dist=0.01, epochs=500)\n" + "--\n\n" + "Uniform Manifold Approximation and Projection (UMAP).\n\n" + "This layout is a probabilistic algorithm that places vertices that are connected\n" + "and have a short distance close by in the embedded space.\n\n" + "B{Reference}: L McInnes, J Healy, J Melville: UMAP: Uniform Manifold Approximation\n" + "and Projection for Dimension Reduction. arXiv:1802.03426.\n\n" + "@param dist: distances associated with the graph edges. If None, all edges will\n" + " be assumed to convey the same distance between the vertices. Either this\n" + " argument of the C{weights} argument can be set, but not both. It is fine to\n" + " set neither.\n" + "@param weights: precomputed edge weights if you have them, as an alternative\n" + " to setting the C{dist} argument. Zero weights will be ignored if this\n" + " argument is set, e.g. if you computed the weights via\n" + " igraph.umap_compute_weights().\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" + "@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 min_dist: the minimal distance in the embedded space beyond which the\n" + " probability of being located closeby decreases.\n" + "@param epochs: the number of epochs (iterations) the algorithm will iterate\n" + " over. Accuracy increases with more epochs, at the cost of longer runtimes.\n" + " Values between 50 and 1000 are typical.\n" + " Notice that UMAP does not technically converge for symmetry reasons, but a\n" + " larger number of epochs should generally give an equivalent or better layout.\n" + "@return: the calculated layout.\n\n" + "Please note that if distances are set, the graph is usually directed, whereas\n" + "if weights are precomputed, the graph will be treated as undirected. A special\n" + "case is when the graph is directed but the precomputed weights are symmetrized\n" + "in a way only one of each pair of opposite edges has nonzero weight, e.g. as\n" + "computed by igraph.umap_compute_weights(). For example:\n" + "C{weights = igraph.umap_compute_weights(graph, dist)}\n" + "C{layout = graph.layout_umap(weights=weights)}\n\n" + "@see: igraph.umap_compute_weights()\n"}, + //////////////////////////// // VISITOR-LIKE FUNCTIONS // //////////////////////////// {"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" + "@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" @@ -14156,15 +17535,26 @@ 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" + "@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" " 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\n" + "Constructs a depth first search (DFS) iterator of the graph.\n\n" + "@param vid: the root vertex ID\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" + " parent of the vertex in the DFS tree as well.\n" + "@return: the DFS iterator as an L{igraph.DFSIter} object.\n"}, ///////////////// // CONVERSIONS // @@ -14173,44 +17563,47 @@ 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=\"both\", loops=\"twice\")\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 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" - " result matrix will contain the number of edges for\n" - " each vertex pair.\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 loops: specifies how loop edges should be handled. C{False} or\n" + " C{\"ignore\"} ignores loop edges. C{\"once\"} counts each loop edge once\n" + " in the diagonal. C{\"twice\"} counts each loop edge twice (i.e. it counts\n" + " the I{endpoints} of the loop edges, not the edges themselves).\n" "@return: the adjacency matrix.\n"}, - // 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."}, - - /* interface to igraph_get_incidence */ - {"get_incidence", (PyCFunction) igraphmodule_Graph_get_incidence, + /* interface to igraph_get_biadjacency */ + {"get_biadjacency", (PyCFunction) igraphmodule_Graph_get_biadjacency, METH_VARARGS | METH_KEYWORDS, - "get_incidence(types)\n\n" + "get_biadjacency(types)\n--\n\n" "Internal function, undocumented.\n\n" - "@see: Graph.get_incidence()\n\n"}, + "@see: Graph.get_biadjacency()\n\n"}, + + /* interface to igraph_get_edgelist */ + {"get_edgelist", (PyCFunction) igraphmodule_Graph_get_edgelist, + METH_NOARGS, + "get_edgelist()\n--\n\n" "Returns the edge list of a graph."}, - // 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" + "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, 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" @@ -14220,39 +17613,48 @@ 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 */ {"laplacian", (PyCFunction) igraphmodule_Graph_laplacian, METH_VARARGS | METH_KEYWORDS, - "laplacian(weights=None, normalized=False)\n\n" + "laplacian(weights=None, normalized=\"unnormalized\", mode=\"out\")\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" - "Normalized Laplacian matrices have 1 or 0 in their diagonals (0 for vertices\n" - "with no edges), edges are denoted by 1 / sqrt(d_i * d_j) where d_i is the\n" - "degree of node i.\n\n" - "Multiple edges and self-loops are silently ignored. Although it is\n" - "possible to calculate the Laplacian matrix of a directed graph, it does\n" - "not make much sense.\n\n" + "Symmetric normalized Laplacian matrices have 1 or 0 in their diagonals\n" + "(0 for vertices with no edges), edges are denoted by 1 / sqrt(d_i * d_j)\n" + "where d_i is the degree of node i.\n\n" + "Left-normalized and right-normalized Laplacian matrices are derived from\n" + "the unnormalized Laplacian by scaling the row or the column sums to be\n" + "equal to 1.\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name. When edge weights are used, the degree\n" - " of a node is considered to be the weight of its incident edges.\n" + " of a node is considered to be the sum of the weights of its incident\n" + " edges.\n" "@param normalized: whether to return the normalized Laplacian matrix.\n" + " C{False} or C{\"unnormalized\"} returns the unnormalized Laplacian matrix.\n" + " C{True} or C{\"symmetric\"} returns the symmetric normalization of the\n" + " Laplacian matrix. C{\"left\"} returns the left-, C{\"right\"} returns the\n" + " right-normalized Laplacian matrix.\n" + "@param mode: for directed graphs, specifies whether to use out- or in-degrees\n" + " in the Laplacian matrix. C{\"all\"} means that the edge directions must be\n" + " ignored, C{\"out\"} means that the out-degrees should be used, C{\"in\"}\n" + " means that the in-degrees should be used. Ignored for undirected graphs.\n" "@return: the Laplacian matrix.\n"}, /////////////////////////////// // LOADING AND SAVING GRAPHS // /////////////////////////////// - // interface to igraph_read_graph_dimacs + // interface to igraph_read_graph_dimacs_flow {"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" + "U{https://lpsolve.sourceforge.net/5.5/DIMACS.htm}\n\n" "Restrictions compared to the official description of the format:\n\n" " - igraph's DIMACS reader requires only three fields in an arc definition,\n" " describing the edge's source and target node and its capacity.\n" @@ -14267,7 +17669,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"}, @@ -14275,24 +17677,26 @@ 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.\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 */ {"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" + "isomorphism testing (see U{https://mivia.unisa.it/datasets/graph-database/arg-database/}).\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_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, 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" @@ -14302,20 +17706,20 @@ 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" "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" @@ -14334,7 +17738,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" @@ -14359,13 +17763,14 @@ 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" - "Reads a Pajek format file and creates a graph based on it.\n\n" + "Read_Pajek(f)\n--\n\n" + "Reads a file in the Pajek format and creates a graph based on it.\n" + "Only Pajek network files (.net extension) are supported, not project files (.paj).\n\n" "@param f: the name of the file or a Python file handle\n"}, - /* interface to igraph_write_graph_dimacs */ + /* interface to igraph_write_graph_dimacs_flow */ {"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" @@ -14376,7 +17781,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" @@ -14385,14 +17790,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" @@ -14404,7 +17809,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" @@ -14420,7 +17825,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" @@ -14437,21 +17842,25 @@ 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, prefixattr=True)\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" + "@param prefixattr: whether attribute names in the written file should be\n" + " prefixed with C{g_}, C{v_} and C{e_} for graph, vertex and edge\n" + " attributes, respectively. This might be needed to ensure the uniqueness\n" + " of attribute identifiers in the written GraphML file.\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" @@ -14470,15 +17879,41 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* ISOMORPHISM */ /***************/ + {"automorphism_group", + (PyCFunction) igraphmodule_Graph_automorphism_group, + METH_VARARGS | METH_KEYWORDS, + "automorphism_group(sh=\"fl\", color=None)\n--\n\n" + "Calculates the generators of the automorphism group of a graph using the\n" + "BLISS isomorphism algorithm.\n\n" + "The generator set may not be minimal and may depend on the splitting\n" + "heuristics. The generators are permutations represented using zero-based\n" + "indexing.\n\n" + "@param sh: splitting heuristics for graph as a case-insensitive string,\n" + " with the following possible values:\n\n" + " - C{\"f\"}: first non-singleton cell\n\n" + " - C{\"fl\"}: first largest non-singleton cell\n\n" + " - C{\"fs\"}: first smallest non-singleton cell\n\n" + " - C{\"fm\"}: first maximally non-trivially connected non-singleton\n" + " cell\n\n" + " - C{\"flm\"}: largest maximally non-trivially connected\n" + " non-singleton cell\n\n" + " - C{\"fsm\"}: smallest maximally non-trivially connected\n" + " non-singleton cell\n\n" + "@param color: optional vector storing a coloring of the vertices\n " + " with respect to which the isomorphism is computed." + " If C{None}, all vertices have the same color.\n" + "@return: a list of integer vectors, each vector representing an automorphism\n" + " group of the graph.\n" + }, {"canonical_permutation", (PyCFunction) igraphmodule_Graph_canonical_permutation, METH_VARARGS | METH_KEYWORDS, - "canonical_permutation(sh=\"fm\")\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" + "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" + "See U{https://users.aalto.fi/~tjunttil/bliss/} for more information\n" "about the BLISS algorithm and canonical permutations.\n\n" "@param sh: splitting heuristics for graph as a case-insensitive string,\n" " with the following possible values:\n\n" @@ -14491,23 +17926,50 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " non-singleton cell\n\n" " - C{\"fsm\"}: smallest maximally non-trivially connected\n" " non-singleton cell\n\n" + "@param color: optional vector storing a coloring of the vertices\n " + " with respect to which the isomorphism is computed." + " If C{None}, all vertices have the same color.\n" "@return: a permutation vector containing vertex IDs. Vertex 0 in the original\n" " graph will be mapped to an ID contained in the first element of this\n" " vector; vertex 1 will be mapped to the second and so on.\n" }, + {"count_automorphisms", + (PyCFunction) igraphmodule_Graph_count_automorphisms, + METH_VARARGS | METH_KEYWORDS, + "count_automorphisms(sh=\"fl\", color=None)\n--\n\n" + "Calculates the number of automorphisms of a graph using the BLISS isomorphism\n" + "algorithm.\n\n" + "See U{https://users.aalto.fi/~tjunttil/bliss/} for more information\n" + "about the BLISS algorithm and canonical permutations.\n\n" + "@param sh: splitting heuristics for graph as a case-insensitive string,\n" + " with the following possible values:\n\n" + " - C{\"f\"}: first non-singleton cell\n\n" + " - C{\"fl\"}: first largest non-singleton cell\n\n" + " - C{\"fs\"}: first smallest non-singleton cell\n\n" + " - C{\"fm\"}: first maximally non-trivially connected non-singleton\n" + " cell\n\n" + " - C{\"flm\"}: largest maximally non-trivially connected\n" + " non-singleton cell\n\n" + " - C{\"fsm\"}: smallest maximally non-trivially connected\n" + " non-singleton cell\n\n" + "@param color: optional vector storing a coloring of the vertices\n " + " with respect to which the isomorphism is computed." + " If C{None}, all vertices have the same color.\n" + "@return: the number of automorphisms of the graph.\n" + }, {"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" - "Isomorphy class calculations are implemented only for graphs with\n" - "3 or 4 vertices.\n\n" + "isoclass(vertices)\n--\n\n" + "Returns the isomorphism class of the graph or its subgraph.\n\n" + "Isomorphism class calculations are implemented only for directed graphs\n" + "with 3 or 4 vertices, or undirected graphs with 3, 4, 5 or 6 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" + "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" @@ -14517,20 +17979,24 @@ 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, METH_VARARGS | METH_KEYWORDS, "isomorphic_bliss(other, return_mapping_12=False, return_mapping_21=False,\n" - " sh1=\"fm\", sh2=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" + "See U{https://users.aalto.fi/~tjunttil/bliss/} for more information\n" "about the BLISS algorithm.\n\n" "@param other: the other graph with which we want to compare the graph.\n" + "@param color1: optional vector storing the coloring of the vertices of\n" + " the first graph. If C{None}, all vertices have the same color.\n" + "@param color2: optional vector storing the coloring of the vertices of\n" + " the second graph. If C{None}, all vertices have the same color.\n" "@param return_mapping_12: if C{True}, calculates the mapping which maps\n" " the vertices of the first graph to the second.\n" "@param return_mapping_21: if C{True}, calculates the mapping which maps\n" @@ -14561,7 +18027,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" @@ -14614,7 +18080,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" @@ -14648,8 +18114,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" @@ -14686,7 +18152,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" @@ -14740,7 +18206,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" @@ -14775,7 +18241,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" @@ -14810,7 +18276,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" @@ -14833,11 +18299,11 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " contains a subgraph that is isomorphic to the given template, C{False}\n" " otherwise. If the mapping is calculated, the result is a tuple, the first\n" " element being the above mentioned boolean, and the second element being\n" - " the mapping from the target to the original graph.\n"}, + " the mapping from the target to the original graph.\n"}, {"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" @@ -14860,54 +18326,64 @@ 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()\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" - "@return: the attribute name list of the graph's vertices\n"}, + "vertex_attributes()\n--\n\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" - "@return: the attribute name list of the graph's edges\n"}, + "edge_attributes()\n--\n\n" + "@return: the attribute name list of the edges of the graph\n"}, /////////////// // OPERATORS // /////////////// + {"complementer", (PyCFunction) igraphmodule_Graph_complementer, - METH_VARARGS, - "complementer(loops=False)\n\n" + METH_VARARGS | METH_KEYWORDS, + "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"}, + {"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"}, - {"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"}, + "difference(other)\n--\n\nSubtracts the given graph from the original"}, + + /* interface to igraph_delete_edges */ + {"reverse_edges", (PyCFunction) igraphmodule_Graph_reverse_edges, + METH_VARARGS | METH_KEYWORDS, + "reverse_edges(es)\n--\n\n" + "Reverses the direction of some edges in the graph.\n\n" + "This function is a no-op for undirected graphs.\n\n" + "@param es: the list of edges to be reversed. Edges are identifed by\n" + " edge IDs. L{EdgeSeq} objects are also accepted here. When omitted,\n" + " all edges will be reversed.\n"}, + + /**********************/ + /* DOMINATORS */ + /**********************/ + + {"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\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." + }, /*****************/ /* MAXIMUM FLOWS */ /*****************/ {"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" @@ -14918,11 +18394,11 @@ 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" - " to use that.\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" + "to use that.\n" "@param source: the source vertex ID\n" "@param target: the target vertex ID\n" "@param capacity: the capacity of the edges. It must be a list or a valid\n" @@ -14942,34 +18418,34 @@ 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" "Every cut is listed exactly once.\n\n" + "Attention: this function has a more convenient interface in class\n" + "L{Graph}, which wraps the result in a list of L{Cut} objects. It is\n" + "advised to use that.\n" "@param source: the source vertex ID\n" "@param target: the target vertex ID\n" - "@attention: this function has a more convenient interface in class\n" - " L{Graph} which wraps the result in a list of L{Cut} objects. It is\n" - " advised to use that.\n" "@return: a tuple where the first element is a list of lists of edge IDs\n" " representing a cut and the second element is a list of lists of vertex\n" " IDs representing the sets of vertices that were separated by the cuts.\n" }, {"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" + "Attention: this function has a more convenient interface in class\n" + "L{Graph}, which wraps the result in a list of L{Cut} objects. It is\n" + "advised to use that.\n\n" "@param source: the source vertex ID\n" "@param target: the target vertex ID\n" - "@attention: this function has a more convenient interface in class\n" - " L{Graph} which wraps the result in a list of L{Cut} objects. It is\n" - " advised to use that.\n" }, {"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" @@ -14983,7 +18459,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" @@ -14994,9 +18470,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "For undirected graphs and no source and target, the method uses the Stoer-Wagner\n" "algorithm. For a given source and target, the method uses the push-relabel\n" "algorithm; see the references below.\n\n" - "@attention: this function has a more convenient interface in class\n" - " L{Graph} which wraps the result in a L{Cut} object. It is advised\n" - " to use that.\n" + "Attention: this function has a more convenient interface in class\n" + "L{Graph}, which wraps the result in a L{Cut} object. It is advised\n" + "to use that.\n\n" + "B{References}\n\n" + " - M. Stoer, F. Wagner: A simple min-cut algorithm. I{Journal of the ACM}\n" + " 44(4):585-591, 1997.\n" + " - A. V. Goldberg, R. E. Tarjan: A new approach to the maximum-flow problem.\n" + " I{Journal of the ACM} 35(4):921-940, 1988.\n\n" "@param source: the source vertex ID. If C{None}, target must also be\n" " {None} and the calculation will be done for the entire graph (i.e. all\n" " possible vertex pairs).\n" @@ -15008,19 +18489,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " same capacity.\n" "@return: the value of the minimum cut, the IDs of vertices in the\n" " first and second partition, and the IDs of edges in the cut,\n" - " packed in a 4-tuple\n\n" - "@newfield ref: Reference\n" - "@ref: M. Stoer, F. Wagner: A simple min-cut algorithm. Journal of\n" - " the ACM 44(4):585-591, 1997.\n" - "@ref: A. V. Goldberg, R. E. Tarjan: A new approach to the maximum-flow problem.\n" - " Journal of the ACM 35(4):921-940, 1988.\n" + " packed in a 4-tuple\n" }, {"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" + "Attention: this function has a more convenient interface in class\n" + "L{Graph}, which wraps the result in a list of L{Cut} objects. It is\n" + "advised to use that.\n\n" "@param source: the source vertex ID\n" "@param target: the target vertex ID\n" "@param capacity: the capacity of the edges. It must be a list or a valid\n" @@ -15029,14 +18508,11 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the value of the minimum cut, the IDs of vertices in the\n" " first and second partition, and the IDs of edges in the cut,\n" " packed in a 4-tuple\n\n" - "@attention: this function has a more convenient interface in class\n" - " L{Graph} which wraps the result in a list of L{Cut} objects. It is\n" - " advised to use that.\n" }, {"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" }, @@ -15046,21 +18522,21 @@ 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" + "B{Reference}: Anne Berry, Jean-Paul Bordat and Olivier Cogis: Generating all the\n" + "minimal separators of a graph. In: Peter Widmayer, Gabriele Neyer and\n" + "Stephan Eidenbenz (eds.): Graph-theoretic concepts in computer science,\n" + "1665, 167-172, 1999. Springer.\n\n" "@return: a list where each item lists the vertex indices of a given\n" " minimal s-t separator.\n" - "@newfield ref: Reference\n" - "@ref: Anne Berry, Jean-Paul Bordat and Olivier Cogis: Generating all the\n" - " minimal separators of a graph. In: Peter Widmayer, Gabriele Neyer and\n" - " Stephan Eidenbenz (eds.): Graph-theoretic concepts in computer science,\n" - " 1665, 167--172, 1999. Springer.\n"}, + }, {"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" @@ -15070,34 +18546,48 @@ 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" "given graph.\n\n" + "B{Reference}: Arkady Kanevsky: Finding all minimum-size separating vertex\n" + "sets in a graph. I{Networks} 23:533-541, 1993.\n\n" "@return: a list where each item lists the vertex indices of a given\n" " separator of minimum size.\n" - "@newfield ref: Reference\n" - "@ref: Arkady Kanevsky: Finding all minimum-size separating vertex sets\n" - " in a graph. Networks 23:533--541, 1993.\n"}, + }, /*******************/ /* COHESIVE BLOCKS */ /*******************/ {"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" - " It is advised to use that.\n" + "Attention: this function has a more convenient interface in class\n" + "L{Graph}, which wraps the result in a L{CohesiveBlocks} object.\n" + "It is advised to use that.\n" + }, + + /************/ + /* COLORING */ + /************/ + {"vertex_coloring_greedy", (PyCFunction) igraphmodule_Graph_vertex_coloring_greedy, + METH_VARARGS | METH_KEYWORDS, + "vertex_coloring_greedy(method=\"colored_neighbors\")\n--\n\n" + "Calculates a greedy vertex coloring for the graph based on some heuristics.\n\n" + "@param method: the heuristics to use. C{colored_neighbors} always picks the\n" + " vertex with the largest number of colored neighbors as the next vertex to\n" + " pick a color for. C{dsatur} picks the vertex with the largest number of\n" + " I{unique} colors in its neighborhood; this is also known as the DSatur\n" + " heuristics (hence the name).\n" }, /********************************/ @@ -15105,17 +18595,20 @@ 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, max_results=None)\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" "@param min: the minimum size of cliques to be returned. If zero or\n" " negative, no lower bound will be used.\n" "@param max: the maximum size of cliques to be returned. If zero or\n" - " negative, no upper bound will be used."}, + " negative, no upper bound will be used.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n" + }, {"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" @@ -15124,7 +18617,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, max_results=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" @@ -15138,30 +18631,35 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param file: a file object or the name of the file to write the results\n" " to. When this argument is C{None}, the maximal cliques will be returned\n" " as a list of lists.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n" "@return: the maximal cliques of the graph as a list of lists, or C{None}\n" " if the C{file} argument was given." "@see: L{largest_cliques()} for the largest cliques."}, {"clique_number", (PyCFunction) igraphmodule_Graph_clique_number, METH_NOARGS, - "clique_number()\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."}, {"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, max_results=None)\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" "@param min: the minimum size of sets to be returned. If zero or\n" " negative, no lower bound will be used.\n" "@param max: the maximum size of sets to be returned. If zero or\n" - " negative, no upper bound will be used."}, + " negative, no upper bound will be used.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n" + }, {"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" @@ -15172,23 +18670,29 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " (nonextendable) independent vertex sets"}, {"maximal_independent_vertex_sets", (PyCFunction) igraphmodule_Graph_maximal_independent_vertex_sets, - METH_NOARGS, - "maximal_independent_vertex_sets()\n\n" + METH_VARARGS | METH_KEYWORDS, + "maximal_independent_vertex_sets(min=0, max=0, max_results=None)\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" "independent vertex set is not necessarily one of the largest\n" "independent vertex sets in the graph.\n\n" + "B{Reference}: S. Tsukiyama, M. Ide, H. Ariyoshi and I. Shirawaka: A new\n" + "algorithm for generating all the maximal independent sets.\n" + "I{SIAM J Computing}, 6:505-517, 1977.\n\n" + "@param min: the minimum size of sets to be returned. If zero or\n" + " negative, no lower bound will be used.\n" + "@param max: the maximum size of sets to be returned. If zero or\n" + " negative, no upper bound will be used.\n" + "@param max_results: the maximum number of results to return. C{None}\n" + " means no limit on the number of results.\n\n" "@see: L{largest_independent_vertex_sets()} for the largest independent\n" - " vertex sets\n\n" - "@newfield ref: Reference\n" - "@ref: S. Tsukiyama, M. Ide, H. Ariyoshi and I. Shirawaka: I{A new\n" - " algorithm for generating all the maximal independent sets}.\n" - " SIAM J Computing, 6:505--517, 1977."}, + " vertex sets\n" + }, {"independence_number", (PyCFunction) igraphmodule_Graph_independence_number, METH_NOARGS, - "independence_number()\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" @@ -15198,82 +18702,133 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /*********************************/ /* COMMUNITIES AND DECOMPOSITION */ /*********************************/ + {"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\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" "total weight of edges incident on vertex M{j} and M{m} is the total\n" "edge weight in the graph.\n\n" - "@attention: method overridden in L{Graph} to allow L{VertexClustering}\n" - " objects as a parameter. This method is not strictly necessary, since\n" - " the L{VertexClustering} class provides a variable called C{modularity}.\n" + "Attention: method overridden in L{Graph} to allow L{VertexClustering}\n" + "objects as a parameter. This method is not strictly necessary, since\n" + "the L{VertexClustering} class provides a variable called C{modularity}.\n\n" + "B{Reference}: MEJ Newman and M Girvan: Finding and evaluating community\n" + "structure in networks. I{Phys Rev E} 69 026113, 2004.\n\n" "@param membership: the membership vector, e.g. the vertex type index for\n" " each vertex.\n" "@param weights: optional edge weights or C{None} if all edges are weighed\n" " equally.\n" - "@return: the modularity score. Score larger than 0.3 usually indicates\n" - " strong community structure.\n" - "@newfield ref: Reference\n" - "@ref: MEJ Newman and M Girvan: Finding and evaluating community structure\n" - " in networks. Phys Rev E 69 026113, 2004.\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.\n" + }, + {"modularity_matrix", (PyCFunction) igraphmodule_Graph_modularity_matrix, + METH_VARARGS | METH_KEYWORDS, + "modularity_matrix(weights=None, resolution=1, directed=True)\n--\n\n" + "Calculates the modularity matrix of the graph.\n\n" + "@param weights: optional edge weights or C{None} if all edges are weighed\n" + " equally.\n" + "@param resolution: the resolution parameter I{gamma} of the modularity\n" + " formula. The classical definition of modularity is retrieved when the\n" + " resolution 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 matrix as a list of lists.\n" }, {"coreness", (PyCFunction) igraphmodule_Graph_coreness, METH_VARARGS | METH_KEYWORDS, - "coreness(mode=ALL)\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" + "B{Reference}: Vladimir Batagelj, Matjaz Zaversnik: An M{O(m)} Algorithm\n" + "for Core Decomposition of Networks.\n\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" - " for Core Decomposition of Networks.}"}, + }, {"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" "community, and communities are merged one by one. In every step, the two\n" "communities being merged are the ones which result in the maximal increase\n" "in modularity.\n\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" + "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" + "B{Reference}: A. Clauset, M. E. J. Newman and C. Moore: Finding community\n" + "structure in very large networks. I{Phys Rev E} 70, 066111 (2004).\n\n" "@param weights: name of an edge attribute or a list containing\n" " edge weights\n" "@return: a tuple with the following elements:\n" " 1. The list of merges\n" " 2. The modularity scores before each merge\n" "\n" - "@newfield ref: Reference\n" - "@ref: A. Clauset, M. E. J. Newman and C. Moore: I{Finding community\n" - " structure in very large networks.} Phys Rev E 70, 066111 (2004).\n" "@see: modularity()\n" }, + {"community_fluid_communities", + (PyCFunction) igraphmodule_Graph_community_fluid_communities, + METH_VARARGS | METH_KEYWORDS, + "community_fluid_communities(no_of_communities)\n--\n\n" + "Community detection based on fluids interacting on the graph.\n\n" + "The algorithm is based on the simple idea of several fluids interacting\n" + "in a non-homogeneous environment (the graph topology), expanding and\n" + "contracting based on their interaction and density. Weighted graphs are\n" + "not supported.\n\n" + "B{Reference}\n\n" + " - Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive,\n" + " Scalable and Diverse Community Detection Algorithm. In: Complex Networks\n" + " & Their Applications VI: Proceedings of Complex Networks 2017 (The Sixth\n" + " International Conference on Complex Networks and Their Applications),\n" + " Springer, vol 689, p 229. https://doi.org/10.1007/978-3-319-72150-7_19\n\n" + "@param no_of_communities: The number of communities to be found. Must be\n" + " greater than 0 and fewer than number of vertices in the graph.\n" + "@return: a list with the community membership of each vertex.\n" + "@note: The graph must be simple and connected. Edge directions will be\n" + " ignored if the graph is directed.\n" + "@note: Time complexity: O(|E|)\n", + }, {"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" + "See U{https://www.mapequation.org} for a visualization of the algorithm\n" "or one of the references provided below.\n\n" + "B{References}\n\n" + " - M. Rosvall and C. T. Bergstrom: I{Maps of information flow reveal\n" + " community structure in complex networks}. PNAS 105, 1118 (2008).\n" + " U{https://arxiv.org/abs/0707.0609}\n" + " - M. Rosvall, D. Axelsson and C. T. Bergstrom: I{The map equation}.\n" + " I{Eur Phys J Special Topics} 178, 13 (2009).\n" + " U{https://arxiv.org/abs/0906.1405}\n" + "\n" "@param edge_weights: name of an edge attribute or a list containing\n" " edge weights.\n" "@param vertex_weights: name of an vertex attribute or a list containing\n" @@ -15281,18 +18836,11 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param trials: the number of attempts to partition the network.\n" "@return: the calculated membership vector and the corresponding\n" " codelength in a tuple.\n" - "\n" - "@newfield ref: Reference\n" - "@ref: M. Rosvall and C. T. Bergstrom: I{Maps of information flow reveal\n" - " community structure in complex networks}. PNAS 105, 1118 (2008).\n" - " U{http://arxiv.org/abs/0707.0609}\n" - "@ref: M. Rosvall, D. Axelsson and C. T. Bergstrom: I{The map equation}.\n" - " Eur Phys J Special Topics 178, 13 (2009). U{http://arxiv.org/abs/0906.1405}\n" }, {"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, variant=\"dominance\")\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" @@ -15303,7 +18851,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Note that since ties are broken randomly, there is no guarantee that\n" "the algorithm returns the same community structure after each run.\n" "In fact, they frequently differ. See the paper of Raghavan et al\n" - "on how to come up with an aggregated community structure.\n\n" + "on how to come up with an aggregated community structure.\n" + "\n" + "B{Reference}: Raghavan, U.N. and Albert, R. and Kumara, S. Near linear\n" + "time algorithm to detect community structures in large-scale\n" + "networks. I{Phys Rev E} 76:036106, 2007.\n" + "U{https://arxiv.org/abs/0709.2938}.\n" + "\n" "@param weights: name of an edge attribute or a list containing\n" " edge weights\n" "@param initial: name of a vertex attribute or a list containing\n" @@ -15316,21 +18870,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " It only makes sense if initial labels are also given. Unlabeled\n" " vertices cannot be fixed. Note that vertex attribute names are not\n" " accepted here.\n" + "@param variant: the variant of the algorithm to use: C{\"dominance\"}, \n" + " C{\"retention\"} or C{\"fast\"}. See the documentation of the C core\n" + " of igraph for details.\n" "@return: the resulting membership vector\n" - "\n" - "@newfield ref: Reference\n" - "@ref: Raghavan, U.N. and Albert, R. and Kumara, S. Near linear\n" - " time algorithm to detect community structures in large-scale\n" - " networks. Phys Rev E 76:036106, 2007. U{http://arxiv.org/abs/0709.2938}.\n" }, {"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" - "@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" + "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" + "B{Reference}: MEJ Newman: Finding community structure in networks using the\n" + "eigenvectors of matrices, arXiv:physics/0605087\n\n" "@param n: the desired number of communities. If negative, the algorithm\n" " tries to do as many splits as possible. Note that the algorithm\n" " won't split a community further if the signs of the leading eigenvector\n" @@ -15341,15 +18895,12 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param weights: name of an edge attribute or a list containing\n" " edge weights\n" "@return: a tuple where the first element is the membership vector of the\n" - " clustering and the second element is the merge matrix.\n\n" - "@newfield ref: Reference\n" - "@ref: MEJ Newman: Finding community structure in networks using the\n" - " eigenvectors of matrices, arXiv:physics/0605087\n" + " clustering and the second element is the merge matrix.\n" }, {"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=False, 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" @@ -15359,29 +18910,33 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "community in the original graph is shrank to a single vertex (while\n" "keeping the total weight of the incident edges) and the process continues\n" "on the next level. The algorithm stops when it is not possible to increase\n" - "the modularity any more after shrinking the communities to vertices.\n\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" + "the modularity any more after shrinking the communities to vertices.\n" + "\n" + "B{Reference}: VD Blondel, J-L Guillaume, R Lambiotte and E Lefebvre: Fast\n" + "unfolding of community hierarchies in large networks. J Stat Mech\n" + "P10008 (2008), U{https://arxiv.org/abs/0803.0476}\n" + "\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" "@param weights: name of an edge attribute or a list containing\n" " edge weights\n" "@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" " modularities (if C{return_levels} is C{True}).\n" - "\n" - "@newfield ref: Reference\n" - "@ref: VD Blondel, J-L Guillaume, R Lambiotte and E Lefebvre: Fast\n" - " unfolding of community hierarchies in large networks. J Stat Mech\n" - " P10008 (2008), http://arxiv.org/abs/0803.0476\n" "@see: modularity()\n" }, {"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" @@ -15390,12 +18945,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "is typically high. So we gradually remove the edge with the highest\n" "betweenness from the network and recalculate edge betweenness after every\n" "removal, as long as all edges are removed.\n\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" + "When edge weights are given, the ratio of betweenness and weight values\n" + "is used to choose which edges to remove first, as described in\n" + "M. E. J. Newman: Analysis of Weighted Networks (2004), Section C.\n" + "Thus, edges with large weights are treated as strong connections,\n" + "and will be removed later than weak connections having similar betweenness.\n" + "Weights are also used for calculating modularity.\n\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" "@param directed: whether to take into account the directedness of the edges\n" " when we calculate the betweenness values.\n" "@param weights: name of an edge attribute or a list containing\n" - " edge weights.\n\n" + " edge weights. Higher weights indicate stronger connections.\n\n" "@return: a tuple with the merge matrix that describes the dendrogram\n" " and the modularity scores before each merge. The modularity scores\n" " use the weights if the original graph was weighted.\n" @@ -15403,7 +18964,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" @@ -15422,7 +18983,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" @@ -15448,7 +19009,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" @@ -15457,24 +19018,89 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " original implementation is used.\n" "@return: the community membership vector.\n" }, + {"community_voronoi", + (PyCFunction) igraphmodule_Graph_community_voronoi, + METH_VARARGS | METH_KEYWORDS, + "community_voronoi(lengths=None, weights=None, mode=\"out\", radius=None)\n--\n\n" + "Finds communities using Voronoi partitioning.\n\n" + "This function finds communities using a Voronoi partitioning of vertices based\n" + "on the given edge lengths divided by the edge clustering coefficient.\n" + "The generator vertices are chosen to be those with the largest local relative\n" + "density within a radius, with the local relative density of a vertex defined as\n" + "C{s * m / (m + k)}, where s is the strength of the vertex, m is the number of\n" + "edges within the vertex's first order neighborhood, while k is the number of\n" + "edges with only one endpoint within this neighborhood.\n\n" + "B{References}\n\n" + " - Deritei et al., Community detection by graph Voronoi diagrams,\n" + " New Journal of Physics 16, 063007 (2014)\n" + " U{https://doi.org/10.1088/1367-2630/16/6/063007}\n" + " - Molnár et al., Community Detection in Directed Weighted Networks\n" + " using Voronoi Partitioning, Scientific Reports 14, 8124 (2024)\n" + " U{https://doi.org/10.1038/s41598-024-58624-4}\n\n" + "@param lengths: edge lengths, or C{None} to consider all edges as having\n" + " unit length. Voronoi partitioning will use edge lengths equal to\n" + " lengths / ECC where ECC is the edge clustering coefficient.\n" + "@param weights: edge weights, or C{None} to consider all edges as having\n" + " unit weight. Weights are used when selecting generator points, as well\n" + " as for computing modularity.\n" + "@param mode: if C{\"out\"} (the default), distances from generator points to all other\n" + " nodes are considered. If C{\"in\"}, the reverse distances are used.\n" + " If C{\"all\"}, edge directions are ignored. This parameter is ignored\n" + " for undirected graphs.\n" + "@param radius: the radius/resolution to use when selecting generator points.\n" + " The larger this value, the fewer partitions there will be. Pass C{None}\n" + " to automatically select the radius that maximizes modularity.\n" + "@return: a tuple containing the membership vector, generator vertices, and\n" + " modularity score: (membership, generators, modularity).\n" + "@rtype: tuple\n" + }, + {"community_leiden", + (PyCFunction) igraphmodule_Graph_community_leiden, + METH_VARARGS | METH_KEYWORDS, + "community_leiden(edge_weights=None, node_weights=None, " + "resolution=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: 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 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. You can\n" + " also set this parameter to a negative number, which means that the\n" + " algorithm will be iterated until an iteration does not change the\n" + " current membership vector any more.\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" "in the same community. The method provides a dendrogram.\n\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" + "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" + "B{Reference}: Pascal Pons, Matthieu Latapy: Computing communities in large\n" + "networks using random walks, U{https://arxiv.org/abs/physics/0512106}.\n\n" "@param weights: name of an edge attribute or a list containing\n" " edge weights\n" "@return: a tuple with the list of merges and the modularity scores corresponding\n" " to each merge\n" "\n" - "@newfield ref: Reference\n" - "@ref: Pascal Pons, Matthieu Latapy: Computing communities in large networks\n" - " using random walks, U{http://arxiv.org/abs/physics/0512106}.\n" "@see: modularity()\n" }, @@ -15483,20 +19109,20 @@ 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" + "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" + "_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" }, /****************/ @@ -15504,50 +19130,71 @@ 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\", weights=None, return_type=\"vertices\")\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" - "@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" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" + "@param return_type: what to return. It can be C{\"vertices\"} (default),\n" + " then the function returns a list of the vertex ids visited; C{\"edges\"},\n" + " then the function returns a list of edge ids visited; or C{\"both\"},\n" + " then the function return a dictionary with keys C{\"vertices\"} and\n" + " C{\"edges\"}.\n" "@return: a random walk that starts from the given vertex and has at most\n" - " the given length (shorter if the random walk got stuck)\n" + " the given length (shorter if the random walk got stuck).\n" + }, + + /**********************/ + /* SPATIAL GRAPHS */ + /**********************/ + {"Nearest_Neighbor_Graph", (PyCFunction)igraphmodule_Graph_Nearest_Neighbor_Graph, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Nearest_Neighbor_Graph(points, k=1, r=-1, metric=\"euclidean\", directed=False)\n--\n\n" + "Constructs a k nearest neighbor graph of a give point set. Each point is\n" + "connected to at most k spatial neighbors within a radius of 1.\n\n" + "@param points: coordinates of the points to use, in an arbitrary number of dimensions\n" + "@param k: at most how many neighbors to connect to. Pass a negative value to ignore\n" + "@param r: only neighbors within this radius are considered. Pass a negative value to ignore\n" + "@param metric: the metric to use. C{\"euclidean\"} and C{\"manhattan\"} are supported.\n" + "@param directed: whethe to create directed edges.\n" + "@return: the nearest neighbor graph.\n" }, /**********************/ /* INTERNAL FUNCTIONS */ /**********************/ -#ifdef IGRAPH_PYTHON3 + {"__graph_as_capsule", (PyCFunction) igraphmodule_Graph___graph_as_capsule__, METH_VARARGS | METH_KEYWORDS, - "__graph_as_capsule()\n\n" + "__graph_as_capsule()\n--\n\n" "Returns the igraph graph encapsulated by the Python object as\n" "a PyCapsule\n\n." "A PyCapsule 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"}, -#else - {"__graph_as_cobject", - (PyCFunction) igraphmodule_Graph___graph_as_cobject__, - METH_VARARGS | METH_KEYWORDS, - "__graph_as_cobject()\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 + + {"__invalidate_cache", + (PyCFunction) igraphmodule_Graph___invalidate_cache__, + METH_NOARGS, + "__invalidate_cache()\n--\n\n" + "Invalidates the internal cache of the low-level C graph object that\n" + "the Python object wraps. This function should not be used directly\n" + "by igraph users, but it may be useful for benchmarking or debugging\n" + "purposes.", + }, {"_raw_pointer", (PyCFunction) igraphmodule_Graph__raw_pointer, METH_NOARGS, - "_raw_pointer()\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" @@ -15557,138 +19204,59 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"__register_destructor", (PyCFunction) igraphmodule_Graph___register_destructor__, METH_VARARGS | METH_KEYWORDS, - "__register_destructor(destructor)\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."}, {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) +/** + * \ingroup python_interface_graph + * Member table for the \c igraph._igraph.GraphBase object */ -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 - */ -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 */ - 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 */ - (binaryfunc) igraphmodule_Graph_intersection, /*nb_and */ - 0, /*nb_xor */ - (binaryfunc) igraphmodule_Graph_union, /*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 */ - 0, /*nb_inplace_rshift */ - 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 +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.Graph", /* 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" - "@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 */ - 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 */ - igraphmodule_Graph_new, /* tp_new */ - 0, /* tp_free */ -}; + "Don't use it directly, use L{igraph.Graph} instead.\n" /* tp_doc */ +); + +int igraphmodule_Graph_register_type() { + PyType_Slot slots[] = { + { Py_tp_new, igraphmodule_Graph_new }, + { 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 } + }; -#undef CREATE_GRAPH + 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 new file mode 100644 index 000000000..f6c775c88 --- /dev/null +++ b/src/_igraph/graphobject.h @@ -0,0 +1,62 @@ +/* -*- mode: C -*- */ +/* + IGraph library. + Copyright (C) 2006-2023 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 IGRAPHMODULE_GRAPHOBJECT_H +#define IGRAPHMODULE_GRAPHOBJECT_H + +#include "preamble.h" + +#include +#include "structmember.h" +#include "common.h" + +extern PyTypeObject* igraphmodule_GraphType; + +/** + * \ingroup python_interface + * \brief A structure containing all the fields required to access an igraph from Python + */ +typedef struct +{ + PyObject_HEAD + // The graph object + igraph_t g; + // Python object to be called upon destruction + PyObject* destructor; + // Python object representing the sequence of vertices + PyObject* vseq; + // Python object representing the sequence of edges + PyObject* eseq; + // Python object of the weak reference list + PyObject* weakreflist; +} igraphmodule_GraphObject; + +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_attributes(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)); +PyObject* igraphmodule_Graph_vertex_attributes(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)); +PyObject* igraphmodule_Graph_edge_attributes(igraphmodule_GraphObject* self, PyObject* Py_UNUSED(_null)); + +#endif diff --git a/src/igraphmodule.c b/src/_igraph/igraphmodule.c similarity index 52% rename from src/igraphmodule.c rename to src/_igraph/igraphmodule.c index c4384d13e..cd57da92a 100644 --- a/src/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -1,77 +1,77 @@ /* vim:set ts=2 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include -#include +#include "preamble.h" + #include #include "arpackobject.h" #include "attributes.h" #include "bfsiter.h" +#include "dfsiter.h" #include "common.h" #include "convert.h" #include "edgeobject.h" #include "edgeseqobject.h" #include "error.h" #include "graphobject.h" -#include "py2compat.h" +#include "pyhelpers.h" #include "random.h" #include "vertexobject.h" #include "vertexseqobject.h" +#include "operators.h" #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 - * + * * These functions provide a way to access \a igraph functions from Python. * It should be of interest of \a igraph developers only. Classes, functions * and methods exposed to Python are still to be documented. Until it is done, * just type the following to get help about \a igraph functions in Python * (assuming you have \c igraph.so somewhere in your Python library path): - * + * * \verbatim import igraph help(igraph) help(igraph.Graph) \endverbatim - * + * * Most of the functions provided here share the same calling conventions * (which are determined by the Python/C API). Since the role of the * arguments are the same across many functions, I won't explain them * everywhere, just give a quick overview of the common argument names here. - * + * * \param self the Python igraph.Graph object the method is working on * \param args pointer to the Python tuple containing the arguments * \param kwds pointer to the Python hash containing the keyword parameters * \param type the type object of a Python igraph.Graph object. Used usually * in constructors and class methods. - * + * * Any arguments not documented here should be mentioned at the documentation * of the appropriate method. - * + * * The functions which implement a Python method always return a pointer to * a \c PyObject. According to Python conventions, this is \c NULL if and * only if an exception was thrown by the method (or any of the functions @@ -80,7 +80,7 @@ help(igraph.Graph) * returning a \c NULL value, because this is the same for every such method. * The conclusion is that a method can return \c NULL even if I don't state * it explicitly. - * + * * Also please take into consideration that I'm documenting the C calls * with the abovementioned parameters here, and \em not the Python methods * which are presented to the user using the Python interface of \a igraph. @@ -89,11 +89,11 @@ help(igraph.Graph) * or use \c pydoc to generate a formatted version. * * \section weakrefs The usage of weak references in the Python interface - * + * * Many classes implemented in the Python interface (e.g. VertexSeq, Vertex...) * use weak references to keep track of the graph they are referencing to. * The use of weak references is twofold: - * + * * -# If we assign a VertexSeq or a Vertex of a given graph to a local * variable and then destroy the graph, real references keep the graph * alive and do not return the memory back to Python. @@ -117,7 +117,7 @@ help(igraph.Graph) /** * Whether the module was initialized already */ -static igraph_bool_t igraphmodule_initialized = 0; +static igraph_bool_t igraphmodule_initialized = false; /** * Module-specific global variables @@ -130,7 +130,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,36 +141,31 @@ 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()) { - IGRAPH_FINALLY_FREE(); - return IGRAPH_INTERRUPTED; - } - return IGRAPH_SUCCESS; +static igraph_bool_t igraphmodule_igraph_interrupt_hook() { + return PyErr_CheckSignals(); } -int igraphmodule_igraph_progress_hook(const char* message, igraph_real_t percent, +igraph_error_t 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; + } } } - + return IGRAPH_SUCCESS; } -int igraphmodule_igraph_status_hook(const char* message, void*data) { +igraph_error_t igraphmodule_igraph_status_hook(const char* message, void*data) { PyObject* status_handler = GETSTATE(0)->status_handler; if (status_handler) { @@ -184,7 +178,7 @@ int igraphmodule_igraph_status_hook(const char* message, void*data) { return IGRAPH_INTERRUPTED; } } - + return IGRAPH_SUCCESS; } @@ -202,7 +196,8 @@ PyObject* igraphmodule_set_progress_handler(PyObject* self, PyObject* o) { Py_XDECREF(progress_handler); if (o == Py_None) - o = 0; + o = NULL; + Py_XINCREF(o); GETSTATE(self)->progress_handler=o; @@ -222,106 +217,158 @@ PyObject* igraphmodule_set_status_handler(PyObject* self, PyObject* o) { Py_RETURN_NONE; Py_XDECREF(status_handler); - if (o == Py_None) - o = 0; - Py_INCREF(o); + if (o == Py_None) { + o = NULL; + } + + Py_XINCREF(o); GETSTATE(self)->status_handler = o; Py_RETURN_NONE; } +PyObject* igraphmodule_align_layout(PyObject* self, PyObject* args, PyObject* kwds) { + static char* kwlist[] = {"graph", "layout", NULL}; + PyObject *graph_o, *layout_o; + PyObject *res; + igraph_t *graph; + igraph_matrix_t layout; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &graph_o, &layout_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_igraph_t(graph_o, &graph)) { + return NULL; + } + + if (igraphmodule_PyObject_to_matrix_t(layout_o, &layout, "layout")) { + return NULL; + } + + if (igraph_layout_align(graph, &layout)) { + igraphmodule_handle_igraph_error(); + igraph_matrix_destroy(&layout); + return NULL; + } + + res = igraphmodule_matrix_t_to_PyList(&layout, IGRAPHMODULE_TYPE_FLOAT); + + igraph_matrix_destroy(&layout); + + return res; +} + 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 = 0, *o2 = 0, *o1_float, *o2_float, *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; - + 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) - PyErr_Warn(PyExc_Warning, "vertex with more than 2 coordinates found, considering only the first 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) { + 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"); 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; } + } else { + PyErr_SetString(PyExc_TypeError, "convex_hull() must receive a list of indexable sequences"); + igraph_matrix_destroy(&mtrx); + 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)=(igraph_real_t)PyFloat_AsDouble(o1); - MATRIX(mtrx, i, 1)=(igraph_real_t)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)) { - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_int_init(&result, 0)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&mtrx); return NULL; } - if (igraph_convex_hull(&mtrx, &result, 0)) { + if (igraph_convex_hull_2d(&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(); igraph_matrix_destroy(&mtrx); return NULL; } - if (igraph_convex_hull(&mtrx, 0, &resmat)) { + if (igraph_convex_hull_2d(&mtrx, 0, &resmat)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&mtrx); 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); } - + igraph_matrix_destroy(&mtrx); return o; @@ -332,42 +379,46 @@ 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; - long int nodes, steps; + igraph_matrix_int_t merges; + igraph_vector_int_t result, csize, *csize_p = 0; + Py_ssize_t nodes, steps; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!ll|O", kwlist, - &PyList_Type, &merges_o, &nodes, &steps, &return_csize)) return NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "Onn|O", kwlist, + &merges_o, &nodes, &steps, &return_csize)) return NULL; - if (igraphmodule_PyList_to_matrix_t(merges_o, &merges)) return NULL; + if (igraphmodule_PyObject_to_matrix_int_t_with_minimum_column_count(merges_o, &merges, 2, "merges")) { + return NULL; + } + + CHECK_SSIZE_T_RANGE(nodes, "number of nodes"); + CHECK_SSIZE_T_RANGE(steps, "number of steps"); - 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)) { + if (igraph_community_to_membership(&merges, nodes, 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; @@ -381,34 +432,39 @@ 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; 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_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); - return PyFloat_FromDouble((double)result); + igraph_vector_int_destroy(&comm1); + igraph_vector_int_destroy(&comm2); + + return igraphmodule_real_t_to_PyObject(result, IGRAPHMODULE_TYPE_FLOAT); } @@ -416,7 +472,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, @@ -425,25 +481,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_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); + 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; @@ -456,7 +512,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, @@ -465,25 +521,108 @@ 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_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); + 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; + else + Py_RETURN_FALSE; +} + + +static PyObject* igraphmodule_i_is_graphical_or_bigraphical( + PyObject *self, PyObject *args, PyObject *kwds, igraph_bool_t is_bigraphical +); + +PyObject* igraphmodule_is_graphical(PyObject *self, PyObject *args, PyObject *kwds) { + return igraphmodule_i_is_graphical_or_bigraphical(self, args, kwds, /* is_bigraphical = */ false); +} + +PyObject* igraphmodule_is_bigraphical(PyObject *self, PyObject *args, PyObject *kwds) { + return igraphmodule_i_is_graphical_or_bigraphical(self, args, kwds, /* is_bigraphical = */ true); +} + +static PyObject* igraphmodule_i_is_graphical_or_bigraphical( + PyObject *self, PyObject *args, PyObject *kwds, igraph_bool_t is_bigraphical +) { + static char* kwlist_graphical[] = { "out_deg", "in_deg", "loops", "multiple", NULL }; + static char* kwlist_bigraphical[] = { "degrees1", "degrees2", "multiple", NULL }; + PyObject *out_deg_o = 0, *in_deg_o = 0; + PyObject *loops = Py_False, *multiple = Py_False; + igraph_vector_int_t out_deg, in_deg; + igraph_bool_t is_directed, result; + int allowed_edge_types; + igraph_error_t retval; + + if (is_bigraphical) { + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "OO|O", kwlist_bigraphical, + &out_deg_o, &in_deg_o, &multiple + )) { + return NULL; + } + } else { + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "O|OOO", kwlist_graphical, + &out_deg_o, &in_deg_o, &loops, &multiple + )) { + return NULL; + } + } + + is_directed = (in_deg_o != 0 && in_deg_o != Py_None) || is_bigraphical; + + if (igraphmodule_PyObject_to_vector_int_t(out_deg_o, &out_deg)) + return NULL; + + if (is_directed && igraphmodule_PyObject_to_vector_int_t(in_deg_o, &in_deg)) { + igraph_vector_int_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; + } + + retval = is_bigraphical + ? igraph_is_bigraphical(&out_deg, is_directed ? &in_deg : 0, allowed_edge_types, &result) + : igraph_is_graphical(&out_deg, is_directed ? &in_deg : 0, allowed_edge_types, &result); + + if (retval) { + igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&out_deg); + if (is_directed) { + igraph_vector_int_destroy(&in_deg); + } + return NULL; + } + + igraph_vector_int_destroy(&out_deg); + if (is_directed) { + igraph_vector_int_destroy(&in_deg); + } if (result) Py_RETURN_TRUE; @@ -493,14 +632,15 @@ PyObject* igraphmodule_is_graphical_degree_sequence(PyObject *self, PyObject* igraphmodule_power_law_fit(PyObject *self, PyObject *args, PyObject *kwds) { - static char* kwlist[] = { "data", "xmin", "force_continuous", NULL }; + static char* kwlist[] = { "data", "xmin", "force_continuous", "p_precision", NULL }; PyObject *data_o, *force_continuous_o = Py_False; igraph_vector_t data; igraph_plfit_result_t result; - double xmin = -1; + double xmin = -1, p_precision = 0.01; + igraph_real_t p; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|dO", kwlist, &data_o, - &xmin, &force_continuous_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|dOd", kwlist, &data_o, + &xmin, &force_continuous_o, &p_precision)) return NULL; if (igraphmodule_PyObject_float_to_vector_t(data_o, &data)) @@ -512,62 +652,183 @@ PyObject* igraphmodule_power_law_fit(PyObject *self, PyObject *args, PyObject *k return NULL; } + if (igraph_plfit_result_calculate_p_value(&result, &p, p_precision)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&data); + return NULL; + } + igraph_vector_destroy(&data); return Py_BuildValue("Oddddd", result.continuous ? Py_True : Py_False, - result.alpha, result.xmin, result.L, result.D, result.p); + result.alpha, result.xmin, result.L, result.D, (double) p); } 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_integer_t distance12, distance21; + igraph_vector_int_t comm1, comm2; + igraph_int_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_int_destroy(&comm1); + igraph_vector_int_destroy(&comm2); + + /* sizeof(Py_ssize_t) is most likely the same as sizeof(igraph_int_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_graph + * \brief Compute weights for Uniform Manifold Approximation and Projection (UMAP) + * \return the weights given that graph + * \sa igraph_umap_compute_weights + */ +PyObject *igraphmodule_umap_compute_weights( + PyObject *self, PyObject * args, PyObject * kwds) +{ + static char *kwlist[] = { "graph", "dist", NULL }; + igraph_vector_t *dist = 0; + igraph_vector_t weights; + PyObject *dist_o = Py_None, *graph_o = Py_None; + PyObject *result_o; + igraphmodule_GraphObject * graph; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O", kwlist, igraphmodule_GraphType, &graph_o, &dist_o)) return NULL; + + /* Initialize distances */ + if (dist_o != Py_None) { + dist = (igraph_vector_t*)malloc(sizeof(igraph_vector_t)); + if (!dist) { + PyErr_NoMemory(); + return NULL; + } + if (igraphmodule_PyObject_to_vector_t(dist_o, dist, 0)) { + free(dist); + return NULL; + } } - igraph_vector_destroy(&comm1); - igraph_vector_destroy(&comm2); - return Py_BuildValue("ll", (long)distance12, (long)distance21); + /* Extract graph from Python object */ + graph = (igraphmodule_GraphObject*)graph_o; + + /* Initialize weights */ + if (igraph_vector_init(&weights, 0)) { + igraph_vector_destroy(dist); free(dist); + PyErr_NoMemory(); + return NULL; + } + + /* Call the function */ + if (igraph_layout_umap_compute_weights(&graph->g, dist, &weights)) { + igraph_vector_destroy(&weights); + igraph_vector_destroy(dist); free(dist); + PyErr_NoMemory(); + return NULL; + } + igraph_vector_destroy(dist); free(dist); + + /* Convert output to Python list */ + result_o = igraphmodule_vector_t_to_PyList(&weights, IGRAPHMODULE_TYPE_FLOAT); + igraph_vector_destroy(&weights); + return (PyObject *) result_o; +} + + +#define LOCALE_CAPSULE_TYPE "igraph._igraph.locale_capsule" + +void igraphmodule__destroy_locale_capsule(PyObject *capsule) { + igraph_safelocale_t* loc = (igraph_safelocale_t*) PyCapsule_GetPointer(capsule, LOCALE_CAPSULE_TYPE); + if (loc) { + PyMem_Free(loc); + } +} + +PyObject* igraphmodule__enter_safelocale(PyObject* self, PyObject* Py_UNUSED(_null)) { + igraph_safelocale_t* loc; + PyObject* capsule; + + loc = PyMem_Malloc(sizeof(loc)); + if (loc == NULL) { + PyErr_NoMemory(); + return NULL; + } + + capsule = PyCapsule_New(loc, LOCALE_CAPSULE_TYPE, igraphmodule__destroy_locale_capsule); + if (capsule == NULL) { + return NULL; + } + + if (igraph_enter_safelocale(loc)) { + Py_DECREF(capsule); + igraphmodule_handle_igraph_error(); + } + + return capsule; +} + +PyObject* igraphmodule__exit_safelocale(PyObject *self, PyObject *capsule) { + igraph_safelocale_t* loc; + + if (!PyCapsule_IsValid(capsule, LOCALE_CAPSULE_TYPE)) { + PyErr_SetString(PyExc_TypeError, "expected locale capsule"); + return NULL; + } + + loc = (igraph_safelocale_t*) PyCapsule_GetPointer(capsule, LOCALE_CAPSULE_TYPE); + if (loc != NULL) { + igraph_exit_safelocale(loc); + } + + Py_RETURN_NONE; } /** \ingroup python_interface * \brief Method table for the igraph Python module */ -static PyMethodDef igraphmodule_methods[] = +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, p_precision=0.01)\n--\n\n" + }, + {"_align_layout", (PyCFunction)igraphmodule_align_layout, + METH_VARARGS | METH_KEYWORDS, + "_align_layout(graph, layout)\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" @@ -579,7 +840,8 @@ 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" "this function may return C{True} for degree sequences that can be realized\n" @@ -594,13 +856,40 @@ 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_bigraphical", (PyCFunction)igraphmodule_is_bigraphical, + METH_VARARGS | METH_KEYWORDS, + "is_bigraphical(degrees1, degrees2, multiple=False)\n--\n\n" + "Returns whether two sequences of integers can be the degree sequences of a\n" + "bipartite graph.\n\n" + "The bipartite graph may or may not have multiple edges, depending\n" + "on the allowed edge types in the remaining arguments.\n\n" + "@param degrees1: the list of degrees in the first partition.\n" + "@param degrees2: the list of degrees in the second partition.\n" + "@param multiple: whether multiple edges are allowed.\n" + "@return: C{True} if there exists some bipartite graph that can realize the\n" + " given degree sequences with or without multiple edges, 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\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" + "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" "this function will return C{False} for degree sequences that cannot be realized\n" @@ -611,10 +900,38 @@ 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" + }, + {"umap_compute_weights", (PyCFunction)igraphmodule_umap_compute_weights, + METH_VARARGS | METH_KEYWORDS, + "umap_compute_weights(graph, dist)\n--\n\n" + "Compute undirected UMAP weights from directed distance graph.\n" + "UMAP is a layout algorithm that usually takes as input a directed\n" + "distance graph, for instance a k nearest neighbor graph based on Euclidean\n" + "distance between points in a vector space. The graph is directed\n" + "because vertex v1 might consider vertex v2 a close neighbor, but v2\n" + "itself might have many neighbors that are closer than v1.\n" + "This function computes the symmetrized weights from the distance graph\n" + "using union as the symmetry operator. In simple terms, if either vertex\n" + "considers the other a close neighbor, they will be treated as close\n" + "neighbors.\n\n" + "This function can be used as a separate preprocessing step to\n" + "Graph.layout_umap(). For efficiency reasons, the returned weights have the\n" + "same length as the input distances, however because of the symmetryzation\n" + "some information is lost. Therefore, the weight of one of the edges is set\n" + "to zero whenever edges in opposite directions are found in the input\n" + "distance graph. You can pipe the output of this function directly into\n" + "Graph.layout_umap() as follows:\n" + "C{weights = igraph.umap_compute_weights(graph, dist)}\n" + "C{layout = graph.layout_umap(weights=weights)}\n\n" + "@param graph: directed graph to compute weights for.\n" + "@param dist: distances associated with the graph edges.\n" + "@return: Symmetrized weights associated with each edge. If the distance\n" + " graph has both directed edges between a pair of vertices, one of the\n" + " returned weights will be set to zero.\n\n" + "@see: Graph.layout_umap()\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" @@ -622,21 +939,24 @@ 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" " Each of them must be callable and their signature and behaviour\n" " must be identical to C{random.random}, C{random.randint} and\n" - " C{random.gauss}. By default, igraph uses the C{random} module for\n" - " random number generation, but you can supply your alternative\n" - " implementation here. If the given generator is C{None}, igraph\n" - " reverts to the default Mersenne twister generator implemented in the\n" - " C layer, which might be slightly faster than calling back to Python\n" - " for random numbers, but you cannot set its seed or save its state.\n" + " C{random.gauss}. Optionally, the object can provide a function named\n" + " C{getrandbits} with a signature identical to C{randpm.getrandbits}\n" + " that provides a given number of random bits on demand. By default,\n" + " igraph uses the C{random} module for random number generation, but\n" + " you can supply your alternative implementation here. If the given\n" + " generator is C{None}, igraph reverts to the default PCG32 generator\n" + " implemented in the C layer, which might be slightly faster than\n" + " calling back to Python for random numbers, but you cannot set its\n" + " 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" @@ -648,21 +968,42 @@ 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)\n--\n\n" + }, + {"_union", (PyCFunction)igraphmodule__union, + METH_VARARGS | METH_KEYWORDS, + "_union(graphs, edgemaps)\n--\n\n" + }, + {"_intersection", (PyCFunction)igraphmodule__intersection, + METH_VARARGS | METH_KEYWORDS, + "_intersection(graphs, edgemaps)\n--\n\n" + }, + {"_enter_safelocale", (PyCFunction)igraphmodule__enter_safelocale, + METH_NOARGS, + "_enter_safelocale()\n--\n\n" + "Helper function for the L{safe_locale()} context manager. Do not use\n" + "directly in your own code." + }, + {"_exit_safelocale", (PyCFunction)igraphmodule__exit_safelocale, + METH_O, + "_exit_safelocale(locale)\n--\n\n" + "Helper function for the L{safe_locale()} context manager. Do not use\n" + "directly in your own code." }, {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" + "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 */ @@ -674,7 +1015,6 @@ static struct PyModuleDef moduledef = { igraphmodule_clear, /* m_clear */ 0 /* m_free */ }; -#endif /****************** Exported API functions *******************/ @@ -721,20 +1061,16 @@ 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]; PyObject *c_api_object; + igraph_error_t retval; + + /* Prevent linking 64-bit igraph to 32-bit Python */ + PY_IGRAPH_ASSERT_AT_BUILD_TIME(sizeof(igraph_int_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. */ @@ -744,36 +1080,35 @@ extern PyObject* igraphmodule_arpack_options_default; INITERROR; } - /* Initialize VertexSeq, EdgeSeq */ - if (PyType_Ready(&igraphmodule_VertexSeqType) < 0) - INITERROR; - 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) + /* Initialize the igraph library */ + retval = igraph_setup(); + if (retval != IGRAPH_SUCCESS) { + PyErr_Format(PyExc_RuntimeError, "Failed to initialize the C core of " + "the igraph library, code: %d", retval); INITERROR; + } - /* Initialize Graph, BFSIter, ARPACKOptions etc */ - if (PyType_Ready(&igraphmodule_GraphType) < 0) + /* Run basic initialization of the pyhelpers.c module */ + if (igraphmodule_helpers_init()) { INITERROR; - if (PyType_Ready(&igraphmodule_BFSIterType) < 0) - INITERROR; - if (PyType_Ready(&igraphmodule_ARPACKOptionsType) < 0) + } + + /* Initialize types */ + if ( + igraphmodule_ARPACKOptions_register_type() || + igraphmodule_BFSIter_register_type() || + 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; + } /* 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; @@ -781,21 +1116,25 @@ extern PyObject* igraphmodule_arpack_options_default; igraphmodule_init_rng(m); /* Add the types to the core module */ - PyModule_AddObject(m, "GraphBase", (PyObject*)&igraphmodule_GraphType); - PyModule_AddObject(m, "BFSIter", (PyObject*)&igraphmodule_BFSIterType); - 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); - PyModule_AddObject(m, "VertexSeq", (PyObject*)&igraphmodule_VertexSeqType); - + 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); + PyModule_AddObject(m, "Vertex", (PyObject*)igraphmodule_VertexType); + PyModule_AddObject(m, "VertexSeq", (PyObject*)igraphmodule_VertexSeqType); + /* Internal error exception type */ igraphmodule_InternalError = PyErr_NewException("igraph._igraph.InternalError", PyExc_Exception, NULL); 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 */ @@ -819,9 +1158,6 @@ extern PyObject* igraphmodule_arpack_options_default; PyModule_AddIntConstant(m, "GET_ADJACENCY_LOWER", IGRAPH_GET_ADJACENCY_LOWER); PyModule_AddIntConstant(m, "GET_ADJACENCY_BOTH", IGRAPH_GET_ADJACENCY_BOTH); - PyModule_AddIntConstant(m, "REWIRING_SIMPLE", IGRAPH_REWIRING_SIMPLE); - PyModule_AddIntConstant(m, "REWIRING_SIMPLE_LOOPS", IGRAPH_REWIRING_SIMPLE_LOOPS); - PyModule_AddIntConstant(m, "ADJ_DIRECTED", IGRAPH_ADJ_DIRECTED); PyModule_AddIntConstant(m, "ADJ_UNDIRECTED", IGRAPH_ADJ_UNDIRECTED); PyModule_AddIntConstant(m, "ADJ_MAX", IGRAPH_ADJ_MAX); @@ -840,11 +1176,17 @@ 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); + + PyModule_AddIntConstant(m, "INTEGER_SIZE", IGRAPH_INTEGER_SIZE); + /* More useful constants */ { const char* version; igraph_version(&version, 0, 0, 0); - PyModule_AddStringConstant(m, "__version__", version); + PyModule_AddStringConstant(m, "__igraph_version__", version); } PyModule_AddStringConstant(m, "__build_date__", __DATE__); @@ -854,7 +1196,7 @@ extern PyObject* igraphmodule_arpack_options_default; igraph_set_status_handler(igraphmodule_igraph_status_hook); igraph_set_warning_handler(igraphmodule_igraph_warning_hook); igraph_set_interruption_handler(igraphmodule_igraph_interrupt_hook); - + /* initialize attribute handlers */ igraphmodule_initialize_attribute_handler(); @@ -863,18 +1205,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/igraphmodule_api.h b/src/_igraph/igraphmodule_api.h similarity index 79% rename from src/igraphmodule_api.h rename to src/_igraph/igraphmodule_api.h index 1551ca5c8..8ab364b50 100644 --- a/src/igraphmodule_api.h +++ b/src/_igraph/igraphmodule_api.h @@ -1,22 +1,22 @@ /* -*- mode: C -*- */ /* vim:set ts=2 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @@ -56,25 +56,8 @@ extern "C" { /* Return -1 and set exception on error, 0 on success */ static int import_igraph(void) { - PyObject *c_api_object; - PyObject *module; - - module = PyImport_ImportModule("igraph._igraph"); - if (module == 0) - return -1; - - c_api_object = PyObject_GetAttrString(module, "_C_API"); - if (c_api_object == 0) { - Py_DECREF(module); - return -1; - } - - if (PyCObject_Check(c_api_object)) - PyIGraph_API = (void**)PyCObject_AsVoidPtr(c_api_object); - - Py_DECREF(c_api_object); - Py_DECREF(module); - return 0; + PyIGraph_API = (void **)PyCapsule_Import("igraph._igraph._C_API", 0); + return (PyIGraph_API != NULL) ? 0 : -1; } #endif diff --git a/src/indexing.c b/src/_igraph/indexing.c similarity index 83% rename from src/indexing.c rename to src/_igraph/indexing.c index f30729b17..8da3f600e 100644 --- a/src/indexing.c +++ b/src/_igraph/indexing.c @@ -1,22 +1,22 @@ /* vim:set ts=4 sw=2 sts=2 et: */ -/* +/* IGraph library - Python interface. Copyright (C) 2006-2011 Tamas Nepusz 5 Avenue Road, Staines, Middlesex, TW18 3AW, United Kingdom - + 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @@ -26,14 +26,13 @@ #include "error.h" #include "indexing.h" #include "platform.h" -#include "py2compat.h" #include "pyhelpers.h" /***************************************************************************/ static PyObject* igraphmodule_i_Graph_adjmatrix_indexing_get_value_for_vertex_pair( - igraph_t* graph, igraph_integer_t from, igraph_integer_t to, PyObject* values) { - igraph_integer_t eid; + igraph_t* graph, igraph_int_t from, igraph_int_t to, PyObject* values) { + igraph_int_t eid; PyObject* result; /* Retrieving a single edge */ @@ -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,19 +48,19 @@ 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); } } -static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, - igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, +static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, + igraph_int_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values); PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, PyObject* row_index, PyObject* column_index, PyObject* attr_name) { PyObject *result = 0, *values; igraph_vs_t vs1, vs2; - igraph_integer_t vid1 = -1, vid2 = -1; + igraph_int_t vid1 = -1, vid2 = -1; char* attr; if (igraphmodule_PyObject_to_vs_t(row_index, &vs1, graph, 0, &vid1)) @@ -131,23 +130,21 @@ PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, return result; } -static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, - igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, +static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, + igraph_int_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values) { - igraph_vector_t eids; - igraph_integer_t eid; + igraph_vector_int_t eids; + igraph_int_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 */ - IGRAPH_PYCHECK(igraph_vector_init(&eids, 0)); - IGRAPH_FINALLY(igraph_vector_destroy, &eids); - IGRAPH_PYCHECK(igraph_incident(graph, &eids, from, neimode)); - - n = igraph_vector_size(&eids); + IGRAPH_PYCHECK(igraph_vector_int_init(&eids, 0)); + IGRAPH_FINALLY(igraph_vector_int_destroy, &eids); + IGRAPH_PYCHECK(igraph_incident(graph, &eids, from, neimode, IGRAPH_LOOPS)); + + n = igraph_vector_int_size(&eids); result = igraphmodule_PyList_Zeroes(igraph_vcount(graph)); if (result == 0) { IGRAPH_FINALLY_FREE(); @@ -155,18 +152,19 @@ 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 - item = PyInt_FromLong(1); + } else { + item = PyLong_FromLong(1); + } Py_INCREF(item); PyList_SetItem(result, v, item); /* reference stolen here */ } IGRAPH_FINALLY_CLEAN(1); - igraph_vector_destroy(&eids); + igraph_vector_int_destroy(&eids); return result; } @@ -221,7 +219,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_AsLongLong(value) == 0); } /** @@ -229,28 +227,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; } @@ -259,19 +257,19 @@ 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); } -static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, - igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, +static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, + igraph_int_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values, PyObject* new_value, igraphmodule_i_Graph_adjmatrix_set_index_data_t* data) { PyObject *iter = 0, *item; igraph_vit_t vit; - igraph_integer_t v, v1, v2, eid; - igraph_bool_t deleting, ok = 1; + igraph_int_t v, v1, v2, eid; + igraph_bool_t deleting, ok = true; /* Check whether new_value is an iterable (and not a string). If not, * every assignment will use the same value (that is, new_value) */ @@ -312,29 +310,29 @@ 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); - ok = 0; + igraph_vector_int_clear(&data->to_delete); + ok = false; break; } } } 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); - ok = 0; + igraph_vector_int_clear(&data->to_add); + ok = false; break; } if (values != 0) { Py_INCREF(new_value); if (PyList_Append(data->to_add_values, new_value)) { Py_DECREF(new_value); - igraph_vector_clear(&data->to_add); - ok = 0; + igraph_vector_int_clear(&data->to_add); + ok = false; break; } } @@ -343,7 +341,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); } } } @@ -351,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 @@ -373,29 +372,29 @@ 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); - ok = 0; + igraph_vector_int_clear(&data->to_delete); + ok = false; break; } } } 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); - ok = 0; + igraph_vector_int_clear(&data->to_add); + ok = false; break; } if (values != 0) { Py_INCREF(new_value); if (PyList_Append(data->to_add_values, new_value)) { Py_DECREF(new_value); - igraph_vector_clear(&data->to_add); - ok = 0; + igraph_vector_int_clear(&data->to_add); + ok = false; break; } } @@ -404,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); } } } @@ -424,8 +423,8 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, PyObject *values; igraph_vs_t vs1, vs2; igraph_vit_t vit; - igraph_integer_t vid1 = -1, vid2 = -1, eid = -1; - igraph_bool_t ok = 1; + igraph_int_t vid1 = -1, vid2 = -1, eid = -1; + igraph_bool_t ok = true; igraphmodule_i_Graph_adjmatrix_set_index_data_t data; char* attr; @@ -452,7 +451,7 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, /* Deleting the edge between vid1 and vid2 if it is there */ if (igraph_delete_edges(graph, igraph_ess_1(eid))) { igraphmodule_handle_igraph_error(); - ok = 0; + ok = false; } } } else { @@ -461,7 +460,7 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, eid = igraph_ecount(graph); if (igraph_add_edge(graph, vid1, vid2)) { igraphmodule_handle_igraph_error(); - ok = 0; + ok = false; } } if (ok && values != 0) { @@ -493,13 +492,13 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, /* Complete submatrix */ if (igraph_vit_create(graph, vs1, &vit)) { igraphmodule_handle_igraph_error(); - ok = 0; + ok = false; } else { while (!IGRAPH_VIT_END(vit)) { vid1 = IGRAPH_VIT_GET(vit); if (igraphmodule_i_Graph_adjmatrix_set_index_row( graph, vid1, &vs2, IGRAPH_OUT, values, new_value, &data) == 0) { - ok = 0; + ok = false; break; } IGRAPH_VIT_NEXT(vit); @@ -512,13 +511,13 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, /* Second phase: do the deletions in one batch */ if (igraph_delete_edges(graph, igraph_ess_vector(&data.to_delete))) { igraphmodule_handle_igraph_error(); - ok = 0; + ok = false; } } 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) { @@ -527,7 +526,7 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, if (PyList_Size(values) != igraph_ecount(graph)) { PyErr_SetString(PyExc_ValueError, "hmmm, attribute value list " "length mismatch, this is most likely a bug."); - ok = 0; + ok = false; } } } @@ -541,4 +540,3 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, return ok ? 0 : -1; } - diff --git a/src/indexing.h b/src/_igraph/indexing.h similarity index 92% rename from src/indexing.h rename to src/_igraph/indexing.h index 105a64f00..d87ccdc54 100644 --- a/src/indexing.h +++ b/src/_igraph/indexing.h @@ -1,30 +1,31 @@ /* vim:set ts=4 sw=2 sts=2 et: */ -/* +/* IGraph library - Python interface. Copyright (C) 2006-2011 Tamas Nepusz 5 Avenue Road, Staines, Middlesex, TW18 3AW, United Kingdom - + 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_INDEXING_H -#define PYTHON_INDEXING_H +#ifndef IGRAPHMODULE_INDEXING_H +#define IGRAPHMODULE_INDEXING_H + +#include "preamble.h" -#include #include PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, diff --git a/src/_igraph/operators.c b/src/_igraph/operators.c new file mode 100644 index 000000000..9949e77f5 --- /dev/null +++ b/src/_igraph/operators.c @@ -0,0 +1,339 @@ +/* vim:set ts=4 sw=2 sts=2 et: */ +/* + IGraph library. + Copyright (C) 2006-2023 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; + Py_ssize_t no_of_graphs; + igraph_vector_ptr_t gs; + 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) { + 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 = igraph_vector_ptr_size(&gs); + + /* 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); + + /* 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 = 0, *graphs, *with_edgemaps_o; + int with_edgemaps = 0; + Py_ssize_t i, j, 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) { + 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 = igraph_vector_ptr_size(&gs); + + if (with_edgemaps) { + /* prepare edgemaps */ + igraph_vector_int_list_t edgemaps; + if (igraph_vector_int_list_init(&edgemaps, 0)) { + igraph_vector_ptr_destroy(&gs); + return igraphmodule_handle_igraph_error(); + } + + /* Create union */ + if (igraph_union_many(&g, &gs, &edgemaps)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_int_list_destroy(&edgemaps); + return igraphmodule_handle_igraph_error(); + } + + /* extract edgemaps */ + em_list = PyList_New(no_of_graphs); + for (i = 0; i < no_of_graphs; i++) { + Py_ssize_t no_of_edges = igraph_ecount(VECTOR(gs)[i]); + igraph_vector_int_t *map = igraph_vector_int_list_get_ptr(&edgemaps, i); + PyObject *emi = PyList_New(no_of_edges); + 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_int_list_destroy(&edgemaps); + Py_XDECREF(dest); + Py_DECREF(emi); + Py_DECREF(em_list); + return NULL; + } + /* reference to 'dest' stolen by PyList_SetItem */ + } + } + if (!emi || PyList_SetItem(em_list, i, emi)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_int_list_destroy(&edgemaps); + Py_XDECREF(emi); + Py_DECREF(em_list); + return NULL; + } + /* reference to 'emi' stolen by PyList_SetItem */ + } + igraph_vector_int_list_destroy(&edgemaps); + } else { + /* Create union */ + if (igraph_union_many(&g, &gs, /* edgemaps */ 0)) { + igraph_vector_ptr_destroy(&gs); + return igraphmodule_handle_igraph_error(); + } + } + + 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 (o != NULL) { + 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; + } + } 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 = 0, *graphs, *with_edgemaps_o; + int with_edgemaps = 0; + Py_ssize_t i, j, 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) { + 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 = igraph_vector_ptr_size(&gs); + + if (with_edgemaps) { + /* prepare edgemaps */ + igraph_vector_int_list_t edgemaps; + if (igraph_vector_int_list_init(&edgemaps, 0)) { + igraph_vector_ptr_destroy(&gs); + return igraphmodule_handle_igraph_error(); + } + + /* Create intersection */ + if (igraph_intersection_many(&g, &gs, &edgemaps)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_int_list_destroy(&edgemaps); + return igraphmodule_handle_igraph_error(); + } + + em_list = PyList_New((Py_ssize_t) no_of_graphs); + for (i = 0; i < no_of_graphs; i++) { + Py_ssize_t no_of_edges = igraph_ecount(VECTOR(gs)[i]); + igraph_vector_int_t *map = igraph_vector_int_list_get_ptr(&edgemaps, i); + PyObject *emi = PyList_New(no_of_edges); + 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_int_list_destroy(&edgemaps); + Py_XDECREF(dest); + Py_DECREF(emi); + Py_DECREF(em_list); + return NULL; + } + /* reference to 'dest' stolen by PyList_SetItem */ + } + } + if (!emi || PyList_SetItem(em_list, i, emi)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_int_list_destroy(&edgemaps); + Py_XDECREF(emi); + Py_DECREF(em_list); + return NULL; + } + /* reference to 'emi' stolen by PyList_SetItem */ + } + igraph_vector_int_list_destroy(&edgemaps); + } else { + /* Create intersection */ + if (igraph_intersection_many(&g, &gs, /* edgemaps */ 0)) { + igraph_vector_ptr_destroy(&gs); + return igraphmodule_handle_igraph_error(); + } + } + + 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 (o != NULL) { + 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; + } + } else { + result = (PyObject *) o; + } + + return result; +} diff --git a/src/_igraph/operators.h b/src/_igraph/operators.h new file mode 100644 index 000000000..0ed19d895 --- /dev/null +++ b/src/_igraph/operators.h @@ -0,0 +1,32 @@ +/* -*- mode: C -*- */ +/* + IGraph library. + Copyright (C) 2006-2023 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 IGRAPHMODULE_OPERATORS_H +#define IGRAPHMODULE_OPERATORS_H + +#include "preamble.h" + +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/platform.h b/src/_igraph/platform.h similarity index 87% rename from src/platform.h rename to src/_igraph/platform.h index c1ff8cf6a..83dd4277c 100644 --- a/src/platform.h +++ b/src/_igraph/platform.h @@ -1,29 +1,29 @@ /* -*- mode: C -*- */ /* vim: set ts=2 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_PLATFORM_H -#define PYTHON_PLATFORM_H +#ifndef IGRAPHMODULE_PLATFORM_H +#define IGRAPHMODULE_PLATFORM_H #ifdef _MSC_VER # define INLINE __forceinline @@ -32,4 +32,3 @@ #endif #endif - diff --git a/src/_igraph/preamble.h b/src/_igraph/preamble.h new file mode 100644 index 000000000..e70f91c0a --- /dev/null +++ b/src/_igraph/preamble.h @@ -0,0 +1,35 @@ +/* -*- 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 IGRAPHMODULE_IGRAPH_PREAMBLE_H +#define IGRAPHMODULE_IGRAPH_PREAMBLE_H + +#ifndef PY_IGRAPH_ALLOW_ENTIRE_PYTHON_API +# ifndef Py_LIMITED_API +# define Py_LIMITED_API 0x03090000 +# endif +#endif + +#define PY_SSIZE_T_CLEAN +#include + +#endif /* PYTHON_IGRAPH_PREAMBLE_H */ diff --git a/src/_igraph/pyhelpers.c b/src/_igraph/pyhelpers.c new file mode 100644 index 000000000..6f0afaf4a --- /dev/null +++ b/src/_igraph/pyhelpers.c @@ -0,0 +1,290 @@ +/* vim:set ts=4 sw=2 sts=2 et: */ +/* + IGraph library - Python interface. + Copyright (C) 2006-2011 Tamas Nepusz + 5 Avenue Road, Staines, Middlesex, TW18 3AW, United Kingdom + + 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 "pyhelpers.h" + +#include + +#ifdef PY_IGRAPH_PROVIDES_PY_NONE +PyObject* Py_None; +#endif + +#ifdef PY_IGRAPH_PROVIDES_BOOL_CONSTANTS +PyObject* Py_True; +PyObject* Py_False; +#endif + +/** + * 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. + * + * \param len the length of the list to be created + * \param item the item with which the list will be filled + */ +PyObject* igraphmodule_PyList_NewFill(Py_ssize_t len, PyObject* item) { + Py_ssize_t i; + PyObject* result = PyList_New(len); + + if (result == 0) { + return 0; + } + + for (i = 0; i < len; i++) { + Py_INCREF(item); + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } + } + + return result; +} + +/** + * Creates a Python list and fills it with zeroes. + * + * \param len the length of the list to be created + */ +PyObject* igraphmodule_PyList_Zeroes(Py_ssize_t len) { + PyObject* zero = PyLong_FromLong(0); + PyObject* result; + + if (zero == 0) + return 0; + + result = igraphmodule_PyList_NewFill(len, zero); + Py_DECREF(zero); + return result; +} + +/** + * Converts a Python object to its string representation and returns it as + * a C string. + * + * It is the responsibility of the caller to release the C string. + */ +char* igraphmodule_PyObject_ConvertToCString(PyObject* string) { + char* result = NULL; + + if (string == NULL) { + /* Nothing to do */ + } else if (PyBaseString_Check(string)) { + result = PyUnicode_CopyAsString(string); + } else { + string = PyObject_Str(string); + if (string == NULL) { + return NULL; + } + + result = PyUnicode_CopyAsString(string); + Py_DECREF(string); + } + + return result; +} + +/** + * Creates a Python range object with the given start and stop indices and step + * size. + * + * The function returns a new reference. It is the responsibility of the caller + * to release it. Returns \c NULL in case of an error. + */ +PyObject* igraphmodule_PyRange_create(Py_ssize_t start, Py_ssize_t stop, Py_ssize_t step) { + static PyObject* builtin_module = 0; + static PyObject* range_func = 0; + PyObject* result; + + if (builtin_module == 0) { + builtin_module = PyImport_ImportModule("builtins"); + if (builtin_module == 0) { + return 0; + } + } + + if (range_func == 0) { + range_func = PyObject_GetAttrString(builtin_module, "range"); + if (range_func == 0) { + return 0; + } + } + + result = PyObject_CallFunction(range_func, "lll", start, stop, step); + 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 == NULL) { + return NULL; + } + + result = PyBytes_AsString(bytes); + if (result == NULL) { + Py_DECREF(bytes); + return NULL; + } + + result = strdup(result); + Py_DECREF(bytes); + if (result == NULL) { + 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. + * + * This function is a copy of \c _Py_HashPointer from \c Objects/object.c in + * the source code of Python's C implementation. + */ +long igraphmodule_Py_HashPointer(void *p) { + long x; + size_t y = (size_t)p; + + /* bottom 3 or 4 bits are likely to be 0; rotate y by 4 to avoid + * excessive hash collisions for dicts and sets */ + y = (y >> 4) | (y << (8 * sizeof(p) - 4)); + x = (long)y; + if (x == -1) + x = -2; + return x; +} + +/** + * @brief Initializer function that must be called from igraphmodule_init() + * + * Initializes borrowed references to \c None, \c True and \c False to cope + * with the fact that \c Py_None, \c Py_False and \c Py_True are not exposed + * in PyPy as part of the limited API. + */ +int igraphmodule_helpers_init() { + static bool called = false; + bool success = false; + + if (called) { + PyErr_SetString(PyExc_RuntimeError, "igraphmodule_helpers_init() called twice"); + return 1; + } + +#ifdef PY_IGRAPH_PROVIDES_PY_NONE + Py_None = Py_BuildValue(""); + if (Py_None == NULL) { + goto cleanup; + } +#endif + +#ifdef PY_IGRAPH_PROVIDES_BOOL_CONSTANTS + Py_False = Py_True = NULL; + + Py_True = PyBool_FromLong(1); + if (Py_True == NULL) { + goto cleanup; + } + + Py_False = PyBool_FromLong(0); + if (Py_False == NULL) { + goto cleanup; + } +#endif + + called = true; + success = true; + +#if defined(PY_IGRAPH_PROVIDES_PY_NONE) || defined(PY_IGRAPH_PROVIDES_BOOL_CONSTANTS) +cleanup: +#endif + if (!success) { +#ifdef PY_IGRAPH_PROVIDES_PY_NONE + Py_XDECREF(Py_None); +#endif +#ifdef PY_IGRAPH_PROVIDES_BOOL_CONSTANTS + Py_XDECREF(Py_True); + Py_XDECREF(Py_False); +#endif + } + + return success ? 0 : 1; +} diff --git a/src/_igraph/pyhelpers.h b/src/_igraph/pyhelpers.h new file mode 100644 index 000000000..68d63d06c --- /dev/null +++ b/src/_igraph/pyhelpers.h @@ -0,0 +1,104 @@ +/* vim:set ts=4 sw=2 sts=2 et: */ +/* + IGraph library - Python interface. + Copyright (C) 2006-2011 Tamas Nepusz + 5 Avenue Road, Staines, Middlesex, TW18 3AW, United Kingdom + + 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 IGRAPHMODULE_HELPERS_H +#define IGRAPHMODULE_HELPERS_H + +#include "preamble.h" + +int igraphmodule_helpers_init(); + +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_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) \ + 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) */ +#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); \ +} + +#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; \ + } \ +} + +#ifndef Py_None +/* This happens on PyPy where Py_None is not part of the public API. Let's + * provide a replacement ourselves. */ +#define PY_IGRAPH_PROVIDES_PY_NONE +#endif + +#ifndef Py_True +/* It is unclear whether Py_True is part of the public API or not, so let's + * prepare for the case when it is not. If Py_True is not part of the public + * API, we assume that Py_False is not part of it either */ +#define PY_IGRAPH_PROVIDES_BOOL_CONSTANTS +#endif + +#ifdef PY_IGRAPH_PROVIDES_PY_NONE +extern PyObject* Py_None; +#endif + +#ifdef PY_IGRAPH_PROVIDES_BOOL_CONSTANTS +extern PyObject* Py_True; +extern PyObject* Py_False; +#endif + +#endif diff --git a/src/_igraph/random.c b/src/_igraph/random.c new file mode 100644 index 000000000..0552aba7d --- /dev/null +++ b/src/_igraph/random.c @@ -0,0 +1,323 @@ +/* -*- mode: C -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* + IGraph library. + Copyright (C) 2006-2023 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 "random.h" +#include "pyhelpers.h" +#include +#include + +/** + * \ingroup python_interface_rng + * \brief Internal data structure for storing references to the + * 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; + +#define RNG_BITS 32 +#ifdef __wasm32__ + /* size_t is 32-bit on wasm32 so we cannot use the shift trick */ + #define RNG_MAX 0xFFFFFFFF +#else + #define RNG_MAX ((((size_t) 1) << RNG_BITS) - 1) +#endif + +static igraph_i_rng_Python_state_t igraph_rng_Python_state = {0}; +static igraph_rng_t igraph_rng_Python = { + /* type = */ 0, /* state = */ 0, /* is_seeded = */ 1 +}; +static igraph_rng_t igraph_rng_default_saved = {0}; + +igraph_error_t igraph_rng_Python_init(void **state) { + IGRAPH_ERROR("Python RNG error, unsupported function called", + IGRAPH_EINTERNAL); + return IGRAPH_SUCCESS; +} + +void igraph_rng_Python_destroy(void *state) { + igraph_error("Python RNG error, unsupported function called", + __FILE__, __LINE__, IGRAPH_EINTERNAL); +} + +/** + * \ingroup python_interface_rng + * \brief Sets the random number generator used by igraph. + */ +PyObject* igraph_rng_Python_set_generator(PyObject* self, PyObject* object) { + igraph_i_rng_Python_state_t new_state, old_state; + PyObject* func; + + 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_saved); + Py_RETURN_NONE; + } + +#define GET_FUNC(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; \ + } \ +} + +#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, (2^RNG_BITS)-1) + * 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_FromSize_t(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); + + Py_RETURN_NONE; +} + +/** + * \ingroup python_interface_rng + * \brief Sets the seed of the random generator. + */ +igraph_error_t igraph_rng_Python_seed(void *state, igraph_uint_t seed) { + IGRAPH_ERROR("Python RNG error, unsupported function called", + IGRAPH_EINTERNAL); + return IGRAPH_SUCCESS; +} + +/** + * \ingroup python_interface_rng + * \brief Generates an unsigned long integer using the Python random number generator. + */ +igraph_uint_t igraph_rng_Python_get(void *state) { + PyObject* result; + PyObject* exc_type; + igraph_uint_t 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) { + 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 -- this should not happen */ + return rand() * RNG_MAX; + } else { + retval = PyLong_AsUnsignedLong(result); + Py_DECREF(result); + return retval; + } +} + +/** + * \ingroup python_interface_rng + * \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* exc_type; + double retval; + PyObject* result = PyObject_CallObject(igraph_rng_Python_state.random_func, 0); + + if (result == 0) { + 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 -- this should not happen */ + return rand(); + } else { + retval = PyFloat_AsDouble(result); + Py_DECREF(result); + return retval; + } +} + +/** + * \ingroup python_interface_rng + * \brief Generates a real number distributed according to the normal distribution + * around zero with unit variance. + */ +igraph_real_t igraph_rng_Python_get_norm(void *state) { + 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) { + 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 -- this should not happen */ + return 0; + } else { + retval = PyFloat_AsDouble(result); + Py_DECREF(result); + return retval; + } +} + +/** + * \ingroup python_interface_rng + * \brief Specification table for Python's random number generator. + * This tells igraph which functions to call to obtain random numbers. + */ +igraph_rng_type_t igraph_rngtype_Python = { + /* name= */ "Python random generator", + /* bits= */ RNG_BITS, + /* init= */ igraph_rng_Python_init, + /* destroy= */ igraph_rng_Python_destroy, + /* seed= */ igraph_rng_Python_seed, + /* get= */ igraph_rng_Python_get, + /* get_int= */ 0, + /* get_real= */ igraph_rng_Python_get_real, + /* get_norm= */ igraph_rng_Python_get_norm, + /* get_geom= */ 0, + /* get_binom= */ 0, + /* get_exp= */ 0, + /* get_gamma= */ 0, + /* get_pois= */ 0, +}; + +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; + } + + random_module = PyImport_ImportModule("random"); + if (random_module == 0) { + PyErr_WriteUnraisable(PyErr_Occurred()); + PyErr_Clear(); + return; + } + + igraph_rng_Python.type = &igraph_rngtype_Python; + igraph_rng_Python.state = &igraph_rng_Python_state; + + if (igraph_rng_Python_set_generator(igraph_module, random_module) == 0) { + PyErr_WriteUnraisable(PyErr_Occurred()); + PyErr_Clear(); + return; + } + + Py_DECREF(random_module); +} diff --git a/src/random.h b/src/_igraph/random.h similarity index 85% rename from src/random.h rename to src/_igraph/random.h index 5fca144e9..64bb64df3 100644 --- a/src/random.h +++ b/src/_igraph/random.h @@ -1,32 +1,31 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_RANDOM_H -#define PYTHON_RANDOM_H +#ifndef IGRAPHMODULE_RANDOM_H +#define IGRAPHMODULE_RANDOM_H -#include +#include "preamble.h" void igraphmodule_init_rng(PyObject*); PyObject* igraph_rng_Python_set_generator(PyObject* self, PyObject* object); #endif - diff --git a/src/vertexobject.c b/src/_igraph/vertexobject.c similarity index 70% rename from src/vertexobject.c rename to src/_igraph/vertexobject.c index 9fe1b7495..0c1ad31e9 100644 --- a/src/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -1,23 +1,23 @@ /* -*- mode: C -*- */ /* vim: set ts=2 sw=2 sts=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @@ -35,17 +35,16 @@ * \defgroup python_interface_vertex Vertex object */ -PyTypeObject igraphmodule_VertexType; +PyTypeObject* igraphmodule_VertexType; + +PyObject* igraphmodule_Vertex_attributes(igraphmodule_VertexObject* self, PyObject* _null); /** * \ingroup python_interface_vertex * \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; } /** @@ -55,7 +54,7 @@ int igraphmodule_Vertex_Check(PyObject* obj) { * exception and returns zero if the vertex object is invalid. */ int igraphmodule_Vertex_Validate(PyObject* obj) { - igraph_integer_t n; + igraph_int_t n; igraphmodule_VertexObject *self; igraphmodule_GraphObject *graph; @@ -92,36 +91,43 @@ int igraphmodule_Vertex_Validate(PyObject* obj) { * \brief Allocates a new Python vertex object * \param gref the \c igraph.Graph being referenced by the vertex * \param idx the index of the vertex - * + * * \warning \c igraph references its vertices by indices, so if * you delete some vertices from the graph, the vertex indices will * change. Since the \c igraph.Vertex objects do not follow these * changes, your existing vertex objects will point to elsewhere * (or they might even get invalidated). */ -PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { - igraphmodule_VertexObject* self; - 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; - } - return (PyObject*)self; +PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_int_t idx) { + return PyObject_CallFunction((PyObject*) 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 */ -int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self) { - PyObject *tmp; +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_int_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; + } - tmp=(PyObject*)self->gref; - self->gref=NULL; - Py_XDECREF(tmp); + Py_INCREF(g); + self->gref = (igraphmodule_GraphObject*)g; + self->idx = vid; + self->hash = -1; return 0; } @@ -130,64 +136,46 @@ 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) { - igraphmodule_Vertex_clear(self); - +static void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self) { RC_DEALLOC("Vertex", self); - - PyObject_Del((PyObject*)self); + Py_CLEAR(self->gref); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_VertexType); } /** \ingroup python_interface_vertex * \brief Formats an \c igraph.Vertex object to a string - * + * * \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; -#ifndef IGRAPH_PYTHON3 - PyObject *grepr, *drepr; -#endif - attrs = igraphmodule_Vertex_attributes(self); - if (attrs == 0) + attrs = igraphmodule_Vertex_attributes(self, NULL); + 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)); + s = PyUnicode_FromFormat("igraph.Vertex(%R, %" IGRAPH_PRId ", %R)", + (PyObject*)self->gref, self->idx, attrs); 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; +static 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 = igraphmodule_integer_t_to_PyObject(self->idx); if (index_o == 0) return -1; @@ -215,7 +203,7 @@ Py_hash_t 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; @@ -258,42 +246,46 @@ 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); +PyObject* igraphmodule_Vertex_attribute_names(igraphmodule_VertexObject* self, PyObject* Py_UNUSED(_null)) { + return self->gref ? igraphmodule_Graph_vertex_attributes(self->gref, NULL) : NULL; } /** \ingroup python_interface_vertex * \brief Returns a dict with attribue names and values */ -PyObject* igraphmodule_Vertex_attributes(igraphmodule_VertexObject* self) { +PyObject* igraphmodule_Vertex_attributes(igraphmodule_VertexObject* self, PyObject* Py_UNUSED(_null)) { igraphmodule_GraphObject *o = self->gref; PyObject *names, *dict; - long i, n; + Py_ssize_t i, n; - if (!igraphmodule_Vertex_Validate((PyObject*)self)) + 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, NULL); if (!names) { Py_DECREF(dict); return NULL; } - n=PyList_Size(names); - for (i=0; igref; PyObject* result; int r; - + if (!igraphmodule_Vertex_Validate((PyObject*)self)) return -1; 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) // we are deleting attribute return PyDict_DelItem(((PyObject**)o->g.attr)[ATTRHASH_IDX_VERTEX], k); - + result=PyDict_GetItem(((PyObject**)o->g.attr)[ATTRHASH_IDX_VERTEX], k); if (result) { /* result is a list, so set the element with index self->idx */ @@ -527,28 +527,28 @@ int igraphmodule_Vertex_set_attribute(igraphmodule_VertexObject* self, PyObject* if (r == -1) { Py_DECREF(v); } return r; } - + /* 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) { @@ -558,7 +558,7 @@ int igraphmodule_Vertex_set_attribute(igraphmodule_VertexObject* self, PyObject* Py_DECREF(result); /* compensating for PyDict_SetItem */ return 0; } - + return -1; } @@ -567,25 +567,17 @@ 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 igraphmodule_integer_t_to_PyObject(self->idx); } /** * \ingroup python_interface_vertex - * Returns the vertex index as an igraph_integer_t + * Returns the vertex index as an igraph_int_t */ -igraph_integer_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self) { +igraph_int_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self) { 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 @@ -599,7 +591,7 @@ PyObject* igraphmodule_Vertex_get_graph(igraphmodule_VertexObject* self, /**************************************************************************/ /* Implementing proxy method in Vertex that just forward the call to the * appropriate Graph method. - * + * * These methods may also execute a postprocessing function on the result * of the Graph method; for instance, this mechanism is used to turn the * result of Graph.neighbors() (which is a list of vertex indices) into a @@ -624,23 +616,36 @@ 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; - int idx_int; + PyObject* idx = PyList_GetItem(obj, i); + PyObject* edge; + igraph_int_t idx_int; + + if (!idx) { + return NULL; + } - if (!PyInt_Check(idx)) { + if (!PyLong_Check(idx)) { PyErr_SetString(PyExc_TypeError, "_convert_to_edge_list expected list of integers"); return NULL; } - if (PyInt_AsInt(idx, &idx_int)) + if (igraphmodule_PyObject_to_integer_t(idx, &idx_int)) { + return NULL; + } + + edge = igraphmodule_Edge_New(vertex->gref, idx_int); + if (!edge) { return NULL; + } - v = igraphmodule_Edge_New(vertex->gref, idx_int); - 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; } @@ -656,20 +661,32 @@ 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; - int idx_int; + igraph_int_t idx_int; + + if (!idx) { + return NULL; + } - if (!PyInt_Check(idx)) { + if (!PyLong_Check(idx)) { PyErr_SetString(PyExc_TypeError, "_convert_to_vertex_list expected list of integers"); return NULL; } - if (PyInt_AsInt(idx, &idx_int)) + if (igraphmodule_PyObject_to_integer_t(idx, &idx_int)) { return NULL; + } v = igraphmodule_Vertex_New(vertex->gref, idx_int); - PyList_SetItem(obj, i, v); /* reference to v stolen, reference to idx discarded */ + if (!v) { + return NULL; + } + + if (PyList_SetItem(obj, i, v)) { /* reference to v stolen, reference to idx discarded */ + Py_DECREF(v); + return NULL; + } } Py_INCREF(obj); @@ -679,18 +696,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; \ - 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); \ - 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); \ @@ -701,6 +725,7 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb Py_DECREF(result); \ return pp_result; \ } \ + \ return NULL; \ } @@ -713,6 +738,7 @@ GRAPH_PROXY_METHOD(constraint, "constraint"); GRAPH_PROXY_METHOD(degree, "degree"); GRAPH_PROXY_METHOD(delete, "delete_vertices"); GRAPH_PROXY_METHOD(diversity, "diversity"); +GRAPH_PROXY_METHOD(distances, "distances"); GRAPH_PROXY_METHOD(eccentricity, "eccentricity"); GRAPH_PROXY_METHOD(get_shortest_paths, "get_shortest_paths"); GRAPH_PROXY_METHOD_PP(incident, "incident", _convert_to_edge_list); @@ -732,16 +758,25 @@ 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 " \ + METHODNAME "(*args, **kwds)\n--\n\n" \ + "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 " \ + METHODNAME "(*args, **kwds)\n--\n\n" \ + "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."} +#define GRAPH_PROXY_METHOD_SPEC_3(FUNC, METHODNAME) \ + {METHODNAME, (PyCFunction)igraphmodule_Vertex_##FUNC, METH_VARARGS | METH_KEYWORDS, \ + METHODNAME "(*args, **kwds)\n--\n\n" \ + "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: L{Graph." METHODNAME "()} for details."} /** * \ingroup python_interface_vertex @@ -750,17 +785,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()\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()\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)\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" @@ -769,63 +804,52 @@ PyMethodDef igraphmodule_Vertex_methods[] = { "dictionaries." }, {"all_edges", (PyCFunction)igraphmodule_Vertex_all_edges, METH_NOARGS, - "Proxy method to L{Graph.incident(..., mode=\"all\")}\n\n" \ + "all_edges()\n--\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" \ + "in_edges()\n--\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" \ + "out_edges()\n--\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"), GRAPH_PROXY_METHOD_SPEC(degree, "degree"), GRAPH_PROXY_METHOD_SPEC_2(delete, "delete", "delete_vertices"), + GRAPH_PROXY_METHOD_SPEC(distances, "distances"), GRAPH_PROXY_METHOD_SPEC(diversity, "diversity"), GRAPH_PROXY_METHOD_SPEC(eccentricity, "eccentricity"), GRAPH_PROXY_METHOD_SPEC(get_shortest_paths, "get_shortest_paths"), GRAPH_PROXY_METHOD_SPEC(incident, "incident"), - GRAPH_PROXY_METHOD_SPEC(indegree, "indegree"), + GRAPH_PROXY_METHOD_SPEC_3(indegree, "indegree"), GRAPH_PROXY_METHOD_SPEC(is_minimal_separator, "is_minimal_separator"), GRAPH_PROXY_METHOD_SPEC(is_separator, "is_separator"), GRAPH_PROXY_METHOD_SPEC(neighbors, "neighbors"), - GRAPH_PROXY_METHOD_SPEC(outdegree, "outdegree"), - GRAPH_PROXY_METHOD_SPEC(pagerank, "pagerank"), - GRAPH_PROXY_METHOD_SPEC(predecessors, "predecessors"), + GRAPH_PROXY_METHOD_SPEC_3(outdegree, "outdegree"), + GRAPH_PROXY_METHOD_SPEC_3(pagerank, "pagerank"), + GRAPH_PROXY_METHOD_SPEC_3(predecessors, "predecessors"), GRAPH_PROXY_METHOD_SPEC(personalized_pagerank, "personalized_pagerank"), - GRAPH_PROXY_METHOD_SPEC(shortest_paths, "shortest_paths"), GRAPH_PROXY_METHOD_SPEC(strength, "strength"), - GRAPH_PROXY_METHOD_SPEC(successors, "successors"), + GRAPH_PROXY_METHOD_SPEC_3(successors, "successors"), {NULL} }; #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 @@ -840,32 +864,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" @@ -873,16 +873,39 @@ 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" - " 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 */ -}; - + " >>> print(v[\"color\"]) #doctest: +SKIP\n" + " red\n" + "\n" + "@ivar index: Index of the vertex\n" + "@ivar graph: The graph the vertex belongs to\t" +); + +int igraphmodule_Vertex_register_type() { + PyType_Slot slots[] = { + { Py_tp_init, igraphmodule_Vertex_init }, + { 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/vertexobject.h b/src/_igraph/vertexobject.h similarity index 54% rename from src/vertexobject.h rename to src/_igraph/vertexobject.h index b50b52351..21f1be9b7 100644 --- a/src/vertexobject.h +++ b/src/_igraph/vertexobject.h @@ -1,31 +1,31 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_VERTEXOBJECT_H -#define PYTHON_VERTEXOBJECT_H +#ifndef IGRAPHMODULE_VERTEXOBJECT_H +#define IGRAPHMODULE_VERTEXOBJECT_H + +#include "preamble.h" -#include #include "graphobject.h" -#include "py2compat.h" /** * \ingroup python_interface_vertex @@ -35,25 +35,17 @@ typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; - igraph_integer_t idx; - Py_hash_t hash; + igraph_int_t idx; + long hash; } igraphmodule_VertexObject; -int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self); -void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self); - -int igraphmodule_Vertex_Check(PyObject *obj); -int igraphmodule_Vertex_Validate(PyObject *obj); +extern PyTypeObject* igraphmodule_VertexType; -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); -long igraphmodule_Vertex_get_index_long(igraphmodule_VertexObject* self); -PyObject* igraphmodule_Vertex_update_attributes(PyObject* self, PyObject* args, - PyObject* kwds); +int igraphmodule_Vertex_register_type(void); -extern PyTypeObject igraphmodule_VertexType; +int igraphmodule_Vertex_Check(PyObject* obj); +PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_int_t idx); +igraph_int_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self); +PyObject* igraphmodule_Vertex_update_attributes(PyObject* self, PyObject* args, PyObject* kwds); #endif diff --git a/src/vertexseqobject.c b/src/_igraph/vertexseqobject.c similarity index 63% rename from src/vertexseqobject.c rename to src/_igraph/vertexseqobject.c index 9af0f139f..4e4f66e59 100644 --- a/src/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -1,33 +1,31 @@ /* -*- mode: C -*- */ /* vim: set ts=2 sts=2 sw=2 et: */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#include #include "attributes.h" #include "common.h" #include "convert.h" #include "error.h" -#include "py2compat.h" #include "pyhelpers.h" #include "vertexseqobject.h" #include "vertexobject.h" @@ -39,27 +37,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; } /** @@ -71,21 +58,23 @@ 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_t v; - if (igraph_vector_copy(&v, o->vs.data.vecptr)) { + igraph_vector_int_t v; + if (igraph_vector_int_init_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; } @@ -109,36 +98,42 @@ 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) { /* 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); + igraph_int_t idx; + + if (igraphmodule_PyObject_to_integer_t(vsobj, &idx)) { + return -1; + } + if (idx < 0 || idx >= 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); + + igraph_vs_1(&vs, idx); } else { - igraph_vector_t v; - igraph_integer_t n = igraph_vcount(&((igraphmodule_GraphObject*)g)->g); - if (igraphmodule_PyObject_to_vector_t(vsobj, &v, 1)) + igraph_vector_int_t v; + igraph_int_t n = igraph_vcount(&((igraphmodule_GraphObject*)g)->g); + if (igraphmodule_PyObject_to_vector_int_t(vsobj, &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, "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; @@ -153,31 +148,39 @@ 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_CLEAR(self->gref); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_VertexSeqType); } /** * \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); + igraph_int_t result; + + 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; } /** @@ -187,26 +190,30 @@ int igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject* self) { PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, Py_ssize_t i) { igraph_t *g; - igraph_integer_t idx = -1; + igraph_int_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: 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)) { - idx = (igraph_integer_t)VECTOR(*self->vs.data.vecptr)[i]; + if (i >= 0 && i < igraph_vector_int_size(self->vs.data.vecptr)) { + idx = VECTOR(*self->vs.data.vecptr)[i]; } break; case IGRAPH_VS_1: @@ -214,16 +221,22 @@ PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, idx = self->vs.data.vid; } break; - case IGRAPH_VS_SEQ: + case IGRAPH_VS_NONE: + break; + case IGRAPH_VS_RANGE: if (i < 0) { - i = self->vs.data.seq.to - self->vs.data.seq.from + i; + i = self->vs.data.range.end - self->vs.data.range.start + 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; + if (i >= 0 && i < self->vs.data.range.end - self->vs.data.range.start) { + idx = self->vs.data.range.start + i; } 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) { @@ -237,8 +250,8 @@ PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, /** \ingroup python_interface_vertexseq * \brief Returns the list of attribute names */ -PyObject* igraphmodule_VertexSeq_attribute_names(igraphmodule_VertexSeqObject* self) { - return igraphmodule_Graph_vertex_attributes(self->gref); +PyObject* igraphmodule_VertexSeq_attribute_names(igraphmodule_VertexSeqObject* self, PyObject* Py_UNUSED(_null)) { + return igraphmodule_Graph_vertex_attributes(self->gref, NULL); } /** \ingroup python_interface_vertexseq @@ -247,7 +260,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_int_t i, n; if (!igraphmodule_attribute_name_check(o)) return 0; @@ -258,7 +271,7 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje PyErr_SetString(PyExc_KeyError, "Attribute does not exist"); return NULL; } else if (PyErr_Occurred()) return NULL; - + switch (igraph_vs_type(&self->vs)) { case IGRAPH_VS_NONE: n = 0; @@ -268,38 +281,74 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje case IGRAPH_VS_ALL: n = PyList_Size(values); result = PyList_New(n); - if (!result) return 0; - - for (i=0; ivs.data.vecptr); + 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_GetItem(values, VECTOR(*self->vs.data.vecptr)[i]); + if (!item) { + Py_DECREF(result); + return 0; + } - for (i=0; ivs.data.vecptr)[i]); Py_INCREF(item); - PyList_SET_ITEM(result, i, item); + + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } } + break; - case IGRAPH_VS_SEQ: - n = self->vs.data.seq.to - self->vs.data.seq.from; + case IGRAPH_VS_RANGE: + n = self->vs.data.range.end - self->vs.data.range.start; result = PyList_New(n); if (!result) return 0; - for (i=0; ivs.data.seq.from+i); + for (i = 0; i < n; i++) { + item = PyList_GetItem(values, self->vs.data.range.start + 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; default: @@ -310,30 +359,45 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje } PyObject* igraphmodule_VertexSeq_get_attribute_values_mapping(igraphmodule_VertexSeqObject *self, PyObject *o) { - long int 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); - } + Py_ssize_t 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); } @@ -344,8 +408,8 @@ 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; - long i, j, n, no_of_nodes; + igraph_vector_int_t vs; + igraph_int_t i, j, n, no_of_nodes; gr = self->gref; dict = ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_VERTEX]; @@ -353,7 +417,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,24 +427,33 @@ 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; 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 = (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; @@ -390,7 +463,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; ig, 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 = 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 */ @@ -445,43 +525,55 @@ 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)) { + if (PyList_SetItem(list, 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 */ + } /* 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 * sequence and copy the rest */ - long n2 = igraph_vcount(&gr->g); + igraph_int_t 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; igref->g, item, &i)) @@ -586,55 +683,62 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, PyObject *args) { igraphmodule_VertexSeqObject *result; igraphmodule_GraphObject *gr; - long i, j, n, m; + igraph_int_t igraph_idx, i, j, n, m; + igraph_bool_t working_on_whole_graph = igraph_vs_is_all(&self->vs); + igraph_vector_int_t v, v2; - gr=self->gref; - 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); /* We can simply bail out here */ - return (PyObject*)result; + return (PyObject*) result; } else if (PyCallable_Check(item)) { /* Call the callable for every vertex in the current sequence to * determine what's up */ - igraph_bool_t was_excluded = 0; - igraph_vector_t v; + igraph_bool_t was_excluded = false; + igraph_vector_int_t v; - if (igraph_vector_init(&v, 0)) { + if (igraph_vector_int_init(&v, 0)) { igraphmodule_handle_igraph_error(); return 0; } m = PySequence_Size((PyObject*)result); - for (j=0; jvs); 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); - } else if (PyInt_Check(item)) { + 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)) { - igraphmodule_handle_igraph_error(); - return 0; - } - if (igraph_vector_init(&v2, 0)) { - igraph_vector_destroy(&v); + if (igraph_vector_int_init(&v, 0)) { igraphmodule_handle_igraph_error(); return 0; } - if (igraph_vs_as_vector(&gr->g, self->vs, &v2)) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); - igraphmodule_handle_igraph_error(); - return 0; + + if (!working_on_whole_graph) { + /* Extract the current vertex sequence into a vector */ + if (igraph_vector_int_init(&v2, 0)) { + igraph_vector_int_destroy(&v); + igraphmodule_handle_igraph_error(); + return 0; + } + if (igraph_vs_as_vector(&gr->g, self->vs, &v2)) { + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); + igraphmodule_handle_igraph_error(); + return 0; + } + m = igraph_vector_int_size(&v2); + } else { + /* v2 left uninitialized, we are not going to use it as it would + * simply contain integers from 0 to vcount(g)-1 */ + m = igraph_vcount(&gr->g); } - m = igraph_vector_size(&v2); - for (; i= m || idx < 0) { + Py_DECREF(result); PyErr_SetString(PyExc_ValueError, "vertex index out of range"); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } return NULL; } - if (igraph_vector_push_back(&v, VECTOR(v2)[idx])) { + if (igraph_vector_int_push_back(&v, working_on_whole_graph ? idx : VECTOR(v2)[idx])) { Py_DECREF(result); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } return NULL; } } - igraph_vector_destroy(&v2); + + if (!working_on_whole_graph) { + 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; /* 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); - 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); - Py_DECREF(result); - igraphmodule_handle_igraph_error(); - return 0; + + if (!working_on_whole_graph) { + /* Extract the current vertex sequence into a vector */ + 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_int_destroy(&v); + igraph_vector_int_destroy(&v2); + Py_DECREF(result); + igraphmodule_handle_igraph_error(); + return 0; + } + m = igraph_vector_int_size(&v2); + } else { + /* v2 left uninitialized, we are not going to use it as it would + * simply contain integers from 0 to vcount(g)-1 */ + m = igraph_vcount(&gr->g); } - m = igraph_vector_size(&v2); /* Create an appropriate iterator */ if (PySlice_Check(item)) { @@ -740,11 +879,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, m, &start, &stop, &step, &sl) == 0); if (ok) { range = igraphmodule_PyRange_create(start, stop, step); ok = (range != 0); @@ -755,8 +890,10 @@ 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); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } PyErr_SetString(PyExc_TypeError, "error while converting slice to iterator"); Py_DECREF(result); return 0; @@ -768,54 +905,67 @@ 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); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } PyErr_SetString(PyExc_TypeError, "invalid vertex filter among positional arguments"); Py_DECREF(result); return 0; } + /* Do the iteration */ - while ((item2=PyIter_Next(iter)) != 0) { - if (PyInt_Check(item2)) { - long idx = PyInt_AsLong(item2); + 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); - if (idx >= m || idx < 0) { + } else { + Py_DECREF(item2); + if (igraph_idx >= m || igraph_idx < 0) { 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); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } return NULL; } - if (igraph_vector_push_back(&v, VECTOR(v2)[idx])) { + if (igraph_vector_int_push_back(&v, working_on_whole_graph ? igraph_idx : 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); + if (!working_on_whole_graph) { + igraph_vector_int_destroy(&v2); + } return NULL; } - } else { - /* We simply ignore elements that we don't know */ - Py_DECREF(item2); } } + /* Deallocate stuff */ - igraph_vector_destroy(&v2); + if (!working_on_whole_graph) { + 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; } + 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); } } @@ -829,7 +979,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); } @@ -845,26 +995,27 @@ PyObject* igraphmodule_VertexSeq_get_graph(igraphmodule_VertexSeqObject* self, /** * \ingroup python_interface_vertexseq - * Returns the indices of the vertices in this vertex sequence + * Returns the indices of the vertices in this vertex sequence */ 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; } @@ -877,8 +1028,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; } @@ -887,7 +1041,7 @@ PyObject* igraphmodule_VertexSeq__name_index(igraphmodule_VertexSeqObject* self, * \ingroup python_interface_vertexseq * Re-creates the dictionary that maps vertex names to vertex IDs. */ -PyObject* igraphmodule_VertexSeq__reindex_names(igraphmodule_VertexSeqObject* self) { +PyObject* igraphmodule_VertexSeq__reindex_names(igraphmodule_VertexSeqObject* self, PyObject* Py_UNUSED(_null)) { igraphmodule_index_vertex_names(&self->gref->g, 1); Py_RETURN_NONE; } @@ -899,17 +1053,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()\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)\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)\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,56 +1074,24 @@ 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)\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(*args, **kwds)\n--\n\n" "For internal use only.\n" }, {"_reindex_names", (PyCFunction)igraphmodule_VertexSeq__reindex_names, METH_NOARGS, + "_reindex_names()\n--\n\n" "Re-creates the dictionary that maps vertex names to IDs.\n\n" "For internal use only.\n" }, {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 @@ -987,58 +1109,47 @@ 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.core.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 */ - "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 */ +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" +); + +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/vertexseqobject.h b/src/_igraph/vertexseqobject.h similarity index 52% rename from src/vertexseqobject.h rename to src/_igraph/vertexseqobject.h index 987f1f768..f872adc7d 100644 --- a/src/vertexseqobject.h +++ b/src/_igraph/vertexseqobject.h @@ -1,29 +1,30 @@ /* -*- mode: C -*- */ -/* +/* IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - + Copyright (C) 2006-2023 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 + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -#ifndef PYTHON_VERTEXSEQOBJECT_H -#define PYTHON_VERTEXSEQOBJECT_H +#ifndef IGRAPHMODULE_VERTEXSEQOBJECT_H +#define IGRAPHMODULE_VERTEXSEQOBJECT_H + +#include "preamble.h" -#include #include "graphobject.h" /** @@ -37,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); - -int 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_t *v); -PyObject* igraphmodule_VertexSeq_get_graph(igraphmodule_VertexSeqObject *self, - void* closure); +extern PyTypeObject* igraphmodule_VertexSeqType; -extern PyTypeObject igraphmodule_VertexSeqType; +int igraphmodule_VertexSeq_Check(PyObject* obj); +int igraphmodule_VertexSeq_register_type(void); #endif diff --git a/src/bfsiter.c b/src/bfsiter.c deleted file mode 100644 index 9f7e26dbd..000000000 --- a/src/bfsiter.c +++ /dev/null @@ -1,261 +0,0 @@ -/* -*- 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 - -*/ - -#include "bfsiter.h" -#include "common.h" -#include "error.h" -#include "py2compat.h" -#include "vertexobject.h" - -/** - * \ingroup python_interface - * \defgroup python_interface_bfsiter BFS iterator object - */ - -PyTypeObject igraphmodule_BFSIterType; - -/** - * \ingroup python_interface_bfsiter - * \brief Allocate a new BFS 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_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { - igraphmodule_BFSIterObject* o; - long int no_of_nodes, r; - - o=PyObject_GC_New(igraphmodule_BFSIterObject, &igraphmodule_BFSIterType); - 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_dqueue_init(&o->queue, 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_dqueue_destroy(&o->queue); - return NULL; - } - - if (PyInt_Check(root)) { - r=PyInt_AsLong(root); - } else { - 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); - 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("BFSIter", o); - - return (PyObject*)o; -} - -/** - * \ingroup python_interface_bfsiter - * \brief Support for cyclic garbage collection in Python - * - * 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, - visitproc visit, void *arg) { - int vret; - - RC_TRAVERSE("BFSIter", self); - - if (self->gref) { - vret=visit((PyObject*)self->gref, arg); - if (vret != 0) return vret; - } - - return 0; -} - -/** - * \ingroup python_interface_bfsiter - * \brief Clears the iterator's subobject (before deallocation) - */ -int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { - PyObject *tmp; - - PyObject_GC_UnTrack(self); - - tmp=(PyObject*)self->gref; - self->gref=NULL; - Py_XDECREF(tmp); - - igraph_dqueue_destroy(&self->queue); - igraph_vector_destroy(&self->neis); - free(self->visited); - self->visited=0; - - return 0; -} - -/** - * \ingroup python_interface_bfsiter - * \brief Deallocates a Python representation of a given BFS iterator object - */ -void igraphmodule_BFSIter_dealloc(igraphmodule_BFSIterObject* self) { - igraphmodule_BFSIter_clear(self); - - RC_DEALLOC("BFSIter", self); - - PyObject_GC_Del(self); -} - -PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { - Py_INCREF(self); - return (PyObject*)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_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; - } - } - } - - if (self->advanced) { - PyObject *vertexobj, *parentobj; - vertexobj = igraphmodule_Vertex_New(self->gref, vid); - if (!vertexobj) - return NULL; - if (parent >= 0) { - parentobj = igraphmodule_Vertex_New(self->gref, parent); - if (!parentobj) - return NULL; - } else { - Py_INCREF(Py_None); - parentobj=Py_None; - } - return Py_BuildValue("NlN", vertexobj, (long int)dist, parentobj); - } else { - return igraphmodule_Vertex_New(self->gref, vid); - } - } else { - return NULL; - } -} - -/** - * \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 */ -}; - diff --git a/src/filehandle.c b/src/filehandle.c deleted file mode 100644 index aeffb407c..000000000 --- a/src/filehandle.c +++ /dev/null @@ -1,348 +0,0 @@ -/* -*- mode: C -*- */ -/* - IGraph library. - Copyright (C) 2010-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 "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->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); - handle->need_close = 1; - } 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; - - if (object == 0 || PyLong_Check(object)) { - PyErr_SetString(PyExc_TypeError, "string or file-like object expected"); - return 1; - } - - 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_FromObject(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); - } - - /* At this stage, handle->object is something we can handle. - * We have to call PyObject_AsFileDescriptor instead - * and then fdopen() it to get the corresponding FILE* object. - */ - fp = PyObject_AsFileDescriptor(handle->object); - if (fp == -1) { - igraphmodule_filehandle_destroy(handle); - /* 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); - /* This already called Py_DECREF(handle->object), no need to call it */ - PyErr_SetString(PyExc_RuntimeError, "fdopen() failed unexpectedly"); - return 1; - } - - 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; - - 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 */ - -static int igraphmodule_i_filehandle_init_pypy_3(igraphmodule_filehandle_t* handle, - PyObject* object, char* mode) { - int fp; - - if (object == 0 || PyLong_Check(object)) { - PyErr_SetString(PyExc_TypeError, "string or file-like object expected"); - return 1; - } - - 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_FromObject(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); - } - - /* At this stage, handle->object is something we can handle. - * We have to call PyObject_AsFileDescriptor instead - * and then fdopen() it to get the corresponding FILE* object. - */ - fp = PyObject_AsFileDescriptor(handle->object); - if (fp == -1) { - igraphmodule_filehandle_destroy(handle); - /* 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); - /* This already called Py_DECREF(handle->object), no need to call it */ - PyErr_SetString(PyExc_RuntimeError, "fdopen() failed unexpectedly"); - return 1; - } - - return 0; -} -# endif /* IGRAPH_PYTHON3 */ -#endif - -/** - * \ingroup python_interface_filehandle - * \brief Constructs a new file handle object from a Python object. - * - * \return 0 if everything was OK, 1 otherwise. An appropriate Python - * exception is raised in this case. - */ -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 -} - -/** - * \ingroup python_interface_filehandle - * \brief Destroys the file handle object. - */ -void igraphmodule_filehandle_destroy(igraphmodule_filehandle_t* handle) { - if (handle->fp != 0) { - fflush(handle->fp); - } - - handle->fp = 0; - - if (handle->object != 0) { - if (handle->need_close) { - if (PyFile_Close(handle->object)) { - PyErr_WriteUnraisable(Py_None); - } - } - Py_DECREF(handle->object); - handle->object = 0; - } - - handle->need_close = 0; -} - -/** - * \ingroup python_interface_filehandle - * \brief Returns the file encapsulated by the given \c igraphmodule_filehandle_t. - */ -FILE* igraphmodule_filehandle_get(const igraphmodule_filehandle_t* handle) { - return handle->fp; -} - diff --git a/src/graphobject.h b/src/graphobject.h deleted file mode 100644 index 5b0fa4bee..000000000 --- a/src/graphobject.h +++ /dev/null @@ -1,231 +0,0 @@ -/* -*- 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_GRAPHOBJECT_H -#define PYTHON_GRAPHOBJECT_H - -#include -#include -#include "structmember.h" -#include "common.h" - -extern PyTypeObject igraphmodule_GraphType; - -/** - * \ingroup python_interface - * \brief A structure containing all the fields required to access an igraph from Python - */ -typedef struct -{ - PyObject_HEAD - // The graph object - igraph_t g; - // Python object to be called upon destruction - PyObject* destructor; - // Python object representing the sequence of vertices - PyObject* vseq; - // Python object representing the sequence of edges - PyObject* eseq; - // Python object of the weak reference list - 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); -void igraphmodule_Graph_dealloc(igraphmodule_GraphObject* self); -int igraphmodule_Graph_init(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -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_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_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_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); - -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* 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); - -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_leading_eigenvector_naive(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_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/__init__.py b/src/igraph/__init__.py new file mode 100644 index 000000000..6a4e189b9 --- /dev/null +++ b/src/igraph/__init__.py @@ -0,0 +1,1286 @@ +""" +igraph library. +""" + +__license__ = """ +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 +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 ( + 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, + GET_ADJACENCY_BOTH, + GET_ADJACENCY_LOWER, + GET_ADJACENCY_UPPER, + GraphBase, + IN, + InternalError, + OUT, + STAR_IN, + STAR_MUTUAL, + STAR_OUT, + STAR_UNDIRECTED, + STRONG, + TRANSITIVITY_NAN, + TRANSITIVITY_ZERO, + TREE_IN, + TREE_OUT, + TREE_UNDIRECTED, + Vertex, + WEAK, + arpack_options as default_arpack_options, + community_to_membership, + convex_hull, + is_bigraphical, + is_degree_sequence, + is_graphical, + is_graphical_degree_sequence, + set_progress_handler, + set_random_number_generator, + set_status_handler, + umap_compute_weights, + __igraph_version__, +) +from igraph.adjacency import ( + _get_adjacency, + _get_adjacency_sparse, + _get_adjlist, + _get_biadjacency, + _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, + _community_infomap, + _community_leading_eigenvector, + _community_label_propagation, + _community_multilevel, + _community_optimal_modularity, + _community_edge_betweenness, + _community_fluid_communities, + _community_spinglass, + _community_voronoi, + _community_walktrap, + _k_core, + _community_leiden, + _modularity, +) +from igraph.clustering import ( + Clustering, + VertexClustering, + Dendrogram, + VertexDendrogram, + Cover, + VertexCover, + CohesiveBlocks, + compare_communities, + split_join_distance, + _biconnected_components, + _cohesive_blocks, + _connected_components, + _clusters, +) +from igraph.cut import ( + Cut, + Flow, + _all_st_cuts, + _all_st_mincuts, + _gomory_hu_tree, + _maxflow, + _mincut, + _st_mincut, +) +from igraph.configuration import Configuration, init as init_configuration +from igraph.drawing import ( + BoundingBox, + CairoGraphDrawer, + DefaultGraphDrawer, + MatplotlibGraphDrawer, + 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.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 import _format_mapping +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_bipartite_graph_from_adjacency, + _construct_full_bipartite_graph, + _construct_random_bipartite_graph, +) +from igraph.io.images import _write_graph_to_svg +from igraph.layout import ( + Layout, + align_layout, + _layout, + _layout_auto, + _layout_sugiyama, + _layout_method_wrapper, + _3d_version_for, + _layout_mapping, +) +from igraph.matching import Matching +from igraph.operators import ( + disjoint_union, + union, + intersection, + operator_method_registry as _operator_method_registry, +) +from igraph.rewiring import _rewire +from igraph.seq import EdgeSeq, VertexSeq, _add_proxy_methods +from igraph.statistics import ( + FittedPowerLaw, + Histogram, + RunningMean, + mean, + median, + percentile, + quantile, + power_law_fit, +) +from igraph.structural import ( + _indegree, + _outdegree, + _degree_distribution, + _pagerank, + _shortest_paths, +) +from igraph.summary import GraphSummary, summary +from igraph.utils import ( + deprecated, + numpy_to_contiguous_memoryview, + rescale, +) +from igraph.version import __version__, __version_info__ + +import os +import sys + + +class Graph(GraphBase): + """Generic graph. + + This class is built on top of L{GraphBase}, so the order of the + 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 + easier to implement in Python than in pure C. An example is the + attribute handling in the constructor: the constructor of L{Graph} + accepts three dictionaries corresponding to the graph, vertex and edge + attributes while the constructor of L{GraphBase} does not. This extension + was needed to make L{Graph} serializable through the C{pickle} module. + L{Graph} also overrides some functions from L{GraphBase} to provide a + more convenient interface; e.g., layout functions return a L{Layout} + instance from L{Graph} instead of a list of coordinate pairs. + + Graphs can also be indexed by strings or pairs of vertex indices or vertex + names. When a graph is indexed by a string, the operation translates to + the retrieval, creation, modification or deletion of a graph attribute: + + >>> g = Graph.Full(3) + >>> g["name"] = "Triangle graph" + >>> g["name"] + 'Triangle graph' + >>> del g["name"] + + When a graph is indexed by a pair of vertex indices or names, the graph + itself is treated as an adjacency matrix and the corresponding cell of + the matrix is returned: + + >>> g = Graph.Full(3) + >>> g.vs["name"] = ["A", "B", "C"] + >>> g[1, 2] + 1 + >>> g["A", "B"] + 1 + >>> g["A", "B"] = 0 + >>> g.ecount() + 2 + + Assigning values different from zero or one to the adjacency matrix will + be translated to one, unless the graph is weighted, in which case the + numbers will be treated as weights: + + >>> g.is_weighted() + False + >>> g["A", "B"] = 2 + >>> g["A", "B"] + 1 + >>> g.es["weight"] = 1.0 + >>> g.is_weighted() + True + >>> g["A", "B"] = 2 + >>> g["A", "B"] + 2 + >>> g.es["weight"] + [1.0, 1.0, 2] + """ + + # Some useful aliases + omega = GraphBase.clique_number + alpha = GraphBase.independence_number + shell_index = GraphBase.coreness + cut_vertices = GraphBase.articulation_points + blocks = GraphBase.biconnected_components + evcent = GraphBase.eigenvector_centrality + vertex_disjoint_paths = GraphBase.vertex_connectivity + edge_disjoint_paths = GraphBase.edge_connectivity + cohesion = GraphBase.vertex_connectivity + adhesion = GraphBase.edge_connectivity + + # Compatibility aliases + shortest_paths = _shortest_paths + shortest_paths_dijkstra = shortest_paths + subgraph = GraphBase.induced_subgraph + + def __init__(self, *args, **kwds): + """__init__(n=0, edges=None, directed=False, graph_attrs=None, + vertex_attrs=None, edge_attrs=None) + + Constructs a graph from scratch. + + @keyword n: the number of vertices. Can be omitted, the default is + zero. Note that if the edge list contains vertices with indexes + larger than or equal to M{n}, then the number of vertices will + be adjusted accordingly. + @keyword edges: the edge list where every list item is a pair of + integers. If any of the integers is larger than M{n-1}, the number + of vertices is adjusted accordingly. C{None} means no edges. + @keyword directed: whether the graph should be directed + @keyword graph_attrs: the attributes of the graph as a dictionary. + @keyword vertex_attrs: the attributes of the vertices as a dictionary. + The keys of the dictionary must be the names of the attributes; the + values must be iterables with exactly M{n} items where M{n} is the + number of vertices. + @keyword edge_attrs: the attributes of the edges as a dictionary. The + keys of the dictionary must be the names of the attributes; the values + must be iterables with exactly M{m} items where M{m} is the number of + edges. + """ + # Pop the special __ptr keyword argument + ptr = kwds.pop("__ptr", None) + + # 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", + ) + params = [0, [], False, {}, {}, {}] + + # Is there any keyword argument in kwds that we don't know? If so, + # freak out. + 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() + ) + ) + + # If the first argument is a list or any other iterable, assume that + # the number of vertices were omitted + args = list(args) + if len(args) > 0 and hasattr(args[0], "__iter__"): + args.insert(0, params[0]) + + # Override default parameters from args + params[: len(args)] = args + + # Override default parameters from keywords + for idx, k in enumerate(kwd_order): + if k in kwds: + params[idx] = kwds[k] + + # Now, translate the params list to argument names + nverts, edges, directed, graph_attrs, vertex_attrs, edge_attrs = params + + # When the number of vertices is None, assume that the user meant zero + if nverts is None: + nverts = 0 + + # When 'edges' is None, assume that the user meant an empty list + if edges is None: + edges = [] + + # When 'edges' is a NumPy array or matrix, convert it into a memoryview + # 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: + pass + + # Initialize the graph + if ptr: + super().__init__(__ptr=ptr) + else: + super().__init__(nverts, edges, directed) + + # Set the graph attributes + 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.items(): + if isinstance(key, int): + key = str(key) + self.vs[key] = value + # Set the edge attributes + for key, value in edge_attrs.items(): + if isinstance(key, int): + key = str(key) + self.es[key] = value + + ############################################# + # 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 + + write_svg = _write_graph_to_svg + + 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) + Biadjacency = classmethod(_construct_bipartite_graph_from_adjacency) + 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) + + ############################################# + # Summary/string representation + 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 = { + "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)) + + ############################################# + # Commonly used attributes + 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() + + ############################################# + # Neighbors + + def predecessors(self, vertex, loops=True, multiple=True): + """Returns the predecessors of a given vertex. + + Equivalent to calling the L{Graph.neighbors()} method with mode=C{\"in\"}. + """ + return self.neighbors(vertex, mode="in", loops=loops, multiple=multiple) + + def successors(self, vertex, loops=True, multiple=True): + """Returns the successors of a given vertex. + + Equivalent to calling the L{Graph.neighbors()} method with mode=C{\"out\"}. + """ + return self.neighbors(vertex, mode="out", loops=loops, multiple=multiple) + + ############################################# + # 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) + + ############################################# + # 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 + + ################### + # 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"] + rewire = _rewire + + ############################################# + # Adjacency/incidence + get_adjacency = _get_adjacency + get_adjacency_sparse = _get_adjacency_sparse + get_adjlist = _get_adjlist + get_biadjacency = _get_biadjacency + get_inclist = _get_inclist + + ############################################# + # Structural properties + indegree = _indegree + outdegree = _outdegree + degree_distribution = _degree_distribution + pagerank = _pagerank + + ############################################# + # 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 + + ############################################# + # Connected components + biconnected_components = _biconnected_components + clusters = _clusters + cohesive_blocks = _cohesive_blocks + connected_components = _connected_components + blocks = _biconnected_components + components = _connected_components + + ############################################# + # Community detection/clustering + community_fastgreedy = _community_fastgreedy + community_infomap = _community_infomap + 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_fluid_communities = _community_fluid_communities + community_spinglass = _community_spinglass + community_voronoi = _community_voronoi + community_walktrap = _community_walktrap + k_core = _k_core + community_leiden = _community_leiden + modularity = _modularity + + ############################################# + # Layout + layout = _layout + layout_auto = _layout_auto + layout_sugiyama = _layout_sugiyama + + ############################################# + # Plotting + __plot__ = _graph_plot + + ############################################# + # Bipartite + maximum_bipartite_matching = _maximum_bipartite_matching + bipartite_projection = _bipartite_projection + bipartite_projection_size = _bipartite_projection_size + + ############################################# + # Automorphisms + count_automorphisms_vf2 = _count_automorphisms_vf2 + get_automorphisms_vf2 = _get_automorphisms_vf2 + + ########################### + # Paths/traversals + def get_all_simple_paths(self, v, to=None, minlen=0, maxlen=-1, mode="out", max_results=None): + """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 + more than once. + + Note that potentially there are exponentially many paths between two + vertices of a graph, especially if your graph is lattice-like. In this + case, you may run out of memory when using this function. + + @param v: the source for the calculated paths + @param to: a vertex selector describing the destination for the calculated + paths. This can be a single vertex ID, a list of vertex IDs, a single + vertex name, a list of vertex names or a L{VertexSeq} object. C{None} + means all the vertices. + @param minlen: minimum length of path that is considered. + @param maxlen: maximum length of path that is considered. If negative, + paths of all lengths are considered. + @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. + @param max_results: the maximum number of results to return. C{None} means + no limit on the number of results. + @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=C{\"in\"}, + the vertices in a path are returned in reversed order! + """ + return self._get_all_simple_paths(v, to, minlen, maxlen, mode, max_results) + + def path_length_hist(self, directed=True): + """Returns the path length histogram of the graph + + @param directed: whether to consider directed paths. Ignored for + undirected graphs. + @return: a L{Histogram} object. The object will also have an + C{unconnected} attribute that stores the number of unconnected + vertex pairs (where the second vertex can not be reached from + the first one). The latter one will be of type long (and not + a simple integer), since this can be I{very} large. + """ + 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 = int(unconn) + return hist + + # 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(-1) + 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, method="auto"): + """Calculates a minimum spanning tree for a graph. + + B{Reference}: Prim, R.C. Shortest connection networks and some + generalizations. I{Bell System Technical Journal} 36:1389-1401, 1957. + + @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 + introduced in igraph 0.6. + @param method: the algorithm to use. C{"auto"} means that the algorithm + is selected automatically. C{"prim"} means that Prim's algorithm is + used. C{"kruskal"} means that Kruskal's algorithm is used. + C{"unweighted"} assumes that the graph is unweighted even if weights + are provided. + @return: the spanning tree as a L{Graph} object if C{return_tree} + is C{True} or the IDs of the edges that constitute the spanning + tree if C{return_tree} is C{False}. + """ + result = GraphBase._spanning_tree(self, weights, method) + if return_tree: + 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}). + + B{Reference}: Holland, P.W. and Leinhardt, S. A Method for Detecting + Structure in Sociometric Data. I{American Journal of Sociology}, 70, + 492-513, 1970. + + @return: a L{DyadCensus} object. + """ + return DyadCensus(GraphBase.dyad_census(self, *args, **kwds)) + + def triad_census(self, *args, **kwds): + """Calculates the triad census of the graph. + + B{Reference}: Davis, J.A. and Leinhardt, S. The Structure of + Positive Interpersonal Relations in Small Groups. In: + J. Berger (Ed.), Sociological Theories in Progress, Volume 2, + 218-251. Boston: Houghton Mifflin (1972). + + @return: a L{TriadCensus} object. + """ + 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. + + In the unweighted case, the transitivity measures the probability that + two neighbors of a vertex are connected. In case of the average local + transitivity, this probability is calculated for each vertex and then + the average is taken. Vertices with less than two neighbors require + special treatment, they will either be left out from the calculation + or they will be considered as having zero transitivity, depending on + the I{mode} parameter. The calculation is slightly more involved for + weighted graphs; in this case, weights are taken into account according + to the formula of Barrat et al (see the references). + + Note that this measure is different from the global transitivity + measure (see L{transitivity_undirected()}) as it simply takes the + average local transitivity across the whole network. + + B{References} + + - Watts DJ and Strogatz S: Collective dynamics of small-world + networks. I{Nature} 393(6884):440-442, 1998. + - Barrat A, Barthelemy M, Pastor-Satorras R and Vespignani A: + The architecture of complex weighted networks. I{PNAS} 101, 3747 + (2004). U{https://arxiv.org/abs/cond-mat/0311416}. + + @param mode: defines how to treat vertices with degree less than two. + If C{TRANSITIVITY_ZERO} or C{"zero"}, these vertices will have zero + transitivity. If C{TRANSITIVITY_NAN} or C{"nan"}, these vertices + will be excluded from the average. + @param weights: edge weights to be used. Can be a sequence or iterable + or even an edge attribute name. + + @see: L{transitivity_undirected()}, L{transitivity_local_undirected()} + """ + if weights is None: + return GraphBase.transitivity_avglocal_undirected(self, mode) + + xs = self.transitivity_local_undirected(mode=mode, weights=weights) + return sum(xs) / float(len(xs)) + + ########################### + # ctypes support + @property + def _as_parameter_(self): + return self._raw_pointer() + + # Other type functions + def __bool__(self): + """Returns True if the graph has at least one vertex, False otherwise.""" + return self.vcount() > 0 + + 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, str)): + return self, other + if isinstance(other, Vertex): + return self, other + if isinstance(other, VertexSeq): + return self, other + if isinstance(other, Edge): + return self, other + if isinstance(other, EdgeSeq): + return self, other + return NotImplemented + + @classmethod + def _reconstruct(cls, attrs, *args, **kwds): + """Reconstructs a Graph object from Python's pickled format. + + This method is for internal use only, it should not be called + directly.""" + result = cls(*args, **kwds) + result.__dict__.update(attrs) + return result + + def __reduce__(self): + """Support for pickling.""" + constructor = self.__class__ + gattrs, vattrs, eattrs = {}, {}, {} + for attr in self.attributes(): + gattrs[attr] = self[attr] + for attr in self.vs.attribute_names(): + 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, + ) + return (constructor, parameters, self.__dict__) + + __iter__ = None # needed for PyPy + __hash__ = None # needed for PyPy + + ########################### + # Deprecated functions + + @classmethod + def Incidence(cls, *args, **kwds): + """Deprecated alias to L{Graph.Biadjacency()}.""" + deprecated("Graph.Incidence() is deprecated; use Graph.Biadjacency() instead") + return cls.Biadjacency(*args, **kwds) + + def are_connected(self, *args, **kwds): + """Deprecated alias to L{Graph.are_adjacent()}.""" + deprecated( + "Graph.are_connected() is deprecated; use Graph.are_adjacent() " "instead" + ) + return self.are_adjacent(*args, **kwds) + + def get_incidence(self, *args, **kwds): + """Deprecated alias to L{Graph.get_biadjacency()}.""" + deprecated( + "Graph.get_incidence() is deprecated; use Graph.get_biadjacency() " + "instead" + ) + return self.get_biadjacency(*args, **kwds) + + +############################################################## +# I/O format mapping +Graph._format_mapping = _format_mapping + + +############################################################## +# Additional methods of VertexSeq and EdgeSeq that call Graph methods +_add_proxy_methods() + + +############################################################## +# Layout mapping +Graph._layout_mapping = _layout_mapping + + +############################################################## +# Making sure that layout methods always return a Layout +for name in dir(Graph): + if not name.startswith("layout_"): + continue + if name in ("layout_auto", "layout_sugiyama"): + 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 +) +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) + + +############################################################## +# Auxiliary global functions +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 igraph is installed already + os.path.join( + sys.prefix, + "include", + "python{0}.{1}".format(*sys.version_info), + "igraph", + ), + # Fallback for cases when igraph is not installed but + # imported directly from the source tree + 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 the Python interface of igraph") + + +def read(filename, *args, **kwds): + """Loads a graph from the given filename. + + This is just a convenience function, calls L{Graph.Read} directly. + All arguments are passed unchanged to L{Graph.Read} + + @param filename: the name of the file to be loaded + """ + return Graph.Read(filename, *args, **kwds) + + +load = read + + +def write(graph, filename, *args, **kwds): + """Saves a graph to the given file. + + This is just a convenience function, calls L{Graph.write} directly. + All arguments are passed unchanged to L{Graph.write} + + @param graph: the graph to be saved + @param filename: the name of the file to be written + """ + return graph.write(filename, *args, **kwds) + + +save = write + + +############################################################## +# Configuration singleton instance +config: Configuration = init_configuration() +"""The main configuration object of igraph. Use this object to modify igraph's +behaviour, typically when used in interactive mode. +""" + + +############################################################## +# Remove modular methods 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, + _format_mapping, + _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_bipartite_graph_from_adjacency, + _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, + _community_fastgreedy, + _community_infomap, + _community_leading_eigenvector, + _community_label_propagation, + _community_multilevel, + _community_optimal_modularity, + _community_edge_betweenness, + _community_fluid_communities, + _community_spinglass, + _community_voronoi, + _community_walktrap, + _k_core, + _community_leiden, + _modularity, + _graph_plot, + _operator_method_registry, + _add_edge, + _add_edges, + _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, + _connected_components, + _add_proxy_methods, + _rewire, +) + +# Re-export from _igraph for API docs +# Because _igraph starts with an underscore, pydoctor skips the whole docs +# except for the objects mentioned down here. +__all__ = ( + "config", + "AdvancedGradientPalette", + "BoundingBox", + "CairoGraphDrawer", + "ClusterColoringPalette", + "Clustering", + "CohesiveBlocks", + "Configuration", + "Cover", + "Cut", + "DefaultGraphDrawer", + "Dendrogram", + "DyadCensus", + "Edge", + "EdgeSeq", + "FittedPowerLaw", + "Flow", + "GradientPalette", + "Graph", + "GraphBase", + "GraphSummary", + "Histogram", + "InternalError", + "Layout", + "Matching", + "MatplotlibGraphDrawer", + "Matrix", + "Palette", + "Plot", + "Point", + "PrecalculatedPalette", + "RainbowPalette", + "Rectangle", + "RunningMean", + "TriadCensus", + "UniqueIdGenerator", + "Vertex", + "VertexClustering", + "VertexCover", + "VertexDendrogram", + "VertexSeq", + "autocurve", + "color_name_to_rgb", + "color_name_to_rgba", + "community_to_membership", + "compare_communities", + "convex_hull", + "default_arpack_options", + "disjoint_union", + "get_include", + "hsla_to_rgba", + "hsl_to_rgb", + "hsva_to_rgba", + "hsv_to_rgb", + "is_bigraphical", + "is_degree_sequence", + "is_graphical", + "is_graphical_degree_sequence", + "intersection", + "known_colors", + "load", + "mean", + "median", + "palettes", + "percentile", + "plot", + "power_law_fit", + "quantile", + "read", + "rescale", + "rgba_to_hsla", + "rgb_to_hsl", + "rgba_to_hsva", + "rgb_to_hsv", + "save", + "set_progress_handler", + "set_random_number_generator", + "set_status_handler", + "split_join_distance", + "summary", + "umap_compute_weights", + "union", + "write", + "__igraph_version__", + "__version__", + "__version_info__", + # enums and stuff + "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", + "GET_ADJACENCY_BOTH", + "GET_ADJACENCY_LOWER", + "GET_ADJACENCY_UPPER", + "IN", + "OUT", + "STAR_IN", + "STAR_MUTUAL", + "STAR_OUT", + "STAR_UNDIRECTED", + "STRONG", + "TRANSITIVITY_NAN", + "TRANSITIVITY_ZERO", + "TREE_IN", + "TREE_OUT", + "TREE_UNDIRECTED", + "WEAK", +) diff --git a/src/igraph/adjacency.py b/src/igraph/adjacency.py new file mode 100644 index 000000000..3d42b0bb2 --- /dev/null +++ b/src/igraph/adjacency.py @@ -0,0 +1,184 @@ +from igraph._igraph import ( + GET_ADJACENCY_BOTH, + GET_ADJACENCY_LOWER, + GET_ADJACENCY_UPPER, + GraphBase, +) +from igraph.datatypes import Matrix + + +__all__ = ( + "_get_adjacency", + "_get_adjacency_sparse", + "_get_adjlist", + "_get_biadjacency", + "_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" + ) from None + + 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() + r, c = zip(*edges) if edges else ([], []) + mtx = sparse.csr_matrix((weights, (r, c)), 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", loops="twice", multiple=True): + """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. + @param loops: whether to return loops in I{undirected} graphs once + (C{"once"}), twice (C{"twice"}) or not at all (C{"ignore"}). C{False} + is accepted as an alias to C{"ignore"} and C{True} is accepted as an + alias to C{"twice"}. For directed graphs, C{"twice"} is equivalent + to C{"once"} (except when C{mode} is C{"all"} because the graph is + then treated as undirected). + @param multiple: whether to return endpoints of multiple edges as many + times as their multiplicities. + """ + return [self.neighbors(idx, mode, loops, multiple) for idx in range(self.vcount())] + + +def _get_biadjacency(graph, types="type", *args, **kwds): + """Returns the bipartite adjacency matrix of a bipartite graph. The + bipartite adjacency 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: a 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 bipartite adjacency 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_biadjacency(types, *args, **kwds) + + +def _get_inclist(graph, mode="out", loops="twice"): + """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 each vertex. If + C{"in"}, returns the predecessors of each vertex. If C{"all"}, both + the predecessors and the successors will be returned. Ignored + for undirected graphs. + @param loops: whether to return loops in I{undirected} graphs once + (C{"once"}), twice (C{"twice"}) or not at all (C{"ignore"}). C{False} + is accepted as an alias to C{"ignore"} and C{True} is accepted as an + alias to C{"twice"}. For directed graphs, C{"twice"} is equivalent + to C{"once"} (except when C{mode} is C{"all"} because the graph is + then treated as undirected). + """ + return [graph.incident(idx, mode, loops) for idx in range(graph.vcount())] diff --git a/igraph/app/__init__.py b/src/igraph/app/__init__.py similarity index 96% rename from igraph/app/__init__.py rename to src/igraph/app/__init__.py index 1dd82a624..049f302eb 100644 --- a/igraph/app/__init__.py +++ b/src/igraph/app/__init__.py @@ -1,2 +1 @@ """User interfaces of igraph""" - diff --git a/igraph/app/shell.py b/src/igraph/app/shell.py similarity index 75% rename from igraph/app/shell.py rename to src/igraph/app/shell.py index c58f014fa..d8318bcd0 100644 --- a/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -19,39 +19,34 @@ Mac OS X users are likely to invoke igraph from the command line. """ -from __future__ import print_function - +from abc import ABCMeta, abstractmethod 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 -# 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 a terminal. - `TerminalController` defines a set of instance variables whose + C{TerminalController} defines a set of instance variables whose values are initialized to the control sequence necessary to perform a given action. These can be simply included in normal 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': + Alternatively, the L{render()} method can used, which replaces + C{${action}} with the string required to perform C{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 @@ -68,44 +63,41 @@ class TerminalController: ... Finally, if the width and height of the terminal are known, then - they will be stored in the `COLS` and `LINES` attributes. + they will be stored in the C{COLS} and C{LINES} attributes. @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 - - # Terminal size: - COLS = None #: Width of the terminal (None for unknown) - LINES = None #: Height of the terminal (None for unknown) + HIDE_CURSOR = "" #: Make the cursor invisible + SHOW_CURSOR = "" #: Make the cursor visible # 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 @@ -117,9 +109,9 @@ class TerminalController: def __init__(self, term_stream=sys.stdout): """ - Create a `TerminalController` and initialize its attributes + Create a C{TerminalController} and initialize its attributes with appropriate values for the current terminal. - `term_stream` is the stream that will be used for terminal + C{term_stream} is the stream that will be used for terminal output; if this stream is not a tty, then the terminal is assumed to be a dumb terminal (i.e., have no capabilities). """ @@ -137,35 +129,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 +167,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 +185,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"r\$\$|\${\w+}", self._render_sub, template) # noqa: W605 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]) @@ -204,7 +198,9 @@ def _render_sub(self, match): class ProgressBar: """ - A 2-line progress bar, which looks like:: + A 2-line progress bar. + + The progress bar looks roughly like this in the console:: Header 20% [===========----------------------------------] @@ -212,18 +208,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 +236,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 +249,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,22 +270,25 @@ 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 = "" -class Shell(object): +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. @@ -298,14 +304,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(): @@ -317,9 +321,10 @@ class IDLEShell(Shell): """IDLE embedded shell interface. This class allows igraph to be embedded in IDLE (the Tk Python IDE). + """ - @todo: no progress bar support yet. Shell/Restart Shell command should - re-import igraph again.""" + # TODO: no progress bar support yet. Shell/Restart Shell command should + # re-import igraph again. def __init__(self): """Constructor. @@ -327,16 +332,16 @@ def __init__(self): Imports IDLE's embedded shell. The implementation of this method is ripped from idlelib.PyShell.main() after removing the unnecessary parts.""" - Shell.__init__(self) + super().__init__() import idlelib.PyShell idlelib.PyShell.use_subprocess = True try: - sys.ps1 + sys.ps1 # noqa: B018 except AttributeError: - sys.ps1 = '>>> ' + sys.ps1 = ">>> " root = idlelib.PyShell.Tk(className="Idle") idlelib.PyShell.fixwordbreaks(root) @@ -354,7 +359,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): @@ -421,6 +426,7 @@ def __init__(self): import sys from IPython import __version__ as ipython_version + self.ipython_version = ipython_version try: @@ -434,6 +440,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 +473,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,31 +491,33 @@ 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 = { + 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: + 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 + if platform.system() == "Windows": shell_classes.insert(0, IDLEShell) shell = None for shell_class in shell_classes: - # pylint: disable-msg=W0703 - # W0703: catch "Exception" try: shell = shell_class() break - except StandardError: + except Exception: # Try the next one if "Classic" in str(shell_class): raise @@ -524,5 +534,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/automorphisms.py b/src/igraph/automorphisms.py new file mode 100644 index 000000000..6c8b5a417 --- /dev/null +++ b/src/igraph/automorphisms.py @@ -0,0 +1,51 @@ +__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/basic.py b/src/igraph/basic.py new file mode 100644 index 000000000..edc016d20 --- /dev/null +++ b/src/igraph/basic.py @@ -0,0 +1,186 @@ +from igraph._igraph import GraphBase +from igraph.seq import EdgeSeq +from igraph.utils import deprecated + + +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, each 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 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. + """ + if isinstance(name, int): + # raise TypeError("cannot use integers as vertex names; use strings instead") + deprecated( + "You are using integers as vertex names. This is discouraged because " + "most igraph functions interpret integers as vertex _IDs_ and strings " + "as vertex names. For sake of consistency, convert your vertex " + "names to strings before assigning them. Future versions from igraph " + "0.11.0 will disallow integers as vertex names." + ) + + 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, each 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 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 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 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: + + Deprecation notice: C{delete_edges(None)} has been replaced by + C{delete_edges()} - with no arguments - since igraph 0.8.3. + + - 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. + """ + 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) + + +def _clear(graph): + """Clears the graph, deleting all vertices, edges, and attributes. + + @see: L{GraphBase.delete_vertices} and L{Graph.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{GraphBase.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{GraphBase.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/igraph/clustering.py b/src/igraph/clustering.py similarity index 74% rename from igraph/clustering.py rename to src/igraph/clustering.py index 7919aa84d..36df961ad 100644 --- a/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -1,44 +1,22 @@ # 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""" - -__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 -""" +"""Classes related to graph clustering.""" 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._igraph import GraphBase, community_to_membership, _compare_communities 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 +from igraph.utils import deprecated + -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 @@ -64,7 +42,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 @@ -74,22 +52,21 @@ 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 """ - def __init__(self, membership, params = None): + def __init__(self, membership, params=None): """Constructor. @param membership: the membership list -- that is, the cluster index in which each element of the set belongs to. @param params: additional parameters to be stored in this - object's dictionary.""" + 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 +88,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) @@ -164,14 +141,15 @@ def sizes(self, *args): """ counts = [0] * len(self) for x in self._membership: - counts[x] += 1 + if x is not None: + counts[x] += 1 if args: return [counts[idx] for idx in 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 +168,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() @@ -224,15 +207,19 @@ 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 _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,11 +235,11 @@ 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) + super().__init__([0] * graph.vcount(), params) else: if len(membership) != graph.vcount(): raise ValueError("membership list has invalid length") - Clustering.__init__(self, membership, params) + super().__init__(membership, params) self._graph = graph self._modularity = modularity @@ -262,7 +249,6 @@ def __init__(self, graph, membership = None, modularity = None, \ 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. @@ -334,20 +320,21 @@ 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{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. """ 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 @@ -355,8 +342,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 +352,7 @@ def modularity(self): if self._modularity_dirty: return self._recalculate_modularity_safe() return self._modularity + q = modularity @property @@ -381,8 +370,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 @@ -395,7 +385,7 @@ def _recalculate_modularity_safe(self): """ try: return self.recalculate_modularity() - except: + except Exception: return None finally: self._modularity_dirty = False @@ -403,42 +393,48 @@ 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] - 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. - @precondition: the vertex set of the graph hasn't been modified since - the moment the clustering was constructed. + 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. """ ss = self.sizes() - max_size = max(ss) - return self.subgraph(ss.index(max_size)) + if ss: + max_size = max(ss) + return self.subgraph(ss.index(max_size)) + else: + return self._graph.copy() - 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 @@ -456,7 +452,7 @@ def __plot__(self, context, bbox, palette, *args, **kwds): - C{True}: all the groups will be highlighted, the colors matching the corresponding color indices from the current palette - (see the C{palette} keyword argument of L{Graph.__plot__}. + (see the C{palette} keyword argument of L{Graph.__plot__}). - A dict mapping cluster indices or tuples of vertex indices to color names. The given clusters or vertex groups will be @@ -485,28 +481,32 @@ 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"] - kwds["edge_color"] = [colors[is_crossing] - for is_crossing in self.crossing()] + 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"]: - kwds["mark_groups"] = ( - (group, color) for color, group in enumerate(self) - ) + 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 + 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 @@ -522,7 +522,8 @@ 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 @@ -551,8 +552,6 @@ class Dendrogram(object): 3 -+ | | | 4 -+---+--- - - @undocumented: _item_box_size, _plot_item, _traverse_inorder """ def __init__(self, merges): @@ -562,7 +561,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 @@ -576,9 +575,9 @@ 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) + idxs = list(range(n)) for rowidx, row in enumerate(merges): i, j = row try: @@ -586,8 +585,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 + ) from None idxs.append(j) return [x for x in tuple_repr if x is not None] @@ -601,7 +602,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 @@ -616,7 +617,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 @@ -640,11 +641,11 @@ 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: - 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 @@ -667,13 +668,19 @@ 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() @@ -683,12 +690,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: @@ -697,8 +704,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: @@ -712,60 +719,21 @@ 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() - 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 - - # pylint: disable-msg=R0913 - 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.) - context.move_to(0, height) - 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 + def __plot__(self, backend, context, *args, **kwds): + """Draws the dendrogram on the given Cairo context or matplotlib Axes. Supported keyword arguments are: @@ -779,127 +747,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 xrange(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 xrange(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., 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.)) - - # 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. - else: - delta_x += font_height / 2. - 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_line_width(1) - - # Draw items - if horiz: - sgn = 0 if orientation == "rl" else -1 - for idx in xrange(self._nitems): - x = layout[idx][0] + sgn * item_boxes[idx][0] - y = layout[idx][1] - item_boxes[idx][1]/2. - 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. - 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): @@ -925,15 +784,14 @@ 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, modularity_params=None): """Creates a dendrogram object for a given graph. @param graph: the graph that will be associated to the clustering @@ -943,13 +801,12 @@ def __init__(self, graph, merges, optimal_count = None, params = None, clustering algorithm that produces the dendrogram. C{None} means that such a hint is not available; the optimal count will then be selected based on the modularity in such a case. - @param params: additional parameters to be stored in this object. @param modularity_params: arguments that should be passed to L{Graph.modularity} when the modularity is (re)calculated. If the original graph was weighted, you should pass a dictionary containing a C{weight} key with the appropriate value here. """ - Dendrogram.__init__(self, merges) + super().__init__(merges) self._graph = graph self._optimal_count = optimal_count if modularity_params is None: @@ -973,11 +830,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): @@ -992,13 +849,17 @@ def optimal_count(self): return self._optimal_count n = self._graph.vcount() - max_q, optimal_count = 0, 1 - for step in xrange(min(n-1, len(self._merges))): + if n == 0: + return 0 + + max_q, optimal_count = 0, n - len(self._merges) + for step in range(min(n - 1, len(self._merges) + 1)): 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 @@ -1006,8 +867,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.""" @@ -1019,17 +880,20 @@ 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, backend, context, *args, **kwds) del self._names return result + ############################################################################### -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 @@ -1065,7 +929,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 @@ -1075,7 +939,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 @@ -1085,8 +949,6 @@ class Cover(object): >>> cover = Cover(clustering) >>> list(clustering) == list(cover) True - - @undocumented: _formatted_cluster_iterator """ def __init__(self, clusters, n=0): @@ -1106,7 +968,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) @@ -1135,7 +997,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) @@ -1163,7 +1025,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 @@ -1182,18 +1044,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() @@ -1214,11 +1075,9 @@ 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): + 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 @@ -1228,10 +1087,14 @@ def __init__(self, graph, clusters = None): if clusters is None: clusters = [range(graph.vcount())] - Cover.__init__(self, clusters, n = graph.vcount()) + self._resolve_names_in_clusters(graph, clusters) + + super().__init__(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 @@ -1239,8 +1102,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): @@ -1250,25 +1115,26 @@ 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] - 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. @@ -1285,7 +1151,7 @@ def __plot__(self, context, bbox, palette, *args, **kwds): - C{True}: all the clusters will be highlighted, the colors matching the corresponding color indices from the current palette - (see the C{palette} keyword argument of L{Graph.__plot__}. + (see the C{palette} keyword argument of L{Graph.__plot__}). - A dict mapping cluster indices or tuples of vertex indices to color names. The given clusters or vertex groups will be @@ -1316,23 +1182,28 @@ 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()] + if backend == "matplotlib": + colors = ["dimgrey", "silver"] + else: + colors = ["grey20", "grey80"] - if "palette" in kwds: - palette = kwds["palette"] - else: - palette = ClusterColoringPalette(len(self)) + kwds["edge_color"] = [ + colors[is_crossing] for is_crossing in self.crossing() + ] + + 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"]: - kwds["mark_groups"] = enumerate(self) + 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) + 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 @@ -1345,27 +1216,48 @@ def _formatted_cluster_iterator(self): for cluster in self: yield ", ".join(str(member) for member in cluster) + @staticmethod + def _resolve_names_in_clusters(graph, clusters): + if not graph.is_named(): + return + + names = graph.vs["name"] + name_to_index = {k: v for v, k in enumerate(names)} + + for idx, cluster in enumerate(clusters): + if any(isinstance(item, str) for item in cluster): + new_cluster = [] + for item in cluster: + if isinstance(item, str): + new_cluster.append(name_to_index.get(item, item)) + else: + new_cluster.append(item) + clusters[idx] = new_cluster + class CohesiveBlocks(VertexCover): """The cohesive block structure of a graph. - Instances of this type are created by L{Graph.cohesive_blocks()}. See - the documentation of L{Graph.cohesive_blocks()} for an explanation of - what cohesive blocks are. + Instances of this type are created by + L{Graph.cohesive_blocks()}. See the + documentation of L{Graph.cohesive_blocks()} + for an explanation of what cohesive blocks are. This class provides a few more methods that make handling of cohesive 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 - arguments will be ignored and L{Graph.cohesive_blocks()} will be - called to calculate the cohesive blocks. Otherwise, these three - variables should describe the *result* of a cohesive block structure - calculation. Chances are that you never have to construct L{CohesiveBlocks} - instances directly, just use L{Graph.cohesive_blocks()}. + arguments will be ignored and + L{Graph.cohesive_blocks()} will be called + to calculate the cohesive blocks. Otherwise, these three variables + should describe the *result* of a cohesive block structure calculation. + Chances are that you never have to construct L{CohesiveBlocks} + instances directly, just use + L{Graph.cohesive_blocks()}. @param graph: the graph itself @param blocks: a list containing the blocks; each block is described @@ -1380,7 +1272,7 @@ def __init__(self, graph, blocks = None, cohesion = None, parent = None): if blocks is None or cohesion is None or parent is None: blocks, cohesion, parent = graph.cohesive_blocks() - VertexCover.__init__(self, graph, blocks) + super().__init__(graph, blocks) self._cohesion = cohesion self._parent = parent @@ -1405,15 +1297,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 @@ -1422,7 +1316,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 @@ -1437,9 +1331,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. @@ -1453,18 +1347,18 @@ 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: - 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: 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): """Handles the mark_groups=... keyword argument in plotting methods of @@ -1477,26 +1371,25 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): to clusters automatically. """ # Handle the case of mark_groups = True, mark_groups containing a list or - # tuple of cluster IDs, and and mark_groups yielding (cluster ID, color) + # tuple of cluster IDs, and mark_groups yielding (cluster ID, color) # pairs if mark_groups is True: group_iter = ((group, color) for color, group in enumerate(clustering)) elif isinstance(mark_groups, dict): - group_iter = mark_groups.iteritems() + group_iter = mark_groups.items() elif hasattr(mark_groups, "__getitem__") and hasattr(mark_groups, "__len__"): # Lists, tuples try: first = mark_groups[0] - except: + except Exception: # Hmm. Maybe not a list or tuple? 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)) + 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 @@ -1506,18 +1399,20 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): # Iterators etc group_iter = mark_groups else: - group_iter = {}.iteritems() + group_iter = {}.items() 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 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 @@ -1535,6 +1430,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 @@ -1545,8 +1441,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: @@ -1562,13 +1459,34 @@ def _ensure_list(obj): def compare_communities(comm1, comm2, method="vi", remove_none=False): """Compares two community structures using various distance measures. + For measures involving entropies (e.g., the variation of information metric), + igraph uses natural logarithms. + + B{References} + + - Meila M: Comparing clusterings by the variation of information. In: + Scholkopf B, Warmuth MK (eds). Learning Theory and Kernel Machines: 16th + Annual Conference on Computational Learning Theory and 7th Kernel + Workship, COLT/Kernel 2003, Washington, DC, USA. Lecture Notes in Computer + Science, vol. 2777, Springer, 2003. ISBN: 978-3-540-40720-1. + - Danon L, Diaz-Guilera A, Duch J, Arenas A: Comparing community structure + identification. I{J Stat Mech} P09008, 2005. + - van Dongen S: Performance criteria for graph clustering and Markov + cluster experiments. Technical Report INS-R0012, National Research + Institute for Mathematics and Computer Science in the Netherlands, + Amsterdam, May 2000. + - Rand WM: Objective criteria for the evaluation of clustering + methods. I{J Am Stat Assoc} 66(336):846-850, 1971. + - Hubert L and Arabie P: Comparing partitions. I{Journal of + Classification} 2:193-218, 1985. + @param comm1: the first community structure as a membership list or as a L{Clustering} object. @param comm2: the second community structure as a membership list or as a L{Clustering} object. @param method: the measure to use. C{"vi"} or C{"meila"} means the variation of information metric of Meila (2003), C{"nmi"} or C{"danon"} - means the normalized mutual information as defined by Danon et al (2005), + means the normalized mutual information as defined by Danon et al. (2005), C{"split-join"} means the split-join distance of van Dongen (2000), C{"rand"} means the Rand index of Rand (1971), C{"adjusted_rand"} means the adjusted Rand index of Hubert and Arabie (1985). @@ -1581,27 +1499,9 @@ def compare_communities(comm1, comm2, method="vi", remove_none=False): are compared. @return: the calculated measure. - @newfield ref: Reference - @ref: Meila M: Comparing clusterings by the variation of information. - In: Scholkopf B, Warmuth MK (eds). Learning Theory and Kernel - Machines: 16th Annual Conference on Computational Learning Theory - and 7th Kernel Workship, COLT/Kernel 2003, Washington, DC, USA. - Lecture Notes in Computer Science, vol. 2777, Springer, 2003. - ISBN: 978-3-540-40720-1. - @ref: Danon L, Diaz-Guilera A, Duch J, Arenas A: Comparing community - structure identification. J Stat Mech P09008, 2005. - @ref: van Dongen D: Performance criteria for graph clustering and Markov - cluster experiments. Technical Report INS-R0012, National Research - Institute for Mathematics and Computer Science in the Netherlands, - Amsterdam, May 2000. - @ref: Rand WM: Objective criteria for the evaluation of clustering - methods. J Am Stat Assoc 66(336):846-850, 1971. - @ref: Hubert L and Arabie P: Comparing partitions. Journal of - 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) + return _compare_communities(vec1, vec2, method) def split_join_distance(comm1, comm2, remove_none=False): @@ -1630,6 +1530,11 @@ def split_join_distance(comm1, comm2, remove_none=False): it is close to zero, then one of the partitions is close to being a subpartition of the other). + B{Reference}: van Dongen S: Performance criteria for graph clustering and + Markov cluster experiments. Technical Report INS-R0012, National Research + Institute for Mathematics and Computer Science in the Netherlands, + Amsterdam, May 2000. + @param comm1: the first community structure as a membership list or as a L{Clustering} object. @param comm2: the second community structure as a membership list or @@ -1644,18 +1549,80 @@ def split_join_distance(comm1, comm2, remove_none=False): @return: the projection distance of C{comm1} from C{comm2} and vice versa in a tuple. The split-join distance is the sum of the two. - @newfield ref: Reference - @ref: van Dongen D: Performance criteria for graph clustering and Markov - cluster experiments. Technical Report INS-R0012, National Research - Institute for Mathematics and Computer Science in the Netherlands, - Amsterdam, May 2000. @see: L{compare_communities()} with C{method = "split-join"} if you are not interested in the individual projection distances but only the sum of them. """ import igraph._igraph + 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 _connected_components(graph, mode="strong"): + """Calculates the (strong or weak) connected components for + a given graph. + + @param mode: must be either C{"strong"} or C{"weak"}, depending on the + connected components being sought. Optional, defaults to C{"strong"}. + @return: a L{VertexClustering} object""" + return VertexClustering(graph, GraphBase.connected_components(graph, mode)) + + +def _clusters(graph, mode="strong"): + """Deprecated alias to L{Graph.connected_components()}.""" + deprecated( + "Graph.clusters() is deprecated; use Graph.connected_components() instead" + ) + return graph.connected_components(mode=mode) diff --git a/src/igraph/community.py b/src/igraph/community.py new file mode 100644 index 000000000..a3c6ce50b --- /dev/null +++ b/src/igraph/community.py @@ -0,0 +1,637 @@ +from igraph._igraph import GraphBase +from igraph.clustering import VertexDendrogram, VertexClustering +from igraph.utils import deprecated + +from typing import List, Sequence, Tuple + + +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. + + B{Reference}: A Clauset, MEJ Newman and C Moore: Finding community structure + in very large networks. I{Phys Rev E} 70, 066111 (2004). + + @param weights: edge attribute name or a list containing edge + weights + @return: an appropriate L{VertexDendrogram} object. + """ + merges, qs = GraphBase.community_fastgreedy(graph, weights) + optimal_count = _optimal_cluster_count_from_merges_and_modularity(graph, merges, qs) + return VertexDendrogram( + graph, merges, optimal_count, modularity_params={"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. + + B{References} + + - M. Rosvall and C. T. Bergstrom: Maps of information flow reveal + community structure in complex networks, I{PNAS} 105, 1118 (2008). + U{https://doi.org/10.1073/pnas.0706851105}, + U{https://arxiv.org/abs/0707.0609}. + - M. Rosvall, D. Axelsson, and C. T. Bergstrom: The map equation, + I{Eur Phys. J Special Topics} 178, 13 (2009). + U{https://doi.org/10.1140/epjst/e2010-01179-1}, + U{https://arxiv.org/abs/0906.1405}. + + @param edge_weights: name of an edge attribute or a list containing + edge weights. + @param vertex_weights: name of a 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. + """ + 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( + 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. + + B{Reference}: MEJ Newman: Finding community structure in networks using the + eigenvectors of matrices, arXiv:physics/0605087 + + @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. + """ + if clusters is None: + clusters = -1 + + kwds = {"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. + + Also note that the community _labels_ (numbers) have no semantic meaning + and igraph is free to re-number communities. If you use fixed labels, + igraph may still re-number the communities, but co-community membership + constraints will be respected: if you had two vertices with fixed labels + that belonged to the same community, they will still be in the same + community at the end. Similarly, if you had two vertices with fixed + labels that belonged to different communities, they will still be in + different communities at the end. + + B{Reference}: Raghavan, U.N. and Albert, R. and Kumara, S. Near linear + time algorithm to detect community structures in large-scale networks. + I{Phys Rev} E 76:036106, 2007. U{https://arxiv.org/abs/0709.2938}. + + @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. + """ + 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={"weights": weights}) + + +def _community_multilevel(graph, weights=None, return_levels=False, resolution=1): + """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 shrunk to a single vertex (while keeping the + total weight of the incident edges) and the process continues on the + next level. The algorithm stops when it is not possible to increase + the modularity anymore after shrinking the communities to vertices. + + This algorithm is said to run almost in linear time on sparse graphs. + + B{Reference}: VD Blondel, J-L Guillaume, R Lambiotte and E Lefebvre: Fast + unfolding of community hierarchies in large networks, I{J Stat Mech} + P10008 (2008). U{https://arxiv.org/abs/0803.0476} + + @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. + @param resolution: the resolution parameter to use in the modularity + measure. Smaller values result in a smaller number of larger clusters, + while higher values yield a large number of small clusters. The classical + modularity measure assumes a resolution parameter of 1. + @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. + """ + if graph.is_directed(): + raise ValueError("input graph must be undirected") + + modularity_params = {"weights": weights, "resolution": resolution} + if return_levels: + levels, qs = GraphBase.community_multilevel( + graph, weights, return_levels=True, resolution=resolution + ) + result = [] + for level, q in zip(levels, qs): + result.append( + VertexClustering(graph, level, q, modularity_params=modularity_params) + ) + else: + membership = GraphBase.community_multilevel( + graph, weights, return_levels=False, resolution=resolution + ) + result = VertexClustering( + graph, membership, modularity_params=modularity_params + ) + + 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. + + When edge weights are given, the ratio of betweenness and weight values + is used to choose which edges to remove first, as described in + M. E. J. Newman: Analysis of Weighted Networks (2004), Section C. + Thus, edges with large weights are treated as strong connections, + and will be removed later than weak connections having similar betweenness. + Weights are also used for calculating modularity. + + @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 that 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. Higher weights indicate stronger connections. + @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 clusters is None: + if qs is not None: + clusters = _optimal_cluster_count_from_merges_and_modularity( + graph, merges, qs + ) + else: + clusters = 1 + + return VertexDendrogram( + graph, merges, clusters, modularity_params={"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. + + B{References} + + - Reichardt J and Bornholdt S: Statistical mechanics of community + detection. I{Phys Rev E} 74:016110 (2006). + U{https://arxiv.org/abs/cond-mat/0603718}. + + - Traag VA and Bruggeman J: Community detection in networks + with positive and negative links. I{Phys Rev E} 80:036115 (2009). + U{https://arxiv.org/abs/0811.2329}. + + @keyword 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 + 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 + 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. + 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 + 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 + 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 + 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. + """ + membership = GraphBase.community_spinglass(graph, *args, **kwds) + if "weights" in kwds: + modularity_params = {"weights": kwds["weights"]} + else: + modularity_params = {} + return VertexClustering(graph, membership, modularity_params=modularity_params) + + +def _community_voronoi(graph, lengths=None, weights=None, mode="out", radius=None): + """Finds communities using Voronoi partitioning. + + This function finds communities using a Voronoi partitioning of vertices based + on the given edge lengths divided by the edge clustering coefficient. + The generator vertices are chosen to be those with the largest local relative + density within a radius, with the local relative density of a vertex defined + as C{s * m / (m + k)}, where C{s} is the strength of the vertex, C{m} is + the number of edges within the vertex's first order neighborhood, while C{k} + is the number of edges with only one endpoint within this neighborhood. + + B{References} + + - Deritei et al., Community detection by graph Voronoi diagrams, + I{New Journal of Physics} 16, 063007 (2014). + U{https://doi.org/10.1088/1367-2630/16/6/063007}. + - Molnár et al., Community Detection in Directed Weighted Networks using + Voronoi Partitioning, I{Scientific Reports} 14, 8124 (2024). + U{https://doi.org/10.1038/s41598-024-58624-4}. + + @param lengths: edge lengths, or C{None} to consider all edges as having + unit length. Voronoi partitioning will use edge lengths equal to + lengths / ECC where ECC is the edge clustering coefficient. + @param weights: edge weights, or C{None} to consider all edges as having + unit weight. Weights are used when selecting generator points, as well + as for computing modularity. + @param mode: specifies how to use the direction of edges when computing + distances from generator points. If C{"out"} (the default), distances + from generator points to all other nodes are considered following the + direction of edges. If C{"in"}, distances are computed in the reverse + direction (i.e., from all nodes to generator points). If C{"all"}, + edge directions are ignored and the graph is treated as undirected. + This parameter is ignored for undirected graphs. + @param radius: the radius/resolution to use when selecting generator points. + The larger this value, the fewer partitions there will be. Pass C{None} + to automatically select the radius that maximizes modularity. + @return: an appropriate L{VertexClustering} object with an extra attribute + called C{generators} (the generator vertices). + """ + # Convert mode string to proper enum value to avoid deprecation warning + if isinstance(mode, str): + mode_map = {"out": "out", "in": "in", "all": "all", "total": "all"} # alias + if mode.lower() in mode_map: + mode = mode_map[mode.lower()] + else: + raise ValueError(f"Invalid mode '{mode}'. Must be one of: out, in, all") + + membership, generators, modularity = GraphBase.community_voronoi(graph, lengths, weights, mode, radius) + + params = {"generators": generators} + modularity_params = {} + if weights is not None: + modularity_params["weights"] = weights + + clustering = VertexClustering( + graph, membership, modularity=modularity, params=params, modularity_params=modularity_params + ) + + clustering.generators = generators + return clustering + + +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. + + B{Reference}: Pascal Pons, Matthieu Latapy: Computing communities in large + networks using random walks, U{https://arxiv.org/abs/physics/0512106}. + + @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. + """ + merges, qs = GraphBase.community_walktrap(graph, weights, steps) + optimal_count = _optimal_cluster_count_from_merges_and_modularity(graph, merges, qs) + return VertexDendrogram( + graph, merges, optimal_count, modularity_params={"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=1.0, + beta=0.01, + initial_membership=None, + n_iterations=2, + node_weights=None, + node_in_weights=None, + **kwds, +): + """Finds the community structure of the graph using the Leiden + algorithm of Traag, van Eck & Waltman. + + B{Reference}: Traag, V. A., Waltman, L., & van Eck, N. J. (2019). From Louvain + to Leiden: guaranteeing well-connected communities. I{Scientific Reports}, + 9(1), 5233. doi: 10.1038/s41598-019-41695-z + + @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: 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. + @param node_in_weights: the inbound node weights used in the directed + variant of 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 with an extra attribute + called C{quality} that stores the value of the internal quality function + optimized by the algorithm. + """ + if objective_function.lower() not in ("cpm", "modularity"): + raise ValueError('objective_function must be "CPM" or "modularity".') + + if "resolution_parameter" in kwds: + deprecated( + "resolution_parameter keyword argument is deprecated, use " + "resolution=... instead" + ) + resolution = kwds.pop("resolution_parameter") + + if kwds: + raise TypeError("unexpected keyword argument") + + membership, quality = GraphBase.community_leiden( + graph, + edge_weights=weights, + node_weights=node_weights, + node_in_weights=node_in_weights, + resolution=resolution, + normalize_resolution=(objective_function == "modularity"), + beta=beta, + initial_membership=initial_membership, + n_iterations=n_iterations, + ) + + params = {"quality": quality} + + modularity_params = {"resolution": resolution} + if weights is not None: + modularity_params["weights"] = weights + + return VertexClustering( + graph, membership, params=params, modularity_params=modularity_params + ) + + +def _community_fluid_communities(graph, no_of_communities): + """Community detection based on fluids interacting on the graph. + + The algorithm is based on the simple idea of several fluids interacting + in a non-homogeneous environment (the graph topology), expanding and + contracting based on their interaction and density. Weighted graphs are + not supported. + + This function implements the community detection method described in: + Parés F, Gasulla DG, et. al. (2018) Fluid Communities: A Competitive, + Scalable and Diverse Community Detection Algorithm. + + @param no_of_communities: The number of communities to be found. Must be + greater than 0 and fewer than or equal to the number of vertices in the graph. + @return: an appropriate L{VertexClustering} object. + """ + # Validate input parameters + if no_of_communities <= 0: + raise ValueError("no_of_communities must be greater than 0") + + if no_of_communities > graph.vcount(): + raise ValueError("no_of_communities must be fewer than or equal to the number of vertices") + + # Check if graph is weighted (not supported) + if graph.is_weighted(): + raise ValueError("Weighted graphs are not supported by the fluid communities algorithm") + + # Handle directed graphs - the algorithm works on undirected graphs + # but can accept directed graphs (they are treated as undirected) + if graph.is_directed(): + import warnings + warnings.warn( + "Directed graphs are treated as undirected in the fluid communities algorithm", + UserWarning, + stacklevel=2 + ) + + membership = GraphBase.community_fluid_communities(graph, no_of_communities) + return VertexClustering(graph, membership) + + +def _modularity(self, membership, weights=None, resolution=1, directed=True): + """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-gamma*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}), and M{gamma} is a resolution + parameter that defaults to 1 for the classical definition of modularity. + 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. + + B{Reference}: MEJ Newman and M Girvan: Finding and evaluating community + structure in networks. I{Phys Rev E} 69 026113, 2004. + + @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. + @param resolution: the resolution parameter I{gamma} in the formula above. + The classical definition of modularity is retrieved when the resolution + parameter is set to 1. + @param directed: whether to consider edge directions if the graph is directed. + C{True} will use the directed variant of the modularity measure where the + in- and out-degrees of nodes are treated separately; C{False} will treat + directed graphs as undirected. + @return: the modularity score + """ + if isinstance(membership, VertexClustering): + if membership.graph != self: + raise ValueError("clustering object belongs to another graph") + return GraphBase.modularity( + self, membership.membership, weights, resolution, directed + ) + else: + return GraphBase.modularity(self, membership, weights, resolution, directed) + + +def _optimal_cluster_count_from_merges_and_modularity( + graph, merges: Sequence[Tuple[int, int]], qs: List[float] +) -> float: + """Helper function to find the optimal cluster count for a hierarchical + clustering of a graph, given the merge matrix and the list of modularity + values after each merge. + + Reverses the modularity vector as a side effect. + """ + no_of_comps = graph.vcount() - len(merges) + qs.reverse() + return qs.index(max(qs)) + no_of_comps diff --git a/igraph/configuration.py b/src/igraph/configuration.py similarity index 66% rename from igraph/configuration.py rename to src/igraph/configuration.py index c14cd0e55..9accb5325 100644 --- a/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -8,59 +8,23 @@ 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 -""" - -from ConfigParser import SafeConfigParser -import platform import os.path +from configparser import ConfigParser +from typing import IO, Optional, Sequence, Tuple, Union -def get_platform_image_viewer(): - """Returns the path of an image viewer on the given platform""" - 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"] - 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 == "Windows" or plat == "Microsoft": # Thanks to Dale Hunscher - # Use the built-in Windows image viewer, if available - return "start" - else: - # Unknown system - return "" - - -class Configuration(object): + +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 ============= @@ -68,9 +32,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 @@ -80,9 +44,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 @@ -113,25 +77,15 @@ class Configuration(object): - 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 @@ -145,16 +99,6 @@ class Configuration(object): vertices automatically if they don't fit within the vertex. Default: C{False}. - Remote repository settings - -------------------------- - - These settings specify how igraph should access remote graph repositories. - Currently only the Nexus repository is supported. All these settings are - stored in section C{remote}. - - - B{nexus.url}: the root URL of the Nexus repository. Default: - C{http://nexus.igraph.org}. - Shell settings -------------- @@ -162,21 +106,15 @@ class Configuration(object): embedded (e.g., IPython and its Qt console). These settings are stored in section C{shell}. - - B{ipython.inlining.Plot}: whether to show instances of the L{Plot} class + - 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 - # R0903: too few public methods - class Types(object): + class Types: """Static class for the implementation of custom getter/setter functions for configuration keys""" - def __init__(self): - pass - @staticmethod def setboolean(obj, section, key, value): """Sets a boolean value in the given configuration object. @@ -187,7 +125,7 @@ def setboolean(obj, section, key, value): @param value: the value itself. C{0}, C{false}, C{no} and C{off} means false, C{1}, C{true}, C{yes} and C{on} means true, everything else results in a C{ValueError} being thrown. - Values are case insensitive + Values are case-insensitive """ value = str(value).lower() if value in ("0", "false", "no", "off"): @@ -224,75 +162,45 @@ def setfloat(obj, section, key, value): obj.set(section, key, str(float(value))) _types = { - "boolean": { - "getter": SafeConfigParser.getboolean, - "setter": Types.setboolean - }, - "int": { - "getter": SafeConfigParser.getint, - "setter": Types.setint - }, - "float": { - "getter": SafeConfigParser.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") + _sections: Sequence[str] = ("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" - }, - - "remote.nexus.url": { - "default": "http://nexus.igraph.org" - }, - - "shell.ipython.inlining.Plot": { - "default": True, - "type": "boolean" - } + "general.shells": {"default": "IPythonShell,ClassicPythonShell"}, + "general.verbose": {"default": True, "type": "boolean"}, + "plotting.backend": {"default": "cairo"}, + "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 _instance = None + _filename: Optional[str] = None + """Name of the file that the configuration was loaded from, or ``None`` if + not known. + """ + def __init__(self, filename=None): """Creates a new configuration instance. - @param filename: file or file pointer to be read. Can be omitted. + @param filename: file or file-like object to be read. Can be omitted. """ - self._config = SafeConfigParser() + self._config = ConfigParser() self._filename = None # Create default sections 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"] @@ -300,7 +208,7 @@ def __init__(self, filename=None): self.load(filename) @property - def filename(self): + def filename(self) -> Optional[str]: """Returns the filename associated to the object. It is usually the name of the configuration file that was used when @@ -310,7 +218,7 @@ def filename(self): information.""" return self._filename - def _get(self, section, key): + def _get(self, section: str, key: str): """Internal function that returns the value of a given key in a given section.""" definition = self._definitions.get("%s.%s" % (section, key), {}) @@ -322,11 +230,11 @@ def _get(self, section, key): return getter(self._config, section, key) @staticmethod - def _item_to_section_key(item): + def _item_to_section_key(item: str) -> Tuple[str, str]: """Converts an item description to a section-key pair. @param item: the item to be converted - @return: if C{item} contains a period (C{.}), it is splitted into two parts + @return: if C{item} contains a period (C{.}), it is split into two parts at the first period, then the two parts are returned, so the part before the period is the section. If C{item} does not contain a period, the section is assumed to be C{general}, and the second part of the returned @@ -337,7 +245,7 @@ def _item_to_section_key(item): section, key = "general", item return section, key - def __contains__(self, item): + def __contains__(self, item: str) -> bool: """Checks whether the given configuration item is set. @param item: the configuration key to check. @@ -346,7 +254,7 @@ def __contains__(self, item): section, key = self._item_to_section_key(item) return self._config.has_option(section, key) - def __getitem__(self, item): + def __getitem__(self, item: str): """Returns the given configuration item. @param item: the configuration key to retrieve. @@ -356,11 +264,11 @@ def __getitem__(self, item): # Special case: retrieving all the keys within a section and # returning it in a dict keys = self._config.items(section) - return dict((key, self._get(section, key)) for key, _ in keys) + return {key: self._get(section, key) for key, _ in keys} else: return self._get(section, key) - def __setitem__(self, item, value): + def __setitem__(self, item: str, value): """Sets the given configuration item. @param item: the configuration key to set @@ -375,7 +283,7 @@ def __setitem__(self, item, value): setter = self._config.__class__.set return setter(self._config, section, key, value) - def __delitem__(self, item): + def __delitem__(self, item: str): """Deletes the given item from the configuration. If the item has a default value, the default value is written back instead @@ -388,14 +296,11 @@ def __delitem__(self, item): else: self._config.remove_option(section, key) - def has_key(self, item): + def has_key(self, item: str) -> bool: """Checks if the configuration has a given key. @param item: the key being sought""" - if "." in item: - section, key = item.split(".", 1) - else: - section, key = "general", item + section, key = self._item_to_section_key(item) return self._config.has_option(section, key) def load(self, stream=None): @@ -406,28 +311,40 @@ 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) - self._filename = getattr(stream, "name", None) + else: + file_was_open = False + + self._config.read_file(stream) + + filename = getattr(stream, "name", None) + self._filename = str(filename) if filename is not None else None + if file_was_open: stream.close() - def save(self, stream=None): + def save(self, stream: Optional[Union[str, IO[str]]] = None): """Saves the configuration. - @param stream: name of a file or a file object. The configuration will be saved - there. Can be omitted, in this case, the user-level configuration file will - be overwritten. + @param stream: name of a file or a file-like object. The configuration + will be saved there. Can be omitted, in this case, the user-level + configuration file will be overwritten. """ stream = stream or get_user_config_file() + if not hasattr(stream, "write") or not hasattr(stream, "close"): - stream = open(stream, "w") + stream = open(stream, "w") # type: ignore file_was_open = True - self._config.write(stream) + else: + file_was_open = False + + self._config.write(stream) # type: ignore + if file_was_open: - stream.close() + stream.close() # type: ignore @classmethod def instance(cls): @@ -443,12 +360,12 @@ def instance(cls): return cls._instance -def get_user_config_file(): +def get_user_config_file() -> str: """Returns the path where the user-level configuration file is stored""" return os.path.expanduser("~/.igraphrc") -def init(): +def init() -> Configuration: """Default mechanism to initiate igraph configuration This method loads the user-specific configuration file from the diff --git a/src/igraph/cut.py b/src/igraph/cut.py new file mode 100644 index 000000000..5aa26d9de --- /dev/null +++ b/src/igraph/cut.py @@ -0,0 +1,329 @@ +# vim:ts=4:sw=4:sts=4:et +# -*- coding: utf-8 -*- +"""Classes representing cuts and flows on graphs.""" + +from igraph._igraph import ( + GraphBase, +) +from igraph.clustering import VertexClustering + + +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: + + - C{value} - the value (capacity) of the cut. It is equal to + the number of edges if there are no capacities on the + edges. + + - C{partition} - vertex IDs in the parts created + after removing edges in the cut + + - C{cut} - edge IDs in the cut + + - C{es} - an edge selector restricted to the edges + in the cut. + + You can use indexing on this object to obtain lists of vertex IDs + for both sides of the partition. + + This class is usually not instantiated directly, everything + is taken care of by the functions that return cuts. + + Examples: + + >>> from igraph import Graph + >>> g = Graph.Ring(20) + >>> mc = g.mincut() + >>> print(mc.value) + 2.0 + >>> print(min(len(x) for x in mc)) + 1 + >>> mc.es["color"] = "red" + """ + + 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 + the functions that return cuts. + """ + # Input validation + if partition is None or cut is None: + raise ValueError("partition and cut must be given") + + # Set up a membership vector, initialize parent class + membership = [1] * graph.vcount() + for vid in partition: + membership[vid] = 0 + super().__init__(graph, membership) + + if value is None: + # Value of the cut not given, count the number of edges + value = len(cut) + self._value = float(value) + + self._partition = sorted(partition) + self._cut = cut + + def __repr__(self): + 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, + ) + + @property + def es(self): + """Returns an edge selector restricted to the cut""" + return self._graph.es.select(self.cut) + + @property + def partition(self): + """Returns the vertex IDs partitioned according to the cut""" + return list(self) + + @property + def cut(self): + """Returns the edge IDs in the cut""" + return self._cut + + @property + def value(self): + """Returns the sum of edge capacities in the cut""" + return self._value + + +class Flow(Cut): + """A flow of a given graph. + + This is a simple class used to represent flows returned by + L{Graph.maxflow}. It has the following attributes: + + - C{graph} - the graph on which this flow is defined + + - 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 + flow on edge M{i}. For undirected graphs, the direction of + the flow is not constrained (since the edges are undirected), + hence positive flow always means a flow from the smaller vertex + ID to the larger, while negative flow means a flow from the + larger vertex ID to the smaller. + + - C{cut} - edge IDs in the minimal cut corresponding to + the flow. + + - C{partition} - vertex IDs in the parts created + after removing edges in the cut + + - C{es} - an edge selector restricted to the edges + in the cut. + + This class is usually not instantiated directly, everything + is taken care of by L{Graph.maxflow}. + + Examples: + + >>> from igraph import Graph + >>> g = Graph.Ring(20) + >>> mf = g.maxflow(0, 10) + >>> print(mf.value) + 2.0 + >>> mf.es["color"] = "red" + """ + + def __init__(self, graph, value, flow, cut, partition): + """Initializes the flow. + + This should not be called directly, everything is + taken care of by L{Graph.maxflow}. + """ + super().__init__(graph, value, 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, + ) + + 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, + ) + + @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 + undirected), hence positive flow always means a flow from the smaller + vertex ID to the larger, while negative flow means a flow from the + 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. + + B{Reference}: JS Provan and DR Shier: A paradigm for listing (s,t)-cuts in + graphs. I{Algorithmica} 15, 351-372, 1996. + + @param source: the source vertex ID + @param target: the target vertex ID + @return: a list of L{Cut} objects. + """ + 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. + + B{Reference}: JS Provan and DR Shier: A paradigm for listing (s,t)-cuts in + graphs. I{Algorithmica} 15, 351-372, 1996. + + @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. + """ + 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/igraph/datatypes.py b/src/igraph/datatypes.py similarity index 63% rename from igraph/datatypes.py rename to src/igraph/datatypes.py index b7f3e1387..b16e89cfe 100644 --- a/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -2,29 +2,10 @@ # -*- coding: utf-8 -*- """Additional auxiliary data types""" -from itertools import islice +__all__ = ("Matrix",) -__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): +class Matrix: """Simple matrix data type. Of course there are much more advanced matrix data types for Python (for @@ -44,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 @@ -63,10 +43,9 @@ 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 @classmethod def Zero(cls, *args): """Creates a matrix filled with zeros. @@ -78,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. @@ -87,9 +65,8 @@ 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 xrange(min(result.shape)): + for i in range(min(result.shape)): result._data[i][i] = 1 return result @@ -104,11 +81,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 +105,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 +162,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,15 +176,19 @@ 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 + def __len__(self): + """Returns the number of rows in the matrix.""" + return len(self._data) + def __ne__(self, other): """Checks whether a given matrix is not equal to another one""" return not self == other @@ -227,8 +212,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 +247,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__ @@ -290,8 +275,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: @@ -341,160 +326,10 @@ 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.)) - border_width = float(kwds.get("border_width", 1.)) - 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 == False: - values = None - if values == 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. - 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.) - 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.) - 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. - for heading in col_names: - _, _, _, h, _, _ = context.text_extents(heading) - context.move_to(x+space_width, y + (dx+h)/2.) - context.show_text(heading) - y += dx - context.restore() - - # Draw matrix - x, y = ox, oy - if style is None: - fill = lambda: None - 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.) - else: - context.set_source_rgb(1., 1., 1.) - 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.) - 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., y+(dy+th)/2.) - 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.) - 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 @@ -506,8 +341,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 +354,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) @@ -537,19 +370,27 @@ 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 = {"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)) @@ -603,23 +444,38 @@ 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 """ - _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 +493,16 @@ def __str__(self): if rowcount * colcount < maxidx: rowcount += 1 - invmap = dict((v, k) for k, v in self._remap.iteritems()) + invmap = {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 = [] @@ -652,12 +510,12 @@ 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). Usage: - + >>> gen = UniqueIdGenerator() >>> gen["A"] 0 @@ -678,16 +536,17 @@ class UniqueIdGenerator(object): """ def __init__(self, id_generator=None, initial=None): - """Creates a new unique ID generator. `id_generator` specifies how do we - assign new IDs to elements that do not have an ID yet. If it is `None`, + """Creates a new unique ID generator. C{id_generator} specifies how do we + assign new IDs to elements that do not have an ID yet. If it is C{None}, elements will be assigned integer identifiers starting from 0. If it is an integer, elements will be assigned identifiers starting from the given - integer. If it is an iterator or generator, its `next` method will be + integer. If it is an iterator or generator, its C{next()} method will be called every time a new ID is needed.""" if id_generator is None: id_generator = 0 if isinstance(id_generator, int): import itertools + self._generator = itertools.count(id_generator) else: self._generator = id_generator @@ -697,38 +556,36 @@ def __init__(self, id_generator=None, initial=None): self.add(value) def __contains__(self, item): - """Checks whether `item` already has an ID or not.""" + """Checks whether C{item} already has an ID or not.""" return item in self._ids def __getitem__(self, item): - """Retrieves the ID corresponding to `item`. Generates a new ID for - `item` if it is the first time we request an ID for it.""" + """Retrieves the ID corresponding to C{item}. Generates a new ID for + C{item} if it is the first time we request an ID for it.""" 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): - """Overrides the ID for `item`.""" + """Overrides the ID for C{item}.""" 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): """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 {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(self._ids.keys(), key=self._ids.__getitem__) add = __getitem__ - - diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py new file mode 100644 index 000000000..05743728a --- /dev/null +++ b/src/igraph/drawing/__init__.py @@ -0,0 +1,321 @@ +""" +Drawing and plotting routines for igraph. + +igraph has two stable plotting backends at the moment: Cairo and Matplotlib. +It also has experimental support for plotly. + +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. Many of our gallery examples use the matplotlib backend. + +The plotly backend uses the U{plotly library } and, +like matplotlib, requires installation from PyPI. + +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. +""" + +from pathlib import Path +from warnings import warn + +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.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", +) + +# TODO: deprecate +Plot = CairoPlot + +# TODO: deprecate +DefaultGraphDrawer = CairoGraphDrawer + + +class DrawerDirectory: + """Static class that finds the object/backend drawer + + This directory is used by the __plot__ functions. + """ + + 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 cls: the class to resolve + @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 + """ + 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}", + ) from None + + +def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): + """Plots the given object to the given target. + + Positional and keyword arguments not explicitly mentioned here will be + passed down to the C{__plot__} method of the object being plotted. + Since you are most likely interested in the keyword arguments available + for graph plots, see L{Graph.__plot__} as well. + + @param obj: the object to be plotted + @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 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 the plot + will be stored there. If you are using the Cairo backend, an + appropriate Cairo surface will be attached to the file. If you are + using the matplotlib backend, the Figure will be saved to that file + using Figure.savefig with default parameters. The supported image + formats for Cairo are: PNG, PDF, SVG and PostScript; matplotlib might + support additional formats. + + - 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{None} -- If you are using the Cairo backend, 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. If you are using + the matplotlib backend, a Figure objet and an Axes are created and + the Axes is returned so you can manipulate it further. Similarly, if + you are using the plotly backend, a Figure object is returned. + + @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 + with two integers, it is interpreted as the width and height of the plot + (in pixels for PNG images and on-screen plots, or in points for PDF, + 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. 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}. 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 + 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. + + @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 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 + 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} + is C{None}. + + @keyword backend: the plotting backend to use; one of C{"cairo"}, + C{"matplotlib"} or C{"plotly"}. C{None} means to try to decide the backend + from the plotting target and the default igraph configuration object. + + @return: an appropriate L{CairoPlot} object for the Cairo backend, the + Matplotlib C{Axes} object for the Matplotlib backend, and the C{Figure} + object for the plotly backend. + + @see: Graph.__plot__ + """ + + VALID_BACKENDS = ("cairo", "matplotlib", "plotly") + + _, plt = find_matplotlib() + cairo = find_cairo() + plotly = find_plotly() + + backend = kwds.pop("backend", None) + + # Switch backend based on target (first) and config (second) if it was not + # selected explicitly + if backend is not None: + pass + elif hasattr(plt, "Axes") and isinstance(target, plt.Axes): + 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 backend not in VALID_BACKENDS: + raise ValueError(f"unknown plotting backend: {backend!r}") + + 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 isinstance(target, (str, Path)): + save_path = str(target) + target = None + else: + save_path = None + + if target is None: + if backend == "matplotlib": + # Use get current axes, customary in these cases + target = plt.gca() + 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,), stacklevel=1) + return + else: + result = plotter( + backend, + target, + palette=palette, + *args, # noqa: B026 + **kwds, + ) + + if save_path is not None: + if backend == "matplotlib": + target.figure.savefig(save_path) + elif backend == "plotly": + target.write_image(save_path) + + # For matplotlib, return the container artist, which makes it easier + # to manipulate post-facto. The user can always get the artist with + # plt.gca() - as we do, in fact. + if backend == "matplotlib": + return result + + return target + + # Cairo backend + inline = False + if target is None and _is_running_in_ipython(): + inline = kwds.get("inline") + if inline is None: + 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 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 new file mode 100644 index 000000000..d9da2e4a6 --- /dev/null +++ b/src/igraph/drawing/baseclasses.py @@ -0,0 +1,369 @@ +""" +Abstract base classes for the drawing routines. +""" + +from abc import ABCMeta, abstractmethod +from math import atan2, pi + +from .text import TextAlignment +from .utils import get_bezier_control_points_for_curved_edge, evaluate_cubic_bezier + +##################################################################### + + +class AbstractDrawer(metaclass=ABCMeta): + """Abstract class that serves as a base class for anything that + draws an igraph object.""" + + @abstractmethod + def draw(self, *args, **kwds): + """Abstract method, must be implemented in derived classes.""" + raise NotImplementedError + + +##################################################################### + + +class AbstractXMLRPCDrawer(AbstractDrawer): + """Abstract drawer that uses a remote service via XML-RPC + to draw something on a remote display. + """ + + 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 + C{None}, requests will be directed to the server proxy object + constructed by C{xmlrpclib.ServerProxy}; if not C{None}, the + given attribute will be looked up in the server proxy object. + """ + import xmlrpc.client + + url = self._resolve_hostname(url) + self.server = xmlrpc.client.ServerProxy(url) + if service is None: + self.service = self.server + else: + self.service = getattr(self.server, service) + + @staticmethod + def _resolve_hostname(url): + """Parses the given URL, resolves the hostname to an IP address + 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 urllib.parse import urlparse, urlunparse + import re + + url_parts = urlparse(url) + hostname = url_parts.netloc + if re.match("[0-9.:]+$", hostname): + # the hostname is already an IP address, possibly with a port + return url + + from socket import gethostbyname + + if ":" in hostname: + 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) + + +##################################################################### + + +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 + 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 = evaluate_cubic_bezier(x1, y1, *aux1, *aux2, x2, y2, 0.5) + else: + pos = ( + (src_vertex.position[0] + dest_vertex.position[0]) / 2.0, + (src_vertex.position[1] + dest_vertex.position[1]) / 2.0, + ) + + # 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) + + 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.0 / pi * atan2(y2 - y1, x2 - x1)) % 360 + # Try to keep text on its head + if 90 < rotation <= 270: + rotation = (180 + rotation) % 360 + return rotation + + +##################################################################### + + +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} and I{graph} has a "layout" + attribute, call this same function with the value of that + attribute. + + - If I{layout} is C{None} and I{graph} does not have a "layout" + attribute, 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): + layout = graph.layout(layout) + elif ( + (layout is None) + and hasattr(graph, "attributes") + and ("layout" in graph.attributes()) + ): + layout = AbstractGraphDrawer.ensure_layout(graph["layout"], graph=graph) + elif 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( + 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( + 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/igraph/drawing/coord.py b/src/igraph/drawing/cairo/coord.py similarity index 76% rename from igraph/drawing/coord.py rename to src/igraph/drawing/cairo/coord.py index 51253b76e..bebdd69fa 100644 --- a/igraph/drawing/coord.py +++ b/src/igraph/drawing/cairo/coord.py @@ -2,16 +2,14 @@ Coordinate systems and related plotting routines """ -from igraph.compat import property -from igraph.drawing.baseclasses import AbstractCairoDrawer -from igraph.drawing.utils import BoundingBox +from abc import abstractmethod -__license__ = "GPL" +from igraph.drawing.cairo.base import AbstractCairoDrawer +from igraph.drawing.utils import BoundingBox ##################################################################### -# pylint: disable-msg=R0922 -# R0922: Abstract class is only referenced 1 times + class CoordinateSystem(AbstractCairoDrawer): """Class implementing a coordinate system object. @@ -22,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): @@ -63,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 @@ -105,15 +95,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/cairo/dendrogram.py b/src/igraph/drawing/cairo/dendrogram.py new file mode 100644 index 000000000..202e057df --- /dev/null +++ b/src/igraph/drawing/cairo/dendrogram.py @@ -0,0 +1,248 @@ +"""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 + """ + super().__init__(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 = [ + self._item_box_size(dendro, 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 element in 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 element in 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/cairo/edge.py b/src/igraph/drawing/cairo/edge.py new file mode 100644 index 000000000..7535004c9 --- /dev/null +++ b/src/igraph/drawing/cairo/edge.py @@ -0,0 +1,343 @@ +""" +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, + get_bezier_control_points_for_curved_edge, + intersect_bezier_curve_and_circle, +) + +from .utils import find_cairo + +__all__ = ( + "AbstractCairoEdgeDrawer", + "AlphaVaryingEdgeDrawer", + "CairoArrowEdgeDrawer", + "DarkToLightEdgeDrawer", + "LightToDarkEdgeDrawer", + "TaperedEdgeDrawer", +) + +cairo = find_cairo() + + +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 + """ + 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 = 1.0 + arrow_width = 1.0 + 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 + + return VisualEdgeBuilder + + 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. + """ + ctx = self.context + 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.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): + """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: # TODO + return self.draw_loop_edge(edge, src_vertex) + + ctx = self.context + ctx.set_source_rgba(*edge.color) + ctx.set_line_width(edge.width) + ctx.move_to(*src_vertex.position) + + 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, + ) + ctx.curve_to(aux1[0], aux1[1], aux2[0], aux2[1], *dest_vertex.position) + else: + ctx.line_to(*dest_vertex.position) + + ctx.stroke() + + +class CairoArrowEdgeDrawer(AbstractCairoEdgeDrawer): + """Edge drawer implementation that draws undirected edges as + straight lines and directed edges as arrows. + """ + + def draw_directed_edge(self, edge, src_vertex, dest_vertex): + 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 + + # Draw the edge + ctx.set_source_rgba(*edge.color) + ctx.set_line_width(edge.width) + ctx.move_to(x1, y1) + + if edge.curved: + # Calculate the curve + aux1, aux2 = get_bezier_control_points_for_curved_edge( + x1, y1, x2, y2, edge.curved + ) + + # 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, aux2 = get_bezier_control_points_for_curved_edge( + x1, y1, x_arrow_mid, y_arrow_mid, edge.curved + ) + + # 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 + 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 + # 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 + ctx.line_to(x_arrow_mid, y_arrow_mid) + + # Draw the edge + ctx.stroke() + + # Draw the arrow head + ctx.move_to(x2, y2) + ctx.line_to(*aux_points[0]) + ctx.line_to(*aux_points[1]) + ctx.line_to(x2, y2) + ctx.fill() + + +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. + """ + + def draw_directed_edge(self, edge, src_vertex, dest_vertex): + 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 + ) + + ctx = self.context + + # 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.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), + ), + ] + ctx.move_to(*dest_pos) + ctx.line_to(*aux_points[0]) + ctx.line_to(*aux_points[1]) + ctx.line_to(*dest_pos) + ctx.fill() + + +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, 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),) + + def draw_directed_edge(self, edge, src_vertex, dest_vertex): + if src_vertex == dest_vertex: # TODO + return self.draw_loop_edge(edge, src_vertex) + + src_pos, dest_pos = src_vertex.position, dest_vertex.position + ctx = self.context + + # Set up the gradient + lg = cairo.LinearGradient(src_pos[0], src_pos[1], dest_pos[0], dest_pos[1]) + edge_color = edge.color[:3] + self.alpha_at_src + edge_color_end = edge_color[:3] + self.alpha_at_dest + lg.add_color_stop_rgba(0, *edge_color) + lg.add_color_stop_rgba(1, *edge_color_end) + + # Draw the edge + ctx.set_source(lg) + ctx.set_line_width(edge.width) + ctx.move_to(*src_pos) + ctx.line_to(*dest_pos) + ctx.stroke() + + +class LightToDarkEdgeDrawer(AlphaVaryingEdgeDrawer): + """Edge drawer implementation that draws undirected edges as + straight lines and directed edges by using an alpha value of + zero (total transparency) at the source and an alpha value of + one (full opacity) at the destination. The alpha value is + interpolated in-between. + """ + + def __init__(self, context, palette): + super().__init__(context, palette, 0.0, 1.0) + + +class DarkToLightEdgeDrawer(AlphaVaryingEdgeDrawer): + """Edge drawer implementation that draws undirected edges as + straight lines and directed edges by using an alpha value of + one (full opacity) at the source and an alpha value of zero + (total transparency) at the destination. The alpha value is + interpolated in-between. + """ + + 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..355e16f74 --- /dev/null +++ b/src/igraph/drawing/cairo/graph.py @@ -0,0 +1,426 @@ +""" +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{CairoVertexDrawer}. + @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}. + """ + super().__init__(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, + stacklevel=1, + ) + + 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( + visual_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..81de8b3bd --- /dev/null +++ b/src/igraph/drawing/cairo/histogram.py @@ -0,0 +1,58 @@ +"""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..8da160177 --- /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 callable(value_format): + 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..22180ae0e --- /dev/null +++ b/src/igraph/drawing/cairo/palette.py @@ -0,0 +1,52 @@ +"""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..5ac247b8f --- /dev/null +++ b/src/igraph/drawing/cairo/plot.py @@ -0,0 +1,368 @@ +""" +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,), stacklevel=1) + else: + if opacity < 1.0: + ctx.push_group() + else: + ctx.save() + plotter( + "cairo", + ctx, + bbox=bbox, + palette=palette, + *args, # noqa: B026 + **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", stacklevel=1 + ) + + 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..5b1d3f27d --- /dev/null +++ b/src/igraph/drawing/cairo/polygon.py @@ -0,0 +1,85 @@ +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 + """ + 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/igraph/drawing/text.py b/src/igraph/drawing/cairo/text.py similarity index 66% rename from igraph/drawing/text.py rename to src/igraph/drawing/cairo/text.py index 73104884d..039102af1 100644 --- a/igraph/drawing/text.py +++ b/src/igraph/drawing/cairo/text.py @@ -1,39 +1,16 @@ """ Drawers for labels on plots. - -@undocumented: test """ import re - -from igraph.compat import property -from igraph.drawing.baseclasses import AbstractCairoDrawer from warnings import warn -__all__ = ["TextAlignment", "TextDrawer"] -__license__ = "GPL" - -__docformat__ = "restructuredtext en" - -##################################################################### - -class TextAlignment(object): - """Text alignment constants.""" +from igraph.drawing.cairo.base import AbstractCairoDrawer - LEFT, CENTER, RIGHT = "left", "center", "right" - TOP, BOTTOM = "top", "bottom" - -##################################################################### +__all__ = ("CairoTextDrawer",) -class TextAlignment(object): - """Text alignment constants.""" - LEFT, CENTER, RIGHT = "left", "center", "right" - TOP, BOTTOM = "top", "bottom" - -##################################################################### - -class TextDrawer(AbstractCairoDrawer): +class CairoTextDrawer(AbstractCairoDrawer): """Class that draws text on a Cairo context. This class supports multi-line text unlike the original Cairo text @@ -43,9 +20,10 @@ class TextDrawer(AbstractCairoDrawer): 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(TextDrawer, self).__init__(context, (0, 0)) + """Constructs a new instance that will draw the given C{text} on + the given Cairo C{context}. + """ + super().__init__(context, (0, 0)) self.text = text self.halign = halign self.valign = valign @@ -53,14 +31,11 @@ def __init__(self, context, text="", halign="center", valign="center"): 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. + Since the class itself is an instance of L{AbstractCairoDrawer}, it + has an attribute named C{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 @@ -78,7 +53,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 @@ -88,34 +63,28 @@ 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): - """Calculates the layout of the current text. `x` and `y` denote the - coordinates where the drawing should start. If they are both ``None``, + def get_text_layout(self, x=None, y=None, width=None, wrap=False): + """Calculates the layout of the current text. C{x} and C{y} denote the + coordinates where the drawing should start. If they are both C{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. - :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 C{wrap} is C{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 C{(x, y, line)} tuples where C{x} and + C{y} refer to reference points on the Cairo canvas and C{line} + refers to the corresponding text that should be plotted there. """ ctx = self.context @@ -124,11 +93,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", stacklevel=1) + iterlines = self._iterlines() else: iterlines = self._iterlines() @@ -139,7 +108,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: @@ -154,34 +123,30 @@ 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 + context and filling it. C{x} and C{y} denote the coordinates where the + drawing should start. If they are both C{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. - :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 C{wrap} is C{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): @@ -203,13 +168,11 @@ def _iterlines_wrapped(self, width): 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 + The difference between this method and L{_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"): @@ -252,7 +215,7 @@ def text(self): def text(self, text): """Sets the text that will be drawn. - If `text` is ``None``, it will be mapped to an empty string; otherwise, + If C{text} is C{None}, it will be mapped to an empty string; otherwise, it will be converted to a string.""" if text is None: self._text = "" @@ -271,8 +234,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:]: @@ -283,10 +252,13 @@ def text_extents(self): return x_bearing, y_bearing, width, height, x_advance, y_advance + def test(): - """Testing routine for L{TextDrawer}""" + """Testing routine for L{CairoTextDrawer}""" import math - from igraph.drawing.utils import find_cairo + 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" @@ -294,10 +266,10 @@ def test(): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) context = cairo.Context(surface) - drawer = TextDrawer(context, text) + drawer = CairoTextDrawer(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() @@ -329,7 +301,8 @@ def mark_point(red, green, blue): context.fill() # Testing drawer.draw_at() - for i, halign in enumerate(("left", "center", "right")): + 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) @@ -352,16 +325,18 @@ 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.valign = TextDrawer.TOP + 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) @@ -378,6 +353,6 @@ def mark_point(red, green, blue): 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..ad183b229 --- /dev/null +++ b/src/igraph/drawing/cairo/utils.py @@ -0,0 +1,25 @@ +from igraph.drawing.utils import FakeModule +from typing import Any + +__all__ = ("find_cairo",) +__docformat__ = "restructuredtext en" + + +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 + except OSError: + # cairocffi throws an OSError if it is installed but libcairo-2 is + # not present on the system + pass + return module diff --git a/igraph/drawing/vertex.py b/src/igraph/drawing/cairo/vertex.py similarity index 52% rename from igraph/drawing/vertex.py rename to src/igraph/drawing/cairo/vertex.py index ff500995c..57cb2f39e 100644 --- a/igraph/drawing/vertex.py +++ b/src/igraph/drawing/cairo/vertex.py @@ -1,45 +1,17 @@ +"""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"] -__license__ = "GPL" +from .base import AbstractCairoDrawer -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. +__all__ = ("AbstractCairoVertexDrawer", "CairoVertexDrawer") - @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 - - 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") class AbstractCairoVertexDrawer(AbstractVertexDrawer, AbstractCairoDrawer): """Abstract base class for vertex drawers that draw on a Cairo canvas.""" @@ -60,40 +32,53 @@ def __init__(self, context, bbox, palette, layout): AbstractCairoDrawer.__init__(self, context, bbox) 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): 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 - position = dict(func=self.layout.__getitem__) + font = "sans-serif" + label_size = 14.0 + position = {"func": self.layout.__getitem__} shape = ("circle", ShapeDrawerDirectory.resolve_default) - size = 20.0 + size = 20.0 + width = None + height = None + return VisualVertexBuilder def draw(self, visual_vertex, vertex, coords): context = self.context - visual_vertex.shape.draw_path(context, \ - coords[0], coords[1], 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) context.fill_preserve() context.set_source_rgba(*visual_vertex.frame_color) context.set_line_width(visual_vertex.frame_width) context.stroke() - - diff --git a/src/igraph/drawing/colors.py b/src/igraph/drawing/colors.py new file mode 100644 index 000000000..687b6871d --- /dev/null +++ b/src/igraph/drawing/colors.py @@ -0,0 +1,2041 @@ +# vim:ts=4:sw=4:sts=4:et +# -*- coding: utf-8 -*- +""" +Color handling functions. +""" + +from abc import ABCMeta, abstractmethod +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", + "default_edge_colors", + "known_colors", +) + + +class Palette(metaclass=ABCMeta): + """Base class of color palettes. + + Color palettes are mappings that assign integers from the range + 0..M{n-1} to colors (4-tuples). M{n} is called the size or length + of the palette. C{igraph} comes with a number of predefined palettes, + so this class is useful for you only if you want to define your + own palette. This can be done by subclassing this class and implementing + the L{Palette._get} method as necessary. + + 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 = {} + + def clear_cache(self): + """Clears the result cache. + + The return values of L{Palette.get} are cached. Use this method + to clear the cache. + """ + self._cache = {} + + def get(self, v): + """Returns the given color from the palette. + + Values are cached: if the specific value given has already been + looked up, its value will be returned from the cache instead of + calculating it again. Use L{Palette.clear_cache} to clear the cache + if necessary. + + @note: you shouldn't override this method in subclasses, override + L{_get} instead. If you override this method, lookups in the + L{known_colors} dict won't work, so you won't be able to refer to + colors by names or RGBA quadruplets, only by integer indices. The + caching functionality will disappear as well. However, + feel free to override this method if this is exactly the behaviour + you want. + + @param v: the color to be retrieved. If it is an integer, it is + passed to L{Palette._get} to be translated to an RGBA quadruplet. + Otherwise it is passed to L{color_name_to_rgb()} to determine the + RGBA values. + + @return: the color as an RGBA quadruplet""" + if isinstance(v, list): + v = tuple(v) + try: + return self._cache[v] + except KeyError: + pass + if isinstance(v, str): + result = color_name_to_rgba(v) + elif hasattr(v, "__iter__"): # lists, tuples etc + return v # no need to cache + else: + if v < 0: + raise IndexError("color index must be non-negative") + if v >= self._length: + raise IndexError("color index too large") + result = self._get(v) + self._cache[v] = result + return result + + def get_many(self, colors): + """Returns multiple colors from the palette. + + Values are cached: if the specific value given has already been + looked upon, its value will be returned from the cache instead of + calculating it again. Use L{Palette.clear_cache} to clear the cache + if necessary. + + @param colors: the list of colors to be retrieved. The palette class + tries to make an educated guess here: if it is not possible to + interpret the value you passed here as a list of colors, the + class will simply try to interpret it as a single color by + forwarding the value to L{Palette.get}. + @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, (str, int)): + # Single color name or index + return [self.get(colors)] + # 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. + + You can safely assume that v is an integer in the range 0..M{n-1} + where M{n} is the size of the palette. + + @param v: numerical index of the color to be retrieved + @return: a 4-tuple containing the RGBA values""" + raise NotImplementedError + + __getitem__ = get + + @property + def length(self): + """Returns the number of colors in this palette""" + return self._length + + def __len__(self): + """Returns the number of colors in this palette""" + return self._length + + def __plot__(self, backend, context, *args, **kwds): + """Plots the colors of the palette on the given Cairo context/mpl Axes + + 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}. + + - C{grid_width}: line width of the grid that separates palette cells. + 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{0}. Fractional widths are also allowed. + + Keyword arguments in matplotlib are passes to Axes.imshow. + """ + 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) + + +class GradientPalette(Palette): + """Base class for gradient palettes + + Gradient palettes contain a gradient between two given colors. + + Example: + + >>> pal = GradientPalette("red", "blue", 5) + >>> pal.get(0) + (1.0, 0.0, 0.0, 1.0) + >>> pal.get(2) + (0.5, 0.0, 0.5, 1.0) + >>> pal.get(4) + (0.0, 0.0, 1.0, 1.0) + """ + + def __init__(self, color1, color2, n=256): + """Creates a gradient palette. + + @param color1: the color where the gradient starts. + @param color2: the color where the gradient ends. + @param n: the number of colors in the palette. + """ + super().__init__(n) + self._color1 = color_name_to_rgba(color1) + self._color2 = color_name_to_rgba(color2) + + def _get(self, v): + """Returns the color corresponding to the given color index. + + @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) + ) + + +class AdvancedGradientPalette(Palette): + """Advanced gradient that consists of more than two base colors. + + Example: + + >>> pal = AdvancedGradientPalette(["red", "black", "blue"], n=9) + >>> pal.get(2) + (0.5, 0.0, 0.0, 1.0) + >>> pal.get(7) + (0.0, 0.0, 0.75, 1.0) + """ + + def __init__(self, colors, indices=None, n=256): + """Creates an advanced gradient palette + + @param colors: the colors in the gradient. + @param indices: the color indices belonging to the given colors. If + C{None}, the colors are distributed equidistantly + @param n: the total number of colors in the palette + """ + super().__init__(n) + + if indices is None: + 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 = 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) + ] + + def _get(self, v): + """Returns the color corresponding to the given color index. + + @param v: numerical index of the color to be retrieved + @return: a 4-tuple containing the RGBA values""" + colors = self._colors + 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.0, 0.0, 1.0) + + +class RainbowPalette(Palette): + """A palette that varies the hue of the colors along a scale. + + Colors in a rainbow palette all have the same saturation, value and + alpha components, while the hue is varied between two given extremes + linearly. This palette has the advantage that it wraps around nicely + if the hue is varied between zero and one (which is the default). + + Example: + + >>> pal = RainbowPalette(n=120) + >>> pal.get(0) + (1.0, 0.0, 0.0, 1.0) + >>> pal.get(20) + (1.0, 1.0, 0.0, 1.0) + >>> pal.get(40) + (0.0, 1.0, 0.0, 1.0) + >>> pal = RainbowPalette(n=120, s=1, v=0.5, alpha=0.75) + >>> pal.get(60) + (0.0, 0.5, 0.5, 0.75) + >>> pal.get(80) + (0.0, 0.0, 0.5, 0.75) + >>> pal.get(100) + (0.5, 0.0, 0.5, 0.75) + >>> pal = RainbowPalette(n=120) + >>> pal2 = RainbowPalette(n=120, start=0.5, end=0.5) + >>> pal.get(60) == pal2.get(0) + True + >>> pal.get(90) == pal2.get(30) + True + + This palette was modeled after the C{rainbow} command of R. + """ + + def __init__(self, n=256, s=1, v=1, start=0, end=1, alpha=1): + """Creates a rainbow palette. + + @param n: the number of colors in the palette. + @param s: the saturation of the colors in the palette. + @param v: the value component of the colors in the palette. + @param start: the hue at which the rainbow begins (between 0 and 1). + @param end: the hue at which the rainbow ends (between 0 and 1). + @param alpha: the alpha component of the colors in the palette. + """ + super().__init__(n) + self._s = float(clamp(s, 0, 1)) + self._v = float(clamp(v, 0, 1)) + self._alpha = float(clamp(alpha, 0, 1)) + self._start = float(start) + if end == self._start: + end += 1 + self._dh = (end - self._start) / n + + def _get(self, v): + """Returns the color corresponding to the given color index. + + @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) + + +class PrecalculatedPalette(Palette): + """A palette that returns colors from a pre-calculated list of colors""" + + 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.""" + super().__init__(len(items)) + for idx, color in enumerate(items): + if isinstance(color, str): + color = color_name_to_rgba(color) + self._cache[idx] = color + + def _get(self, v): + """This method will only be called if the requested color index is + outside the size of the palette. In that case, we throw an exception""" + raise ValueError("palette index outside bounds: %s" % v) + + +class ClusterColoringPalette(PrecalculatedPalette): + """A palette suitable for coloring vertices when plotting a clustering. + + This palette tries to make sure that the colors are easily distinguishable. + This is achieved by using a set of base colors and their lighter and darker + variants, depending on the number of elements in the palette. + + When the desired size of the palette is less than or equal to the number of + base colors (denoted by M{n}), only the bsae colors will be used. When the + size of the palette is larger than M{n} but less than M{2*n}, the base colors + and their lighter variants will be used. Between M{2*n} and M{3*n}, the + base colors and their lighter and darker variants will be used. Above M{3*n}, + more darker and lighter variants will be generated, but this makes the individual + colors less and less distinguishable. + """ + + def __init__(self, n): + 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) + colors = base_colors[:] + + blocks_to_add = ceil(float(n - num_base_colors) / num_base_colors) + ratio_increment = 1.0 / (ceil(blocks_to_add / 2.0) + 1) + + adding_darker = True + ratio = ratio_increment + while len(colors) < n: + if adding_darker: + new_block = [darken(color, ratio) for color in base_colors] + else: + new_block = [lighten(color, ratio) for color in base_colors] + ratio += ratio_increment + colors.extend(new_block) + adding_darker = not adding_darker + + colors = colors[0:n] + super().__init__(colors) + + +def clamp(value, min_value, max_value): + """Clamps the given value between min and max""" + if value > max_value: + return max_value + if value < min_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. + + This is done by calling L{color_name_to_rgba} and then throwing away + the alpha value. + + @see: color_name_to_rgba for more details about what formats are + understood by this function. + """ + 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. + + Examples: + + >>> color_name_to_rgba("red") + (1.0, 0.0, 0.0, 1.0) + >>> color_name_to_rgba("#ff8000") == (1.0, 128/255.0, 0.0, 1.0) + True + >>> color_name_to_rgba("#ff800080") == (1.0, 128/255.0, 0.0, 128/255.0) + True + >>> color_name_to_rgba("#08f") == (0.0, 136/255.0, 1.0, 1.0) + True + >>> color_name_to_rgba("rgb(100%, 50%, 0%)") + (1.0, 0.5, 0.0, 1.0) + >>> color_name_to_rgba("rgba(100%, 50%, 0%, 25%)") + (1.0, 0.5, 0.0, 0.25) + >>> color_name_to_rgba("hsla(120, 100%, 50%, 0.5)") + (0.0, 1.0, 0.0, 0.5) + >>> color_name_to_rgba("hsl(60, 100%, 50%)") + (1.0, 1.0, 0.0, 1.0) + >>> color_name_to_rgba("hsv(60, 100%, 100%)") + (1.0, 1.0, 0.0, 1.0) + + @param color: the color to be converted in one of the following formats: + - B{CSS3 color specification}: C{#rrggbb}, C{#rgb}, C{#rrggbbaa}, C{#rgba}, + C{rgb(red, green, blue)}, C{rgba(red, green, blue, alpha)}, + C{hsl(hue, saturation, lightness)}, C{hsla(hue, saturation, lightness, alpha)}, + C{hsv(hue, saturation, value)} and C{hsva(hue, saturation, value, alpha)} + where the components are given as hexadecimal numbers in the first four + cases and as decimals or percentages (0%-100%) in the remaining cases. + Red, green and blue components are between 0 and 255; hue is between 0 + and 360; saturation, lightness and value is between 0 and 100; alpha is + between 0 and 1. + - B{Valid HTML color names}, i.e. those that are present in the HTML 4.0 + specification + - B{Valid X11 color names}, see U{http://en.wikipedia.org/wiki/X11_color_names} + - B{Red-green-blue components} given separately in either a comma-, slash- or + whitespace-separated string or a list or a tuple, in the range of 0-255. + An alpha value of 255 (maximal opacity) will be assumed. + - B{Red-green-blue-alpha components} given separately in either a comma-, slash- + or whitespace-separated string or a list or a tuple, in the range of 0-255 + - B{A single palette index} given either as a string or a number. Uses + the palette given in the C{palette} parameter of the method call. + @param palette: the palette to be used if a single number is passed to + the method. Must be an instance of L{colors.Palette}. + + @return: the RGBA values corresponding to the given color in a 4-tuple. + Since these colors are primarily used by Cairo routines, the tuples + contain floats in the range 0.0-1.0 + """ + if not isinstance(color, str): + if hasattr(color, "__iter__"): + components = list(color) + else: + # A single index is given as a number + try: + components = palette.get(color) + except AttributeError: + raise ValueError( + "palette index used when no palette was given" + ) from None + if len(components) < 4: + components += [1.0] * (4 - len(components)) + else: + if color[0] == "#": + color = color[1:] + if len(color) == 3: + 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.0 / 255.0 for i in color] + elif len(color) == 6: + 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.0 for i in (0, 2, 4, 6)] + elif color.lower() in known_colors: + components = known_colors[color.lower()] + else: + color_mode = "rgba" + 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_mode = mode[:-1] + if mode[0] == "h": + maximums = (360.0, 100.0, 100.0, 1.0) + break + + if " " in color or "/" in color or "," in color: + color = color.replace(",", " ").replace("/", " ") + components = color.split() + for idx, comp in enumerate(components): + if comp[-1] == "%": + components[idx] = float(comp[:-1]) / 100.0 + else: + components[idx] = float(comp) / maximums[idx] + if len(components) < 4: + components += [1.0] * (4 - len(components)) + if color_mode[:3] == "hsv": + components = hsva_to_rgba(*components) + elif color_mode[:3] == "hsl": + components = hsla_to_rgba(*components) + else: + components = palette.get(int(color)) + + # At this point, the components are floats + 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. + + The HTML format is simply given by C{#rrggbbaa}, where C{rr} gives + the red component in hexadecimal format, C{gg} gives the green + component C{bb} gives the blue component and C{gg} gives the + alpha level. The alpha level is optional. + """ + color = [int(clamp(component * 256, 0, 255)) for component in color] + if len(color) == 4: + 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. + + This is done by mixing the original color with black using the given + ratio. A ratio of 1.0 will yield a completely black color, a ratio + of 0.0 will yield the original color. The alpha values are left intact. + """ + ratio = 1.0 - ratio + red, green, blue, alpha = color + return (red * ratio, green * ratio, blue * ratio, alpha) + + +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. + + Each of the HSLA coordinates must be in the range [0, 1]. + """ + # 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.0 + h1 = int(h1) + if h1 < 3: + if h1 < 1: + return (c + m, x + m, m, alpha) + elif h1 < 2: + return (x + m, c + m, m, alpha) + else: + return (m, c + m, x + m, alpha) + else: + if h1 < 4: + return (m, x + m, c + m, alpha) + elif h1 < 5: + return (x + m, m, c + m, alpha) + else: + return (c + m, m, x + m, alpha) + + +def hsl_to_rgb(h, s, l): # noqa: E741 + """Converts a color given by its HSL coordinates (hue, saturation, + lightness) to RGB coordinates. + + Each of the HSL coordinates must be in the range [0, 1]. + """ + return hsla_to_rgba(h, s, l)[:3] + + +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. + + Each of the HSVA coordinates must be in the range [0, 1]. + """ + # 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 + h1 = int(h1) + if h1 < 3: + if h1 < 1: + return (c + m, x + m, m, alpha) + elif h1 < 2: + return (x + m, c + m, m, alpha) + else: + return (m, c + m, x + m, alpha) + else: + if h1 < 4: + return (m, x + m, c + m, alpha) + elif h1 < 5: + return (x + m, m, c + m, alpha) + else: + return (c + m, m, x + m, alpha) + + +def hsv_to_rgb(h, s, v): + """Converts a color given by its HSV coordinates (hue, saturation, + value) to RGB coordinates. + + Each of the HSV coordinates must be in the range [0, 1]. + """ + 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). + + Each of the RGBA coordinates must be in the range [0, 1]. + """ + alpha = float(alpha) + rgb_min, rgb_max = float(min(r, g, b)), float(max(r, g, b)) + + if rgb_min == rgb_max: + return 0.0, 0.0, rgb_min, alpha + + lightness = (rgb_min + rgb_max) / 2.0 + d = rgb_max - rgb_min + if lightness > 0.5: + sat = d / (2 - rgb_max - rgb_min) + else: + sat = d / (rgb_max + rgb_min) + + d *= 6.0 + if rgb_max == r: + hue = (g - b) / d + if g < b: + hue += 1 + elif rgb_max == g: + hue = 1 / 3.0 + (b - r) / d + else: + 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). + + Each of the RGBA coordinates must be in the range [0, 1]. + """ + # This is based on the formulae found at: + # http://en.literateprograms.org/RGB_to_HSV_color_space_conversion_(C) + rgb_min, rgb_max = float(min(r, g, b)), float(max(r, g, b)) + alpha = float(alpha) + value = float(rgb_max) + if value <= 0: + return 0.0, 0.0, 0.0, alpha + sat = 1.0 - rgb_min / value + if sat <= 0: + return 0.0, 0.0, value, alpha + d = rgb_max - rgb_min + r = (r - rgb_min) / d + g = (g - rgb_min) / d + b = (b - rgb_min) / d + rgb_max = max(r, g, b) + if rgb_max == r: + hue = 0.0 + (g - b) / 6.0 + if hue < 0: + hue += 1 + elif rgb_max == g: + hue = 1 / 3.0 + (b - r) / 6.0 + else: + 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). + + Each of the RGB coordinates must be in the range [0, 1]. + """ + 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). + + Each of the RGB coordinates must be in the range [0, 1]. + """ + 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. + + This is done by mixing the original color with white using the given + ratio. A ratio of 1.0 will yield a completely white color, a ratio + 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, + ) + + +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), + "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), + "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"), + "red-blue": GradientPalette("red", "blue"), + "red-purple-blue": AdvancedGradientPalette(["red", "purple", "blue"]), + "red-green": GradientPalette("red", "green"), + "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%)"] + ), +} diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py new file mode 100644 index 000000000..00f77f402 --- /dev/null +++ b/src/igraph/drawing/graph.py @@ -0,0 +1,561 @@ +""" +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.drawing.baseclasses import AbstractGraphDrawer, AbstractXMLRPCDrawer + +__all__ = ("CytoscapeGraphDrawer", "__plot__") + + +class CytoscapeGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): + """Graph drawer that sends/receives graphs to/from Cytoscape using + CytoscapeRPC. + + This graph drawer cooperates with U{Cytoscape} + using U{CytoscapeRPC}. + You need to install the CytoscapeRPC plugin first and start the + XML-RPC server on a given port (port 9000 by default) from the + appropriate Plugins submenu in Cytoscape. + + Graph, vertex and edge attributes are transferred to Cytoscape whenever + possible (i.e. when a suitable mapping exists between a Python type + and a Cytoscape type). If there is no suitable Cytoscape type for a + Python type, the drawer will use a string attribute on the Cytoscape + side and invoke C{str()} on the Python attributes. + + If an attribute to be created on the Cytoscape side already exists with + a different type, an underscore will be appended to the attribute name + to resolve the type conflict. + + You can use the C{network_id} attribute of this class to figure out the + network ID of the last graph drawn with this drawer. + """ + + 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().__init__(url, "Cytoscape") + self.network_id = None + + 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. + @param create_view: whether to create a view for the network + in Cytoscape.The default is C{True}. + @keyword node_ids: specifies the identifiers of the nodes to + be used in Cytoscape. This must either be the name of a + 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 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, + stacklevel=1, + ) + + cy = self.service + + # Create the network + if not create_view: + 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.", + stacklevel=1, + ) + network_id = cy.createNetwork(name) + else: + network_id = cy.createNetwork(name) + self.network_id = network_id + + # Create the nodes + if "node_ids" in kwds: + node_ids = kwds["node_ids"] + if isinstance(node_ids, str): + node_ids = graph.vs[node_ids] + else: + node_ids = list(range(graph.vcount())) + node_ids = [str(identifier) for identifier in node_ids] + cy.createNodes(network_id, node_ids) + + # Create the edges + edgelists = [[], []] + 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, + ) + + if "layout" in kwds: + # Calculate/get the layout of the graph + 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.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 + # being transferred + cy.performDefaultLayout(network_id) + + # Send the network attributes + attr_names = set(cy.getNetworkAttributeNames()) + for attr in graph.attributes(): + cy_type, value = self.infer_cytoscape_type([graph[attr]]) + value = value[0] + if value is None: + continue + + # Resolve type conflicts (if any) + try: + while ( + attr in attr_names and cy.getNetworkAttributeType(attr) != cy_type + ): + attr += "_" + except Fault: + # getNetworkAttributeType is not available in some older versions + # so we simply pass here + pass + cy.addNetworkAttributes(attr, cy_type, {network_id: value}) + + # Send the node attributes + 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 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: + attr += "_" + # Send the attribute values + cy.addNodeAttributes(attr, cy_type, values, True) + + # Send the edge attributes + 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 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: + attr += "_" + # Send the attribute values + cy.addEdgeAttributes(attr, cy_type, values) + + 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 + of vertices and edges are not converted by default. Use the + C{keep_canonical_names} parameter to retrieve these attributes as well. + + @param name: the name of the network in Cytoscape. + @param directed: whether the network is directed. + @param keep_canonical_names: whether to keep the C{canonicalName} + vertex/edge attributes that are added automatically by Cytoscape + @return: an appropriately constructed igraph L{Graph}.""" + from igraph import Graph + + cy = self.service + + # Check the version number. Anything older than 1.3 is bad. + version = cy.version() + if " " in version: + 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" + ) + + # 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().items() if v == name] + if not network_id: + raise ValueError("no such network: %r" % name) + elif len(network_id) > 1: + raise ValueError("more than one network exists with name: %r" % name) + network_id = network_id[0] + + # Fetch the list of all the nodes and edges + vertices = cy.getNodes(network_id) + edges = cy.getEdges(network_id) + n, m = len(vertices), len(edges) + + # Fetch the graph attributes + graph_attrs = cy.getNetworkAttributes(network_id) + + # Fetch the vertex attributes + vertex_attr_names = cy.getNodeAttributeNames() + vertex_attrs = {} + for attr_name in vertex_attr_names: + if attr_name == "canonicalName" and not keep_canonical_names: + 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 zip(vertices, has_attr) if ok] + ) + attrs = [None] * n + for idx, value in zip(filtered, values): + attrs[idx] = value + vertex_attrs[attr_name] = attrs + + # Fetch the edge attributes + edge_attr_names = cy.getEdgeAttributeNames() + edge_attrs = {} + for attr_name in edge_attr_names: + if attr_name == "canonicalName" and not keep_canonical_names: + 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 zip(edges, has_attr) if ok] + ) + attrs = [None] * m + for idx, value in zip(filtered, values): + attrs[idx] = value + edge_attrs[attr_name] = attrs + + # Create a vertex name index + vertex_name_index = {v: k for k, v in enumerate(vertices)} + del vertices + + # Remap the edges list to numeric IDs + edge_list = [] + for edge in edges: + parts = edge.split() + 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, + ) + + @staticmethod + def infer_cytoscape_type(values): + """Returns a Cytoscape type that can be used to represent all the + values in C{values} and an appropriately converted copy of C{values} that + is suitable for an XML-RPC call. Note that the string type in + Cytoscape is used as a catch-all type; if no other type fits, attribute + values will be converted to string and then posted to Cytoscape. + + C{None} entries are allowed in C{values}, they will be ignored on the + Cytoscape side. + """ + 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, 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, 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. + + 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. + + 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 + L{GephiGraphStreamer} that is responsible for generating the JSON objects, + encoding them and writing them to a file-like object. If you want to customize + the encoding process, this is the object where you can tweak things to your taste. + """ + + def __init__(self, conn=None, *args, **kwds): + """Constructs a Gephi graph streaming drawer that will post graphs to the + given Gephi connection. If C{conn} is C{None}, the remaining arguments of + the constructor are forwarded intact to the constructor of + L{GephiConnection} in order to create a connection. This means that any of + the following are valid: + + - C{GephiGraphStreamingDrawer()} will construct a drawer that connects to + workspace 0 of the local Gephi instance on port 8080. + + - C{GephiGraphStreamingDrawer(workspace=2)} will connect to workspace 2 + of the local Gephi instance on port 8080. + + - C{GephiGraphStreamingDrawer(port=1234)} will connect to workspace 0 + of the local Gephi instance on port 1234. + + - C{GephiGraphStreamingDrawer(host="remote", port=1234, workspace=7)} + will connect to workspace 7 of the Gephi instance on host C{remote}, + port 1234. + + - C{GephiGraphStreamingDrawer(url="http://remote:1234/workspace7)} is + the same as above, but with an explicit URL. + """ + super().__init__() + + from igraph.remote.gephi import GephiGraphStreamer, GephiConnection + + self.connection = conn or GephiConnection(*args, **kwds) + self.streamer = GephiGraphStreamer() + + def draw(self, graph, *args, **kwds): + """Draws (i.e. sends) the given graph to the destination of the drawer using + the Gephi graph streaming API. + + The following keyword arguments are allowed: + + - C{encoder} lets one specify an instance of C{json.JSONEncoder} that + will be used to encode the JSON objects. + """ + # Positional arguments are not used + if args: + warn( + "Positional arguments to plot functions are ignored " + "and will be deprecated soon.", + DeprecationWarning, + stacklevel=1, + ) + + 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{GraphBase.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), + ) + return drawer.draw(self, *args, **kwds) 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..bda21703f --- /dev/null +++ b/src/igraph/drawing/matplotlib/dendrogram.py @@ -0,0 +1,180 @@ +""" +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 dendro: the dendrogram object + @param orientation: 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. + + Other keyword arguments are passed to mpl.patches.Polygon. + + @param dendro: the igraph.Dendrogram to plot. + @param 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{utils.str_to_orientation}). + """ + from igraph.layout import Layout + + ax = self.context + + # Pop unneeded arguments from kwds that are passed down to us by + # default but cannot be interpreted by Matplotlib + kwds.pop("palette", None) + + # 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 element in 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 element in 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..c4b21cab1 --- /dev/null +++ b/src/igraph/drawing/matplotlib/edge.py @@ -0,0 +1,592 @@ +"""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 ( + get_bezier_control_points_for_curved_edge, + FakeModule, +) + +__all__ = ("MatplotlibEdgeDrawer", "EdgeCollection") + +mpl, plt = find_matplotlib() +try: + PatchCollection = mpl.collections.PatchCollection +except AttributeError: + PatchCollection = FakeModule + + +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 = 15 + arrow_width = 15 + 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 + background = None + align_label = False + zorder = 1 + loop_size = 30 + + return VisualEdgeBuilder + + def build_patch(self, edge, src_vertex, dest_vertex): + art = mpl.patches.PathPatch( + mpl.path.Path([[0, 0]]), + edgecolor=edge.color, + facecolor=edge.color if src_vertex != dest_vertex else "none", + linewidth=edge.width, + zorder=edge.zorder, + clip_on=True, + ) + return art + + # The following two methods are replaced by dummy functions, the rest is + # taken care of in EdgeCollection for efficiency + def draw_directed_edge(self, edge, src_vertex, dest_vertex): + return self.build_patch(edge) + + def draw_undirected_edge(self, edge, src_vertex, dest_vertex): + return self.build_patch(edge) + + +class EdgeCollection(PatchCollection): + def __init__(self, *args, **kwargs): + kwargs["match_original"] = True + self._visual_vertices = kwargs.pop("visual_vertices", None) + self._directed = kwargs.pop("directed", False) + self._arrow_sizes = kwargs.pop("arrow_sizes", None) + self._arrow_widths = kwargs.pop("arrow_widths", None) + self._loop_sizes = kwargs.pop("loop_sizes", None) + self._curved = kwargs.pop("curved", None) + super().__init__(*args, **kwargs) + + @staticmethod + def _get_edge_vertex_sizes(edge_vertices): + sizes = [] + for visual_vertex in edge_vertices: + if visual_vertex.size is not None: + sizes.append(visual_vertex.size) + else: + sizes.append(max(visual_vertex.width, visual_vertex.height)) + return sizes + + @staticmethod + def _compute_edge_angles(path, trans, directed, curved): + """Compute edge angles for both starting and ending vertices. + + NOTE: The domain of atan2 is (-pi, pi]. + """ + positions = trans(path.vertices) + + # first angle + if not directed: + x1, y1 = positions[0] + x2, y2 = positions[1] + elif not curved: + x1, y1 = positions[1] + x2, y2 = positions[0] + else: + x1, y1 = positions[3] + x2, y2 = positions[2] + angle1 = atan2(y2 - y1, x2 - x1) + + # second angle + if not directed: + x1, y1 = positions[-1] + x2, y2 = positions[-2] + else: + x1, y1 = positions[-3] + x2, y2 = positions[-1] + angle2 = atan2(y2 - y1, x2 - x1) + return (angle1, angle2) + + def _compute_paths(self, transform=None): + import numpy as np + + visual_vertices = self._visual_vertices + if transform is None: + transform = self.get_transform() + trans = transform.transform + trans_inv = transform.inverted().transform + + # Loops split the largest wedge left open by other + # edges of that vertex. The algo is: + # (i) Find what vertices each loop belongs to + # (ii) While going through the edges, record the angles + # for vertices with loops + # (iii) Plot each loop based on the recorded angles + loop_vertex_dict = {} + for i, edge_vertices in enumerate(visual_vertices): + if edge_vertices[0] == edge_vertices[1]: + if edge_vertices[0] not in loop_vertex_dict: + loop_vertex_dict[edge_vertices[0]] = { + "indices": [], + "sizes": [], + "edge_angles": [], + } + if self._directed: + loop_vertex_dict[edge_vertices[0]]["arrow_sizes"] = [] + loop_vertex_dict[edge_vertices[0]]["arrow_widths"] = [] + loop_vertex_dict[edge_vertices[0]]["indices"].append(i) + + # Get actual coordinates of the vertex border (rough) + paths = [] + for i, edge_vertices in enumerate(visual_vertices): + if self._directed: + if (self._arrow_sizes is None) or (self._arrow_widths is None): + arrow_size = 0 + arrow_width = 0 + else: + arrow_size = self._arrow_sizes[i] + arrow_width = self._arrow_widths[i] + + # Loops are positioned post-facto in the space left by the othter edges + if edge_vertices[0] == edge_vertices[1]: + paths.append(None) + loop_vertex_dict[edge_vertices[0]]["sizes"].append( + self._loop_sizes[i], + ) + if self._directed: + loop_vertex_dict[edge_vertices[0]]["arrow_sizes"].append( + arrow_size, + ) + loop_vertex_dict[edge_vertices[0]]["arrow_widths"].append( + arrow_width, + ) + continue + + coords = np.vstack( + [ + edge_vertices[0].position, + edge_vertices[1].position, + ] + ) + coordst = trans(coords) + sizes = self._get_edge_vertex_sizes(edge_vertices) + if self._curved is not None: + curved = self._curved[i] + else: + curved = False + + if self._directed: + path = self._compute_path_directed( + coordst, + sizes, + trans_inv, + curved, + arrow_size, + arrow_width, + ) + else: + path = self._compute_path_undirected( + coordst, + sizes, + trans_inv, + curved, + ) + + # Collect angles for this vertex, to be used for loops plotting below + angles = self._compute_edge_angles(path, trans, self._directed, curved) + if edge_vertices[0] in loop_vertex_dict: + loop_vertex_dict[edge_vertices[0]]["edge_angles"].append(angles[0]) + if edge_vertices[1] in loop_vertex_dict: + loop_vertex_dict[edge_vertices[1]]["edge_angles"].append(angles[1]) + + # Add the path for this non-loop edge + paths.append(path) + + # Deal with loops at the end + for visual_vertex, ldict in loop_vertex_dict.items(): + coords = np.vstack([visual_vertex.position] * 2) + coordst = trans(coords) + vertex_size = self._get_edge_vertex_sizes([visual_vertex])[0] + + edge_angles = ldict["edge_angles"] + if edge_angles: + edge_angles.sort() + # Circle around + edge_angles.append(edge_angles[0] + 2 * pi) + wedges = [ + (a2 - a1) for a1, a2 in zip(edge_angles[:-1], edge_angles[1:]) + ] + # Argsort + imax = max(range(len(wedges)), key=lambda i: wedges[i]) + angle1, angle2 = edge_angles[imax], edge_angles[imax + 1] + else: + # Isolated vertices with loops + angle1, angle2 = -pi, pi + + nloops = len(ldict["indices"]) + for i in range(nloops): + angle1i = angle1 + (angle2 - angle1) * i / nloops + angle2i = angle1 + (angle2 - angle1) * (i + 1) / nloops + if self._directed: + loop_kwargs = { + "arrow_size": ldict["arrow_sizes"][i], + "arrow_width": ldict["arrow_widths"][i], + } + else: + loop_kwargs = {} + path = self._compute_path_loop( + coordst[0], + vertex_size, + ldict["sizes"][i], + angle1i, + angle2i, + trans_inv, + angle_padding_fraction=0.1, + **loop_kwargs, + ) + paths[ldict["indices"][i]] = path + + return paths + + def _compute_path_loop( + self, + coordt, + vertex_size, + loop_size, + angle1, + angle2, + trans_inv, + angle_padding_fraction=0.1, + arrow_size=None, + arrow_width=None, + ): + import numpy as np + + # Special argument for loop size to scale with vertices + if loop_size < 0: + loop_size = -loop_size * vertex_size + + # Pad angles to make a little space for tight arrowheads + angle1, angle2 = ( + angle1 * (1 - angle_padding_fraction) + angle2 * angle_padding_fraction, + angle1 * angle_padding_fraction + angle2 * (1 - angle_padding_fraction), + ) + + # Too large wedges, just use a quarter + if angle2 - angle1 > pi / 3: + angle_mid = (angle2 + angle1) * 0.5 + angle1 = angle_mid - pi / 6 + angle2 = angle_mid + pi / 6 + + start = vertex_size / 2 * np.array([cos(angle1), sin(angle1)]) + end = vertex_size / 2 * np.array([cos(angle2), sin(angle2)]) + amix = 0.05 + aux1 = loop_size * np.array( + [ + cos(angle1 * (1 - amix) + angle2 * amix), + sin(angle1 * (1 - amix) + angle2 * amix), + ] + ) + aux2 = loop_size * np.array( + [ + cos(angle1 * amix + angle2 * (1 - amix)), + sin(angle1 * amix + angle2 * (1 - amix)), + ] + ) + + if not self._directed: + vertices = np.vstack( + [ + start, + aux1, + aux2, + end, + aux2, + aux1, + start, + ] + ) + codes = ["MOVETO"] + ["CURVE4"] * 6 + else: + # Tweak the bezier points + aux1 *= (loop_size + arrow_size) / loop_size + aux2 *= (loop_size + arrow_size) / loop_size + # Angle between end/tip and vertex centre + theta = angle2 + voff_unity = np.array([cos(theta), sin(theta)]) + voff_unity_90 = voff_unity @ [[0, 1], [-1, 0]] + headbase = end + arrow_size * voff_unity + headleft = headbase + 0.5 * arrow_width * voff_unity_90 + headright = headbase - 0.5 * arrow_width * voff_unity_90 + vertices = np.vstack( + [ + start, + aux1, + aux2, + headbase, + headleft, + end, + headright, + headbase, + aux2, + aux1, + start, + ] + ) + codes = ["MOVETO"] + ["CURVE4"] * 3 + ["LINETO"] * 4 + ["CURVE4"] * 3 + + # Offset to place and transform to data coordinates + vertices = trans_inv(coordt + vertices) + codes = [getattr(mpl.path.Path, x) for x in codes] + path = mpl.path.Path( + vertices, + codes=codes, + ) + return path + + def _compute_path_undirected(self, coordst, sizes, trans_inv, curved): + path = {"vertices": [], "codes": []} + path["codes"].append("MOVETO") + if not curved: + path["codes"].append("LINETO") + + # Start + theta = atan2(*((coordst[1] - coordst[0])[::-1])) + voff = 0 * coordst[0] + voff[:] = [cos(theta), sin(theta)] + voff *= sizes[0] / 2 + path["vertices"].append(coordst[0] + voff) + + # End + voff[:] = [cos(theta), sin(theta)] + voff *= sizes[1] / 2 + path["vertices"].append(coordst[1] - voff) + else: + path["codes"].extend(["CURVE4"] * 3) + + aux1, aux2 = get_bezier_control_points_for_curved_edge( + *coordst.ravel(), + curved, + ) + + # Start + theta = atan2(*((aux1 - coordst[0])[::-1])) + voff = 0 * coordst[0] + voff[:] = [cos(theta), sin(theta)] + voff *= sizes[0] / 2 + path["vertices"].append(coordst[0] + voff) + + # Bezier + path["vertices"].append(aux1) + path["vertices"].append(aux2) + + # End + theta = atan2(*((coordst[1] - aux2)[::-1])) + voff = 0 * coordst[0] + voff[:] = [cos(theta), sin(theta)] + voff *= sizes[1] / 2 + path["vertices"].append(coordst[1] - voff) + + # This is a dirty trick to make the facecolor work + # without making a separate Patch, which would be a little messy + path["codes"].extend(["CURVE4"] * 3) + path["vertices"].extend(path["vertices"][-2::-1]) + + path = mpl.path.Path( + path["vertices"], + codes=[getattr(mpl.path.Path, x) for x in path["codes"]], + ) + path.vertices = trans_inv(path.vertices) + return path + + def _compute_path_directed( + self, coordst, sizes, trans_inv, curved, arrow_size, arrow_width + ): + path = {"vertices": [], "codes": []} + path["codes"].append("MOVETO") + if not curved: + path["codes"].extend(["LINETO"] * 6) + + # Start + theta = atan2(*((coordst[1] - coordst[0])[::-1])) + voff = 0 * coordst[0] + voff[:] = [cos(theta), sin(theta)] + voff *= sizes[0] / 2 + start = coordst[0] + voff + + # End with arrow (base-left-top-right-base) + theta = atan2(*((coordst[1] - coordst[0])[::-1])) + voff_unity = 0 * coordst[0] + voff_unity[:] = [cos(theta), sin(theta)] + voff = voff_unity * sizes[1] / 2 + tip = coordst[1] - voff + + voff_unity_90 = voff_unity @ [[0, 1], [-1, 0]] + headbase = tip - arrow_size * voff_unity + headleft = headbase + 0.5 * arrow_width * voff_unity_90 + headright = headbase - 0.5 * arrow_width * voff_unity_90 + # This is a dirty trick to make the facecolor work + # without making a separate Patch, which would be a little messy + path["vertices"].extend( + [ + headbase, + start, + headbase, + headleft, + tip, + headright, + headbase, + ] + ) + else: + # Bezier + aux1, aux2 = get_bezier_control_points_for_curved_edge( + *coordst.ravel(), + curved, + ) + + # Start + theta = atan2(*((aux1 - coordst[0])[::-1])) + voff_unity = 0 * coordst[0] + voff_unity[:] = [cos(theta), sin(theta)] + start = coordst[0] + voff_unity * sizes[0] / 2 + + # End with arrow (base-left-top-right-base) + theta = atan2(*((coordst[1] - aux2)[::-1])) + voff_unity = 0 * coordst[0] + voff_unity[:] = [cos(theta), sin(theta)] + voff_unity_90 = voff_unity @ [[0, 1], [-1, 0]] + tip = coordst[1] - voff_unity * sizes[1] / 2 + headbase = tip - arrow_size * voff_unity + headleft = headbase + 0.5 * arrow_width * voff_unity_90 + headright = headbase - 0.5 * arrow_width * voff_unity_90 + + # This is a dirty trick to make the facecolor work + # without making a separate Patch, which would be a little messy + path["codes"].extend(["CURVE4"] * 6 + ["LINETO"] * 4) + path["vertices"].extend( + [ + headbase, + aux2, + aux1, + start, + aux1, + aux2, + headbase, + headleft, + tip, + headright, + headbase, + ] + ) + + path = mpl.path.Path( + path["vertices"], + codes=[getattr(mpl.path.Path, x) for x in path["codes"]], + ) + path.vertices = trans_inv(path.vertices) + return path + + def draw(self, renderer): + if self._visual_vertices is not None: + self._paths = self._compute_paths() + return super().draw(renderer) + + def get_arrow_sizes(self): + """Same as get_arrow_size.""" + return self.get_arrow_size() + + def get_arrow_size(self): + """Get arrow sizes for the edges (directed only). + + @return: An array of arrow sizes. + """ + import numpy as np + + if self._arrow_sizes is None: + arrow_sizes = [0 for x in self.get_paths()] + else: + arrow_sizes = self._arrow_sizes + return np.array(arrow_sizes) + + def set_arrow_size(self, sizes): + """Set arrow sizes. + + @param sizes: A sequence of arrow sizes or a single size. + """ + try: + iter(sizes) + except TypeError: + sizes = [sizes] * len(self._paths) + self._arrow_sizes = sizes + self.stale = True + + def set_arrow_sizes(self, sizes): + """Same as set_arrow_size""" + return self.set_arrow_size(sizes) + + def get_arrow_widths(self): + """Same as get_arrow_width.""" + return self.get_arrow_width() + + def get_arrow_width(self): + """Get arrow widths for the edges (directed only). + + @return: An array of arrow widths. + """ + import numpy as np + + if self._arrow_widths is None: + arrow_widths = [0 for x in self.get_paths()] + else: + arrow_widths = self._arrow_widths + return np.array(arrow_widths) + + def set_arrow_width(self, widths): + """Set arrow widths. + + @param widths: A sequence of arrow widths or a single width. + """ + try: + iter(widths) + except TypeError: + widths = [widths] * len(self._paths) + self._arrow_widths = widths + self.stale = True + + def set_arrow_widths(self, widths): + """Same as set_arrow_width""" + return self.set_arrow_width(widths) + + @property + def stale(self): + return super().stale + + @stale.setter + def stale(self, val): + PatchCollection.stale.fset(self, val) + if val and hasattr(self, "stale_callback_post"): + self.stale_callback_post(self) diff --git a/src/igraph/drawing/matplotlib/graph.py b/src/igraph/drawing/matplotlib/graph.py new file mode 100644 index 000000000..73c5e6ab4 --- /dev/null +++ b/src/igraph/drawing/matplotlib/graph.py @@ -0,0 +1,848 @@ +""" +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 functools import wraps, partial + +from igraph._igraph import convex_hull, VertexSeq +from igraph.drawing.baseclasses import AbstractGraphDrawer +from igraph.drawing.utils import FakeModule + +from .edge import MatplotlibEdgeDrawer, EdgeCollection +from .polygon import HullCollection +from .utils import find_matplotlib +from .vertex import MatplotlibVertexDrawer, VertexCollection + +__all__ = ("MatplotlibGraphDrawer",) + +mpl, plt = find_matplotlib() +try: + Artist = mpl.artist.Artist + Affine2D = mpl.transforms.Affine2D +except AttributeError: + Artist = FakeModule + Affine2D = FakeModule + +##################################################################### + + +# NOTE: https://github.com/networkx/grave/blob/main/grave/grave.py +def _stale_wrapper(func): + """Decorator to manage artist state.""" + + @wraps(func) + def inner(self, *args, **kwargs): + try: + func(self, *args, **kwargs) + finally: + self.stale = False + + return inner + + +def _forwarder(forwards, cls=None): + """Decorator to forward specific methods to Artist children.""" + if cls is None: + return partial(_forwarder, forwards) + + def make_forward(name): + def method(self, *args, **kwargs): + ret = getattr(cls.mro()[1], name)(self, *args, **kwargs) + for c in self.get_children(): + getattr(c, name)(*args, **kwargs) + return ret + + return method + + for f in forwards: + method = make_forward(f) + method.__name__ = f + method.__doc__ = "broadcasts {} to children".format(f) + setattr(cls, f, method) + + return cls + + +def _additional_set_methods(attributes, cls=None): + """Decorator to add specific set methods for children properties.""" + if cls is None: + return partial(_additional_set_methods, attributes) + + def make_setter(name): + def method(self, value): + self.set(**{name: value}) + + return method + + for attr in attributes: + desc = attr.replace("_", " ") + method = make_setter(attr) + method.__name__ = f"set_{attr}" + method.__doc__ = f"Set {desc}." + setattr(cls, f"set_{attr}", method) + + return cls + + +@_additional_set_methods( + ( + "vertex_color", + "vertex_size", + "vertex_font", + "vertex_label", + "vertex_label_angle", + "vertex_label_color", + "vertex_label_dist", + "vertex_label_size", + "vertex_order", + "vertex_shape", + "vertex_size", + "edge_color", + "edge_curved", + "edge_font", + "edge_arrow_size", + "edge_arrow_width", + "edge_width", + "edge_label", + "edge_background", + "edge_align_label", + "autocurve", + "layout", + ) +) +@_forwarder( + ( + "set_clip_path", + "set_clip_box", + "set_transform", + "set_snap", + "set_sketch_params", + "set_figure", + "set_animated", + "set_picker", + ) +) +class GraphArtist(Artist, AbstractGraphDrawer): + """Artist for an igraph.Graph object. + + @param graph: An igraph.Graph object to plot + @param layout: A layout object or matrix of coordinates to use for plotting. + Each element or row should describes the coordinates for a vertex. + @param vertex_style: A dictionary specifying style options for vertices. + @param edge_style: A dictionary specifying style options for edges. + """ + + def __init__( + self, + graph, + vertex_drawer_factory=MatplotlibVertexDrawer, + edge_drawer_factory=MatplotlibEdgeDrawer, + mark_groups=None, + layout=None, + palette=None, + **kwds, + ): + super().__init__() + self.graph = graph + self._vertex_drawer_factory = vertex_drawer_factory + self._edge_drawer_factory = edge_drawer_factory + self.kwds = kwds + self.kwds["mark_groups"] = mark_groups + self.kwds["palette"] = palette + self.kwds["layout"] = layout + + self._kwds_post_update() + + def _kwds_post_update(self): + self.kwds["layout"] = self.ensure_layout(self.kwds["layout"], self.graph) + self._set_edge_curve() + self._clear_state() + self.stale = True + + def _clear_state(self): + self._vertices = None + self._edges = None + self._vertex_labels = [] + self._edge_labels = [] + self._groups = None + self._legend_info = {} + + def get_children(self): + artists = [] + if self._groups is not None: + artists.append(self._groups) + # This way vertices are on top of edges, since they are drawn later + if self._edges is not None: + artists.append(self._edges) + if self._vertices is not None: + artists.append(self._vertices) + artists.extend(self._edge_labels) + artists.extend(self._vertex_labels) + return tuple(artists) + + def _set_edge_curve(self): + graph = self.graph + kwds = self.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 self.graph.edge_attributes() + and self.graph.ecount() < 10000 + ): + from igraph import autocurve + + default = kwds.get("edge_curved", 0) + if default is True: + default = 0.5 + default = float(default) + self.kwds["edge_curved"] = autocurve( + graph, + attribute=None, + default=default, + ) + + def get_vertices(self): + """Get VertexCollection artist.""" + return self._vertices + + def get_edges(self): + """Get EdgeCollection artist.""" + return self._edges + + def get_groups(self): + """Get HullCollection group/cluster/cover artists.""" + return self._groups + + def get_vertex_labels(self): + """Get list of vertex label artists.""" + return self._vertex_labels + + def get_edge_labels(self): + """Get list of edge label artists.""" + return self._edge_labels + + def get_datalim(self): + """Get limits on x/y axes based on the graph layout data. + + There is a small padding based on the size of the vertex marker to + ensure it fits into the canvas. + """ + import numpy as np + + layout = self.kwds["layout"] + + if len(layout) == 0: + mins = np.array([0, 0]) + maxs = np.array([1, 1]) + return (mins, maxs) + + # Use the layout as a base, and expand using bboxes from other artists + mins = np.min(layout, axis=0).astype(float) + maxs = np.max(layout, axis=0).astype(float) + + # NOTE: unlike other Collections, the vertices are basically a + # PatchCollection with an offset transform using transData. Therefore, + # care should be taken if one wants to include it here + if self._vertices is not None: + trans = self.axes.transData.transform + trans_inv = self.axes.transData.inverted().transform + verts = self._vertices + for path, offset in zip(verts.get_paths(), verts._offsets): + bbox = path.get_extents() + mins = np.minimum(mins, trans_inv(bbox.min + trans(offset))) + maxs = np.maximum(maxs, trans_inv(bbox.max + trans(offset))) + + if self._edges is not None: + for path in self._edges.get_paths(): + bbox = path.get_extents() + mins = np.minimum(mins, bbox.min) + maxs = np.maximum(maxs, bbox.max) + + if self._groups is not None: + for path in self._groups.get_paths(): + bbox = path.get_extents() + mins = np.minimum(mins, bbox.min) + maxs = np.maximum(maxs, bbox.max) + + # 5% padding, on each side + pad = (maxs - mins) * 0.05 + mins -= pad + maxs += pad + + return (mins, maxs) + + def _draw_vertex_labels(self): + import numpy as np + + kwds = self.kwds + layout = self.kwds["layout"] + vertex_builder = self._vertex_builder + vertex_order = self._vertex_order + + self._vertex_labels = [] + + # 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 + + label_size = kwds.get( + "vertex_label_size", + vertex.label_size, + ) + + label_color = kwds.get( + "vertex_label_color", + vertex.label_color, + ) + + # Locate text relative to vertex in data units. This is consistent + # with the vertex size being in data units, but might be not fully + # satisfactory when zooming in/out. In that case, revisit this + # section + dist = vertex.label_dist + angle = vertex.label_angle + if vertex.size is not None: + vertex_width = vertex.size + vertex_height = vertex.size + else: + vertex_width = vertex.width + vertex_height = vertex.height + xtext = dist * 0.5 * vertex_width * np.cos(angle) + ytext = dist * 0.5 * vertex_height * np.sin(angle) + xytext = (xtext, ytext) + + art = mpl.text.Annotation( + vertex.label, + coords, + xytext=xytext, + textcoords="offset points", + fontsize=label_size, + color=label_color, + ha="center", + va="center", + clip_on=True, + zorder=3, + ) + self._vertex_labels.append(art) + + def _draw_edge_labels(self): + graph = self.graph + kwds = self.kwds + vertex_builder = self._vertex_builder + edge_builder = self._edge_builder + edge_drawer = self._edge_drawer + edge_order = self._edge_order or range(self.graph.ecount()) + + self._edge_labels = [] + + labels = kwds.get("edge_label", None) + if labels is None: + return + + edge_label_iter = ( + (labels[i], edge_builder[i], graph.es[i]) for i in edge_order + ) + 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( + visual_edge, + src_vertex, + dest_vertex, + ) + + text_kwds = {} + text_kwds["ha"] = halign.value + text_kwds["va"] = valign.value + + if visual_edge.background is not None: + text_kwds["bbox"] = { + "facecolor": visual_edge.background, + "edgecolor": "none", + } + text_kwds["ha"] = "center" + text_kwds["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_kwds["rotation"] = rotation + + art = mpl.text.Annotation( + label, + (x, y), + fontsize=visual_edge.label_size, + color=visual_edge.label_color, + transform=self.axes.transData, + clip_on=True, + zorder=3, + **text_kwds, + ) + self._edge_labels.append(art) + + def _draw_groups(self): + """Draw the highlighted vertex groups, if requested""" + # Deferred import to avoid a cycle in the import graph + from igraph.clustering import VertexClustering, VertexCover + + mark_groups = self.kwds["mark_groups"] + if not mark_groups: + return + + kwds = self.kwds + palette = self.kwds["palette"] + layout = self.kwds["layout"] + vertex_builder = self._vertex_builder + + # 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__"): + # One-off generators: we need to store the actual list for future + # calls (e.g. resizing, recoloring, etc.). If we don't do this, + # the generator is exhausted: we cannot rewind it. + self.mark_groups = mark_groups = list(mark_groups) + # 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 + polygons = [] + corner_radii = [] + facecolors = [] + edgecolors = [] + 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])] + + # Construct the hull polygon + polygon = [layout[idx] for idx in hull] + + # Calculate rounding radius and facecolor + corner_radius = 1.25 * max(vertex_builder[idx].size for idx in hull) + facecolor = (color[0], color[1], color[2], 0.25 * color[3]) + + 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 len(polygon) >= 1: + polygons.append(mpl.path.Path(polygon)) + corner_radii.append(corner_radius) + facecolors.append(facecolor) + edgecolors.append(color) + + art = HullCollection( + polygons, + corner_radius=corner_radii, + facecolor=facecolors, + edgecolor=edgecolors, + transform=self.axes.transData, + ) + self._groups = art + + if kwds.get("legend", False): + self.legend_info = legend_info + + def _draw_vertices(self): + """Draw the vertices""" + graph = self.graph + layout = self.kwds["layout"] + vertex_drawer = self._vertex_drawer + vertex_builder = self._vertex_builder + vertex_order = self._vertex_order + + 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 + ) + offsets = [] + patches = [] + for vertex, visual_vertex, coords in vertex_coord_iter: + art = vertex_drawer.draw(visual_vertex, vertex, coords) + patches.append(art) + offsets.append(list(coords)) + + art = VertexCollection( + patches, + offsets=offsets if offsets else None, + offset_transform=self.axes.transData, + match_original=True, + transform=Affine2D(), + ) + self._vertices = art + + def _draw_edges(self): + """Draw the edges""" + graph = self.graph + vertex_builder = self._vertex_builder + edge_drawer = self._edge_drawer + edge_builder = self._edge_builder + edge_order = self._edge_order + + 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) + + directed = graph.is_directed() + + visual_vertices = [] + edgepatches = [] + arrow_sizes = [] + arrow_widths = [] + loop_sizes = [] + curved = [] + for edge, visual_edge in edge_coord_iter: + edge_vertices = [vertex_builder[v] for v in edge.tuple] + art = edge_drawer.build_patch(visual_edge, *edge_vertices) + edgepatches.append(art) + visual_vertices.append(edge_vertices) + arrow_sizes.append(visual_edge.arrow_size) + arrow_widths.append(visual_edge.arrow_width) + loop_sizes.append(visual_edge.loop_size) + curved.append(visual_edge.curved) + + art = EdgeCollection( + edgepatches, + visual_vertices=visual_vertices, + directed=directed, + arrow_sizes=arrow_sizes, + arrow_widths=arrow_widths, + loop_sizes=loop_sizes, + curved=curved, + transform=self.axes.transData, + ) + self._edges = art + + def _reprocess(self): + """Prepare artist and children for the actual drawing. + + Children are not drawn here, but the dictionaries of properties are + marshalled to their specific artists. + """ + # clear state and mark as stale + # since all children artists are part of the state, clearing it + # will trigger a deletion by the backend at the next draw cycle + self._clear_state() + self.stale = True + + # get local refs to everything (just for less typing) + graph = self.graph + palette = self.kwds["palette"] + layout = self.kwds["layout"] + kwds = self.kwds + + # Construct the vertex, edge and label drawers + if not hasattr(self, "_vertex_drawer"): + self._vertex_drawer = self._vertex_drawer_factory( + self.axes, palette, layout + ) + if not hasattr(self, "_edge_drawer"): + self._edge_drawer = self._edge_drawer_factory(self.axes, palette) + + # Construct the visual vertex/edge builders based on the specifications + # provided by the vertex_drawer and the edge_drawer + if not hasattr(self, "_vertex_builder"): + self._vertex_builder = self._vertex_drawer.VisualVertexBuilder( + graph.vs, kwds + ) + if not hasattr(self, "_edge_builder"): + self._edge_builder = self._edge_drawer.VisualEdgeBuilder(graph.es, kwds) + + # Determine the order in which we will draw the vertices and edges + # These methods come from AbstractGraphDrawer + self._vertex_order = self._determine_vertex_order(graph, kwds) + self._edge_order = self._determine_edge_order(graph, kwds) + + self._draw_groups() + self._draw_vertices() + self._draw_edges() + self._draw_vertex_labels() + self._draw_edge_labels() + + # Callbacks for other vertex properties, to ensure they are in sync + # with vertex_builder. + # NOTE: no need to reprocess here because it does not affect other + # parts of the container artist (e.g. edges) + def vertex_stale_callback(artist): + # If the stale state emerges from other properties, we can salvage + # the other artists but we have to update the vertex builder anyway + # in case a _reprocess is triggered by something else. + prop_pairs = ( + ("edgecolor", "frame_color"), + ("facecolor", "color"), + ("linewidth", "frame_width"), + ("zorder", "zorder"), + ("sizes", "size"), + ) + for mpl_prop, ig_prop in prop_pairs: + values = getattr(artist, "get_" + mpl_prop)() + try: + iter(values) + except TypeError: + values = [values] * len(artist.get_paths()) + for value, visual_vertex in zip(values, self._vertex_builder): + setattr(visual_vertex, ig_prop, value) + + # If the size is stale, one needs to redraw everything + if artist._stale_size: + self._reprocess() + + # Edge callback, keeps the edge builder in sync with the actual state + # of the artist + def edge_stale_callback(artist): + prop_pairs = ( + ("edgecolor", "color"), + ("linewidth", "width"), + ("zorder", "zorder"), + ("arrow_size", "arrow_size"), + ("arrow_width", "arrow_width"), + ) + for mpl_prop, ig_prop in prop_pairs: + values = getattr(artist, "get_" + mpl_prop)() + try: + iter(values) + except TypeError: + values = [values] * len(artist.get_paths()) + for value, visual_edge in zip(values, self._edge_builder): + setattr(visual_edge, ig_prop, value) + + # Sync facecolor from edgecolor + if mpl_prop == "edgecolor": + artist._facecolors = artist._edgecolors + + self._vertices.stale_callback_post = vertex_stale_callback + self._edges.stale_callback_post = edge_stale_callback + + # Forward mpl properties to children + # TODO sort out all of the things that need to be forwarded + for child in self.get_children(): + # set the figure / axes on child, this ensures each primitive + # knows where to draw + if hasattr(child, "set_figure"): + child.set_figure(self.figure) + child.axes = self.axes + + # forward the clippath/box to the children need this logic + # because mpl exposes some fast-path logic + clip_path = self.get_clip_path() + if clip_path is None: + clip_box = self.get_clip_box() + child.set_clip_box(clip_box) + else: + child.set_clip_path(clip_path) + + @_stale_wrapper + def draw(self, renderer, *args, **kwds): + """Draw each of the children, with some buffering mechanism.""" + if not self.get_visible(): + return + + if not self.get_children(): + self._reprocess() + + # NOTE: looks like we have to manage the zorder ourselves + children = list(self.get_children()) + children.sort(key=lambda x: x.zorder) + for art in children: + art.draw(renderer, *args, **kwds) + + def set( + self, + **kwds, + ): + """Set multiple parameters at once. + + The same options can be used as in the igraph.plot function. + """ + if len(kwds) == 0: + return + + self.kwds.update(kwds) + self._kwds_post_update() + + def contains(self, mouseevent): + """Track 'contains' event for mouse interactions.""" + props = {"vertices": [], "edges": []} + hit = False + for i, art in enumerate(self._edges): + edge_hit = art.contains(mouseevent)[0] + hit |= edge_hit + props["edges"].append(i) + + for i, art in enumerate(self._vertices): + vertex_hit = art.contains(mouseevent)[0] + hit |= vertex_hit + props["vertices"].append(i) + + return hit, props + + def pick(self, mouseevent): + """Track 'pick' event for mouse interactions.""" + if self.pickable(): + picker = self.get_picker() + if callable(picker): + inside, prop = picker(self, mouseevent) + else: + inside, prop = self.contains(mouseevent) + if inside: + self.figure.canvas.pick_event(mouseevent, self, **prop) + + +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): + if args: + warn( + "Positional arguments to plot functions are ignored " + "and will be deprecated soon.", + DeprecationWarning, + stacklevel=1, + ) + + # Some abbreviations for sake of simplicity + ax = self.ax + + # Create artist + art = GraphArtist( + graph, + vertex_drawer_factory=self.vertex_drawer_factory, + edge_drawer_factory=self.edge_drawer_factory, + *args, # noqa: B026 + **kwds, + ) + + # Bind artist to axes + ax.add_artist(art) + + # Create children artists (this also binds them to the axes) + art._reprocess() + + # Legend for groups + if ("mark_groups" in kwds) and kwds.get("legend", False): + ax.legend( + art._legend_info["handles"], + art._legend_info["labels"], + ) + + # Set new data limits + ax.update_datalim(art.get_datalim()) + + # 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([]) + + # Autoscale for x/y axis limits + ax.autoscale_view() + + return art diff --git a/src/igraph/drawing/matplotlib/histogram.py b/src/igraph/drawing/matplotlib/histogram.py new file mode 100644 index 000000000..8100ebf47 --- /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..05f38a7f1 --- /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..53f53dbe5 --- /dev/null +++ b/src/igraph/drawing/matplotlib/polygon.py @@ -0,0 +1,134 @@ +from copy import deepcopy + +from igraph.drawing.utils import calculate_corner_radii +from igraph.utils import consecutive_pairs +from igraph.drawing.utils import Point, FakeModule + +from .utils import find_matplotlib + +__all__ = ("HullCollection",) + +mpl, plt = find_matplotlib() +try: + PathCollection = mpl.collections.PathCollection +except AttributeError: + PathCollection = FakeModule + + +class HullCollection(PathCollection): + """Collection for hulls connecting vertex covers/clusters. + + The class takes the normal arguments of a PathCollection, plus one argument + called "corner_radius" that specifies how much to smoothen the polygon + vertices into round corners. This argument can be a float or a sequence + of floats, one for each hull to be drawn. + """ + + def __init__(self, *args, **kwargs): + self._corner_radii = kwargs.pop("corner_radius", None) + super().__init__(*args, **kwargs) + self._paths_original = deepcopy(self._paths) + try: + self._corner_radii = list(iter(self._corner_radii)) + except TypeError: + self._corner_radii = [self._corner_radii for x in self._paths] + + def _update_paths(self): + paths_original = self._paths_original + corner_radii = self._corner_radii + trans = self.axes.transData.transform + trans_inv = self.axes.transData.inverted().transform + + for i, (path_orig, radius) in enumerate(zip(paths_original, corner_radii)): + self._paths[i] = self._compute_path_with_corner_radius( + path_orig, + radius, + trans, + trans_inv, + ) + + @staticmethod + def _round_corners(points, corner_radius): + if corner_radius <= 0: + return (points, None) + + # 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 + + return (path, codes) + + @staticmethod + def _expand_path(coordst, radius): + if len(coordst) == 1: + # Expand a rectangle around a single vertex + a = Point(*coordst[0]) + c = Point(radius, 0) + n = Point(-c[1], c[0]) + polygon = [a + n, a - c, a - n, a + c] + elif len(coordst) == 2: + # Flat line, make it an actual shape + a, b = Point(*coordst[0]), Point(*coordst[1]) + c = 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(*coordst)] + ) + polygon = [Point(*point).towards(center, -radius) for point in coordst] + return polygon + + def _compute_path_with_corner_radius( + self, + path_orig, + radius, + trans, + trans_inv, + ): + # Move to point/canvas coordinates + coordst = trans(path_orig.vertices) + # Expand around vertices + polygon = self._expand_path(coordst, radius) + # Compute round corners + (polygon, codes) = self._round_corners(polygon, radius) + # Return to data coordinates + polygon = [trans_inv(x) for x in polygon] + return mpl.path.Path(polygon, codes) + + def draw(self, renderer): + if self._corner_radii is not None: + self._update_paths() + return super().draw(renderer) diff --git a/src/igraph/drawing/matplotlib/utils.py b/src/igraph/drawing/matplotlib/utils.py new file mode 100644 index 000000000..5ee190db0 --- /dev/null +++ b/src/igraph/drawing/matplotlib/utils.py @@ -0,0 +1,27 @@ +from igraph.drawing.utils import FakeModule +from typing import Any + +__all__ = ("find_matplotlib",) +__docformat__ = "restructuredtext en" + + +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..fb5b7cdec --- /dev/null +++ b/src/igraph/drawing/matplotlib/vertex.py @@ -0,0 +1,162 @@ +"""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 +from igraph.drawing.matplotlib.utils import find_matplotlib +from igraph.drawing.utils import FakeModule + +mpl, _ = find_matplotlib() +try: + IdentityTransform = mpl.transforms.IdentityTransform + PatchCollection = mpl.collections.PatchCollection +except AttributeError: + IdentityTransform = FakeModule + PatchCollection = FakeModule + + +__all__ = ("MatplotlibVertexDrawer", "VertexCollection") + + +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 = {"func": self.layout.__getitem__} + shape = ("circle", ShapeDrawerDirectory.resolve_default) + size = 30 + width = None + height = None + zorder = 2 + + return VisualVertexBuilder + + def draw(self, visual_vertex, vertex, coords): + """Build the Artist for a vertex and return it.""" + 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 + ) + + art = visual_vertex.shape.draw_path( + ax, + 0, + 0, + width, + height, + facecolor=visual_vertex.color, + edgecolor=visual_vertex.frame_color, + linewidth=visual_vertex.frame_width, + zorder=visual_vertex.zorder, + transform=IdentityTransform(), + ) + return art + + +class VertexCollection(PatchCollection): + """Collection of vertex patches for plotting. + + This class takes additional keyword arguments compared to PatchCollection: + + @param vertex_builder: A list of vertex builders to construct the visual + vertices. This is updated if the size of the vertices is changed. + @param size_callback: A function to be triggered after vertex sizes are + changed. Typically this redraws the edges. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._stale_size = False + + def get_sizes(self): + """Same as get_size.""" + return self.get_size() + + def get_size(self): + """Get vertex sizes. + + If width and height are unequal, get the largest of the two. + + @return: An array of vertex sizes. + """ + import numpy as np + + sizes = [] + for path in self.get_paths(): + bbox = path.get_extents() + mins, maxs = bbox.min, bbox.max + width, height = maxs - mins + size = max(width, height) + sizes.append(size) + return np.array(sizes) + + def set_size(self, sizes): + """Set vertex sizes. + + This rescales the current vertex symbol/path linearly, using this + value as the largest of width and height. + + @param sizes: A sequence of vertex sizes or a single size. + """ + paths = self._paths + try: + iter(sizes) + except TypeError: + sizes = [sizes] * len(paths) + + sizes = list(sizes) + current_sizes = self.get_sizes() + for path, cursize in zip(paths, current_sizes): + # Circular use of sizes + size = sizes.pop(0) + sizes.append(size) + # Rescale the path for this vertex + path.vertices *= size / cursize + + self._stale_size = True + self.stale = True + + def set_sizes(self, sizes): + """Same as set_size.""" + self.set_size(sizes) + + @property + def stale(self): + return super().stale + + @stale.setter + def stale(self, val): + PatchCollection.stale.fset(self, val) + if val and hasattr(self, "stale_callback_post"): + self.stale_callback_post(self) diff --git a/igraph/drawing/metamagic.py b/src/igraph/drawing/metamagic.py similarity index 88% rename from igraph/drawing/metamagic.py rename to src/igraph/drawing/metamagic.py index 44a22263e..63546bcc6 100644 --- a/igraph/drawing/metamagic.py +++ b/src/igraph/drawing/metamagic.py @@ -60,33 +60,32 @@ class VisualEdgeBuilder(AttributeCollectorBase): @see: AttributeCollectorMeta, AttributeCollectorBase """ -from ConfigParser import NoOptionError -from itertools import izip +from configparser import NoOptionError + from igraph.configuration import Configuration -__all__ = ["AttributeSpecification", "AttributeCollectorBase"] +__all__ = ("AttributeSpecification", "AttributeCollectorBase") + -# pylint: disable-msg=R0903 -# R0903: too few public methods -class AttributeSpecification(object): +class AttributeSpecification: """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 +94,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 @@ -110,8 +107,8 @@ def __init__(self, name, default=None, alt_name=None, transform=None, self.func = func self.accessor = None - if self.transform and not hasattr(self.transform, "__call__"): - raise TypeError, "transform must be callable" + if self.transform and not callable(self.transform): + raise TypeError("transform must be callable") if self.transform is None and self.default is not None: self.transform = type(self.default) @@ -119,7 +116,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,8 +151,8 @@ class AttributeCollectorMeta(type): def __new__(mcs, name, bases, attrs): attr_specs = [] - for attr, value in attrs.iteritems(): - if attr.startswith("_") or hasattr(value, "__call__"): + for attr, value in attrs.items(): + if attr.startswith("_") or callable(value): continue if isinstance(value, AttributeSpecification): attr_spec = value @@ -173,37 +170,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().__new__(mcs, name, bases, attrs) @classmethod - def record_generator(mcs, name, slots): + def record_generator(cls, 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""" + __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 +210,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 +257,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 +266,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 +291,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 +310,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 +326,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,16 +337,10 @@ 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.""" - # pylint: disable-msg=E1101 - # E1101: instance has no '_attributes' member return self._cache[index] def __len__(self): return len(self.seq) - - - 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..d9ee1e2cb --- /dev/null +++ b/src/igraph/drawing/plotly/edge.py @@ -0,0 +1,275 @@ +"""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, + get_bezier_control_points_for_curved_edge, + 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, aux2 = get_bezier_control_points_for_curved_edge( + x1, y1, x2, y2, edge.curved + ) + + # 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, aux2 = get_bezier_control_points_for_curved_edge( + x_src, y_src, x_arrow_mid, y_arrow_mid, edge.curved + ) + + # 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 = { + "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 = { + "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, aux2 = get_bezier_control_points_for_curved_edge( + x1, y1, x2, y2, edge.curved + ) + + 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 = { + "type": "path", + "path": path, + "line_color": format_rgba(edge.color), + "line_width": edge.width, + } + fig.add_shape(stroke, layer="below") diff --git a/src/igraph/drawing/plotly/graph.py b/src/igraph/drawing/plotly/graph.py new file mode 100644 index 000000000..ea89b5000 --- /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 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, + stacklevel=1, + ) + + # 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"] + + # 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 + ) + + # 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 + # 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 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) + + # 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": [], + # "textfont_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( + visual_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) + # FIXME: colors do not work yet; apparently we need to convert + # visual_edge.label_color to Plotly's format + # lab_args["textfont_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..b3d8929cb --- /dev/null +++ b/src/igraph/drawing/plotly/polygon.py @@ -0,0 +1,109 @@ +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 + """ + 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..f84f36cab --- /dev/null +++ b/src/igraph/drawing/plotly/utils.py @@ -0,0 +1,74 @@ +from igraph.drawing.utils import FakeModule +from typing import Any + +__all__ = ("find_plotly",) +__docformat__ = "restructuredtext en" + + +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.0 + 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..014fa3674 --- /dev/null +++ b/src/igraph/drawing/plotly/vertex.py @@ -0,0 +1,92 @@ +"""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 = {"func": self.layout.__getitem__} + shape = "circle" + size = 20.0 + 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/igraph/drawing/shapes.py b/src/igraph/drawing/shapes.py similarity index 54% rename from igraph/drawing/shapes.py rename to src/igraph/drawing/shapes.py index 741aa9ef6..0c3ccd1c8 100644 --- a/igraph/drawing/shapes.py +++ b/src/igraph/drawing/shapes.py @@ -16,46 +16,27 @@ name in the C{shape} attribute of vertices. """ -from __future__ import division - -__all__ = ["ShapeDrawerDirectory"] - -__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 -""" +__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(object): +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. The method I{must not} stroke or fill, it should just set up the current 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. @@ -68,12 +49,10 @@ 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 - # 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 +73,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,115 +84,141 @@ 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 hasattr(plt, "Axes") and isinstance(ctx, plt.Axes): + return mpl.patches.Rectangle( + (-width / 2, -height / 2), + width, + height, + clip_on=True, + **kwargs, + ) + else: + 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" + + 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 hasattr(plt, "Axes") and isinstance(ctx, plt.Axes): + return mpl.patches.Circle( + (0, 0), + width / 2, + clip_on=True, + **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): + 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" + + 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 hasattr(plt, "Axes") and 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, + clip_on=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): + 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 +228,40 @@ 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" + + 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 hasattr(plt, "Axes") and isinstance(ctx, plt.Axes): + vertices = [ + [-0.5 * width, 0.333 * height], + [0.5 * width, 0.333 * height], + [0, -0.667 * height], + ] + return mpl.patches.Polygon( + vertices, + closed=True, + clip_on=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): + 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 +271,41 @@ 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" + + 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 hasattr(plt, "Axes") and isinstance(ctx, plt.Axes): + vertices = [ + [-0.5 * width, 0], + [0, -0.5 * height], + [0.5 * width, 0], + [0, 0.5 * height], + ] + return mpl.patches.Polygon( + vertices, + closed=True, + clip_on=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): + 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,108 +329,16 @@ 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 = []): - """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(PolygonDrawer, self).__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 xrange(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(object): + +class ShapeDrawerDirectory: """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 +353,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 +364,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 +374,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 @@ -439,13 +383,13 @@ def resolve(cls, shape): try: return cls.known_shapes[shape] except KeyError: - raise ValueError("unknown shape: %s" % shape) + raise ValueError("unknown shape: %s" % shape) from None @classmethod 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 +398,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 new file mode 100644 index 000000000..57d2f9a90 --- /dev/null +++ b/src/igraph/drawing/text.py @@ -0,0 +1,19 @@ +""" +Drawers for labels on plots. +""" + +from enum import Enum + +__all__ = ("TextAlignment",) + +##################################################################### + + +class TextAlignment(Enum): + """Text alignment constants.""" + + LEFT = "left" + CENTER = "center" + RIGHT = "right" + TOP = "top" + BOTTOM = "bottom" diff --git a/igraph/drawing/utils.py b/src/igraph/drawing/utils.py similarity index 53% rename from igraph/drawing/utils.py rename to src/igraph/drawing/utils.py index 47f601687..c96e6d146 100644 --- a/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -2,17 +2,29 @@ Utility classes for drawing routines. """ -from igraph.compat import property -from itertools import izip -from math import atan2, cos, sin -from operator import itemgetter - -__all__ = ["BoundingBox", "FakeModule", "Point", "Rectangle"] -__license__ = "GPL" +from collections import defaultdict +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", + "get_bezier_control_points_for_curved_edge", + "intersect_bezier_curve_and_circle", + "str_to_orientation", + "autocurve", +) ##################################################################### -class Rectangle(object): + +class Rectangle: """Class representing a rectangle.""" __slots__ = ("_left", "_top", "_right", "_bottom") @@ -42,7 +54,7 @@ def __init__(self, *args): try: coords = tuple(float(coord) for coord in coords) except ValueError: - raise ValueError("invalid coordinate format, numbers expected") + raise ValueError("invalid coordinate format, numbers expected") from None self.coords = coords @@ -174,13 +186,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) @@ -194,7 +206,7 @@ def expand(self, margins): return self.contract([-float(margin) for margin in margins]) def isdisjoint(self, other): - """Returns ``True`` if the two rectangles have no intersection. + """Returns C{True} if the two rectangles have no intersection. Example:: @@ -210,11 +222,15 @@ 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 + """Returns C{True} if the rectangle is empty (i.e. it has zero width and height). Example:: @@ -249,10 +265,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 +312,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 +337,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 +361,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 +384,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,106 +404,62 @@ 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), ) ##################################################################### -# pylint: disable-msg=R0903 -# R0903: too few public methods -class FakeModule(object): - """Fake module that raises an exception for everything""" - def __getattr__(self, _): - raise TypeError("plotting not available") - def __call__(self, _): - raise TypeError("plotting not available") - def __setattr__(self, key, value): - raise TypeError("plotting not available") +class FakeModule: + """Fake module that raises an exception for everything""" -##################################################################### + def __init__(self, message): + """Constructor. -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() - for module_name in module_names: - try: - module = __import__(module_name) - break - except ImportError: - pass - return module + @param message: message to print in exceptions raised from this module + """ + self._message = message -##################################################################### + def __getattr__(self, _): + raise AttributeError(self._message) -class Point(tuple): - """Class representing a point on the 2D plane.""" - __slots__ = () - _fields = ('x', 'y') + def __call__(self, _): + raise TypeError(self._message) - def __new__(cls, x, y): - """Creates a new point with the given coordinates""" - return tuple.__new__(cls, (x, y)) + def __setattr__(self, key, value): + if key == "_message": + super().__setattr__(key, value) + else: + raise AttributeError(self._message) - # 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""" - 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)) - - # pylint: disable-msg=W0141 - # W0141: used builtin function 'map' - 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' % 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") +class Point(NamedTuple("_Point", [("x", float), ("y", float)])): + """Class representing a point on the 2D plane.""" 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. @@ -499,7 +481,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. @@ -508,44 +490,257 @@ 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 self.__class__( + 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 point.""" - return (self.x ** 2 + self.y ** 2) ** 0.5 + return (self.x**2 + self.y**2) ** 0.5 def normalized(self): """Normalizes the coordinates of the point s.t. its length will be 1 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 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 self.__class__( + self.x + distance * cos(angle), self.y + distance * sin(angle) + ) @classmethod def FromPolar(cls, radius, angle): """Constructs a point from polar coordinates. - `radius` is the distance of the point from the origin; `angle` is the + C{radius} is the distance of the point from the origin; C{angle} is the angle between the X axis and the vector pointing to the point from 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 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 + + +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", + "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 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/igraph/formula.py b/src/igraph/formula.py similarity index 76% rename from igraph/formula.py rename to src/igraph/formula.py index ac4fb994c..c644b8d78 100644 --- a/igraph/formula.py +++ b/src/igraph/formula.py @@ -1,60 +1,44 @@ # vim:ts=4:sw=4:sts=4:et # -*- coding: utf-8 -*- """ -Implementation of `igraph.Graph.Formula()` +Implementation of L{igraph.Graph.Formula()}. You should use this module directly only if you have a very strong reason to do so. In almost all cases, you are better off with calling -`igraph.Graph.Formula()`. +L{igraph.Graph.Formula()}. """ -from cStringIO import StringIO +from io import StringIO from igraph.datatypes import UniqueIdGenerator +import re import tokenize import token -__all__ = ["construct_graph_from_formula"] +__all__ = ("construct_graph_from_formula",) -__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 -""" 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 + - 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] return - name_tokens = set([token.NAME, token.NUMBER, token.STRING]) + name_tokens = {token.NAME, token.NUMBER, token.STRING} edge_chars = "<>-+" start_names, end_names, arrowheads = [], [], [False, False] 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 @@ -82,11 +66,17 @@ def generate_edges(formula): elif tok == ":" and token_type == token.OP: # Separating semicolon between vertex names, we just go on continue + elif token_type == token.NEWLINE: + # Newlines are fine + pass elif token_type == token.ENDMARKER: # End markers are fine pass else: - msg = "invalid token found in edge specification: %s" % formula + 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 @@ -107,10 +97,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. @@ -139,13 +128,13 @@ def construct_graph_from_formula(cls, formula = None, attr = "name", 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): @@ -158,19 +147,19 @@ def construct_graph_from_formula(cls, formula = None, attr = "name", >>> 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): A->B - - If you have may disconnected componnets, you can separate them + + If you have many disconnected componnets, you can separate them 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 + >>> g.connected_components().membership [0, 0, 1, 1, 2, 2, 3, 3, 4, 5, 6] The colon (C{:}) operator can be used to specify vertex sets. @@ -192,42 +181,39 @@ def construct_graph_from_formula(cls, formula = None, attr = "name", @param formula: the formula itself @param attr: name of the vertex attribute where the vertex names will be stored - @param simplify: whether the simplify the constructed graph + @param simplify: whether to 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 - for part in formula.split(","): - # Drop newlines from the part - part = part.strip().replace("\n", "").replace("\t", "") + for part in re.compile(r"[,\n]").split(formula): + # Strip leading and trailing whitespace in the part + part = part.strip() # 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/io/__init__.py b/src/igraph/io/__init__.py new file mode 100644 index 000000000..aae930b26 --- /dev/null +++ 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/io/adjacency.py b/src/igraph/io/adjacency.py new file mode 100644 index 000000000..7f09ed167 --- /dev/null +++ b/src/igraph/io/adjacency.py @@ -0,0 +1,159 @@ +from igraph.sparse_matrix import ( + _graph_from_sparse_matrix, + _graph_from_weighted_sparse_matrix, +) + + +def _construct_graph_from_adjacency(cls, matrix, mode="directed", loops="once"): + """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 + specifies the number of edges between two vertices. + - C{"undirected"} - the graph will be undirected and a matrix element + specifies the number of edges between two vertices. The matrix must + be symmetric. + - 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) + @param loops: specifies how to handle loop edges. When C{False} or + C{"ignore"}, the diagonal of the adjacency matrix will be ignored. When + C{True} or C{"once"}, the diagonal is assumed to contain the multiplicity + of the corresponding loop edge. When C{"twice"}, the diagonal is assumed + to contain I{twice} the multiplicity of the corresponding loop edge. + """ + # 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, 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).Adjacency(matrix, mode=mode, loops=loops) + + # 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="once" +): + """Generates a graph from its weighted adjacency matrix. + + Only edges with a non-zero weight are created. + + @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 + specifies the weight of the corresponding edge. + - C{"undirected"} - the graph will be undirected and a matrix element + specifies the weight of the corresponding edge. The matrix must + be symmetric. + - C{"max"} - undirected graph will be created and the weight of the + edge 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: specifies how to handle loop edges. When C{False} or + C{"ignore"}, the diagonal of the adjacency matrix will be ignored. When + C{True} or C{"once"}, the diagonal is assumed to contain the weight of the + corresponding loop edge. When C{"twice"}, the diagonal is assumed to + contain I{twice} the weight of the corresponding loop edge. + """ + # 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, weights = super(Graph, cls)._Weighted_Adjacency( + matrix, + mode=mode, + loops=loops, + ) + graph.es[attr] = weights + + # 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..1268dc0bf --- /dev/null +++ b/src/igraph/io/bipartite.py @@ -0,0 +1,163 @@ +def _construct_bipartite_graph_from_adjacency( + cls, + matrix, + directed=False, + mode="out", + multiple=False, + weighted=None, + *args, + **kwds, +): + """Creates a bipartite graph from a bipartite adjacency matrix. + + Example: + + >>> g = Graph.Biadjacency([[0, 1, 1], [1, 1, 0]]) + + @param matrix: the bipartite adjacency 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 + adjacency 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 C{"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._Biadjacency(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_biadjacency() + 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", + allowed_edge_types="simple", edge_labeled=False, *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. + @param allowed_edge_types: controls whether multi-edges are allowed + during the generation process. Possible values are: + + - C{"simple"}: simple graphs (no self-loops) + - C{"multi"}: multi-edges allowed + + @param edge_labeled: whether to sample uniformly from the set of + I{ordered} edge lists. Use C{False} to recover the classic random + bipartite model. + """ + if p is None: + p = -1 + if m is None: + m = -1 + result, types = cls._Random_Bipartite( + n1, n2, p, m, directed, neimode, allowed_edge_types, edge_labeled, + *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..56e674f90 --- /dev/null +++ b/src/igraph/io/files.py @@ -0,0 +1,494 @@ +import gzip +import os + +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 [ + ".dimacs", + ".dl", + ".dot", + ".edge", + ".edges", + ".edgelist", + ".gml", + ".graphml", + ".graphmlz", + ".gw", + ".lgl", + ".lgr", + ".ncol", + ".net", + ".pajek", + ".pickle", + ".picklez", + ".svg", + ]: + 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{Graph.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: + graph, weights = cls._Weighted_Adjacency(matrix, *args, **kwds) + graph.es[attribute] = weights + + 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, 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, 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." + ) from None + 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." + ) from None + 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 isinstance(f, os.PathLike): + f = str(f) + 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)) from None + 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()" + ) from None + + 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()" + ) from None + + if isinstance(capacity, str) and capacity not in graph.edge_attributes(): + warn("'%s' edge attribute does not exist" % capacity, stacklevel=1) + 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 isinstance(f, os.PathLike): + f = str(f) + 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)) from None + 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..da1c8b2c9 --- /dev/null +++ b/src/igraph/io/images.py @@ -0,0 +1,361 @@ +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 -= vertex_size * math.cos(angle) + 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)" + ) from None + 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..f35cc9545 --- /dev/null +++ b/src/igraph/io/libraries.py @@ -0,0 +1,272 @@ +def _export_graph_to_networkx( + graph, + create_using=None, + vertex_attr_hashable: str = "_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: 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: str = "_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 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: 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 next(iter(g.nodes.values())): + # 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 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 = list(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 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 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 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 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 in edge_attributes.keys(): + 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(): + # val.get_array() returns None for non-scalar types so use the slower + # way if this happens + prop = val.get_array() + arr = prop if prop is not None else val + for i in range(vcount): + graph.vs[i][key] = arr[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..0a78c5fe4 --- /dev/null +++ b/src/igraph/io/objects.py @@ -0,0 +1,854 @@ +from typing import Union, Sequence +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: bool = False, + vertex_name_attr: str = "name", + edge_foreign_keys=("source", "target"), + iterative: bool = 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 v in vertex_attrs.values(): + 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: bool = False, + vertex_name_attr: str = "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: bool = False, + vertex_name_attr: str = "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: whether to create a directed graph + @param vertex_name_attr: 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: bool = False, + vertex_name_attr: str = "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. + + 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) + + @param edges: the dict of dict of dicts specifying the edges and their + attributes + @param directed: whether to create a directed graph + @param vertex_name_attr: vertex attribute that will store the names + + @returns: a Graph object + """ + 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: bool = True, + vertices=None, + use_vids: bool = 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 C{use_vids} is False. Further columns + may contain edge attributes. + @param directed: 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 C{len(vertices) - 1}. If C{use_vids} + is C{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 C{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 C{edges} + are not integers, an error is thrown. + + @return: the graph + + Vertex names in either the C{edges} or C{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" + ) from None + + 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 str(edges.dtypes.iloc[0]).startswith(("int", "Int")) and str( + edges.dtypes.iloc[1] + ).startswith(("int", "Int")): + # Check pandas nullable integer data type: + # https://pandas.pydata.org/docs/user_guide/integer_na.html + if (edges.iloc[:, :2].isna()).any(axis=None): + raise ValueError("Source and target IDs must not be null") + + if (edges.iloc[:, :2] < 0).any(axis=None): + raise ValueError("Source and target IDs must not be negative") + else: + raise TypeError( + f"Source and target IDs must be 0-based integers, found types {edges.dtypes.tolist()[:2]}" + ) + + 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\"", + stacklevel=1, + ) + 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": pd.unique(edges.values[:, :2].ravel())}) + + if vertices.iloc[:, 0].isna().any(): + warn( + "In the first column of 'vertices' NA elements were replaced with string \"NA\"", + stacklevel=1, + ) + 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[edges.columns[0]] = edges.iloc[:, 0].map(vid_map) + edges[edges.columns[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: bool = True, + skip_none: bool = False, + vertex_name_attr: str = "name", +): + """Export graph as two lists of dictionaries, for vertices and edges. + + This function is the reverse of Graph.DictList. + + 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"}]) + + @param use_vids: 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: 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. + @param vertex_name_attr: 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. + """ + # 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: bool = True, + edge_attrs: Union[str, Sequence[str]] = None, + vertex_name_attr: str = "name", +): + """Export graph to a list of edge tuples + + This function is the reverse of Graph.TupleList. + + 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")] + + @param use_vids: 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: 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. + @param vertex_name_attr: 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. + """ + # 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: bool = True, + sequence_constructor: callable = list, + vertex_name_attr: str = "name", +): + """Export graph to a dictionary of lists (or other sequences). + + This function is the reverse of Graph.ListDict. + + 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']} + + @param use_vids: 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: 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. + @param vertex_name_attr: 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. + """ + 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: bool = True, + edge_attrs: Union[str, Sequence[str]] = None, + skip_none: bool = False, + vertex_name_attr: str = "name", +): + """Export graph to dictionary of dicts of edge attributes + + This function is the reverse of Graph.DictDict. + + 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'}}} + + @param use_vids: 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: 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: 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. + @param vertex_name_attr: 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. + """ + 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" + ) from None + + 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 C{drop=False} + option to keep the C{source} and C{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" + ) from None + + 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/src/igraph/io/utils.py b/src/igraph/io/utils.py new file mode 100644 index 000000000..527090a58 --- /dev/null +++ b/src/igraph/io/utils.py @@ -0,0 +1,23 @@ +from contextlib import contextmanager +from typing import Iterator + +__all__ = ("safe_locale",) + + +@contextmanager +def safe_locale() -> Iterator[None]: + """Helper function that establishes a context that temporarily switches the + current locale to use decimal dots when printing numbers. + + This can be used to establish an execution context within which it is safe + to call functions that read or write graphs from/to the disk and ensure that + they use decimal dots for interoperability with systems that are running + with another locale. + """ + from igraph._igraph import _enter_safelocale, _exit_safelocale + + locale = _enter_safelocale() + try: + yield + finally: + _exit_safelocale(locale) diff --git a/src/igraph/layout.py b/src/igraph/layout.py new file mode 100644 index 000000000..ad5fffc09 --- /dev/null +++ b/src/igraph/layout.py @@ -0,0 +1,768 @@ +# vim:ts=4:sw=4:sts=4:et +# -*- coding: utf-8 -*- +""" +Layout-related code in the igraph library. + +This package contains the implementation of the L{Layout} object. +""" + +from math import sin, cos, pi + +from igraph._igraph import GraphBase +from igraph.drawing.utils import BoundingBox +from igraph.statistics import RunningMean + + +__all__ = ( + "Layout", + "align_layout", + "_layout", + "_layout_auto", + "_layout_sugiyama", + "_layout_method_wrapper", + "_3d_version_for", + "_layout_mapping", +) + + +class Layout: + """Represents the layout of a graph. + + A layout is practically a list of coordinates in an n-dimensional + space. This class is generic in the sense that it can store coordinates + in any n-dimensional space. + + Layout objects are not associated directly with a graph. This is deliberate: + there were times when I worked with almost identical copies of the same + graph, the only difference was that they had different colors assigned to + the vertices. It was particularly convenient for me to use the same layout + for all of them, especially when I made figures for a paper. However, + C{igraph} will of course refuse to draw a graph with a layout that has + fewer coordinates than the node count of the graph. + + Layouts behave exactly like lists when they are accessed using the item + index operator (C{[...]}). They can even be iterated through. Items + returned by the index operator are only copies of the coordinates, + but the stored coordinates can be modified by directly assigning to + an index. + + >>> layout = Layout([(0, 1), (0, 2)]) + >>> coords = layout[1] + >>> print(coords) + [0, 2] + >>> coords = (0, 3) + >>> print(layout[1]) + [0, 2] + >>> layout[1] = coords + >>> print(layout[1]) + [0, 3] + + Optionally, a layout may have I{edge routing} information attached to + each edge in the layout in the L{edge_routing} property. + + @ivar edge_routing: C{None} if no edge routing information is available, + or a list of lists of control point coordinates, one for each edge. When + an edge has control points, it should be drawn in a way that the edge + passes through all the control points in the order they appear in the list. + """ + + def __init__(self, coords=None, dim=None): + """Constructor. + + @param coords: the coordinates to be stored in the layout. + @param dim: the number of dimensions. If C{None}, the number of + dimensions is determined automatically from the length of the first + item of the coordinate list. If there are no entries in the coordinate + list, the default will be 2. Generally, this should be given if the + length of the coordinate list is zero, otherwise it should be left as + is. + """ + self.edge_routing = None + + if coords is not None: + self._coords = [list(coord) for coord in coords] + else: + self._coords = [] + + if dim is None: + if len(self._coords) == 0: + self._dim = 2 + else: + self._dim = len(self._coords[0]) + else: + 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 + ) + + def __len__(self): + return len(self._coords) + + def __getitem__(self, idx): + return self._coords[idx] + + def __setitem__(self, idx, value): + if len(value) != self._dim: + raise ValueError("assigned item must have %d elements" % self._dim) + self._coords[idx] = list(value) + + def __delitem__(self, idx): + del self._coords[idx] + + def __copy__(self): + return self.__class__(self.coords, self.dim) + + def __repr__(self): + if not self.coords: + vertex_count = "no vertices" + elif len(self.coords) == 1: + vertex_count = "1 vertex" + else: + vertex_count = "%d vertices" % len(self.coords) + if self.dim == 1: + dim_count = "1 dimension" + else: + dim_count = "%d dimensions" % self.dim + return "<%s with %s and %s>" % ( + self.__class__.__name__, + vertex_count, + dim_count, + ) + + @property + def dim(self): + """Returns the number of dimensions""" + return self._dim + + @property + def coords(self): + """The coordinates as a list of lists""" + return [row[:] for row in self._coords] + + 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]]) + + def mirror(self, dim): + """Mirrors the layout along the given dimension(s) + + @param dim: the list of dimensions or a single dimension + """ + if isinstance(dim, int): + dim = [dim] + else: + dim = [int(x) for x in dim] + + for current_dim in 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. + + @param angle: the angle of the rotation, specified in degrees. + @param dim1: the first axis of the plane of the rotation. + @param dim2: the second axis of the plane of the rotation. + @keyword origin: the origin of the rotation. If not specified, the + origin will be the origin of the coordinate system. + """ + + 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.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] + + def scale(self, *args, **kwds): + """Scales the layout. + + Scaling parameters can be provided either through the C{scale} keyword + argument or through plain unnamed arguments. If a single integer or + float is given, it is interpreted as a uniform multiplier to be applied + on all dimensions. If it is a list or tuple, its length must be equal to + the number of dimensions in the layout, and each element must be an + integer or float describing the scaling coefficient in one of the + dimensions. + + @keyword scale: scaling coefficients (integer, float, list or tuple) + @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.0] * self._dim)) + if len(origin) != self._dim: + raise ValueError("origin must have %d dimensions" % self._dim) + + scaling = kwds.get("scale") or args + if isinstance(scaling, (int, float)): + scaling = [scaling] + if len(scaling) == 0: + raise ValueError("scaling factor must be given") + elif len(scaling) == 1: + if isinstance(scaling[0], (int, float)): + scaling *= self._dim + else: + scaling = scaling[0] + if len(scaling) != 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 range(self._dim) + ] + + def translate(self, *args, **kwds): + """Translates the layout. + + The translation vector can be provided either through the C{v} keyword + argument or through plain unnamed arguments. If unnamed arguments are + used, the vector can be supplied as a single list (or tuple) or just as + a series of arguments. In all cases, the translation vector must have + the same number of dimensions as the layout. + + @keyword v: the translation vector + """ + v = kwds.get("v") or args + if len(v) == 0: + raise ValueError("translation vector must be given") + elif len(v) == 1 and not isinstance(v[0], (int, float)): + v = v[0] + if len(v) != 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 range(self._dim)] + + 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 + layout is transformed to an angle, with min(x) corresponding to + the parameter called I{min_angle} and max(y) corresponding to + I{max_angle}. Angles are given in degrees, zero degree corresponds + to the direction pointing upwards. The Y coordinate is + interpreted as a radius, with min(y) belonging to the minimum and + max(y) to the maximum radius given in the arguments. + + This is not a fully generic polar coordinate transformation, but + it is fairly useful in creating radial tree layouts from ordinary + 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. + + @param min_angle: the angle corresponding to the minimum X value + @param max_angle: the angle corresponding to the maximum X value + @param min_radius: the radius corresponding to the minimum Y value + @param max_radius: the radius corresponding to the maximum Y value + """ + if self._dim != 2: + raise TypeError("implemented only for 2D layouts") + bbox = self.bounding_box() + + while min_angle > max_angle: + max_angle += 360 + while min_angle > 360: + min_angle -= 360 + max_angle -= 360 + while min_angle < 0: + min_angle += 360 + max_angle += 360 + + ratio_x = (max_angle - min_angle) / bbox.width + 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] + + def transform(self, function, *args, **kwds): + """Performs an arbitrary transformation on the layout + + Additional positional and keyword arguments are passed intact to + the given function. + + @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 + ] + + 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 range(self._dim)] + for row in self._coords: + for dim in range(self._dim): + centroid[dim].add(row[dim]) + return [rm.mean for rm in centroid] + + def boundaries(self, border=0): + """Returns the boundaries of the layout. + + The boundaries are the minimum and maximum coordinates along all + dimensions. + + @param border: this value gets subtracted from the minimum bounds + and gets added to the maximum bounds before returning the coordinates + of the box. Defaults to zero. + @return: the minimum and maximum coordinates along all dimensions, + in a tuple containing two lists, one for the minimum coordinates, + the other one for the maximum. + @raises ValueError: if the layout contains no layout items + """ + if not self._coords: + raise ValueError("layout contains no layout items") + + mins, maxs = [], [] + for dim in range(self._dim): + col = [row[dim] for row in self._coords] + 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. + + The bounding box of the layout is the smallest box enclosing all the + points in the layout. + + @param border: this value gets subtracted from the minimum bounds + and gets added to the maximum bounds before returning the coordinates + of the box. Defaults to zero. + @return: the coordinates of the lower left and the upper right corner + of the box. "Lower left" means the minimum coordinates and "upper right" + means the maximum. These are encapsulated in a L{BoundingBox} object. + """ + if self._dim != 2: + raise ValueError("Layout.boundary_box() supports 2D layouts only") + + try: + (x0, y0), (x1, y1) = self.boundaries(border) + return BoundingBox(x0, y0, x1, y1) + except ValueError: + return BoundingBox(0, 0, 0, 0) + + def center(self, *args, **kwds): + """Centers the layout around the given point. + + The point itself can be supplied as multiple unnamed arguments, as a + simple unnamed list or as a keyword argument. This operation moves + the centroid of the layout to the given point. If no point is supplied, + defaults to the origin of the coordinate system. + + @keyword p: the point where the centroid of the layout will be after + the operation.""" + center = kwds.get("p") or args + if len(center) == 0: + center = [0.0] * self._dim + elif len(center) == 1 and not isinstance(center[0], (int, float)): + center = center[0] + if len(center) != self._dim: + raise ValueError("the given point must have %d dimensions" % self._dim) + centroid = self.centroid() + 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__() + + def fit_into(self, bbox, keep_aspect_ratio=True): + """Fits the layout into the given bounding box. + + The layout will be modified in-place. + + @param bbox: the bounding box in which to fit the layout. If the + dimension of the layout is d, it can either be a d-tuple (defining + the sizes of the box), a 2d-tuple (defining the coordinates of the + top left and the bottom right point of the box), or a L{BoundingBox} + object (for 2D layouts only). + @param keep_aspect_ratio: whether to keep the aspect ratio of the current + layout. If C{False}, the layout will be rescaled to fit exactly into + the bounding box. If C{True}, the original aspect ratio of the layout + will be kept and it will be centered within the bounding box. + """ + if isinstance(bbox, BoundingBox): + if self._dim != 2: + 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.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 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 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 zip(mins, maxs)] + + for i, size in enumerate(sizes): + if size == 0: + sizes[i] = 2 + mins[i] -= 1 + maxs[i] += 1 + + 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 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) + + +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 + 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{Graph.layout_auto}) + + - C{bipartite}: bipartite layout (see L{GraphBase.layout_bipartite}) + + - C{circle}, C{circular}: circular layout + (see L{GraphBase.layout_circle}) + + - C{dh}, C{davidson_harel}: Davidson-Harel layout (see + L{GraphBase.layout_davidson_harel}) + + - C{drl}: DrL layout for large graphs (see L{GraphBase.layout_drl}) + + - C{drl_3d}: 3D DrL layout for large graphs + (see L{GraphBase.layout_drl}) + + - C{fr}, C{fruchterman_reingold}: Fruchterman-Reingold layout + (see L{GraphBase.layout_fruchterman_reingold}). + + - C{fr_3d}, C{fr3d}, C{fruchterman_reingold_3d}: 3D Fruchterman- + Reingold layout (see L{GraphBase.layout_fruchterman_reingold}). + + - C{grid}: regular grid layout in 2D (see L{GraphBase.layout_grid}) + + - C{grid_3d}: regular grid layout in 3D (see L{GraphBase.layout_grid}) + + - C{graphopt}: the graphopt algorithm (see L{GraphBase.layout_graphopt}) + + - C{kk}, C{kamada_kawai}: Kamada-Kawai layout + (see L{GraphBase.layout_kamada_kawai}) + + - C{kk_3d}, C{kk3d}, C{kamada_kawai_3d}: 3D Kamada-Kawai layout + (see L{GraphBase.layout_kamada_kawai}) + + - C{lgl}, C{large}, C{large_graph}: Large Graph Layout + (see L{GraphBase.layout_lgl}) + + - C{mds}: multidimensional scaling layout (see L{GraphBase.layout_mds}) + + - C{random}: random layout (see L{GraphBase.layout_random}) + + - C{random_3d}: random 3D layout (see L{GraphBase.layout_random}) + + - C{rt}, C{tree}, C{reingold_tilford}: Reingold-Tilford tree + layout (see L{GraphBase.layout_reingold_tilford}) + + - C{rt_circular}, C{reingold_tilford_circular}: circular + Reingold-Tilford tree layout + (see L{GraphBase.layout_reingold_tilford_circular}) + + - C{sphere}, C{spherical}, C{circle_3d}, C{circular_3d}: spherical + layout (see L{GraphBase.layout_circle}) + + - C{star}: star layout (see L{GraphBase.layout_star}) + + - C{sugiyama}: Sugiyama layout (see L{Graph.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 callable(layout): + 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(graph.__class__, graph._layout_mapping[layout]) + if not callable(method): + raise ValueError("layout method must be callable") + layout = method(graph, *args, **kwds) + if not isinstance(layout, Layout): + layout = Layout(layout) + return layout + + +def align_layout(graph, layout): + """Aligns a graph layout with the coordinate axes + + This function centers a vertex layout on the coordinate system origin and + rotates the layout to achieve a visually pleasing alignment with the coordinate + axes. Doing this is particularly useful with force-directed layouts such as + L{Graph.layout_fruchterman_reingold}. Layouts in arbitrary dimensional spaces + are supported. + + @param graph: the graph that the layout is associated with. + @param layout: the L{Layout} object containing the vertex coordinates + to align. + @return: a new aligned L{Layout} object. + """ + from igraph._igraph import _align_layout + + if not isinstance(layout, Layout): + layout = Layout(layout) + + return Layout(_align_layout(graph, layout.coords)) + + +def _layout_auto(graph, *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{GraphBase.layout_kamada_kawai()}). + + 4. Otherwise, if the graph has at most 1000 vertices, the + Fruchterman-Reingold layout will be used (see + L{GraphBase.layout_fruchterman_reingold()}). + + 5. If everything else above failed, the DrL layout algorithm + will be used (see L{GraphBase.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 graph.attributes(): + layout = graph["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 callable(layout): + # Callables are called + return Layout(layout(*args, **kwds)) + # Try Graph.layout() + return graph.layout(layout, *args, **kwds) + + dim = kwds.get("dim", 2) + vattrs = graph.vertex_attributes() + if "x" in vattrs and "y" in vattrs: + if dim == 3 and "z" in vattrs: + return Layout(list(zip(graph.vs["x"], graph.vs["y"], graph.vs["z"]))) + else: + return Layout(list(zip(graph.vs["x"], graph.vs["y"]))) + + if graph.vcount() <= 100 and graph.is_connected(): + algo = "kk" + elif graph.vcount() <= 1000: + algo = "fr" + else: + algo = "drl" + return graph.layout(algo, *args, **kwds) + + +def _layout_sugiyama( + graph, + layers=None, + weights=None, + hgap=1, + vgap=1, + maxiter=100, +): + """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. + + B{References}: + + - K Sugiyama, S Tagawa, M Toda: Methods for visual understanding of + hierarchical system structures. I{IEEE Systems, Man and Cybernetics} + 11(2):109-125, 1981. + + - P Eades, X Lin and WF Smyth: A fast effective heuristic for the + feedback arc set problem. I{Information Processing Letters} 47:319-323, 1993. + + @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. + @return: the calculated layout and an additional list of matrices where the + i-th matrix contains the control points of edge I{i} in the original graph + (or an empty matrix if no control points are needed on the edge) + """ + coords, routing = GraphBase._layout_sugiyama( + graph, layers, weights, hgap, vgap, maxiter + ) + layout = Layout(coords) + layout.edge_routing = routing + return 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 + + +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 + + +# 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/igraph/matching.py b/src/igraph/matching.py similarity index 81% rename from igraph/matching.py rename to src/igraph/matching.py index 0e75ad9d2..afc966407 100644 --- a/igraph/matching.py +++ b/src/igraph/matching.py @@ -2,30 +2,10 @@ # -*- coding: utf-8 -*- """Classes representing matchings on graphs.""" -from igraph.clustering import VertexClustering from igraph._igraph import Vertex -__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 -""" - -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 @@ -65,7 +45,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 +56,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 +78,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 +107,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/__init__.py b/src/igraph/operators/__init__.py new file mode 100644 index 000000000..b2db4c2b7 --- /dev/null +++ b/src/igraph/operators/__init__.py @@ -0,0 +1,41 @@ +# 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__, + _disjoint_union, + _union, + _intersection, +) + +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, +} diff --git a/src/igraph/operators/functions.py b/src/igraph/operators/functions.py new file mode 100644 index 000000000..4ba3ede43 --- /dev/null +++ b/src/igraph/operators/functions.py @@ -0,0 +1,513 @@ +# vim:ts=4:sw=4:sts=4:et +# -*- coding: utf-8 -*- +"""Implementation of union, disjoint union and intersection operators.""" + +__all__ = ("disjoint_union", "union", "intersection") +__docformat__ = "google en" + +from igraph._igraph import GraphBase, _union, _intersection, _disjoint_union + +from warnings import warn + + +def name_set(names): + """Converts a list of names to a set of names while checking for duplicates. + + Parameters: + names: the list of names to convert + + Returns: + the set of unique names appearing in the list + + Raises: + RuntimeError: if the input name list has duplicates + """ + nameset = set(names) + if len(nameset) != len(names): + raise AttributeError("Graph contains duplicate vertex names") + return nameset + + +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. + + Parameters: + graphs: list of graphs. A lazy sequence is not acceptable. + + Returns: + 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: + raise ValueError("disjoint_union() needs at least one graph") + 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[a_name] + del graph_union[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. + + Parameters: + graphs: list of graphs. A lazy sequence is not acceptable. + 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). + + Returns: + the union graph + + Raises: + RuntimeError: if 'byname' is set to True and some graphs are not named or + the set of names is not unique in one of the graphs + """ + + 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( + f"Some, but not all graphs are named (got {n_named} named, " + f"{ngr-n_named} unnamed), not using vertex names", + stacklevel=1, + ) + elif byname and (n_named != ngr): + raise RuntimeError( + f"Some graphs are not named (got {n_named} named, {ngr-n_named} unnamed)" + ) + # Now we know that byname is only used if all graphs are named + + # Trivial cases + if ngr == 0: + raise ValueError("union() needs at least one graph") + 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(*(name_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"]] + + # permute_vertices() needs the inverse permutation + inv_permutation = [0] * len(permutation) + for i, x in enumerate(permutation): + inv_permutation[x] = i + ng = ng.permute_vertices(inv_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] + # 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 + if byname: + graph_union.vs["name"] = uninames + attrs = set.union(*(set(g.vertex_attributes()) for g in newgraphs)) - {"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. + + Parameters: + graphs: list of graphs. A lazy sequence is not acceptable. + 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. + + Returns: + the intersection graph + + Raises: + RuntimeError: if 'byname' is set to True and some graphs are not named or + the set of names is not unique in one of the graphs + """ + + 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( + f"Some, but not all graphs are named (got {n_named} named, " + f"{ngr-n_named} unnamed), not using vertex names", + stacklevel=1, + ) + elif byname and (n_named != ngr): + raise RuntimeError( + f"Some graphs are not named (got {n_named} named, {ngr-n_named} unnamed)" + ) + # Now we know that byname is only used if all graphs are named + + # Trivial cases + if ngr == 0: + raise ValueError("intersection() needs at least one graph") + 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(*(name_set(vns) for vns in allnames))) + else: + uninames = list(set.intersection(*(name_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"]] + + # permute_vertices() needs the inverse permutation + inv_permutation = [0] * len(permutation) + for i, x in enumerate(permutation): + inv_permutation[x] = i + ng = ng.permute_vertices(inv_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[a_name] + del graph_intsec[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)) - {"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/src/igraph/operators/methods.py b/src/igraph/operators/methods.py new file mode 100644 index 000000000..9306f9087 --- /dev/null +++ b/src/igraph/operators/methods.py @@ -0,0 +1,258 @@ +from igraph._igraph import ( + GraphBase, + Vertex, + Edge, +) +from igraph.seq import VertexSeq, EdgeSeq +from igraph.operators.functions import ( + disjoint_union, + union, + intersection, +) + + +__all__ = ( + "__iadd__", + "__add__", + "__and__", + "__isub__", + "__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.operators.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.operators.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). + + @see: L{__add__} + """ + if isinstance(other, (int, str)): + graph.add_vertices(other) + return graph + elif isinstance(other, tuple) and len(other) == 2: + graph.add_edges([other]) + return graph + elif isinstance(other, list): + if not other: + return graph + if isinstance(other[0], tuple): + graph.add_edges(other) + return graph + if isinstance(other[0], str): + graph.add_vertices(other) + return graph + return NotImplemented + + +def __add__(graph, 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 = graph.copy() + g.add_vertices(other) + elif isinstance(other, tuple) and len(other) == 2: + g = graph.copy() + g.add_edges([other]) + elif isinstance(other, list): + if len(other) > 0: + if isinstance(other[0], tuple): + g = graph.copy() + g.add_edges(other) + elif isinstance(other[0], str): + g = graph.copy() + g.add_vertices(other) + elif isinstance(other[0], Graph): + return graph.disjoint_union(other) + else: + return NotImplemented + else: + return graph.copy() + + elif isinstance(other, Graph): + return graph.disjoint_union(other) + else: + return NotImplemented + + return g + + +def __and__(graph, 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 graph.intersection(other) + else: + return NotImplemented + + +def __isub__(graph, other): + """In-place subtraction (difference). + + @see: L{__sub__}""" + if isinstance(other, int): + graph.delete_vertices([other]) + elif isinstance(other, tuple) and len(other) == 2: + graph.delete_edges([other]) + elif isinstance(other, list): + if len(other) > 0: + if isinstance(other[0], tuple): + graph.delete_edges(other) + elif isinstance(other[0], (int, str)): + graph.delete_vertices(other) + else: + return NotImplemented + elif isinstance(other, Vertex): + graph.delete_vertices(other) + elif isinstance(other, VertexSeq): + graph.delete_vertices(other) + elif isinstance(other, Edge): + graph.delete_edges(other) + elif isinstance(other, EdgeSeq): + graph.delete_edges(other) + else: + return NotImplemented + return graph + + +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 + 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 graph.difference(other) + + result = graph.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__(graph, 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 graph + elif other > 1: + return graph.disjoint_union([graph] * (other - 1)) + else: + return NotImplemented + + return NotImplemented + + +def __or__(graph, 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 graph.union(other) + else: + return NotImplemented diff --git a/igraph/remote/__init__.py b/src/igraph/remote/__init__.py similarity index 98% rename from igraph/remote/__init__.py rename to src/igraph/remote/__init__.py index e9e07dbc1..ba7da4dc7 100644 --- a/igraph/remote/__init__.py +++ b/src/igraph/remote/__init__.py @@ -1,2 +1 @@ """Classes that help igraph communicate with remote applications.""" - diff --git a/igraph/remote/gephi.py b/src/igraph/remote/gephi.py similarity index 70% rename from igraph/remote/gephi.py rename to src/igraph/remote/gephi.py index 61a7617cd..0a0694369 100644 --- a/igraph/remote/gephi.py +++ b/src/igraph/remote/gephi.py @@ -2,59 +2,30 @@ # -*- coding: utf-8 -*- """Classes that help igraph communicate with Gephi (http://www.gephi.org).""" -from igraph.compat import property -import urllib2 - -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__ = 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. +import json +import urllib.error +import urllib.parse +import urllib.request -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 -""" +__all__ = ("GephiConnection", "GephiGraphStreamer", "GephiGraphStreamingAPIFormat") +__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=0): + + def __init__(self, url=None, host="127.0.0.1", port=8080, workspace=1): """Constructs a connection to a Gephi master server. - The connection object can be constructed either by specifying the `url` - directly, or by specifying the `host`, `port` and `workspace` arguments. - The latter three are evaluated only if `url` is None; otherwise the - `url` will take precedence. + The connection object can be constructed either by specifying the + ``url`` directly, or by specifying the ``host``, ``port`` and + ``workspace`` arguments. The latter three are evaluated only if + ``url`` is None; otherwise the ``url`` will take precedence. - The `url` argument does not have to include the operation (e.g., + The ``url`` argument does not have to include the operation (e.g., ``?operation=updateGraph``); the connection will take care of it. - E.g., if you wish to connect to workspace 2 in a local Gephi instance on - port 7341, the correct form to use for the `url` is as follows:: + E.g., if you wish to connect to workspace 2 in a local Gephi instance + on port 7341, the correct form to use for the ``url`` is as follows:: http://localhost:7341/workspace0 """ @@ -66,7 +37,7 @@ def __init__(self, url=None, host="127.0.0.1", port=8080, workspace=0): def __del__(self): try: self.close() - except urllib2.URLError: + except urllib.error.URLError: # Happens when Gephi is closed before the connection is destroyed pass @@ -81,9 +52,9 @@ def close(self): def flush(self): """Flushes all the pending operations to the Gephi master server in a single request.""" - data = "".join(self._pending_operations) + 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 @@ -108,12 +79,12 @@ 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. """ - def get_add_node_event(self, identifier, attributes={}): + def get_add_node_event(self, identifier, attributes=None): """Generates a Python object corresponding to the event that adds a node with the given identifier and attributes in the Gephi graph streaming API. @@ -125,14 +96,14 @@ def get_add_node_event(self, identifier, attributes={}): >>> api.get_add_node_event("spam", dict(ham="eggs")) {'an': {'spam': {'ham': 'eggs'}}} """ - return {"an": {identifier: attributes}} + return {"an": {identifier: attributes if attributes is not None else {}}} - def get_add_edge_event(self, identifier, source, target, directed, attributes={}): + def get_add_edge_event(self, identifier, source, target, directed, attributes=None): """Generates a Python object corresponding to the event that adds an edge with the given source, target, directednessr and attributes in the Gephi graph streaming API. """ - result = dict(attributes) + result = dict(attributes if attributes is not None else {}) result["source"] = source result["target"] = target result["directed"] = bool(directed) @@ -195,13 +166,13 @@ 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. - + 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. @@ -214,7 +185,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"}}} @@ -228,20 +199,21 @@ def __init__(self, encoder=None): """Constructs a Gephi graph streamer that will post graphs to a given file-like object or a Gephi connection. - `encoder` must either be ``None`` or an instance of ``json.JSONEncoder`` + ``encoder`` must either be ``None`` or an instance of ``json.JSONEncoder`` and it must contain the JSON encoder to be used when posting JSON objects. """ self.encoder = encoder or json.JSONEncoder(ensure_ascii=True) self.format = GephiGraphStreamingAPIFormat() def iterjsonobj(self, graph): - """Iterates over the JSON objects that build up the graph using the Gephi - graph streaming API. The objects returned from this function are Python - objects; they must be formatted with ``json.dumps`` before sending them - to the destination.""" + """Iterates over the JSON objects that build up the graph using the + Gephi graph streaming API. The objects returned from this function are + Python objects; they must be formatted with ``json.dumps`` before + sending them 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,17 +224,21 @@ 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 - given JSON encoder. When `encoder` is ``None``, it falls back to the default - JSON encoder of the streamer in the `encoder` property. - - `destination` must be a file-like object or an instance of `GephiConnection`. + given JSON encoder. When ``encoder`` is ``None``, it falls back to the + default JSON encoder of the streamer in the `encoder` property. + + ``destination`` must be a file-like object or an instance of + `GephiConnection`. """ encoder = encoder or self.encoder for jsonobj in self.iterjsonobj(graph): @@ -271,18 +247,18 @@ def post(self, graph, destination, encoder=None): def send_event(self, event, destination, encoder=None, flush=True): """Sends a single JSON event to the given destination using the given - JSON encoder. When `encoder` is ``None``, it falls back to the default - JSON encoder of the streamer in the `encoder` property. - - `destination` must be a file-like object or an instance of `GephiConnection`. - - The method flushes the destination after sending the event. If you want to - avoid this (e.g., because you are sending many events), set `flush` to - ``False``. + JSON encoder. When ``encoder`` is ``None``, it falls back to the + default JSON encoder of the streamer in the `encoder` property. + + ``destination`` must be a file-like object or an instance of + `GephiConnection`. + + The method flushes the destination after sending the event. If you want + to avoid this (e.g., because you are sending many events), set + ``flush`` to ``False``. """ encoder = encoder or self.encoder - destination.write(encoder.encode(event)) - destination.write("\r\n") + destination.write(encoder.encode(event).encode("utf-8")) + destination.write(b"\r\n") if flush: destination.flush() - diff --git a/src/igraph/rewiring.py b/src/igraph/rewiring.py new file mode 100644 index 000000000..b4881a6c8 --- /dev/null +++ b/src/igraph/rewiring.py @@ -0,0 +1,25 @@ +from igraph._igraph import GraphBase + +from .utils import deprecated + +__all__ = ("_rewire", ) + + +def _rewire(graph, n=None, allowed_edge_types="simple", *, mode=None): + """Randomly rewires the graph while preserving the degree distribution. + + The rewiring is done \"in-place\", so the original graph will be modified. + If you want to preserve the original graph, use the L{copy} method before + rewiring. + + @param n: the number of rewiring trials. The default is 10 times the number + of edges. + @param allowed_edge_types: the rewiring algorithm to use. It can either be + C{"simple"} or C{"loops"}; the former does not create or destroy + loop edges while the latter does. + """ + if mode is not None: + deprecated("The 'mode' keyword argument is deprecated, use 'allowed_edge_types' instead") + allowed_edge_types = mode + + return GraphBase._rewire(graph, n, allowed_edge_types) diff --git a/src/igraph/seq.py b/src/igraph/seq.py new file mode 100644 index 000000000..3790cf510 --- /dev/null +++ b/src/igraph/seq.py @@ -0,0 +1,797 @@ +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 += "_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 = {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 += "_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 = {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, mode="all")) + + 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) + + +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 callable(func): + + 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", + "distances", + "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)) + + EdgeSeq.edge_betweenness = _graphmethod( + lambda self, result: [result[i] for i in self.indices], "edge_betweenness" + ) diff --git a/src/igraph/sparse_matrix.py b/src/igraph/sparse_matrix.py new file mode 100644 index 000000000..93c6fa7ae --- /dev/null +++ b/src/igraph/sparse_matrix.py @@ -0,0 +1,243 @@ +# vim:ts=4:sw=4:sts=4:et +# -*- coding: utf-8 -*- +"""Implementation of Python-level sparse matrix operations.""" + +from __future__ import with_statement + +__all__ = () +__docformat__ = "restructuredtext en" + +from operator import add +from igraph._igraph import ( + ADJ_DIRECTED, + ADJ_UNDIRECTED, + ADJ_MAX, + ADJ_MIN, + ADJ_PLUS, + ADJ_UPPER, + ADJ_LOWER, +) + + +_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 + + +def _maybe_halve_diagonal(m, condition): + """Halves all items in the diagonal of the given SciPy sparse matrix in + coo mode *if* and *only if* the given condition is True. + + Returns the row, column and data arrays. + """ + data_array = m.data + if condition: + # We can't do data_array[m.row == m.col] /= 2 here because we would be + # modifying the array in-place and the end user wouldn't like if we + # messed with their matrix. So we make a copy. + data_array = data_array.copy() + (idxs,) = (m.row == m.col).nonzero() + for i in idxs: + data_array[i] /= 2 + + return m.row, m.col, data_array + + +# 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="directed", loops="once"): + """Construct graph from sparse matrix, unweighted. + + @param loops: specifies how the diagonal of the matrix should be handled: + + - C{"ignore"} - ignore loop edges in the diagonal + - C{"once"} - treat the diagonal entries as loop edge counts + - C{"twice"} - treat the diagonal entries as I{twice} the number + of loop edges + """ + # 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 + + 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 + + mode = _convert_mode_argument(mode) + + keep_loops = loops == "twice" or loops == "once" or loops is True + m_row, m_col, m_data = _maybe_halve_diagonal(m, loops == "twice") + + if mode == "directed": + edges = [] + for i, j, n in zip(m_row, m_col, m_data): + if i != j or keep_loops: + edges.extend([(i, j)] * n) + + 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): + if i == j and not keep_loops: + continue + 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 and keep_loops: + 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 == "upper": + edges = [] + for i, j, n in zip(m_row, m_col, m_data): + if j > i or (keep_loops and j == i): + edges.extend([(i, j)] * n) + + elif mode == "lower": + edges = [] + for i, j, n in zip(m_row, m_col, m_data): + if j < i or (keep_loops and j == i): + edges.extend([(i, j)] * n) + + else: + raise ValueError(f"invalid mode: {mode!r}") + + return klass(nvert, edges=edges, directed=(mode == "directed")) + + +def _graph_from_weighted_sparse_matrix( + klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops="once" +): + """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. + + @param loops: specifies how to handle loop edges. When C{False} or + C{"ignore"}, the diagonal of the adjacency matrix will be ignored. When + C{True} or C{"once"}, the diagonal is assumed to contain the weight of the + corresponding loop edge. When C{"twice"}, the diagonal is assumed to + contain I{twice} the weight of the corresponding loop edge. + """ + # 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 + + 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 + + mode = _convert_mode_argument(mode) + + keep_loops = loops == "twice" or loops == "once" or loops is True + m_row, m_col, m_data = _maybe_halve_diagonal(m, loops == "twice") + + if mode == "directed": + if not keep_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: # loops == "once" or True + 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): + if i == j and not keep_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 == "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 keep_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 or (keep_loops and j == i): + edges.append((i, j)) + weights.append(n) + + elif mode == "lower": + edges, weights = [], [] + for i, j, n in zip(m_row, m_col, m_data): + if j < i or (keep_loops and j == i): + edges.append((i, j)) + weights.append(n) + + else: + raise ValueError(f"invalid mode: {mode!r}") + + return klass( + nvert, edges=edges, directed=(mode == "directed"), edge_attrs={attr: weights} + ) diff --git a/igraph/statistics.py b/src/igraph/statistics.py similarity index 74% rename from igraph/statistics.py rename to src/igraph/statistics.py index 15a0e1aa5..e134f7751 100644 --- a/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -4,56 +4,45 @@ Statistics related stuff in igraph """ -__license__ = u"""\ -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__ = ["FittedPowerLaw", "Histogram", "RunningMean", "mean", "median", \ - "percentile", "quantile", "power_law_fit"] +__all__ = ( + "FittedPowerLaw", + "Histogram", + "RunningMean", + "mean", + "median", + "percentile", + "quantile", + "power_law_fit", +) -class FittedPowerLaw(object): +class FittedPowerLaw: """Result of fitting a power-law to a vector of samples Example: >>> 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 + 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 @@ -69,9 +58,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 +79,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 +95,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): +class Histogram: """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 +132,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 +142,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 @@ -182,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 @@ -196,13 +192,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 +211,7 @@ def add_many(self, data): iterator = iter([data]) for x in iterator: self.add(x) + __lshift__ = add_many def clear(self): @@ -225,37 +222,20 @@ 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): + 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.) - 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. @@ -277,8 +257,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 +266,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 +280,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 +297,9 @@ 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 list, tuple, iterable or any other data source. The mean is calculated on the fly without explicitly summing the values, @@ -329,12 +308,11 @@ 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) - + 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 +337,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 +355,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: @@ -427,28 +405,27 @@ def sd(self): @property def var(self): """Returns the current variation""" - return self._sd ** 2 + 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) def __int__(self): return int(self._mean) - def __long__(self): - return long(self._mean) - def __complex__(self): return complex(self._mean) @@ -471,6 +448,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 +463,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 +479,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,12 +498,20 @@ 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): + +def power_law_fit(data, xmin=None, method="auto", p_precision=0.01): """Fitting a power-law distribution to empirical data + B{References} + + - MEJ Newman: Power laws, Pareto distributions and Zipf's law. + I{Contemporary Physics} 46, 323-351 (2005) + - A Clauset, CR Shalizi, MEJ Newman: Power-law distributions + in empirical data. E-print (2007). arXiv:0706.1062 + @param data: the data to fit, a list containing integer values @param xmin: the lower bound for fitting the power-law. If C{None}, the optimal xmin value will be estimated as well. Zero means that @@ -543,23 +530,23 @@ def power_law_fit(data, xmin=None, method="auto", return_alpha_only=False): size if n is small. - C{discrete}: exact maximum likelihood estimation when the - input comes from a discrete scale (see Clauset et al among the + input comes from a discrete scale (see Clauset et al. among the references). - C{auto}: exact maximum likelihood estimation where the continuous method is used if the input vector contains at least one fractional value and the discrete method is used if the input vector contains integers only. + @param p_precision: desired precision of the p-value calculation. The + precision ultimately depends on the number of resampling attempts. The + number of resampling trials is determined by 0.25 divided by the square + of the required precision. For instance, a required precision of 0.01 + means that 2500 samples will be drawn. @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) - @ref: A Clauset, CR Shalizi, MEJ Newman: Power-law distributions - in empirical data. E-print (2007). arXiv:0706.1062""" + """ from igraph._igraph import _power_law_fit if xmin is None or xmin < 0: @@ -570,14 +557,8 @@ def power_law_fit(data, xmin=None, method="auto", return_alpha_only=False): raise ValueError("unknown method: %s" % method) force_continuous = method in ("continuous", "hill") - 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") - return fit.alpha - else: - return fit + return FittedPowerLaw(*_power_law_fit(data, xmin, force_continuous, p_precision)) + 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 +603,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 +631,7 @@ def sd(xs): """ return RunningMean(xs).sd + def var(xs): """Returns the variance of an iterable. diff --git a/src/igraph/structural.py b/src/igraph/structural.py new file mode 100644 index 000000000..9f8014271 --- /dev/null +++ b/src/igraph/structural.py @@ -0,0 +1,89 @@ +from igraph._igraph import ( + IN, + OUT, + arpack_options as default_arpack_options, +) +from igraph.statistics import Histogram +from igraph.utils import deprecated + + +def _indegree(graph, *args, **kwds): + """Returns the in-degrees in a list. + + See L{GraphBase.degree} for possible arguments. + """ + kwds["mode"] = IN + return graph.degree(*args, **kwds) + + +def _outdegree(graph, *args, **kwds): + """Returns the out-degrees in a list. + + See L{GraphBase.degree} for possible arguments. + """ + kwds["mode"] = OUT + return graph.degree(*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{GraphBase.degree}. + + @param bin_width: the bin width of the histogram + @return: a histogram representing the degree distribution of the + graph. + """ + result = Histogram(bin_width, graph.degree(*args, **kwds)) + return result + + +def _pagerank( + graph, + vertices=None, + directed=True, + damping=0.85, + weights=None, + arpack_options=None, + implementation="prpack", +): + """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 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. + @return: a list with the PageRank values of the specified vertices. + """ + if arpack_options is None: + arpack_options = default_arpack_options + return graph.personalized_pagerank( + vertices, + directed, + damping, + None, + None, + weights, + arpack_options, + implementation, + ) + + +def _shortest_paths(graph, *args, **kwds): + """Deprecated alias to L{Graph.distances()}.""" + deprecated("Graph.shortest_paths() is deprecated; use Graph.distances() instead") + return graph.distances(*args, **kwds) diff --git a/igraph/summary.py b/src/igraph/summary.py similarity index 74% rename from igraph/summary.py rename to src/igraph/summary.py index 60dbdf0f0..122ad9982 100644 --- a/igraph/summary.py +++ b/src/igraph/summary.py @@ -1,41 +1,19 @@ # vim:ts=4:sw=4:sts=4:et # -*- coding: utf-8 -*- -"""Summary representation of a graph. +"""Summary representation of a graph.""" -@undocumented: _get_wrapper_for_width, FakeWrapper -""" +import sys -from igraph.vendor import vendor_import from igraph.statistics import median from itertools import islice from math import ceil +from texttable import Texttable from textwrap import TextWrapper -__all__ = ["GraphSummary"] +__all__ = ("GraphSummary", "summary") -__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 -""" - -texttable = vendor_import("texttable") - -class FakeWrapper(object): +class FakeWrapper: """Object whose interface is compatible with C{textwrap.TextWrapper} but does no wrapping.""" @@ -48,6 +26,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,9 +35,10 @@ def _get_wrapper_for_width(width, *args, **kwds): """ if width is None: return FakeWrapper(*args, **kwds) - return TextWrapper(width=width, *args, **kwds) + return TextWrapper(width, *args, **kwds) -class GraphSummary(object): + +class GraphSummary: """Summary representation of a graph. The summary representation includes a header line and the list of @@ -82,21 +62,20 @@ 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__(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 @@ -133,8 +112,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):" @@ -147,14 +125,13 @@ 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 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) @@ -164,7 +141,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)) @@ -177,7 +154,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] @@ -192,8 +169,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) @@ -206,9 +185,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")) @@ -224,7 +206,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 @@ -235,8 +217,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:"] @@ -247,36 +231,38 @@ def _construct_vertex_attributes(self): 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(), - ) + params = { + "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 def _edge_attribute_iterator(self, attribute_order): """Returns an iterator that yields the rows of the edge attribute table - in the summary. `attribute_order` must be a list containing the names of + in the summary. C{attribute_order} must be a list containing the names of the attributes to be presented in this table.""" arrow = self._arrow_format @@ -284,13 +270,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 @@ -321,7 +309,7 @@ def _infer_column_alignment(self, vertex_attrs=None, edge_attrs=None): def _new_table(self, headers=None): """Constructs a new table to pretty-print vertex and edge attributes""" - table = texttable.Texttable(max_width=0) + table = Texttable(max_width=0) table.set_deco(0) if headers is not None: table.header(headers) @@ -329,7 +317,7 @@ def _new_table(self, headers=None): def _vertex_attribute_iterator(self, attribute_order): """Returns an iterator that yields the rows of the vertex attribute table - in the summary. `attribute_order` must be a list containing the names of + in the summary. C{attribute_order} must be a list containing the names of the attributes to be presented in this table.""" for vertex in self._graph.vs: yield ["[%d]" % vertex.index] + [vertex[attr] for attr in attribute_order] @@ -349,7 +337,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" @@ -367,3 +355,21 @@ def __str__(self): 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") diff --git a/igraph/utils.py b/src/igraph/utils.py similarity index 64% rename from igraph/utils.py rename to src/igraph/utils.py index 8316f0800..867d68870 100644 --- a/igraph/utils.py +++ b/src/igraph/utils.py @@ -1,49 +1,44 @@ # 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 collections import MutableMapping + +from collections.abc import MutableMapping from itertools import chain +from warnings import warn import os import tempfile -__all__ = ["dbl_epsilon", "multidict", "named_temporary_file", "rescale", \ - "safemin", "safemax"] +__all__ = ( + "dbl_epsilon", + "deprecated", + "multidict", + "named_temporary_file", + "numpy_to_contiguous_memoryview", + "rescale", + "safemin", + "safemax", +) + __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 - -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 deprecated(message): + """Prints a warning message related to the deprecation of some igraph + feature.""" + warn(message, DeprecationWarning, stacklevel=3) + def _is_running_in_ipython(): """Internal function that determines whether igraph is running inside IPython or not.""" try: - # get_ipython is injected into the Python builtins by IPython so - # this should succeed in IPython but throw a NameError otherwise - get_ipython - return True - except NameError: + from IPython import get_ipython + + return get_ipython() is not None + except ImportError: return False @@ -62,33 +57,66 @@ def named_temporary_file(*args, **kwds): finally: os.unlink(tmpfile) -def rescale(values, out_range = (0., 1.), in_range = None, clamp = False, - scale = None): + +def numpy_to_contiguous_memoryview(obj): + """Converts a NumPy array or matrix into a contiguous memoryview object + that is suitable to be forwarded to the Graph constructor. + + This is used internally to allow us to use a NumPy array or matrix + directly when constructing a Graph. + """ + # Deferred import to prevent a hard dependency on NumPy + from numpy import int32, int64, require + from igraph._igraph import INTEGER_SIZE + + if INTEGER_SIZE == 64: + dtype = int64 + elif INTEGER_SIZE == 32: + dtype = int32 + else: + raise TypeError( + f"size of igraph_int_t in the C layer ({INTEGER_SIZE} bits) is not supported" + ) + + return memoryview(require(obj, dtype=dtype, requirements="AC")) + + +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 + ``out_range`` gives the range of the output values; by default, the minimum of the original numbers in the list will be mapped to the first element in the output range and the maximum will be mapped to the second element. Elements between the minimum and maximum values in the input list will be interpolated linearly between the first and second values of the output range. - `in_range` may be used to override which numbers are mapped to the first + ``in_range`` may be used to override which numbers are mapped to the first and second values of the output range. This must also be a tuple, where the first element will be mapped to the first element of the output range and the second element to the second. - If `clamp` is ``True``, elements which are outside the given `out_range` + If ``clamp`` is ``True``, elements which are outside the given ``out_range`` after rescaling are clamped to the output range to ensure that no number - will be outside `out_range` in the result. + will be outside ``out_range`` in the result. - If `scale` is not ``None``, it will be called for every element of `values` + If ``scale`` is not ``None``, it will be called for every element of ``values`` and the rescaling will take place on the results instead. This can be used, for instance, to transform the logarithm of the original values instead of the actual values. A typical use-case is to map a range of values to color - identifiers on a logarithmic scale. Scaling also applies to the `in_range` + identifiers on a logarithmic scale. Scaling also applies to the ``in_range`` parameter if present. + :param out_range: the range of output values + :param in_range: the range of the input values; this is the range that is mapped + to ``out_range``. ``None`` means to use the minimum and maximum of + the input, respectively. + :param clamp: specifies what to do when an input value falls outside ``in_range``. + ``True`` means to clamp the value to the bounds of ``in_range``, + ``False`` means not to clamp. + :param scale: an optional transformation to perform on the input values before + mapping them to the output range. + Examples: >>> rescale(range(5), (0, 8)) @@ -119,9 +147,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] @@ -130,49 +158,11 @@ def rescale(values, out_range = (0., 1.), in_range = None, clamp = False, else: 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. - When `circular` is ``True``, the pair consisting of the last + When ``circular`` is ``True``, the pair consisting of the last and first elements is also returned. Example: @@ -193,7 +183,7 @@ def consecutive_pairs(iterable, circular=False): it = iter(iterable) try: - prev = it.next() + prev = next(it) except StopIteration: return first = prev @@ -208,12 +198,13 @@ def consecutive_pairs(iterable, circular=False): except UnboundLocalError: yield first, first + class multidict(MutableMapping): """A dictionary-like object that is customized to deal with multiple values for the same key. Each value in this dictionary will be a list. Methods which emulate - the methods of a standard Python `dict` object will return or manipulate + the methods of a standard Python ``dict`` object will return or manipulate the first items of the lists only. Special methods are provided to deal with keys having multiple values. """ @@ -221,22 +212,25 @@ class multidict(MutableMapping): 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))) + raise ValueError( + "%r expected at most 1 argument, got %d" + % (self.__class__.__name__, len(args)) + ) if args: args = args[0] self.update(args) self.update(kwds) def __contains__(self, key): - """Returns whether there are any items associated to the given `key`.""" + """Returns whether there are any items associated to the given + ``key``.""" try: return len(self._dict[key]) > 0 except KeyError: return False def __delitem__(self, key): - """Removes all the items associated to the given `key`.""" + """Removes all the items associated to the given ``key``.""" del self._dict[key] def __getitem__(self, key): @@ -252,7 +246,7 @@ def __getitem__(self, key): try: return self._dict[key][0] except IndexError: - raise KeyError(key) + raise KeyError(key) from None def __iter__(self): """Iterates over the keys of the multidict.""" @@ -263,8 +257,8 @@ def __len__(self): return len(self._dict) def __setitem__(self, key, value): - """Sets the item associated to the given `key`. Any values associated to the - key will be erased and replaced by `value`. + """Sets the item associated to the given ``key``. Any values associated to the + key will be erased and replaced by ``value``. Example: @@ -276,7 +270,7 @@ def __setitem__(self, key, value): self._dict[key] = [value] def add(self, key, value): - """Adds `value` to the list of items associated to `key`. + """Adds `value` to the list of items associated to ``key``. Example: @@ -298,8 +292,8 @@ def clear(self): self._dict.clear() def get(self, key, default=None): - """Returns an arbitrary item associated to the given `key`. If `key` - does not exist or has zero associated items, `default` will be + """Returns an arbitrary item associated to the given ``key``. If ``key`` + does not exist or has zero associated items, ``default`` will be returned.""" try: items = self._dict[key] @@ -308,7 +302,7 @@ def get(self, key, default=None): return default def getlist(self, key): - """Returns the list of values for the given `key`. An empty list will + """Returns the list of values for the given ``key``. An empty list will be returned if there is no such key.""" try: return self._dict[key] @@ -318,12 +312,12 @@ 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): @@ -332,9 +326,10 @@ def update(self, arg, **kwds): 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) + def safemax(iterable, default=0): """Safer variant of ``max()`` that returns a default value if the iterable is empty. @@ -350,12 +345,13 @@ def safemax(iterable, default=0): """ it = iter(iterable) try: - first = it.next() + first = next(it) except StopIteration: return default else: return max(chain([first], it)) + def safemin(iterable, default=0): """Safer variant of ``min()`` that returns a default value if the iterable is empty. @@ -371,12 +367,13 @@ def safemin(iterable, default=0): """ it = iter(iterable) try: - first = it.next() + first = next(it) except StopIteration: return default else: return min(chain([first], it)) + def dbl_epsilon(): """Approximates the machine epsilon value for doubles.""" epsilon = 1.0 @@ -384,4 +381,5 @@ def dbl_epsilon(): epsilon /= 2 return epsilon + dbl_epsilon = dbl_epsilon() diff --git a/src/igraph/version.py b/src/igraph/version.py new file mode 100644 index 000000000..b69224ddc --- /dev/null +++ b/src/igraph/version.py @@ -0,0 +1,2 @@ +__version_info__ = (1, 0, 0) +__version__ = ".".join("{0}".format(x) for x in __version_info__) diff --git a/src/py2compat.c b/src/py2compat.c deleted file mode 100644 index 31fbec867..000000000 --- a/src/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/py2compat.h b/src/py2compat.h deleted file mode 100644 index 6d8a5bfb0..000000000 --- a/src/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) - -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/pyhelpers.c b/src/pyhelpers.c deleted file mode 100644 index 633e4d73b..000000000 --- a/src/pyhelpers.c +++ /dev/null @@ -1,146 +0,0 @@ -/* vim:set ts=4 sw=2 sts=2 et: */ -/* - IGraph library - Python interface. - Copyright (C) 2006-2011 Tamas Nepusz - 5 Avenue Road, Staines, Middlesex, TW18 3AW, United Kingdom - - 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" -#include "pyhelpers.h" - -/** - * Creates a Python list and fills it with a pre-defined item. - * - * \param len the length of the list to be created - * \param item the item with which the list will be filled - */ -PyObject* igraphmodule_PyList_NewFill(Py_ssize_t len, PyObject* item) { - Py_ssize_t i; - PyObject* result = PyList_New(len); - - if (result == 0) - return 0; - - for (i = 0; i < len; i++) { - Py_INCREF(item); - PyList_SET_ITEM(result, i, item); /* reference to item stolen */ - } - - return result; -} - -/** - * Creates a Python list and fills it with zeroes. - * - * \param len the length of the list to be created - */ -PyObject* igraphmodule_PyList_Zeroes(Py_ssize_t len) { - PyObject* zero = PyInt_FromLong(0); - PyObject* result; - - if (zero == 0) - return 0; - - result = igraphmodule_PyList_NewFill(len, zero); - Py_DECREF(zero); - return result; -} - -/** - * Converts a Python object to its string representation and returns it as - * a C string. - * - * It is the responsibility of the caller to release the C string. - */ -char* igraphmodule_PyObject_ConvertToCString(PyObject* string) { - char* result; - - if (string == 0) - return 0; - - if (!PyBaseString_Check(string)) { - string = PyObject_Str(string); - if (string == 0) - return 0; - } else { - Py_INCREF(string); - } - - result = PyString_CopyAsString(string); - Py_DECREF(string); - - return result; -} - -/** - * Creates a Python range object with the given start and stop indices and step - * size. - * - * The function returns a new reference. It is the responsibility of the caller - * to release it. Returns \c NULL in case of an error. - */ -PyObject* igraphmodule_PyRange_create(Py_ssize_t start, Py_ssize_t stop, Py_ssize_t step) { - static PyObject* builtin_module = 0; - static PyObject* range_func = 0; - 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; - } - } - - result = PyObject_CallFunction(range_func, "lll", start, stop, step); - return result; -} - -/** - * Generates a hash value for a plain C pointer. - * - * This function is a copy of \c _Py_HashPointer from \c Objects/object.c in - * the source code of Python's C implementation. - */ -long igraphmodule_Py_HashPointer(void *p) { - long x; - size_t y = (size_t)p; - - /* bottom 3 or 4 bits are likely to be 0; rotate y by 4 to avoid - * excessive hash collisions for dicts and sets */ - y = (y >> 4) | (y << (8 * sizeof(p) - 4)); - x = (long)y; - if (x == -1) - x = -2; - return x; -} diff --git a/src/pyhelpers.h b/src/pyhelpers.h deleted file mode 100644 index 00c518e95..000000000 --- a/src/pyhelpers.h +++ /dev/null @@ -1,40 +0,0 @@ -/* vim:set ts=4 sw=2 sts=2 et: */ -/* - IGraph library - Python interface. - Copyright (C) 2006-2011 Tamas Nepusz - 5 Avenue Road, Staines, Middlesex, TW18 3AW, United Kingdom - - 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_HELPERS_H -#define PYTHON_HELPERS_H - -#include - -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); -long igraphmodule_Py_HashPointer(void *p); - -#define PY_IGRAPH_DEPRECATED(msg) \ - PyErr_WarnEx(PyExc_DeprecationWarning, (msg), 1) -#define PY_IGRAPH_WARN(msg) \ - PyErr_WarnEx(PyExc_RuntimeWarning, (msg), 1) - -#endif diff --git a/src/random.c b/src/random.c deleted file mode 100644 index bb623dfcc..000000000 --- a/src/random.c +++ /dev/null @@ -1,205 +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" -#include "random.h" -#include -#include - -/** - * \ingroup python_interface_rng - * \brief Internal data structure for storing references to the - * functions used from Python's random number generator. - */ -typedef struct { - PyObject* randint_func; - PyObject* random_func; - PyObject* gauss_func; -} igraph_i_rng_Python_state_t; - -static igraph_i_rng_Python_state_t igraph_rng_Python_state = {0, 0, 0}; -static igraph_rng_t igraph_rng_Python = {0, 0, 0}; - -int igraph_rng_Python_init(void **state) { - IGRAPH_ERROR("Python RNG error, unsupported function called", - IGRAPH_EINTERNAL); - return 0; -} - -void igraph_rng_Python_destroy(void *state) { - igraph_error("Python RNG error, unsupported function called", - __FILE__, __LINE__, IGRAPH_EINTERNAL); -} - -/** - * \ingroup python_interface_rng - * \brief Sets the random number generator used by igraph. - */ -PyObject* igraph_rng_Python_set_generator(PyObject* self, PyObject* object) { - igraph_i_rng_Python_state_t new_state, old_state; - PyObject* func; - - 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()); - Py_RETURN_NONE; - } - -#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; \ - } \ -} - - GET_FUNC("randint"); new_state.randint_func = func; - GET_FUNC("random"); new_state.random_func = func; - GET_FUNC("gauss"); new_state.gauss_func = func; - - old_state = igraph_rng_Python_state; - igraph_rng_Python_state = new_state; - Py_XDECREF(old_state.randint_func); - Py_XDECREF(old_state.random_func); - Py_XDECREF(old_state.gauss_func); - - igraph_rng_set_default(&igraph_rng_Python); - - Py_RETURN_NONE; -} - -/** - * \ingroup python_interface_rng - * \brief Sets the seed of the random generator. - */ -int igraph_rng_Python_seed(void *state, unsigned long int seed) { - IGRAPH_ERROR("Python RNG error, unsupported function called", - IGRAPH_EINTERNAL); - return 0; -} - -/** - * \ingroup python_interface_rng - * \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); - unsigned long int retval; - - if (result == 0) { - PyErr_WriteUnraisable(PyErr_Occurred()); - PyErr_Clear(); - /* Fallback to the C random generator */ - return rand() * LONG_MAX; - } - retval = PyInt_AsLong(result); - Py_DECREF(result); - return retval; -} - -/** - * \ingroup python_interface_rng - * \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); - double retval; - - if (result == 0) { - PyErr_WriteUnraisable(PyErr_Occurred()); - PyErr_Clear(); - /* Fallback to the C random generator */ - return rand(); - } - - retval = PyFloat_AsDouble(result); - Py_DECREF(result); - return retval; -} - -/** - * \ingroup python_interface_rng - * \brief Generates a real number distributed according to the normal distribution - * 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); - double retval; - - if (result == 0) { - PyErr_WriteUnraisable(PyErr_Occurred()); - PyErr_Clear(); - /* Fallback to the C random generator */ - return 0; - } - - retval = PyFloat_AsDouble(result); - Py_DECREF(result); - return retval; -} - -/** - * \ingroup python_interface_rng - * \brief Specification table for Python's random number generator. - * This tells igraph which functions to call to obtain random numbers. - */ -igraph_rng_type_t igraph_rngtype_Python = { - /* name= */ "Python random generator", - /* min= */ 0, - /* max= */ LONG_MAX, - /* init= */ igraph_rng_Python_init, - /* destroy= */ igraph_rng_Python_destroy, - /* seed= */ igraph_rng_Python_seed, - /* get= */ igraph_rng_Python_get, - /* get_real */ igraph_rng_Python_get_real, - /* get_norm= */ igraph_rng_Python_get_norm, - /* get_geom= */ 0, - /* get_binom= */ 0 -}; - -void igraphmodule_init_rng(PyObject* igraph_module) { - PyObject* random_module; - - if (igraph_rng_Python.state != 0) - return; - - random_module = PyImport_ImportModule("random"); - if (random_module == 0) { - PyErr_WriteUnraisable(PyErr_Occurred()); - PyErr_Clear(); - return; - } - - igraph_rng_Python.type = &igraph_rngtype_Python; - igraph_rng_Python.state = &igraph_rng_Python_state; - - if (igraph_rng_Python_set_generator(igraph_module, random_module) == 0) { - PyErr_WriteUnraisable(PyErr_Occurred()); - PyErr_Clear(); - return; - } - Py_DECREF(random_module); -} diff --git a/test.sh b/test.sh index f6249f4bb..cc34216c2 100755 --- a/test.sh +++ b/test.sh @@ -1,59 +1,68 @@ #!/bin/bash -if [ x$1 == x-d -a -x /usr/bin/valgrind ]; then - # Checking memory leaks with Valgrind - echo "Valgrind memory leak debugging enabled" - FNAME=/tmp/igraph_${RANDOM}.supp - cat /usr/lib/valgrind/python.supp >$FNAME - cat $0|awk 'BEGIN { ok=0 } /[S]UPPRESSIONS/ { first=1 } { if (first) { ok=1; first=0; } else if (ok) { print; } }' >>$FNAME - PRE="valgrind --tool=memcheck --leak-check=yes --trace-children=yes --suppressions=$FNAME" - shift + +PYTHON=python3 + +############################################################################### + +set -e + +CLEAN=0 +VENV_DIR=.venv + +while getopts ":ce:k:s" OPTION; do + case $OPTION in + c) + CLEAN=1 + ;; + e) + VENV_DIR=$OPTARG + ;; + k) + PYTEST_ARGS="${PYTEST_ARGS} -k $OPTARG" + ;; + s) + USE_SANITIZERS=1 + ;; + \?) + echo "Usage: $0 [-c] [-e VIRTUALENV] [-s]" + echo "" + echo "Options:" + echo " -c: clean igraph's already-built C core before running tests" + echo " -e VIRTUALENV: use the given virtualenv instead of .venv" + echo " -s: compile the C core and the Python module with sanitizers enabled" + exit 1 + ;; + esac +done +shift $((OPTIND -1)) + +if [ "x$USE_SANITIZERS" = x1 ]; then + if [ "`python3 -c 'import platform; print(platform.system())'`" != "Linux" ]; then + echo "Compiling igraph with sanitizers is currently supported on Linux only." + exit 1 + fi +fi + +if [ ! -d $VENV_DIR ]; then + $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 + +# pip install is called in verbose mode so we can see the compiler warnings +if [ "x$USE_SANITIZERS" = x1 ]; then + # Do not run plotting tests -- they tend to have lots of false positives in + # the sanitizer output + IGRAPH_USE_SANITIZERS=1 $VENV_DIR/bin/pip install -v .[test] + LD_PRELOAD=$(gcc -print-file-name=libasan.so) \ + LSAN_OPTIONS=suppressions=etc/lsan-suppr.txt:print_suppressions=false \ + ASAN_OPTIONS=detect_stack_use_after_return=1 \ + $VENV_DIR/bin/pytest tests ${PYTEST_ARGS} else - PRE="" + $VENV_DIR/bin/pip install -v .[plotting,test] + $VENV_DIR/bin/pytest tests ${PYTEST_ARGS} fi -DYLD_LIBRARY_PATH=src/.libs LD_LIBRARY_PATH=src/.libs $PRE python $1 interfaces/python/setup.py test -if [ x$FNAME != x ]; then rm -f $FNAME; fi - -exit 0 -################## SUPPRESSIONS for Valgrind ######################## -{ - - Memcheck:Cond - obj:/lib/ld-2.3.5.so - obj:/lib/ld-2.3.5.so - obj:/lib/ld-2.3.5.so - obj:/lib/ld-2.3.5.so - obj:/lib/ld-2.3.5.so -} -{ - - Memcheck:Cond - obj:/lib/ld-2.3.5.so - obj:/lib/ld-2.3.5.so - obj:/lib/ld-2.3.5.so - obj:/lib/tls/i686/cmov/libc-2.3.5.so - obj:/lib/ld-2.3.5.so - fun:_dl_open - obj:/lib/tls/i686/cmov/libdl-2.3.5.so - obj:/lib/ld-2.3.5.so - obj:/lib/tls/i686/cmov/libdl-2.3.5.so - fun:dlopen - fun:_PyImport_GetDynLoadFunc - fun:_PyImport_LoadDynamicModule -} -{ - - Memcheck:Cond - obj:/lib/ld-2.3.5.so - obj:/lib/tls/i686/cmov/libc-2.3.5.so - obj:/lib/ld-2.3.5.so - fun:_dl_open - obj:/lib/tls/i686/cmov/libdl-2.3.5.so - obj:/lib/ld-2.3.5.so - obj:/lib/tls/i686/cmov/libdl-2.3.5.so - fun:dlopen - fun:_PyImport_GetDynLoadFunc - fun:_PyImport_LoadDynamicModule - obj:/usr/bin/python2.4 - obj:/usr/bin/python2.4 -} - diff --git a/test/cytoscape_test.py b/test/cytoscape_test.py deleted file mode 100755 index 111ab1cba..000000000 --- a/test/cytoscape_test.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -"""Simple script that tests CytoscapeGraphDrawer. - -This script is kept separate from the unit tests as it is very -hard to test for the correctness of CytoscapeGraphDrawer without -a working instance of Cytoscape. - -Prerequisites for running this test: - - 1. Start Cytoscape - 2. Activate the Cytoscape RPC plugin, listening at port 9000 - -""" - -from igraph import Graph -from igraph.drawing.graph import CytoscapeGraphDrawer - -def test(): - g = Graph.GRG(100, 0.2) - - ### Adding network attributes - g["name"] = "Network name" - g["version"] = 5 - g["obsolete"] = False - g["density"] = g.density() - - ### Adding vertex attributes - # String attribute - g.vs["name"] = ["Node %d" % (i+1) for i in xrange(g.vcount())] - # Integer attribute - g.vs["degree"] = g.degree() - # Float attribute - g.vs["pagerank"] = g.pagerank() - # Boolean attribute - g.vs["even"] = [i % 2 for i in xrange(g.vcount())] - # Mixed attribute - g.vs["mixed"] = ["abc", 123, None, 1.0] * ((g.vcount()+3) / 4) - # Special attribute with Hungarian accents - g.vs[0]["name"] = u"árvíztűrő tükörfúrógép ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP" - - ### Adding edge attributes - # String attribute - g.es["name"] = ["Edge %d -- %d" % edge.tuple for edge in g.es] - # Integer attribute - g.es["multiplicity"] = g.count_multiple() - # Float attribute - g.es["betweenness"] = g.edge_betweenness() - # Boolean attribute - g.es["even"] = [i % 2 for i in xrange(g.ecount())] - # Mixed attribute - g.es["mixed"] = [u"yay", 123, None, 0.7] * ((g.ecount()+3) / 4) - - # Sending graph - drawer = CytoscapeGraphDrawer() - drawer.draw(g, layout="fr") - - # Fetching graph - g2 = drawer.fetch() - del g2.vs["hiddenLabel"] - del g2.es["interaction"] - - # Check isomorphism - result = g2.isomorphic(g) - if not result: - raise ValueError("g2 not isomorphic to g") - - # Check the graph attributes - if set(g.attributes()) != set(g2.attributes()): - raise ValueError("Graph attribute name set mismatch") - for attr_name in g.attributes(): - if g[attr_name] != g2[attr_name]: - raise ValueError("Graph attribute mismatch for %r" % attr_name) - - # Check the vertex attribute names - if set(g.vertex_attributes()) != set(g2.vertex_attributes()): - raise ValueError("Vertex attribute name set mismatch") - - # Check the edge attribute names - if set(g.edge_attributes()) != set(g2.edge_attributes()): - raise ValueError("Edge attribute name set mismatch") - - -if __name__ == "__main__": - test() - diff --git a/test/unittests.py b/test/unittests.py deleted file mode 100755 index eb0f993ba..000000000 --- a/test/unittests.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -""" -Simple script that runs the unit tests of igraph. -""" - -import sys - - -def run_unittests(): - from igraph.test import run_tests - if "-v" in sys.argv: - verbosity = 2 - else: - verbosity = 1 - return run_tests(verbosity=verbosity) - - -if __name__ == "__main__": - if run_unittests(): - sys.exit(0) - else: - sys.exit(1) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb 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 000000000..282f3fb21 Binary files /dev/null and b/tests/drawing/cairo/baseline_images/clustering_directed.png differ 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 000000000..eb6b54aa6 Binary files /dev/null and b/tests/drawing/cairo/baseline_images/clustering_directed_large.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_basic.png b/tests/drawing/cairo/baseline_images/graph_basic.png new file mode 100644 index 000000000..cfd394e49 Binary files /dev/null and b/tests/drawing/cairo/baseline_images/graph_basic.png differ 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 000000000..b94e82169 Binary files /dev/null and b/tests/drawing/cairo/baseline_images/graph_directed.png differ 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 000000000..b94e82169 Binary files /dev/null and b/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png differ 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 000000000..0e437f288 Binary files /dev/null and b/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_null.png b/tests/drawing/cairo/baseline_images/graph_null.png new file mode 100644 index 000000000..5cd2e6483 Binary files /dev/null and b/tests/drawing/cairo/baseline_images/graph_null.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_with_curved_edges.png b/tests/drawing/cairo/baseline_images/graph_with_curved_edges.png new file mode 100644 index 000000000..68846cc94 Binary files /dev/null and b/tests/drawing/cairo/baseline_images/graph_with_curved_edges.png differ diff --git a/tests/drawing/cairo/test_graph.py b/tests/drawing/cairo/test_graph.py new file mode 100644 index 000000000..7f99eae77 --- /dev/null +++ b/tests/drawing/cairo/test_graph.py @@ -0,0 +1,144 @@ +import random +import unittest + +from igraph import Graph, 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 are_tests_supported, find_image_comparison, result_image_folder +except ImportError: + from utils import are_tests_supported, find_image_comparison, result_image_folder + + +image_comparison = find_image_comparison() + + +class GraphTestRunner(unittest.TestCase): + @classmethod + def setUpClass(cls): + supported, msg = are_tests_supported() + if not supported: + raise unittest.SkipTest(f"{msg}, skipping tests") + result_image_folder.mkdir(parents=True, exist_ok=True) + + def setUp(self) -> None: + random.seed(42) + + @image_comparison(baseline_images=["graph_basic"]) + def test_basic(self): + g = Graph.Ring(5) + lo = g.layout("auto") + plot( + g, + layout=lo, + target=result_image_folder / "graph_basic.png", + backend="cairo", + ) + + @image_comparison(baseline_images=["graph_directed"]) + def test_directed(self): + g = Graph.Ring(5, directed=True) + lo = g.layout("auto") + plot( + g, + layout=lo, + target=result_image_folder / "graph_directed.png", + backend="cairo", + ) + + @image_comparison(baseline_images=["graph_mark_groups_directed"]) + def test_mark_groups(self): + g = Graph.Ring(5, directed=True) + lo = g.layout("auto") + plot( + g, + layout=lo, + target=result_image_folder / "graph_mark_groups_directed.png", + backend="cairo", + mark_groups=True, + ) + + @image_comparison(baseline_images=["graph_mark_groups_squares_directed"]) + def test_mark_groups_squares(self): + g = Graph.Ring(5, directed=True) + lo = g.layout("auto") + plot( + g, + layout=lo, + target=result_image_folder / "graph_mark_groups_squares_directed.png", + backend="cairo", + mark_groups=True, + vertex_shape="square", + ) + + @image_comparison(baseline_images=["graph_with_curved_edges"]) + def test_graph_with_curved_edges(self): + g = Graph.Ring(24, directed=True, mutual=True) + lo = g.layout("circle") + plot( + g, + layout=lo, + target=result_image_folder / "graph_with_curved_edges.png", + bbox=(800, 800), + edge_curved=0.25, + backend="cairo", + ) + + @image_comparison(baseline_images=["graph_null"]) + def test_null_graph(self): + g = Graph() + plot(g, backend="cairo", target=result_image_folder / "graph_null.png") + + +class ClusteringTestRunner(unittest.TestCase): + @classmethod + def setUpClass(cls): + supported, msg = are_tests_supported() + if not supported: + raise unittest.SkipTest(f"{msg}, skipping tests") + result_image_folder.mkdir(parents=True, exist_ok=True) + + def setUp(self) -> None: + random.seed(42) + + @image_comparison(baseline_images=["clustering_directed"]) + def test_clustering_directed_small(self): + g = Graph.Ring(5, directed=True) + clu = VertexClustering(g, [0] * 5) + lo = g.layout("auto") + plot( + clu, + layout=lo, + backend="cairo", + target=result_image_folder / "clustering_directed.png", + mark_groups=True, + ) + + @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) + layout = [(x * 2.5, y * 2.5) for x, y in g.layout("circle")] + plot( + clu, + backend="cairo", + layout=layout, + target=result_image_folder / "clustering_directed_large.png", + mark_groups=True, + ) + + +def suite(): + graph = unittest.defaultTestLoader.loadTestsFromTestCase(GraphTestRunner) + clustering = unittest.defaultTestLoader.loadTestsFromTestCase(ClusteringTestRunner) + return unittest.TestSuite([graph, clustering]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/tests/drawing/cairo/utils.py b/tests/drawing/cairo/utils.py new file mode 100644 index 000000000..08b7a9360 --- /dev/null +++ b/tests/drawing/cairo/utils.py @@ -0,0 +1,184 @@ +# 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 + +from igraph.drawing import find_cairo + +cairo = find_cairo() +if not hasattr(cairo, "version"): + cairo = None + +__all__ = ("find_image_comparison", "result_image_folder") + + +result_image_folder = Path("result_images") / "cairo" + + +def find_open_image_png_function(): + try: + from cv2 import imread + + def fun(filename): + return imread(str(filename)) + + return fun + except ImportError: + pass + + try: + import numpy as np + from PIL import Image + + def fun(filename): + with Image.open(filename) as f: + return np.asarray(f) + + return fun + except ImportError: + pass + + raise ImportError("PIL+NumPy or OpenCV required to run Cairo tests") + + +def find_image_comparison(): + def dummy_comparison(*args, **kwargs): + return lambda *args, **kwargs: None + + if cairo is None: + return dummy_comparison + return image_comparison + + +def are_tests_supported(): + if cairo is None: + return False, "cairo not found" + + try: + find_open_image_png_function() + except ImportError: + return False, "PIL+NumPy or OpenCV not found" + + return True, "" + + +def _load_image(filename, fmt): + if fmt == "png": + return find_open_image_png_function()(filename) + + raise NotImplementedError(f"Image format {fmt} not implemented yet") + + +def _load_baseline_images(filenames, fmt="png"): + baseline_folder = Path(__file__).parent / "baseline_images" + + images = [] + for fn in filenames: + fn_abs = baseline_folder / f"{fn}.{fmt}" + image = _load_image(fn_abs, fmt) + assert image is not None + images.append(image) + return images + + +def _load_result_images(filenames, fmt="png"): + images = [] + for fn in filenames: + fn_abs = result_image_folder / f"{fn}.{fmt}" + image = _load_image(fn_abs, fmt) + assert image is not None + images.append(image) + return images + + +def _compare_image_png(baseline, fig, tol=0): + import numpy as np + + diff = (np.abs(baseline - fig)).sum() + if diff <= tol: + return False + else: + return diff + + +def compare_image(baseline, fig, tol=0, fmt="png"): + if fmt == "png": + return _compare_image_png(baseline, fig, tol=tol) + + raise NotImplementedError(f"Image format {fmt} not implemented yet") + + +def _unittest_image_comparison( + baseline_images, + tol, +): + """ + Decorate function with image comparison for unittest. + This function creates a decorator that wraps a figure-generating function + with image comparison code. + """ + + 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): + # This decorator is applied to unittest methods + self = args[0] + + # Three steps: + # 1. run the function + func(*args, **kwargs) + + # 2. locate the control and result images + baselines = _load_baseline_images(baseline_images) + figs = _load_result_images(baseline_images) + + # 3. compare them one by one + for _i, (baseline, fig) in enumerate(zip(baselines, figs)): + res = compare_image(baseline, fig, tol) + self.assertLessEqual(res, tol) + + 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, +): + """ + 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. + """ + if sys.maxsize <= 2**32: + tol += 0.06 + return _unittest_image_comparison( + baseline_images=baseline_images, + tol=tol, + ) diff --git a/tests/drawing/matplotlib/__init__.py b/tests/drawing/matplotlib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed.png b/tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed.png new file mode 100644 index 000000000..3d87f0980 Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed.png differ diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed_large.png b/tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed_large.png new file mode 100644 index 000000000..db95896af Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed_large.png differ 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 000000000..06cbf300a Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_basic.png differ diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_directed.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_directed.png new file mode 100644 index 000000000..6235d7c16 Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_directed.png differ diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_directed_curved_loops.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_directed_curved_loops.png new file mode 100644 index 000000000..9062bfb10 Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_directed_curved_loops.png differ 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 000000000..21234699a Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_edit_children.png differ diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_labels.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_labels.png new file mode 100644 index 000000000..817b0c7e8 Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_labels.png differ diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_layout_attribute.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_layout_attribute.png new file mode 100644 index 000000000..ed7d14a4f Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_layout_attribute.png differ 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 000000000..6235d7c16 Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_mark_groups_directed.png differ diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_mark_groups_squares_directed.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_mark_groups_squares_directed.png new file mode 100644 index 000000000..732781227 Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_mark_groups_squares_directed.png differ diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_null.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_null.png new file mode 100644 index 000000000..14b94a347 Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_null.png differ diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_with_curved_edges.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_with_curved_edges.png new file mode 100644 index 000000000..324147111 Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/graph_with_curved_edges.png differ diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/multigraph_with_curved_edges_undirected.png b/tests/drawing/matplotlib/baseline_images/test_graph/multigraph_with_curved_edges_undirected.png new file mode 100644 index 000000000..2a5f6eed8 Binary files /dev/null and b/tests/drawing/matplotlib/baseline_images/test_graph/multigraph_with_curved_edges_undirected.png differ diff --git a/tests/drawing/matplotlib/test_graph.py b/tests/drawing/matplotlib/test_graph.py new file mode 100644 index 000000000..b77d42174 --- /dev/null +++ b/tests/drawing/matplotlib/test_graph.py @@ -0,0 +1,296 @@ +import os +import unittest + + +from igraph import Graph, plot, VertexClustering, Layout + +from ...utils import overridden_configuration + +# 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 + + mpl.use("agg") + import matplotlib.pyplot as plt +except ImportError: + mpl = None + plt = None + +image_comparison = find_image_comparison() + + +class GraphTestRunner(unittest.TestCase): + def setUp(self): + if mpl is None or plt is None: + raise unittest.SkipTest("matplotlib not found, skipping tests") + + @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_labels"], remove_text=True) + def test_labels(self): + plt.close("all") + g = Graph.Ring(5) + fig, ax = plt.subplots(figsize=(3, 3)) + plot( + g, + target=ax, + layout=self.layout_small_ring, + vertex_label=["1", "2", "3", "4", "5"], + vertex_label_color="white", + vertex_label_size=16, + ) + + @image_comparison(baseline_images=["graph_layout_attribute"], remove_text=True) + def test_layout_attribute(self): + plt.close("all") + g = Graph.Ring(5) + g["layout"] = Layout([(x, x) for x in range(g.vcount())]) + fig, ax = plt.subplots() + plot(g, target=ax) + + @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_directed_curved_loops"], remove_text=True) + def test_directed_curved_loops(self): + plt.close("all") + g = Graph.Ring(5, directed=True) + g.add_edge(0, 0) + g.add_edge(0, 0) + g.add_edge(2, 2) + fig, ax = plt.subplots() + ax.set_xlim(-1.2, 1.2) + ax.set_ylim(-1.2, 1.2) + plot( + g, + target=ax, + layout=self.layout_small_ring, + edge_curved=[0] * 4 + [0.3], + edge_loop_size=[0] * 5 + [30, 50, 40], + ) + + @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_edit_children(self): + plt.close("all") + g = Graph.Ring(5) + fig, ax = plt.subplots() + plot(g, target=ax, vertex_shape="o", layout=self.layout_small_ring) + graph_artist = ax.get_children()[0] + + dots = graph_artist.get_vertices() + dots.set_facecolors(["blue"] + list(dots.get_facecolors()[1:])) + dots.set_sizes([20] + list(dots.get_sizes()[1:])) + + lines = graph_artist.get_edges() + lines.set_edgecolor("green") + + @image_comparison(baseline_images=["graph_basic"], remove_text=True) + def test_gh_587(self): + plt.close("all") + g = Graph.Ring(5) + with overridden_configuration("plotting.backend", "matplotlib"): + plot(g, target="graph_basic.png", layout=self.layout_small_ring) + os.unlink("graph_basic.png") + + @image_comparison(baseline_images=["graph_with_curved_edges"]) + def test_graph_with_curved_edges(self): + plt.close("all") + g = Graph.Ring(24, directed=True, mutual=True) + fig, ax = plt.subplots() + lo = g.layout("circle") + lo.scale(3) + plot( + g, + target=ax, + layout=lo, + vertex_size=15, + edge_arrow_size=5, + edge_arrow_width=5, + ) + ax.set_aspect(1.0) + + @image_comparison(baseline_images=["multigraph_with_curved_edges_undirected"]) + def test_graph_with_curved_edges_undirected(self): + plt.close("all") + g = Graph.Ring(24, directed=False) + g.add_edges([(0, 1), (1, 2)]) + fig, ax = plt.subplots() + lo = g.layout("circle") + lo.scale(3) + plot( + g, + target=ax, + layout=lo, + vertex_size=15, + edge_arrow_size=5, + edge_arrow_width=5, + ) + ax.set_aspect(1.0) + + @image_comparison(baseline_images=["graph_null"]) + def test_null_graph(self): + plt.close("all") + g = Graph() + fig, ax = plt.subplots() + plot(g, target=ax) + ax.set_aspect(1.0) + + +class ClusteringTestRunner(unittest.TestCase): + def setUp(self): + if mpl is None or plt is None: + raise unittest.SkipTest("matplotlib not found, skipping tests") + + @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, + vertex_size=17, + edge_arrow_size=5, + edge_arrow_width=5, + layout=self.layout_large_ring, + target=ax, + mark_groups=True, + ) + + +def suite(): + graph = unittest.defaultTestLoader.loadTestsFromTestCase(GraphTestRunner) + clustering = unittest.defaultTestLoader.loadTestsFromTestCase(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..f32ad3f73 --- /dev/null +++ b/tests/drawing/matplotlib/utils.py @@ -0,0 +1,119 @@ +# Functions adapted from matplotlib.testing. Credit for the original functions +# goes to the amazing folks over at matplotlib. +import sys +import inspect +import functools + +try: + import matplotlib + from matplotlib.testing.decorators import _collect_new_figures, _ImageComparisonBase +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. + """ + + 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() + + with _collect_new_figures() as figs: + func(*args, **kwargs) + + assert len(figs) == len( + baseline_images + ), "Test generated {} images but there are {} baseline images".format( + len(figs), len(baseline_images) + ) + for fig, baseline in zip(figs, baseline_images): + img.compare(fig, 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=4.0, + 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 = {} # 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..281eeffd6 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/clustering_directed.json @@ -0,0 +1,1055 @@ +{ + "data": [ + { + "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 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + } + ], + "layout": { + "shapes": [ + { + "fillcolor": "rgba(255,0,0,63)", + "line": { + "color": "rgba(255,0,0,255)" + }, + "path": "M 17.102240893427787,-12.23969476826467L 17.102240893427783,-12.239694768264666C 12.645988843102797,-18.461358578089076 0.8955252694779539,-22.367474687721593 -6.398686253821905,-20.051926987529704L -6.398686253821911,-20.0519269875297C -13.69289777712177,-17.73637928733781 -21.038927892627104,-7.768080596031269 -21.09074648483258,-0.11532960491661903L -21.09074648483258,-0.11532960491661903C -21.142565077038054,7.537421386198031 -13.932197797175824,17.60428489706909 -6.670011925108119,20.018397416825497L -6.670011925108115,20.018397416825497C 0.5921739469595897,22.432509936581905 12.394456112789907,18.685875055099775 16.93455240655252,12.525127653861238L 16.934552406552527,12.525127653861228C 21.47464870031514,6.36438025262269 21.558492943752768,-6.018030958440259 17.102240893427787,-12.23969476826467 Z", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.1950972580065446,1.0184451153265959", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.9649307455828062,0.6328259400443561", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.9566534831822209,-0.5895891303732176", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.2084901499044931,-0.9594640169691344", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "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.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.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "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.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.0, + "#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.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.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.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.0, + "#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 + } + } +} \ No newline at end of file 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..7213d1ba4 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/clustering_directed_large.json @@ -0,0 +1,2990 @@ +{ + "data": [ + { + "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, + 0.044706825614332625, + -0.044706825614332625, + 0.0 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 20.0, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.5 + ], + "y": [ + 0.0 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.4802867532861947 + ], + "y": [ + -0.3133330839107595 + ] + } + ], + "layout": { + "shapes": [ + { + "fillcolor": "rgba(0,0,255,63)", + "line": { + "color": "rgba(0,0,255,255)" + }, + "path": "M 8.413141377617196,-25.896264101676145L 8.155152177765178,-25.9805285854218C 6.833557178763122,-26.41218820673784 4.128701954952482,-26.983613777141827 2.745441730143898,-27.12337972622978L 2.4846737173375413,-27.149727978097353C 0.9710294861257793,-27.302668053119092 -2.0505946108051303,-27.254346482977223 -3.5585744765242784,-27.053084837813618L -3.7543123520242156,-27.02696079747362C -5.360161155493332,-26.81263713214002 -8.472329653542033,-26.001919309518726 -9.97864934812162,-25.405525152231043L -10.117156398738466,-25.35068633147587C -11.623476093318052,-24.754292174188183 -14.447123830844985,-23.214854094068826 -15.764451873792332,-22.27181017123716L -15.925021780439813,-22.156861980872392C -17.16206487006342,-21.271292153223108 -19.397812431581155,-19.238073031125833 -20.396516903475288,-18.090423736677845L -20.568571984283913,-17.892708699193616C -21.48124891577373,-16.84391692348774 -23.06183579517491,-14.575764794341183 -23.729745743086276,-13.356404440900498L -23.860128767511288,-13.118372515724491C -24.462847203210146,-12.018028124871812 -25.445040714149187,-9.721846805970955 -25.824515789389366,-8.526009877922782L -25.899262810803666,-8.290460155948939C -26.241364375336698,-7.212398088887686 -26.734295576952675,-5.013398145996662 -26.885125214035625,-3.8924602701668927L -26.913102784791924,-3.6845361556429297C -27.04994363649672,-2.667560337075141 -27.16404214849462,-0.6246956257750191 -27.141299808787714,0.4011932669573146L -27.137495282725656,0.5728124178221539C -27.11665520604978,1.5128917351220679 -26.94259526292006,3.380714068636491 -26.789375396466216,4.308457084851L -26.76688888882022,4.444612407985373C -26.62491227618937,5.304277762632695 -26.230646745122016,6.997928837870707 -25.978357826685507,7.831914558461397L -25.94644875166113,7.937395659623136C -25.71011437073681,8.718640829632957 -25.144765028541553,10.246844573146316 -24.815750067270613,10.993803146649851L -24.780296766639943,11.074292334019272C -24.46900845568434,11.781006313838098 -23.767936083377943,13.154363712079272 -23.378152022027148,13.821007130501622L -23.342631483495033,13.88175751918912C -22.970607691410294,14.51802574326772 -22.15969179189009,15.746419522342974 -21.72079968445462,16.338545077339628L -21.687138017026985,16.38395927634213C -21.265076743305336,16.953377731837534 -20.363853048610867,18.04507625641636 -19.88469062763805,18.56735632549978L -19.85386256512327,18.60095846522359C -19.390114175407845,19.106437464445104 -18.413939099670152,20.067976195576676 -17.901512413647882,20.52403592748673L -17.873928924555894,20.548585231774698C -17.37529398307962,20.992370311540768 -16.33679664947991,21.82875009256347 -15.796934257356478,22.22134479382011L -15.796934257356478,22.221344793820105C -15.257071865233044,22.613939495076743 -14.096565733835174,22.760803956469676 -13.47592199456074,22.51507371660598L 25.236152912086663,7.187878982388844C 25.856796651361094,6.942148742525145 26.60223357107002,6.040668044025645 26.727026751504514,5.384917585389842L 26.727026751504514,5.384917585389842C 26.851819931939005,4.72916712675404 27.03631054923762,3.4085721615485616 27.09600798610174,2.7437276549788856L 27.099310329081725,2.7069497846985935C 27.16065893743584,2.023716342988772 27.21404138688488,0.6545469509673095 27.206075227979806,-0.03138899934433126L 27.20554567089632,-0.07698716599310718C 27.197314733449506,-0.7857221996291359 27.10695946762353,-2.1984662466468032 27.02483513924437,-2.9024752600284422L 27.01828528025058,-2.958623783359041C 26.932886022374525,-3.690707058405979 26.683124866195204,-5.141277717639568 26.518762967891934,-5.85976510182622L 26.503069823350884,-5.928365711136275C 26.33086135277709,-6.681153399977953 25.90179328536861,-8.162204921041777 25.644933688533918,-8.89046875326392L 25.615679392159933,-8.973412300045215C 25.34419264713825,-9.743147905658006 24.710187131240467,-11.244168747025656 24.347668360364366,-11.975453982780515L 24.298722292456734,-12.07418966127408C 23.91173048762682,-12.854842736275721 23.039753748641374,-14.359354564182366 22.55476881448584,-15.08321331708737L 22.477956040691623,-15.19785934984027C 21.95456471963898,-15.979041119121725 20.80283658467503,-17.459777232178354 20.174499770763724,-18.159331575953534L 20.059791757858704,-18.28704093010392C 19.37410093749489,-19.050449950954288 17.892489575888607,-20.461528429379452 17.09656903464614,-21.10919788695424L 16.933840412443192,-21.241616078234436C 16.056555560099252,-21.955494631449323 14.191905131800675,-23.221061974030174 13.20453955584604,-23.77275076339614L 12.9888064417623,-23.89329126372697C 11.893574308765796,-24.50525030325835 9.605741776693243,-25.506736722232937 8.413141377617196,-25.896264101676145 Z", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 2.5,0.0 L 2.385279912777263,0.31333308391076065", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 2.4802867532861947,0.31333308391076065 L 2.3264510623126458,0.621724717912137", + "type": "path" + }, + { + "line": { + "color": "rgb(204,204,204)", + "width": 2.0 + }, + "path": "M 2.4214579028215777,0.621724717912137 L 2.229434374211696,0.9203113817116949", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 2.324441214720628,0.9203113817116949 L 2.095759859600727,1.2043841852542883", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 2.190766700109659,1.2043841852542883 L 1.9275356454284365,1.469463130731183", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 2.0225424859373686,1.469463130731183 L 1.7274147280445968,1.7113677648217218", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 1.822421568553529,1.7113677648217218 L 1.498553133862792,1.926283106939473", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 1.5935599743717241,1.926283106939473 L 1.2445601469385592,2.110819813755038", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 1.3395669874474914,2.110819813755038 L 0.9694413884037497,2.262067631165049", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 1.0644482289126818,2.262067631165049 L 0.6775356454284366,2.3776412907378837", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 0.7725424859373686,2.3776412907378837 L 0.37344644595537924,2.4557181268217216", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 0.4684532864643113,2.4557181268217216 L 0.0619694583143512,2.495066821070679", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 0.15697629882328326,2.495066821070679 L -0.25198313933221556,2.495066821070679", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -0.1569762988232835,2.495066821070679 L -0.5634601269732441,2.4557181268217216", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -0.46845328646431206,2.4557181268217216 L -0.8675493264463009,2.3776412907378837", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -0.7725424859373689,2.3776412907378837 L -1.159455069421614,2.2620676311650487", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -1.0644482289126818,2.2620676311650487 L -1.4345738279564244,2.1108198137550374", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -1.3395669874474923,2.1108198137550374 L -1.6885668148806565,1.926283106939473", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -1.5935599743717244,1.926283106939473 L -1.9174284090624614,1.7113677648217211", + "type": "path" + }, + { + "line": { + "color": "rgb(204,204,204)", + "width": 2.0 + }, + "path": "M -1.8224215685535292,1.7113677648217211 L -2.1175493264463,1.4694631307311832", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.022542485937368,1.4694631307311832 L -2.285773540618591,1.204384185254288", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.190766700109659,1.204384185254288 L -2.4194480552295605,0.9203113817116944", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.3244412147206286,0.9203113817116944 L -2.5164647433305096,0.621724717912137", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.4214579028215777,0.621724717912137 L -2.5752935937951267,0.3133330839107602", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.4802867532861947,0.3133330839107602 L -2.595006840508932,-8.049116928532385e-16", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.5,-8.040613248383183e-16 L -2.5752935937951267,-0.3133330839107607", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.4802867532861947,-0.3133330839107607 L -2.5164647433305096,-0.6217247179121376", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.4214579028215777,-0.6217247179121376 L -2.41944805522956,-0.9203113817116958", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.324441214720628,-0.9203113817116958 L -2.2857735406185906,-1.2043841852542885", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.1907667001096587,-1.2043841852542885 L -2.1175493264463,-1.4694631307311834", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -2.022542485937368,-1.4694631307311834 L -1.9174284090624611,-1.7113677648217218", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -1.822421568553529,-1.7113677648217218 L -1.6885668148806559,-1.9262831069394735", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -1.5935599743717237,-1.9262831069394735 L -1.434573827956423,-2.1108198137550382", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -1.339566987447491,-2.1108198137550382 L -1.1594550694216126,-2.2620676311650496", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -1.0644482289126804,-2.2620676311650496 L -0.8675493264463009,-2.3776412907378837", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -0.7725424859373689,-2.3776412907378837 L -0.5634601269732437,-2.4557181268217216", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -0.46845328646431156,-2.4557181268217216 L -0.25198313933221506,-2.495066821070679", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M -0.156976298823283,-2.495066821070679 L 0.06196945831435223,-2.495066821070679", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 0.1569762988232843,-2.495066821070679 L 0.3734464459553808,-2.4557181268217216", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 0.46845328646431283,-2.4557181268217216 L 0.677535645428436,-2.377641290737884", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 0.7725424859373681,-2.377641290737884 L 0.9694413884037495,-2.262067631165049", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 1.0644482289126815,-2.262067631165049 L 1.2445601469385597,-2.1108198137550374", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 1.3395669874474918,-2.1108198137550374 L 1.4985531338627929,-1.9262831069394726", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 1.593559974371725,-1.9262831069394726 L 1.7274147280445975,-1.7113677648217207", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 1.8224215685535297,-1.7113677648217207 L 1.9275356454284374,-1.4694631307311814", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 2.0225424859373695,-1.4694631307311814 L 2.095759859600727,-1.2043841852542883", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 2.190766700109659,-1.2043841852542883 L 2.2294343742116967,-0.9203113817116947", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 2.3244412147206286,-0.9203113817116947 L 2.326451062312646,-0.6217247179121362", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2.0 + }, + "path": "M 2.421457902821578,-0.6217247179121362 L 2.385279912777263,-0.3133330839107595", + "type": "path" + }, + { + "line": { + "color": "rgb(204,204,204)", + "width": 2.0 + }, + "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.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.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "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.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.0, + "#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.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.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.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.0, + "#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 + } + } +} \ No newline at end of file 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..e3c6c7cf3 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_basic.json @@ -0,0 +1,967 @@ +{ + "data": [ + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + } + ], + "layout": { + "shapes": [ + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.29010409851547664,1.0184451153265959", + "type": "path" + }, + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.8699239050738742,0.6328259400443561", + "type": "path" + }, + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.8616466426732888,-0.5895891303732176", + "type": "path" + }, + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.30349699041342515,-0.9594640169691343", + "type": "path" + }, + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "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.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.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "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.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.0, + "#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.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.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.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.0, + "#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 + } + } +} \ No newline at end of file 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..4f25d0ecb --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_directed.json @@ -0,0 +1,1047 @@ +{ + "data": [ + { + "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 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + } + ], + "layout": { + "shapes": [ + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.1950972580065446,1.0184451153265959", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.9649307455828062,0.6328259400443561", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.9566534831822209,-0.5895891303732176", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.2084901499044931,-0.9594640169691344", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "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.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.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "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.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.0, + "#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.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.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.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.0, + "#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 + } + } +} \ No newline at end of file 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..e3c6c7cf3 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_edit_children.json @@ -0,0 +1,967 @@ +{ + "data": [ + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + } + ], + "layout": { + "shapes": [ + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.29010409851547664,1.0184451153265959", + "type": "path" + }, + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.8699239050738742,0.6328259400443561", + "type": "path" + }, + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.8616466426732888,-0.5895891303732176", + "type": "path" + }, + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.30349699041342515,-0.9594640169691343", + "type": "path" + }, + { + "layer": "below", + "line": { + "color": "#444", + "width": 2.0 + }, + "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.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.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "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.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.0, + "#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.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.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.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.0, + "#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 + } + } +} \ No newline at end of file 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..4f25d0ecb --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_mark_groups_directed.json @@ -0,0 +1,1047 @@ +{ + "data": [ + { + "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 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "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": 20.0, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + } + ], + "layout": { + "shapes": [ + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.1950972580065446,1.0184451153265959", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.9649307455828062,0.6328259400443561", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.9566534831822209,-0.5895891303732176", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.2084901499044931,-0.9594640169691344", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "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.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.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "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.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.0, + "#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.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.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.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.0, + "#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 + } + } +} \ No newline at end of file diff --git a/tests/drawing/plotly/baseline_images/graph_mark_groups_squares_directed.json b/tests/drawing/plotly/baseline_images/graph_mark_groups_squares_directed.json new file mode 100644 index 000000000..b5cd608e9 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_mark_groups_squares_directed.json @@ -0,0 +1,1047 @@ +{ + "data": [ + { + "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 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 20.0, + "symbol": "square" + }, + "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": 20.0, + "symbol": "square" + }, + "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": 20.0, + "symbol": "square" + }, + "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": 20.0, + "symbol": "square" + }, + "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": 20.0, + "symbol": "square" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + } + ], + "layout": { + "shapes": [ + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.1950972580065446,1.0184451153265959", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.9649307455828062,0.6328259400443561", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.9566534831822209,-0.5895891303732176", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.2084901499044931,-0.9594640169691344", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2.0 + }, + "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.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.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "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.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.0, + "#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.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.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.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.0, + "#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 + } + } +} \ No newline at end of file diff --git a/tests/drawing/plotly/baseline_images/graph_null.json b/tests/drawing/plotly/baseline_images/graph_null.json new file mode 100644 index 000000000..6d7df64d9 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_null.json @@ -0,0 +1,829 @@ +{ + "data": [], + "layout": { + "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.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.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.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.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "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.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.0, + "#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.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.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.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.0, + "#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 + } + } +} \ No newline at end of file diff --git a/tests/drawing/plotly/test_graph.py b/tests/drawing/plotly/test_graph.py new file mode 100644 index 000000000..98b89b0c3 --- /dev/null +++ b/tests/drawing/plotly/test_graph.py @@ -0,0 +1,199 @@ +import unittest + + +from igraph import Graph, 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: + plotly = None + +if plotly is not None: + from plotly import graph_objects as go + +image_comparison = find_image_comparison() + + +class GraphTestRunner(unittest.TestCase): + def setUp(self): + if plotly is None: + raise unittest.SkipTest("plotly not found, skipping tests") + + @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_graph_edit_children(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 + + @image_comparison(baseline_images=["graph_null"]) + def test_null_graph(self): + g = Graph() + fig = go.Figure() + plot(g, target=fig) + return fig + + +class ClusteringTestRunner(unittest.TestCase): + def setUp(self): + if plotly is None: + raise unittest.SkipTest("plotly not found, skipping tests") + + @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.defaultTestLoader.loadTestsFromTestCase(GraphTestRunner) + clustering = unittest.defaultTestLoader.loadTestsFromTestCase(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..0569442f5 --- /dev/null +++ b/tests/drawing/plotly/utils.py @@ -0,0 +1,275 @@ +# 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 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 "shapes" not in baseline["layout"]: + baseline["layout"]["shapes"] = [] + if "shapes" not in figd["layout"]: + figd["layout"]["shapes"] = [] + 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. + """ + + 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" + + 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_atlas.py b/tests/test_atlas.py new file mode 100644 index 000000000..49f49531e --- /dev/null +++ b/tests/test_atlas.py @@ -0,0 +1,202 @@ +import warnings +import unittest + +from igraph import Graph + + +class AtlasTestBase: + def testPageRank(self): + for idx, g in enumerate(self.__class__.graphs): + try: + pr = g.pagerank() + except Exception as 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), + ) + + def testEigenvectorCentrality(self): + # Temporarily turn off the warning handler because g.evcent() will print + # a warning for DAGs + warnings.simplefilter("ignore") + + try: + for idx, g in enumerate(self.__class__.graphs): + 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), + ) + raise + + if g.vcount() == 0: + self.assertEqual([], ec) + continue + + if not g.is_connected(): + # Skip disconnected graphs; this will be fixed in igraph 0.7 + continue + + 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, + ) + 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, + ) + + 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), + ) + finally: + # Reset the warning handler + warnings.resetwarnings() + + def testHubScore(self): + for idx, g in enumerate(self.__class__.graphs): + try: + if g.ecount() == 0: + with self.assertWarns(RuntimeWarning, msg="The graph has no edges"): + sc = g.hub_score() + elif not g.is_directed(): + with self.assertWarns(RuntimeWarning, msg="Hub and authority scores requested for undirected graph"): + sc = g.hub_score() + else: + 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 + + 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): + try: + if g.ecount() == 0: + with self.assertWarns(RuntimeWarning, msg="The graph has no edges"): + sc = g.authority_score() + elif not g.is_directed(): + with self.assertWarns(RuntimeWarning, msg="Hub and authority scores requested for undirected graph"): + sc = g.authority_score() + else: + 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 + + 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 some problematic graphs +GraphAtlasTests.graphs = [ + g for idx, g in enumerate(GraphAtlasTests.graphs) if idx not in {70, 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) + ] + + +# Skip some problematic graphs +IsoclassTests.graphs = [ + g for idx, g in enumerate(IsoclassTests.graphs) if idx not in {136} +] + + +def suite(): + atlas_suite = unittest.defaultTestLoader.loadTestsFromTestCase(GraphAtlasTests) + isoclass_suite = unittest.defaultTestLoader.loadTestsFromTestCase(IsoclassTests) + return unittest.TestSuite([atlas_suite, isoclass_suite]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/igraph/test/attributes.py b/tests/test_attributes.py similarity index 71% rename from igraph/test/attributes.py rename to tests/test_attributes.py index 799869c57..1299cada9 100644 --- a/igraph/test/attributes.py +++ b/tests/test_attributes.py @@ -1,6 +1,8 @@ # vim:ts=4 sw=4 sts=4: import unittest -from igraph import * + +from igraph import Graph + class AttributeTests(unittest.TestCase): def testGraphAttributes(self): @@ -42,25 +44,25 @@ def testEdgeAttributes(self): def testMassVertexAttributeAssignment(self): g = Graph.Full(5) - g.vs.set_attribute_values("name", range(5)) - self.assertTrue(g.vs.get_attribute_values("name") == range(5)) - g.vs["name"] = range(5,10) - self.assertTrue(g.vs["name"] == 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", 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.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", range(10)) - self.assertTrue(g.es.get_attribute_values("name") == range(10)) - g.es["name"] = range(10,20) - self.assertTrue(g.es["name"] == 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", 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.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") @@ -70,10 +72,47 @@ 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, ["bar", "thud", 0]) + + def testVertexNameIndexingBytes(self): + g = Graph.Famous("bull") + g.vs["name"] = [b"foo", b"bar", b"baz", b"fred", b"thud"] + self.assertTrue(g.degree(b"bar") == 3) + self.assertTrue(g.degree([b"bar", b"fred", 0]) == [3, 1, 2]) + g.vs[2]["name"] = b"quack" + self.assertRaises(ValueError, g.degree, b"baz") + self.assertTrue(g.degree(b"quack") == 3) del g.vs["name"] - self.assertRaises(ValueError, g.degree, [u"bar", u"thud", 0]) + self.assertRaises(ValueError, g.degree, [b"bar", b"thud", 0]) + + def testUnhashableVertexNames(self): + g = Graph.Famous("bull") + g.vs["name"] = [str(x) for x in range(4)] + + value = "this is not hashable".split() + g.vs[2]["name"] = value + + # Trigger an indexing by doing a lookup by name + try: + g.vs.find("3") + err = None + except Exception as ex: + err = ex + + # Check the exception + self.assertTrue(isinstance(err, RuntimeError)) + self.assertTrue(repr(value) in str(err)) + + def testVertexNameIndexingBug196(self): + g = Graph() + a, b = b"a", b"b" + g.add_vertices([a, b]) + g.add_edges([(a, b)]) + self.assertEqual(g.ecount(), 1) + self.assertTrue(g.are_adjacent(a, b)) def testInvalidAttributeNames(self): g = Graph.Famous("bull") @@ -87,9 +126,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] @@ -110,7 +150,7 @@ def testCombinationRandom(self): g = self.g g.simplify(combine_edges="random") del g.es["weight2"] - for i in xrange(100): + for _i in range(100): self.assertTrue(g.es[0]["weight"] in (1, 2)) self.assertTrue(g.es[1]["weight"] == 3) self.assertTrue(g.es[2]["weight"] in (4, 5, 6)) @@ -140,13 +180,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] @@ -164,7 +197,7 @@ def testCombinationLast(self): def testCombinationConcat(self): g = self.g g.es["name"] = list("ABCDEFG") - g.simplify(combine_edges=dict(name="concat")) + g.simplify(combine_edges={"name": "concat"}) self.assertFalse("weight" in g.edge_attributes()) self.assertFalse("weight2" in g.edge_attributes()) self.assertTrue(g.es["name"] == ["AB", "C", "DEF"]) @@ -188,8 +221,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}) @@ -228,20 +261,31 @@ 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]) + attribute_suite = unittest.defaultTestLoader.loadTestsFromTestCase(AttributeTests) + attribute_combination_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + AttributeCombinationTests + ) + unicode_attributes_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + UnicodeAttributeTests + ) + 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 new file mode 100644 index 000000000..35d511b1b --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,1020 @@ +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, + is_degree_sequence, + is_graphical, + is_graphical_degree_sequence, + Matrix, + Vertex, + VertexSeq, +) +from igraph._igraph import EdgeSeq as _EdgeSeq, VertexSeq as _VertexSeq + +from .utils import is_pypy + +try: + import numpy as np +except ImportError: + np = None + + +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()) + + 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 = 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 = 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 = 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 = Graph(8, None) + self.assertEqual(8, g.vcount()) + self.assertEqual(0, g.ecount()) + self.assertFalse(g.is_directed()) + + g = Graph(edges=None) + self.assertEqual(0, g.vcount()) + self.assertEqual(0, g.ecount()) + self.assertFalse(g.is_directed()) + + self.assertRaises(TypeError, Graph, edgelist=[(1, 2)]) + + @unittest.skipIf(np is None, "test case depends on NumPy") + def testGraphCreationWithNumPy(self): + # NumPy array with integers + 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() + ) + + # Sliced NumPy array -- the sliced array is non-contiguous but we + # automatically make it so + 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() + ) + + # 1D NumPy array -- should raise a TypeError because we need a 2D array + arr = np.array([0, 1, 1, 2, 2, 3]) + 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])]) + self.assertRaises(TypeError, Graph, arr) + + # NumPy array with strings -- should be a casting error + arr = np.array([("a", "b"), ("c", "d"), ("e", "f")]) + self.assertRaises(ValueError, Graph, arr) + + def testAddVertex(self): + g = Graph() + + vertex = g.add_vertex() + self.assertTrue(g.vcount() == 1 and g.ecount() == 0) + self.assertEqual(0, vertex.index) + self.assertFalse("name" in g.vertex_attributes()) + + vertex = g.add_vertex("foo") + self.assertTrue(g.vcount() == 2 and g.ecount() == 0) + self.assertEqual(1, vertex.index) + self.assertTrue("name" in g.vertex_attributes()) + self.assertEqual(g.vs["name"], [None, "foo"]) + + vertex = g.add_vertex("3") + self.assertTrue(g.vcount() == 3 and g.ecount() == 0) + self.assertEqual(2, vertex.index) + self.assertTrue("name" in g.vertex_attributes()) + self.assertEqual(g.vs["name"], [None, "foo", "3"]) + + vertex = g.add_vertex(name="bar") + self.assertTrue(g.vcount() == 4 and g.ecount() == 0) + self.assertEqual(3, vertex.index) + self.assertTrue("name" in g.vertex_attributes()) + self.assertEqual(g.vs["name"], [None, "foo", "3", "bar"]) + + 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(g.vs["spam"], [None] * 4 + ["cheese"]) + self.assertEqual(g.vs["ham"], [None] * 4 + [42]) + + with self.assertWarns(DeprecationWarning, msg="integers as vertex names"): + g.add_vertex(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()) + self.assertEqual(6, g.ecount()) + + # Delete a single vertex + g.delete_vertices(4) + self.assertEqual(5, g.vcount()) + self.assertEqual(4, g.ecount()) + + # Delete multiple vertices + g.delete_vertices([1, 3]) + self.assertEqual(3, g.vcount()) + self.assertEqual(1, g.ecount()) + + # Delete a vertex sequence + g.delete_vertices(g.vs[:2]) + self.assertEqual(1, g.vcount()) + self.assertEqual(0, g.ecount()) + + # Delete a single vertex object + g.vs[0].delete() + self.assertEqual(0, g.vcount()) + self.assertEqual(0, g.ecount()) + + # Delete vertices by name + g = Graph.Full(4) + g.vs["name"] = ["spam", "bacon", "eggs", "ham"] + self.assertEqual(4, g.vcount()) + g.delete_vertices("spam") + self.assertEqual(3, g.vcount()) + g.delete_vertices(["bacon", "ham"]) + self.assertEqual(1, g.vcount()) + + # Deleting a nonexistent vertex + 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"]) + + edge = g.add_edge(0, 1) + self.assertEqual(g.vcount(), 4) + self.assertEqual(g.get_edgelist(), [(0, 1)]) + self.assertEqual(0, edge.index) + self.assertEqual((0, 1), edge.tuple) + + edge = g.add_edge(1, 2, foo="bar") + self.assertEqual(g.vcount(), 4) + self.assertEqual(g.get_edgelist(), [(0, 1), (1, 2)]) + self.assertEqual(1, edge.index) + self.assertEqual((1, 2), edge.tuple) + self.assertEqual("bar", edge["foo"]) + self.assertEqual([None, "bar"], g.es["foo"]) + + def testAddEdges(self): + g = Graph() + g.add_vertices(["spam", "bacon", "eggs", "ham"]) + + g.add_edges([(0, 1)]) + self.assertEqual(g.vcount(), 4) + self.assertEqual(g.get_edgelist(), [(0, 1)]) + + g.add_edges([(1, 2), (2, 3), (1, 3)]) + self.assertEqual(g.vcount(), 4) + self.assertEqual(g.get_edgelist(), [(0, 1), (1, 2), (2, 3), (1, 3)]) + + 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"]) + + def testDeleteEdges(self): + g = Graph.Famous("petersen") + g.vs["name"] = list("ABCDEFGHIJ") + el = g.get_edgelist() + + self.assertEqual(15, g.ecount()) + + # Deleting single edge + g.delete_edges(14) + el[14:] = [] + self.assertEqual(14, g.ecount()) + self.assertEqual(el, g.get_edgelist()) + + # Deleting multiple edges + g.delete_edges([2, 5, 7]) + el[7:8] = [] + el[5:6] = [] + el[2:3] = [] + self.assertEqual(11, g.ecount()) + self.assertEqual(el, g.get_edgelist()) + + # Deleting edge object + g.es[6].delete() + el[6:7] = [] + self.assertEqual(10, g.ecount()) + self.assertEqual(el, g.get_edgelist()) + + # Deleting edge sequence object + g.es[1:4].delete() + el[1:4] = [] + self.assertEqual(7, g.ecount()) + self.assertEqual(el, g.get_edgelist()) + + # Deleting edges by IDs + g.delete_edges([(2, 7), (5, 8)]) + el[4:5] = [] + el[1:2] = [] + self.assertEqual(5, g.ecount()) + self.assertEqual(el, g.get_edgelist()) + + # Deleting edges by names + g.delete_edges([("D", "I"), ("G", "I")]) + el[3:4] = [] + el[1:2] = [] + self.assertEqual(3, g.ecount()) + self.assertEqual(el, g.get_edgelist()) + + # 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)]) + + # 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") + edges_to_ids = {v: k for k, v in enumerate(g.get_edgelist())} + for (source, target), edge_id in 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)) + + self.assertRaises(InternalError, g.get_eid, 0, 11) + self.assertRaises(ValueError, g.get_eid, "A", "K") + + def testGraphGetEids(self): + g = Graph.Famous("petersen") + eids = g.get_eids(pairs=[(0, 1), (0, 5), (1, 6), (4, 9), (8, 6)]) + self.assertTrue(eids == [0, 2, 4, 9, 12]) + eids = g.get_eids(pairs=[(7, 9), (9, 6)]) + self.assertTrue(eids == [14, 13]) + self.assertRaises(InternalError, g.get_eids, pairs=[(0, 1), (0, 2)]) + self.assertRaises(TypeError, g.get_eids, pairs=None) + + def testAdjacency(self): + g = Graph(4, [(0, 1), (1, 2), (2, 0), (2, 3)], directed=True) + self.assertTrue(g.neighbors(2) == [0, 1, 3]) + self.assertTrue(g.predecessors(2) == [1]) + self.assertTrue(g.successors(2) == [0, 3]) + self.assertTrue(g.get_adjlist() == [[1], [2], [0, 3], []]) + 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 testAdjacencyWithLoopsAndMultiEdges(self): + g = Graph(4, [(0, 0), (0, 1), (1, 2), (2, 0), (2, 3), (2, 3), (3, 3), (3, 3)], directed=True) + + self.assertTrue(g.neighbors(2) == [0, 1, 3, 3]) + self.assertTrue(g.predecessors(2) == [1]) + self.assertTrue(g.successors(2) == [0, 3, 3]) + + self.assertTrue(g.get_adjlist() == [[0, 1], [2], [0, 3, 3], [3, 3]]) + self.assertTrue(g.get_adjlist(IN) == [[0, 2], [0], [1], [2, 2, 3, 3]]) + self.assertTrue(g.get_adjlist(ALL) == [[0, 0, 1, 2], [0, 2], [0, 1, 3, 3], [2, 2, 3, 3, 3, 3]]) + + self.assertTrue(g.get_adjlist(ALL, loops="once") == [[0, 1, 2], [0, 2], [0, 1, 3, 3], [2, 2, 3, 3]]) + self.assertTrue(g.get_adjlist(ALL, loops="once", multiple=False) == [[0, 1, 2], [0, 2], [0, 1, 3], [2, 3]]) + + 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, 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], [0, 1], [2, 1, 3], [3]]) + + def testEdgeIncidenceWithLoopsAndMultiEdges(self): + g = Graph(4, [(0, 1), (1, 2), (2, 0), (2, 3), (2, 3), (0, 0), (3, 3), (3, 3)], directed=True) + + self.assertTrue(g.incident(2) == [2, 4, 3]) + self.assertTrue(g.incident(2, IN) == [1]) + self.assertTrue(g.incident(2, ALL) == [2, 1, 4, 3]) + + self.assertTrue(g.get_inclist() == [[5, 0], [1], [2, 4, 3], [7, 6]]) + self.assertTrue(g.get_inclist(IN) == [[5, 2], [0], [1], [4, 3, 7, 6]]) + self.assertTrue(g.get_inclist(ALL) == [[5, 5, 0, 2], [0, 1], [2, 1, 4, 3], [4, 3, 7, 7, 6, 6]]) + + self.assertTrue(g.get_inclist(ALL, loops="once") == [[5, 0, 2], [0, 1], [2, 1, 4, 3], [4, 3, 7, 6]]) + + def testMultiplesLoops(self): + g = Graph.Tree(7, 2) + + # has_multiple + self.assertFalse(g.has_multiple()) + + g.add_vertices(1) + 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(list(range(6, 8))) == [False, True]) + + # is_multiple + 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(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) + self.assertTrue(g.is_multiple() == [False, False]) + + def testPickling(self): + import pickle + + g = Graph([(0, 1), (1, 2)]) + g["data"] = "abcdef" + g.vs["data"] = [3, 4, 5] + g.es["data"] = ["A", "B"] + g.custom_data = None + pickled = pickle.dumps(g) + + g2 = pickle.loads(pickled) + self.assertTrue(g["data"] == g2["data"]) + self.assertTrue(g.vs["data"] == g2.vs["data"]) + self.assertTrue(g.es["data"] == g2.es["data"]) + self.assertTrue(g.vcount() == g2.vcount()) + self.assertTrue(g.ecount() == g2.ecount()) + self.assertTrue(g.is_directed() == g2.is_directed()) + self.assertTrue(g2.custom_data == g.custom_data) + + def testHashing(self): + g = Graph([(0, 1), (1, 2)]) + self.assertRaises(TypeError, hash, g) + + def testIteration(self): + g = Graph() + self.assertRaises(TypeError, iter, g) + + +class DatatypeTests(unittest.TestCase): + def testMatrix(self): + m = Matrix([[1, 2, 3], [4, 5], [6, 7, 8]]) + self.assertTrue(m.shape == (3, 3)) + + # Reading data + self.assertTrue(m.data == [[1, 2, 3], [4, 5, 0], [6, 7, 8]]) + self.assertTrue(m[1, 1] == 5) + self.assertTrue(m[0] == [1, 2, 3]) + self.assertTrue(m[0, :] == [1, 2, 3]) + self.assertTrue(m[:, 0] == [1, 4, 6]) + self.assertTrue(m[2, 0:2] == [6, 7]) + self.assertTrue(m[:, :].data == [[1, 2, 3], [4, 5, 0], [6, 7, 8]]) + self.assertTrue(m[:, 1:3].data == [[2, 3], [5, 0], [7, 8]]) + + # Writing data + m[1, 1] = 10 + self.assertTrue(m[1, 1] == 10) + m[1] = (6, 5, 4) + self.assertTrue(m[1] == [6, 5, 4]) + m[1:3] = [[4, 5, 6], (7, 8, 9)] + self.assertTrue(m[1:3].data == [[4, 5, 6], [7, 8, 9]]) + + # Minimums and maximums + self.assertTrue(m.min() == 1) + self.assertTrue(m.max() == 9) + self.assertTrue(m.min(0) == [1, 2, 3]) + self.assertTrue(m.max(0) == [7, 8, 9]) + self.assertTrue(m.min(1) == [1, 4, 7]) + self.assertTrue(m.max(1) == [3, 6, 9]) + + # Special constructors + m = Matrix.Fill(2, (3, 3)) + self.assertTrue(m.min() == 2 and m.max() == 2 and m.shape == (3, 3)) + m = Matrix.Zero(5, 4) + self.assertTrue(m.min() == 0 and m.max() == 0 and m.shape == (5, 4)) + m = Matrix.Identity(3) + self.assertTrue(m.data == [[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + m = Matrix.Identity(3, 2) + self.assertTrue(m.data == [[1, 0], [0, 1], [0, 0]]) + + # Conversion to string + m = Matrix.Identity(3) + self.assertTrue(str(m) == "[[1, 0, 0]\n [0, 1, 0]\n [0, 0, 1]]") + self.assertTrue(repr(m) == "Matrix([[1, 0, 0], [0, 1, 0], [0, 0, 1]])") + + +class GraphDictListTests(unittest.TestCase): + def setUp(self): + self.vertices = [ + {"name": "Alice", "age": 48, "gender": "F"}, + {"name": "Bob", "age": 33, "gender": "M"}, + {"name": "Cecil", "age": 45, "gender": "F"}, + {"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}, + ] + + def testGraphFromDictList(self): + g = Graph.DictList(self.vertices, self.edges) + self.checkIfOK(g, "name") + g = Graph.DictList(self.vertices, self.edges, iterative=True) + self.checkIfOK(g, "name") + + 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) + self.checkIfOK(g, "name") + + def testGraphFromDictIteratorNoVertices(self): + g = Graph.DictList(None, iter(self.edges)) + self.checkIfOK(g, "name", check_vertex_attrs=False) + g = Graph.DictList(None, iter(self.edges), iterative=True) + self.checkIfOK(g, "name", check_vertex_attrs=False) + + def testGraphFromDictListExtraVertexName(self): + 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.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)]) + + def testGraphFromDictListAlternativeName(self): + for vdata in self.vertices: + vdata["name_alternative"] = vdata["name"] + del vdata["name"] + g = Graph.DictList( + self.vertices, self.edges, vertex_name_attr="name_alternative" + ) + self.checkIfOK(g, "name_alternative") + g = Graph.DictList( + 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.vs[name_attr] == ["Alice", "Bob", "Cecil", "David"]) + if check_vertex_attrs: + self.assertTrue(g.vs["age"] == [48, 33, 45, 34]) + self.assertTrue(g.vs["gender"] == ["F", "M", "F", "M"]) + self.assertTrue(g.es["friendship"] == [4, 5, 5, 2, 1]) + self.assertTrue(g.es["advice"] == [4, 5, 5, 4, 2]) + + +class GraphTupleListTests(unittest.TestCase): + def setUp(self): + self.edges = [ + ("Alice", "Bob", 4, 4), + ("Cecil", "Bob", 5, 5), + ("Cecil", "Alice", 5, 5), + ("David", "Alice", 2, 4), + ("David", "Bob", 1, 2), + ] + + def testGraphFromTupleList(self): + g = Graph.TupleList(self.edges) + self.checkIfOK(g, "name", ()) + + 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",)) + + def testGraphFromTupleListWithDifferentNameAttribute(self): + g = Graph.TupleList(self.edges, vertex_name_attr="spam") + self.checkIfOK(g, "spam", ()) + + def testGraphFromTupleListWithWeights(self): + g = Graph.TupleList(self.edges, weights=True) + self.checkIfOK(g, "name", ("weight",)) + g = Graph.TupleList(self.edges, weights="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", + ) + + def testNoneForMissingAttributes(self): + 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.attributes() == []) + self.assertTrue(g.vertex_attributes() == [name_attr]) + self.assertTrue(g.vs[name_attr] == ["Alice", "Bob", "Cecil", "David"]) + if edge_attrs: + self.assertTrue(sorted(g.edge_attributes()) == sorted(edge_attrs)) + self.assertTrue(g.es[edge_attrs[0]] == [4, 5, 5, 2, 1]) + if len(edge_attrs) > 1: + self.assertTrue(g.es[edge_attrs[1]] == [4, 5, 5, 4, 2]) + if len(edge_attrs) > 2: + self.assertTrue(g.es[edge_attrs[2]] == [None] * 5) + else: + 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 testGraphFromDictDictWithProps(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 + # 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])) + 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 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])) + 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])) + + +class InheritedGraph(Graph): + def __init__(self, *args, **kwds): + super().__init__(*args, **kwds) + self.init_called = True + + def __new__(cls, *args, **kwds): + result = Graph.__new__(cls) + result.new_called = True + return result + + @classmethod + def Adjacency(cls, *args, **kwds): + result = super().Adjacency(*args, **kwds) + result.adjacency_called = True + return result + + +class InheritedGraphWithEarlyMethodCallInInit(Graph): + def __init__(self, *args, **kwds): + self.call_result = self.degree() + super().__init__(*args, **kwds) + self.init_called = True + + +class InheritedGraphWithEarlyMethodCallInNew(Graph): + def __new__(cls, *args, **kwds): + instance = super().__new__(cls, *args, **kwds) + instance.call_result = instance.degree() + return instance + + +class InheritedGraphThatReturnsNonGraphFromNew(Graph): + def __new__(cls, *args, **kwds): + super().__new__(cls, *args, **kwds) + return 42 + + +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 testCallingInstanceMethodTooEarly(self): + # Creating an InheritedGraphWithEarlyMethodCallInInit instance should + # not crash the runtime + g = InheritedGraphWithEarlyMethodCallInInit([(0, 1), (1, 2), (2, 3)]) + self.assertEqual(4, g.vcount()) + self.assertEqual([1, 2, 2, 1], g.degree()) + self.assertEqual([], g.call_result) + + # Creating an InheritedGraphWithEarlyMethodCallInNew instance should + # not crash the runtime either + g = InheritedGraphWithEarlyMethodCallInNew([(0, 1), (1, 2), (2, 3)]) + self.assertEqual(4, g.vcount()) + self.assertEqual([1, 2, 2, 1], g.degree()) + self.assertEqual([], g.call_result) + + def testInheritedGraphThatReturnsSomethingElseFromNew(self): + g = InheritedGraphThatReturnsNonGraphFromNew([(0, 1)], directed=True) + self.assertEqual(42, g) + + +@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) + + +@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): + 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.defaultTestLoader.loadTestsFromTestCase(BasicTests) + datatype_suite = unittest.defaultTestLoader.loadTestsFromTestCase(DatatypeTests) + graph_dict_list_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + GraphDictListTests + ) + graph_tuple_list_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + GraphTupleListTests + ) + graph_list_dict_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + GraphListDictTests + ) + graph_dict_dict_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + GraphDictDictTests + ) + degree_sequence_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + DegreeSequenceTests + ) + inheritance_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + InheritanceTests + ) + refcount_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + ReferenceCountTests + ) + return unittest.TestSuite( + [ + basic_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, + ] + ) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/tests/test_bipartite.py b/tests/test_bipartite.py new file mode 100644 index 000000000..932838655 --- /dev/null +++ b/tests/test_bipartite.py @@ -0,0 +1,235 @@ +import unittest +from igraph import Graph + + +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() is False + ) + self.assertTrue(g.is_bipartite()) + 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() 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) + + g = Graph.Full_Bipartite(10, 5, directed=True, mode="out") + 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) + + g = Graph.Full_Bipartite(10, 5, directed=True, mode="in") + 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() 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) + + def testBiadjacency(self): + g = Graph.Biadjacency([[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)]) + + g = Graph.Biadjacency([[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)] + ) + + g = Graph.Biadjacency([[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)]) + + g = Graph.Biadjacency([[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)]) + + # Create a weighted Graph + g = Graph.Biadjacency([[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.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es["weight"], [1, 1, 2, 1]) + + # Graph is not weighted when weighted=`str` + g = Graph.Biadjacency([[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.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es["some_attr_name"], [1, 1, 2, 1]) + + # Graph is not weighted when weighted="" + g = Graph.Biadjacency([[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.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es[""], [1, 1, 2, 1]) + + # Should work when directed=True and mode=out with weighted + g = Graph.Biadjacency([[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.get_edgelist(), [(1, 2), (0, 3), (1, 3), (0, 4)]) + self.assertListEqual(g.es["weight"], [1, 1, 2, 1]) + + # Should work when directed=True and mode=in with weighted + g = Graph.Biadjacency( + [[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.get_edgelist(), [(2, 1), (3, 0), (3, 1), (4, 0)]) + self.assertListEqual(g.es["weight"], [1, 1, 2, 1]) + + # Should work when directed=True and mode=all with weighted + g = Graph.Biadjacency( + [[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.get_edgelist(), + [(1, 2), (2, 1), (0, 3), (3, 0), (1, 3), (3, 1), (0, 4), (4, 0)] + ) + self.assertListEqual(g.es["weight"], [1, 1, 1, 1, 2, 2, 1, 1]) + + def testBiadjacencyError(self): + msg = "arguments weighted and multiple can not co-exist" + with self.assertRaises(ValueError) as e: + Graph.Biadjacency([[0, 1, 1], [1, 2, 0]], multiple=True, weighted=True) + self.assertIn(msg, e.exception.args) + + with self.assertRaises(ValueError) as e: + Graph.Biadjacency([[0, 1, 1], [1, 2, 0]], multiple=True, weighted="string") + self.assertIn(msg, e.exception.args) + + with self.assertRaises(ValueError) as e: + Graph.Biadjacency([[0, 1, 1], [1, 2, 0]], multiple=True, weighted="") + self.assertIn(msg, e.exception.args) + + def testGetBiadjacency(self): + mat = [[0, 1, 1], [1, 1, 0]] + v1, v2 = [0, 1], [2, 3, 4] + g = Graph.Biadjacency(mat) + self.assertTrue(g.get_biadjacency() == (mat, v1, v2)) + g.vs["type2"] = g.vs["type"] + self.assertTrue(g.get_biadjacency("type2") == (mat, v1, v2)) + self.assertTrue(g.get_biadjacency(g.vs["type2"]) == (mat, v1, v2)) + + def testBipartiteProjection(self): + g = Graph.Full_Bipartite(10, 5) + + g1, g2 = g.bipartite_projection() + self.assertTrue(g1.is_complete()) + self.assertTrue(g1.isomorphic(Graph.Full(10))) + self.assertTrue(g2.is_complete()) + self.assertTrue(g2.isomorphic(Graph.Full(5))) + self.assertTrue(g.bipartite_projection(which=0).isomorphic(g1)) + self.assertTrue(g.bipartite_projection(which=1).isomorphic(g2)) + self.assertTrue(g.bipartite_projection(which=False).isomorphic(g1)) + self.assertTrue(g.bipartite_projection(which=True).isomorphic(g2)) + self.assertTrue(g1.es["weight"] == [5] * 45) + self.assertTrue(g2.es["weight"] == [10] * 10) + self.assertTrue(g.bipartite_projection_size() == (10, 45, 5, 10)) + + g1, g2 = g.bipartite_projection(probe1=10) + self.assertTrue(g1.is_complete()) + self.assertTrue(g1.isomorphic(Graph.Full(5))) + self.assertTrue(g2.is_complete()) + self.assertTrue(g2.isomorphic(Graph.Full(10))) + self.assertTrue(g.bipartite_projection(which=0).isomorphic(g2)) + self.assertTrue(g.bipartite_projection(which=1).isomorphic(g1)) + self.assertTrue(g.bipartite_projection(which=False).isomorphic(g2)) + self.assertTrue(g.bipartite_projection(which=True).isomorphic(g1)) + + g1, g2 = g.bipartite_projection(multiplicity=False) + self.assertTrue(g1.is_complete()) + self.assertTrue(g1.isomorphic(Graph.Full(10))) + self.assertTrue(g2.is_complete()) + self.assertTrue(g2.isomorphic(Graph.Full(5))) + self.assertTrue(g.bipartite_projection(which=0).isomorphic(g1)) + self.assertTrue(g.bipartite_projection(which=1).isomorphic(g2)) + self.assertTrue(g.bipartite_projection(which=False).isomorphic(g1)) + self.assertTrue(g.bipartite_projection(which=True).isomorphic(g2)) + self.assertTrue("weight" not in g1.edge_attributes()) + self.assertTrue("weight" not in g2.edge_attributes()) + + def testIsBipartite(self): + g = Graph.Star(10) + 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() is True) + g = Graph.Ring(9) + self.assertTrue(g.is_bipartite() is False) + self.assertTrue(g.is_bipartite(True) == (False, None)) + g = Graph.Ring(10) + self.assertTrue(g.is_bipartite() is True) + g += (2, 0) + self.assertTrue(g.is_bipartite(True) == (False, None)) + + +def suite(): + bipartite_suite = unittest.defaultTestLoader.loadTestsFromTestCase(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 new file mode 100644 index 000000000..40c0f9ea3 --- /dev/null +++ b/tests/test_cliques.py @@ -0,0 +1,301 @@ +import unittest + +from math import inf + +from igraph import Graph + +from .utils import temporary_file + + +class CliqueTests(unittest.TestCase): + def setUp(self): + 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(): + self.assertEqual(sorted(exp), sorted(map(sorted, self.g.cliques(lo, hi)))) + + for (lo, hi), exp in tests.items(): + self.assertEqual(sorted(exp), sorted(map(sorted, self.g.cliques(lo, hi, max_results=inf)))) + + for (lo, hi), exp in tests.items(): + observed = [sorted(cl) for cl in self.g.cliques(lo, hi, max_results=10)] + for cl in observed: + self.assertTrue(cl in exp) + + for (lo, hi), _ in tests.items(): + self.assertEqual([], self.g.cliques(lo, hi, max_results=0)) + + for (lo, hi), _ in tests.items(): + with self.assertRaises(ValueError): + self.g.cliques(lo, hi, max_results=-2) + + def testLargestCliques(self): + self.assertEqual( + sorted(map(sorted, self.g.largest_cliques())), [[1, 2, 3, 4], [1, 2, 4, 5]] + ) + self.assertTrue(all(map(self.g.is_clique, self.g.largest_cliques()))) + + 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.assertTrue(all(map(self.g.is_clique, self.g.maximal_cliques()))) + 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) + + 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) + ) + + with temporary_file() as fname: + self.g.maximal_cliques(min=4, file=fname) + self.assertEqual([[1, 2, 3, 4], [1, 2, 4, 5]], read_cliques(fname)) + + with temporary_file() as fname: + self.g.maximal_cliques(max=3, file=fname) + self.assertEqual([[0, 3, 4], [0, 4, 5]], read_cliques(fname)) + + def testCliqueNumber(self): + self.assertEqual(self.g.clique_number(), 4) + self.assertEqual(self.g.omega(), 4) + + +class IndependentVertexSetTests(unittest.TestCase): + def setUp(self): + self.g1 = Graph.Tree(5, 2, "undirected") + self.g2 = Graph.Tree(10, 2, "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(): + 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.assertTrue( + all( + map( + self.g1.is_independent_vertex_set, + self.g1.largest_independent_vertex_sets(), + ) + ) + ) + + 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.assertTrue( + all( + map( + self.g2.is_independent_vertex_set, + self.g2.maximal_independent_vertex_sets(), + ) + ) + ) + + def testIndependenceNumber(self): + self.assertEqual(self.g2.independence_number(), 6) + self.assertEqual(self.g2.alpha(), 6) + + +class CliqueBenchmark: + """This is a benchmark, not a real test case. You can run it + using: + + >>> from igraph.test.cliques import CliqueBenchmark + >>> CliqueBenchmark().run() + """ + + def __init__(self): + from time import time + import gc + + self.time = time + self.gc_collect = gc.collect + + def run(self): + self.printIntro() + self.testRandom() + self.testMoonMoser() + self.testGRG() + + def printIntro(self): + print("n = number of vertices") + print("#cliques = number of maximal cliques found") + print("t1 = time required to determine the clique number") + print("t2 = time required to determine and save all maximal cliques") + print() + + def timeit(self, g): + start = self.time() + g.clique_number() + mid = self.time() + cl = g.maximal_cliques() + end = self.time() + self.gc_collect() + 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], + } + + print() + print("Erdos-Renyi random graphs") + print(" n p #cliques t1 t2") + for n in sorted(np.keys()): + for p in np[n]: + g = Graph.Erdos_Renyi(n, p) + result = self.timeit(g) + print("%8d %8.3f %8d %8.4fs %8.4fs" % tuple([n, p] + list(result))) + + def testMoonMoser(self): + ns = [15, 27, 33] + + print() + print("Moon-Moser graphs") + print(" n exp_clqs #cliques t1 t2") + for n in ns: + 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)) + ) + + def testGRG(self): + ns = [100, 1000, 5000, 10000, 25000, 50000] + + print() + print("Geometric random graphs") + print(" n d #cliques t1 t2") + for n in ns: + 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))) + + +def suite(): + clique_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CliqueTests) + indvset_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + IndependentVertexSetTests + ) + return unittest.TestSuite([clique_suite, indvset_suite]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/tests/test_coloring.py b/tests/test_coloring.py new file mode 100644 index 000000000..b765e09f7 --- /dev/null +++ b/tests/test_coloring.py @@ -0,0 +1,39 @@ +import unittest +from igraph import Graph + + +def assert_valid_vertex_coloring(graph, coloring): + assert min(coloring) == 0 + for edge in graph.es: + source, target = edge.tuple + assert source == target or coloring[source] != coloring[target] + + +class VertexColoringTests(unittest.TestCase): + def testGreedyVertexColoring(self): + g = Graph.Famous("petersen") + + col = g.vertex_coloring_greedy() + assert_valid_vertex_coloring(g, col) + + col = g.vertex_coloring_greedy("colored_neighbors") + assert_valid_vertex_coloring(g, col) + + col = g.vertex_coloring_greedy("dsatur") + assert_valid_vertex_coloring(g, col) + + +def suite(): + vertex_coloring_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + VertexColoringTests + ) + return unittest.TestSuite([vertex_coloring_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 new file mode 100644 index 000000000..ffedf5a33 --- /dev/null +++ b/tests/test_colortests.py @@ -0,0 +1,118 @@ +import unittest + +from igraph import ( + hsv_to_rgb, + hsva_to_rgba, + hsl_to_rgb, + hsla_to_rgba, + rgb_to_hsl, + rgba_to_hsla, + rgba_to_hsva, + rgb_to_hsv, + GradientPalette, + AdvancedGradientPalette, +) + + +class ColorTests(unittest.TestCase): + def assertAlmostEqualMany(self, 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), + ] + self.data = [dict(list(zip(columns, value))) for value in values] + for row in self.data: + row["h"] /= 360.0 + + def _testGeneric(self, method, args1, args2=("r", "g", "b")): + 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] + self.assertAlmostEqualMany(method(*vals1), vals2, 2) + + def testHSVtoRGB(self): + self._testGeneric(hsv_to_rgb, "h s_hsv v".split()) + + def testHSVAtoRGBA(self): + self._testGeneric(hsva_to_rgba, "h s_hsv v alpha".split()) + + def testHSLtoRGB(self): + self._testGeneric(hsl_to_rgb, "h s_hsl l".split()) + + def testHSLAtoRGBA(self): + self._testGeneric(hsla_to_rgba, "h s_hsl l alpha".split()) + + 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() + ) + + 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() + ) + + +class PaletteTests(unittest.TestCase): + def testGradientPalette(self): + gp = GradientPalette("red", "blue", 3) + 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.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.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(): + color_suite = unittest.defaultTestLoader.loadTestsFromTestCase(ColorTests) + palette_suite = unittest.defaultTestLoader.loadTestsFromTestCase(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 new file mode 100644 index 000000000..6f3cdc22e --- /dev/null +++ b/tests/test_conversion.py @@ -0,0 +1,214 @@ +import random +import unittest + +from igraph import Graph, Matrix + + +class DirectedUndirectedTests(unittest.TestCase): + def testToUndirected(self): + 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() is False) + 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() is False) + self.assertTrue(sorted(graph2.get_edgelist()) == [(0, 1), (0, 2)]) + + graph2 = graph.copy() + 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() is False) + 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) + graph2 = graph.copy() + 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() is 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] + ) + + def testToDirectedNoModeArg(self): + 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)] + ) + + 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) + 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): + # Undirected case + g = Graph.Tree(6, 3) + 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], + ] + ) + ) + + # 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], + ] + ) + ) + + def testGetSparseAdjacency(self): + try: + from scipy import sparse # noqa: F401 + import numpy as np + except ImportError: + self.skipTest("Scipy and numpy are dependencies of this test.") + + # Undirected case + g = Graph.Tree(6, 3) + 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)) + ) + + def testGetAdjacencyRoundtrip(self): + g = Graph.Tree(6, 3) + adj = g.get_adjacency() + g2 = Graph.Adjacency(adj, mode="undirected") + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(g.is_directed(), g2.is_directed()) + self.assertTrue(g.get_edgelist() == g2.get_edgelist()) + + +class PruferTests(unittest.TestCase): + def testFromPrufer(self): + g = Graph.Prufer([3, 3, 3, 4]) + self.assertEqual(6, g.vcount()) + self.assertEqual(5, g.ecount()) + self.assertEqual( + [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)], sorted(g.get_edgelist()) + ) + + def testToPrufer(self): + g = Graph([(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)]) + self.assertEqual([3, 3, 3, 4], g.to_prufer()) + + +def suite(): + direction_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + DirectedUndirectedTests + ) + representation_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + GraphRepresentationTests + ) + prufer_suite = unittest.defaultTestLoader.loadTestsFromTestCase(PruferTests) + return unittest.TestSuite([direction_suite, representation_suite, prufer_suite]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/tests/test_cycles.py b/tests/test_cycles.py new file mode 100644 index 000000000..e3549121c --- /dev/null +++ b/tests/test_cycles.py @@ -0,0 +1,206 @@ +import random +import unittest + +from igraph import Graph + + +class CycleTests(unittest.TestCase): + def setUp(self): + random.seed(42) + + def test_is_acyclic(self): + g = Graph.Tree(121, 3, mode="out") + self.assertTrue(g.is_acyclic()) + + g = Graph() + self.assertTrue(g.is_acyclic()) + + g = Graph.Ring(5) + self.assertFalse(g.is_acyclic()) + + def test_is_dag(self): + g = Graph(5, [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)], directed=True) + self.assertTrue(g.is_dag()) + g.to_undirected() + self.assertFalse(g.is_dag()) + g = Graph.Barabasi(1000, 2, directed=True) + self.assertTrue(g.is_dag()) + g = Graph.GRG(100, 0.2) + self.assertFalse(g.is_dag()) + g = Graph.Ring(10, directed=True, mutual=False) + self.assertFalse(g.is_dag()) + + def test_fundamental_cycles(self): + g = Graph( + [ + (1, 2), + (2, 3), + (3, 1), + (4, 5), + (5, 4), + (4, 5), + (6, 7), + (7, 8), + (8, 9), + (9, 6), + (6, 8), + (10, 10), + (10, 11), + (12, 12), + ] + ) + cycles = [sorted(cycle) for cycle in g.fundamental_cycles()] + assert cycles == [[0, 1, 2], [4, 5], [3, 5], [6, 7, 10], [8, 9, 10], [11], [13]] + + cycles = [sorted(cycle) for cycle in g.fundamental_cycles(start_vid=6)] + assert cycles == [[6, 7, 10], [8, 9, 10]] + + cycles = [ + sorted(cycle) for cycle in g.fundamental_cycles(start_vid=6, cutoff=1) + ] + assert cycles == [[6, 7, 10], [8, 9, 10]] + + def test_simple_cycles(self): + g = Graph( + [ + (0, 1), + (1, 2), + (2, 0), + (0, 0), + (0, 3), + (3, 4), + (4, 5), + (5, 0), + ] + ) + + vertices = g.simple_cycles(output="vpath") + edges = g.simple_cycles(output="epath") + assert len(vertices) == 3 + assert len(edges) == 3 + + def test_minimum_cycle_basis(self): + g = Graph( + [ + (1, 2), + (2, 3), + (3, 1), + (4, 5), + (5, 4), + (4, 5), + (6, 7), + (7, 8), + (8, 9), + (9, 6), + (6, 8), + (10, 10), + (10, 11), + (12, 12), + ] + ) + cycles = g.minimum_cycle_basis() + assert cycles == [ + (11,), + (13,), + (3, 5), + (4, 5), + (0, 1, 2), + (6, 7, 10), + (8, 9, 10), + ] + + g = Graph.Lattice((5, 6), circular=True) + cycles = g.minimum_cycle_basis() + expected = [ + (0, 1, 10, 3), + (0, 51, 50, 53), + (1, 8, 9, 18), + (2, 3, 12, 5), + (2, 53, 52, 55), + (4, 5, 14, 7), + (4, 55, 54, 57), + (6, 7, 16, 9), + (6, 57, 56, 59), + (8, 59, 58, 51), + (10, 11, 20, 13), + (11, 18, 19, 28), + (12, 13, 22, 15), + (14, 15, 24, 17), + (16, 17, 26, 19), + (20, 21, 30, 23), + (21, 28, 29, 38), + (22, 23, 32, 25), + (24, 25, 34, 27), + (26, 27, 36, 29), + (30, 31, 40, 33), + (31, 38, 39, 48), + (32, 33, 42, 35), + (34, 35, 44, 37), + (36, 37, 46, 39), + (40, 41, 50, 43), + (41, 48, 49, 58), + (42, 43, 52, 45), + (44, 45, 54, 47), + (0, 8, 6, 4, 2), + (1, 51, 41, 31, 21, 11), + ] + assert len(cycles) == len(expected) + for expected_cycle, observed_cycle in zip(expected, cycles): + assert expected_cycle == observed_cycle or expected_cycle == ( + observed_cycle[0], + ) + tuple(reversed(observed_cycle[1:])) + + g = Graph.Lattice((5, 6), circular=True) + cycles = g.minimum_cycle_basis(cutoff=2, complete=False) + expected = [ + (0, 1, 10, 3), + (0, 51, 50, 53), + (1, 8, 9, 18), + (2, 3, 12, 5), + (2, 53, 52, 55), + (4, 5, 14, 7), + (4, 55, 54, 57), + (6, 7, 16, 9), + (6, 57, 56, 59), + (8, 59, 58, 51), + (10, 11, 20, 13), + (11, 18, 19, 28), + (12, 13, 22, 15), + (14, 15, 24, 17), + (16, 17, 26, 19), + (20, 21, 30, 23), + (21, 28, 29, 38), + (22, 23, 32, 25), + (24, 25, 34, 27), + (26, 27, 36, 29), + (30, 31, 40, 33), + (31, 38, 39, 48), + (32, 33, 42, 35), + (34, 35, 44, 37), + (36, 37, 46, 39), + (40, 41, 50, 43), + (41, 48, 49, 58), + (42, 43, 52, 45), + (44, 45, 54, 47), + (0, 8, 6, 4, 2), + (1, 51, 41, 31, 21, 11), + ] + assert len(cycles) == len(expected) - 1 + for expected_cycle, observed_cycle in zip(expected[:-1], cycles): + assert expected_cycle == observed_cycle or expected_cycle == ( + observed_cycle[0], + ) + tuple(reversed(observed_cycle[1:])) + + +def suite(): + cycle_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CycleTests) + return unittest.TestSuite([cycle_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 new file mode 100644 index 000000000..e9cb73ddc --- /dev/null +++ b/tests/test_decomposition.py @@ -0,0 +1,798 @@ +import math +import random +import unittest + +from igraph import ( + Clustering, + CohesiveBlocks, + Cover, + Graph, + Histogram, + InternalError, + UniqueIdGenerator, + VertexClustering, + compare_communities, + split_join_distance, + set_random_number_generator, +) + + +class SubgraphTests(unittest.TestCase): + def testSubgraph(self): + g = Graph.Lattice([10, 10], circular=False, mutual=False) + 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.vs["id"] == vs) + + def testSubgraphEdges(self): + g = Graph.Lattice([10, 10], circular=False, mutual=False) + g.es["id"] = list(range(g.ecount())) + + es = [0, 1, 2, 5, 20, 21, 22, 24, 38, 40] + sg = g.subgraph_edges(es) + exp = Graph.Lattice([3, 3], circular=False, mutual=False) + exp.delete_edges([7, 8]) + + self.assertTrue(sg.isomorphic(exp)) + self.assertTrue(sg.es["id"] == es) + + +class DecompositionTests(unittest.TestCase): + def testDecomposeUndirected(self): + g = Graph([(0, 1), (1, 2), (2, 3)], n=4, directed=False) + components = g.decompose() + + assert len(components) == 1 + assert components[0].isomorphic(g) + + g = Graph.Full(5) + Graph.Full(3) + components = g.decompose() + + assert len(components) == 2 + assert components[0].isomorphic(Graph.Full(5)) + assert components[1].isomorphic(Graph.Full(3)) + + def testDecomposeDirected(self): + g = Graph([(0, 1), (1, 2), (2, 3)], n=4, directed=True) + components = g.decompose() + + g1 = Graph(1, directed=True) + assert len(components) == 4 + for component in components: + assert component.isomorphic(g1) + + 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]) + self.assertTrue(g.shell_index() == g.coreness()) + + 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): + def setUp(self): + self.cl = Clustering([0, 0, 0, 1, 1, 2, 1, 1, 4, 4]) + + def testClusteringIndex(self): + self.assertTrue(self.cl[0] == [0, 1, 2]) + self.assertTrue(self.cl[1] == [3, 4, 6, 7]) + self.assertTrue(self.cl[2] == [5]) + self.assertTrue(self.cl[3] == []) + self.assertTrue(self.cl[4] == [8, 9]) + + 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]) + + def testClusteringSizes(self): + self.assertTrue(self.cl.sizes() == [3, 4, 1, 0, 2]) + self.assertTrue(self.cl.sizes(2, 4, 1) == [1, 2, 4]) + self.assertTrue(self.cl.size(2) == 1) + + def testClusteringHistogram(self): + self.assertTrue(isinstance(self.cl.size_histogram(), Histogram)) + + +class VertexClusteringTests(unittest.TestCase): + def setUp(self): + self.graph = Graph.Full(10) + self.graph.vs["string"] = list("aaabbcccab") + self.graph.vs["int"] = [17, 41, 23, 25, 64, 33, 3, 24, 47, 15] + + def testFromStringAttribute(self): + cl = VertexClustering.FromAttribute(self.graph, "string") + self.assertTrue(cl.membership == [0, 0, 0, 1, 1, 2, 2, 2, 0, 1]) + + def testFromIntAttribute(self): + cl = VertexClustering.FromAttribute(self.graph, "int") + self.assertTrue(cl.membership == list(range(10))) + cl = VertexClustering.FromAttribute(self.graph, "int", 15) + self.assertTrue(cl.membership == [0, 1, 0, 0, 2, 1, 3, 0, 4, 0]) + cl = VertexClustering.FromAttribute(self.graph, "int", [10, 20, 30]) + self.assertTrue(cl.membership == [0, 1, 2, 2, 1, 1, 3, 2, 1, 0]) + + 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]))) + clg = cl.cluster_graph({"string": "concat", "int": max}) + + self.assertTrue(sorted(clg.get_edgelist()) == [(0, 2), (1, 2)]) + self.assertTrue(not clg.is_directed()) + self.assertTrue(clg.vs["string"] == ["aaa", "bbc", "ccab"]) + self.assertTrue(clg.vs["int"] == [41, 64, 47]) + + clg = cl.cluster_graph({"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(not clg.is_directed()) + self.assertTrue(clg.vs["string"] == ["aaa", "bbc", "ccab"]) + self.assertTrue(clg.vs["int"] == [41, 64, 47]) + + def testSizesWithNone(self): + cl = VertexClustering(self.graph, [0, 0, 0, None, 1, 1, 2, None, 2, None]) + self.assertTrue(cl.sizes() == [3, 2, 2]) + + def testClusteringOfNullGraph(self): + null_graph = Graph() + cl = VertexClustering(null_graph, []) + self.assertTrue(cl.sizes() == []) + self.assertTrue(cl.giant().vcount() == 0) + self.assertTrue(cl.giant().ecount() == 0) + + +class CoverTests(unittest.TestCase): + def setUp(self): + 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]) + self.assertTrue(self.cl[1] == [3, 4, 5, 6, 9]) + self.assertTrue(self.cl[2] == []) + self.assertTrue(self.cl[3] == [8, 9]) + + def testCoverLength(self): + self.assertTrue(len(self.cl) == 4) + + def testCoverSizes(self): + self.assertTrue(self.cl.sizes() == [4, 5, 0, 2]) + self.assertTrue(self.cl.sizes(1, 3, 0) == [5, 2, 4]) + self.assertTrue(self.cl.size(1) == 5) + self.assertTrue(self.cl.size(2) == 0) + + def testCoverHistogram(self): + self.assertTrue(isinstance(self.cl.size_histogram(), Histogram)) + + def testCoverConstructorWithN(self): + self.assertTrue(self.cl.n == 10) + cl = Cover(self.cl, n=15) + self.assertTrue(cl.n == 15) + cl = Cover(self.cl, n=1) + self.assertTrue(cl.n == 10) + + +class CommunityTests(unittest.TestCase): + def reindexMembership(self, cl): + if hasattr(cl, "membership"): + cl = cl.membership + idgen = UniqueIdGenerator() + return [idgen[i] for i in cl] + + def assertMembershipsEqual(self, observed, expected): + if hasattr(observed, "membership"): + observed = observed.membership + if hasattr(expected, "membership"): + expected = expected.membership + 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.assertAlmostEqual(cl.q, 0.4523, places=3) + + # Lollipop, weighted + g = Graph.Full(4) + Graph.Full(2) + 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.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.assertAlmostEqual(cl.q, 0.1796, places=3) + + # Disconnected graph + g = Graph.Full(4) + Graph.Full(4) + Graph.Full(3) + Graph.Full(2) + cl = g.community_fastgreedy().as_clustering() + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3]) + + # Empty graph + g = Graph(20) + cl = g.community_fastgreedy().as_clustering() + 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) + + # Full graph with weights + g.es["weight"] = 1 + g[0, 1] = g[1, 2] = g[2, 0] = g[3, 4] = 10 + + cl = g.community_edge_betweenness(weights="weight").as_clustering() + 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.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.assertAlmostEqual(cl.q, 0.4523, places=3) + + def testFluidCommunities(self): + # Test with a simple graph: two cliques connected by a single edge + g = Graph.Full(5) + Graph.Full(5) + g.add_edges([(0, 5)]) + + # Test basic functionality - should find 2 communities + cl = g.community_fluid_communities(2) + self.assertEqual(len(set(cl.membership)), 2) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) + + # Test with 3 cliques + g = Graph.Full(4) + Graph.Full(4) + Graph.Full(4) + g += [(0, 4), (4, 8)] # Connect the cliques + cl = g.community_fluid_communities(3) + self.assertEqual(len(set(cl.membership)), 3) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2]) + + # Test error conditions + # Number of communities must be positive + with self.assertRaises(Exception): + g.community_fluid_communities(0) + + # Number of communities cannot exceed number of vertices + with self.assertRaises(Exception): + g.community_fluid_communities(g.vcount() + 1) + + # Test with disconnected graph (should raise error) + g_disconnected = Graph.Full(3) + Graph.Full(3) # No connecting edge + with self.assertRaises(Exception): + g_disconnected.community_fluid_communities(2) + + # Test with single vertex (edge case) + g_single = Graph(1) + cl = g_single.community_fluid_communities(1) + self.assertEqual(cl.membership, [0]) + + # Test with small connected graph + g_small = Graph([(0, 1), (1, 2), (2, 0)]) # Triangle + cl = g_small.community_fluid_communities(1) + self.assertEqual(len(set(cl.membership)), 1) + self.assertEqual(cl.membership, [0, 0, 0]) + + # Test deterministic behavior on simple structure + # Note: Fluid communities can be non-deterministic due to randomization, + # but on very simple structures it should be consistent + g_path = Graph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]) + cl = g_path.community_fluid_communities(2) + self.assertEqual(len(set(cl.membership)), 2) + + # Test that it returns a VertexClustering object + g = Graph.Full(6) + cl = g.community_fluid_communities(2) + self.assertIsInstance(cl, VertexClustering) + self.assertEqual(len(cl.membership), g.vcount()) + + def testInfomap(self): + g = Graph.Famous("zachary") + cl = g.community_infomap() + self.assertAlmostEqual(cl.codelength, 4.31179, 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, + ) + + # Smoke testing with vertex and edge weights + v_weights = [random.randint(1, 5) for _ in range(g.vcount())] + e_weights = [random.randint(1, 5) for _ in range(g.ecount())] + cl = g.community_infomap(edge_weights=e_weights) + cl = g.community_infomap(vertex_weights=v_weights) + cl = g.community_infomap(edge_weights=e_weights, vertex_weights=v_weights) + + def testLabelPropagation(self): + # Nothing to test there really, since the algorithm is pretty + # nondeterministic. We just do a few quick smoke tests. + g = Graph.GRG(100, 0.2) + cl = g.community_label_propagation() + + 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] + ) + + g = Graph.GRG(100, 0.2) + g.vs["initial"] = [ + 0 if i == 0 else 1 if i == 99 else 2 if i == 49 else random.randint(0, 50) + for i in range(g.vcount()) + ] + g.vs["dont_move"] = [i in (0, 49, 99) for i in range(g.vcount())] + cl = g.community_label_propagation(initial="initial", fixed="dont_move") + + # igraph is free to reorder the clusters so only co-membership will be + # preserved, hence the next assertion + self.assertTrue( + cl.membership[0] != cl.membership[49] + and cl.membership[49] != cl.membership[99] + ) + self.assertTrue(x >= 0 and x <= 5 for x in cl.membership) + + def testMultilevel(self): + # Example graph from the paper + random.seed(42) + 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), + ] + + 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.assertAlmostEqual(cls[0].q, 0.346301, places=5) + self.assertAlmostEqual(cls[1].q, 0.392219, places=5) + + cls = g.community_multilevel() + self.assertTrue(len(cls.membership) == g.vcount()) + self.assertMembershipsEqual( + cls, [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1] + ) + self.assertAlmostEqual(cls.q, 0.392219, places=5) + + def testOptimalModularity(self): + try: + g = Graph.Famous("bull") + + cl = g.community_optimal_modularity() + self.assertTrue(len(cl) == 2) + self.assertMembershipsEqual(cl, [0, 0, 1, 0, 1]) + self.assertAlmostEqual(cl.q, 0.08, places=7) + + 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 + ) + + 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.assertAlmostEqual(cl.q, 0.4197896, places=7) + + 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 + ) + + except NotImplementedError: + # Well, meh + pass + + def testSpinglass(self): + g = Graph.Full(5) + Graph.Full(5) + Graph.Full(5) + 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, + ]: + ok = True + break + self.assertTrue(ok) + + def testVoronoi(self): + # Test 1: Two disconnected cliques - should find exactly 2 communities + g = Graph.Full(5) + Graph.Full(5) # Two separate complete graphs + cl = g.community_voronoi() + + # Should find exactly 2 communities + self.assertEqual(len(cl), 2) + + # Vertices 0-4 should be in one community, vertices 5-9 in another + communities = [set(), set()] + for vertex, community in enumerate(cl.membership): + communities[community].add(vertex) + + # One community should have vertices 0-4, the other should have 5-9 + expected_communities = [{0, 1, 2, 3, 4}, {5, 6, 7, 8, 9}] + self.assertEqual( + set(frozenset(c) for c in communities), + set(frozenset(c) for c in expected_communities) + ) + + # Test 2: Two cliques connected by a single bridge edge + g = Graph.Full(4) + Graph.Full(4) + g.add_edges([(0, 4)]) # Bridge connecting the two cliques + + cl = g.community_voronoi() + + # Should still find 2 communities (bridge is weak) + self.assertEqual(len(cl), 2) + + # Check that vertices within each clique are in the same community + # Vertices 0,1,2,3 should be together, and 4,5,6,7 should be together + comm_0123 = {cl.membership[i] for i in [0, 1, 2, 3]} + comm_4567 = {cl.membership[i] for i in [4, 5, 6, 7]} + + self.assertEqual(len(comm_0123), 1) # All in same community + self.assertEqual(len(comm_4567), 1) # All in same community + self.assertNotEqual(comm_0123, comm_4567) # Different communities + + # Test 3: Three disconnected triangles + g = Graph(9) + g.add_edges([(0, 1), (1, 2), (2, 0), # Triangle 1 + (3, 4), (4, 5), (5, 3), # Triangle 2 + (6, 7), (7, 8), (8, 6)]) # Triangle 3 + + cl = g.community_voronoi() + + # Should find exactly 3 communities + self.assertEqual(len(cl), 3) + + def testWalktrap(self): + g = Graph.Full(5) + Graph.Full(5) + Graph.Full(5) + 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]) + 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]) + + 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), + ] + G = Graph.TupleList(edges, weights=True) + + import random + + random.seed(42) + set_random_number_generator(random) + # We don't find the optimal partition if we are greedy + cl = G.community_leiden( + "CPM", resolution=1, weights="weight", beta=0, n_iterations=-1 + ) + self.assertMembershipsEqual(cl, [0, 0, 1, 1, 1, 2, 2, 2]) + + random.seed(42) + set_random_number_generator(random) + # We can find the optimal partition if we allow for non-decreasing moves + # (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=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())) + ) + + def testCohesiveBlocks1(self): + # Taken from the igraph R manual + g = Graph.Full(4) + Graph(2) + [(3, 4), (4, 5), (4, 2)] + g *= 3 + g += [(0, 6), (1, 7), (0, 12), (4, 0), (4, 1)] + + cbs = g.cohesive_blocks() + self.genericTests(cbs) + self.assertEqual( + sorted(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.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" + ) + + 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 + ) + 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)] + ) + + def testCohesiveBlockingErrors(self): + g = Graph.GRG(100, 0.2) + g.to_directed() + self.assertRaises(InternalError, g.cohesive_blocks) + + +class ComparisonTests(unittest.TestCase): + def setUp(self): + self.clusterings = [ + ([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], + ), + ] + + def _testMethod(self, method, expected): + for (comm1, comm2), result in zip(self.clusterings, expected): + self.assertAlmostEqual( + compare_communities(comm1, comm2, method=method), result, places=3 + ) + + def testCompareVI(self): + expected = [0, 0.8675, math.log(6)] + self._testMethod(None, expected) + self._testMethod("vi", expected) + + def testCompareNMI(self): + expected = [1, 0.5158, 0] + self._testMethod("nmi", expected) + + def testCompareSplitJoin(self): + expected = [0, 3, 5, 11] + 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)) + + def testCompareRand(self): + expected = [1, 2 / 3.0, 0, 0.590909] + self._testMethod("rand", expected) + + def testCompareAdjustedRand(self): + expected = [1, 0.242424, 0, -0.04700353] + self._testMethod("adjusted_rand", expected) + + 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 + ) + + +def suite(): + decomposition_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + DecompositionTests + ) + clustering_suite = unittest.defaultTestLoader.loadTestsFromTestCase(ClusteringTests) + vertex_clustering_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + VertexClusteringTests + ) + cover_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CoverTests) + community_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CommunityTests) + cohesive_blocks_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + CohesiveBlocksTests + ) + comparison_suite = unittest.defaultTestLoader.loadTestsFromTestCase(ComparisonTests) + 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/igraph/test/edgeseq.py b/tests/test_edgeseq.py similarity index 53% rename from igraph/test/edgeseq.py rename to tests/test_edgeseq.py index ccd7a8859..7b4d64959 100644 --- a/igraph/test/edgeseq.py +++ b/tests/test_edgeseq.py @@ -1,14 +1,17 @@ # vim:ts=4 sw=4 sts=4: import unittest -from igraph import * -from igraph.test.utils import is_pypy, skipIf + +from igraph import Graph, Edge, EdgeSeq, VertexSeq + +from .utils import is_pypy try: import numpy as np except ImportError: np = None + class EdgeTests(unittest.TestCase): def setUp(self): self.g = Graph.Full(10) @@ -16,30 +19,30 @@ def setUp(self): def testHash(self): data = {} n = self.g.ecount() - for i in xrange(n): + for i in range(n): code1 = hash(self.g.es[i]) code2 = hash(self.g.es[i]) self.assertEqual(code1, code2) data[self.g.es[i]] = i - for i in xrange(n): + for i in range(n): self.assertEqual(i, data[self.g.es[i]]) def testRichCompare(self): - idxs = [2,5,9,13,42] + idxs = [2, 5, 9, 13, 42] g2 = Graph.Full(10) for i in idxs: for j in idxs: self.assertEqual(i == j, self.g.es[i] == self.g.es[j]) self.assertEqual(i != j, self.g.es[i] != self.g.es[j]) - self.assertEqual(i < j, self.g.es[i] < self.g.es[j]) - self.assertEqual(i > j, self.g.es[i] > self.g.es[j]) + self.assertEqual(i < j, self.g.es[i] < self.g.es[j]) + self.assertEqual(i > j, self.g.es[i] > self.g.es[j]) self.assertEqual(i <= j, self.g.es[i] <= self.g.es[j]) self.assertEqual(i >= j, self.g.es[i] >= self.g.es[j]) self.assertFalse(self.g.es[i] == g2.es[j]) self.assertFalse(self.g.es[i] != g2.es[j]) - self.assertFalse(self.g.es[i] < g2.es[j]) - self.assertFalse(self.g.es[i] > g2.es[j]) + self.assertFalse(self.g.es[i] < g2.es[j]) + self.assertFalse(self.g.es[i] > g2.es[j]) self.assertFalse(self.g.es[i] <= g2.es[j]) self.assertFalse(self.g.es[i] >= g2.es[j]) @@ -49,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) @@ -60,13 +63,13 @@ def testUpdateAttributes(self): self.assertEqual(e["a"], 2) e.update_attributes([("a", 3), ("b", 4)], c=5, d=6) - self.assertEqual(e.attributes(), dict(a=3, b=4, c=5, d=6)) + self.assertEqual(e.attributes(), {"a": 3, "b": 4, "c": 5, "d": 6}) - e.update_attributes(dict(b=44, c=55)) - self.assertEqual(e.attributes(), dict(a=3, b=44, c=55, d=6)) + e.update_attributes({"b": 44, "c": 55}) + self.assertEqual(e.attributes(), {"a": 3, "b": 44, "c": 55, "d": 6}) def testPhantomEdge(self): - e = self.g.es[self.g.ecount()-1] + e = self.g.es[self.g.ecount() - 1] e.delete() # v is now a phantom edge; try to freak igraph out now :) @@ -82,7 +85,9 @@ 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] @@ -92,8 +97,7 @@ def testProxyMethods(self): ignore = set(ignore.split()) # Methods not listed here are expected to return an int or a float - return_types = { - } + return_types = {} for name in Edge.__dict__: if name in ignore: @@ -106,71 +110,86 @@ def testProxyMethods(self): continue result = func() - self.assertEqual(getattr(g, name)(e.index), result, - msg=("Edge.%s proxy method misbehaved" % name)) + self.assertEqual( + getattr(g, name)(e.index), + result, + msg=("Edge.%s proxy method misbehaved" % name), + ) return_type = return_types.get(name, (int, float)) - self.assertTrue(isinstance(result, return_type), - msg=("Edge.%s proxy method did not return %s" % (name, return_type)) + self.assertTrue( + isinstance(result, return_type), + msg=("Edge.%s proxy method did not return %s" % (name, return_type)), ) - class EdgeSeqTests(unittest.TestCase): + def assert_edges_unique_in(self, es): + pairs = sorted(e.tuple for e in es) + self.assertEqual(pairs, sorted(set(pairs))) + 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) self.assertTrue(len(EdgeSeq(self.g, 2)) == 1) - self.assertTrue(len(EdgeSeq(self.g, [1,2,3])) == 3) - self.assertTrue(EdgeSeq(self.g, [1,2,3]).indices == [1,2,3]) + self.assertTrue(len(EdgeSeq(self.g, [1, 2, 3])) == 3) + self.assertTrue(EdgeSeq(self.g, [1, 2, 3]).indices == [1, 2, 3]) self.assertRaises(ValueError, EdgeSeq, self.g, 112) self.assertRaises(ValueError, EdgeSeq, self.g, [112]) self.assertTrue(self.g.es.graph == self.g) def testIndexing(self): n = self.g.ecount() - for i in xrange(n): + for i in range(n): self.assertEqual(i, self.g.es[i].index) - self.assertEqual(n-i-1, self.g.es[-i-1].index) + self.assertEqual(n - i - 1, self.g.es[-i - 1].index) self.assertRaises(IndexError, self.g.es.__getitem__, n) - self.assertRaises(IndexError, self.g.es.__getitem__, -n-1) + 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): + assert np is not None + n = self.g.ecount() - for i in xrange(n): + for i in range(n): arr = np.array([i]) self.assertEqual(i, self.g.es[arr[0]].index) arr = np.array([n]) self.assertRaises(IndexError, self.g.es.__getitem__, arr[0]) - arr = np.array([-n-1]) + arr = np.array([-n - 1]) self.assertRaises(IndexError, self.g.es.__getitem__, arr[0]) arr = np.array([1.5]) self.assertRaises(TypeError, self.g.es.__getitem__, arr[0]) + ind = [1, 3, 5, 8, 3, 2] + arr = np.array(ind) + self.assertEqual(ind, [edge.index for edge in self.g.es[arr.tolist()]]) + self.assertEqual(ind, [edge.index for edge in self.g.es[list(arr)]]) + def testPartialAttributeAssignment(self): only_even = self.g.es.select(lambda e: (e.index % 2 == 0)) - only_even["test"] = [0]*len(only_even) - expected = [[0,i][i % 2] for i in xrange(self.g.ecount())] + only_even["test"] = [0] * len(only_even) + expected = [[0, i][i % 2] for i in range(self.g.ecount())] self.assertTrue(self.g.es["test"] == expected) - only_even["test2"] = range(23) - expected = [[i//2, None][i % 2] for i in xrange(self.g.ecount())] + 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) def testSequenceReusing(self): - if "test" in self.g.edge_attributes(): del self.g.es["test"] + if "test" in self.g.edge_attributes(): + del self.g.es["test"] self.g.es["test"] = ["A", "B", "C"] - self.assertTrue(self.g.es["test"] == ["A", "B", "C"]*15) + self.assertTrue(self.g.es["test"] == ["A", "B", "C"] * 15) self.g.es["test"] = "ABC" self.assertTrue(self.g.es["test"] == ["ABC"] * 45) @@ -187,7 +206,7 @@ def testSequenceReusing(self): def testAllSequence(self): self.assertTrue(len(self.g.es) == 45) - self.assertTrue(self.g.es["test"] == range(45)) + self.assertTrue(self.g.es["test"] == list(range(45))) def testEmptySequence(self): empty_es = self.g.es.select(None) @@ -209,58 +228,60 @@ def testCallableFilteringSelect(self): only_even = self.g.es.select(lambda e: (e.index % 2 == 0)) self.assertTrue(len(only_even) == 23) self.assertRaises(KeyError, only_even.__getitem__, "nonexistent") - self.assertTrue(only_even["test"] == [i*2 for i in xrange(23)]) + self.assertTrue(only_even["test"] == [i * 2 for i in range(23)]) def testChainedCallableFilteringSelect(self): - only_div_six = self.g.es.select(lambda e: (e.index % 2 == 0), - lambda e: (e.index % 3 == 0)) + only_div_six = self.g.es.select( + lambda e: (e.index % 2 == 0), lambda e: (e.index % 3 == 0) + ) self.assertTrue(len(only_div_six) == 8) self.assertTrue(only_div_six["test"] == [0, 6, 12, 18, 24, 30, 36, 42]) - only_div_six = self.g.es.select(lambda e: (e.index % 2 == 0)).select(\ - lambda e: (e.index % 3 == 0)) + only_div_six = self.g.es.select(lambda e: (e.index % 2 == 0)).select( + lambda e: (e.index % 3 == 0) + ) self.assertTrue(len(only_div_six) == 8) self.assertTrue(only_div_six["test"] == [0, 6, 12, 18, 24, 30, 36, 42]) def testIntegerFilteringFind(self): self.assertEqual(self.g.es.find(3).index, 3) - self.assertEqual(self.g.es.select(2,3,4,2).find(3).index, 2) + self.assertEqual(self.g.es.select(2, 3, 4, 2).find(3).index, 2) self.assertRaises(IndexError, self.g.es.find, 178) def testIntegerFilteringSelect(self): - subset = self.g.es.select(2,3,4,2) + subset = self.g.es.select(2, 3, 4, 2) self.assertTrue(len(subset) == 4) - self.assertTrue(subset["test"] == [2,3,4,2]) + self.assertTrue(subset["test"] == [2, 3, 4, 2]) self.assertRaises(TypeError, self.g.es.select, 2, 3, 4, 2, None) - subset = self.g.es[2,3,4,2] + subset = self.g.es[2, 3, 4, 2] self.assertTrue(len(subset) == 4) - self.assertTrue(subset["test"] == [2,3,4,2]) + self.assertTrue(subset["test"] == [2, 3, 4, 2]) def testIterableFilteringSelect(self): - subset = self.g.es.select(xrange(5,8)) + subset = self.g.es.select(list(range(5, 8))) self.assertTrue(len(subset) == 3) - self.assertTrue(subset["test"] == [5,6,7]) + self.assertTrue(subset["test"] == [5, 6, 7]) def testSliceFilteringSelect(self): subset = self.g.es.select(slice(5, 8)) self.assertTrue(len(subset) == 3) - self.assertTrue(subset["test"] == [5,6,7]) + self.assertTrue(subset["test"] == [5, 6, 7]) subset = self.g.es[40:56:2] self.assertTrue(len(subset) == 3) - self.assertTrue(subset["test"] == [40,42,44]) + self.assertTrue(subset["test"] == [40, 42, 44]) def testKeywordFilteringSelect(self): g = Graph.Barabasi(1000, 2) g.es["betweenness"] = g.edge_betweenness() - g.es["parity"] = [i % 2 for i in xrange(g.ecount())] + g.es["parity"] = [i % 2 for i in range(g.ecount())] self.assertTrue(len(g.es(betweenness_gt=10)) < 2000) self.assertTrue(len(g.es(betweenness_gt=10, parity=0)) < 2000) def testSourceTargetFiltering(self): - g = Graph.Barabasi(1000, 2) - es1 = set(e.source for e in g.es.select(_target_in = [2,4])) - es2 = set(v1 for v1, v2 in g.get_edgelist() if v2 in [2, 4]) + g = Graph.Barabasi(1000, 2, directed=True) + es1 = {e.source for e in g.es.select(_target_in=[2, 4])} + es2 = {v1 for v1, v2 in g.get_edgelist() if v2 in [2, 4]} self.assertTrue(es1 == es2) def testWithinFiltering(self): @@ -268,28 +289,110 @@ def testWithinFiltering(self): vs = [0, 1, 2, 10, 11, 12, 20, 21, 22] vs2 = (0, 1, 10, 11) - es1 = g.es.select(_within = vs) - es2 = g.es.select(_within = VertexSeq(g, vs)) + es1 = g.es.select(_within=vs) + es2 = g.es.select(_within=VertexSeq(g, vs)) for es in [es1, es2]: self.assertTrue(len(es) == 12) self.assertTrue(all(e.source in vs and e.target in vs for e in es)) + self.assert_edges_unique_in(es) - es_filtered = es.select(_within = vs2) + es_filtered = es.select(_within=vs2) self.assertTrue(len(es_filtered) == 4) - self.assertTrue(all(e.source in vs2 and e.target in vs2 for e in es_filtered)) + self.assertTrue( + all(e.source in vs2 and e.target in vs2 for e in es_filtered) + ) + self.assert_edges_unique_in(es_filtered) def testBetweenFiltering(self): g = Graph.Lattice([10, 10]) vs1, vs2 = [10, 11, 12], [20, 21, 22] - es1 = g.es.select(_between = (vs1, vs2)) - es2 = g.es.select(_between = (VertexSeq(g, vs1), VertexSeq(g, vs2))) + es1 = g.es.select(_between=(vs1, vs2)) + es2 = g.es.select(_between=(VertexSeq(g, vs1), VertexSeq(g, vs2))) for es in [es1, es2]: self.assertTrue(len(es) == 3) - self.assertTrue(all((e.source in vs1 and e.target in vs2) or \ - (e.target in vs1 and e.source in vs2) for e in es)) + self.assertTrue( + all( + (e.source in vs1 and e.target in vs2) + or (e.target in vs1 and e.source in vs2) + for e in es + ) + ) + self.assert_edges_unique_in(es) + + def testIncidentFiltering(self): + g = Graph.Lattice([10, 10], circular=False) + vs = (0, 1, 10, 11) + vs2 = (11, 0, 24) + vs3 = sorted(set(vs).intersection(set(vs2))) + + es = g.es.select(_incident=vs) + self.assertEqual(8, len(es)) + self.assertTrue(all((e.source in vs or e.target in vs) for e in es)) + self.assert_edges_unique_in(es) + + es_filtered = es.select(_incident=vs2) + self.assertEqual(6, len(es_filtered)) + self.assertTrue(all((e.source in vs3 or e.target in vs3) for e in es_filtered)) + self.assert_edges_unique_in(es_filtered) + + def testIncidentFilteringDirected(self): + # Test case from https://igraph.discourse.group/t/edge-select-using-incident-on-directed-graphs/1645 + g = Graph([(0, 1), (1, 2), (2, 3)], directed=True) + + vs = (1,) + es = g.es.select(_incident=vs) + self.assertEqual(2, len(es)) + self.assertTrue(all((e.source in vs or e.target in vs) for e in es)) + self.assert_edges_unique_in(es) + + def testIncidentFilteringByNames(self): + g = Graph.Lattice([10, 10], circular=False) + vs = (0, 1, 10, 11) + g.vs[vs]["name"] = ["A", "B", "C", "D"] + + vs2 = (11, 0, 24) + g.vs[24]["name"] = "X" + + vs3 = sorted(set(vs).intersection(set(vs2))) + + es = g.es.select(_incident=("A", "B", "C", "D")) + self.assertEqual(8, len(es)) + self.assertTrue(all((e.source in vs or e.target in vs) for e in es)) + self.assert_edges_unique_in(es) + + es_filtered = es.select(_incident=("D", "A", "X")) + self.assertEqual(6, len(es_filtered)) + self.assertTrue(all((e.source in vs3 or e.target in vs3) for e in es_filtered)) + self.assert_edges_unique_in(es_filtered) + + es_filtered = es_filtered.select(_from="A") + self.assertEqual(2, len(es_filtered)) + self.assertTrue(all((e.source == 0 or e.target == 0) for e in es_filtered)) + self.assert_edges_unique_in(es_filtered) + + def testSourceAndTargetFilteringForUndirectedGraphs(self): + g = Graph.Lattice([10, 10], circular=False) + vs = (0, 1, 10, 11) + vs2 = (11, 0, 24) + vs3 = sorted(set(vs).intersection(set(vs2))) + + es = g.es.select(_from=vs) + self.assertEqual(8, len(es)) + self.assertTrue(all((e.source in vs or e.target in vs) for e in es)) + self.assert_edges_unique_in(es) + + es_filtered = es.select(_to_in=vs2) + self.assertEqual(6, len(es_filtered)) + self.assertTrue(all((e.source in vs3 or e.target in vs3) for e in es_filtered)) + self.assert_edges_unique_in(es_filtered) + + es_filtered = es_filtered.select(_from_eq=0) + self.assertEqual(2, len(es_filtered)) + self.assertTrue(all((e.source == 0 or e.target == 0) for e in es_filtered)) + self.assert_edges_unique_in(es_filtered) def testIndexOutOfBoundsSelect(self): g = Graph.Full(3) @@ -300,6 +403,10 @@ def testIndexOutOfBoundsSelect(self): self.assertRaises(ValueError, g.es.select, (2, -1)) self.assertRaises(ValueError, g.es.__getitem__, (0, 1000000)) + def testIndexAndKeywordFilteringFind(self): + self.assertRaises(ValueError, self.g.es.find, 2, test=4) + self.assertTrue(self.g.es.find(2, test=2) == self.g.es[2]) + def testGraphMethodProxying(self): idxs = [1, 3, 5, 7, 9] g = Graph.Barabasi(100) @@ -308,29 +415,33 @@ def testGraphMethodProxying(self): self.assertEqual([ebs[i] for i in idxs], es.edge_betweenness()) idxs = [1, 3] - g = Graph([(0, 1), (1, 2), (2, 0), (1, 0)], directed=True) + g = Graph([(0, 1), (1, 2), (2, 0), (1, 0), (2, 2)], directed=True) es = g.es(*idxs) mutual = g.is_mutual(es) self.assertEqual(mutual, es.is_mutual()) for e, m in zip(es, mutual): self.assertEqual(e.is_mutual(), m) + self.assertTrue(g.es[4].is_mutual()) + self.assertFalse(g.es[4].is_mutual(loops=False)) + def testIsAll(self): g = Graph.Full(5) self.assertTrue(g.es.is_all()) - self.assertFalse(g.es.select(1,2,3).is_all()) - self.assertFalse(g.es.select(_within=[1,2,3]).is_all()) + self.assertFalse(g.es.select(1, 2, 3).is_all()) + self.assertFalse(g.es.select(_within=[1, 2, 3]).is_all()) def suite(): - edge_suite = unittest.makeSuite(EdgeTests) - es_suite = unittest.makeSuite(EdgeSeqTests) + edge_suite = unittest.defaultTestLoader.loadTestsFromTestCase(EdgeTests) + es_suite = unittest.defaultTestLoader.loadTestsFromTestCase(EdgeSeqTests) return unittest.TestSuite([edge_suite, es_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/igraph/test/flow.py b/tests/test_flow.py similarity index 72% rename from igraph/test/flow.py rename to tests/test_flow.py index 4fa0c2cec..785a4d82d 100644 --- a/igraph/test/flow.py +++ b/tests/test_flow.py @@ -1,9 +1,10 @@ import unittest -from igraph import * +from igraph import Cut, Graph, EdgeSeq, InternalError 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,15 +12,14 @@ 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) self.assertTrue(Graph.Barabasi(50).edge_connectivity() == 1) self.assertTrue(self.g.adhesion() == 2) self.assertTrue(Graph.Tree(10, 3).adhesion() == 1) - self.assertTrue(Graph.Tree(10, 3, TREE_OUT).adhesion() == 0) + self.assertTrue(Graph.Tree(10, 3, "out").adhesion() == 0) self.assertRaises(ValueError, self.g.edge_connectivity, 0) def testVertexConnectivity(self): @@ -27,7 +27,7 @@ def testVertexConnectivity(self): self.assertTrue(Graph.Barabasi(50).vertex_connectivity() == 1) self.assertTrue(self.g.cohesion() == 2) self.assertTrue(Graph.Tree(10, 3).cohesion() == 1) - self.assertTrue(Graph.Tree(10, 3, TREE_OUT).cohesion() == 0) + self.assertTrue(Graph.Tree(10, 3, "out").cohesion() == 0) self.assertRaises(ValueError, self.g.vertex_connectivity, 0) self.assertRaises(InternalError, self.g.vertex_connectivity, 0, 1) self.assertTrue(self.g.vertex_connectivity(0, 1, neighbors="nodes") == 4) @@ -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 = zip(range(0, 5), range(1, 6)) - el += zip(range(6, 11), range(7, 12)) - el += 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,57 +133,64 @@ 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" g = self.constructLadderGraph(directed=True) cuts = g.all_st_cuts(0, 11) self.assertEqual(len(cuts), 36) - self.assertEqual(len(set(tuple(cut.membership) for cut in cuts)), 36) + self.assertEqual(len({tuple(cut.membership) for cut in cuts}), 36) 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() g.to_directed("mutual") cuts = g.all_st_mincuts(0, 11) self.assertEqual(len(cuts), 7) - self.assertEqual(len(set(tuple(cut.membership) for cut in cuts)), 7) + self.assertEqual(len({tuple(cut.membership) for cut in cuts}), 7) for cut in cuts: 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,29 +203,35 @@ 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): g = Graph.GRG(25, 0.4) self.validate_gomory_hu_tree(g, g.gomory_hu_tree()) - g.es["capacity"] = [randint(0, 10) for _ in xrange(g.ecount())] + g.es["capacity"] = [randint(0, 10) for _ in range(g.ecount())] self.validate_gomory_hu_tree(g, g.gomory_hu_tree("capacity")) 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) + flow_suite = unittest.defaultTestLoader.loadTestsFromTestCase(MaxFlowTests) + cut_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CutTests) + gomory_hu_suite = unittest.defaultTestLoader.loadTestsFromTestCase(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 new file mode 100644 index 000000000..83664e5f5 --- /dev/null +++ b/tests/test_foreign.py @@ -0,0 +1,902 @@ +import gzip +import io +import unittest +import warnings + +from igraph import Graph, InternalError + +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 +except ImportError: + pd = None + + +GRAPHML_EXAMPLE_FILE = """\ + + + + + + + a + + + b + + + c + + + d + + + e + + + f + + + + + + + + + + + + + + + + + +""" + + +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.assertListEqual(g.es["weight"], [1.0, 2.0, 1.0, 3.0, 1.0]) + + g = func(fname, directed=False) + self.assertTrue( + "name" in g.vertex_attributes() and "weight" in g.edge_attributes() + ) + self.assertListEqual(g.vs["name"], ["eggs", "spam", "ham", "bacon"]) + self.assertListEqual(g.es["weight"], [1.0, 2.0, 1.0, 3.0, 1.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() + ) + + @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( + """\ + # 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.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.es["weight"] == [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2]) + + g.write_adjacency(tmpfname) + + def testGraphML(self): + with temporary_file(GRAPHML_EXAMPLE_FILE) 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) + g.write_graphml(tmpfname, prefixattr=False) + + 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()) + + 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 = 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) + + 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)]) + + # 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), {"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"]) + + @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)]) + + # 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), {"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), {"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), {"source", "target", "name", "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"]) + + @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())) + + # Test networkx with custom node hashables + # In this case each node is a tuple of ints + g_nx = nx.DiGraph() + inf = float("inf") + g_nx.add_edges_from( + ( + ((0, 0), (1, 1), {"weight": 3.0}), + ((0, 0), (1, 0), {"weight": inf}), + ) + ) + g = Graph.from_networkx(g_nx) + self.assertTrue(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertTrue(g.es["weight"] == [3.0, inf] or g.es["weight"] == [inf, 3.0]) + + @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( + 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: + # 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.defaultTestLoader.loadTestsFromTestCase(ForeignTests) + return unittest.TestSuite([foreign_suite]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/igraph/test/games.py b/tests/test_games.py similarity index 54% rename from igraph/test/games.py rename to tests/test_games.py index 07bc62eb2..cf36c75ce 100644 --- a/igraph/test/games.py +++ b/tests/test_games.py @@ -1,5 +1,7 @@ import unittest -from igraph import * + +from igraph import Graph, InternalError, Layout + class GameTests(unittest.TestCase): def testGRG(self): @@ -9,44 +11,80 @@ 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]]) - self.assertTrue(isinstance(g, Graph) and len(g.clusters()) == 2) + g = Graph.Preference(100, [1, 1], [[1, 0], [0, 1]]) + self.assertTrue(isinstance(g, Graph)) + self.assertEqual(len(g.connected_components()), 2) - 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) + g = Graph.Preference(100, [1, 1], [[1, 0], [0, 1]], attribute="type") + types = g.vs.get_attribute_values("type") + self.assertTrue(min(types) == 0 and max(types) == 1) 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) + g = Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[0, 1], [1, 0]]) + self.assertTrue(isinstance(g, Graph)) + self.assertEqual(len(g.connected_components()), 2) + + g = Graph.Asymmetric_Preference( + 100, [[0, 1], [1, 0]], [[1, 0], [0, 1]], attribute="type" + ) + types = g.vs.get_attribute_values("type") + types1 = [i[0] for i in types] + types2 = [i[1] for i in types] + self.assertTrue( + min(types1) == 0 + and max(types1) == 1 + and min(types2) == 0 + and max(types2) == 1 + ) + + g = Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[1, 0], [0, 1]]) + self.assertTrue(isinstance(g, Graph)) + self.assertEqual(len(g.connected_components()), 1) + + 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") + + # 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()) - 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) + # 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()) - g=Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[1, 0], [0, 1]]) - self.assertTrue(isinstance(g, Graph) and len(g.clusters()) == 1) + # 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) - 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 +92,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 +107,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 +115,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 +124,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 +132,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 +141,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 +150,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) @@ -125,20 +163,20 @@ def testRewire(self): self.assertTrue(g.is_simple()) # Rewiring with loops (1) - g.rewire(10000, mode="loops") + g.rewire(10000, allowed_edge_types="loops") self.assertEqual(degrees, g.degree()) self.assertFalse(any(g.is_multiple())) # Rewiring with loops (2) g = Graph.Full(4) - g[1,3] = 0 + g[1, 3] = 0 degrees = g.degree() - g.rewire(100, mode="loops") + g.rewire(100, allowed_edge_types="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) @@ -147,19 +185,21 @@ def testRewire(self): self.assertTrue(g.is_simple()) # Directed graph with loops - g.rewire(10000, mode="loops") + g.rewire(10000, allowed_edge_types="loops") self.assertEqual(indeg, g.indegree()) self.assertEqual(outdeg, g.outdegree()) self.assertFalse(any(g.is_multiple())) + def suite(): - game_suite = unittest.makeSuite(GameTests) + game_suite = unittest.defaultTestLoader.loadTestsFromTestCase(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 new file mode 100644 index 000000000..0e1227658 --- /dev/null +++ b/tests/test_generators.py @@ -0,0 +1,930 @@ +import unittest + +from igraph import Graph, InternalError + + +try: + import numpy as np +except ImportError: + np = None + +try: + import scipy.sparse as sparse +except ImportError: + sparse = None + +try: + import pandas as pd +except ImportError: + pd = None + + +class GeneratorTests(unittest.TestCase): + def testStar(self): + 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)] + 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)] + 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)] + self.assertTrue(not g.is_directed()) + self.assertTrue(sorted(g.get_edgelist()) == el) + + def testFamous(self): + g = Graph.Famous("tutte") + self.assertTrue(g.vcount() == 46 and g.ecount() == 69) + self.assertRaises(InternalError, Graph.Famous, "unknown") + + def testFormula(self): + tests = [ + (None, [], []), + ("", [""], []), + ("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"], + [(0, 1), (2, 3), (4, 5), (6, 7)], + ), + ( + "A:B:C:D -- A:B:C:D", + ["A", "B", "C", "D"], + [(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", + ["Alice", "Bob", "Cecil", "Daniel", "Eugene", "Gordon"], + [(0, 1), (1, 2), (0, 2), (2, 3), (2, 4), (2, 5)], + ), + ( + "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)], + ), + ( + "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)], + ), + ( + "Alice -- Bob -- Daniel, Cecil:Gordon, Helen", + ["Alice", "Bob", "Daniel", "Cecil", "Gordon", "Helen"], + [(0, 1), (1, 2)], + ), + ( + '"+" -- "-", "*" -- "/", "%%" -- "%/%"', + ["+", "-", "*", "/", "%%", "%/%"], + [(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)]), + ] + for formula, names, edges in tests: + g = Graph.Formula(formula) + self.assertEqual(g.vs["name"], names) + self.assertEqual(g.get_edgelist(), sorted(edges)) + + def testFull(self): + g = Graph.Full(20, directed=True) + el = g.get_edgelist() + el.sort() + self.assertTrue(g.is_complete()) + 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() + 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)]) + + 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)]) + + self.assertRaises(ValueError, Graph.Full_Citation, -2) + + def testHexagonalLattice(self): + el = [ + (0, 1), + (0, 6), + (1, 2), + (2, 3), + (2, 8), + (3, 4), + (4, 10), + (5, 6), + (5, 11), + (6, 7), + (7, 8), + (7, 13), + (8, 9), + (9, 10), + (9, 15), + (11, 12), + (12, 13), + (13, 14), + (14, 15), + ] + g = Graph.Hexagonal_Lattice([2, 2]) + self.assertEqual(sorted(g.get_edgelist()), el) + + g = Graph.Hexagonal_Lattice([2, 2], directed=True, mutual=False) + self.assertEqual(sorted(g.get_edgelist()), el) + + g = Graph.Hexagonal_Lattice([2, 2], directed=True, mutual=True) + self.assertEqual(sorted(g.get_edgelist()), sorted(el + [(y, x) for x, y in el])) + + def testHypercube(self): + el = [ + (0, 1), + (0, 2), + (0, 4), + (1, 3), + (1, 5), + (2, 3), + (2, 6), + (3, 7), + (4, 5), + (4, 6), + (5, 7), + (6, 7), + ] + g = Graph.Hypercube(3) + self.assertEqual(g.get_edgelist(), el) + + def testLCF(self): + g1 = Graph.LCF(12, (5, -5), 6) + g2 = Graph.Famous("Franklin") + self.assertTrue(g1.isomorphic(g2)) + self.assertRaises(ValueError, Graph.LCF, 12, (5, -5), -3) + + def testRealizeDegreeSequence(self): + # Test case insensitivity of options too + g = Graph.Realize_Degree_Sequence( + [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", + "largest", + ) + + g = Graph.Realize_Degree_Sequence( + [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", + "index", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == [1, 1]) + + g = Graph.Realize_Degree_Sequence( + [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", + "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 testRealizeBipartiteDegreeSequence(self): + deg1 = [2, 2] + deg2 = [1, 1, 2] + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "simple", + "smallest", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.is_connected()) + self.assertTrue(g.degree() == deg1 + deg2) + + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "simple", + "largest", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == deg1 + deg2) + + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "simple", + "index", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == deg1 + deg2) + + deg1 = [3, 1, 1] + deg2 = [2, 3] + self.assertRaises( + InternalError, + Graph.Realize_Bipartite_Degree_Sequence, + deg1, + deg2, + "simple", + "smallest", + ) + + self.assertRaises( + InternalError, + Graph.Realize_Bipartite_Degree_Sequence, + deg1, + deg2, + "simple", + "index", + ) + + g = Graph.Realize_Bipartite_Degree_Sequence( + deg1, + deg2, + "multi", + "smallest", + ) + self.assertFalse(g.is_directed()) + self.assertTrue(g.is_connected()) + self.assertTrue(g.degree() == deg1 + deg2) + + def testKautz(self): + 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) + + 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) + # 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) + + def testLattice(self): + g = Graph.Lattice([4, 3], circular=False) + self.assertEqual( + sorted(sorted(x) for x in g.get_edgelist()), + [ + [0, 1], + [0, 4], + [1, 2], + [1, 5], + [2, 3], + [2, 6], + [3, 7], + [4, 5], + [4, 8], + [5, 6], + [5, 9], + [6, 7], + [6, 10], + [7, 11], + [8, 9], + [9, 10], + [10, 11], + ], + ) + + g = Graph.Lattice([4, 3], circular=True) + self.assertEqual( + sorted(sorted(x) for x in g.get_edgelist()), + [ + [0, 1], + [0, 3], + [0, 4], + [0, 8], + [1, 2], + [1, 5], + [1, 9], + [2, 3], + [2, 6], + [2, 10], + [3, 7], + [3, 11], + [4, 5], + [4, 7], + [4, 8], + [5, 6], + [5, 9], + [6, 7], + [6, 10], + [7, 11], + [8, 9], + [8, 11], + [9, 10], + [10, 11], + ], + ) + + g = Graph.Lattice([4, 3], circular=(False, 1)) + self.assertEqual( + sorted(sorted(x) for x in g.get_edgelist()), + [ + [0, 1], + [0, 4], + [0, 8], + [1, 2], + [1, 5], + [1, 9], + [2, 3], + [2, 6], + [2, 10], + [3, 7], + [3, 11], + [4, 5], + [4, 8], + [5, 6], + [5, 9], + [6, 7], + [6, 10], + [7, 11], + [8, 9], + [9, 10], + [10, 11], + ], + ) + + def testSBM(self): + pref_matrix = [[0.5, 0, 0], [0, 0, 0.5], [0, 0.5, 0]] + types = [20, 20, 20] + g = Graph.SBM(pref_matrix, types) + + # 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.connected_components().membership) + g2 = g.subgraph(list(range(20, 60))) + self.assertTrue(not any(e.source // 20 == e.target // 20 for e in g2.es)) + + # Check allowed_edge_types argument + g = Graph.SBM(pref_matrix, types, allowed_edge_types="loops") + self.assertFalse(g.is_simple()) + self.assertTrue(sum(g.is_loop()) > 0) + + # Check directedness + g = Graph.SBM(pref_matrix, types, directed=True) + self.assertTrue(g.is_directed()) + self.assertTrue(sum(g.is_mutual()) < g.ecount()) + self.assertTrue(sum(g.is_loop()) == 0) + + # Check error conditions + pref_matrix[0][1] = 0.7 + self.assertRaises(InternalError, Graph.SBM, pref_matrix, types) + + def testTriangularLattice(self): + g = Graph.Triangular_Lattice([2, 2]) + self.assertEqual( + sorted(g.get_edgelist()), [(0, 1), (0, 2), (0, 3), (1, 3), (2, 3)] + ) + + g = Graph.Triangular_Lattice([2, 2], directed=True, mutual=False) + self.assertEqual( + sorted(g.get_edgelist()), [(0, 1), (0, 2), (0, 3), (1, 3), (2, 3)] + ) + + g = Graph.Triangular_Lattice([2, 2], directed=True, mutual=True) + self.assertEqual( + sorted(g.get_edgelist()), + [ + (0, 1), + (0, 2), + (0, 3), + (1, 0), + (1, 3), + (2, 0), + (2, 3), + (3, 0), + (3, 1), + (3, 2), + ], + ) + + @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.assertListEqual( + sorted(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.assertListEqual(sorted(el), [(0, 1), (2, 2), (2, 2)]) + + # ADJ MAX + g = Graph.Adjacency(mat, mode="max") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 3), (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.assertListEqual(sorted(el), [(0, 1), (1, 3), (2, 2), (2, 2)]) + + # ADJ UPPER + g = Graph.Adjacency(mat, mode="upper") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (2, 2), (2, 2)]) + + @unittest.skipIf(np is None, "test case depends on NumPy") + def testAdjacencyNumPyLoopHandling(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.assertListEqual( + sorted(el), [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)] + ) + + # ADJ MIN + g = Graph.Adjacency(mat, mode="min", loops="twice") + el = g.get_edgelist() + self.assertListEqual(sorted(el), [(0, 1), (2, 2)]) + + # ADJ MAX + g = Graph.Adjacency(mat, mode="max", loops="twice") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 3), (2, 2)]) + + # ADJ LOWER + g = Graph.Adjacency(mat, mode="lower", loops="twice") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(sorted(el), [(0, 1), (1, 3), (2, 2), (2, 2)]) + + # ADJ UPPER + g = Graph.Adjacency(mat, mode="upper", loops="twice") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (2, 2), (2, 2)]) + + # ADJ_DIRECTED (default) + g = Graph.Adjacency(mat, loops=False) + el = g.get_edgelist() + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 0), (3, 1)]) + + # ADJ MIN + g = Graph.Adjacency(mat, mode="min", loops=False) + el = g.get_edgelist() + self.assertListEqual(sorted(el), [(0, 1)]) + + # ADJ MAX + g = Graph.Adjacency(mat, mode="max", loops=False) + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(sorted(el), [(0, 1), (0, 2), (1, 3)]) + + # ADJ LOWER + g = Graph.Adjacency(mat, mode="lower", loops=False) + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(sorted(el), [(0, 1), (1, 3)]) + + # ADJ UPPER + g = Graph.Adjacency(mat, mode="upper", loops=False) + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(sorted(el), [(0, 1), (0, 2)]) + + @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(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 MAX + g = Graph.Adjacency(mat, mode="max") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertTrue(el == [(0, 1), (0, 2), (2, 2), (2, 2), (1, 3)]) + + # 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)]) + + # ADJ UPPER + g = Graph.Adjacency(mat, mode="upper") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertTrue(el == [(0, 1), (0, 2), (2, 2), (2, 2)]) + + @unittest.skipIf( + (sparse is None) or (np is None), "test case depends on NumPy/SciPy" + ) + def testSparseAdjacencyLoopHandling(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, 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)]) + + # ADJ MIN + g = Graph.Adjacency(mat, mode="min", loops=False) + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertTrue(el == [(0, 1)]) + + # ADJ LOWER + g = Graph.Adjacency(mat, mode="lower", loops=False) + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertTrue(el == [(0, 1), (1, 3)]) + + # ADJ_DIRECTED (default) + g = Graph.Adjacency(mat, loops="twice") + 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)]) + + # ADJ MAX + g = Graph.Adjacency(mat, mode="max", loops="twice") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertTrue(el == [(0, 1), (0, 2), (2, 2), (1, 3)]) + + # ADJ MIN + g = Graph.Adjacency(mat, mode="min", loops="twice") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertTrue(el == [(0, 1), (2, 2)]) + + # ADJ LOWER + g = Graph.Adjacency(mat, mode="lower", loops="twice") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertTrue(el == [(0, 1), (2, 2), (1, 3)]) + + # ADJ UPPER + g = Graph.Adjacency(mat, mode="upper", loops="twice") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertTrue(el == [(0, 1), (0, 2), (2, 2)]) + + def testWeightedAdjacency(self): + mat = [[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.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) + + g = Graph.Weighted_Adjacency(mat, mode="plus") + el = g.get_edgelist() + self.assertListEqual(el, [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertListEqual(g.es["weight"], [3, 2, 1, 2.5]) + + g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) + el = g.get_edgelist() + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2]) + + g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") + el = g.get_edgelist() + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) + + @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.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) + + g = Graph.Weighted_Adjacency(mat, mode="plus") + el = g.get_edgelist() + self.assertListEqual(el, [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertListEqual(g.es["weight"], [3, 2, 1, 2.5]) + + g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) + el = g.get_edgelist() + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2]) + + g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") + el = g.get_edgelist() + self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)]) + self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5]) + + @unittest.skipIf( + (sparse is None) or (np is None), "test case depends on NumPy/SciPy" + ) + def testSparseWeightedAdjacency(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(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) + self.assertListEqual(g.es["w0"], [1, 2, 2, 2.5, 1]) + + g = Graph.Weighted_Adjacency(mat, mode="plus") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(el, [(0, 1), (0, 2), (2, 2), (1, 3)]) + self.assertListEqual(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()) + self.assertListEqual(el, [(0, 1), (2, 2)]) + self.assertListEqual(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.assertListEqual(el, [(0, 1), (0, 2), (1, 0), (3, 1)]) + self.assertListEqual(g.es["w0"], [1, 2, 2, 1]) + + g = Graph.Weighted_Adjacency(mat, attr="w0", loops="twice") + el = g.get_edgelist() + self.assertTrue(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertListEqual(el, [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) + self.assertListEqual(g.es["w0"], [1, 2, 2, 1.25, 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, 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, use_vids=False) + self.assertListEqual(g.vs["name"], ["A", "B", "C"]) + self.assertListEqual(g.vs["color"], ["blue", "yellow", "blue"]) + self.assertListEqual(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, use_vids=False) + self.assertListEqual(g.vs["name"], [1, 2, 3, 4, 5, 6]) + self.assertListEqual(g.vs["label"], ["1", "2", "3", "4", "5", "6"]) + + # 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) + + # 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) + self.assertTrue(df_edges.equals(g_clone.get_edge_dataframe())) + self.assertTrue(df_vertices.equals(g_clone.get_vertex_dataframe())) + + # pandas Int64 data type + edges = pd.DataFrame(np.array([[0, 1], [1, 1], [1, 2]]), dtype="Int64") + g = Graph.DataFrame(edges) + self.assertTrue(g.vcount() == 3) + + # dataframe with both int data and str data + edges = pd.DataFrame({"source": [1, 2, 2], "target": ["A", "B", "A"]}) + g = Graph.DataFrame(edges, use_vids=False) + self.assertTrue(g.vs["name"] == [1, "A", 2, "B"]) + + # 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) + with self.assertRaisesRegex(ValueError, "negative"): + edges = -pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + 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) + 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) + 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) + 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) + 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, 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, 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) + with self.assertRaisesRegex(ValueError, "null"): + edges = pd.DataFrame(np.array([[0, 1], [1, np.nan], [1, 2]]), dtype="Int64") + Graph.DataFrame(edges) + + def testNearestNeighborGraph(self): + points = [[0, 0], [1, 2], [-3, -3]] + + g = Graph.Nearest_Neighbor_Graph(points) + # expecting 1 - 2, 3 - 1 + self.assertFalse(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertEqual(g.ecount(), 2) + + g = Graph.Nearest_Neighbor_Graph(points, directed=True) + # expecting 1 <-> 2, 3 -> 1 + self.assertTrue(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertEqual(g.ecount(), 3) + + # expecting a complete graph + g = Graph.Nearest_Neighbor_Graph(points, k=2) + self.assertFalse(g.is_directed()) + self.assertEqual(g.vcount(), 3) + self.assertTrue(g.is_complete()) + + +def suite(): + generator_suite = unittest.defaultTestLoader.loadTestsFromTestCase(GeneratorTests) + return unittest.TestSuite([generator_suite]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/igraph/test/homepage.py b/tests/test_homepage.py similarity index 66% rename from igraph/test/homepage.py rename to tests/test_homepage.py index a0e8dad1d..a54cde8c7 100644 --- a/igraph/test/homepage.py +++ b/tests/test_homepage.py @@ -1,6 +1,7 @@ import unittest -from igraph import * +from igraph import Graph, Layout + class HomepageExampleTests(unittest.TestCase): """Smoke tests for the Python examples found on the homepage to ensure @@ -11,40 +12,42 @@ 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 - fr = g.layout("fr") + g.layout("fr") def testKautz(self): g = Graph.Kautz(m=3, n=2) - adj = g.get_adjacency() + g.get_adjacency() # Plotting omitted 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] - mst = g.spanning_tree(weights) + g.es["width"] = [6 - 5 * weight / max_weight for weight in weights] + g.spanning_tree(weights) # Plotting omitted + def suite(): - homepage_example_suite = unittest.makeSuite(HomepageExampleTests) + homepage_example_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + HomepageExampleTests + ) return unittest.TestSuite([homepage_example_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - - diff --git a/igraph/test/indexing.py b/tests/test_indexing.py similarity index 74% rename from igraph/test/indexing.py rename to tests/test_indexing.py index aefa83cea..56f368738 100644 --- a/igraph/test/indexing.py +++ b/tests/test_indexing.py @@ -1,6 +1,7 @@ # vim:ts=4 sw=4 sts=4: import unittest -from igraph import * +from igraph import Graph + class GraphAdjacencyMatrixLikeIndexingTests(unittest.TestCase): def testSingleEdgeRetrieval(self): @@ -8,7 +9,7 @@ def testSingleEdgeRetrieval(self): for v1, v2 in g.get_edgelist(): self.assertEqual(g[v1, v2], 1) self.assertEqual(g[v2, v1], 1) - for v1 in xrange(g.vcount()): + for v1 in range(g.vcount()): for v2 in set(range(g.vcount())) - set(g.neighbors(v1)): self.assertEqual(g[v1, v2], 0) self.assertEqual(g[v2, v1], 0) @@ -18,34 +19,38 @@ 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) - for v1 in xrange(g.vcount()): + for v1 in range(g.vcount()): for v2 in set(range(g.vcount())) - set(g.neighbors(v1)): self.assertEqual(g[v1, v2], 0) self.assertEqual(g[v2, v1], 0) 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) - for v1 in xrange(g.vcount()): + 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) self.assertEqual(g[v2, v1, "value"], 0) def suite(): - adjacency_suite = unittest.makeSuite(GraphAdjacencyMatrixLikeIndexingTests) + adjacency_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + 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 new file mode 100644 index 000000000..7984e9b34 --- /dev/null +++ b/tests/test_isomorphism.py @@ -0,0 +1,461 @@ +import unittest +from random import shuffle + +from igraph import Graph + + +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), + ], + ) + + # 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.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]) + ) + + # 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(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] + ) + ) + + # 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"] = 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] + self.assertTrue(g1.isomorphic_vf2(g2, node_compat_fn=node_compat)) + 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"] = 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.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], + ] + self.assertTrue(sorted(maps) == expected_maps) + + maps[:] = [] + g3 = Graph.Full(4) + 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]] + 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)]) + self.assertTrue(g.count_automorphisms_vf2() == 4) + + # Some more tests with colors + g3 = Graph.Full(4) + 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 + ) + + # Test VF2 with node/edge compatibility function + 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] + self.assertTrue(g3.count_isomorphisms_vf2(node_compat_fn=node_compat) == 2) + 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)]) + 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], + ] + 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 + ) + + +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)] + + self.assertTrue(g.subisomorphic_lad(g2)) + self.assertFalse(g2.subisomorphic_lad(g)) + + # Test 'induced' + self.assertFalse(g3.subisomorphic_lad(g, induced=True)) + self.assertTrue(g3.subisomorphic_lad(g, induced=False)) + self.assertTrue(g3.subisomorphic_lad(g)) + self.assertTrue(g3.subisomorphic_lad(g2, induced=True)) + self.assertTrue(g3.subisomorphic_lad(g2, induced=False)) + 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], + ] + 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], + ] + self.assertTrue(not g.subisomorphic_lad(g2, domains=domains)) + + # Corner cases + empty = Graph() + self.assertTrue(g.subisomorphic_lad(empty)) + 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)] + + 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 \ + 6743 7436 7458 7634 7854 8547 8745" + all_subiso = sorted([int(x) for x in item] for item in all_subiso.split()) + + self.assertEqual(all_subiso, sorted(g.get_subisomorphisms_lad(g2))) + self.assertEqual([], sorted(g2.get_subisomorphisms_lad(g))) + + # 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() + ) + 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], + ] + self.assertEqual([], sorted(g.get_subisomorphisms_lad(g2, domains=domains))) + + # Corner cases + empty = Graph() + self.assertEqual([[]], g.get_subisomorphisms_lad(empty)) + 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)]) + 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] + self.assertTrue(g.subisomorphic_vf2(g2, node_compat_fn=node_compat)) + 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) + 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) + 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 + ) + + # 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 + ) + + +class PermutationTests(unittest.TestCase): + def testCanonicalPermutation(self): + # Simple case: two ring graphs + g1 = Graph(4, [(0, 1), (1, 2), (2, 3), (3, 0)]) + g2 = Graph(4, [(0, 1), (1, 3), (3, 2), (2, 0)]) + + cp = g1.canonical_permutation() + g3 = g1.permute_vertices(cp) + + cp = g2.canonical_permutation() + g4 = g2.permute_vertices(cp) + + self.assertTrue(g3.vcount() == g4.vcount()) + self.assertTrue(sorted(g3.get_edgelist()) == sorted(g4.get_edgelist())) + + # Simple case with coloring + cp = g1.canonical_permutation(color=[0, 0, 1, 1]) + g3 = g1.permute_vertices(cp) + + 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)) + shuffle(perm) + g2 = g.permute_vertices(perm) + self.assertTrue(g.isomorphic(g2)) + g3 = g.permute_vertices(g.canonical_permutation()) + g4 = g2.permute_vertices(g2.canonical_permutation()) + + self.assertTrue(g3.vcount() == g4.vcount()) + 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), + ], + ) + _, mapping, _ = g1.isomorphic_vf2(g2, return_mapping_12=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())) + + +class AutomorphismTests(unittest.TestCase): + def testCountAutomorphisms(self): + g = Graph.Famous("petersen") + self.assertEqual(120, g.count_automorphisms()) + + g = Graph.Lattice([16, 16]) + self.assertEqual(2048, g.count_automorphisms()) + + def testAutomorphismGroup(self): + g = Graph.Famous("petersen") + generators = g.automorphism_group() + generators.sort() + self.assertEqual( + generators, + [ + [0, 1, 2, 7, 5, 4, 6, 3, 9, 8], + [0, 4, 3, 8, 5, 1, 9, 2, 6, 7], + [1, 2, 3, 8, 6, 0, 7, 4, 5, 9], + ], + ) + + +def suite(): + isomorphism_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + IsomorphismTests + ) + subisomorphism_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + SubisomorphismTests + ) + permutation_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + PermutationTests + ) + automorphism_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + AutomorphismTests + ) + return unittest.TestSuite( + [ + isomorphism_suite, + subisomorphism_suite, + permutation_suite, + automorphism_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 new file mode 100644 index 000000000..cc9e5e2a6 --- /dev/null +++ b/tests/test_iterators.py @@ -0,0 +1,73 @@ +import unittest + +from igraph import Graph + + +class IteratorTests(unittest.TestCase): + def testBFS(self): + g = Graph.Tree(10, 2) + vs, layers, ps = g.bfs(0) + self.assertEqual(vs, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + self.assertEqual(ps, [-1, 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, 2, 6, 5, 1, 4, 9, 3, 8, 7]) + self.assertEqual(ps, [-1, 0, 2, 2, 0, 1, 4, 1, 3, 3]) + + 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(): + iterator_suite = unittest.defaultTestLoader.loadTestsFromTestCase(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 new file mode 100644 index 000000000..337f498f8 --- /dev/null +++ b/tests/test_layouts.py @@ -0,0 +1,481 @@ +import unittest +from math import hypot +from igraph import Graph, Layout, BoundingBox, InternalError, align_layout +from igraph import umap_compute_weights + + +class LayoutTests(unittest.TestCase): + def testConstructor(self): + 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) + self.assertEqual(layout.dim, 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)]) + self.assertEqual(len(layout), 4) + self.assertEqual(layout[1], [0, 1, 0]) + self.assertEqual(layout[3], [2, 1, 3]) + + row = layout[2] + row[2] = 1 + self.assertEqual(layout[2], [1, 0, 1]) + + del layout[1] + self.assertEqual(len(layout), 3) + + def testScaling(self): + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0), (2, 1, 3)]) + layout.scale(1.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.scale(1, 1, 3) + 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.scale((2, 2, 1)) + 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)]) + 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, 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)]) + 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.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([]) + self.assertRaises(ValueError, layout.boundaries) + layout = Layout([], dim=3) + 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([]) + self.assertEqual(layout.bounding_box(), BoundingBox(0, 0, 0, 0)) + + def testCenter(self): + 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.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) + 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)) + 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([]) + 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, 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 + + layout = Layout([(1, 2), (3, 4)]) + layout.transform(tr, 2, -1) + self.assertEqual(layout.coords, [[3, 1], [5, 3]]) + + +class LayoutAlgorithmTests(unittest.TestCase): + def testAuto(self): + def layout_test(graph, test_with_dims=(2, 3)): + lo = graph.layout("auto") + self.assertTrue(isinstance(lo, Layout)) + self.assertEqual(len(lo[0]), 2) + for dim in test_with_dims: + lo = graph.layout("auto", dim=dim) + self.assertTrue(isinstance(lo, Layout)) + self.assertEqual(len(lo[0]), dim) + return lo + + g = Graph.Barabasi(10) + layout_test(g) + + g = Graph.GRG(101, 0.2) + del g.vs["x"] + del g.vs["y"] + layout_test(g) + + g = Graph.Full(10) * 2 + layout_test(g) + + g["layout"] = "graphopt" + layout_test(g, test_with_dims=()) + + 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(list(range(20)), list(range(20, 40)))), + ) + + g.vs["z"] = list(range(40, 60)) + lo = layout_test(g) + 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 = 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) + + g = Graph.Ring(8) + layout = g.layout("circle") + test_is_proper_circular_layout(g, g.layout("circle")) + + order = [0, 2, 4, 6, 1, 3, 5, 7] + ordered_layout = g.layout("circle", order=order) + test_is_proper_circular_layout(g, g.layout("circle")) + for v, w in enumerate(order): + self.assertAlmostEqual(layout[v][0], ordered_layout[w][0]) + self.assertAlmostEqual(layout[v][1], ordered_layout[w][1]) + + def testDavidsonHarel(self): + # Quick smoke testing only + g = Graph.Barabasi(100) + lo = g.layout("dh") + self.assertTrue(isinstance(lo, Layout)) + + def testFruchtermanReingold(self): + g = Graph.Barabasi(100) + + lo = g.layout("fr") + self.assertTrue(isinstance(lo, Layout)) + + 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=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 + ) + self.assertTrue(isinstance(lo, Layout)) + bbox = lo.bounding_box() + self.assertTrue(bbox.top >= 2) + self.assertTrue(bbox.bottom <= 3) + self.assertTrue(bbox.left >= 4) + self.assertTrue(bbox.right <= 6) + + def testFruchtermanReingoldGrid(self): + g = Graph.Barabasi(100) + for grid_opt in ["grid", "nogrid", "auto", True, False]: + 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 + ) + + self.assertTrue(isinstance(lo, Layout)) + bbox = lo.bounding_box() + self.assertTrue(bbox.top >= 2) + self.assertTrue(bbox.bottom <= 3) + self.assertTrue(bbox.left >= 4) + self.assertTrue(bbox.right <= 6) + + lo = g.layout( + "kk", + miny=[2] * 100, + maxy=[3] * 100, + minx=[4] * 100, + maxx=[6] * 100, + weights=range(10, g.ecount() + 10), + ) + + self.assertTrue(isinstance(lo, Layout)) + bbox = lo.bounding_box() + self.assertTrue(bbox.top >= 2) + self.assertTrue(bbox.bottom <= 3) + self.assertTrue(bbox.left >= 4) + self.assertTrue(bbox.right <= 6) + + def testMDS(self): + g = Graph.Tree(10, 2) + lo = g.layout("mds") + self.assertTrue(isinstance(lo, Layout)) + + dists = g.distances() + lo = g.layout("mds", dists) + self.assertTrue(isinstance(lo, Layout)) + + g += Graph.Tree(10, 2) + lo = g.layout("mds") + self.assertTrue(isinstance(lo, Layout)) + + def testUMAP(self): + g = Graph() + + self.assertRaises( + InternalError, + g.layout_umap, + min_dist=-0.01, + ) + + self.assertRaises( + ValueError, + g.layout_umap, + epochs=-1, + ) + + self.assertRaises( + ValueError, + g.layout_umap, + dim=1, + ) + + # Empty graph + lo = g.layout_umap() + self.assertTrue(isinstance(lo, Layout)) + self.assertEqual(lo.coords, []) + + # Singleton graph + g = Graph(n=1) + lo = g.layout_umap() + self.assertEqual(lo.coords, [[0, 0]]) + + # Graph with two articulation points + edges = [ + 0, + 1, + 0, + 2, + 0, + 3, + 1, + 2, + 1, + 3, + 2, + 3, + 3, + 4, + 4, + 5, + 5, + 6, + 6, + 7, + 7, + 8, + 6, + 8, + 7, + 9, + 6, + 9, + 8, + 9, + 7, + 10, + 8, + 10, + 9, + 10, + 10, + 11, + 9, + 11, + 8, + 11, + 7, + 11, + ] + edges = list(zip(edges[::2], edges[1::2])) + dist = [ + 0.1, + 0.09, + 0.12, + 0.09, + 0.1, + 0.1, + 0.9, + 0.9, + 0.9, + 0.2, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.08, + 0.05, + 0.1, + 0.08, + 0.12, + 0.09, + 0.11, + ] + g = Graph(edges) + lo = g.layout_umap(dist=dist, epochs=500) + self.assertTrue(isinstance(lo, Layout)) + + # One should get two clusters in this case + x, y = list(zip(*lo.coords)) + xmax, ymax, xmin, ymin = max(x), max(y), min(x), min(y) + distmax = max(xmax - xmin, ymax - ymin) + for iclu in range(0, 8, 7): + xclu = sum(x[iclu : iclu + 4]) / 4 + yclu = sum(y[iclu : iclu + 4]) / 4 + for i in range(4): + dx = x[iclu + i] - xclu + dy = y[iclu + i] - yclu + dxy = hypot(dx, dy) + # Distance from each cluster's center should be relatively small + self.assertLess(dxy, 0.2 * distmax) + + # Test single epoch with seed + lo_adj = g.layout_umap(dist=dist, epochs=1, seed=lo) + self.assertTrue(isinstance(lo_adj, Layout)) + + # Same but inputting the coordinates + lo_adj = g.layout_umap(dist=dist, epochs=1, seed=lo.coords) + self.assertTrue(isinstance(lo_adj, Layout)) + + def testUMAPComputeWeights(self): + edges = [0, 1, 0, 2, 1, 2, 1, 3, 2, 3, 2, 0] + edges = list(zip(edges[::2], edges[1::2])) + dist = [1, 1.5, 1.8, 2.0, 3.4, 0.5] + # NOTE: you need a directed graph to make sense of the symmetryzation + g = Graph(edges, directed=True) + weights = umap_compute_weights(g, dist) + self.assertEqual( + weights, [1.0, 1.0, 1.0, 1.1253517471925912e-07, 6.14421235332821e-06, 0.0] + ) + + def testLGL(self): + g = Graph.Barabasi(100) + lo = g.layout("lgl") + self.assertTrue(isinstance(lo, Layout)) + + lo = g.layout("lgl", root=5) + self.assertTrue(isinstance(lo, Layout)) + + def testReingoldTilford(self): + g = Graph.Barabasi(100) + lo = g.layout("rt") + ys = [coord[1] for coord in lo] + root = ys.index(0.0) + self.assertEqual(ys, g.distances(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) + lo = g.layout("rt", root=[0, 100], rootlevel=[2, 10]) + self.assertEqual(lo[100][1] - lo[0][1], 8) + + # test named vertices + g.vs["name"] = [f"v{i}" for i in range(g.vcount())] + lo = g.layout("rt", root=["v0", "v100"]) + self.assertEqual(lo[100][1] - lo[0][1], 0) + + def testBipartite(self): + g = Graph.Full_Bipartite(3, 2) + + lo = g.layout("bipartite") + ys = [coord[1] for coord in lo] + self.assertEqual([1, 1, 1, 0, 0], ys) + + lo = g.layout("bipartite", vgap=3) + ys = [coord[1] for coord in lo] + self.assertEqual([3, 3, 3, 0, 0], ys) + + lo = g.layout("bipartite", hgap=5) + self.assertEqual({0, 5, 10}, {coord[0] for coord in lo if coord[1] == 1}) + self.assertEqual({2.5, 7.5}, {coord[0] for coord in lo if coord[1] == 0}) + + def testDRL(self): + # Regression test for bug #1091891 + g = Graph.Ring(10, circular=False) + 1 + lo = g.layout("drl") + self.assertTrue(isinstance(lo, Layout)) + + def testAlign(self): + g = Graph.Ring(3, circular=False) + lo = Layout([[1,1], [2,2], [3,3]]) + lo = align_layout(g, lo) + self.assertTrue(isinstance(lo, Layout)) + self.assertTrue(all(abs(lo[i][1]) < 1e-10 for i in range(3))) + + +def suite(): + layout_suite = unittest.defaultTestLoader.loadTestsFromTestCase(LayoutTests) + layout_algorithm_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + LayoutAlgorithmTests + ) + return unittest.TestSuite([layout_suite, layout_algorithm_suite]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/igraph/test/matching.py b/tests/test_matching.py similarity index 63% rename from igraph/test/matching.py rename to tests/test_matching.py index 3ac4a7eff..f6410cb2c 100644 --- a/igraph/test/matching.py +++ b/tests/test_matching.py @@ -1,6 +1,7 @@ import unittest -from igraph import * +from igraph import Graph, InternalError, Matching + def powerset(iterable): items_powers = [(item, 1 << i) for i, item in enumerate(iterable)] @@ -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,23 +80,26 @@ 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(ValueError, g.maximum_bipartite_matching, - types=[0,1,1,1]) + self.assertRaises( + InternalError, g.maximum_bipartite_matching, types=[0, 1, 1, 1] + ) def suite(): - matching_suite = unittest.makeSuite(MatchingTests) - bipartite_unweighted_suite = unittest.makeSuite(MaximumBipartiteMatchingTests) + matching_suite = unittest.defaultTestLoader.loadTestsFromTestCase(MatchingTests) + bipartite_unweighted_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + 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_motifs.py b/tests/test_motifs.py new file mode 100644 index 000000000..d0f9131fa --- /dev/null +++ b/tests/test_motifs.py @@ -0,0 +1,68 @@ +import unittest + +from igraph import Graph + + +class MotifTests(unittest.TestCase): + 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 + dc = self.g.dyad_census() + accessors = ["mut", "mutual", "asym", "asymm", "asymmetric", "null"] + for a in accessors: + self.assertTrue(isinstance(getattr(dc, a), int)) + self.assertTrue(isinstance(dc[a], int)) + self.assertTrue(isinstance(list(dc), list)) + self.assertTrue(isinstance(tuple(dc), tuple)) + self.assertTrue(len(list(dc)) == 3) + 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 + 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(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 TrianglesTests(unittest.TestCase): + def testListTriangles(self): + g = Graph.Famous("petersen") + self.assertEqual([], g.list_triangles()) + + g = Graph([(0, 1), (1, 2), (2, 0), (1, 3), (3, 2), (4, 2), (4, 3)]) + observed = g.list_triangles() + self.assertTrue(all(isinstance(x, tuple) for x in observed)) + observed = sorted(sorted(tri) for tri in observed) + expected = [[0, 1, 2], [1, 2, 3], [2, 3, 4]] + self.assertEqual(observed, expected) + + g = Graph.GRG(100, 0.2) + tri = Graph.Full(3) + observed = sorted(sorted(tri) for tri in g.list_triangles()) + expected = sorted(x for x in g.get_subisomorphisms_vf2(tri) if x == sorted(x)) + self.assertEqual(observed, expected) + + +def suite(): + motif_suite = unittest.defaultTestLoader.loadTestsFromTestCase(MotifTests) + triangles_suite = unittest.defaultTestLoader.loadTestsFromTestCase(TrianglesTests) + return unittest.TestSuite([motif_suite, triangles_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 new file mode 100644 index 000000000..523181311 --- /dev/null +++ b/tests/test_operators.py @@ -0,0 +1,549 @@ +import unittest + +from igraph import Graph, disjoint_union, intersection, union + +try: + import numpy as np +except ImportError: + 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 + and g.connected_components().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 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 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) + 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 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) + + def testUnionNoGraphs(self): + self.assertRaises(ValueError, union, []) + + 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])] + 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) == {"A", "B"}: + self.assertTrue(e["attr"] == "set") + else: + self.assertTrue(e["attr"] == "set_too") + + 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 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]) == {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 + + # Adding vertices + g += 2 + self.assertTrue( + g.vcount() == 5 + and g.ecount() == 3 + and g.connected_components().membership == [0, 0, 0, 1, 2] + ) + + # Adding a vertex by name + g += "spam" + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 3 + and g.connected_components().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.connected_components().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.connected_components().membership == [0] * 6 + ) + + # Adding two more vertices + g += ["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 + # could be moved to the end + self.assertTrue(id(g) == id(orig)) + + # Adding another graph + g += Graph.Full(3) + self.assertTrue( + g.vcount() == 11 + and g.ecount() == 10 + and g.connected_components().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.connected_components().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.connected_components().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.connected_components().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.connected_components().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.connected_components().membership == [0] * 6 + ) + g0 = g + + # Adding another graph + g = g0 + Graph.Full(3) + self.assertTrue( + g.vcount() == 9 + and g.ecount() == 10 + and g.connected_components().membership == [0, 0, 0, 0, 0, 0, 1, 1, 1] + ) + + def testInPlaceSubtraction(self): + g = Graph.Full(8) + orig = g + + # Deleting a vertex by vertex selector + g -= 7 + self.assertTrue( + g.vcount() == 7 + and g.ecount() == 21 + and g.connected_components().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.connected_components().membership == [0, 0, 0, 0, 0, 0] + ) + + # Deleting two vertices + g -= [4, 5] + self.assertTrue( + g.vcount() == 4 + and g.ecount() == 6 + and g.connected_components().membership == [0, 0, 0, 0] + ) + + # Deleting an edge + g -= (1, 2) + self.assertTrue( + g.vcount() == 4 + and g.ecount() == 5 + and g.connected_components().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.connected_components().membership == [0, 0, 1, 1] + ) + + # Did we really use the original graph so far? + self.assertTrue(id(g) == id(orig)) + + # Subtracting a graph + g2 = Graph.Tree(3, 2) + g -= g2 + self.assertTrue( + g.vcount() == 4 + and g.ecount() == 1 + and g.connected_components().membership == [0, 1, 2, 2] + ) + + def testNonzero(self): + self.assertTrue(Graph(1)) + self.assertFalse(Graph(0)) + + def testLength(self): + self.assertRaises(TypeError, len, Graph(15)) + self.assertTrue(len(Graph(15).vs) == 15) + 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)] + g = Graph(el) + g.es["weight"] = [1, 2, 3, 4, 5, 6, 7] + + g2 = g.copy() + g2.simplify() + self.assertTrue(g2.vcount() == g.vcount()) + self.assertTrue(g2.ecount() == 3) + + g2 = g.copy() + g2.simplify(loops=False) + self.assertTrue(g2.vcount() == g.vcount()) + self.assertTrue(g2.ecount() == 4) + + g2 = g.copy() + g2.simplify(multiple=False) + self.assertTrue(g2.vcount() == g.vcount()) + self.assertTrue(g2.ecount() == g.ecount() - 1) + + def testContractVertices(self): + g = Graph.Full(4) + Graph.Full(4) + [(0, 5), (1, 4)] + + g2 = g.copy() + 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), + ], + ) + + 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), + ], + ) + + g2 = Graph(10) + g2.contract_vertices([0, 0, 1, 1, 2, 2, 3, 3, 4, 4]) + self.assertEqual(g2.vcount(), 5) + self.assertEqual(g2.ecount(), 0) + + @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() + 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), + ], + ) + + def testReverseEdges(self): + g = Graph.Tree(10, 3, mode="out") + g.reverse_edges([0, 1, 2]) + self.assertEqual( + g.get_edgelist(), + [(1, 0), (2, 0), (3, 0), (1, 4), (1, 5), (1, 6), (2, 7), (2, 8), (2, 9)], + ) + + g = Graph.Tree(13, 3, mode="in") + g.reverse_edges() + self.assertTrue(g.isomorphic(Graph.Tree(13, 3, mode="out"))) + + +def suite(): + operator_suite = unittest.defaultTestLoader.loadTestsFromTestCase(OperatorTests) + return unittest.TestSuite([operator_suite]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/igraph/test/rng.py b/tests/test_rng.py similarity index 82% rename from igraph/test/rng.py rename to tests/test_rng.py index 29ca51492..23055ada8 100644 --- a/igraph/test/rng.py +++ b/tests/test_rng.py @@ -1,9 +1,10 @@ import random import unittest -from igraph import * +from igraph import Graph, set_random_number_generator -class FakeRNG(object): + +class FakeRNG: @staticmethod def random(): return 0.1 @@ -16,7 +17,8 @@ def randint(a, b): def gauss(mu, sigma): return 0.3 -class InvalidRNG(object): + +class InvalidRNG: pass @@ -30,8 +32,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() @@ -42,13 +43,16 @@ def testSeeding(self): def suite(): - random_suite = unittest.makeSuite(RandomNumberGeneratorTests) + random_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + RandomNumberGeneratorTests + ) return unittest.TestSuite([random_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/igraph/test/separators.py b/tests/test_separators.py similarity index 74% rename from igraph/test/separators.py rename to tests/test_separators.py index 62094f2b4..98a226b2b 100644 --- a/igraph/test/separators.py +++ b/tests/test_separators.py @@ -1,6 +1,7 @@ import unittest -from igraph import * +from igraph import Graph, InternalError + def powerset(iterable): items_powers = [(item, 1 << i) for i, item in enumerate(iterable)] @@ -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,8 +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(list(range(32)))) - self.assertRaises(InternalError, g.is_separator, range(32)) + self.assertRaises(InternalError, g.is_separator, list(range(33))) def testIsMinimalSeparator(self): g = Graph.Lattice([8, 4], circular=False) @@ -29,13 +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(list(range(32)))) - self.assertRaises(InternalError, g.is_minimal_separator, range(32)) + 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())): + min_st_seps = {tuple(x) for x in g.all_minimal_st_separators()} + for vs in powerset(list(range(g.vcount()))): if vs in min_st_seps: self.assertTrue(g.is_minimal_separator(vs)) else: @@ -43,25 +47,29 @@ def testAllMinimalSTSeparators(self): def testMinimumSizeSeparators(self): g = Graph.Famous("zachary") - min_st_seps = set(tuple(x) for x in g.all_minimal_st_separators()) + min_st_seps = {tuple(x) for x in g.all_minimal_st_separators()} min_size_seps = [tuple(x) for x in g.minimum_size_separators()] self.assertTrue(set(min_size_seps).issubset(min_st_seps)) self.assertTrue(len(set(min_size_seps)) == len(min_size_seps)) 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) + is_separator_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + 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 new file mode 100644 index 000000000..a415b9470 --- /dev/null +++ b/tests/test_spectral.py @@ -0,0 +1,109 @@ +# vim:set ts=4 sw=4 sts=4 et: +import unittest + +from igraph import Graph + + +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 testLaplacian(self): + 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]], + ) + self.assertAlmostEqualMatrix( + g.laplacian(normalized="symmetric"), + [[1, -0.5, -0.5], [-0.5, 1, -0.5], [-0.5, -0.5, 1]], + ) + self.assertAlmostEqualMatrix( + g.laplacian(normalized="left"), + [[1, -0.5, -0.5], [-0.5, 1, -0.5], [-0.5, -0.5, 1]], + ) + self.assertAlmostEqualMatrix( + g.laplacian(normalized="right"), + [[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.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], + ] + ) + + g = Graph.Formula("A --> B --> C --> D --> E --> A, A --> C") + self.assertAlmostEqualMatrix( + g.laplacian(mode="out"), + [ + [2, -1, -1, 0, 0], + [0, 1, -1, 0, 0], + [0, 0, 1, -1, 0], + [0, 0, 0, 1, -1], + [-1, 0, 0, 0, 1], + ], + ) + self.assertAlmostEqualMatrix( + g.laplacian(mode="in"), + [ + [1, -1, -1, 0, 0], + [0, 1, -1, 0, 0], + [0, 0, 2, -1, 0], + [0, 0, 0, 1, -1], + [-1, 0, 0, 0, 1], + ], + ) + self.assertAlmostEqualMatrix( + g.laplacian(mode="out", normalized="left"), + [ + [1, -0.5, -0.5, 0, 0], + [0, 1, -1, 0, 0], + [0, 0, 1, -1, 0], + [0, 0, 0, 1, -1], + [-1, 0, 0, 0, 1], + ], + ) + self.assertAlmostEqualMatrix( + g.laplacian(mode="in", normalized="right"), + [ + [1, -1, -0.5, 0, 0], + [0, 1, -0.5, 0, 0], + [0, 0, 1, -1, 0], + [0, 0, 0, 1, -1], + [-1, 0, 0, 0, 1], + ], + ) + + +def suite(): + spectral_suite = unittest.defaultTestLoader.loadTestsFromTestCase(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 new file mode 100644 index 000000000..c0f1bdb59 --- /dev/null +++ b/tests/test_structural.py @@ -0,0 +1,1217 @@ +import math +import unittest +import warnings + +from igraph import Graph, InternalError, IN, OUT, ALL, TREE_IN +from math import inf, isnan + + +class SimplePropertiesTests(unittest.TestCase): + 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 + ) + 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) + + def testMeanDegree(self): + self.assertEqual(9.0, self.gfull.mean_degree()) + self.assertEqual(0.0, self.gempty.mean_degree()) + self.assertEqual(2.5, self.g.mean_degree()) + self.assertEqual(7 / 4, self.gdir.mean_degree()) + self.assertAlmostEqual(13 / 7, self.tree.mean_degree(), places=5) + + def testDiameter(self): + self.assertTrue(self.gfull.diameter() == 1) + 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) + self.assertTrue(self.tree.diameter() == 5) + + 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, inf)) + + d = self.tree.get_diameter() + self.assertTrue(d[0] == 13 or d[-1] == 13) + + weights = [1, 1, 1, 5, 1, 5, 1, 1, 1, 1, 1, 1, 5] + self.assertTrue(self.tree.diameter(weights=weights) == 15) + + d = self.tree.farthest_points(weights=weights) + 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.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(Graph().eccentricity(), []) + + def testWeightedEccentricity(self): + self.assertEqual( + self.gfull.eccentricity(weights=[2] * self.gfull.ecount()), + [2] * self.gfull.vcount(), + ) + self.assertEqual( + self.gempty.eccentricity(weights=[]), [0] * self.gempty.vcount() + ) + self.assertEqual( + self.g.eccentricity(weights=range(1, self.g.ecount() + 1)), [4, 5, 6, 6] + ) + self.assertEqual(Graph().eccentricity(), []) + + def testRadius(self): + self.assertEqual(self.gfull.radius(), 1) + self.assertEqual(self.gempty.radius(), 0) + self.assertEqual(self.g.radius(), 1) + self.assertEqual(self.gdir.radius(), 1) + self.assertEqual(self.tree.radius(), 3) + self.assertTrue(isnan(Graph().radius())) + + def testWeightedRadius(self): + self.assertEqual(self.gfull.radius(weights=[2] * self.gfull.ecount()), 2) + self.assertEqual(self.gempty.radius(weights=[]), 0) + self.assertEqual(self.g.radius(weights=range(1, self.g.ecount() + 1)), 4) + self.assertTrue(isnan(Graph().radius())) + + def testTransitivity(self): + self.assertTrue(self.gfull.transitivity_undirected() == 1.0) + self.assertTrue(self.tree.transitivity_undirected() == 0.0) + 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() + ) + + 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] + self.assertAlmostEqual( + 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.0, places=4 + ) + + def testModularity(self): + 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 + self.assertAlmostEqual(g.modularity(cl, ws), 0.4523, places=3) + ws = [2] * 21 + self.assertAlmostEqual(g.modularity(cl, ws), 0.4523, places=3) + 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) + 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 + ) + tree = Graph.Tree(10, 3) + + def testKnn(self): + knn, knnk = self.gfull.knn() + self.assertTrue(knn == [9.0] * 10) + self.assertAlmostEqual(knnk[8], 9.0, places=6) + + g = self.g.copy() + g.simplify() + + knn, knnk = g.knn() + 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.0, places=6) + + def testKnnNonSimple(self): + knn, knnk = self.gfull.knn() + self.assertTrue(knn == [9.0] * 10) + self.assertAlmostEqual(knnk[8], 9.0, places=6) + + # knn works for non-simple graphs as well + knn, knnk = self.g.knn() + diff = max(abs(a - b) for a, b in zip(knn, [17 / 5.0, 3, 4, 4])) + self.assertAlmostEqual(diff, 0.0, places=6) + self.assertEqual(len(knnk), 5) + self.assertAlmostEqual(knnk[1], 4, places=6) + self.assertAlmostEqual(knnk[2], 3, places=6) + self.assertAlmostEqual(knnk[4], 3.4, places=6) + + def testDegree(self): + self.assertTrue(self.gfull.degree() == [9] * 10) + self.assertTrue(self.gempty.degree() == [0] * 10) + self.assertTrue(self.g.degree(loops=False) == [3, 3, 2, 2]) + self.assertTrue(self.g.degree() == [5, 3, 2, 2]) + self.assertTrue(self.gdir.degree(mode=IN) == [1, 2, 2, 2]) + self.assertTrue(self.gdir.degree(mode=OUT) == [3, 2, 1, 1]) + self.assertTrue(self.gdir.degree(mode=ALL) == [4, 4, 3, 3]) + vs = self.gdir.vs.select(0, 2) + self.assertTrue(self.gdir.degree(vs, mode=ALL) == [4, 3]) + self.assertTrue(self.gdir.degree(self.gdir.vs[1], mode=ALL) == 4) + + def testMaxDegree(self): + self.assertTrue(self.gfull.maxdegree() == 9) + self.assertTrue(self.gempty.maxdegree() == 0) + self.assertTrue(self.g.maxdegree() == 3) + self.assertTrue(self.g.maxdegree(loops=True) == 5) + self.assertTrue(self.g.maxdegree([1, 2], loops=True) == 3) + self.assertTrue(self.gdir.maxdegree(mode=IN) == 2) + self.assertTrue(self.gdir.maxdegree(mode=OUT) == 3) + self.assertTrue(self.gdir.maxdegree(mode=ALL) == 4) + + def testStrength(self): + # Turn off warnings about calling strength without weights + import warnings + + warnings.filterwarnings( + "ignore", "No edge weights for strength calculation", RuntimeWarning + ) + + # No weights + self.assertTrue(self.gfull.strength() == [9] * 10) + self.assertTrue(self.gempty.strength() == [0] * 10) + self.assertTrue(self.g.degree(loops=False) == [3, 3, 2, 2]) + 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) == [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]) + 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) + + +class LocalTransitivityTests(unittest.TestCase): + def testLocalTransitivityFull(self): + trans = Graph.Full(10).transitivity_local_undirected() + self.assertTrue(trans == [1.0] * 10) + + def testLocalTransitivityTree(self): + trans = Graph.Tree(10, 3).transitivity_local_undirected() + self.assertTrue(trans[0:3] == [0.0, 0.0, 0.0]) + + def testLocalTransitivityHalf(self): + g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) + trans = g.transitivity_local_undirected() + trans = [round(x, 3) for x in trans] + self.assertTrue(trans == [0.667, 0.667, 1.0, 1.0]) + + def testLocalTransitivityPartial(self): + g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) + 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)]) + g4 = Graph.Full(2) + g5 = Graph.Full(1) + + def testBiconnectedComponents(self): + s = self.g1.biconnected_components() + 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))) + + s = self.g3.biconnected_components() + 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] + ) + + def testArticulationPoints(self): + self.assertTrue(self.g1.articulation_points() == []) + self.assertTrue(self.g2.cut_vertices() == [1, 2, 3]) + self.assertTrue(self.g3.articulation_points() == [2]) + self.assertTrue(self.g4.articulation_points() == []) + + def testIsBiconnected(self): + self.assertTrue(self.g1.is_biconnected()) + self.assertFalse(self.g2.is_biconnected()) + self.assertFalse(self.g3.is_biconnected()) + self.assertTrue(self.g4.is_biconnected()) + self.assertFalse(self.g5.is_biconnected()) + + +class CentralityTests(unittest.TestCase): + def testBetweennessCentrality(self): + g = Graph.Star(5) + 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.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] + ) + + observed = g.betweenness(sources=[0, 8], targets=[0, 8]) + self.assertEqual(len(observed), g.vcount()) + for x, y in zip( + observed, [0, 1 / 2, 1 / 6, 1 / 2, 2 / 3, 1 / 2, 1 / 6, 1 / 2, 0] + ): + self.assertAlmostEqual(x, y) + self.assertRaises( + ValueError, g.betweenness, cutoff=2, sources=[0, 8], targets=[0, 8] + ) + + def testEdgeBetweennessCentrality(self): + g = Graph.Star(5) + 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.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.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] + ) + + g = Graph.Lattice([3, 3], circular=False) + observed = g.edge_betweenness(sources=[0, 8], targets=[0, 8]) + self.assertEqual(len(observed), g.ecount()) + for x, y in zip( + observed, + [ + 1 / 2, + 1 / 2, + 1 / 6, + 1 / 3, + 1 / 6, + 1 / 3, + 1 / 6, + 1 / 3, + 1 / 3, + 1 / 2, + 1 / 6, + 1 / 2, + ], + ): + self.assertAlmostEqual(x, y) + self.assertRaises( + ValueError, g.edge_betweenness, cutoff=2, sources=[0, 8], targets=[0, 8] + ) + + def testClosenessCentrality(self): + g = Graph.Star(5) + cl = g.closeness() + 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) + + g = Graph.Star(5) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cl = g.closeness(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.closeness(weights=weights) + 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) + + g = Graph.Star(5) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cl = g.closeness(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) + + # 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, + ] + 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, + ] + 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, 0.25, 0.25, 0.25, 0.25] + 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, 0.25, 0.25, 0.25, 0.25] + for idx in range(g.vcount()): + self.assertAlmostEqual(cl[idx], cl2[idx], places=3) + + def testPageRank(self): + g = Graph.Star(11) + cent = g.pagerank() + self.assertTrue(cent.index(max(cent)) == 0) + self.assertAlmostEqual(max(cent), 0.4668, places=3) + + 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.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) + + def testEigenvectorCentrality(self): + g = Graph.Star(11) + cent = g.evcent() + self.assertTrue(cent.index(max(cent)) == 0) + 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] + self.assertTrue(cent.index(max(cent)) == 0) + self.assertAlmostEqual(cent[1] / cent[0], 0.3162, places=3) + self.assertAlmostEqual(ev, 3.162, places=3) + + def testAuthorityScore(self): + g = Graph.Tree(15, 2, TREE_IN) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + asc = g.authority_score() + self.assertAlmostEqual(max(asc), 1.0, places=3) + + # Smoke testing + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + g.authority_score(scale=False, return_eigenvalue=True) + + def testHubScore(self): + g = Graph.Tree(15, 2, TREE_IN) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + hsc = g.hub_score() + self.assertAlmostEqual(max(hsc), 1.0, places=3) + + # Smoke testing + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + 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)] + self.assertEqual(g.coreness("all"), [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], + ] + ) + + 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] + ) + + +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]) + g = Graph(7, [(0, 1), (1, 2), (2, 0), (1, 6), (1, 3), (1, 4), (3, 5), (4, 5)]) + self.assertEqual(g.bridges(), [3]) + 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 + + def testTopologicalSorting(self): + g = Graph(5, [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)], directed=True) + self.assertTrue(g.topological_sorting() == [0, 4, 1, 2, 3]) + self.assertTrue(g.topological_sorting(IN) == [3, 4, 2, 1, 0]) + g.to_undirected() + self.assertRaises(InternalError, g.topological_sorting) + + 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 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() + el.sort() + 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() + 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() + + 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 testDistances(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.es["weight"] = ws + 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], + ] + self.assertTrue(g.distances(weights=ws) == expected) + self.assertTrue(g.distances(weights="weight") == expected) + self.assertTrue( + g.distances(weights="weight", target=[2, 3]) + == [row[2:4] for row in expected] + ) + self.assertTrue( + g.distances(weights="weight", target=[2, 3], algorithm="bellman_ford") + == [row[2:4] for row in expected] + ) + self.assertTrue( + g.distances(weights="weight", target=[2, 3], algorithm="johnson") + == [row[2:4] for row in expected] + ) + + def testGetShortestPath(self): + g = Graph(4, [(0, 1), (0, 2), (1, 3), (3, 2), (2, 1)], directed=True) + self.assertEqual([0, 1, 3], g.get_shortest_path(0, 3)) + self.assertEqual([0, 1, 3], g.get_shortest_path(0, 3, output="vpath")) + self.assertEqual([0, 2], g.get_shortest_path(0, 3, output="epath")) + self.assertRaises(ValueError, g.get_shortest_path, 0, 3, output="x") + self.assertRaises(TypeError, g.get_shortest_path, 0) + + def testGetShortestPathManualAlgorithmSelection(self): + g = Graph(4, [(0, 1), (0, 2), (1, 3), (3, 2), (2, 1)], directed=True) + g.es["weight"] = [1] * g.ecount() + + self.assertEqual([0, 1, 3], g.get_shortest_path(0, 3, algorithm="bellman_ford")) + self.assertRaises(ValueError, g.get_shortest_path, 0, 3, algorithm="johnson") + self.assertRaises( + ValueError, g.get_shortest_path, 0, 3, algorithm="johnson", mode="in" + ) + + def testGetShortestPaths(self): + 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) + sps = g.get_shortest_paths(0, output="vpath") + expected = [[0], [0, 1], [0, 2], [0, 1, 3]] + self.assertTrue(sps == expected) + sps = g.get_shortest_paths(0, output="epath") + expected = [[], [0], [1], [0, 2]] + self.assertTrue(sps == expected) + 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) + + sps = sorted(g.get_all_shortest_paths(0, 0)) + expected = [[0]] + self.assertEqual(expected, sps) + + sps = sorted(g.get_all_shortest_paths(0, 5)) + expected = [[0, 1, 2, 4, 5], [0, 1, 3, 4, 5]] + self.assertEqual(expected, sps) + + sps = sorted(g.get_all_shortest_paths(1, 4)) + expected = [[1, 2, 4], [1, 3, 4]] + self.assertEqual(expected, sps) + + 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], + ] + 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], + ] + 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.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) + + g = Graph.Lattice([4, 4], circular=False) + g.es["weight"] = 1 + 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)) + self.assertEqual(4, sum(1 for path in sps if path[-1] == 12)) + self.assertEqual(12, sum(1 for path in sps if path[-1] == 15)) + + def testGetKShortestPaths(self): + g = Graph(4, [(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)], directed=True) + + sps = sorted(g.get_k_shortest_paths(0, 0)) + expected = [[0]] + self.assertEqual(expected, sps) + + sps = sorted(g.get_k_shortest_paths(0, 5, 2)) + expected = [[0, 1, 2, 4, 5], [0, 1, 3, 4, 5]] + self.assertEqual(expected, sps) + + sps = sorted(g.get_k_shortest_paths(1, 4, 2)) + expected = [[1, 2, 4], [1, 3, 4]] + self.assertEqual(expected, sps) + + g = Graph.Lattice([5, 5], circular=False) + + sps = sorted(g.get_k_shortest_paths(0, 12, 6)) + 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_k_shortest_paths(0, 202, 6)) + 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([(0, 1), (1, 2), (0, 2)]) + g.es["weight"] = [0.5, 0.5, 1] + sps = sorted(g.get_k_shortest_paths(0, 2, 2, weights="weight")) + self.assertEqual(sorted([[0, 2], [0, 1, 2]]), sorted(sps)) + + 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, + ) + + 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) + 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) + 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, + ) + + g = Graph.Lattice([4, 4], circular=False) + g = Graph([(min(u, v), max(u, v)) for u, v in g.get_edgelist()], directed=True) + sps = sorted(g.get_all_simple_paths(0, 15)) + self.assertEqual(20, len(sps)) + for path in sps: + self.assertEqual(0, path[0]) + self.assertEqual(15, path[-1]) + curr = path[0] + for next in path[1:]: + self.assertTrue(g.are_adjacent(curr, next)) + curr = next + + def testPathLengthHist(self): + g = Graph.Tree(15, 2) + h = g.path_length_hist() + self.assertTrue(h.unconnected == 0) + self.assertTrue( + [(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) + h = g.path_length_hist() + self.assertTrue(h.unconnected == 20) + g.to_directed() + h = g.path_length_hist() + self.assertTrue(h.unconnected == 40) + h = g.path_length_hist(False) + self.assertTrue(h.unconnected == 20) + + def testGetShortestPathsAStar(self): + n = 4 + g = Graph.Lattice((n, n), circular=False) + xs, ys = list(range(n)) * n, sum(([i] * n for i in range(n)), []) + g.vs["coord"] = list(zip(xs, ys)) + + def heuristics(graph, u, v): + ux, uy = graph.vs[u]["coord"] + vx, vy = graph.vs[v]["coord"] + return ((ux - vx) ** 2 + (uy - vy) ** 2) ** 0.5 + + self.assertEqual( + [0, 1, 5, 6, 10, 11], g.get_shortest_path_astar(0, 11, heuristics) + ) + + +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)): + if math.isnan(a) and math.isnan(b): + continue + elif a == b: + continue + else: + return False + return True + + 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] + 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, + ) + 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"), + ] + r = g.dominator(0, mode=OUT) + self.assertTrue(self.compareDomTrees(s, r)) + + +def suite(): + simple_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + SimplePropertiesTests + ) + degree_suite = unittest.defaultTestLoader.loadTestsFromTestCase(DegreeTests) + local_transitivity_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + LocalTransitivityTests + ) + biconnected_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + BiconnectedComponentTests + ) + centrality_suite = unittest.defaultTestLoader.loadTestsFromTestCase(CentralityTests) + neighborhood_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + NeighborhoodTests + ) + path_suite = unittest.defaultTestLoader.loadTestsFromTestCase(PathTests) + misc_suite = unittest.defaultTestLoader.loadTestsFromTestCase(MiscTests) + dominator_suite = unittest.defaultTestLoader.loadTestsFromTestCase(DominatorTests) + 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 new file mode 100644 index 000000000..1c782adbd --- /dev/null +++ b/tests/test_unicode_issues.py @@ -0,0 +1,27 @@ +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}) + 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.defaultTestLoader.loadTestsFromTestCase(UnicodeTests) + return unittest.TestSuite([generator_suite]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/igraph/test/vertexseq.py b/tests/test_vertexseq.py similarity index 60% rename from igraph/test/vertexseq.py rename to tests/test_vertexseq.py index bf7046f8f..57154d4e2 100644 --- a/igraph/test/vertexseq.py +++ b/tests/test_vertexseq.py @@ -1,14 +1,17 @@ # vim:ts=4 sw=4 sts=4: import unittest -from igraph import * -from igraph.test.utils import is_pypy, skipIf + +from igraph import Graph, Vertex, VertexSeq + +from .utils import is_pypy try: import numpy as np except ImportError: np = None + class VertexTests(unittest.TestCase): def setUp(self): self.g = Graph.Full(10) @@ -16,29 +19,29 @@ def setUp(self): def testHash(self): data = {} n = self.g.vcount() - for i in xrange(n): + for i in range(n): code1 = hash(self.g.vs[i]) code2 = hash(self.g.vs[i]) self.assertEqual(code1, code2) data[self.g.vs[i]] = i - for i in xrange(n): + for i in range(n): self.assertEqual(i, data[self.g.vs[i]]) def testRichCompare(self): g2 = Graph.Full(10) - for i in xrange(self.g.vcount()): - for j in xrange(self.g.vcount()): + for i in range(self.g.vcount()): + for j in range(self.g.vcount()): self.assertEqual(i == j, self.g.vs[i] == self.g.vs[j]) self.assertEqual(i != j, self.g.vs[i] != self.g.vs[j]) - self.assertEqual(i < j, self.g.vs[i] < self.g.vs[j]) - self.assertEqual(i > j, self.g.vs[i] > self.g.vs[j]) + self.assertEqual(i < j, self.g.vs[i] < self.g.vs[j]) + self.assertEqual(i > j, self.g.vs[i] > self.g.vs[j]) self.assertEqual(i <= j, self.g.vs[i] <= self.g.vs[j]) self.assertEqual(i >= j, self.g.vs[i] >= self.g.vs[j]) self.assertFalse(self.g.vs[i] == g2.vs[j]) self.assertFalse(self.g.vs[i] != g2.vs[j]) - self.assertFalse(self.g.vs[i] < g2.vs[j]) - self.assertFalse(self.g.vs[i] > g2.vs[j]) + self.assertFalse(self.g.vs[i] < g2.vs[j]) + self.assertFalse(self.g.vs[i] > g2.vs[j]) self.assertFalse(self.g.vs[i] <= g2.vs[j]) self.assertFalse(self.g.vs[i] >= g2.vs[j]) self.assertFalse(self.g.es[i] == self.g.vs[j]) @@ -50,10 +53,10 @@ def testUpdateAttributes(self): self.assertEqual(v["a"], 2) v.update_attributes([("a", 3), ("b", 4)], c=5, d=6) - self.assertEqual(v.attributes(), dict(a=3, b=4, c=5, d=6)) + self.assertEqual(v.attributes(), {"a": 3, "b": 4, "c": 5, "d": 6}) - v.update_attributes(dict(b=44, c=55)) - self.assertEqual(v.attributes(), dict(a=3, b=44, c=55, d=6)) + v.update_attributes({"b": 44, "c": 55}) + self.assertEqual(v.attributes(), {"a": 3, "b": 44, "c": 55, "d": 6}) def testPhantomVertex(self): v = self.g.vs[9] @@ -70,41 +73,38 @@ def testIncident(self): g = Graph.Famous("petersen") g.to_directed() - method_table = { - "all": "all_edges", - "in": "in_edges", - "out": "out_edges" - } + method_table = {"all": "all_edges", "in": "in_edges", "out": "out_edges"} - for i in xrange(g.vcount()): + for i in range(g.vcount()): vertex = g.vs[i] for mode, method_name in method_table.items(): method = getattr(vertex, method_name) - self.assertEquals( + self.assertEqual( g.incident(i, mode=mode), - [edge.index for edge in vertex.incident(mode=mode)] + [edge.index for edge in vertex.incident(mode=mode)], ) - self.assertEquals( - g.incident(i, mode=mode), - [edge.index for edge in method()] + self.assertEqual( + g.incident(i, mode=mode), [edge.index for edge in method()] ) def testNeighbors(self): g = Graph.Famous("petersen") g.to_directed() - for i in xrange(g.vcount()): + for i in range(g.vcount()): vertex = g.vs[i] for mode in "all in out".split(): - self.assertEquals( + self.assertEqual( g.neighbors(i, mode=mode), - [edge.index for edge in vertex.neighbors(mode=mode)] + [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 + # print a warning when distances() is invoked on them and we want # to avoid that in the test output. while True: g = Graph.GRG(10, 0.6) @@ -121,16 +121,16 @@ def testProxyMethods(self): # edge indices. # - pagerank() and personalized_pagerank() are ignored because of numerical # inaccuracies + # - shortest_paths() is ignored because it's a deprecated alias # - delete() is ignored because it mutates the graph - ignore = "neighbors predecessors successors pagerank personalized_pagerank"\ - " delete incident all_edges in_edges out_edges" + ignore = ( + "neighbors predecessors successors pagerank personalized_pagerank" + " delete incident all_edges in_edges out_edges shortest_paths" + ) ignore = set(ignore.split()) # Methods not listed here are expected to return an int or a float - return_types = { - "get_shortest_paths": list, - "shortest_paths": list - } + return_types = {"distances": list, "get_shortest_paths": list} for name in Vertex.__dict__: if name in ignore: @@ -143,84 +143,101 @@ def testProxyMethods(self): continue result = func() - self.assertEqual(getattr(g, name)(v.index), result, - msg=("Vertex.%s proxy method misbehaved" % name)) + self.assertEqual( + getattr(g, name)(v.index), + result, + msg=("Vertex.%s proxy method misbehaved" % name), + ) return_type = return_types.get(name, (int, float)) - self.assertTrue(isinstance(result, return_type), - msg=("Vertex.%s proxy method did not return %s" % (name, return_type)) + self.assertTrue( + isinstance(result, return_type), + msg=("Vertex.%s proxy method did not return %s" % (name, return_type)), ) 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): self.assertTrue(len(VertexSeq(self.g)) == 10) self.assertTrue(len(VertexSeq(self.g, 2)) == 1) - self.assertTrue(len(VertexSeq(self.g, [1,2,3])) == 3) - self.assertTrue(VertexSeq(self.g, [1,2,3]).indices == [1,2,3]) + self.assertTrue(len(VertexSeq(self.g, [1, 2, 3])) == 3) + self.assertTrue(VertexSeq(self.g, [1, 2, 3]).indices == [1, 2, 3]) self.assertRaises(ValueError, VertexSeq, self.g, 12) self.assertRaises(ValueError, VertexSeq, self.g, [12]) self.assertTrue(self.g.vs.graph == self.g) def testIndexing(self): n = self.g.vcount() - for i in xrange(n): - print(repr(i)) + for i in range(n): self.assertEqual(i, self.g.vs[i].index) - self.assertEqual(n-i-1, self.g.vs[-i-1].index) + self.assertEqual(n - i - 1, self.g.vs[-i - 1].index) self.assertRaises(IndexError, self.g.vs.__getitem__, n) - self.assertRaises(IndexError, self.g.vs.__getitem__, -n-1) + 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 xrange(self.g.vcount()): + for i in range(self.g.vcount()): arr = np.array([i]) self.assertEqual(i, self.g.vs[arr[0]].index) - arr = np.array([-i-1]) - self.assertEqual(n-i-1, self.g.vs[arr[0]].index) + arr = np.array([-i - 1]) + self.assertEqual(n - i - 1, self.g.vs[arr[0]].index) arr = np.array([n]) self.assertRaises(IndexError, self.g.vs.__getitem__, arr[0]) - arr = np.array([-n-1]) + arr = np.array([-n - 1]) self.assertRaises(IndexError, self.g.vs.__getitem__, arr[0]) arr = np.array([1.5]) self.assertRaises(TypeError, self.g.vs.__getitem__, arr[0]) + ind = [1, 3, 5, 8, 3, 2] + arr = np.array(ind) + self.assertEqual(ind, [vertex.index for vertex in self.g.vs[arr.tolist()]]) + self.assertEqual(ind, [vertex.index for vertex in self.g.vs[list(arr)]]) + 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) - self.assertTrue(self.g.vs["test2"] == [0,None,1,None,2,None,3,None,4,None]) + 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"] = list(range(5)) + self.assertTrue( + self.g.vs["test2"] == [0, None, 1, None, 2, None, 3, None, 4, None] + ) def testSequenceReusing(self): if "test" in self.g.vertex_attributes(): del self.g.vs["test"] self.g.vs["test"] = ["A", "B", "C"] - self.assertTrue(self.g.vs["test"] == ["A", "B", "C", "A", "B", "C", "A", "B", "C", "A"]) + self.assertTrue( + self.g.vs["test"] == ["A", "B", "C", "A", "B", "C", "A", "B", "C", "A"] + ) self.g.vs["test"] = "ABC" self.assertTrue(self.g.vs["test"] == ["ABC"] * 10) only_even = self.g.vs.select(lambda v: (v.index % 2 == 0)) only_even["test"] = ["D", "E"] - self.assertTrue(self.g.vs["test"] == ["D", "ABC", "E", "ABC", "D", "ABC", "E", "ABC", "D", "ABC"]) + self.assertTrue( + self.g.vs["test"] + == ["D", "ABC", "E", "ABC", "D", "ABC", "E", "ABC", "D", "ABC"] + ) del self.g.vs["test"] only_even["test"] = ["D", "E"] - self.assertTrue(self.g.vs["test"] == ["D", None, "E", None, "D", None, "E", None, "D", None]) + self.assertTrue( + self.g.vs["test"] == ["D", None, "E", None, "D", None, "E", None, "D", None] + ) def testAllSequence(self): self.assertTrue(len(self.g.vs) == 10) - self.assertTrue(self.g.vs["test"] == range(10)) + self.assertTrue(self.g.vs["test"] == list(range(10))) def testEmptySequence(self): empty_vs = self.g.vs.select(None) @@ -245,59 +262,67 @@ def testCallableFilteringSelect(self): self.assertTrue(only_even["test"] == [0, 2, 4, 6, 8]) def testChainedCallableFilteringSelect(self): - only_div_six = self.g.vs.select(lambda v: (v.index % 2 == 0), - lambda v: (v.index % 3 == 0)) + only_div_six = self.g.vs.select( + lambda v: (v.index % 2 == 0), lambda v: (v.index % 3 == 0) + ) self.assertTrue(len(only_div_six) == 2) self.assertTrue(only_div_six["test"] == [0, 6]) - only_div_six = self.g.vs.select(lambda v: (v.index % 2 == 0)).select(\ - lambda v: (v.index % 3 == 0)) + only_div_six = self.g.vs.select(lambda v: (v.index % 2 == 0)).select( + lambda v: (v.index % 3 == 0) + ) self.assertTrue(len(only_div_six) == 2) self.assertTrue(only_div_six["test"] == [0, 6]) def testIntegerFilteringFind(self): self.assertEqual(self.g.vs.find(3).index, 3) - self.assertEqual(self.g.vs.select(2,3,4,2).find(3).index, 2) + self.assertEqual(self.g.vs.select(2, 3, 4, 2).find(3).index, 2) self.assertRaises(IndexError, self.g.vs.find, 17) def testIntegerFilteringSelect(self): - subset = self.g.vs.select(2,3,4,2) + subset = self.g.vs.select(2, 3, 4, 2) self.assertEqual(len(subset), 4) - self.assertEqual(subset["test"], [2,3,4,2]) + self.assertEqual(subset["test"], [2, 3, 4, 2]) self.assertRaises(TypeError, self.g.vs.select, 2, 3, 4, 2, None) - subset = self.g.vs[2,3,4,2] + subset = self.g.vs[2, 3, 4, 2] self.assertTrue(len(subset) == 4) - self.assertTrue(subset["test"] == [2,3,4,2]) + self.assertTrue(subset["test"] == [2, 3, 4, 2]) def testStringFilteringFind(self): self.assertEqual(self.g.vs.find("D").index, 3) - self.assertEqual(self.g.vs.select(2,3,4,2).find("C").index, 2) - self.assertRaises(ValueError, self.g.vs.select(2,3,4,2).find, "F") + self.assertEqual(self.g.vs.select(2, 3, 4, 2).find("C").index, 2) + self.assertRaises(ValueError, self.g.vs.select(2, 3, 4, 2).find, "F") self.assertRaises(ValueError, self.g.vs.find, "NoSuchName") def testIterableFilteringSelect(self): - subset = self.g.vs.select(xrange(5,8)) + subset = self.g.vs.select(list(range(5, 8))) self.assertTrue(len(subset) == 3) - self.assertTrue(subset["test"] == [5,6,7]) + self.assertTrue(subset["test"] == [5, 6, 7]) def testSliceFilteringSelect(self): subset = self.g.vs.select(slice(5, 8)) self.assertTrue(len(subset) == 3) - self.assertTrue(subset["test"] == [5,6,7]) + self.assertTrue(subset["test"] == [5, 6, 7]) subset = self.g.vs[5:16:2] self.assertTrue(len(subset) == 3) - self.assertTrue(subset["test"] == [5,7,9]) + self.assertTrue(subset["test"] == [5, 7, 9]) def testKeywordFilteringSelect(self): g = Graph.Barabasi(10000) g.vs["degree"] = g.degree() - g.vs["parity"] = [i % 2 for i in xrange(g.vcount())] - l = len(g.vs(degree_gt=30)) - self.assertTrue(l < 1000) + g.vs["parity"] = [i % 2 for i in range(g.vcount())] + length = len(g.vs(degree_gt=30)) + self.assertTrue(length < 1000) self.assertTrue(len(g.vs(degree_gt=30, parity=0)) <= 500) del g.vs["degree"] - self.assertTrue(len(g.vs(_degree_gt=30)) == l) + self.assertTrue(len(g.vs(_degree_gt=30)) == length) + + def testIndexAndKeywordFilteringFind(self): + self.assertRaises(ValueError, self.g.vs.find, 2, name="G") + self.assertRaises(ValueError, self.g.vs.find, 2, test=4) + self.assertTrue(self.g.vs.find(2, name="C") == self.g.vs[2]) + self.assertTrue(self.g.vs.find(2, test=2) == self.g.vs[2]) def testIndexOutOfBoundsSelect(self): g = Graph.Full(3) @@ -310,22 +335,43 @@ def testIndexOutOfBoundsSelect(self): def testGraphMethodProxying(self): g = Graph.Barabasi(100) - vs = g.vs(1,3,5,7,9) + vs = g.vs(1, 3, 5, 7, 9) self.assertEqual(vs.degree(), g.degree(vs)) self.assertEqual(g.degree(vs), g.degree(vs.indices)) for v, d in zip(vs, vs.degree()): self.assertEqual(v.degree(), d) + def testBug73(self): + # This is a regression test for igraph/python-igraph#73 + g = Graph() + 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(): - vertex_suite = unittest.makeSuite(VertexTests) - vs_suite = unittest.makeSuite(VertexSeqTests) + vertex_suite = unittest.defaultTestLoader.loadTestsFromTestCase(VertexTests) + vs_suite = unittest.defaultTestLoader.loadTestsFromTestCase(VertexSeqTests) return unittest.TestSuite([vertex_suite, vs_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/tests/test_walks.py b/tests/test_walks.py new file mode 100644 index 000000000..4217565d1 --- /dev/null +++ b/tests/test_walks.py @@ -0,0 +1,127 @@ +import random +import unittest + +from igraph import Graph, InternalError + + +class RandomWalkTests(unittest.TestCase): + def setUp(self): + random.seed(42) + + def validate_walk(self, g, walk, start, length, mode="out"): + self.assertEqual(len(walk), length + 1) + + prev = None + for vertex in walk: + if prev is not None: + self.assertTrue(vertex in g.neighbors(prev, mode=mode)) + else: + self.assertEqual(start, vertex) + prev = vertex + + def validate_edge_walk(self, g, walk, start, length, mode="out"): + self.assertEqual(len(walk), length) + + prev_vertices = None + for edgeid in walk: + vertices = g.es[edgeid].tuple + if prev_vertices is not None: + self.assertTrue( + vertices[0] in prev_vertices or vertices[1] in prev_vertices + ) + else: + self.assertTrue(start in vertices) + prev_vertices = vertices + + def testRandomWalkUndirected(self): + g = Graph.GRG(100, 0.2) + for _i in range(100): + 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) + + def testRandomWalkDirectedOut(self): + g = Graph.Tree(121, 3, mode="out") + mode = "out" + for _i in range(100): + start = 0 + length = random.randint(0, 4) + walk = g.random_walk(start, length, mode) + self.validate_walk(g, walk, start, length, mode) + + def testRandomWalkDirectedIn(self): + g = Graph.Tree(121, 3, mode="out") + mode = "in" + for _i in range(100): + 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) + + def testRandomWalkDirectedAll(self): + g = Graph.Tree(121, 3, mode="out") + mode = "all" + for _i in range(100): + 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) + + def testRandomWalkStuck(self): + g = Graph.Ring(10, circular=False, directed=True) + walk = g.random_walk(5, 20) + self.assertEqual([5, 6, 7, 8, 9], walk) + self.assertRaises(InternalError, g.random_walk, 5, 20, stuck="error") + + def testRandomWalkUndirectedVertices(self): + g = Graph.GRG(100, 0.2) + for _i in range(10): + start = random.randint(0, g.vcount() - 1) + length = random.randint(0, 10) + walk = g.random_walk(start, length, return_type="vertices") + self.validate_walk(g, walk, start, length) + + def testRandomWalkUndirectedEdges(self): + g = Graph.GRG(100, 0.2) + for _i in range(10): + start = random.randint(0, g.vcount() - 1) + length = random.randint(0, 10) + walk = g.random_walk(start, length, return_type="edges") + self.validate_edge_walk(g, walk, start, length) + + def testRandomWalkUndirectedBoth(self): + g = Graph.GRG(100, 0.2) + for _i in range(10): + start = random.randint(0, g.vcount() - 1) + length = random.randint(0, 10) + walk_dic = g.random_walk(start, length, return_type="both") + self.assertTrue("vertices" in walk_dic) + self.assertTrue("edges" in walk_dic) + self.validate_edge_walk(g, walk_dic["edges"], start, length) + self.validate_walk(g, walk_dic["vertices"], start, length) + + def testRandomWalkUndirectedWeighted(self): + g = Graph.GRG(100, 0.2) + g.es["weight"] = [1.0 for i in range(g.ecount())] + for _i in range(100): + start = random.randint(0, g.vcount() - 1) + length = random.randint(0, 10) + walk = g.random_walk(start, length, weights="weight") + self.validate_walk(g, walk, start, length) + + +def suite(): + random_walk_suite = unittest.defaultTestLoader.loadTestsFromTestCase( + RandomWalkTests + ) + return unittest.TestSuite([random_walk_suite]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..19b177181 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,54 @@ +"""Utility functions for unit testing.""" + +import os +import platform +import tempfile + +from contextlib import contextmanager +from textwrap import dedent + +__all__ = ("temporary_file",) + + +@contextmanager +def overridden_configuration(key, value): + from igraph import config + + old_value = config[key] + config[key] = value + try: + yield + finally: + config[key] = old_value + + +@contextmanager +def temporary_file(content=None, mode=None, binary=False): + tmpf, tmpfname = tempfile.mkstemp() + os.close(tmpf) + + if mode is None: + if content is None: + mode = "rb" + else: + mode = "wb" + + tmpf = open(tmpfname, mode) + if content is not None: + if hasattr(content, "encode") and not binary: + tmpf.write(dedent(content).encode("utf8")) + else: + tmpf.write(content) + + tmpf.close() + yield 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" diff --git a/tox.ini b/tox.ini index a6fa0e3e2..04fb5ca92 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,32 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests +# Tox (https://tox.wiki) 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 = py27, py34, pypy, pypy3 +envlist = py38, py39, py310, py311, py312, py313, pypy3 + +[gh-actions] +python = + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + pypy-3.7: pypy3 [testenv] -commands = - python test/doctests.py - python test/unittests.py +commands = python -m unittest {posargs} deps = + scipy; platform_python_implementation != "PyPy" + numpy; platform_python_implementation != "PyPy" + networkx; platform_python_implementation != "PyPy" + pandas; platform_python_implementation != "PyPy" + matplotlib; platform_python_implementation != "PyPy" + pytest; platform_python_implementation != "PyPy" +extras = + test +passenv = PATH setenv = TESTING_IN_TOX=1 diff --git a/vendor/source/igraph b/vendor/source/igraph new file mode 160000 index 000000000..7b4ae766c --- /dev/null +++ b/vendor/source/igraph @@ -0,0 +1 @@ +Subproject commit 7b4ae766cbdee6b2017aa5b76752457db2a2972f