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.
Files changed (65) hide show
  1. {boolforge-1.0.0 → boolforge-1.0.1}/PKG-INFO +10 -8
  2. {boolforge-1.0.0 → boolforge-1.0.1}/README.md +4 -3
  3. {boolforge-1.0.0 → boolforge-1.0.1}/boolforge/__init__.py +2 -0
  4. boolforge-1.0.1/boolforge/_version.py +1 -0
  5. boolforge-1.0.1/boolforge/backend/__init__.py +9 -0
  6. boolforge-1.0.1/boolforge/backend/_numba.py +24 -0
  7. boolforge-1.0.1/boolforge/backend/dynamics_async.py +78 -0
  8. boolforge-1.0.1/boolforge/backend/dynamics_sync.py +339 -0
  9. boolforge-1.0.1/boolforge/backend/function_analysis.py +82 -0
  10. boolforge-1.0.1/boolforge/backend/helpers.py +27 -0
  11. boolforge-1.0.1/boolforge/backend/robustness_async.py +40 -0
  12. boolforge-1.0.1/boolforge/backend/robustness_sync.py +237 -0
  13. {boolforge-1.0.0 → boolforge-1.0.1}/boolforge/bio_models.py +21 -8
  14. boolforge-1.0.1/boolforge/boolean_function/__init__.py +42 -0
  15. boolforge-1.0.1/boolforge/boolean_function/analysis.py +408 -0
  16. boolforge-1.0.1/boolforge/boolean_function/canalization.py +348 -0
  17. boolforge-1.0.1/boolforge/boolean_function/collective_canalization.py +348 -0
  18. boolforge-1.0.1/boolforge/boolean_function/conversions.py +258 -0
  19. boolforge-1.0.1/boolforge/boolean_function/core.py +463 -0
  20. boolforge-1.0.1/boolforge/boolean_function/interoperability.py +72 -0
  21. boolforge-1.0.1/boolforge/boolean_function/parsing.py +199 -0
  22. boolforge-1.0.1/boolforge/boolean_network/__init__.py +35 -0
  23. boolforge-1.0.1/boolforge/boolean_network/control.py +255 -0
  24. boolforge-1.0.1/boolforge/boolean_network/core.py +669 -0
  25. boolforge-1.0.1/boolforge/boolean_network/dynamics_async.py +569 -0
  26. boolforge-1.0.1/boolforge/boolean_network/dynamics_sync.py +691 -0
  27. boolforge-1.0.1/boolforge/boolean_network/interoperability.py +432 -0
  28. boolforge-1.0.1/boolforge/boolean_network/modularity.py +649 -0
  29. boolforge-1.0.1/boolforge/boolean_network/robustness_async.py +140 -0
  30. boolforge-1.0.1/boolforge/boolean_network/robustness_sync.py +980 -0
  31. boolforge-1.0.1/boolforge/generate/__init__.py +77 -0
  32. boolforge-1.0.1/boolforge/generate/canalization.py +708 -0
  33. boolforge-1.0.1/boolforge/generate/dispatch.py +325 -0
  34. boolforge-1.0.1/boolforge/generate/functions.py +305 -0
  35. boolforge-1.0.1/boolforge/generate/networks.py +609 -0
  36. boolforge-1.0.1/boolforge/generate/wiring.py +569 -0
  37. boolforge-1.0.1/boolforge/modularity/__init__.py +14 -0
  38. boolforge-1.0.1/boolforge/modularity/plotting.py +139 -0
  39. boolforge-1.0.0/boolforge/modularity.py → boolforge-1.0.1/boolforge/modularity/trajectories.py +5 -131
  40. boolforge-1.0.1/boolforge/theory/__init__.py +2 -0
  41. {boolforge-1.0.0 → boolforge-1.0.1}/boolforge/utils.py +197 -3
  42. boolforge-1.0.1/boolforge/wiring_diagram/__init__.py +20 -0
  43. boolforge-1.0.1/boolforge/wiring_diagram/core.py +271 -0
  44. boolforge-1.0.1/boolforge/wiring_diagram/interoperability.py +154 -0
  45. boolforge-1.0.1/boolforge/wiring_diagram/modularity.py +71 -0
  46. boolforge-1.0.1/boolforge/wiring_diagram/motifs.py +230 -0
  47. boolforge-1.0.1/boolforge/wiring_diagram/plotting.py +607 -0
  48. {boolforge-1.0.0 → boolforge-1.0.1}/boolforge.egg-info/PKG-INFO +10 -8
  49. boolforge-1.0.1/boolforge.egg-info/SOURCES.txt +57 -0
  50. {boolforge-1.0.0 → boolforge-1.0.1}/boolforge.egg-info/requires.txt +1 -0
  51. {boolforge-1.0.0 → boolforge-1.0.1}/pyproject.toml +6 -5
  52. boolforge-1.0.1/tests/test_conversions.py +96 -0
  53. boolforge-1.0.1/tests/test_generate_functions.py +275 -0
  54. boolforge-1.0.1/tests/test_generate_networks.py +179 -0
  55. boolforge-1.0.1/tests/test_string_parser.py +118 -0
  56. boolforge-1.0.0/boolforge/_version.py +0 -1
  57. boolforge-1.0.0/boolforge/boolean_function.py +0 -2073
  58. boolforge-1.0.0/boolforge/boolean_network.py +0 -4476
  59. boolforge-1.0.0/boolforge/generate.py +0 -2358
  60. boolforge-1.0.0/boolforge/wiring_diagram.py +0 -1311
  61. boolforge-1.0.0/boolforge.egg-info/SOURCES.txt +0 -17
  62. {boolforge-1.0.0 → boolforge-1.0.1}/LICENSE +0 -0
  63. {boolforge-1.0.0 → boolforge-1.0.1}/boolforge.egg-info/dependency_links.txt +0 -0
  64. {boolforge-1.0.0 → boolforge-1.0.1}/boolforge.egg-info/top_level.txt +0 -0
  65. {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.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://ckadelka.github.io/BoolForge/
8
- Project-URL: Documentation, https://ckadelka.github.io/BoolForge/
9
- Project-URL: Repository, https://github.com/ckadelka/BoolForge
10
- Project-URL: Issues, https://github.com/ckadelka/BoolForge/issues
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 **BNet format** commonly used by
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://ckadelka.github.io/BoolForge/
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: A Python toolbox for Boolean functions and Boolean networks*.
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 **BNet format** commonly used by
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://ckadelka.github.io/BoolForge/
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: A Python toolbox for Boolean functions and Boolean networks*.
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,9 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Low-level computational backends for BoolForge.
6
+
7
+ Contains numba-accelerated kernels and graph algorithms used by
8
+ Boolean network and wiring diagram analyses.
9
+ """
@@ -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