qmm-core 0.2.2__py3-none-any.whl

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/__init__.py ADDED
@@ -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
+ ]
qmm/core/helper.py ADDED
@@ -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)
qmm/core/prediction.py ADDED
@@ -0,0 +1,74 @@
1
+ import numpy as np
2
+ import sympy as sp
3
+ import pandas as pd
4
+ import seaborn as sns
5
+ import matplotlib.pyplot as plt
6
+
7
+
8
+ def table_of_predictions(M, t1=0.8, t2=1, index=None, columns=None):
9
+ if isinstance(M, sp.Matrix):
10
+ M = sp.matrix2numpy(M, dtype=float)
11
+ conditions = [
12
+ (lambda x: x == sp.nan, "0"),
13
+ (lambda x: x >= t2, "+"),
14
+ (lambda x: x >= t1 and x < t2, "(+)"),
15
+ (lambda x: x > -t1 and x < t1, "?"),
16
+ (lambda x: x > -t2 and x <= -t1, "(\u2212)"),
17
+ (lambda x: x <= -t2, "\u2212"),
18
+ ]
19
+
20
+ predictions = np.empty(M.shape, dtype=object)
21
+ for i in range(M.shape[0]):
22
+ for j in range(M.shape[1]):
23
+ value = M[i, j]
24
+ predictions[i, j] = next(
25
+ (val for cond, val in conditions if cond(value)), "0"
26
+ )
27
+ return pd.DataFrame(predictions, index=index, columns=columns)
28
+
29
+
30
+ def compare_predictions(M1, M2):
31
+ if not M1.index.equals(M2.index) or not M1.columns.equals(M2.columns):
32
+ raise ValueError("M1 and M2 must have the same index and columns")
33
+ combined = np.vectorize(lambda x, y: x if x == y else f"{x}, {y}")(
34
+ M1.values, M2.values
35
+ )
36
+ return pd.DataFrame(combined, index=M1.index, columns=M1.columns)
37
+
38
+
39
+ def create_plot(data, **kwargs):
40
+ plt.rcParams.update(
41
+ {
42
+ "xtick.top": True,
43
+ "xtick.bottom": False,
44
+ "xtick.labeltop": True,
45
+ "xtick.labelbottom": False,
46
+ "xtick.major.width": 0.5,
47
+ "ytick.major.width": 0.5,
48
+ "xtick.major.size": 3,
49
+ "ytick.major.size": 3,
50
+ "xtick.minor.size": 1.5,
51
+ "ytick.minor.size": 1.5,
52
+ }
53
+ )
54
+ args = {
55
+ "annot": True,
56
+ "linewidths": 0.75,
57
+ "linecolor": "white",
58
+ "cbar": False,
59
+ "cmap": None,
60
+ }
61
+ args.update(kwargs)
62
+ figsize = args.pop("figsize", None)
63
+ if figsize:
64
+ fig, ax = plt.subplots(figsize=figsize)
65
+ else:
66
+ fig, ax = plt.subplots()
67
+ sns.heatmap(data, ax=ax, **args)
68
+ plt.setp(ax.get_xticklabels(), rotation=0, ha="center")
69
+ plt.setp(ax.get_yticklabels(), rotation=0, ha="right")
70
+ for spine in ax.spines.values():
71
+ spine.set_visible(True)
72
+ spine.set_color("white")
73
+ spine.set_linewidth(0.5)
74
+ return fig, ax
qmm/core/press.py ADDED
@@ -0,0 +1,119 @@
1
+ import numpy as np
2
+ import sympy as sp
3
+ from functools import cache
4
+ from .structure import create_matrix
5
+ from .helper import perm, get_weight, get_nodes, sign_determinacy
6
+
7
+
8
+ @cache
9
+ def adjoint_matrix(G, form="symbolic", perturb=None):
10
+ A = create_matrix(G, form=form)
11
+ A = sp.Matrix(-A)
12
+ nodes = get_nodes(G, "state")
13
+ n = len(nodes)
14
+ if perturb is not None:
15
+ src_id = nodes.index(perturb)
16
+ return sp.Matrix(
17
+ [sp.Integer(-1) ** (src_id + j) * A.minor(src_id, j)
18
+ for j in range(n)]
19
+ )
20
+ adjoint_matrix = sp.expand(A.adjugate())
21
+ return sp.Matrix(adjoint_matrix)
22
+
23
+
24
+ @cache
25
+ def absolute_feedback_matrix(G, perturb=None):
26
+ A = create_matrix(G, form="binary")
27
+ A_np = np.array(sp.matrix2numpy(A), dtype=int)
28
+ nodes = get_nodes(G, "state")
29
+ n = A_np.shape[0]
30
+ if perturb is not None:
31
+ perturb_index = nodes.index(perturb)
32
+ result = np.zeros(n, dtype=int)
33
+ for j in range(n):
34
+ minor = np.delete(np.delete(A_np, perturb_index, 0), j, 1)
35
+ result[j] = int(perm(minor.astype(float)))
36
+ return sp.Matrix(result)
37
+ tmat = np.zeros((n, n), dtype=int)
38
+ for i in range(n):
39
+ for j in range(n):
40
+ minor = np.delete(np.delete(A_np, j, 0), i, 1)
41
+ tmat[i, j] = int(perm(minor.astype(float)))
42
+ return sp.Matrix(tmat)
43
+
44
+
45
+ @cache
46
+ def weighted_predictions_matrix(G, perturb=None, as_nan=True, as_abs=False):
47
+ amat = adjoint_matrix(G, perturb=perturb, form="signed")
48
+ if as_abs:
49
+ amat = sp.Abs(amat)
50
+ tmat = absolute_feedback_matrix(G, perturb=perturb)
51
+ if as_nan:
52
+ wmat = get_weight(amat, tmat)
53
+ else:
54
+ wmat = get_weight(amat, tmat, sp.Integer(1))
55
+ return sp.Matrix(wmat)
56
+
57
+
58
+ @cache
59
+ def sign_determinacy_matrix(
60
+ G, perturb=None, method="average", as_nan=True, as_abs=False
61
+ ):
62
+ wmat = weighted_predictions_matrix(
63
+ G, perturb=perturb, as_nan=as_nan, as_abs=as_abs
64
+ )
65
+ tmat = sp.Matrix(absolute_feedback_matrix(G, perturb=perturb))
66
+ pmat = sign_determinacy(wmat, tmat, method)
67
+ return sp.Matrix(pmat)
68
+
69
+
70
+ @cache
71
+ def numerical_simulations(G, perturb=None, n_sim=10000, dist="uniform", seed=42, as_abs=False):
72
+ np.random.seed(seed)
73
+ A = create_matrix(G, form="symbolic", matrix_type="A")
74
+ state_nodes = get_nodes(G, "state")
75
+ node_idx = {node: i for i, node in enumerate(state_nodes)}
76
+ n = len(state_nodes)
77
+ symbols = list(A.free_symbols)
78
+ A_sp = sp.lambdify(symbols, A)
79
+ pert_idx, sign = (
80
+ (node_idx[perturb[0]], perturb[1])
81
+ if perturb else (None, 1)
82
+ )
83
+ dist_funcs = {
84
+ "uniform": lambda size: np.random.uniform(0, 1, size),
85
+ "weak": lambda size: np.random.beta(1, 3, size),
86
+ "moderate": lambda size: np.random.beta(2, 2, size),
87
+ "strong": lambda size: np.random.beta(3, 1, size),
88
+ }
89
+ positive = np.zeros((n, n), dtype=int)
90
+ negative = np.zeros((n, n), dtype=int)
91
+ total_simulations = 0
92
+ while total_simulations < n_sim:
93
+ values = dist_funcs[dist](len(symbols))
94
+ sim_A = A_sp(*values)
95
+ if np.all(np.real(np.linalg.eigvals(sim_A)) < 0):
96
+ try:
97
+ inv_A = np.linalg.inv(-sim_A)
98
+ effect = (
99
+ inv_A[:, pert_idx] * sign
100
+ if pert_idx is not None else inv_A
101
+ )
102
+ positive += effect > 0
103
+ negative += effect < 0
104
+ total_simulations += 1
105
+ except np.linalg.LinAlgError:
106
+ continue
107
+ smat = np.where(negative > positive, -negative / n_sim, positive / n_sim)
108
+ smat = sp.Matrix(smat)
109
+ tmat = absolute_feedback_matrix(G)
110
+ tmat_np = np.array(tmat.tolist(), dtype=bool)
111
+ smat = sp.Matrix(
112
+ [
113
+ [sp.nan if not tmat_np[i, j] else smat[i, j] for j in range(n)]
114
+ for i in range(n)
115
+ ]
116
+ )
117
+ if as_abs:
118
+ smat = sp.Abs(smat)
119
+ return sp.Matrix(smat)
qmm/core/stability.py ADDED
@@ -0,0 +1,367 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import networkx as nx
4
+ import sympy as sp
5
+ from itertools import combinations
6
+ from functools import cache
7
+ from .structure import create_matrix
8
+ from .helper import perm, get_positive, get_negative, get_weight
9
+
10
+
11
+ def colour_test(G):
12
+ A = create_matrix(G, form="signed")
13
+ n = A.shape[0]
14
+ colour = {i: "black" if A[i, i] != 0 else "white" for i in range(n)}
15
+ if n <= 4 or "white" not in colour.values():
16
+ return "Fail"
17
+ else:
18
+ while "white" in colour.values():
19
+ progress_made = False
20
+ for i in [i for i, c in colour.items() if c == "white"]:
21
+ neighbours = [(j, colour[j]) for j in range(n)
22
+ if A[i, j] * A[j, i] < 0]
23
+ white_neighbours = [j for j, c in neighbours if c == "white"]
24
+ if not white_neighbours or any(
25
+ sum(
26
+ 1
27
+ for k in range(n)
28
+ if A[j, k] * A[k, j] < 0 and colour[k] == "white"
29
+ )
30
+ <= 1
31
+ for j in [j for j, c in neighbours if c == "black"]
32
+ ):
33
+ colour[i] = "black"
34
+ progress_made = True
35
+ break
36
+ if not progress_made:
37
+ return "Pass"
38
+ return "Fail"
39
+
40
+
41
+ def sign_stability(G):
42
+ A = sp.matrix2numpy(create_matrix(G, form="signed")).astype(int)
43
+ n = A.shape[0]
44
+ conditions = [
45
+ all(A[i, i] <= 0 for i in range(n)),
46
+ any(A[i, i] < 0 for i in range(n)),
47
+ all(A[i, j] * A[j, i] <= 0 for i in range(n) for j in range(n) if i != j),
48
+ all(len(cycle) < 3 for cycle in nx.simple_cycles(nx.DiGraph(A))),
49
+ np.linalg.det(A) != 0,
50
+ ]
51
+ colour_result = colour_test(G) == "Fail"
52
+ is_sign_stable = all(conditions) and colour_result
53
+ return pd.DataFrame(
54
+ {
55
+ "Test": [
56
+ "Condition i",
57
+ "Condition ii",
58
+ "Condition iii",
59
+ "Condition iv",
60
+ "Condition v",
61
+ "Colour test",
62
+ "Sign stable",
63
+ ],
64
+ "Definition": [
65
+ "No positive self-effects",
66
+ "At least one node is self-regulating",
67
+ "The product of any pairwise interaction is non-positive",
68
+ "No cycles greater than length two",
69
+ "Non-zero determinant (all nodes have at least " +
70
+ "one incoming and outgoing link)",
71
+ "Fails Jeffries' colour test",
72
+ "Satisfies necessary and sufficient conditions for sign stability",
73
+ ],
74
+ "Result": conditions + [colour_result] + [is_sign_stable],
75
+ }
76
+ )
77
+
78
+
79
+ @cache
80
+ def system_feedback(G, level=None, form="symbolic"):
81
+ A = create_matrix(G, form=form)
82
+ if level == 0:
83
+ return sp.Matrix([-1])
84
+ n = A.shape[0]
85
+ lam = sp.symbols("lambda")
86
+ p = A.charpoly(lam).as_expr()
87
+ if level is None:
88
+ fb = [-p.coeff(lam, n - k) for k in range(n + 1)]
89
+ else:
90
+ fb = [-p.coeff(lam, n - level)]
91
+ return sp.Matrix(fb)
92
+
93
+
94
+ @cache
95
+ def net_feedback(G, level=None):
96
+ return system_feedback(G, level=level, form="signed")
97
+
98
+
99
+ @cache
100
+ def absolute_feedback(G, level=None, method="combinations"):
101
+ A = create_matrix(G, form="signed")
102
+ if level == 0:
103
+ return sp.Matrix([1])
104
+ n = A.shape[0]
105
+ if method == "combinations":
106
+ A = sp.matrix2numpy(A).astype(int)
107
+ A = np.abs(A)
108
+ if level is None:
109
+ fb = []
110
+ for k in range(n + 1):
111
+ fb_k = sum(
112
+ perm(A[np.ix_(c, c)], method="glynn")
113
+ for c in combinations(range(n), k)
114
+ )
115
+ fb.append(int(fb_k))
116
+ else:
117
+ fb_k = sum(
118
+ perm(A[np.ix_(c, c)], method="glynn")
119
+ for c in combinations(range(n), level)
120
+ )
121
+ fb = [int(fb_k)]
122
+ elif method == "polynomial":
123
+ lam = sp.Symbol("lambda")
124
+ A_abs = sp.Matrix(sp.Abs(A) + lam * sp.eye(n))
125
+ P = sp.per(A_abs)
126
+ if level is None:
127
+ fb = [P.coeff(lam, n - k) for k in range(n + 1)]
128
+ else:
129
+ fb = [P.coeff(lam, n - level)]
130
+ return sp.Matrix(fb)
131
+
132
+
133
+ @cache
134
+ def weighted_feedback(G, level=None):
135
+ net_fb = net_feedback(G, level=level)
136
+ tot_fb = absolute_feedback(G, level=level)
137
+ return get_weight(net_fb, tot_fb)
138
+
139
+
140
+ def hurwitz_matrix(fb, level):
141
+ fb_pos = fb * sp.Integer(-1)
142
+ if level == 0:
143
+ return sp.Matrix([fb_pos[0]])
144
+ H = sp.zeros(level, level)
145
+ for i in range(level):
146
+ for j in range(level):
147
+ index = 2 * j - i + 1
148
+ if 0 <= index < len(fb_pos):
149
+ H[i, j] = fb_pos[index]
150
+ return H
151
+
152
+
153
+ @cache
154
+ def feedback_metrics(G):
155
+ net = net_feedback(G)
156
+ absolute = absolute_feedback(G)
157
+ positive = get_positive(net, absolute)
158
+ negative = get_negative(net, absolute)
159
+ weighted = weighted_feedback(G)
160
+ n = len(positive)
161
+ levels = [f"$F_{{{i}}}$" for i in range(n)]
162
+
163
+ df = {
164
+ "Level": levels,
165
+ "Net": [net[i, 0] for i in range(n)],
166
+ "Absolute": [absolute[i, 0] for i in range(n)],
167
+ "Positive": [positive[i, 0] for i in range(n)],
168
+ "Negative": [negative[i, 0] for i in range(n)],
169
+ "Weighted": [weighted[i, 0] for i in range(n)],
170
+ }
171
+
172
+ return pd.DataFrame(df)
173
+
174
+
175
+ @cache
176
+ def hurwitz_determinants(G, level=None, form="symbolic"):
177
+ fb = system_feedback(G, level=None, form=form)
178
+ n = len(fb) - 1
179
+ if n > 5 and form == "symbolic":
180
+ raise ValueError("Limited to systems with five or fewer variables.")
181
+ if level is None:
182
+ h = hurwitz_matrix(fb, n)
183
+ hd = sp.Matrix([sp.det(h[:k, :k]) for k in range(0, n + 1)])
184
+ else:
185
+ h = hurwitz_matrix(fb, level)
186
+ hd = sp.Matrix([sp.det(h[:level, :level])])
187
+ return sp.Matrix(hd)
188
+
189
+
190
+ @cache
191
+ def net_determinants(G, level=None):
192
+ return hurwitz_determinants(G, level=level, form="signed")
193
+
194
+
195
+ @cache
196
+ def absolute_determinants(G, level=None):
197
+ tot_fb = absolute_feedback(G)
198
+ n = tot_fb.shape[0] - 1
199
+ h = hurwitz_matrix(tot_fb, n)
200
+ if level is None:
201
+ td = [sp.Integer(1)]
202
+ for k in range(1, n + 1):
203
+ h_k = np.array(h[:k, :k].tolist(), dtype=float)
204
+ td.append(sp.Abs(sp.Integer(int(perm(h_k)))))
205
+ else:
206
+ if level < 0 or level > n:
207
+ raise ValueError(f"Level must be between 0 and {n}")
208
+ if level == 0:
209
+ td = [sp.Integer(1)]
210
+ else:
211
+ H_k = np.array(h[:level, :level].tolist(), dtype=float)
212
+ td = [sp.Abs(sp.Integer(int(perm(H_k))))]
213
+ return sp.Matrix(td)
214
+
215
+
216
+ @cache
217
+ def weighted_determinants(G, level=None):
218
+ net_det = net_determinants(G, level=level)
219
+ tot_det = absolute_determinants(G, level=level)
220
+ wgt_det = get_weight(net_det, tot_det)
221
+ return wgt_det
222
+
223
+
224
+ @cache
225
+ def determinants_metrics(G):
226
+ net = net_determinants(G)
227
+ absolute = absolute_determinants(G)
228
+ weighted = weighted_determinants(G)
229
+ n = len(net)
230
+ levels = [f"$\Delta_{{{i}}}$" for i in range(n)]
231
+ df = {
232
+ "Hurwitz determinant": levels,
233
+ "Net": [net[i, 0] for i in range(n)],
234
+ "Absolute": [absolute[i, 0] for i in range(n)],
235
+ "Weighted": [weighted[i, 0] for i in range(n)],
236
+ }
237
+ return pd.DataFrame(df)
238
+
239
+
240
+ @cache
241
+ def create_model_c(n):
242
+ C = nx.DiGraph()
243
+ for i in range(n):
244
+ C.add_node(i)
245
+ for i in range(1, n):
246
+ C.add_edge(i - 1, i, sign=-1)
247
+ C.add_edge(i, i - 1, sign=1)
248
+ C.add_edge(n - 1, n - 1, sign=-1)
249
+ nx.set_node_attributes(C, "state", "category")
250
+ return C
251
+
252
+
253
+ @cache
254
+ def conditional_stability(G):
255
+ A = create_matrix(G, form="signed")
256
+ n = A.shape[0]
257
+ w_fb = weighted_feedback(G)
258
+ w_det = weighted_determinants(G, level=n - 1)[0]
259
+ C = create_model_c(n)
260
+ w_det_c = weighted_determinants(C, level=n - 1)[0]
261
+ ratio_C = w_det / w_det_c
262
+ max_fb_n = np.max(w_fb) == w_fb[-1]
263
+ kmax = len(w_fb) - 1 - np.argmax(w_fb[::-1])
264
+ is_sign_stable = sign_stability(G)["Result"].iloc[-1]
265
+ if is_sign_stable:
266
+ model_class = "Sign stable"
267
+ elif max_fb_n and ratio_C >= 1:
268
+ model_class = "Class I"
269
+ else:
270
+ model_class = "Class II"
271
+ stability_metrics = pd.DataFrame(
272
+ {
273
+ "Test": [
274
+ "Weighted feedback",
275
+ "Weighted determinant",
276
+ "Ratio to model-c system",
277
+ "Model class",
278
+ ],
279
+ "Definition": [
280
+ f"Maximum weighted feedback (level {kmax})",
281
+ "n-1 weighted determinant at level",
282
+ "Ratio to a 'model-c' type system",
283
+ "Class of the model based on conditional stability metrics",
284
+ ],
285
+ "Result": [
286
+ np.max(w_fb).evalf(2),
287
+ w_det.evalf(2),
288
+ ratio_C.evalf(2),
289
+ model_class,
290
+ ],
291
+ }
292
+ )
293
+ return stability_metrics
294
+
295
+
296
+ @cache
297
+ def simulation_stability(G, n_sim=10000):
298
+ A = create_matrix(G, "signed")
299
+ A = sp.matrix2numpy(A).astype(int)
300
+ n_stable = 0
301
+ n_unstable = 0
302
+ n_hurwitz_i_fail = 0
303
+ n_hurwitz_ii_fail = 0
304
+ n_hurwitz_i_only_fail = 0
305
+ n_hurwitz_ii_only_fail = 0
306
+ for _ in range(n_sim):
307
+ M = np.random.rand(*A.shape)
308
+ S = A * M
309
+ if np.all(np.real(np.linalg.eigvals(S)) < 0):
310
+ n_stable += 1
311
+ else:
312
+ n_unstable += 1
313
+ pc = np.poly(S)
314
+ hurwitz_i = np.all(pc[1:] > 0) or np.all(pc[1:] < 0)
315
+ n = len(pc)
316
+ H = np.zeros((n - 1, n - 1))
317
+ for i in range(1, n):
318
+ for j in range(1, n):
319
+ index = 2 * j - i
320
+ if 0 <= index < n:
321
+ H[i - 1, j - 1] = pc[index]
322
+ hd = [np.linalg.det(H[: k + 1, : k + 1]) for k in range(n - 1)]
323
+ hurwitz_ii = np.all(np.array(hd[1:-1]) > 0)
324
+ if not hurwitz_i:
325
+ n_hurwitz_i_fail += 1
326
+ if hurwitz_ii:
327
+ n_hurwitz_i_only_fail += 1
328
+ if not hurwitz_ii:
329
+ n_hurwitz_ii_fail += 1
330
+ if hurwitz_i:
331
+ n_hurwitz_ii_only_fail += 1
332
+ prop_stable = n_stable / n_sim
333
+ prop_unstable = n_unstable / n_sim
334
+ prop_hurwitz_i_fail = n_hurwitz_i_fail / n_sim
335
+ prop_hurwitz_ii_fail = n_hurwitz_ii_fail / n_sim
336
+ prop_hurwitz_i_only_fail = n_hurwitz_i_only_fail / n_sim
337
+ prop_hurwitz_ii_only_fail = n_hurwitz_ii_only_fail / n_sim
338
+ sim_df = pd.DataFrame(
339
+ {
340
+ "Test": [
341
+ "Stable matrices",
342
+ "Unstable matrices",
343
+ "Hurwitz criterion i",
344
+ "Hurwitz criterion ii",
345
+ "Hurwitz criterion i only",
346
+ "Hurwitz criterion ii only",
347
+ ],
348
+ "Definition": [
349
+ "Proportion where all eigenvalues have negative real parts",
350
+ "Proportion where one or more eigenvalues have positive real parts",
351
+ "Proportion where polynomial coefficients are not " +
352
+ "all of the same sign",
353
+ "Proportion where Hurwitz determinants are not all positive",
354
+ "Proportion where only Hurwitz criterion i fails",
355
+ "Proportion where only Hurwitz criterion ii fails",
356
+ ],
357
+ "Result": [
358
+ f"{prop_stable:.2%}",
359
+ f"{prop_unstable:.2%}",
360
+ f"{prop_hurwitz_i_fail:.2%}",
361
+ f"{prop_hurwitz_ii_fail:.2%}",
362
+ f"{prop_hurwitz_i_only_fail:.2%}",
363
+ f"{prop_hurwitz_ii_only_fail:.2%}",
364
+ ],
365
+ }
366
+ )
367
+ return sim_df
qmm/core/structure.py ADDED
@@ -0,0 +1,120 @@
1
+ import json
2
+ import networkx as nx
3
+ import sympy as sp
4
+ from .helper import get_nodes
5
+
6
+
7
+ def import_digraph(file_path):
8
+ with open(file_path, "r") as file:
9
+ data = json.load(file)
10
+ G = nx.DiGraph()
11
+ for node in data["nodes"]:
12
+ att = {k: v for k, v in node.items() if k != "id"}
13
+ G.add_node(node["id"], **att)
14
+ for edge in data["edges"]:
15
+ source = edge["from"]
16
+ target = edge["to"]
17
+ att = {k: v for k, v in edge.items() if k not in ["from", "to", "arrows"]}
18
+ arr = edge.get("arrows", {}).get("to", {})
19
+ if isinstance(arr, dict):
20
+ arr_type = arr.get("type")
21
+ if arr_type == "triangle":
22
+ att["sign"] = 1
23
+ elif arr_type == "circle":
24
+ att["sign"] = -1
25
+ G.add_edge(source, target, **att)
26
+ nx.set_node_attributes(G, "state", "category")
27
+ return G
28
+
29
+
30
+ def create_matrix(G, form="symbolic", matrix_type="A"):
31
+ if not isinstance(G, nx.DiGraph):
32
+ raise TypeError("Input must be a networkx.DiGraph.")
33
+ if form not in ["symbolic", "signed", "binary"]:
34
+ raise ValueError("Invalid form. Choose 'symbolic', 'signed', or 'binary'.")
35
+ if matrix_type not in ["A", "B", "C", "D"]:
36
+ raise ValueError("Invalid matrix_type. Choose 'A', 'B', 'C', or 'D'.")
37
+
38
+ def sym(source, target, prefix):
39
+ return sp.Symbol(f"{prefix}_{target},{source}")
40
+
41
+ def sign(source, target, prefix):
42
+ if form == "symbolic":
43
+ return sym(source, target, prefix) * G[source][target].get("sign", 1)
44
+ elif form == "signed":
45
+ return G[source][target].get("sign", 1)
46
+ else: # form == 'binary'
47
+ return int(G.has_edge(source, target))
48
+
49
+ def product(path):
50
+ effect = 1
51
+ for i in range(len(path) - 1):
52
+ effect *= sign(path[i], path[i + 1], prefix)
53
+ return effect
54
+
55
+ state_n = get_nodes(G, "state")
56
+ input_n = get_nodes(G, "input")
57
+ output_n = get_nodes(G, "output")
58
+ matrix_configs = {
59
+ "A": (state_n, state_n, "a", "state"),
60
+ "B": (state_n, input_n, "b", "input"),
61
+ "C": (output_n, state_n, "c", "output"),
62
+ "D": (output_n, input_n, "d", "input"),
63
+ }
64
+ rows, cols, prefix, category = matrix_configs[matrix_type]
65
+ matrix = sp.zeros(len(rows), len(cols))
66
+ for i, target in enumerate(rows):
67
+ for j, source in enumerate(cols):
68
+ if matrix_type == "A":
69
+ if G.has_edge(source, target):
70
+ matrix[i, j] = sign(source, target, prefix)
71
+ else:
72
+ paths = nx.all_simple_paths(G, source, target)
73
+ valid = [
74
+ p
75
+ for p in paths
76
+ if all(G.nodes[n]["category"] == category for n in p[1:-1])
77
+ ]
78
+ matrix[i, j] = sum(product(path) for path in valid)
79
+ return matrix
80
+
81
+
82
+ def create_equations(G, form="state", density_independent=None):
83
+ if not isinstance(G, nx.DiGraph):
84
+ raise TypeError("Input must be a networkx.DiGraph.")
85
+ A = create_matrix(G, form="symbolic", matrix_type="A")
86
+ B = create_matrix(G, form="symbolic", matrix_type="B")
87
+ C = create_matrix(G, form="symbolic", matrix_type="C")
88
+ D = create_matrix(G, form="symbolic", matrix_type="D")
89
+ state_n = get_nodes(G, "state")
90
+ input_n = get_nodes(G, "input")
91
+ output_n = get_nodes(G, "output")
92
+ x = sp.Matrix([sp.Symbol(f"x_{i}") for i in state_n])
93
+ u = sp.Matrix([sp.Symbol(f"u_{i}") for i in input_n]) if input_n else None
94
+ y = sp.Matrix([sp.Symbol(f"y_{i}") for i in output_n]) if output_n else None
95
+ if density_independent is None:
96
+ k = sp.zeros(A.shape[0], 1)
97
+ else:
98
+ if len(density_independent) != A.shape[0]:
99
+ raise ValueError(f"The length of k must be equal to {A.shape[0]}")
100
+ k = sp.Matrix(density_independent)
101
+ state_equations = x.multiply_elementwise(A * x + k)
102
+ if B.shape[1] > 0 and u is not None:
103
+ state_equations += B * u
104
+ output_equations = None
105
+ if C.shape[0] > 0:
106
+ output_equations = C * x
107
+ if D.shape[1] > 0 and u is not None:
108
+ output_equations += D * u
109
+ if form == "vector":
110
+ return A, B, C, D, x, u, y, k
111
+ elif form == "state":
112
+ return state_equations
113
+ elif form == "output":
114
+ if output_equations is None:
115
+ print("No output equations available.")
116
+ return output_equations
117
+ elif form == "jacobian":
118
+ return state_equations.jacobian(x)
119
+ else:
120
+ raise ValueError("Choose 'vector', 'state', 'output' or 'jacobian'.")
@@ -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.
@@ -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 Logo](https://github.com/jaydenhyman/qmm/blob/6f9beb2d90807e8fbc9622a4db3cdf7c0fcd06aa/logo.png) 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.
@@ -0,0 +1,11 @@
1
+ qmm/__init__.py,sha256=SVnmNEHf91LBaitoJ2H_sJlcoEGvN6ocSy2PZAq68O4,1940
2
+ qmm/core/helper.py,sha256=9EsVxZIGw3ZObW2aqDy5W2x8auDLv5C7ucemI7KBRdY,10198
3
+ qmm/core/prediction.py,sha256=xo4XqgBECkrx4QmEJVZmlSclkTRzUz9jAUwqEs-d1KM,2315
4
+ qmm/core/press.py,sha256=wWXUDSRuL4xNbUof3C0MGz1dXv71NXsvbwctS7_W6NQ,3958
5
+ qmm/core/stability.py,sha256=JDuzumGFl3Ul6nodBfhyF5Ccadd3r4SZcoPsdU4bXHs,11764
6
+ qmm/core/structure.py,sha256=CIaSUhdN7eb1Nl6nMREF7RsO2w3Oy9FWJYcVruw5IYQ,4598
7
+ qmm_core-0.2.2.dist-info/LICENSE,sha256=9BC7XxEa_nD9uc8dHjJG-FuPTA0DllmuJTOPAUYJtrc,1527
8
+ qmm_core-0.2.2.dist-info/METADATA,sha256=cdm4Kipj266EmF5ZMY4QXOCFnq79YU_r1hVaJ0NZkRU,3654
9
+ qmm_core-0.2.2.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
10
+ qmm_core-0.2.2.dist-info/top_level.txt,sha256=Nl8juSbthZpsufyisqeDeXVq1jnpHeADFAW0_vN85cg,4
11
+ qmm_core-0.2.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (74.1.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ qmm