boolforge 1.0.0__tar.gz → 1.0.1__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.
- {boolforge-1.0.0 → boolforge-1.0.1}/PKG-INFO +10 -8
- {boolforge-1.0.0 → boolforge-1.0.1}/README.md +4 -3
- {boolforge-1.0.0 → boolforge-1.0.1}/boolforge/__init__.py +2 -0
- boolforge-1.0.1/boolforge/_version.py +1 -0
- boolforge-1.0.1/boolforge/backend/__init__.py +9 -0
- boolforge-1.0.1/boolforge/backend/_numba.py +24 -0
- boolforge-1.0.1/boolforge/backend/dynamics_async.py +78 -0
- boolforge-1.0.1/boolforge/backend/dynamics_sync.py +339 -0
- boolforge-1.0.1/boolforge/backend/function_analysis.py +82 -0
- boolforge-1.0.1/boolforge/backend/helpers.py +27 -0
- boolforge-1.0.1/boolforge/backend/robustness_async.py +40 -0
- boolforge-1.0.1/boolforge/backend/robustness_sync.py +237 -0
- {boolforge-1.0.0 → boolforge-1.0.1}/boolforge/bio_models.py +21 -8
- boolforge-1.0.1/boolforge/boolean_function/__init__.py +42 -0
- boolforge-1.0.1/boolforge/boolean_function/analysis.py +408 -0
- boolforge-1.0.1/boolforge/boolean_function/canalization.py +348 -0
- boolforge-1.0.1/boolforge/boolean_function/collective_canalization.py +348 -0
- boolforge-1.0.1/boolforge/boolean_function/conversions.py +258 -0
- boolforge-1.0.1/boolforge/boolean_function/core.py +463 -0
- boolforge-1.0.1/boolforge/boolean_function/interoperability.py +72 -0
- boolforge-1.0.1/boolforge/boolean_function/parsing.py +199 -0
- boolforge-1.0.1/boolforge/boolean_network/__init__.py +35 -0
- boolforge-1.0.1/boolforge/boolean_network/control.py +255 -0
- boolforge-1.0.1/boolforge/boolean_network/core.py +669 -0
- boolforge-1.0.1/boolforge/boolean_network/dynamics_async.py +569 -0
- boolforge-1.0.1/boolforge/boolean_network/dynamics_sync.py +691 -0
- boolforge-1.0.1/boolforge/boolean_network/interoperability.py +432 -0
- boolforge-1.0.1/boolforge/boolean_network/modularity.py +649 -0
- boolforge-1.0.1/boolforge/boolean_network/robustness_async.py +140 -0
- boolforge-1.0.1/boolforge/boolean_network/robustness_sync.py +980 -0
- boolforge-1.0.1/boolforge/generate/__init__.py +77 -0
- boolforge-1.0.1/boolforge/generate/canalization.py +708 -0
- boolforge-1.0.1/boolforge/generate/dispatch.py +325 -0
- boolforge-1.0.1/boolforge/generate/functions.py +305 -0
- boolforge-1.0.1/boolforge/generate/networks.py +609 -0
- boolforge-1.0.1/boolforge/generate/wiring.py +569 -0
- boolforge-1.0.1/boolforge/modularity/__init__.py +14 -0
- boolforge-1.0.1/boolforge/modularity/plotting.py +139 -0
- boolforge-1.0.0/boolforge/modularity.py → boolforge-1.0.1/boolforge/modularity/trajectories.py +5 -131
- boolforge-1.0.1/boolforge/theory/__init__.py +2 -0
- {boolforge-1.0.0 → boolforge-1.0.1}/boolforge/utils.py +197 -3
- boolforge-1.0.1/boolforge/wiring_diagram/__init__.py +20 -0
- boolforge-1.0.1/boolforge/wiring_diagram/core.py +271 -0
- boolforge-1.0.1/boolforge/wiring_diagram/interoperability.py +154 -0
- boolforge-1.0.1/boolforge/wiring_diagram/modularity.py +71 -0
- boolforge-1.0.1/boolforge/wiring_diagram/motifs.py +230 -0
- boolforge-1.0.1/boolforge/wiring_diagram/plotting.py +607 -0
- {boolforge-1.0.0 → boolforge-1.0.1}/boolforge.egg-info/PKG-INFO +10 -8
- boolforge-1.0.1/boolforge.egg-info/SOURCES.txt +57 -0
- {boolforge-1.0.0 → boolforge-1.0.1}/boolforge.egg-info/requires.txt +1 -0
- {boolforge-1.0.0 → boolforge-1.0.1}/pyproject.toml +6 -5
- boolforge-1.0.1/tests/test_conversions.py +96 -0
- boolforge-1.0.1/tests/test_generate_functions.py +275 -0
- boolforge-1.0.1/tests/test_generate_networks.py +179 -0
- boolforge-1.0.1/tests/test_string_parser.py +118 -0
- boolforge-1.0.0/boolforge/_version.py +0 -1
- boolforge-1.0.0/boolforge/boolean_function.py +0 -2073
- boolforge-1.0.0/boolforge/boolean_network.py +0 -4476
- boolforge-1.0.0/boolforge/generate.py +0 -2358
- boolforge-1.0.0/boolforge/wiring_diagram.py +0 -1311
- boolforge-1.0.0/boolforge.egg-info/SOURCES.txt +0 -17
- {boolforge-1.0.0 → boolforge-1.0.1}/LICENSE +0 -0
- {boolforge-1.0.0 → boolforge-1.0.1}/boolforge.egg-info/dependency_links.txt +0 -0
- {boolforge-1.0.0 → boolforge-1.0.1}/boolforge.egg-info/top_level.txt +0 -0
- {boolforge-1.0.0 → boolforge-1.0.1}/setup.cfg +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boolforge
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Methods to generate and analyze random Boolean functions and Boolean networks, with a focus on canalization.
|
|
5
5
|
Author-email: Claus Kadelka <ckadelka@iastate.edu>, Benjamin Coberly <ckadelka@iastate.edu>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://
|
|
8
|
-
Project-URL: Documentation, https://
|
|
9
|
-
Project-URL: Repository, https://github.com/
|
|
10
|
-
Project-URL: Issues, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://KadelkaLab.github.io/BoolForge/
|
|
8
|
+
Project-URL: Documentation, https://KadelkaLab.github.io/BoolForge/
|
|
9
|
+
Project-URL: Repository, https://github.com/KadelkaLab/BoolForge
|
|
10
|
+
Project-URL: Issues, https://github.com/KadelkaLab/BoolForge/issues
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
13
13
|
Requires-Python: >=3.10
|
|
@@ -16,6 +16,7 @@ License-File: LICENSE
|
|
|
16
16
|
Requires-Dist: numpy
|
|
17
17
|
Requires-Dist: networkx
|
|
18
18
|
Requires-Dist: pandas
|
|
19
|
+
Requires-Dist: scipy
|
|
19
20
|
Provides-Extra: cana
|
|
20
21
|
Requires-Dist: cana; extra == "cana"
|
|
21
22
|
Provides-Extra: bio
|
|
@@ -97,6 +98,7 @@ optional packages that can be installed via *extras*.
|
|
|
97
98
|
|
|
98
99
|
Some internal routines are automatically accelerated if
|
|
99
100
|
[numba](https://numba.pydata.org/) is available.
|
|
101
|
+
Exact asynchronous attractor computation requires numba.
|
|
100
102
|
|
|
101
103
|
To enable numba acceleration:
|
|
102
104
|
|
|
@@ -178,7 +180,7 @@ pip install boolforge[all]
|
|
|
178
180
|
BoolForge supports import and export of Boolean network representations used by
|
|
179
181
|
other software packages.
|
|
180
182
|
|
|
181
|
-
In particular, BoolForge supports the
|
|
183
|
+
In particular, BoolForge supports the **.bnet format** commonly used by
|
|
182
184
|
[pyboolnet](https://github.com/hklarner/pyboolnet), without requiring pyboolnet
|
|
183
185
|
itself to be installed.
|
|
184
186
|
|
|
@@ -191,7 +193,7 @@ BoolForge also supports conversion to and from the format used by
|
|
|
191
193
|
|
|
192
194
|
Full documentation, including tutorials and API reference, is available at:
|
|
193
195
|
|
|
194
|
-
https://
|
|
196
|
+
https://kadelkalab.github.io/BoolForge/
|
|
195
197
|
|
|
196
198
|
---
|
|
197
199
|
|
|
@@ -201,7 +203,7 @@ If you use BoolForge in your research, please cite the accompanying
|
|
|
201
203
|
application note:
|
|
202
204
|
|
|
203
205
|
Kadelka, C., & Coberly, B. (2025).
|
|
204
|
-
*BoolForge:
|
|
206
|
+
*BoolForge: Controlled Generation and Analysis of Boolean Functions and Networks*.
|
|
205
207
|
arXiv:2509.02496.
|
|
206
208
|
https://arxiv.org/abs/2509.02496
|
|
207
209
|
|
|
@@ -55,6 +55,7 @@ optional packages that can be installed via *extras*.
|
|
|
55
55
|
|
|
56
56
|
Some internal routines are automatically accelerated if
|
|
57
57
|
[numba](https://numba.pydata.org/) is available.
|
|
58
|
+
Exact asynchronous attractor computation requires numba.
|
|
58
59
|
|
|
59
60
|
To enable numba acceleration:
|
|
60
61
|
|
|
@@ -136,7 +137,7 @@ pip install boolforge[all]
|
|
|
136
137
|
BoolForge supports import and export of Boolean network representations used by
|
|
137
138
|
other software packages.
|
|
138
139
|
|
|
139
|
-
In particular, BoolForge supports the
|
|
140
|
+
In particular, BoolForge supports the **.bnet format** commonly used by
|
|
140
141
|
[pyboolnet](https://github.com/hklarner/pyboolnet), without requiring pyboolnet
|
|
141
142
|
itself to be installed.
|
|
142
143
|
|
|
@@ -149,7 +150,7 @@ BoolForge also supports conversion to and from the format used by
|
|
|
149
150
|
|
|
150
151
|
Full documentation, including tutorials and API reference, is available at:
|
|
151
152
|
|
|
152
|
-
https://
|
|
153
|
+
https://kadelkalab.github.io/BoolForge/
|
|
153
154
|
|
|
154
155
|
---
|
|
155
156
|
|
|
@@ -159,7 +160,7 @@ If you use BoolForge in your research, please cite the accompanying
|
|
|
159
160
|
application note:
|
|
160
161
|
|
|
161
162
|
Kadelka, C., & Coberly, B. (2025).
|
|
162
|
-
*BoolForge:
|
|
163
|
+
*BoolForge: Controlled Generation and Analysis of Boolean Functions and Networks*.
|
|
163
164
|
arXiv:2509.02496.
|
|
164
165
|
https://arxiv.org/abs/2509.02496
|
|
165
166
|
|
|
@@ -25,6 +25,7 @@ from .utils import (
|
|
|
25
25
|
dec2bin,
|
|
26
26
|
get_left_side_of_truth_table,
|
|
27
27
|
hamming_weight_to_ncf_layer_structure,
|
|
28
|
+
get_shannon_entropy,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
from .modularity import (
|
|
@@ -46,6 +47,7 @@ __all__ = [
|
|
|
46
47
|
"dec2bin",
|
|
47
48
|
"get_left_side_of_truth_table",
|
|
48
49
|
"hamming_weight_to_ncf_layer_structure",
|
|
50
|
+
"get_shannon_entropy",
|
|
49
51
|
"BooleanFunction",
|
|
50
52
|
"display_truth_table",
|
|
51
53
|
"get_layer_structure_from_canalized_outputs",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '1.0.1'
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
import numba
|
|
6
|
+
from numba import njit
|
|
7
|
+
from numba.typed import List
|
|
8
|
+
int64 = numba.int64
|
|
9
|
+
__LOADED_NUMBA__ = True
|
|
10
|
+
except ModuleNotFoundError:
|
|
11
|
+
# List = list
|
|
12
|
+
# int64 = int
|
|
13
|
+
# def njit(*args, **kwargs):
|
|
14
|
+
# def decorator(func):
|
|
15
|
+
# return func
|
|
16
|
+
# return decorator
|
|
17
|
+
|
|
18
|
+
__LOADED_NUMBA__ = False
|
|
19
|
+
|
|
20
|
+
def _numba_required(feature: str):
|
|
21
|
+
raise ImportError(
|
|
22
|
+
f"{feature} requires numba. "
|
|
23
|
+
"Install it with: pip install numba"
|
|
24
|
+
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ._numba import njit, __LOADED_NUMBA__
|
|
7
|
+
|
|
8
|
+
if __LOADED_NUMBA__:
|
|
9
|
+
@njit
|
|
10
|
+
def _build_async_transition_coo(
|
|
11
|
+
F_array_list,
|
|
12
|
+
I_array_list,
|
|
13
|
+
N
|
|
14
|
+
):
|
|
15
|
+
nstates = 1 << N
|
|
16
|
+
max_edges = nstates * N
|
|
17
|
+
|
|
18
|
+
rows = np.empty(max_edges, dtype=np.int32)
|
|
19
|
+
cols = np.empty(max_edges, dtype=np.int32)
|
|
20
|
+
data = np.empty(max_edges, dtype=np.float32)
|
|
21
|
+
|
|
22
|
+
edge_count = 0
|
|
23
|
+
powers = np.empty(N, dtype=np.int32)
|
|
24
|
+
for j in range(N):
|
|
25
|
+
powers[j] = 1 << (N - 1 - j)
|
|
26
|
+
|
|
27
|
+
for s in range(nstates):
|
|
28
|
+
unstable_count = 0
|
|
29
|
+
# count unstable nodes
|
|
30
|
+
for j in range(N):
|
|
31
|
+
regs = I_array_list[j]
|
|
32
|
+
idx = 0
|
|
33
|
+
for k in range(len(regs)):
|
|
34
|
+
bit = (s >> (N - 1 - regs[k])) & 1
|
|
35
|
+
idx = (idx << 1) | bit
|
|
36
|
+
new_val = F_array_list[j][idx]
|
|
37
|
+
current = (s >> (N - 1 - j)) & 1
|
|
38
|
+
if new_val != current:
|
|
39
|
+
unstable_count += 1
|
|
40
|
+
|
|
41
|
+
# fixed point self-loop
|
|
42
|
+
if unstable_count == 0:
|
|
43
|
+
rows[edge_count] = s
|
|
44
|
+
cols[edge_count] = s
|
|
45
|
+
data[edge_count] = 1.0
|
|
46
|
+
edge_count += 1
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
p = 1.0 / unstable_count
|
|
50
|
+
|
|
51
|
+
# emit transitions
|
|
52
|
+
for j in range(N):
|
|
53
|
+
regs = I_array_list[j]
|
|
54
|
+
idx = 0
|
|
55
|
+
for k in range(len(regs)):
|
|
56
|
+
bit = (s >> (N - 1 - regs[k])) & 1
|
|
57
|
+
idx = (idx << 1) | bit
|
|
58
|
+
new_val = F_array_list[j][idx]
|
|
59
|
+
current = (s >> (N - 1 - j)) & 1
|
|
60
|
+
if new_val != current:
|
|
61
|
+
y = s ^ powers[j]
|
|
62
|
+
rows[edge_count] = s
|
|
63
|
+
cols[edge_count] = y
|
|
64
|
+
data[edge_count] = p
|
|
65
|
+
edge_count += 1
|
|
66
|
+
return (
|
|
67
|
+
rows[:edge_count],
|
|
68
|
+
cols[:edge_count],
|
|
69
|
+
data[:edge_count]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_dimension_trap_space(terminal_scc):
|
|
74
|
+
ref = terminal_scc[0]
|
|
75
|
+
varying = 0
|
|
76
|
+
for s in terminal_scc[1:]:
|
|
77
|
+
varying |= (ref ^ s)
|
|
78
|
+
return varying.bit_count()
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
# load optional but desirable package
|
|
8
|
+
from ._numba import njit, List, int64, __LOADED_NUMBA__
|
|
9
|
+
|
|
10
|
+
if __LOADED_NUMBA__:
|
|
11
|
+
@njit(fastmath=True) # safe: operations are integer-only
|
|
12
|
+
def _update_network_synchronously_numba(
|
|
13
|
+
x,
|
|
14
|
+
F_array_list,
|
|
15
|
+
I_array_list,
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Perform one synchronous update of a Boolean network.
|
|
19
|
+
|
|
20
|
+
Given a binary state vector ``x``, this function computes the next network
|
|
21
|
+
state under synchronous updating by evaluating each node’s Boolean update
|
|
22
|
+
function based on its regulators.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
x : np.ndarray
|
|
27
|
+
Binary state vector of shape ``(N,)`` with dtype ``uint8``.
|
|
28
|
+
F_array_list : list[np.ndarray]
|
|
29
|
+
List of truth tables for each node, where the ``j``-th entry is an
|
|
30
|
+
array of length ``2**k_j`` giving the update rule for node ``j`` with
|
|
31
|
+
``k_j`` regulators.
|
|
32
|
+
I_array_list : list[np.ndarray]
|
|
33
|
+
List of regulator index arrays, where the ``j``-th entry contains the
|
|
34
|
+
indices of the regulators of node ``j``.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
np.ndarray
|
|
39
|
+
Updated binary state vector of shape ``(N,)`` with dtype ``uint8``.
|
|
40
|
+
"""
|
|
41
|
+
N = x.shape[0]
|
|
42
|
+
fx = np.empty(N, dtype=np.uint8)
|
|
43
|
+
for j in range(N):
|
|
44
|
+
regulators = I_array_list[j]
|
|
45
|
+
if regulators.shape[0] == 0:
|
|
46
|
+
fx[j] = F_array_list[j][0]
|
|
47
|
+
else:
|
|
48
|
+
idx = 0
|
|
49
|
+
for k in range(regulators.shape[0]):
|
|
50
|
+
idx = (idx << 1) | x[regulators[k]]
|
|
51
|
+
fx[j] = F_array_list[j][idx]
|
|
52
|
+
return fx
|
|
53
|
+
|
|
54
|
+
@njit
|
|
55
|
+
def _compute_synchronous_stg_numba(
|
|
56
|
+
F_array_list,
|
|
57
|
+
I_array_list,
|
|
58
|
+
N_variables
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Compute the synchronous state transition graph (STG).
|
|
62
|
+
|
|
63
|
+
This Numba-compiled function computes, for every possible binary state
|
|
64
|
+
of a Boolean network, the index of its successor state under synchronous
|
|
65
|
+
updating.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
F_array_list : list[np.ndarray]
|
|
70
|
+
List of Boolean update tables. The ``j``-th entry is a NumPy array of
|
|
71
|
+
length ``2**k_j`` representing the update rule for node ``j`` with
|
|
72
|
+
``k_j`` regulators.
|
|
73
|
+
I_array_list : list[np.ndarray]
|
|
74
|
+
List of regulator index arrays. The ``j``-th entry contains the indices
|
|
75
|
+
of the regulators of node ``j``.
|
|
76
|
+
N_variables : int
|
|
77
|
+
Number of variables (nodes) in the network.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
np.ndarray
|
|
82
|
+
One-dimensional array of length ``2**N_variables`` containing, for
|
|
83
|
+
each state index, the index of the successor state under synchronous
|
|
84
|
+
updating.
|
|
85
|
+
"""
|
|
86
|
+
nstates = 2 ** N_variables
|
|
87
|
+
states = np.zeros((nstates, N_variables), dtype=np.uint8)
|
|
88
|
+
for i in range(nstates):
|
|
89
|
+
# binary representation of i
|
|
90
|
+
for j in range(N_variables):
|
|
91
|
+
states[i, N_variables - 1 - j] = (i >> j) & 1
|
|
92
|
+
|
|
93
|
+
next_states = np.zeros_like(states)
|
|
94
|
+
powers_of_two = 2 ** np.arange(N_variables - 1, -1, -1)
|
|
95
|
+
|
|
96
|
+
# Compute next state for each node
|
|
97
|
+
for j in range(N_variables):
|
|
98
|
+
regulators = I_array_list[j]
|
|
99
|
+
if len(regulators) == 0:
|
|
100
|
+
# constant node
|
|
101
|
+
next_states[:, j] = F_array_list[j][0]
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
n_reg = len(regulators)
|
|
105
|
+
reg_powers = 2 ** np.arange(n_reg - 1, -1, -1)
|
|
106
|
+
for s in range(nstates):
|
|
107
|
+
idx = 0
|
|
108
|
+
for k in range(n_reg):
|
|
109
|
+
idx += states[s, regulators[k]] * reg_powers[k]
|
|
110
|
+
next_states[s, j] = F_array_list[j][idx]
|
|
111
|
+
|
|
112
|
+
# Convert each next state to integer index
|
|
113
|
+
next_indices = np.zeros(nstates, dtype=np.int64) # NOTE: this cannot be an unsigned int for safe indexing inside Numba kernels.
|
|
114
|
+
for s in range(nstates):
|
|
115
|
+
val = 0
|
|
116
|
+
for j in range(N_variables):
|
|
117
|
+
val += next_states[s, j] * powers_of_two[j]
|
|
118
|
+
next_indices[s] = val
|
|
119
|
+
|
|
120
|
+
return next_indices
|
|
121
|
+
|
|
122
|
+
@njit
|
|
123
|
+
def _compute_synchronous_stg_numba_low_memory(
|
|
124
|
+
F_array_list,
|
|
125
|
+
I_array_list,
|
|
126
|
+
N_variables
|
|
127
|
+
):
|
|
128
|
+
"""
|
|
129
|
+
Compute the synchronous state transition graph (STG) using minimal memory.
|
|
130
|
+
|
|
131
|
+
For each integer state index ``i`` in ``[0, 2**N_variables)``, this function
|
|
132
|
+
decodes ``i`` into its binary state vector, computes the synchronous update
|
|
133
|
+
of the Boolean network, and encodes the resulting state back into an integer
|
|
134
|
+
index.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
F_array_list : list[np.ndarray]
|
|
139
|
+
List of Boolean update tables. The ``j``-th entry is an array of length
|
|
140
|
+
``2**k_j`` representing the update rule for node ``j`` with ``k_j``
|
|
141
|
+
regulators.
|
|
142
|
+
I_array_list : list[np.ndarray]
|
|
143
|
+
List of regulator index arrays. The ``j``-th entry contains the indices
|
|
144
|
+
of the regulators of node ``j``.
|
|
145
|
+
N_variables : int
|
|
146
|
+
Number of variables (nodes) in the network.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
np.ndarray
|
|
151
|
+
One-dimensional array of length ``2**N_variables`` containing, for each
|
|
152
|
+
state index, the index of the successor state under synchronous updating.
|
|
153
|
+
|
|
154
|
+
Notes
|
|
155
|
+
-----
|
|
156
|
+
This implementation avoids storing the full state matrix and therefore
|
|
157
|
+
reduces memory usage from ``O(N * 2**N)`` to ``O(N + 2**N)``. The time
|
|
158
|
+
complexity remains exponential in ``N_variables``.
|
|
159
|
+
"""
|
|
160
|
+
nstates = 2 ** N_variables
|
|
161
|
+
next_indices = np.zeros(nstates, dtype=np.int64) # NOTE: this cannot be an unsigned int for safe indexing inside Numba kernels.
|
|
162
|
+
powers_of_two = 2 ** np.arange(N_variables - 1, -1, -1)
|
|
163
|
+
|
|
164
|
+
state = np.zeros(N_variables, dtype=np.uint8)
|
|
165
|
+
next_state = np.zeros(N_variables, dtype=np.uint8)
|
|
166
|
+
|
|
167
|
+
for i in range(nstates):
|
|
168
|
+
# --- Decode i into binary vector (most-significant bit first)
|
|
169
|
+
tmp = i
|
|
170
|
+
for j in range(N_variables):
|
|
171
|
+
state[N_variables - 1 - j] = tmp & 1
|
|
172
|
+
tmp >>= 1
|
|
173
|
+
|
|
174
|
+
# --- Compute next-state values
|
|
175
|
+
for j in range(N_variables):
|
|
176
|
+
regulators = I_array_list[j]
|
|
177
|
+
if regulators.shape[0] == 0:
|
|
178
|
+
next_state[j] = F_array_list[j][0]
|
|
179
|
+
else:
|
|
180
|
+
n_reg = regulators.shape[0]
|
|
181
|
+
idx = 0
|
|
182
|
+
for k in range(n_reg):
|
|
183
|
+
idx = (idx << 1) | state[regulators[k]]
|
|
184
|
+
next_state[j] = F_array_list[j][idx]
|
|
185
|
+
|
|
186
|
+
# --- Encode next_state back to integer
|
|
187
|
+
val = 0
|
|
188
|
+
for j in range(N_variables):
|
|
189
|
+
val += next_state[j] * powers_of_two[j]
|
|
190
|
+
next_indices[i] = val
|
|
191
|
+
return next_indices
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@njit(cache=True)
|
|
195
|
+
def _attractors_functional_graph(next_state):
|
|
196
|
+
"""
|
|
197
|
+
Identify attractors and basins in a functional graph.
|
|
198
|
+
|
|
199
|
+
Given a functional graph represented by a successor array, this function
|
|
200
|
+
identifies all attractors (cycles), assigns each state to an attractor,
|
|
201
|
+
and computes basin sizes and cycle properties.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
next_state : np.ndarray
|
|
206
|
+
One-dimensional integer array of length ``n`` such that
|
|
207
|
+
``next_state[x]`` gives the successor of state ``x`` and lies in
|
|
208
|
+
``[0, n-1]``.
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
attr_id : np.ndarray
|
|
213
|
+
Integer array of length ``n`` mapping each state to its attractor
|
|
214
|
+
index.
|
|
215
|
+
basin_sizes : np.ndarray
|
|
216
|
+
Integer array of length ``n_attr`` giving the basin size of each
|
|
217
|
+
attractor.
|
|
218
|
+
cycle_rep : np.ndarray
|
|
219
|
+
Integer array of length ``n_attr`` containing one representative
|
|
220
|
+
state from each attractor cycle.
|
|
221
|
+
cycle_len : np.ndarray
|
|
222
|
+
Integer array of length ``n_attr`` giving the length of each cycle.
|
|
223
|
+
n_attr : np.int32
|
|
224
|
+
Number of attractors in the functional graph.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
n = next_state.shape[0]
|
|
228
|
+
attr_id = np.full(n, -1, dtype=np.int32)
|
|
229
|
+
|
|
230
|
+
# For detecting cycles within the current walk:
|
|
231
|
+
# seen[u] == run_id means u was visited in this run
|
|
232
|
+
# pos[u] = index of u in the current path (when first visited this run)
|
|
233
|
+
seen = np.zeros(n, dtype=np.int32)
|
|
234
|
+
pos = np.zeros(n, dtype=np.int32)
|
|
235
|
+
|
|
236
|
+
# Upper bounds: in the worst case every node could be its own 1-cycle
|
|
237
|
+
basin_sizes_full = np.zeros(n, dtype=np.int32)
|
|
238
|
+
cycle_rep_full = np.empty(n, dtype=np.int64)
|
|
239
|
+
cycle_len_full = np.zeros(n, dtype=np.int32)
|
|
240
|
+
|
|
241
|
+
n_attr = 0
|
|
242
|
+
|
|
243
|
+
# Numba typed list for the current path
|
|
244
|
+
path = List.empty_list(int64)
|
|
245
|
+
|
|
246
|
+
for start in range(n):
|
|
247
|
+
if attr_id[start] != -1:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
path.clear()
|
|
251
|
+
u = start
|
|
252
|
+
run_id = start + 1 # unique per start; safe while n << 2**31 (always true in practice)
|
|
253
|
+
|
|
254
|
+
# Walk until we hit a known attractor or revisit a node in this run
|
|
255
|
+
while attr_id[u] == -1 and seen[u] != run_id:
|
|
256
|
+
seen[u] = run_id
|
|
257
|
+
pos[u] = len(path)
|
|
258
|
+
path.append(u)
|
|
259
|
+
u = next_state[u]
|
|
260
|
+
|
|
261
|
+
if attr_id[u] != -1:
|
|
262
|
+
# This path flows into an already-known attractor
|
|
263
|
+
aid = attr_id[u]
|
|
264
|
+
for i in range(len(path)):
|
|
265
|
+
v = path[i]
|
|
266
|
+
attr_id[v] = aid
|
|
267
|
+
basin_sizes_full[aid] += 1
|
|
268
|
+
else:
|
|
269
|
+
# We found a cycle within the current run.
|
|
270
|
+
# u is the first repeated node; cycle starts at pos[u] in path
|
|
271
|
+
cyc_start = pos[u]
|
|
272
|
+
aid = n_attr
|
|
273
|
+
n_attr += 1
|
|
274
|
+
|
|
275
|
+
# Representative and length of the cycle
|
|
276
|
+
cycle_rep_full[aid] = u
|
|
277
|
+
cycle_len_full[aid] = len(path) - cyc_start
|
|
278
|
+
|
|
279
|
+
# Assign all nodes on the path to this new attractor
|
|
280
|
+
for i in range(len(path)):
|
|
281
|
+
v = path[i]
|
|
282
|
+
attr_id[v] = aid
|
|
283
|
+
basin_sizes_full[aid] += 1
|
|
284
|
+
|
|
285
|
+
return attr_id, basin_sizes_full[:n_attr], cycle_rep_full[:n_attr], cycle_len_full[:n_attr], np.int32(n_attr)
|
|
286
|
+
|
|
287
|
+
@njit(cache=True)
|
|
288
|
+
def _transient_lengths_functional_numba(
|
|
289
|
+
succ,
|
|
290
|
+
is_attr_mask
|
|
291
|
+
):
|
|
292
|
+
"""
|
|
293
|
+
Compute exact transient length (distance to attractor) for a functional graph.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
succ : int64 array, shape (n_states,)
|
|
298
|
+
succ[x] = successor of state x
|
|
299
|
+
is_attr_mask : uint8/bool array, shape (n_states,)
|
|
300
|
+
1 if state lies on an attractor cycle, else 0
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
dist : int64 array, shape (n_states,)
|
|
305
|
+
dist[x] = number of steps from x to its attractor
|
|
306
|
+
"""
|
|
307
|
+
n = succ.shape[0]
|
|
308
|
+
dist = np.full(n, -1, dtype=np.int64)
|
|
309
|
+
|
|
310
|
+
# Attractor states have distance 0
|
|
311
|
+
for i in range(n):
|
|
312
|
+
if is_attr_mask[i]:
|
|
313
|
+
dist[i] = 0
|
|
314
|
+
|
|
315
|
+
for i in range(n):
|
|
316
|
+
if dist[i] >= 0:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
v = i
|
|
320
|
+
|
|
321
|
+
# Walk forward until we hit a known distance
|
|
322
|
+
while dist[v] == -1:
|
|
323
|
+
dist[v] = -2 # temporary marker: "in current path"
|
|
324
|
+
v = succ[v]
|
|
325
|
+
|
|
326
|
+
# Now dist[v] is either:
|
|
327
|
+
# 0,1,2,... (known)
|
|
328
|
+
# or -2 (should not happen if cycles were pre-marked)
|
|
329
|
+
d = dist[v]
|
|
330
|
+
|
|
331
|
+
# Unwind path, assigning distances
|
|
332
|
+
v = i
|
|
333
|
+
while dist[v] == -2:
|
|
334
|
+
d += 1
|
|
335
|
+
nxt = succ[v]
|
|
336
|
+
dist[v] = d
|
|
337
|
+
v = nxt
|
|
338
|
+
|
|
339
|
+
return dist
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ._numba import njit
|
|
7
|
+
|
|
8
|
+
@njit
|
|
9
|
+
def _is_degenerate_numba(f : np.ndarray, n : int) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Check whether a Boolean function contains a non-essential variable.
|
|
12
|
+
|
|
13
|
+
This Numba-accelerated helper determines whether there exists at least
|
|
14
|
+
one input variable whose value can be flipped without affecting the
|
|
15
|
+
output of the Boolean function.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
f : np.ndarray
|
|
20
|
+
Truth table of the Boolean function, of length ``2**n``.
|
|
21
|
+
n : int
|
|
22
|
+
Number of input variables.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
bool
|
|
27
|
+
``True`` if the function contains at least one non-essential
|
|
28
|
+
variable, ``False`` otherwise.
|
|
29
|
+
"""
|
|
30
|
+
N = 1 << n # 2**n
|
|
31
|
+
for i in range(n):
|
|
32
|
+
stride = 1 << (n - 1 - i)
|
|
33
|
+
step = stride << 1 # 2 * stride
|
|
34
|
+
depends_on_i = False
|
|
35
|
+
# Iterate in blocks that differ only in bit i
|
|
36
|
+
for base in range(0, N, step):
|
|
37
|
+
for offset in range(stride):
|
|
38
|
+
if f[base + offset] != f[base + offset + stride]:
|
|
39
|
+
depends_on_i = True
|
|
40
|
+
break
|
|
41
|
+
if depends_on_i:
|
|
42
|
+
break
|
|
43
|
+
if not depends_on_i:
|
|
44
|
+
return True # found non-essential variable
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def _get_essential_variables_numba(f : np.ndarray, n : int) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Check whether a Boolean function contains a non-essential variable.
|
|
50
|
+
|
|
51
|
+
This Numba-accelerated helper determines all input variables whose value
|
|
52
|
+
cannot be flipped without affecting the output of the Boolean function.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
f : np.ndarray
|
|
57
|
+
Truth table of the Boolean function, of length ``2**n``.
|
|
58
|
+
n : int
|
|
59
|
+
Number of input variables.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
np.ndarray[bool]
|
|
64
|
+
Array of length n. ``True`` if the variable at position i is essential.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
N = 1 << n # 2**n
|
|
68
|
+
is_essential = np.zeros(n, dtype=bool)
|
|
69
|
+
for i in range(n):
|
|
70
|
+
stride = 1 << (n - 1 - i)
|
|
71
|
+
step = stride << 1 # 2 * stride
|
|
72
|
+
depends_on_i = False
|
|
73
|
+
# Iterate in blocks that differ only in bit i
|
|
74
|
+
for base in range(0, N, step):
|
|
75
|
+
for offset in range(stride):
|
|
76
|
+
if f[base + offset] != f[base + offset + stride]:
|
|
77
|
+
depends_on_i = True
|
|
78
|
+
is_essential[i] = True
|
|
79
|
+
break
|
|
80
|
+
if depends_on_i:
|
|
81
|
+
break
|
|
82
|
+
return is_essential
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
def _compress_with_known_cycle(traj, cycle_len):
|
|
7
|
+
len_traj = len(traj)
|
|
8
|
+
best_trajectory = []
|
|
9
|
+
best_cycle_len = -1
|
|
10
|
+
best_length = math.inf
|
|
11
|
+
for s in range(len_traj):
|
|
12
|
+
for p in range(1, min(cycle_len, len_traj - s) + 1):
|
|
13
|
+
proposed_period = traj[s : s + p]
|
|
14
|
+
good_proposal = True
|
|
15
|
+
for i in range(s, len_traj):
|
|
16
|
+
if traj[i] != proposed_period[(i - s) % p]:
|
|
17
|
+
good_proposal = False
|
|
18
|
+
break
|
|
19
|
+
if not good_proposal:
|
|
20
|
+
continue
|
|
21
|
+
|
|
22
|
+
len_proposal = s + p
|
|
23
|
+
if len_proposal < best_length:
|
|
24
|
+
best_length = len_proposal
|
|
25
|
+
best_trajectory = traj[:s] + proposed_period
|
|
26
|
+
best_cycle_len = p
|
|
27
|
+
return best_trajectory, best_cycle_len
|