tempnet 1.0.0rc2__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.
- tempnet/__init__.py +75 -0
- tempnet/_version.py +24 -0
- tempnet/logger.py +46 -0
- tempnet/parallel_expm.py +270 -0
- tempnet/synth_temp_network.py +732 -0
- tempnet/temporal_network.py +2803 -0
- tempnet-1.0.0rc2.dist-info/METADATA +210 -0
- tempnet-1.0.0rc2.dist-info/RECORD +10 -0
- tempnet-1.0.0rc2.dist-info/WHEEL +4 -0
- tempnet-1.0.0rc2.dist-info/licenses/LICENSE +166 -0
tempnet/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tempnet
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Logging
|
|
7
|
+
-------
|
|
8
|
+
The package sets up a default logger on import. You can adjust the logging level:
|
|
9
|
+
|
|
10
|
+
>>> import tempnet
|
|
11
|
+
>>> tempnet.set_log_level("DEBUG")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Author
|
|
15
|
+
------
|
|
16
|
+
Alexandre Bovet <alexandre.bovet@maths.ox.ac.uk>
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Contributors
|
|
20
|
+
............
|
|
21
|
+
|
|
22
|
+
- Jonas I. Liechti <j-i-l@t4d.ch>
|
|
23
|
+
|
|
24
|
+
License
|
|
25
|
+
-------
|
|
26
|
+
GNU Lesser General Public License v3 or later (LGPLv3+).
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
# try to import version (provided by hatch (see pyproject.toml)
|
|
31
|
+
from ._version import __version__
|
|
32
|
+
except ImportError:
|
|
33
|
+
# Fallback if the package wasn't installed properly
|
|
34
|
+
__version__ = "unknown"
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
|
|
38
|
+
from .logger import setup_logger, get_logger
|
|
39
|
+
|
|
40
|
+
# Default log level
|
|
41
|
+
setup_logger() # Set up the logger with the default level
|
|
42
|
+
|
|
43
|
+
from .temporal_network import ( # noqa: F401
|
|
44
|
+
ContTempNetwork,
|
|
45
|
+
set_to_zeroes,
|
|
46
|
+
sparse_lapl_expm
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def set_log_level(level):
|
|
50
|
+
"""
|
|
51
|
+
Set the logging level for the package.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
level : str
|
|
56
|
+
The logging level as a string (e.g., 'DEBUG', 'INFO').
|
|
57
|
+
"""
|
|
58
|
+
level_dict = {
|
|
59
|
+
'DEBUG': logging.DEBUG,
|
|
60
|
+
'INFO': logging.INFO,
|
|
61
|
+
'WARNING': logging.WARNING,
|
|
62
|
+
'ERROR': logging.ERROR,
|
|
63
|
+
'CRITICAL': logging.CRITICAL,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if level in level_dict:
|
|
67
|
+
logger = get_logger()
|
|
68
|
+
logger.setLevel(level_dict[level])
|
|
69
|
+
for handler in logger.handlers:
|
|
70
|
+
handler.setLevel(level_dict[level])
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Invalid log level: {level}. "
|
|
74
|
+
f"Choose from {list(level_dict.keys())}."
|
|
75
|
+
)
|
tempnet/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.0.0rc2'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 0, 'rc2')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
tempnet/logger.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
# Create a logger instance
|
|
5
|
+
logger = logging.getLogger("flowstab")
|
|
6
|
+
|
|
7
|
+
class CustomPathnameFilter(logging.Filter):
|
|
8
|
+
def filter(self, record):
|
|
9
|
+
# Get the full pathname
|
|
10
|
+
full_path = record.pathname
|
|
11
|
+
|
|
12
|
+
# Split the path into parts
|
|
13
|
+
path_parts = full_path.split(os.sep)
|
|
14
|
+
|
|
15
|
+
# Limit to the last 2 parts
|
|
16
|
+
if len(path_parts) > 2:
|
|
17
|
+
record.pathname = os.sep.join(path_parts[-2:])
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
def setup_logger(log_level=logging.INFO):
|
|
21
|
+
"""
|
|
22
|
+
Set up the logger for the package.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
log_level (int): The logging level (e.g., logging.DEBUG, logging.INFO).
|
|
26
|
+
"""
|
|
27
|
+
logger.setLevel(log_level)
|
|
28
|
+
|
|
29
|
+
logger.addFilter(CustomPathnameFilter())
|
|
30
|
+
|
|
31
|
+
# Create console handler and set level
|
|
32
|
+
ch = logging.StreamHandler()
|
|
33
|
+
ch.setLevel(log_level)
|
|
34
|
+
|
|
35
|
+
# Create formatter
|
|
36
|
+
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(pathname)s:%(lineno)d - PID:%(process)d - %(message)s')
|
|
37
|
+
# Add formatter to ch
|
|
38
|
+
ch.setFormatter(formatter)
|
|
39
|
+
|
|
40
|
+
# Add ch to logger if it doesn't have handlers
|
|
41
|
+
if not logger.hasHandlers():
|
|
42
|
+
logger.addHandler(ch)
|
|
43
|
+
|
|
44
|
+
def get_logger():
|
|
45
|
+
"""Return the logger instance."""
|
|
46
|
+
return logger
|
tempnet/parallel_expm.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""#
|
|
2
|
+
# flow stability
|
|
3
|
+
#
|
|
4
|
+
# Copyright (C) 2021 Alexandre Bovet <alexandre.bovet@maths.ox.ac.uk>
|
|
5
|
+
#
|
|
6
|
+
# This program is free software; you can redistribute it and/or modify it under
|
|
7
|
+
# the terms of the GNU Lesser General Public License as published by the Free
|
|
8
|
+
# Software Foundation; either version 3 of the License, or (at your option) any
|
|
9
|
+
# later version.
|
|
10
|
+
#
|
|
11
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
12
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
13
|
+
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
|
14
|
+
# details.
|
|
15
|
+
#
|
|
16
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
17
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import time
|
|
24
|
+
from multiprocessing import Pool, RawArray
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
from scipy.sparse import csc_matrix, csr_matrix, isspmatrix_csc, vstack
|
|
28
|
+
from scipy.sparse.csgraph import connected_components
|
|
29
|
+
from scipy.sparse.linalg import expm, expm_multiply
|
|
30
|
+
|
|
31
|
+
from stochmat import inplace_csr_row_normalize
|
|
32
|
+
|
|
33
|
+
# A global dictionary storing the variables passed from the initializer.
|
|
34
|
+
var_dict = {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _init_worker(data, indices, indptr, N):
|
|
38
|
+
# reconstruct A from shared arrays
|
|
39
|
+
var_dict["A"] = csc_matrix((np.frombuffer(data, dtype=np.float64),
|
|
40
|
+
np.frombuffer(indices, dtype=np.int32),
|
|
41
|
+
np.frombuffer(indptr, dtype=np.int32)),
|
|
42
|
+
shape=(N,N))
|
|
43
|
+
var_dict["N"] = N
|
|
44
|
+
|
|
45
|
+
def _worker(args):
|
|
46
|
+
|
|
47
|
+
i, thresh_ratio = args
|
|
48
|
+
|
|
49
|
+
delta_i = np.zeros(var_dict["N"],
|
|
50
|
+
dtype=np.float64)
|
|
51
|
+
delta_i[i] = 1.0
|
|
52
|
+
|
|
53
|
+
Tcol_i = expm_multiply(var_dict["A"],
|
|
54
|
+
delta_i)
|
|
55
|
+
|
|
56
|
+
if thresh_ratio is not None:
|
|
57
|
+
|
|
58
|
+
Tcol_i[Tcol_i<Tcol_i.max()/thresh_ratio]=0
|
|
59
|
+
|
|
60
|
+
return csc_matrix(Tcol_i)
|
|
61
|
+
|
|
62
|
+
def compute_parallel_expm(A, nproc=1, thresh_ratio=None,
|
|
63
|
+
normalize_rows=True,
|
|
64
|
+
verbose=True):
|
|
65
|
+
"""Computes the exponential matrix of A by computing each column separately
|
|
66
|
+
exploiting the fact that the column i of expm(A) is expm_multiply(A,delta_i)
|
|
67
|
+
where delta i is the vector with zeros everywhere except on i.
|
|
68
|
+
This only works if A is equal to (minus) a Laplacian matrix
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
A : scipy csc sparse matrix
|
|
74
|
+
Square csc sparse matrix representing (+ or -) a Laplacian.
|
|
75
|
+
If A is not csc, it will be converted to csc format.
|
|
76
|
+
nproc : int, optional
|
|
77
|
+
number of parallel processes. The default is 1.
|
|
78
|
+
thresh_ratio: float, optional.
|
|
79
|
+
Threshold ratio used to trim negligible values in the resulting matrix.
|
|
80
|
+
For each columns `c`, values smaller than `max(c)/thresh_ratio` are set to
|
|
81
|
+
zero. Default is None.
|
|
82
|
+
normalize_rows: bool, optional.
|
|
83
|
+
Whether rows of the resulting matrix are normalized to sum to 1.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
expm(A).
|
|
89
|
+
scipy csr sparse matrix
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
if not isspmatrix_csc(A):
|
|
93
|
+
A = csc_matrix(A)
|
|
94
|
+
|
|
95
|
+
N = A.shape[0]
|
|
96
|
+
|
|
97
|
+
# create arrays to share A between processes
|
|
98
|
+
indices = RawArray("i",A.indices)
|
|
99
|
+
indptr = RawArray("i",A.indptr)
|
|
100
|
+
data = RawArray("d", A.data)
|
|
101
|
+
|
|
102
|
+
if verbose:
|
|
103
|
+
print("PID ", os.getpid(), " : ",f"compute_parallel_expm starting a pool of {nproc} workers")
|
|
104
|
+
t0 = time.time()
|
|
105
|
+
with Pool(nproc, initializer=_init_worker,
|
|
106
|
+
initargs=(data, indices, indptr, N)) as p:
|
|
107
|
+
res = p.map(_worker, [(i,thresh_ratio) for i in range(N)])
|
|
108
|
+
|
|
109
|
+
# delete shared arrays
|
|
110
|
+
global var_dict
|
|
111
|
+
var_dict = {}
|
|
112
|
+
|
|
113
|
+
# seems faster than _stack_sparse_cols
|
|
114
|
+
T = vstack(res).T.tocsr()
|
|
115
|
+
|
|
116
|
+
if normalize_rows:
|
|
117
|
+
inplace_csr_row_normalize(T)
|
|
118
|
+
|
|
119
|
+
if verbose:
|
|
120
|
+
print("PID ", os.getpid(), " : ", f"compute_parallel_expm took {time.time()-t0:.3f}s, computed expm has {T.getnnz()} non-zeros.")
|
|
121
|
+
|
|
122
|
+
return T
|
|
123
|
+
|
|
124
|
+
def _stack_sparse_cols(col_list):
|
|
125
|
+
|
|
126
|
+
# create csc sparse matric from sparse column list
|
|
127
|
+
data_size = sum(C.data.size for C in col_list)
|
|
128
|
+
N = max(col_list[0].shape)
|
|
129
|
+
|
|
130
|
+
data = np.zeros(data_size, dtype=np.float64)
|
|
131
|
+
indices = np.zeros(data_size, dtype=np.int32)
|
|
132
|
+
indptr = np.zeros(N+1, dtype=np.int32)
|
|
133
|
+
|
|
134
|
+
ptr = 0
|
|
135
|
+
for col, C in enumerate(col_list):
|
|
136
|
+
nnz_col = C.data.size
|
|
137
|
+
data[ptr:ptr+nnz_col] = C.T.tocsc().data
|
|
138
|
+
indices[ptr:ptr+nnz_col] = C.T.tocsc().indices
|
|
139
|
+
ptr = ptr + nnz_col
|
|
140
|
+
indptr[col+1] = ptr
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
return csc_matrix((data, indices, indptr), shape=(N,N))
|
|
144
|
+
|
|
145
|
+
def _expm_worker(cmp_ind):
|
|
146
|
+
|
|
147
|
+
return expm(var_dict["A"][cmp_ind,:][:,cmp_ind]).toarray()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def compute_subspace_expm_parallel(A, n_comp=None, comp_labels=None, verbose=False,
|
|
151
|
+
nproc=1,
|
|
152
|
+
thresh_ratio=None,
|
|
153
|
+
normalize_rows=True):
|
|
154
|
+
"""Compute the exponential matrix of `A` by applying expm on each connected
|
|
155
|
+
subgraphs defined by A and recomposing it to return expm(A).
|
|
156
|
+
Small subgraphs are computed in parallel, each using scipy expm,
|
|
157
|
+
and large subgraphs are computed with compute_parallel_expm.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
A : scipy.sparse.csc_matrix
|
|
162
|
+
|
|
163
|
+
nproc : int, optional
|
|
164
|
+
number of parallel processes. The default is 1.
|
|
165
|
+
thresh_ratio: float, optional.
|
|
166
|
+
Threshold ratio used to trim negligible values in the resulting matrix.
|
|
167
|
+
Values smaller than `max(expm(A))/thresh_ratio` are set to
|
|
168
|
+
zero. Default is None.
|
|
169
|
+
normalize_rows: bool, optional.
|
|
170
|
+
Whether rows of the resulting matrix are normalized to sum to 1.
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
expm(A) : scipy.sparse.csr_matrix
|
|
176
|
+
matrix exponential of A
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
num_nodes = A.shape[0]
|
|
180
|
+
|
|
181
|
+
# otherwise 0 values may count as an edge
|
|
182
|
+
A.eliminate_zeros()
|
|
183
|
+
A.sort_indices()
|
|
184
|
+
|
|
185
|
+
if (n_comp is None) or (comp_labels is None):
|
|
186
|
+
n_comp, comp_labels = connected_components(A,directed=False)
|
|
187
|
+
comp_sizes = np.bincount(comp_labels)
|
|
188
|
+
cmp_indices = [(comp_labels == cmp).nonzero()[0] for \
|
|
189
|
+
cmp in range(n_comp)]
|
|
190
|
+
|
|
191
|
+
if verbose:
|
|
192
|
+
print("PID ", os.getpid(), f" : subspace_expm with {n_comp} components")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
large_comps, = np.nonzero(comp_sizes >= 1000)
|
|
196
|
+
small_comps, = np.nonzero(comp_sizes < 1000)
|
|
197
|
+
|
|
198
|
+
subnets_expms = dict()
|
|
199
|
+
# first compute large comps with exmp_multiply
|
|
200
|
+
for cmp in large_comps:
|
|
201
|
+
cmp_ind = cmp_indices[cmp]
|
|
202
|
+
if verbose:
|
|
203
|
+
print("PID ", os.getpid(), f" : computing component {cmp} over {n_comp}, with size {cmp_ind.size} using expm_multiply")
|
|
204
|
+
subnets_expms[cmp] = compute_parallel_expm(A[cmp_ind,:][:,cmp_ind],
|
|
205
|
+
nproc=nproc,
|
|
206
|
+
thresh_ratio=None,
|
|
207
|
+
normalize_rows=False,
|
|
208
|
+
verbose=verbose,
|
|
209
|
+
).toarray()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
Aindices = RawArray("i",A.indices)
|
|
213
|
+
Aindptr = RawArray("i",A.indptr)
|
|
214
|
+
Adata = RawArray("d", A.data)
|
|
215
|
+
|
|
216
|
+
# now compute small comps in parallel
|
|
217
|
+
if verbose:
|
|
218
|
+
print("PID ", os.getpid(), f" : computing {small_comps.size} small components with {nproc} workers")
|
|
219
|
+
t0 = time.time()
|
|
220
|
+
with Pool(nproc, initializer=_init_worker,
|
|
221
|
+
initargs=(Adata, Aindices, Aindptr, num_nodes)) as p:
|
|
222
|
+
res = p.map(_expm_worker, [cmp_indices[c] for c in small_comps])
|
|
223
|
+
|
|
224
|
+
# delete shared arrays
|
|
225
|
+
global var_dict
|
|
226
|
+
var_dict = {}
|
|
227
|
+
|
|
228
|
+
if verbose:
|
|
229
|
+
print("PID ", os.getpid(), f" : small components computation took {time.time()-t0:.3f}s")
|
|
230
|
+
t0 = time.time()
|
|
231
|
+
|
|
232
|
+
# organize results
|
|
233
|
+
for c, sub_expm in zip(small_comps, res):
|
|
234
|
+
subnets_expms[c] = sub_expm
|
|
235
|
+
|
|
236
|
+
# constructors for sparse array
|
|
237
|
+
data = np.zeros((comp_sizes**2).sum(), dtype=np.float64)
|
|
238
|
+
indices = np.zeros((comp_sizes**2).sum(), dtype=np.int32)
|
|
239
|
+
indptr = np.zeros(num_nodes+1, dtype=np.int32)
|
|
240
|
+
|
|
241
|
+
# reconstruct csr sparse matrix
|
|
242
|
+
if verbose:
|
|
243
|
+
print("PID ", os.getpid(), " : reconstructing expm mat")
|
|
244
|
+
data_ind = 0
|
|
245
|
+
for row in range(num_nodes):
|
|
246
|
+
cmp = comp_labels[row]
|
|
247
|
+
cmp_expm = subnets_expms[cmp]
|
|
248
|
+
sub_expm_row, = np.where(cmp_indices[cmp] == row)
|
|
249
|
+
|
|
250
|
+
data[data_ind:data_ind+comp_sizes[cmp]] = cmp_expm[sub_expm_row,:]
|
|
251
|
+
|
|
252
|
+
indices[data_ind:data_ind+comp_sizes[cmp]] = cmp_indices[cmp]
|
|
253
|
+
|
|
254
|
+
indptr[row] = data_ind
|
|
255
|
+
|
|
256
|
+
data_ind += comp_sizes[cmp]
|
|
257
|
+
indptr[num_nodes] = data_ind
|
|
258
|
+
|
|
259
|
+
expmA = csr_matrix((data, indices, indptr), shape=(num_nodes,num_nodes),
|
|
260
|
+
dtype=np.float64)
|
|
261
|
+
|
|
262
|
+
if thresh_ratio is not None:
|
|
263
|
+
expmA.data[expmA.data<expmA.data.max()/thresh_ratio] = 0.0
|
|
264
|
+
expmA.eliminate_zeros()
|
|
265
|
+
if normalize_rows:
|
|
266
|
+
inplace_csr_row_normalize(expmA)
|
|
267
|
+
|
|
268
|
+
return expmA
|
|
269
|
+
|
|
270
|
+
|