qmm-core 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qmm_core-0.2.2/LICENSE +28 -0
- qmm_core-0.2.2/PKG-INFO +100 -0
- qmm_core-0.2.2/README.md +76 -0
- qmm_core-0.2.2/pyproject.toml +40 -0
- qmm_core-0.2.2/qmm/__init__.py +90 -0
- qmm_core-0.2.2/qmm/core/helper.py +324 -0
- qmm_core-0.2.2/qmm/core/prediction.py +74 -0
- qmm_core-0.2.2/qmm/core/press.py +119 -0
- qmm_core-0.2.2/qmm/core/stability.py +367 -0
- qmm_core-0.2.2/qmm/core/structure.py +120 -0
- qmm_core-0.2.2/qmm_core.egg-info/PKG-INFO +100 -0
- qmm_core-0.2.2/qmm_core.egg-info/SOURCES.txt +15 -0
- qmm_core-0.2.2/qmm_core.egg-info/dependency_links.txt +1 -0
- qmm_core-0.2.2/qmm_core.egg-info/requires.txt +9 -0
- qmm_core-0.2.2/qmm_core.egg-info/top_level.txt +2 -0
- qmm_core-0.2.2/setup.cfg +4 -0
- qmm_core-0.2.2/tests/test_qmm.py +86 -0
qmm_core-0.2.2/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Jayden Hyman
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
qmm_core-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: qmm-core
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: A software platform for analysing the structure and function of complex systems
|
|
5
|
+
Author-email: Jayden Hyman <j.hyman@uq.edu.au>
|
|
6
|
+
Project-URL: Homepage, https://github.com/jaydenhyman/qmm
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/jaydenhyman/qmm/issues
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: pytest
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: sympy
|
|
18
|
+
Requires-Dist: networkx
|
|
19
|
+
Requires-Dist: graphviz
|
|
20
|
+
Requires-Dist: numba
|
|
21
|
+
Requires-Dist: pandas
|
|
22
|
+
Requires-Dist: seaborn
|
|
23
|
+
Requires-Dist: matplotlib
|
|
24
|
+
|
|
25
|
+
#  QMM: Qualitative Mathematical Modelling
|
|
26
|
+
|
|
27
|
+
QMM is a software platform for analysing the structure and function of complex systems.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Interactive web application for creating signed digraph (network) models representing the mathematical structure of a complex system.
|
|
32
|
+
- Python package (`qmm`) for qualitative mathematical modelling, including core modules for defining model structure, stability analysis, press perturbation analysis and making qualitative predictions.
|
|
33
|
+
|
|
34
|
+
## Contact
|
|
35
|
+
|
|
36
|
+
For any additional information or questions, please contact:
|
|
37
|
+
|
|
38
|
+
Jayden Hyman: <j.hyman@uq.edu.au>
|
|
39
|
+
|
|
40
|
+
## How to use
|
|
41
|
+
|
|
42
|
+
1. Install Python and required packages:
|
|
43
|
+
|
|
44
|
+
Option 1: Using Anaconda and JupyterLab (Recommended)
|
|
45
|
+
|
|
46
|
+
a. Install Anaconda from <https://www.anaconda.com/products/distribution>
|
|
47
|
+
|
|
48
|
+
b. Launch Anaconda Navigator
|
|
49
|
+
|
|
50
|
+
c. Create a new environment:
|
|
51
|
+
- Click on "Environments" in the left sidebar
|
|
52
|
+
- Click "Create" at the bottom
|
|
53
|
+
- Name your environment (e.g., "qmm") and select Python 3.10
|
|
54
|
+
- Click "Create"
|
|
55
|
+
- In the "Environments" list, click on your newly created environment
|
|
56
|
+
|
|
57
|
+
d. Install JupyterLab:
|
|
58
|
+
- With your new environment selected, go to the "Home" tab
|
|
59
|
+
- Find JupyterLab in the list of applications
|
|
60
|
+
- Click "Install"
|
|
61
|
+
|
|
62
|
+
This method uses Anaconda's default packages, which include NumPy, SymPy, NetworkX, Pandas, Numba, and Graphviz. JupyterLab provides an integrated development environment for running QMM package functions.
|
|
63
|
+
|
|
64
|
+
Option 2: Using Miniconda
|
|
65
|
+
|
|
66
|
+
a. Install Miniconda from <https://docs.conda.io/en/latest/miniconda.html>
|
|
67
|
+
|
|
68
|
+
b. Open a Miniconda prompt and create a new environment:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
conda create -n qmm python=3.10
|
|
72
|
+
conda activate qmm
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
c. Install the required packages:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
conda install numpy=1.26.4 networkx=3.3 pandas=2.0.2 numba=0.60.0 sympy=1.13
|
|
79
|
+
conda install -c conda-forge graphviz=0.20.3
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
2. Use the web application to create signed digraph models: [Open in browser](https://d2x70551if0frn.cloudfront.net/)
|
|
83
|
+
|
|
84
|
+
3. The `qmm.ipynb` file provides core functions to analyse signed digraph models. To get started with analysing your model, open this file in JupyterLab or your preferred Python IDE.
|
|
85
|
+
|
|
86
|
+
## Documentation
|
|
87
|
+
|
|
88
|
+
Detailed documentation for the `qmm` package and its modules is not currently available.
|
|
89
|
+
|
|
90
|
+
## Licensing
|
|
91
|
+
|
|
92
|
+
This model is licensed under a BSD 3-Clause License. See LICENSE.md for further information.
|
|
93
|
+
|
|
94
|
+
## Attribution
|
|
95
|
+
|
|
96
|
+
A Zenodo will be available in the near future for attribution.
|
|
97
|
+
|
|
98
|
+
## Contributing
|
|
99
|
+
|
|
100
|
+
We welcome contributions to improve and expand the QMM software. As the project is in its early stages of development, we appreciate your patience and support in helping us refine the software.
|
qmm_core-0.2.2/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#  QMM: Qualitative Mathematical Modelling
|
|
2
|
+
|
|
3
|
+
QMM is a software platform for analysing the structure and function of complex systems.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Interactive web application for creating signed digraph (network) models representing the mathematical structure of a complex system.
|
|
8
|
+
- Python package (`qmm`) for qualitative mathematical modelling, including core modules for defining model structure, stability analysis, press perturbation analysis and making qualitative predictions.
|
|
9
|
+
|
|
10
|
+
## Contact
|
|
11
|
+
|
|
12
|
+
For any additional information or questions, please contact:
|
|
13
|
+
|
|
14
|
+
Jayden Hyman: <j.hyman@uq.edu.au>
|
|
15
|
+
|
|
16
|
+
## How to use
|
|
17
|
+
|
|
18
|
+
1. Install Python and required packages:
|
|
19
|
+
|
|
20
|
+
Option 1: Using Anaconda and JupyterLab (Recommended)
|
|
21
|
+
|
|
22
|
+
a. Install Anaconda from <https://www.anaconda.com/products/distribution>
|
|
23
|
+
|
|
24
|
+
b. Launch Anaconda Navigator
|
|
25
|
+
|
|
26
|
+
c. Create a new environment:
|
|
27
|
+
- Click on "Environments" in the left sidebar
|
|
28
|
+
- Click "Create" at the bottom
|
|
29
|
+
- Name your environment (e.g., "qmm") and select Python 3.10
|
|
30
|
+
- Click "Create"
|
|
31
|
+
- In the "Environments" list, click on your newly created environment
|
|
32
|
+
|
|
33
|
+
d. Install JupyterLab:
|
|
34
|
+
- With your new environment selected, go to the "Home" tab
|
|
35
|
+
- Find JupyterLab in the list of applications
|
|
36
|
+
- Click "Install"
|
|
37
|
+
|
|
38
|
+
This method uses Anaconda's default packages, which include NumPy, SymPy, NetworkX, Pandas, Numba, and Graphviz. JupyterLab provides an integrated development environment for running QMM package functions.
|
|
39
|
+
|
|
40
|
+
Option 2: Using Miniconda
|
|
41
|
+
|
|
42
|
+
a. Install Miniconda from <https://docs.conda.io/en/latest/miniconda.html>
|
|
43
|
+
|
|
44
|
+
b. Open a Miniconda prompt and create a new environment:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
conda create -n qmm python=3.10
|
|
48
|
+
conda activate qmm
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
c. Install the required packages:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
conda install numpy=1.26.4 networkx=3.3 pandas=2.0.2 numba=0.60.0 sympy=1.13
|
|
55
|
+
conda install -c conda-forge graphviz=0.20.3
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2. Use the web application to create signed digraph models: [Open in browser](https://d2x70551if0frn.cloudfront.net/)
|
|
59
|
+
|
|
60
|
+
3. The `qmm.ipynb` file provides core functions to analyse signed digraph models. To get started with analysing your model, open this file in JupyterLab or your preferred Python IDE.
|
|
61
|
+
|
|
62
|
+
## Documentation
|
|
63
|
+
|
|
64
|
+
Detailed documentation for the `qmm` package and its modules is not currently available.
|
|
65
|
+
|
|
66
|
+
## Licensing
|
|
67
|
+
|
|
68
|
+
This model is licensed under a BSD 3-Clause License. See LICENSE.md for further information.
|
|
69
|
+
|
|
70
|
+
## Attribution
|
|
71
|
+
|
|
72
|
+
A Zenodo will be available in the near future for attribution.
|
|
73
|
+
|
|
74
|
+
## Contributing
|
|
75
|
+
|
|
76
|
+
We welcome contributions to improve and expand the QMM software. As the project is in its early stages of development, we appreciate your patience and support in helping us refine the software.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qmm-core"
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Jayden Hyman", email="j.hyman@uq.edu.au" },
|
|
10
|
+
]
|
|
11
|
+
description = "A software platform for analysing the structure and function of complex systems"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: BSD License",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"pytest",
|
|
22
|
+
"numpy",
|
|
23
|
+
"sympy",
|
|
24
|
+
"networkx",
|
|
25
|
+
"graphviz",
|
|
26
|
+
"numba",
|
|
27
|
+
"pandas",
|
|
28
|
+
"seaborn",
|
|
29
|
+
"matplotlib",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
"Homepage" = "https://github.com/jaydenhyman/qmm"
|
|
34
|
+
"Bug Tracker" = "https://github.com/jaydenhyman/qmm/issues"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
exclude = ["tests*"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
qmm = ["py.typed"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from .core.structure import (
|
|
2
|
+
import_digraph,
|
|
3
|
+
create_matrix,
|
|
4
|
+
create_equations,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from .core.stability import (
|
|
8
|
+
sign_stability,
|
|
9
|
+
system_feedback,
|
|
10
|
+
net_feedback,
|
|
11
|
+
absolute_feedback,
|
|
12
|
+
weighted_feedback,
|
|
13
|
+
feedback_metrics,
|
|
14
|
+
hurwitz_determinants,
|
|
15
|
+
net_determinants,
|
|
16
|
+
absolute_determinants,
|
|
17
|
+
weighted_determinants,
|
|
18
|
+
determinants_metrics,
|
|
19
|
+
conditional_stability,
|
|
20
|
+
simulation_stability,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .core.press import (
|
|
24
|
+
adjoint_matrix,
|
|
25
|
+
absolute_feedback_matrix,
|
|
26
|
+
weighted_predictions_matrix,
|
|
27
|
+
sign_determinacy_matrix,
|
|
28
|
+
numerical_simulations,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from .core.prediction import (
|
|
32
|
+
table_of_predictions,
|
|
33
|
+
compare_predictions,
|
|
34
|
+
create_plot,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from .core.helper import (
|
|
38
|
+
list_to_digraph,
|
|
39
|
+
digraph_to_list,
|
|
40
|
+
powerplay_labels,
|
|
41
|
+
perm,
|
|
42
|
+
get_nodes,
|
|
43
|
+
get_positive,
|
|
44
|
+
get_negative,
|
|
45
|
+
get_weight,
|
|
46
|
+
sign_determinacy,
|
|
47
|
+
display_digraph,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# structure.py
|
|
52
|
+
"import_digraph",
|
|
53
|
+
"display_digraph",
|
|
54
|
+
"create_matrix",
|
|
55
|
+
"create_equations",
|
|
56
|
+
# stability.py
|
|
57
|
+
"sign_stability",
|
|
58
|
+
"system_feedback",
|
|
59
|
+
"net_feedback",
|
|
60
|
+
"absolute_feedback",
|
|
61
|
+
"weighted_feedback",
|
|
62
|
+
"feedback_metrics",
|
|
63
|
+
"hurwitz_determinants",
|
|
64
|
+
"net_determinants",
|
|
65
|
+
"absolute_determinants",
|
|
66
|
+
"weighted_determinants",
|
|
67
|
+
"determinants_metrics",
|
|
68
|
+
"conditional_stability",
|
|
69
|
+
"simulation_stability",
|
|
70
|
+
# press.py
|
|
71
|
+
"adjoint_matrix",
|
|
72
|
+
"absolute_feedback_matrix",
|
|
73
|
+
"weighted_predictions_matrix",
|
|
74
|
+
"sign_determinacy_matrix",
|
|
75
|
+
"numerical_simulations",
|
|
76
|
+
# prediction.py
|
|
77
|
+
"table_of_predictions",
|
|
78
|
+
"compare_predictions",
|
|
79
|
+
"create_plot",
|
|
80
|
+
# helper.py
|
|
81
|
+
"list_to_digraph",
|
|
82
|
+
"digraph_to_list",
|
|
83
|
+
"powerplay_labels",
|
|
84
|
+
"perm",
|
|
85
|
+
"get_nodes",
|
|
86
|
+
"get_positive",
|
|
87
|
+
"get_negative",
|
|
88
|
+
"get_weight",
|
|
89
|
+
"sign_determinacy",
|
|
90
|
+
]
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import sympy as sp
|
|
3
|
+
import networkx as nx
|
|
4
|
+
from numba import jit
|
|
5
|
+
import graphviz
|
|
6
|
+
|
|
7
|
+
def list_to_digraph(matrix, ids=None):
|
|
8
|
+
if not isinstance(matrix, (list, np.ndarray)):
|
|
9
|
+
raise ValueError("Input must be a list of lists or a numpy array")
|
|
10
|
+
if isinstance(matrix, list):
|
|
11
|
+
matrix = np.array(matrix)
|
|
12
|
+
if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
|
|
13
|
+
raise ValueError("Input must be a square matrix")
|
|
14
|
+
G = nx.DiGraph()
|
|
15
|
+
n = matrix.shape[0]
|
|
16
|
+
if ids is None:
|
|
17
|
+
node_ids = [str(i) for i in range(1, n + 1)]
|
|
18
|
+
else:
|
|
19
|
+
if len(ids) != n:
|
|
20
|
+
raise ValueError("Number of ids must match matrix dimensions")
|
|
21
|
+
node_ids = ids
|
|
22
|
+
G.add_nodes_from(node_ids)
|
|
23
|
+
for i in range(n):
|
|
24
|
+
for j in range(n):
|
|
25
|
+
if matrix[i][j] != 0:
|
|
26
|
+
G.add_edge(node_ids[j], node_ids[i], sign=int(matrix[i][j]))
|
|
27
|
+
nx.set_node_attributes(G, "state", "category")
|
|
28
|
+
return G
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def digraph_to_list(G):
|
|
32
|
+
if not isinstance(G, nx.DiGraph):
|
|
33
|
+
raise TypeError("Input must be a networkx.DiGraph.")
|
|
34
|
+
n = G.number_of_nodes()
|
|
35
|
+
nodes = sorted(G.nodes())
|
|
36
|
+
node_to_index = {node: i for i, node in enumerate(nodes)}
|
|
37
|
+
matrix = [[0 for _ in range(n)] for _ in range(n)]
|
|
38
|
+
for source, target, data in G.edges(data=True):
|
|
39
|
+
i, j = node_to_index[source], node_to_index[target]
|
|
40
|
+
sign = data.get("sign", 1)
|
|
41
|
+
matrix[j][i] = sign
|
|
42
|
+
return str(matrix)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def powerplay_labels(input_str):
|
|
46
|
+
return [item.split(": ")[1] for item in input_str.split(", ")]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def perm(A, method="glynn"):
|
|
50
|
+
if not isinstance(A, np.ndarray):
|
|
51
|
+
raise TypeError("Input matrix must be a NumPy array.")
|
|
52
|
+
matshape = A.shape
|
|
53
|
+
if matshape[0] != matshape[1]:
|
|
54
|
+
raise ValueError("Input matrix must be square.")
|
|
55
|
+
if np.isnan(A).any():
|
|
56
|
+
raise ValueError("Input matrix must not contain NaNs.")
|
|
57
|
+
if matshape[0] == 0:
|
|
58
|
+
return A.dtype.type(1.0)
|
|
59
|
+
if matshape[0] == 1:
|
|
60
|
+
return A[0, 0]
|
|
61
|
+
if matshape[0] == 2:
|
|
62
|
+
return A[0, 0] * A[1, 1] + A[0, 1] * A[1, 0]
|
|
63
|
+
if matshape[0] == 3:
|
|
64
|
+
return (
|
|
65
|
+
A[0, 2] * A[1, 1] * A[2, 0]
|
|
66
|
+
+ A[0, 1] * A[1, 2] * A[2, 0]
|
|
67
|
+
+ A[0, 2] * A[1, 0] * A[2, 1]
|
|
68
|
+
+ A[0, 0] * A[1, 2] * A[2, 1]
|
|
69
|
+
+ A[0, 1] * A[1, 0] * A[2, 2]
|
|
70
|
+
+ A[0, 0] * A[1, 1] * A[2, 2]
|
|
71
|
+
)
|
|
72
|
+
return _ryser(A) if method != "glynn" else _glynn(A)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@jit(nopython=True)
|
|
76
|
+
def _ryser(A):
|
|
77
|
+
n = len(A)
|
|
78
|
+
if n == 0:
|
|
79
|
+
return A.dtype.type(1.0)
|
|
80
|
+
row_comb = np.zeros((n), dtype=A.dtype)
|
|
81
|
+
total = 0
|
|
82
|
+
old_grey = 0
|
|
83
|
+
sign = +1
|
|
84
|
+
binary_power_dict = [2**i for i in range(n)]
|
|
85
|
+
num_loops = 2**n
|
|
86
|
+
for k in range(0, num_loops):
|
|
87
|
+
bin_index = (k + 1) % num_loops
|
|
88
|
+
reduced = np.prod(row_comb)
|
|
89
|
+
total += sign * reduced
|
|
90
|
+
new_grey = bin_index ^ (bin_index // 2)
|
|
91
|
+
grey_diff = old_grey ^ new_grey
|
|
92
|
+
grey_diff_index = binary_power_dict.index(grey_diff)
|
|
93
|
+
new_vector = A[grey_diff_index]
|
|
94
|
+
direction = (old_grey > new_grey) - (old_grey < new_grey)
|
|
95
|
+
for i in range(n):
|
|
96
|
+
row_comb[i] += new_vector[i] * direction
|
|
97
|
+
sign = -sign
|
|
98
|
+
old_grey = new_grey
|
|
99
|
+
return total
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@jit(nopython=True)
|
|
103
|
+
def _glynn(A):
|
|
104
|
+
n = len(A)
|
|
105
|
+
if n == 0:
|
|
106
|
+
return A.dtype.type(1.0)
|
|
107
|
+
row_comb = np.sum(A, 0)
|
|
108
|
+
total = 0
|
|
109
|
+
old_gray = 0
|
|
110
|
+
sign = +1
|
|
111
|
+
binary_power_dict = [2**i for i in range(n)]
|
|
112
|
+
num_loops = 2 ** (n - 1)
|
|
113
|
+
for bin_index in range(1, num_loops + 1):
|
|
114
|
+
reduced = np.prod(row_comb)
|
|
115
|
+
total += sign * reduced
|
|
116
|
+
new_gray = bin_index ^ (bin_index // 2)
|
|
117
|
+
gray_diff = old_gray ^ new_gray
|
|
118
|
+
gray_diff_index = binary_power_dict.index(gray_diff)
|
|
119
|
+
new_vector = A[gray_diff_index]
|
|
120
|
+
direction = 2 * ((old_gray > new_gray) - (old_gray < new_gray))
|
|
121
|
+
for i in range(n):
|
|
122
|
+
row_comb[i] += new_vector[i] * direction
|
|
123
|
+
sign = -sign
|
|
124
|
+
old_gray = new_gray
|
|
125
|
+
return total / num_loops
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_nodes(G, node_type="state", labels=False):
|
|
129
|
+
if not isinstance(G, nx.DiGraph):
|
|
130
|
+
raise TypeError("Input must be a networkx.DiGraph.")
|
|
131
|
+
|
|
132
|
+
if node_type == "all":
|
|
133
|
+
return list(G.nodes()) if not labels else list(G.nodes(data=True))
|
|
134
|
+
else:
|
|
135
|
+
return [
|
|
136
|
+
n if not labels else d.get("label", n)
|
|
137
|
+
for n, d in G.nodes(data=True)
|
|
138
|
+
if d.get("category") == node_type
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_weight(net, absolute, no_effect=sp.nan):
|
|
143
|
+
if net.shape != absolute.shape:
|
|
144
|
+
raise ValueError("Matrices must have the same shape")
|
|
145
|
+
result = sp.zeros(*net.shape)
|
|
146
|
+
for i in range(net.shape[0]):
|
|
147
|
+
for j in range(net.shape[1]):
|
|
148
|
+
if absolute[i, j] == 0:
|
|
149
|
+
result[i, j] = no_effect
|
|
150
|
+
else:
|
|
151
|
+
result[i, j] = net[i, j] / absolute[i, j]
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_positive(net, absolute):
|
|
156
|
+
if net.shape != absolute.shape:
|
|
157
|
+
raise ValueError("Matrices must have the same shape")
|
|
158
|
+
result = sp.zeros(*net.shape)
|
|
159
|
+
for i in range(net.shape[0]):
|
|
160
|
+
for j in range(net.shape[1]):
|
|
161
|
+
result[i, j] = (net[i, j] + absolute[i, j]) // 2
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def get_negative(net, absolute):
|
|
166
|
+
if net.shape != absolute.shape:
|
|
167
|
+
raise ValueError("Matrices must have the same shape")
|
|
168
|
+
result = sp.zeros(*net.shape)
|
|
169
|
+
for i in range(net.shape[0]):
|
|
170
|
+
for j in range(net.shape[1]):
|
|
171
|
+
result[i, j] = (absolute[i, j] - net[i, j]) // 2
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def sign_determinacy(wmat, tmat, method="average"):
|
|
176
|
+
def compute_prob(w, t, method):
|
|
177
|
+
if w == sp.Integer(0):
|
|
178
|
+
return sp.Rational(1, 2)
|
|
179
|
+
elif w == sp.Integer(1):
|
|
180
|
+
return sp.Integer(1)
|
|
181
|
+
elif w == sp.Integer(-1):
|
|
182
|
+
return sp.Integer(-1)
|
|
183
|
+
elif t == sp.Integer(0):
|
|
184
|
+
return sp.nan
|
|
185
|
+
return (
|
|
186
|
+
compute_prob_average(w, t)
|
|
187
|
+
if method == "average"
|
|
188
|
+
else compute_prob_95_bound(w, t)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def compute_prob_average(w, t):
|
|
192
|
+
bw = 3.45962
|
|
193
|
+
bwt = 0.03417
|
|
194
|
+
prob = sp.exp(bw * w + bwt * w * t) / (1 + sp.exp(bw * w + bwt * w * t))
|
|
195
|
+
return max(sp.Rational(1, 2), prob)
|
|
196
|
+
|
|
197
|
+
def compute_prob_95_bound(w, t):
|
|
198
|
+
bw = 9.766
|
|
199
|
+
bwt = 0.139
|
|
200
|
+
prob = sp.exp(bw * w + bwt * w * t) / (1253.992 + sp.exp(bw * w + bwt * w * t))
|
|
201
|
+
return max(sp.Rational(1, 2), prob)
|
|
202
|
+
|
|
203
|
+
if method not in ["average", "95_bound"]:
|
|
204
|
+
raise ValueError("Invalid method. Choose 'average' or '95_bound'.")
|
|
205
|
+
|
|
206
|
+
rows, cols = wmat.shape
|
|
207
|
+
|
|
208
|
+
def calc_prob(i, j):
|
|
209
|
+
w, t = wmat[i, j], tmat[i, j]
|
|
210
|
+
if w.is_zero:
|
|
211
|
+
return sp.Rational(1, 2)
|
|
212
|
+
prob = compute_prob(sp.Abs(w), t, method)
|
|
213
|
+
return sp.sign(w) * prob if prob is not None else sp.nan
|
|
214
|
+
|
|
215
|
+
pmat = sp.Matrix(rows, cols, lambda i, j: calc_prob(i, j))
|
|
216
|
+
return pmat
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def arrows(G, path):
|
|
220
|
+
arrows = []
|
|
221
|
+
for i in range(len(path) - 1):
|
|
222
|
+
if G[path[i]][path[i + 1]]["sign"] > 0:
|
|
223
|
+
arrows.append(f"{path[i]} $\\rightarrow$") # Right arrow
|
|
224
|
+
else:
|
|
225
|
+
arrows.append(f"{path[i]} $\\multimap$") # Multimap
|
|
226
|
+
arrows.append(str(path[-1]))
|
|
227
|
+
return " ".join(arrows)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def sign_string(G, path):
|
|
231
|
+
signs = []
|
|
232
|
+
for from_node, to_node in zip(path, path[1:]):
|
|
233
|
+
sign = G[from_node][to_node]["sign"]
|
|
234
|
+
if sign != 0:
|
|
235
|
+
signs.append(int(sign))
|
|
236
|
+
product = sp.prod(signs)
|
|
237
|
+
if product > 0:
|
|
238
|
+
return "+"
|
|
239
|
+
elif product < 0:
|
|
240
|
+
return "\u2212"
|
|
241
|
+
else:
|
|
242
|
+
return "0"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def symbolic_path(G, A, path, nodes):
|
|
246
|
+
if len(path) == 1:
|
|
247
|
+
return str(A[nodes.index(path[0]), nodes.index(path[0])])
|
|
248
|
+
return " * ".join(
|
|
249
|
+
str(A[nodes.index(path[i]), nodes.index(path[i + 1])])
|
|
250
|
+
for i in range(len(path) - 1)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def display_digraph(G, label=False, output_format="svg", self_effects=True):
|
|
256
|
+
def set_lbl(node, idx):
|
|
257
|
+
return G.nodes[node].get("label", str(idx + 1)) if label else node
|
|
258
|
+
|
|
259
|
+
def dot_node(node, idx):
|
|
260
|
+
lbl = set_lbl(node, idx)
|
|
261
|
+
x = G.nodes[node].get("x", None)
|
|
262
|
+
y = G.nodes[node].get("y", None)
|
|
263
|
+
pos = f"{x / 100},{-y / 100}!" if x is not None and y is not None else ""
|
|
264
|
+
category = G.nodes[node].get("category")
|
|
265
|
+
if category == "input":
|
|
266
|
+
shape = "rectangle"
|
|
267
|
+
size = "0.45,0.45"
|
|
268
|
+
color = "#FFD0C9" # Light red for input nodes
|
|
269
|
+
elif category == "output":
|
|
270
|
+
shape = "diamond"
|
|
271
|
+
size = "0.6,0.6"
|
|
272
|
+
color = "lightblue" # Light blue for output nodes
|
|
273
|
+
elif category == "state":
|
|
274
|
+
shape = "oval"
|
|
275
|
+
size = "0.52,0.52"
|
|
276
|
+
color = "cornsilk" # Light yellow for state nodes
|
|
277
|
+
else:
|
|
278
|
+
shape = "oval"
|
|
279
|
+
size = "0.52,0.52"
|
|
280
|
+
color = "whitesmoke" # Default color
|
|
281
|
+
wrap = "\\n" if label else ""
|
|
282
|
+
fixsz = "false" if label else "true"
|
|
283
|
+
return (
|
|
284
|
+
f' {node} [label="{lbl}{wrap}", pos="{pos}", '
|
|
285
|
+
f'shape={shape}, width={size.split(",")[0]}, '
|
|
286
|
+
f'height={size.split(",")[1]}, fixedsize={fixsz}, '
|
|
287
|
+
f'color="black", style="filled", fillcolor="{color}"];'
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def dot_edge(src, tgt, sign):
|
|
291
|
+
arrow = "normal" if sign.get("sign", 1) == 1 else "dot"
|
|
292
|
+
return f" {src} -> {tgt} [arrowhead={arrow}];"
|
|
293
|
+
|
|
294
|
+
has_positions = any(
|
|
295
|
+
"x" in G.nodes[node] and "y" in G.nodes[node] for node in G.nodes
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
dot = [
|
|
299
|
+
"digraph G {",
|
|
300
|
+
'layout = "neato";',
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
if not has_positions:
|
|
304
|
+
dot.extend(
|
|
305
|
+
[
|
|
306
|
+
'graph [overlap=false, splines=true, sep="+10"];',
|
|
307
|
+
"node [fixedsize=true];",
|
|
308
|
+
]
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
dot.extend(
|
|
312
|
+
[
|
|
313
|
+
"edge [labelangle=45, labeldistance=2.0];",
|
|
314
|
+
'edge [layer="back"];',
|
|
315
|
+
'node [layer="front"];',
|
|
316
|
+
]
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
dot.extend(
|
|
320
|
+
dot_edge(s, t, d) for s, t, d in G.edges(data=True) if self_effects or s != t
|
|
321
|
+
)
|
|
322
|
+
dot.extend(dot_node(n, i) for i, n in enumerate(G.nodes))
|
|
323
|
+
dot.append("}")
|
|
324
|
+
return graphviz.Source("\n".join(dot), format=output_format)
|