Skip to content

Commit 5cc63b9

Browse files
authored
Merge pull request #106 from justinehansen/networks
2 parents 45a7716 + 79a5877 commit 5cc63b9

File tree

1 file changed

+178
-0
lines changed

1 file changed

+178
-0
lines changed

netneurotools/networks.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,181 @@ def threshold_network(network, retain=10):
376376
mst = np.array((mst + mst.T) != 0, dtype=int)
377377

378378
return mst
379+
380+
381+
def match_length_degree_distribution(W, D, nbins=10, nswap=1000,
382+
replacement=False, weighted=True,
383+
seed=None):
384+
"""
385+
Generates degree- and edge length-preserving surrogate connectomes.
386+
387+
Parameters
388+
----------
389+
W : (N, N) array-like
390+
weighted or binary symmetric connectivity matrix.
391+
D : (N, N) array-like
392+
symmetric distance matrix.
393+
nbins : int
394+
number of distance bins (edge length matrix is performed by swapping
395+
connections in the same bin). Default = 10.
396+
nswap : int
397+
total number of edge swaps to perform. Recommended = nnodes * 20
398+
Default = 1000.
399+
replacement : bool, optional
400+
if True all the edges are available for swapping. Default= False
401+
weighted : bool, optional
402+
Whether to return weighted rewired connectivity matrix. Default = True
403+
seed : float, optional
404+
Random seed. Default = None
405+
406+
Returns
407+
-------
408+
newB : (N, N) array-like
409+
binary rewired matrix
410+
newW: (N, N) array-like
411+
weighted rewired matrix. Returns matrix of zeros if weighted=False.
412+
nr : int
413+
number of successful rewires
414+
415+
Notes
416+
-----
417+
Takes a weighted, symmetric connectivity matrix `data` and Euclidean/fiber
418+
length matrix `distance` and generates a randomized network with:
419+
1. exactly the same degree sequence
420+
2. approximately the same edge length distribution
421+
3. exactly the same edge weight distribution
422+
4. approximately the same weight-length relationship
423+
424+
Reference
425+
---------
426+
Betzel, R. F., Bassett, D. S. (2018) Specificity and robustness of
427+
long-distance connections in weighted, interareal connectomes. PNAS.
428+
429+
"""
430+
431+
rs = check_random_state(seed)
432+
N = len(W)
433+
# divide the distances by lengths
434+
bins = np.linspace(D[D.nonzero()].min(), D[D.nonzero()].max(), nbins + 1)
435+
bins[-1] += 1
436+
L = np.zeros((N, N))
437+
for n in range(nbins):
438+
i, j = np.where(np.logical_and(bins[n] <= D, D < bins[n + 1]))
439+
L[i, j] = n + 1
440+
441+
# binarized connectivity
442+
B = (W != 0).astype(np.int_)
443+
444+
# existing edges (only upper triangular cause it's symmetric)
445+
cn_x, cn_y = np.where(np.triu((B != 0) * B, k=1))
446+
447+
tries = 0
448+
nr = 0
449+
newB = np.copy(B)
450+
451+
while ((len(cn_x) >= 2) & (nr < nswap)):
452+
# choose randomly the edge to be rewired
453+
r = rs.randint(len(cn_x))
454+
n_x, n_y = cn_x[r], cn_y[r]
455+
tries += 1
456+
457+
# options to rewire with
458+
# connected nodes that doesn't involve (n_x, n_y)
459+
index = (cn_x != n_x) & (cn_y != n_y) & (cn_y != n_x) & (cn_x != n_y)
460+
if len(np.where(index)[0]) == 0:
461+
cn_x = np.delete(cn_x, r)
462+
cn_y = np.delete(cn_y, r)
463+
464+
else:
465+
ops1_x, ops1_y = cn_x[index], cn_y[index]
466+
# options that will preserve the distances
467+
# (ops1_x, ops1_y) such that
468+
# L(n_x,n_y) = L(n_x, ops1_x) & L(ops1_x,ops1_y) = L(n_y, ops1_y)
469+
index = (L[n_x, n_y] == L[n_x, ops1_x]) & (
470+
L[ops1_x, ops1_y] == L[n_y, ops1_y])
471+
if len(np.where(index)[0]) == 0:
472+
cn_x = np.delete(cn_x, r)
473+
cn_y = np.delete(cn_y, r)
474+
475+
else:
476+
ops2_x, ops2_y = ops1_x[index], ops1_y[index]
477+
# options of edges that didn't exist before
478+
index = [(newB[min(n_x, ops2_x[i])][max(n_x, ops2_x[i])] == 0)
479+
& (newB[min(n_y, ops2_y[i])][max(n_y,
480+
ops2_y[i])] == 0)
481+
for i in range(len(ops2_x))]
482+
if (len(np.where(index)[0]) == 0):
483+
cn_x = np.delete(cn_x, r)
484+
cn_y = np.delete(cn_y, r)
485+
486+
else:
487+
ops3_x, ops3_y = ops2_x[index], ops2_y[index]
488+
489+
# choose randomly one edge from the final options
490+
r1 = rs.randint(len(ops3_x))
491+
nn_x, nn_y = ops3_x[r1], ops3_y[r1]
492+
493+
# Disconnect the existing edges
494+
newB[n_x, n_y] = 0
495+
newB[nn_x, nn_y] = 0
496+
# Connect the new edges
497+
newB[min(n_x, nn_x), max(n_x, nn_x)] = 1
498+
newB[min(n_y, nn_y), max(n_y, nn_y)] = 1
499+
# one successfull rewire!
500+
nr += 1
501+
502+
# rewire with replacement
503+
if replacement:
504+
cn_x[r], cn_y[r] = min(n_x, nn_x), max(n_x, nn_x)
505+
index = np.where((cn_x == nn_x) & (cn_y == nn_y))[0]
506+
cn_x[index], cn_y[index] = min(
507+
n_y, nn_y), max(n_y, nn_y)
508+
# rewire without replacement
509+
else:
510+
cn_x = np.delete(cn_x, r)
511+
cn_y = np.delete(cn_y, r)
512+
index = np.where((cn_x == nn_x) & (cn_y == nn_y))[0]
513+
cn_x = np.delete(cn_x, index)
514+
cn_y = np.delete(cn_y, index)
515+
516+
if nr < nswap:
517+
print(f"I didn't finish, out of rewirable edges: {len(cn_x)}")
518+
519+
i, j = np.triu_indices(N, k=1)
520+
# Make the connectivity matrix symmetric
521+
newB[j, i] = newB[i, j]
522+
523+
# check the number of edges is preserved
524+
if len(np.where(B != 0)[0]) != len(np.where(newB != 0)[0]):
525+
print(
526+
f"ERROR --- number of edges changed, \
527+
B:{len(np.where(B!=0)[0])}, newB:{len(np.where(newB!=0)[0])}")
528+
# check that the degree of the nodes it's the same
529+
for i in range(N):
530+
if np.sum(B[i]) != np.sum(newB[i]):
531+
print(
532+
f"ERROR --- node {i} changed k by: \
533+
{np.sum(B[i]) - np.sum(newB[i])}")
534+
535+
newW = np.zeros((N, N))
536+
if weighted:
537+
# Reassign the weights
538+
mask = np.triu(B != 0, k=1)
539+
inids = D[mask]
540+
iniws = W[mask]
541+
inids_index = np.argsort(inids)
542+
# Weights from the shortest to largest edges
543+
iniws = iniws[inids_index]
544+
mask = np.triu(newB != 0, k=1)
545+
finds = D[mask]
546+
i, j = np.where(mask)
547+
# Sort the new edges from the shortest to the largest
548+
finds_index = np.argsort(finds)
549+
i_sort = i[finds_index]
550+
j_sort = j[finds_index]
551+
# Assign the initial sorted weights
552+
newW[i_sort, j_sort] = iniws
553+
# Make it symmetrical
554+
newW[j_sort, i_sort] = iniws
555+
556+
return newB, newW, nr

0 commit comments

Comments
 (0)