├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── README.md ├── netdynflow ├── __init__.py ├── core.py ├── metrics.py ├── netmodels.py └── tools.py ├── requirements.txt └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019-2024. Gorka Zamora-López, Matthieu Gilson and Nikos E. Kouvaris. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # NetDynFlow 3 | 4 | A package to study complex networks based on the temporal evolution of their Dynamic Communicability and Flow. 5 | 6 | Graph theory constitutes a widely used and established field providing powerful tools for the characterization of complex networks. However, the diversity of complex networks studied nowadays overcomes the capabilities of classical graph metrics (originally developed for binary adjacency matrices) to provide with information to understand networks and their function. Also, in several domains, networks are often inferred from real-data-forming dynamic systems and thus, their analysis requires a different angle. The tools given in this package allow to overcome those limitations for a variety of complex networks, specially those that are weighted and whose structure is associated with dynamical phenomena. 7 | 8 | *Dynamic Flow* characterises the transient network response over time, as the network dynamics relax towards their resting-state after a pulse perturbation (either independent or correlated Gaussian noise) has been applied to selected nodes. On the other hand, *Dynamic Communicability* corresponds to the special case where uncorrelated Gaussian noise has initially been applied to all nodes. The behaviour of the interactions during this transition allows to uncover properties of networks and their function. From a computational point of view dynamic communicability and flow are characterised by a series of matrices, encoding the temporal evolution of the pair-wise interactions between nodes. 9 | 10 | 11 | #### Reference and Citation 12 | 13 | * M. Gilson, N. Kouvaris, G. Deco and G. Zamora-Lopez "*[Framework based on communicability and flow to analyze complex networks](https://journals.aps.org/pre/abstract/10.1103/PhysRevE.97.052301)*" Phys. Rev. E **97**, 052301 (2018). 14 | * M. Gilson, N. Kouvaris, et al. "*[Network analysis of whole-brain fMRI 15 | dynamics: A new framework based on dynamic communicability](https://doi.org/10.1016/j.neuroimage.2019.116007)*" NeuroImage **201**, 116007 (2019). 16 | 17 | 18 | 19 | ### INSTALLATION 20 | 21 | Installation of *NetDynFlow* is simple. An existing python distribution and the [pip](https://github.com/pypa/pip) package manager need to be installed. If Python was installed via the [Canopy](https://www.enthought.com/product/canopy/) or the [Anaconda](https://www.anaconda.com) distributions, then pip is surely installed. To check, open a terminal and type: 22 | 23 | $ pip --help 24 | 25 | *NetDynFlow* is still not registered in PyPI (the Python Packages Index) and installation follows directly from GitHub. However, pip will automatically take care of the dependencies (see the *requirements.txt* file). There are two alternative manners to install: the easy and the lazy. 26 | 27 | **- The easy installation**: Visit the GitHub repository [https://github.com/gorkazl/NetDynFlow/](https://github.com/gorkazl/NetDynFlow/) and click on the "Clone or download" button at the right hand side (the green button). Select "Download ZIP". Unzip the file, open a terminal and move to the folder, e.g., 28 | 29 | $ cd ~/Downloads/NetDynFlow-master/ 30 | 31 | Once on the folder that contains the *setup.py* file, type the following 32 | 33 | $ pip install . 34 | 35 | Do not forget the "." at the end which means "*look for the setup.py file in the current directory*." This will check for the dependencies and install *NetDynFlow*. To confirm the installation open an interactive session and try to import the library by typing `import netdynflow`. 36 | 37 | > **NOTE**: After installation the current folder "*~/Downloads/NetDynFlow-master/*" can be safely deleted, or moved somewhere else if you want to conserve the examples and the tests. 38 | 39 | **- The lazy installation**: If [git](https://git-scm.com) is also installed in your computer, then open a terminal and type: 40 | 41 | $ pip install git+https://github.com/mb-BCA/NetDynFlow.git@master 42 | 43 | This will install the package, that is, the content in the folder *netdynflow/*. Other files (Examples/, README.md, LICENSE.txt, etc.) need to be downloaded manually, if wanted. 44 | 45 | 46 | 47 | ### HOW TO USE *NetDynFlow* 48 | 49 | The package is organised into two modules: 50 | 51 | - *core.py*: Functions to obtain the temporal evolution of dynamic communicability and flow. 52 | - *metrics.py*: Network descriptors to analyse the temporal evolution of the dynamic communicability and flow. 53 | 54 | To see the list of all functions available use the standard help in an interactive session, e.g., 55 | 56 | >>> import netdynflow as ndf 57 | >>> ndf.core? 58 | >>> ndf.metrics? 59 | 60 | >**NOTE:** Importing *NetDynflow* brings all functions in the two modules into its local namespace. Thus, functions in each of the two modules are called as `ndf.func()` instead of `ndf.core.func()` or `ndf.metrics.func()`. Details of each function is also found using the usual help, e.g., 61 | 62 | >>> ndf.DynCom? 63 | >>> ndf.Diversity? 64 | 65 | 66 | #### Getting started 67 | Create a simple weighted network of N = 4 nodes (a numpy array) and compute its dynamic communicability over time: 68 | 69 | >>> net = np.array(( (0, 1.2, 0, 0), 70 | (0, 0, 1.1, 0), 71 | (0, 0, 0, 0.7), 72 | (1.0, 0, 0, 0)), float) 73 | >>> tau = 0.8 74 | >>> dyncom = ndf.DynCom(net, tau, tmax=15, timestep=0.01) 75 | 76 | The resulting variable `dyncom` is an array of rank-3 with dimensions ((tmax x tstep) x N x N) containing tmax / tstep = 1500 matrices of size 4 x 4, each describing the state of the network at a given time step. 77 | 78 | > **NOTE**: *NetDynFlow* employs the convention in graph theory that rows of the connectivity matrix encode the outputs of the node. That is, `net[i,j] = 1` implies that the node in row `i` projects over the node in column `j`. 79 | 80 | Now we calculate the *total communicability* and *diversity* of the network over time as: 81 | 82 | >>> totalcom = ndf.TotalEvolution(dyncom) 83 | >>> divers = ndf.Diversity(dyncom) 84 | 85 | `totalcom` and `divers` are two numpy arrays of length (tmax / tsteps) = 1500. 86 | 87 | 88 | ### LICENSE 89 | 90 | Copyright 2019, Gorka Zamora-López, Matthieu Gilson and Nikos E. Kouvaris. E-mail: 91 | 92 | Licensed under the Apache License, Version 2.0 (the "License"); 93 | you may not use this software except in compliance with the License. 94 | You may obtain a copy of the License at 95 | 96 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 97 | 98 | Unless required by applicable law or agreed to in writing, software 99 | distributed under the License is distributed on an "AS IS" BASIS, 100 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 101 | See the License for the specific language governing permissions and 102 | limitations under the License. 103 | 104 | 105 | ------------------------------------------------------------------------------- 106 | ### VERSION HISTORY 107 | 108 | ##### March 14, 2024 109 | Small bugs fixed: 110 | 111 | - Remaining *Numba* dependency removed. 112 | - Fixed the new aliases for `int` and `float` in *Numpy*. All arrays are now declared as `np.int64` or `np.float64`, and individual numbers as standard Python `int` or `float`. 113 | 114 | ##### December 14, 2023 115 | *core.py* module has been simplified to three practical functions `DynFlow()`, `IntrinsicFlow()` and `ExtrinsicFlow()`. 116 | 117 | New measures added to *metrics.py* module: `Time2Peak()`, `Time2Decay` and `AreaUnderCurve()`. These functions accept either the temporal response matrices of shape (nt,N,N) (e.g., the output of DynFlow), the temporal responses of nodes of shape (nt,N) or the global network response of shape (nt,1). The functions will return a matrix, a vector or an scalar accordingly for each case. 118 | 119 | ##### November 20, 2019 120 | Official **version 1.0.0** has been uploaded. 121 | The package went through an in-depth internal revision but implies minor changes from the user point of view. 122 | 123 | 1. Function `CalcTensor()` was created, which makes most of the dirty job while the rest of core functions, e.g., `DynCom()` or `DynFlow()` became wrappers calling `CalcTensor()`. This internal redesign was made to avoid reproducing code in several parts and thus simplify maintenance. 124 | 2. Some internal variables were remaned for uniformity with the [pyMOU](https://github.com/mb-BCA/pyMOU) package. 125 | 3. An examples folder was added to host tutorials (Jupyter Notebooks). These examples will be further updated but version of the package will remain v1.0.0 until changes are done at the core files. 126 | 127 | ##### July 10, 2019 128 | First release of *NetDynFlow* (Beta). 129 | 130 | 131 | -------------------------------------------------------------------------------- /netdynflow/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2023, Gorka Zamora-López, Matthieu Gilson and Nikos E. Kouvaris 3 | # 4 | # 5 | # Released under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this software except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | """ 12 | Network Analysis Based on Perturbation-Induced Flows 13 | ==================================================== 14 | 15 | A package to study complex networks based on the spatio-temporal propagation of 16 | flows due to external perturbations. 17 | Compatible with Python 3.X. 18 | 19 | - 'Dynamic Flow' characterises the transient network response over time, as the 20 | network dynamics relax towards their resting-state after a pulse perturbation 21 | (either independent or correlated Gaussian noise) has been applied to selected 22 | nodes. 23 | 24 | NetDynFlow treats networks as connectivity matrices, represented as 2D NumPy 25 | arrays. Given (i) a connectivity matrix and (ii) a an input covariance matrix, 26 | conditional pair-wise network flows return a set of matrices (a 3D NumPy array), 27 | each describing the state of the state of the pairwise node interations at 28 | at consecutive time points. 29 | 30 | Reference and Citation 31 | ********************** 32 | 33 | 1. M. Gilson, N. Kouvaris, G. Deco & G.Zamora-Lopez "Framework based on communi- 34 | cability and flow to analyze complex networks" Phys. Rev. E 97, 052301 (2018). 35 | 36 | 2. M. Gilson, N. Kouvaris, et al. "Network analysis of whole-brain fMRI 37 | dynamics: A new framework based on dynamic communicability" NeuroImage 201, 38 | 116007 (2019). 39 | 40 | Available functions 41 | ******************* 42 | 43 | The package is organised into two modules: 44 | 45 | core.py 46 | Functions to calculate spatio-temporal evolution of conditiona network flows. 47 | metrics.py 48 | Descriptors to analyse the spatio-temporal evolution of perturbation-induced 49 | flows in a network. 50 | 51 | To see the list of all functions available use the standard help in an 52 | interactive session, for both modules :: 53 | 54 | >>> import netdynflow as ndf 55 | >>> ndf.core? 56 | >>> ndf.metrics? 57 | 58 | **NOTE:** 59 | Importing NetDynflow brings all functions in the two modules into its 60 | local namespace. Thus, functions in each of the two modules are called as 61 | `ndf.func()` instead of `ndf.core.func()` or `ndf.metrics.func()`. Details 62 | of each function is also found using the usual help, e.g., 63 | 64 | >>> ndf.DynFlow? 65 | >>> ndf.Diversity? 66 | 67 | In an IPython interactive session, or in a Jupyter Notebook, typing ``netdynflow`` 68 | and then pressing will show all the modules and functions available in 69 | the package. 70 | 71 | Using NetDynFlow 72 | ^^^^^^^^^^^^^^^^ 73 | Since NetDynFlow depends on NumPy, it is recommended to import NumPy first, 74 | although this is not necessary for loading the package: :: 75 | 76 | >>> import numpy as np 77 | >>> import netdynflow as ndf 78 | 79 | **Note**: 80 | Importing netdynflow imports also all functions in module *core.py* 81 | into its namespace. Module *metrics.py* is imported separately. Therefore, 82 | if the import is relative those functions can be called as, e.g., :: 83 | 84 | >>> import netdynflow as ndf 85 | >>> ... 86 | >>> sigma = np.identity(N) 87 | >>> dynflow = ndf.DynFlow(net, tau, sigma) 88 | 89 | We did not have to call ``netdynflow.core.DynFlow()``. In the case of an absolute 90 | import (using an asterisk ``*``) all functions in *core.py* are imported to the 91 | base namespace: :: 92 | 93 | >>> from netdynflow import * 94 | >>> ... 95 | >>> sigma = np.identity(N) 96 | >>> dynflow = DynFlow(net, tau, sigma) 97 | 98 | Getting started 99 | *************** 100 | Create a simple weighted network of N = 4 nodes (a numpy array) and compute its 101 | dynamic flow over time: :: 102 | 103 | >>> net = np.array(((0, 1.2, 0, 0), 104 | (0, 0, 1.1, 0), 105 | (0, 0, 0, 0.7), 106 | (1.0, 0, 0, 0)), float) 107 | 108 | >>> tau = 0.8 109 | >>> sigma = np.identity(N) # Matrix of initial perturbations 110 | >>> dynflow = ndf.DynFlow(net, tau, sigma, tmax=15, timestep=0.01) 111 | 112 | The resulting variable ``dynflow`` is an array of rank-3 with dimensions 113 | (nt,N,N) where nt = (tmax / tstep) containing nt = 1500 matrices of size 4 x 4, 114 | each describing the pair-wise flows in the network over time. 115 | 116 | **NOTE**: 117 | NetDynFlow employs the convention in graph theory that rows of the 118 | connectivity matrix encode the outputs of the node. That is, `net[i,j] = 1` 119 | implies that the node in row ``i`` projects over the node in column ``j``. 120 | 121 | Now we calculate the *total flow* and the ``diversity`` of the network over 122 | time as: :: 123 | 124 | >>> totalflow = ndf.TotalEvolution(dynflow) 125 | >>> divers = ndf.Diversity(dynflow) 126 | 127 | ``totalflow`` and ``divers`` are two arrays of length (tmax / tsteps) = 1500. 128 | 129 | 130 | License 131 | ------- 132 | Copyright (c) 2023, Gorka Zamora-López, Matthieu Gilson and Nikos E. Kouvaris 133 | 134 | Released under the Apache License, Version 2.0 (the "License"); 135 | you may not use this software except in compliance with the License. 136 | You may obtain a copy of the License at 137 | 138 | http://www.apache.org/licenses/LICENSE-2.0 139 | 140 | or see the LICENSE.txt file. 141 | 142 | """ 143 | 144 | # Branch to test different options to organise the core.py module 145 | from . import core 146 | from .core import * 147 | from . import metrics 148 | from .metrics import * 149 | from . import tools 150 | from . import netmodels 151 | 152 | 153 | __author__ = "Gorka Zamora-Lopez, Matthieu Gilson and Nikos E. Kouvaris" 154 | __email__ = "galib@Zamora-Lopez.xyz" 155 | __copyright__ = "Copyright 2024" 156 | __license__ = "Apache License version 2.0" 157 | __version__ = "1.0.2" 158 | __update__ = "14/03/2024" 159 | 160 | 161 | # 162 | -------------------------------------------------------------------------------- /netdynflow/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2023, Gorka Zamora-López, Matthieu Gilson and Nikos E. Kouvaris 3 | # 4 | # 5 | # Released under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this software except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | """ 12 | Calculation of dynamic communicability and flow 13 | =============================================== 14 | 15 | This module contains functions to calculate the temporal evolution of the 16 | pair-wise, conditional flows in a network due to perturbations. The location 17 | and intensity of the initial perturbation can be defined by the users. 18 | Networks are treated as connectivity matrices, represented as 2D NumPy arrays. 19 | Given a connectivity matrix, the subsequent flows are returned as a series of 20 | matrices arranged into a tensor (a numpy array of rank-3), each describing the 21 | state of the network at consecutive time points. 22 | 23 | Helper functions 24 | ---------------- 25 | JacobianMOU 26 | Calculates the Jacobian matrix for the MOU dynamic system. 27 | 28 | Generation of main tensors 29 | -------------------------- 30 | CalcTensor 31 | Generic function to create time evolution of the flows. 32 | DynFlow 33 | Pair-wise conditional flows on a network over time for a given input. 34 | IntrinsicFlow 35 | Returns the flow dissipated through each node over time. 36 | FullFlow 37 | Returns the complete flow on a network over time for a given input. 38 | 39 | Reference and Citation 40 | ---------------------- 41 | 1. M. Gilson, N. Kouvaris, G. Deco & G.Zamora-Lopez "Framework based on communi- 42 | cability and flow to analyze complex networks" Phys. Rev. E 97, 052301 (2018). 43 | 2. M. Gilson, N. Kouvaris, et al. "Network analysis of whole-brain fMRI 44 | dynamics: A new framework based on dynamic communicability" NeuroImage 201, 45 | 116007 (2019). 46 | 47 | """ 48 | # Standard libary imports 49 | 50 | # Third party packages 51 | import numpy as np 52 | import numpy.linalg 53 | import scipy.linalg 54 | 55 | __all__ = ['JacobianMOU', 'DynFlow', 'FullFlow', 'IntrinsicFlow'] 56 | 57 | 58 | ## USEFUL FUNCTIONS ########################################################## 59 | def JacobianMOU(con, tau): 60 | """Calculates the Jacobian matrix for the MOU dynamic system. 61 | 62 | Parameters 63 | ---------- 64 | con : ndarray of rank-2 65 | The adjacency matrix of the network. 66 | tau : real valued number, or ndarray of rank-1 67 | The decay rate at the nodes. Positive values expected. 68 | If a number is given, then the function considers all nodes have same 69 | decay rate. Alternatively, an array can be inputed with the decay rate 70 | of each node. 71 | 72 | Returns 73 | ------- 74 | jac : ndarray of rank-2 75 | The Jacobian matrix of shape N x N for the MOU dynamical system. 76 | """ 77 | # 0) SECURITY CHECKS 78 | # Check the input connectivity matrix 79 | con_shape = np.shape(con) 80 | if len(con_shape) != 2: 81 | raise ValueError( "'con' not a matrix." ) 82 | if con_shape[0] != con_shape[1]: 83 | raise ValueError( "'con' not a square matrix." ) 84 | # Make sure con is a ndarray of dtype = np.float64 85 | con = np.array(con, dtype=np.float64) 86 | N = con_shape[0] 87 | 88 | # Check the tau constant, in case it is a 1-dimensional array-like. 89 | tau_shape = np.shape(tau) 90 | if tau_shape: 91 | if len(tau_shape) != 1: 92 | raise ValueError( "tau must be either a float or a 1D array." ) 93 | if tau_shape[0] != N: 94 | raise ValueError( "'con' and tau not aligned." ) 95 | # Make sure tau is a ndarray of dytpe = np.float64 96 | tau = np.array(tau, dtype=np.float64) 97 | else: 98 | tau = tau * np.ones(N, dtype=np.float64) 99 | 100 | # 1) CALCULATE THE JACOBIAN MATRIX 101 | jacdiag = -np.ones(N, dtype=np.float64) / tau 102 | jac = np.diag(jacdiag) + con 103 | 104 | return jac 105 | 106 | 107 | ## GENERATION OF THE MAIN TENSORS ############################################# 108 | def CalcTensor(con, tau, sigma, tmax=20, timestep=0.1, 109 | normed=False, case='DynFlow'): 110 | """Generic function to create time evolution of the flows. 111 | 112 | Parameters 113 | ---------- 114 | con : ndarray of rank-2 115 | The adjacency matrix of the network. 116 | tau : real valued number, or ndarray of rank-1 117 | The decay rate at the nodes. Positive value expected. 118 | If a number is given, then the function considers all nodes have same 119 | decay rate. Alternatively, an array can be inputed with the decay rate 120 | of each node. 121 | sigma : ndarray of rank-2 122 | The covariance matrix of fluctuating inputs. 123 | tmax : real valued number, positive (optional) 124 | Final time for integration. 125 | timestep : real valued number, positive (optional) 126 | Sampling time-step. 127 | Warning: Not an integration step, just the desired sampling rate. 128 | normed : boolean (optional) 129 | If True, normalises the tensor by the scaling factor, to make networks 130 | of different size comparable. 131 | 132 | Returns 133 | ------- 134 | flow_tensor : ndarray of rank-3 135 | Temporal evolution of the network's dynamic flow. A tensor of shape 136 | (nt,N,N), where N is the number of nodes and nt = tmax * timestep is 137 | the number of time steps. 138 | """ 139 | # 0) SECURITY CHECKS 140 | caselist = ['DynFlow', 'FullFlow', 'IntrinsicFlow'] 141 | if case not in caselist: 142 | raise ValueError( "Please enter one of accepted cases: %s" %str(caselist) ) 143 | 144 | if tmax <= 0.0: raise ValueError("'tmax' must be positive") 145 | if timestep <= 0.0: raise ValueError( "'timestep' must be positive") 146 | if timestep > tmax: raise ValueError("Incompatible values, timestep < tmax given") 147 | 148 | # 1) CALCULATE THE JACOBIAN MATRIX 149 | jac = JacobianMOU(con, tau) 150 | jacdiag = np.diagonal(jac) 151 | N = len(jac) 152 | 153 | # 2) CALCULATE THE DYNAMIC FLOW 154 | # 2.1) Calculate the extrinsic flow over integration time 155 | nt = int(tmax / timestep) + 1 156 | sigma_sqrt = scipy.linalg.sqrtm(sigma) 157 | 158 | flow_tensor = np.zeros((nt,N,N), dtype=np.float64) 159 | 160 | if case == 'DynFlow': 161 | for i_t in range(nt): 162 | t = i_t * timestep 163 | # Calculate the term for jacdiag without using expm(), to speed up 164 | jacdiag_t = np.diag( np.exp(jacdiag * t) ) 165 | # Calculate the jaccobian at given time 166 | jac_t = scipy.linalg.expm(jac * t) 167 | # Calculate the dynamic communicability at time t 168 | flow_tensor[i_t] = np.dot( sigma_sqrt, jac_t - jacdiag_t ) 169 | 170 | elif case == 'IntrinsicFlow': 171 | for i_t in range(nt): 172 | t = i_t * timestep 173 | # Calculate the term for jacdiag without using expm(), to speed up 174 | jacdiag_t = np.diag( np.exp(jacdiag * t) ) 175 | # Calculate the intrinsic flow at time t. 176 | flow_tensor[i_t] = np.dot( sigma_sqrt, jacdiag_t) 177 | 178 | elif case == 'FullFlow': 179 | for i_t in range(nt): 180 | t = i_t * timestep 181 | # Calculate the jaccobian at given time 182 | jac_t = scipy.linalg.expm(jac * t) 183 | # Calculate the non-normalised flow at time t. 184 | flow_tensor[i_t] = np.dot( sigma_sqrt, jac_t ) 185 | 186 | # 2.2) Normalise by the scaling factor 187 | if normed: 188 | scaling_factor = np.abs(1./jacdiag).sum() 189 | flow_tensor /= scaling_factor 190 | 191 | return flow_tensor 192 | 193 | 194 | ## Wrappers using CalcTensor() ___________________________________________ 195 | def DynFlow(con, tau, sigma, tmax=20, timestep=0.1, normed=False): 196 | """Pair-wise conditional flows on a network over time for a given input. 197 | 198 | Parameters 199 | ---------- 200 | con : ndarray of rank-2 201 | The adjacency matrix of the network. 202 | tau : real valued number, or ndarray of rank-1 203 | The decay rate at the nodes. Positive value expected. 204 | If a number is given, then the function considers all nodes have same 205 | decay rate. Alternatively, an array can be inputed with the decay rate 206 | of each node. 207 | sigma : ndarray of rank-2 208 | The covariance matrix of fluctuating inputs. 209 | tmax : real valued number, positive (optional) 210 | Final time for integration. 211 | timestep : real valued number, positive (optional) 212 | Sampling time-step. 213 | Warning: Not an integration step, just the desired sampling rate. 214 | normed : boolean (optional) 215 | If True, normalises the tensor by the scaling factor, to make networks 216 | of different size comparable. 217 | 218 | Returns 219 | ------- 220 | dynflow_tensor : ndarray of rank-3 221 | Temporal evolution of the network's pair-wise conditional flows. 222 | A tensor of shape (nt,N,N), where N is the number of nodes and 223 | nt = tmax * timestep is the number of time steps. 224 | """ 225 | dynflow_tensor = CalcTensor(con, tau, sigma, tmax=tmax, 226 | timestep=timestep, normed=normed, case='DynFlow') 227 | 228 | return dynflow_tensor 229 | 230 | def IntrinsicFlow(con, tau, sigma, tmax=20, timestep=0.1, normed=False): 231 | """Returns the flow dissipated through each node over time. 232 | 233 | Parameters 234 | ---------- 235 | con : ndarray of rank-2 236 | The adjacency matrix of the network. 237 | tau : real valued number, or ndarray of rank-1 238 | The decay rate at the nodes. Positive value expected. 239 | If a number is given, then the function considers all nodes have same 240 | decay rate. Alternatively, an array can be inputed with the decay rate 241 | of each node. 242 | sigma : ndarray of rank-2 243 | The matrix of Gaussian noise covariances. 244 | tmax : real valued number, positive (optional) 245 | Final time for integration.dynflow 246 | timestep : real valued number, positive (optional) 247 | Sampling time-step. 248 | Warning: Not an integration step, just the desired sampling rate. 249 | normed : boolean (optional) 250 | If True, normalises the tensor by the scaling factor, to make networks 251 | of different size comparable. 252 | 253 | Returns 254 | ------- 255 | flow_tensor : ndarray of rank-3 256 | Temporal evolution of disspation through the nodes of the network. 257 | A tensor of shape (nt,N,N), where N is the number of nodes and 258 | nt = tmax * timestep is the number of time steps. 259 | """ 260 | flow_tensor = CalcTensor(con, tau, sigma, tmax=tmax, 261 | timestep=timestep, normed=normed, case='IntrinsicFlow') 262 | 263 | return flow_tensor 264 | 265 | def FullFlow(con, tau, sigma, tmax=20, timestep=0.1, normed=False): 266 | """Returns the complete flow on a network over time for a given input. 267 | 268 | Parameters 269 | ---------- 270 | con : ndarray of rank-2 271 | The adjacency matrix of the network. 272 | tau : real valued number, or ndarray of rank-1 273 | The decay rate at the nodes. Positive value expected. 274 | If a number is given, then the function considers all nodes have same 275 | decay rate. Alternatively, an array can be inputed with the decay rate 276 | of each node. 277 | sigma : ndarray of rank-2 278 | The matrix of Gaussian noise covariances. 279 | tmax : real valued number, positive (optional) 280 | Final time for integration. 281 | timestep : real valued number, positive (optional) 282 | Sampling time-step. NOT an integration step, but the sampling step. 283 | normed : boolean (optional) 284 | If True, normalises the tensor by the scaling factor, to make networks 285 | of different size comparable. 286 | 287 | Returns 288 | ------- 289 | flow_tensor : ndarray of rank-3 290 | Temporal evolution of the network's full-flow. A tensor of shape 291 | (nt,N,N), where N is the number of nodes and nt = tmax * timestep is 292 | the number of time steps. 293 | """ 294 | flow_tensor = CalcTensor(con, tau, sigma, tmax=tmax, 295 | timestep=timestep, normed=normed, case='FullFlow') 296 | 297 | return flow_tensor 298 | 299 | 300 | ## 301 | -------------------------------------------------------------------------------- /netdynflow/metrics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2023, Gorka Zamora-López, Matthieu Gilson and Nikos E. Kouvaris 3 | # 4 | # 5 | # Released under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this software except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | """ 12 | Analysis of dynamic communicability and flow 13 | ============================================ 14 | 15 | Functions to analyse the spatio-temporal evolution of perturbation-induced 16 | flows in a network. The data shall be given in the form of 3D tensor arrays 17 | previously calculated from a given network. See 'core.py' module. 18 | 19 | Metrics derived from the tensors 20 | -------------------------------- 21 | TotalEvolution 22 | Calculates total amount of flow through the network at every time point. 23 | NodeFlows 24 | Temporal evolution of the input and output flows for each node. 25 | Diversity 26 | Inhomogeneity of the pair-wise flows in a networks over time. 27 | TimeToPeak 28 | The time for links, nodes or networks need to reach maximal flow. 29 | TimeToDecay 30 | The time need for pair-wise, nodes or networks flows to decay. 31 | AreaUnderCurve 32 | Total amount of flow across time for pair-wise, node or whole-network. 33 | 34 | 35 | Reference and Citation 36 | ---------------------- 37 | 1. M. Gilson, N. Kouvaris, G. Deco & G.Zamora-Lopez "Framework based on communi- 38 | cability and flow to analyze complex networks" Phys. Rev. E 97, 052301 (2018). 39 | 2. M. Gilson, N. Kouvaris, et al. "Network analysis of whole-brain fMRI 40 | dynamics: A new framework based on dynamic communicability" NeuroImage 201, 41 | 116007 (2019). 42 | 43 | """ 44 | # Standard library imports 45 | 46 | # Third party packages 47 | import numpy as np 48 | import numpy.linalg 49 | import scipy.linalg 50 | 51 | 52 | ## METRICS EXTRACTED FROM THE FLOW AND COMMUNICABILITY TENSORS ################ 53 | def TotalEvolution(tensor): 54 | """ 55 | Calculates total amount of flow through the network at every time point. 56 | 57 | Parameters 58 | ---------- 59 | tensor : ndarray of rank-3 60 | Spatio-temporal evolution of the network's flow. A tensor of shape 61 | (nt,N,N) is expected, where N is the number of nodes and nt is the 62 | number of time steps. 63 | 64 | Returns 65 | ------- 66 | totaldyncom : ndarray of rank-1 67 | Array containing temporal evolution of the total network flow. 68 | """ 69 | 70 | # 0) SECURITY CHECKS 71 | # Check the input tensor has the correct 3D shape 72 | tensor_shape = np.shape(tensor) 73 | if (len(tensor_shape) != 3) or (tensor_shape[1] != tensor_shape[2]): 74 | raise ValueError("Input array not aligned. A 3D array of shape (N x N x nt) expected.") 75 | 76 | totaldyncom = tensor.sum(axis=(1,2)) 77 | 78 | return totaldyncom 79 | 80 | def Diversity(tensor): 81 | """ 82 | Inhomogeneity of the pair-wise flows in a networks over time. 83 | 84 | Parameters 85 | ---------- 86 | tensor : ndarray of rank-3 87 | Temporal evolution of the network's dynamic communicability or flow. 88 | A tensor of shape (nt,N,N) is expected, where N is the number of nodes 89 | and nt is the number of time steps. 90 | 91 | Returns 92 | ------- 93 | diversity : ndarray of rank-1 94 | Array containing temporal evolution of the pair-wise flow inhomogeneities. 95 | """ 96 | # 0) SECURITY CHECKS 97 | # Check the input tensor has the correct 3D shape 98 | tensor_shape = np.shape(tensor) 99 | if (len(tensor_shape) != 3) or (tensor_shape[1] != tensor_shape[2]): 100 | raise ValueError("Input array not aligned. A 3D array of shape (nt x N x N) expected.") 101 | 102 | nt = tensor_shape[0] 103 | diversity = np.zeros(nt, np.float64) 104 | diversity[0] = np.nan 105 | for i_t in range(1,nt): 106 | temp = tensor[i_t] 107 | diversity[i_t] = temp.std() / temp.mean() 108 | 109 | return diversity 110 | 111 | def NodeFlows(tensor, selfloops=False): 112 | """ 113 | Temporal evolution of the input and output flows for each node. 114 | 115 | Parameters 116 | ---------- 117 | tensor : ndarray of rank-3 118 | Temporal evolution of the network's dynamic communicability. A tensor 119 | of shape (nt,N,N) is expected, where N is the number of nodes and nt is 120 | the number of time steps. 121 | selfloops : boolean 122 | If False (default), the function only returns the in-flows into a node 123 | due to perturbations on other nodes, and the out-flows that the node 124 | causes on other nodes, excluding the initial perturbations on itself. 125 | If True, the function includes the effect of the perturbation on itself 126 | for the calculation of the input and output flows. 127 | 128 | Returns 129 | ------- 130 | nodeflows : tuple. 131 | Temporal evolution of the input and output flows for all nodes. 132 | The result consists of a tuple containing two ndarrays of shape (nt,N). 133 | The first is for the input flows into a node over time and the second 134 | array for the output flows. 135 | """ 136 | 137 | # 0) SECURITY CHECKS 138 | # Check the input tensor has the correct 3D shape 139 | arr_shape = np.shape(tensor) 140 | if (len(arr_shape) != 3) or (arr_shape[1] != arr_shape[2]): 141 | raise ValueError("Input array not aligned. A 3D array of shape (nt x N x N) expected.") 142 | 143 | # 1) Calculate the input and output node properties 144 | # When self-loops shall be included to the temporal nodel flows 145 | if selfloops: 146 | inflows = tensor.sum(axis=1) 147 | outflows = tensor.sum(axis=2) 148 | 149 | # Excluding the self-flows a node due to inital perturbation on itself. 150 | else: 151 | nt, N,N = arr_shape 152 | inflows = np.zeros((nt,N), np.float64) 153 | outflows = np.zeros((nt,N), np.float64) 154 | for i in range(N): 155 | tempdiags = tensor[:,i,i] 156 | inflows[:,i] = tensor[:,:,i].sum(axis=1) - tempdiags 157 | outflows[:,i] = tensor[:,i,:].sum(axis=1) - tempdiags 158 | 159 | nodeflows = ( inflows, outflows ) 160 | return nodeflows 161 | 162 | def Time2Peak(arr, timestep): 163 | """ 164 | The time for links, nodes or networks need to reach maximal flow. 165 | 166 | In terms of binary graphs, time-to-peak is equivalen to the pathlength 167 | between two nodes. 168 | 169 | The function calculates the time-to-peak for either links, nodes or the 170 | whole network, depending on the input array given. 171 | - If 'arr' is a (nt,N,N) flow tensor, the output 'ttp_arr' will be an 172 | (N,N) matrix with the ttp between every pair of nodes. 173 | - If 'arr' is a (nt,N) temporal flow of the N nodes, the output 'ttp_arr' 174 | will be an array of length N, containing the ttp of all N nodes. 175 | - If 'arr' is an array of length nt (total network flow over time), 'ttp_arr' 176 | will be a scalar, indicating the time at which the whole-network flow peaks. 177 | 178 | Parameters 179 | ---------- 180 | arr : ndarray of adaptive shape, according to the case. 181 | Temporal evolution of the flow. An array of optional shapes. Either 182 | (nt,N,N) for the pair-wise flows, shape (nt,N,N) for the in- or output 183 | flows of nodes, or a 1D array of length nt for the network flow. 184 | timestep : real valued number. 185 | Sampling time-step. This has to be the time-step employed to simulate 186 | the temporal evolution encoded in 'arr'. 187 | 188 | Returns 189 | ------- 190 | ttp_arr : ndarray of variable rank 191 | The time(s) taken for links, nodes or the network to reach peak flow. 192 | Output shape depends on input. 193 | """ 194 | 195 | # 0) SECURITY CHECKS 196 | ## TODO1: Write a check to verify the curve has a real peak and decays after 197 | ## the peak. Raise a warning that maybe longer simulation is needed. 198 | ## TODO2: Silent nodes (non-perturbed) should return inf instead of zero. 199 | # Check correct shape, in case input is the 3D array for the pair-wise flow 200 | arr_shape = np.shape(arr) 201 | if arr_shape==3: 202 | if arr_shape[1] != arr_shape[2]: 203 | raise ValueError("Input array not aligned. For 3D arrays shape (nt x N x N) is expected.") 204 | 205 | # 1) Get the indices at which every element peaks 206 | ttp_arr = arr.argmax(axis=0) 207 | # 2) Convert into simulation time 208 | ttp_arr = timestep * ttp_arr 209 | 210 | return ttp_arr 211 | 212 | def Time2Decay(arr, dt, fraction=0.99): 213 | """ 214 | The time need for pair-wise, nodes or networks flows to decay. 215 | 216 | Strictly speaking, this function measures the time that the cumulative 217 | flow (area under the curve) needs to reach x% of the total (cumulative) 218 | value. Here 'x%' is controled by the optional parameter 'fraction'. 219 | For example, 'fraction = 0.99' means the time needed to reach 99% 220 | of the area under the curve, given a response curve. 221 | 222 | The function calculates the time-to-decay either for all pair-wise 223 | interactions, for the nodes or for the whole network, depending on the 224 | input array given. 225 | - If 'arr' is a (nt,N,N) flow tensor, the output 'ttd_arr' will be an 226 | (N,N) matrix with the ttd between every pair of nodes. 227 | - If 'arr' is a (nt,N) temporal flow of the N nodes, the output 'ttd_arr' 228 | will be an array of length N, containing the ttd of all N nodes. 229 | - If 'arr' is an array of length nt (total network flow over time), 'ttd_arr' 230 | will be a scalar, indicating the time at which the whole-network flow decays. 231 | 232 | Parameters 233 | ---------- 234 | arr : ndarray of adaptive shape, according to the case. 235 | Temporal evolution of the flow. An array of optional shapes. Either 236 | (nt,N,N) for the pair-wise flows, shape (nt,N,N) for the in- or output 237 | flows of nodes, or a 1D array of length nt for the network flow. 238 | timestep : real valued number. 239 | Sampling time-step. This has to be the time-step employed to simulate 240 | the temporal evolution encoded in 'arr'. 241 | fraction : scalar, optional 242 | The fraction of the total area-under-the-curve to be reached. 243 | For example, 'fraction = 0.99' means the time the flow needs to 244 | reach 99% of the area under the curve. 245 | 246 | Returns 247 | ------- 248 | ttd_arr : ndarray of variable rank 249 | The time(s) taken for the flows through links, nodes or the network to 250 | decay. Output shape depends on input. 251 | """ 252 | 253 | # 0) SECURITY CHECKS 254 | ## TODO: Write a check to verify the curve(s) has (have) really decayed back 255 | ## to zero. At this moment, it is the user's responsability to guarantee 256 | ## that all the curves have decayed reasonably well. 257 | ## The check should rise a warning to simulate for longer time. 258 | 259 | # Check correct shape, in case input is the 3D array for the pair-wise flow 260 | arr_shape = np.shape(arr) 261 | if arr_shape==3: 262 | if arr_shape[1] != arr_shape[2]: 263 | raise ValueError("Input array not aligned. For 3D arrays shape (nt x N x N) is expected.") 264 | 265 | # 1) Set the level of cummulative flow to be reached over time 266 | targetcflow = fraction * arr.sum(axis=0) 267 | 268 | # 2) Calculate the time the flow(s) need to decay 269 | # Initialise the output array, to return the final time-point 270 | ## NOTE: This version iterates over all the times. This is not necessary. 271 | ## We could start from the end and save plenty of iterations. 272 | ttd_shape = arr_shape[1:] 273 | nsteps = arr_shape[0] 274 | ttd_arr = nsteps * np.ones(ttd_shape, np.int64) 275 | 276 | # Iterate over time, calculating the cumulative flow(s) 277 | cflow = arr[0].copy() 278 | for t in range(1,nsteps): 279 | cflow += arr[t] 280 | ttd_arr = np.where(cflow < targetcflow, t, ttd_arr) 281 | 282 | # Finally, convert the indices into integration time 283 | ttd_arr = ttd_arr.astype(np.float64) * dt 284 | 285 | return ttd_arr 286 | 287 | def AreaUnderCurve(arr, timestep, timespan='alltime'): 288 | ## TODO: The name of this function needs good thinking. Different options 289 | ## are possible depending on the interpretation and naming of other 290 | ## variables or metrics. 291 | ## The most explicit would be to call it "AreaUnderCurve()" because that is 292 | ## exactly what it does. But that doesn't sound very sexy nor hints on the 293 | ## interpretation of what it measures, given that 'arr' will be the 294 | ## temporal evolution of a flow, response curve or dyncom ... which are 295 | ## indeed the same !! Maybe. 296 | """ 297 | Total amount of flow across time for pair-wise, node or whole-network. 298 | 299 | The function calculates the area-under-the-curve for the flow curves over 300 | time. It does so for all pair-wise interactions, for the nodes or for 301 | the whole network, depending on the input array given. 302 | - If 'arr' is a (nt,N,N) flow tensor, the output 'totalflow' will be an 303 | (N,N) matrix with the accumulated flow passed between every pair of nodes. 304 | - If 'arr' is a (nt,N) temporal flow of the N nodes, the output 'totalflow' 305 | will be an array of length N, containing the accumulated flow passed through 306 | all the nodes. 307 | - If 'arr' is an array of length nt (total network flow over time), 'totalflow' 308 | will be a scalar, indicating the total amount of flow that went through the 309 | whole network. 310 | 311 | Parameters 312 | ---------- 313 | arr : ndarray of adaptive shape, according to the case. 314 | Temporal evolution of the flow. An array of shape nt x N x N for the 315 | flow of the links, an array of shape N X nt for the flow of the nodes, 316 | or a 1-dimensional array of length nt for the network flow. 317 | timestep : real valued number. 318 | Sampling time-step. This has to be the time-step employed to simulate 319 | the temporal evolution encoded in 'arr'. 320 | timespan : string, optional 321 | If timespan = 'alltime', the function calculates the area under the 322 | curve(s) along the whole time span (nt) that 'arr' contains, from t0 = 0 323 | to tfinal. 324 | If timespan = 'raise', the function calculates the area-under-the- 325 | curve from t0 = 0, to the time the flow(s) reach a peak value. 326 | If timespan = 'decay', it returns the area-under-the-curve for the 327 | time spanning from the time the flow peaks, until the end of the signal. 328 | 329 | Returns 330 | ------- 331 | totalflow : ndarray of variable rank 332 | The accumulated flow (area-under-the-curve) between pairs of nodes, 333 | by nodes or by the whole network, over a period of time. 334 | """ 335 | 336 | # 0) SECURITY CHECKS 337 | ## TODO: Write a check to verify the curve has a real peak and decays after 338 | ## the peak. Raise a warning that maybe longer simulation is needed. 339 | 340 | # Check correct shape, in case input is the 3D array for the pair-wise flow 341 | arr_shape = np.shape(arr) 342 | if arr_shape==3: 343 | if arr_shape[1] != arr_shape[2]: 344 | raise ValueError("Input array not aligned. For 3D arrays shape (nt x N x N) is expected.") 345 | 346 | # Validate options for optional variable 'timespan' 347 | caselist = ['alltime', 'raise', 'decay'] 348 | if timespan not in caselist : 349 | raise ValueError( "Optional parameter 'timespan' requires one of the following values: %s" %str(caselist) ) 350 | 351 | # 1) DO THE CALCULATIONS 352 | # 1.1) Easy case. Integrate area-under-the-curve along whole time interval 353 | if timespan == 'alltime': 354 | totalflow = timestep * arr.sum(axis=0) 355 | 356 | # 1.2) Integrate area-under-the-curve until or from the peak time 357 | else: 358 | # Get the temporal indices at which the flow(s) peak 359 | tpidx = arr.argmax(axis=0) 360 | 361 | # Initialise the final array 362 | tf_shape = arr_shape[1:] 363 | totalflow = np.zeros(tf_shape, np.float64) 364 | 365 | # Sum the flow(s) over time, only in the desired time interval 366 | nsteps = arr_shape[0] 367 | for t in range(1,nsteps): 368 | # Check if the flow at time t should be accounted for or ignored 369 | if timespan == 'raise': 370 | counts = np.where(t < tpidx, True, False) 371 | elif timespan == 'decay': 372 | counts = np.where(t < tpidx, False, True) 373 | # Sum the flow at the given iteration, if accepted 374 | totalflow += (counts * arr[t]) 375 | 376 | # Finally, normalise the integral by the time-step 377 | totalflow *= timestep 378 | 379 | return totalflow 380 | 381 | 382 | 383 | 384 | 385 | ## 386 | -------------------------------------------------------------------------------- /netdynflow/netmodels.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2023, Gorka Zamora-López, Matthieu Gilson and Nikos E. Kouvaris 3 | # 4 | # 5 | # Released under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this software except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | """ 12 | Network and surrogate generation module 13 | ======================================= 14 | 15 | Functions to construct synthetic networks and generate surrogates out of given 16 | binary or weighted networks. 17 | 18 | Generation and randomization of binary graphs 19 | --------------------------------------------- 20 | These functions are all imported from the GAlib library 21 | (https://github.com/gorkazl/pyGAlib) 22 | Please see doctsring of module "galib.models" for a list of functions. :: 23 | 24 | >>> import galib 25 | >>> help(galib.models) 26 | 27 | Surrogates for weighted networks 28 | -------------------------------- 29 | ShuffleLinkWeights 30 | Randomly re-allocates the weights associated to the links. 31 | 32 | RandomiseWeightedNetwork 33 | Randomises a connectivity matrix and its weights. 34 | 35 | Spatially embedded (weighted) networks 36 | -------------------------------------- 37 | SpatialWeightSorting 38 | Sorts the link weights of a network by the spatial distance between nodes. 39 | SpatialLatticeFromNetwork 40 | Generates spatial weighted lattices with same weights as `con`. 41 | 42 | 43 | """ 44 | 45 | # Standard library imports 46 | 47 | # Third party packages 48 | import numpy as np 49 | import numpy.linalg 50 | import scipy.linalg 51 | # from numba import jit 52 | # Import GAlib for graph analysis and graph generation tools 53 | ### ACHTUNG!! For now I am importing GAlib but, for the long run, we must 54 | ### decide whether we want GAlib as a dependency of NetDynFlow, or we 55 | ### prefer to copy/paste the useful functions from GAlib into this module and 56 | ### therefore make NetDynFlow independent of GAlib. 57 | import galib 58 | from galib.models import* 59 | 60 | 61 | ## DETERMINISTIC GRAPH MODELS ################################################## 62 | # NOTE: See GAlib.models package 63 | 64 | 65 | ## RANDOM GRAPH MODELS ######################################################### 66 | # NOTE: See GAlib.models package 67 | 68 | 69 | ## GENERATION OF SURROGATE NETWORKS ############################################ 70 | def ShuffleLinkWeights(con): 71 | """ 72 | Randomly re-allocates the link weights of an input network. 73 | 74 | The function does not alter the position of the links, it only shuffles 75 | the weights associated to the links. Therefore, the binarised version 76 | is preserved. 77 | 78 | Parameters 79 | ---------- 80 | con : ndarray 81 | Adjacency matrix of the (weighted) network. 82 | 83 | Returns 84 | ------- 85 | newcon : ndarray of rank-2 and shape (N x N). 86 | A connectivity matrix with links between same nodes as `con` but the 87 | link weights shuffled. 88 | 89 | """ 90 | # 0) SECURITY CHECKS 91 | if not type(con) == numpy.ndarray: 92 | raise TypeError( "Please enter the connectivity matrix as a numpy array." ) 93 | con_shape = np.shape(con) 94 | if (len(con_shape) != 2) or (con_shape[0] != con_shape[1]): 95 | raise ValueError( "Input not aligned. 'con' should be a 2D array of shape (N x N)." ) 96 | 97 | # 1) EXTRACT THE CONSTRAINTS FROM THE con MATRIX 98 | N = con_shape[0] 99 | nzidx = con.nonzero() 100 | weights = con[nzidx] 101 | 102 | # 2) GENERATE THE NEW NETWORK WITH THE WEIGHTS SHUFFLED 103 | numpy.random.shuffle(weights) 104 | newcon = np.zeros((N,N), dtype=con.dtype) 105 | newcon[nzidx] = weights 106 | 107 | return newcon 108 | 109 | # @jit 110 | def RandomiseWeightedNetwork(con): 111 | """ 112 | Randomises a connectivity matrix and its weights. 113 | 114 | Returns a random connectivity matrix (Erdos-Renyi-type) with the same number 115 | of links and same link weights as the input matrix `con`. Therefore, both 116 | the total weight (sum of link weights) and the distribution of link weights 117 | are conserved, but the input/output degrees of the nodes, or their individual 118 | strengths, are not conserved. 119 | 120 | The function identifies some properties of `con` in order to conserve 121 | elementary properties of `con`. For example: 122 | (1) The resulting random weighted network will only contain self-connections 123 | (non-zero diagonal entries) if `con` contains self-connections. 124 | (2) If `con` is an unweighted adjacency matrix (directed or undirected), the 125 | result is an Erdos-Renyi-type random graph (directed or undirected), 126 | of same size and number of links as `con`. 127 | (3) If `con` is an undirected network but contains asymmetric link weights, 128 | the result will be an undirected random graph with asymmetric weights. 129 | (4) If `con` is a directed weighted network, the result will be a directed 130 | and weighted network. In this case, weights cannot be symmetric. 131 | 132 | Parameters 133 | ---------- 134 | con : ndarray 135 | Adjacency matrix of the (weighted) network. 136 | 137 | Returns 138 | ------- 139 | newcon : ndarray of rank-2 and shape (N x N) 140 | A connectivity matrix with links between same nodes as `con` but the 141 | link weights shuffled. 142 | 143 | """ 144 | # 0) SECURITY CHECKS 145 | if not type(con) == numpy.ndarray: 146 | raise TypeError( "Please enter the connectivity matrix as a numpy array." ) 147 | con_shape = np.shape(con) 148 | if (len(con_shape) != 2) or (con_shape[0] != con_shape[1]): 149 | raise ValueError( "Input not aligned. 'con' should be a 2D array of shape (N x N)." ) 150 | 151 | # 1) EXTRACT INFORMATION NEEDED FROM THE con MATRIX 152 | N = con_shape[0] 153 | 154 | # Find out whether con is symmetric 155 | if abs(con - con.T).sum() == 0: 156 | symmetric = True 157 | else: 158 | symmetric = False 159 | 160 | # Find out whether con is directed and calculate the number of links 161 | if Reciprocity(con) == 1.0: 162 | directed = False 163 | L = int( round(0.5*con.astype(bool).sum()) ) 164 | else: 165 | directed = True 166 | L = con.astype(bool).sum() 167 | 168 | # Find out whether `con` allows self-loops (non-zero diagonal elements) 169 | if con.trace() == 0: 170 | selfloops = False 171 | else: 172 | selfloops = True 173 | 174 | # Get the weights, as a 1D array 175 | if symmetric: 176 | nzidx = np.triu(con, k=1).nonzero() 177 | weights = con[nzidx] 178 | else: 179 | nzidx = con.nonzero() 180 | weights = con[nzidx] 181 | 182 | # 2) GENERATE THE NEW NETWORK WITH THE WEIGHTS SHUFFLED 183 | # Initialise the matrix. Give same dtype as `con` 184 | newcon = np.zeros((N,N), dtype=con.dtype) 185 | 186 | # Shuffle the list of weights 187 | numpy.random.shuffle(weights) 188 | 189 | # Finally, add the links at random 190 | counter = 0 191 | while counter < L: 192 | # 2.1) Pick up two nodes at random 193 | source = int(N * numpy.random.rand()) 194 | target = int(N * numpy.random.rand()) 195 | 196 | # 2.2) Check if they can be linked, otherwise look for another pair 197 | if newcon[source,target]: continue 198 | if source == target and not selfloops: continue 199 | 200 | # 2.3) Perform the rewiring 201 | newcon[source,target] = weights[counter] 202 | if not directed and symmetric: 203 | newcon[target,source] = weights[counter] 204 | elif not directed and not symmetric: 205 | newcon[target,source] = weights[-(counter+1)] 206 | counter += 1 207 | 208 | return newcon 209 | 210 | 211 | ## SPATIALLY EMBEDDED SURROGATES ############################################### 212 | def SpatialWeightSorting(con, distmat, descending=True): 213 | """Sorts the link weights of a network by the spatial distance between nodes. 214 | 215 | The function reads the weights from a connectivity matrix and re-allocates 216 | them according to the euclidean distance between the nodes. The sorting 217 | conserves the position of the links, therefore, if `con` is a binary graph, 218 | the function will return a copy of `con`. The distance between nodes shall 219 | be given as input `distmat`. 220 | 221 | If descending = True, the larger weigths are assigned to the links between 222 | closer nodes, and the smaller weights to the links between distant nodes. 223 | 224 | If descending = False, the larger weights are assigned to the links between 225 | distant nodes, and the smaller weights to links between close nodes. 226 | 227 | Parameters 228 | ---------- 229 | con : ndarray, rank-2. 230 | Adjacency matrix of the (weighted) network. 231 | distmat : ndarray, rank-2. 232 | A matrix containing the spatial distance between all pair of ROIs. 233 | This can be either the euclidean distance, the fiber length or any 234 | other geometric distance. 235 | descending : boolean, optional. 236 | Determines whether links weights are assigend in descending or in 237 | ascending order, according to the euclidean distance between the nodes. 238 | 239 | Returns 240 | ------- 241 | newcon : ndarray of rank-2 and shape (N x N). 242 | Connectivity matrix with weights sorted according to spatial distance 243 | between the nodes. 244 | 245 | """ 246 | # 0) SECURITY CHECKS 247 | con_shape = np.shape(con) 248 | dist_shape = np.shape(distmat) 249 | if con_shape != dist_shape: 250 | raise ValueError( "Data not aligned. 'con' and 'distmat' of same shape expectted. " ) 251 | 252 | # 1) EXTRACT THE NEEDED INFORMATION FROM THE con MATRIX 253 | N = len(con) 254 | # The indices of the links and their weights, distance 255 | nzidx = con.nonzero() 256 | weights = con[nzidx] 257 | distances = distmat[nzidx] 258 | 259 | # 2) SORT THE WEIGHTS IN DESCENDING ORDER 260 | weights.sort() 261 | if descending: 262 | weights = weights[::-1] 263 | 264 | # Get the indices that would sort the links by distance 265 | sortdistidx = distances.argsort() 266 | newidx = (nzidx[0][sortdistidx], nzidx[1][sortdistidx]) 267 | 268 | # 3) CREATE THE NEW CONNECTIVITY WITH THE LINK WEIGHTS SORTED SPATIALLY 269 | newcon = np.zeros((N,N), np.float64) 270 | newcon[newidx] = weights 271 | 272 | return newcon 273 | 274 | def SpatialLatticeFromNetwork(con, distmat, descending=True): 275 | """Generates spatial weighted lattices with same weights as `con`. 276 | 277 | The function reads the weights from a connectivity matrix and generates a 278 | spatially embedded weighted lattice, assigning the largest weights in 279 | descending order to the nodes that are closer from each other. Therefore, 280 | it requires also the euclidean distance between the nodes is given as input. 281 | 282 | If `con` is a binary graph of L links, the function returns a graph with 283 | links between the L spatially closest pairs of nodes. 284 | 285 | If `descending = True`, the larger weigths are assigned to the links between 286 | closer nodes, and the smaller weights to the links between distant nodes. 287 | 288 | If `descending = False`, the larger weights are assigned to the links between 289 | distant nodes, and the smaller weights to links between close nodes. 290 | 291 | Note 292 | ---- 293 | Even if `con` is either a directed network or undirected but with asymmetric 294 | weights, the resulting lattice will be undirected and (quasi-)symmetric 295 | due to the fact that the spatial distance between two nodes is symmetric. 296 | 297 | Parameters 298 | ---------- 299 | con : ndarray, rank-2. 300 | Adjacency matrix of the (weighted) network. 301 | distmat : ndarray, rank-2. 302 | A matrix containing the spatial distance between all pair of ROIs. 303 | This can be either the euclidean distance, the fiber length or any 304 | other geometric distance. 305 | descending : boolean, optional. 306 | Determines whether links weights are assigend in descending or in 307 | ascending order, according to the euclidean distance between the nodes. 308 | 309 | Returns 310 | ------- 311 | newcon : ndarray of rank-2 and shape (N x N). 312 | Connectivity matrix of a weighted lattice. 313 | 314 | """ 315 | # 0) SECURITY CHECKS 316 | con_shape = np.shape(con) 317 | dist_shape = np.shape(distmat) 318 | if con_shape != dist_shape: 319 | raise ValueError( "Data not aligned. 'con' and 'distmat' of same shape expectted. " ) 320 | 321 | # 1) EXTRACT THE NEEDED INFORMATION FROM THE con MATRIX 322 | N = len(con) 323 | 324 | # Sort the weights of the network 325 | weights = con.flatten() 326 | weights.sort() 327 | if descending: 328 | weights = weights[::-1] 329 | 330 | # Find the indices that sort the euclidean distances, from shorter to longer 331 | if descending: 332 | distmat[np.diag_indices(N)] = np.inf 333 | else: 334 | distmat[np.diag_indices(N)] = 0.0 335 | distances = distmat.ravel() 336 | sortdistidx = distances.argsort() 337 | newidx = np.unravel_index( sortdistidx, (N,N) ) 338 | 339 | # And finally, create the coonectivity matrix with the weights sorted 340 | newcon = np.zeros((N,N), np.float64) 341 | newcon[newidx] = weights 342 | 343 | return newcon 344 | 345 | 346 | 347 | ## 348 | -------------------------------------------------------------------------------- /netdynflow/tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2023, Gorka Zamora-López, Matthieu Gilson and Nikos E. Kouvaris 3 | # 4 | # 5 | # Released under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this software except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | """ 12 | Miscelaneous tools and helpers 13 | ============================== 14 | 15 | Module for any extra helper functionality that doesn't fit as core, metric 16 | or network generator categories. 17 | 18 | NNt2tNN 19 | Transposes a 3D array from shape (N,N,nt) to (nt,N,N) shape. 20 | tNN2NNt 21 | Transposes a 3D array from shape (nt,N,N) to (N,N,nt) shape. 22 | Reciprocity 23 | Computes the fraction of reciprocal links in a graph. 24 | 25 | """ 26 | # Standard library imports 27 | 28 | # Third party packages 29 | import numpy as np 30 | import numpy.random 31 | 32 | 33 | ## MISCELLANEOUS FUNCTIONS ##################################################### 34 | def NNt2tNN(tensor): 35 | """Transposes a 3D array from shape (N,N,nt) to (nt,N,N), 36 | 37 | where N is the number of nodes in a network (features) and nt is the 38 | number of time-points (samples). 39 | 40 | Parameters 41 | ---------- 42 | tensor : ndarray of rank-3. 43 | Temporal evolution of the N x N elements in a matrix, arranged with 44 | shape (N,N,nt). 45 | 46 | Returns 47 | ------- 48 | newtensor : ndarray of rank-3. 49 | Same as input 'tensor' but in shape (nt,N,N). Matrix rows an columns at 50 | each slice in 'tensor' are conserved row and columns in 'newtensor'. 51 | 52 | Notes 53 | ----- 54 | Please remind that np.transpose() function returns a view of the array, 55 | not a copy! If you want a copy with the entries properly sorted in memory, 56 | call the function as follows: 57 | 58 | >>> arr2 = np.copy( NNt2tNN(arr1), order='C' ) 59 | 60 | """ 61 | # Security checks 62 | assert len(np.shape(tensor)) == 3, "3D array required." 63 | n0, n1, n2 = np.shape(tensor) 64 | if n0 != n1: 65 | raise TypeError("3D array of shape (N,N,nt) required.") 66 | 67 | # Transpose the array 68 | newtensor = np.transpose(tensor, axes=(2,0,1)) 69 | return newtensor 70 | 71 | def tNN2NNt(tensor): 72 | """Transposes a 3D array from shape (nt,N,N) to (N,N,nt), 73 | 74 | where N is the number of nodes in a network (features) and nt is the 75 | number of time-points (samples). 76 | 77 | Parameters 78 | ---------- 79 | tensor : ndarray of rank-3. 80 | Temporal evolution of the N x N elements in a matrix, arranged with 81 | shape (nt,N,N). 82 | 83 | Returns 84 | ------- 85 | newtensor : ndarray of rank-3. 86 | Same as input 'tensor' but in shape (N,N,nt). Matrix rows an columns at 87 | each slice in 'tensor' are conserved row and columns in 'newtensor'. 88 | 89 | Notes 90 | ----- 91 | Please remind that np.transpose() function returns a view of the array, 92 | not a copy! If you want a copy with the entries properly sorted in memory, 93 | call the function as follows: 94 | 95 | >>> arr2 = np.copy( tNN2NNt(arr1), order='C' ) 96 | 97 | """ 98 | # Security checks 99 | assert len(np.shape(tensor)) == 3, "3D array required." 100 | n0, n1, n2 = np.shape(tensor) 101 | if n1 != n2: 102 | raise TypeError("3D array of shape (nt,N,N) required.") 103 | 104 | newtensor = np.transpose(tensor, axes=(1,2,0)) 105 | return newtensor 106 | 107 | def Reciprocity(adjmatrix): 108 | """Computes the fraction of reciprocal links to total number of links. 109 | 110 | Both weighted and unweighted input matrices are permitted. Weights 111 | are ignored for the calculation. 112 | Parameters 113 | ---------- 114 | adjmatrix : ndarray of rank-2 115 | The adjacency matrix of the network. 116 | Returns 117 | ------- 118 | reciprocity : float 119 | A scalar value between 0 (for acyclic directed networks) and 1 (for 120 | fully reciprocal). 121 | """ 122 | # 0) PREPARE FOR COMPUTATIONS 123 | adjmatrix = adjmatrix.astype('bool') 124 | 125 | # 1) COMPUTE THE RECIPROCITY 126 | L = adjmatrix.sum() 127 | if L == 0: 128 | reciprocity = 0 129 | else: 130 | # Find the assymmetric links 131 | # Rest = np.abs(adjmatrix - adjmatrix.T) 132 | Rest = np.abs(adjmatrix ^ adjmatrix.T) 133 | Lsingle = 0.5*Rest.sum() 134 | reciprocity = float(L-Lsingle) / L 135 | 136 | return reciprocity 137 | 138 | 139 | 140 | 141 | ## 142 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | '''setup.py''' 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | with open("requirements.txt") as reqs_file: 7 | REQS = [line.rstrip() for line in reqs_file.readlines() if line[0] not in ['\n', '-', '#']] 8 | 9 | setup( 10 | name = 'netdynflow', 11 | description = 'A package to study complex networks based on temporal flow propagations.', 12 | url = 'https://github.com/gorkazl/NetDynFlow', 13 | version = '1.0.2', 14 | license = 'Apache License 2.0', 15 | 16 | author = 'Gorka Zamora-Lopez, Matthieu Gilson, Nikos Kouvaris', 17 | author_email = 'galib@zamora-lopez.xyz', 18 | 19 | install_requires = REQS, 20 | packages = find_packages(exclude=['doc', '*tests*']), 21 | scripts = [], 22 | include_package_data = True, 23 | 24 | keywords = 'graph theory, complex networks, network analysis, weighted networks', 25 | classifiers = [ 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Intended Audience :: Developers', 28 | 'Intended Audience :: Education', 29 | 'Intended Audience :: Science/Research', 30 | 'License :: OSI Approved :: Apache Software License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3', 34 | 'Topic :: Scientific/Engineering :: Bio-Informatics', 35 | 'Topic :: Scientific/Engineering :: Information Analysis', 36 | 'Topic :: Scientific/Engineering :: Mathematics', 37 | 'Topic :: Scientific/Engineering :: Physics', 38 | 'Topic :: Software Development :: Libraries :: Python Modules' 39 | ] 40 | ) 41 | --------------------------------------------------------------------------------