@@ -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 math_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 of 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