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 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
@@ -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
+