vrfcd 0.1.1__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.
- vrfcd/__init__.py +14 -0
- vrfcd/communities.py +130 -0
- vrfcd/distance.py +109 -0
- vrfcd/graph.py +55 -0
- vrfcd/kernels.py +71 -0
- vrfcd/pipeline.py +131 -0
- vrfcd/plotting.py +119 -0
- vrfcd-0.1.1.dist-info/METADATA +278 -0
- vrfcd-0.1.1.dist-info/RECORD +12 -0
- vrfcd-0.1.1.dist-info/WHEEL +5 -0
- vrfcd-0.1.1.dist-info/licenses/LICENSE +0 -0
- vrfcd-0.1.1.dist-info/top_level.txt +1 -0
vrfcd/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from .distance import compute_van_rossum_distance
|
|
2
|
+
from .kernels import distance_to_functional_matrix
|
|
3
|
+
from .graph import build_functional_graph
|
|
4
|
+
from .communities import detect_communities
|
|
5
|
+
from .pipeline import VRFCDPipeline, VRFCDResult
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"compute_van_rossum_distance",
|
|
9
|
+
"distance_to_functional_matrix",
|
|
10
|
+
"build_functional_graph",
|
|
11
|
+
"detect_communities",
|
|
12
|
+
"VRFCDPipeline",
|
|
13
|
+
"VRFCDResult",
|
|
14
|
+
]
|
vrfcd/communities.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
from networkx.algorithms.community import louvain_communities
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def detect_communities(
|
|
8
|
+
G,
|
|
9
|
+
method="louvain",
|
|
10
|
+
seed=42,
|
|
11
|
+
resolution=1.0,
|
|
12
|
+
threshold=1e-10,
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Detect communities in a weighted functional graph.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
G : networkx.Graph
|
|
20
|
+
Weighted graph.
|
|
21
|
+
method : {"louvain", "leiden"}
|
|
22
|
+
Community detection method.
|
|
23
|
+
seed : int
|
|
24
|
+
Random seed.
|
|
25
|
+
resolution : float
|
|
26
|
+
Louvain resolution parameter.
|
|
27
|
+
threshold : float
|
|
28
|
+
Louvain threshold parameter.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
partition : dict
|
|
33
|
+
Mapping node -> community label.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
if method == "louvain":
|
|
37
|
+
communities = louvain_communities(
|
|
38
|
+
G,
|
|
39
|
+
weight="weight",
|
|
40
|
+
seed=seed,
|
|
41
|
+
resolution=resolution,
|
|
42
|
+
threshold=threshold,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
partition = {node: cid for cid, comm in enumerate(communities) for node in comm}
|
|
46
|
+
|
|
47
|
+
elif method == "leiden":
|
|
48
|
+
try:
|
|
49
|
+
import igraph as ig
|
|
50
|
+
import leidenalg
|
|
51
|
+
except ImportError as exc:
|
|
52
|
+
raise ImportError(
|
|
53
|
+
"Leiden requires optional dependencies. "
|
|
54
|
+
"Install using: pip install -e '.[leiden]'"
|
|
55
|
+
) from exc
|
|
56
|
+
|
|
57
|
+
nodes = list(G.nodes())
|
|
58
|
+
node_to_idx = {node: idx for idx, node in enumerate(nodes)}
|
|
59
|
+
|
|
60
|
+
edges = [(node_to_idx[u], node_to_idx[v]) for u, v in G.edges()]
|
|
61
|
+
|
|
62
|
+
weights = [G[u][v].get("weight", 1.0) for u, v in G.edges()]
|
|
63
|
+
|
|
64
|
+
g_ig = ig.Graph()
|
|
65
|
+
g_ig.add_vertices(len(nodes))
|
|
66
|
+
g_ig.add_edges(edges)
|
|
67
|
+
g_ig.es["weight"] = weights
|
|
68
|
+
g_ig.vs["name"] = nodes
|
|
69
|
+
|
|
70
|
+
part = leidenalg.find_partition(
|
|
71
|
+
g_ig,
|
|
72
|
+
leidenalg.ModularityVertexPartition,
|
|
73
|
+
weights="weight",
|
|
74
|
+
seed=seed,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
partition = {}
|
|
78
|
+
|
|
79
|
+
for cid, comm in enumerate(part):
|
|
80
|
+
for idx in comm:
|
|
81
|
+
partition[g_ig.vs[idx]["name"]] = cid
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError("method must be 'louvain' or 'leiden'.")
|
|
85
|
+
|
|
86
|
+
return partition
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def add_community_attributes(G, partition, attribute="community"):
|
|
90
|
+
"""
|
|
91
|
+
Add community labels as node attributes.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
G_out = G.copy()
|
|
95
|
+
nx.set_node_attributes(G_out, partition, attribute)
|
|
96
|
+
return G_out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def community_counts(partition):
|
|
100
|
+
"""
|
|
101
|
+
Count nodes in each community.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
return Counter(partition.values())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def make_partition_weighted_graph(
|
|
108
|
+
G,
|
|
109
|
+
partition,
|
|
110
|
+
within_scale=2.0,
|
|
111
|
+
between_scale=0.5,
|
|
112
|
+
):
|
|
113
|
+
"""
|
|
114
|
+
Modify edge weights for plotting.
|
|
115
|
+
|
|
116
|
+
Within-community edges are strengthened and between-community edges
|
|
117
|
+
are weakened.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
G_comm = G.copy()
|
|
121
|
+
|
|
122
|
+
for u, v, data in G_comm.edges(data=True):
|
|
123
|
+
w = data.get("weight", 1.0)
|
|
124
|
+
|
|
125
|
+
if partition[u] == partition[v]:
|
|
126
|
+
data["weight"] = w * within_scale
|
|
127
|
+
else:
|
|
128
|
+
data["weight"] = w * between_scale
|
|
129
|
+
|
|
130
|
+
return G_comm
|
vrfcd/distance.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import signal
|
|
3
|
+
from joblib import Parallel, delayed
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute_van_rossum_distance(
|
|
7
|
+
spike_matrix,
|
|
8
|
+
t,
|
|
9
|
+
t_R,
|
|
10
|
+
traces=False,
|
|
11
|
+
n_jobs=6,
|
|
12
|
+
normalise=True,
|
|
13
|
+
verbose=10,
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Compute the normalised van Rossum distance matrix.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
spike_matrix : array-like, shape (n_neurons, n_time_bins)
|
|
21
|
+
Binary or count spike matrix.
|
|
22
|
+
t : array-like, shape (n_time_bins,)
|
|
23
|
+
Time axis.
|
|
24
|
+
t_R : float
|
|
25
|
+
van Rossum kernel time constant.
|
|
26
|
+
traces : bool, default=False
|
|
27
|
+
If True, also return convolved spike traces.
|
|
28
|
+
n_jobs : int, default=6
|
|
29
|
+
Number of parallel workers.
|
|
30
|
+
normalise : bool, default=True
|
|
31
|
+
If True, divide distances by sqrt average spike count.
|
|
32
|
+
verbose : int, default=10
|
|
33
|
+
Joblib verbosity.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
D : ndarray
|
|
38
|
+
van Rossum distance matrix.
|
|
39
|
+
waveforms : ndarray, optional
|
|
40
|
+
Convolved spike traces.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
t = np.asarray(t, dtype=float)
|
|
44
|
+
|
|
45
|
+
if not isinstance(spike_matrix, np.ndarray):
|
|
46
|
+
spike_matrix = np.asarray(spike_matrix.todense())
|
|
47
|
+
else:
|
|
48
|
+
spike_matrix = np.asarray(spike_matrix)
|
|
49
|
+
|
|
50
|
+
if spike_matrix.ndim != 2:
|
|
51
|
+
raise ValueError("spike_matrix must have shape (n_neurons, n_time_bins).")
|
|
52
|
+
|
|
53
|
+
if spike_matrix.shape[1] != len(t):
|
|
54
|
+
raise ValueError("Length of t must match the number of time bins.")
|
|
55
|
+
|
|
56
|
+
if t_R <= 0:
|
|
57
|
+
raise ValueError("t_R must be positive.")
|
|
58
|
+
|
|
59
|
+
dt = np.mean(np.diff(t))
|
|
60
|
+
n_neurons, n_time = spike_matrix.shape
|
|
61
|
+
|
|
62
|
+
kernel = np.exp(-t / t_R)
|
|
63
|
+
|
|
64
|
+
waveforms = np.zeros((n_neurons, n_time))
|
|
65
|
+
|
|
66
|
+
for j in range(n_neurons):
|
|
67
|
+
waveforms[j, :] = signal.convolve(
|
|
68
|
+
spike_matrix[j, :],
|
|
69
|
+
kernel,
|
|
70
|
+
mode="full",
|
|
71
|
+
)[:n_time]
|
|
72
|
+
|
|
73
|
+
spike_counts = (spike_matrix > 0).sum(axis=1)
|
|
74
|
+
|
|
75
|
+
def compute_row(j):
|
|
76
|
+
waveform_difference = waveforms - waveforms[j]
|
|
77
|
+
|
|
78
|
+
raw = np.sqrt(dt * np.sum(waveform_difference**2, axis=1) / t_R)
|
|
79
|
+
|
|
80
|
+
if normalise:
|
|
81
|
+
avg_spikes = np.sqrt((spike_counts + spike_counts[j]) / 2)
|
|
82
|
+
|
|
83
|
+
row = np.divide(
|
|
84
|
+
raw,
|
|
85
|
+
avg_spikes,
|
|
86
|
+
out=np.zeros_like(raw),
|
|
87
|
+
where=avg_spikes > 0,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
row = raw
|
|
91
|
+
|
|
92
|
+
return j, row
|
|
93
|
+
|
|
94
|
+
results = Parallel(n_jobs=n_jobs, verbose=verbose)(
|
|
95
|
+
delayed(compute_row)(j) for j in range(n_neurons)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
D = np.zeros((n_neurons, n_neurons))
|
|
99
|
+
|
|
100
|
+
for j, row in results:
|
|
101
|
+
D[j, :] = row
|
|
102
|
+
|
|
103
|
+
D = 0.5 * (D + D.T)
|
|
104
|
+
np.fill_diagonal(D, 0.0)
|
|
105
|
+
|
|
106
|
+
if traces:
|
|
107
|
+
return D, waveforms
|
|
108
|
+
|
|
109
|
+
return D
|
vrfcd/graph.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import networkx as nx
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def build_functional_graph(
|
|
6
|
+
A,
|
|
7
|
+
node_names=None,
|
|
8
|
+
threshold=0.0,
|
|
9
|
+
):
|
|
10
|
+
"""
|
|
11
|
+
Build a weighted NetworkX graph from a functional adjacency matrix.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
A : ndarray
|
|
16
|
+
Functional adjacency matrix.
|
|
17
|
+
node_names : list, optional
|
|
18
|
+
Node labels.
|
|
19
|
+
threshold : float
|
|
20
|
+
Only edges with weight > threshold are added.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
G : networkx.Graph
|
|
25
|
+
Weighted graph.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
A = np.asarray(A, dtype=float)
|
|
29
|
+
|
|
30
|
+
if A.ndim != 2 or A.shape[0] != A.shape[1]:
|
|
31
|
+
raise ValueError("A must be a square matrix.")
|
|
32
|
+
|
|
33
|
+
n = A.shape[0]
|
|
34
|
+
|
|
35
|
+
if node_names is None:
|
|
36
|
+
node_names = list(range(n))
|
|
37
|
+
|
|
38
|
+
if len(node_names) != n:
|
|
39
|
+
raise ValueError("node_names must have length equal to A.shape[0].")
|
|
40
|
+
|
|
41
|
+
G = nx.Graph()
|
|
42
|
+
G.add_nodes_from(node_names)
|
|
43
|
+
|
|
44
|
+
for i in range(n):
|
|
45
|
+
for j in range(i + 1, n):
|
|
46
|
+
weight = A[i, j]
|
|
47
|
+
|
|
48
|
+
if weight > threshold:
|
|
49
|
+
G.add_edge(
|
|
50
|
+
node_names[i],
|
|
51
|
+
node_names[j],
|
|
52
|
+
weight=float(weight),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return G
|
vrfcd/kernels.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def distance_to_functional_matrix(
|
|
5
|
+
D,
|
|
6
|
+
kernel="minmax",
|
|
7
|
+
beta=0.1,
|
|
8
|
+
q_low=0.0,
|
|
9
|
+
q_high=1.0,
|
|
10
|
+
zero_diagonal=True,
|
|
11
|
+
):
|
|
12
|
+
"""
|
|
13
|
+
Convert a van Rossum distance matrix into a functional matrix.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
D : ndarray
|
|
18
|
+
Distance matrix.
|
|
19
|
+
kernel : {"clipping", "exponential", "minmax"}
|
|
20
|
+
Similarity transformation.
|
|
21
|
+
beta : float
|
|
22
|
+
Scale parameter for the exponential kernel.
|
|
23
|
+
q_low : float
|
|
24
|
+
Lower quantile for minmax scaling.
|
|
25
|
+
q_high : float
|
|
26
|
+
Upper quantile for minmax scaling.
|
|
27
|
+
zero_diagonal : bool
|
|
28
|
+
Whether to set the diagonal of A to zero.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
A : ndarray
|
|
33
|
+
Functional adjacency matrix.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
D = np.asarray(D, dtype=float)
|
|
37
|
+
|
|
38
|
+
if D.ndim != 2 or D.shape[0] != D.shape[1]:
|
|
39
|
+
raise ValueError("D must be a square matrix.")
|
|
40
|
+
|
|
41
|
+
if kernel not in {"clipping", "exponential", "minmax"}:
|
|
42
|
+
raise ValueError("kernel must be one of: 'clipping', 'exponential', 'minmax'.")
|
|
43
|
+
|
|
44
|
+
if kernel == "clipping":
|
|
45
|
+
D_cap = np.clip(D, 0.0, 1.0)
|
|
46
|
+
A = 1.0 - D_cap
|
|
47
|
+
|
|
48
|
+
elif kernel == "exponential":
|
|
49
|
+
if beta <= 0:
|
|
50
|
+
raise ValueError("beta must be positive.")
|
|
51
|
+
A = np.exp(-D / beta)
|
|
52
|
+
|
|
53
|
+
elif kernel == "minmax":
|
|
54
|
+
dvals = D[np.triu_indices_from(D, k=1)]
|
|
55
|
+
|
|
56
|
+
lo = np.quantile(dvals, q_low)
|
|
57
|
+
hi = np.quantile(dvals, q_high)
|
|
58
|
+
|
|
59
|
+
if np.isclose(hi, lo):
|
|
60
|
+
A = np.zeros_like(D)
|
|
61
|
+
else:
|
|
62
|
+
Dnorm = (D - lo) / (hi - lo)
|
|
63
|
+
Dnorm = np.clip(Dnorm, 0.0, 1.0)
|
|
64
|
+
A = 1.0 - Dnorm
|
|
65
|
+
|
|
66
|
+
A = 0.5 * (A + A.T)
|
|
67
|
+
|
|
68
|
+
if zero_diagonal:
|
|
69
|
+
np.fill_diagonal(A, 0.0)
|
|
70
|
+
|
|
71
|
+
return A
|
vrfcd/pipeline.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .distance import compute_van_rossum_distance
|
|
4
|
+
from .kernels import distance_to_functional_matrix
|
|
5
|
+
from .graph import build_functional_graph
|
|
6
|
+
from .communities import (
|
|
7
|
+
detect_communities,
|
|
8
|
+
add_community_attributes,
|
|
9
|
+
make_partition_weighted_graph,
|
|
10
|
+
community_counts,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class VRFCDResult:
|
|
16
|
+
D: object
|
|
17
|
+
A: object
|
|
18
|
+
G: object
|
|
19
|
+
partition: dict | None = None
|
|
20
|
+
G_partitioned: object | None = None
|
|
21
|
+
counts: object | None = None
|
|
22
|
+
waveforms: object | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VRFCDPipeline:
|
|
26
|
+
"""
|
|
27
|
+
End-to-end pipeline:
|
|
28
|
+
|
|
29
|
+
spike matrix -> van Rossum distance -> functional matrix
|
|
30
|
+
-> weighted graph -> community labels
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
t_R,
|
|
36
|
+
kernel="minmax",
|
|
37
|
+
beta=0.1,
|
|
38
|
+
q_low=0.0,
|
|
39
|
+
q_high=1.0,
|
|
40
|
+
graph_threshold=0.0,
|
|
41
|
+
community_method="louvain",
|
|
42
|
+
seed=42,
|
|
43
|
+
n_jobs=6,
|
|
44
|
+
traces=False,
|
|
45
|
+
normalise=True,
|
|
46
|
+
verbose=10,
|
|
47
|
+
):
|
|
48
|
+
self.t_R = t_R
|
|
49
|
+
self.kernel = kernel
|
|
50
|
+
self.beta = beta
|
|
51
|
+
self.q_low = q_low
|
|
52
|
+
self.q_high = q_high
|
|
53
|
+
self.graph_threshold = graph_threshold
|
|
54
|
+
self.community_method = community_method
|
|
55
|
+
self.seed = seed
|
|
56
|
+
self.n_jobs = n_jobs
|
|
57
|
+
self.traces = traces
|
|
58
|
+
self.normalise = normalise
|
|
59
|
+
self.verbose = verbose
|
|
60
|
+
|
|
61
|
+
def fit(self, spike_matrix, t, node_names=None, detect=True):
|
|
62
|
+
"""
|
|
63
|
+
Run the full VRFCD pipeline.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
if self.traces:
|
|
67
|
+
D, waveforms = compute_van_rossum_distance(
|
|
68
|
+
spike_matrix,
|
|
69
|
+
t,
|
|
70
|
+
self.t_R,
|
|
71
|
+
traces=True,
|
|
72
|
+
n_jobs=self.n_jobs,
|
|
73
|
+
normalise=self.normalise,
|
|
74
|
+
verbose=self.verbose,
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
D = compute_van_rossum_distance(
|
|
78
|
+
spike_matrix,
|
|
79
|
+
t,
|
|
80
|
+
self.t_R,
|
|
81
|
+
traces=False,
|
|
82
|
+
n_jobs=self.n_jobs,
|
|
83
|
+
normalise=self.normalise,
|
|
84
|
+
verbose=self.verbose,
|
|
85
|
+
)
|
|
86
|
+
waveforms = None
|
|
87
|
+
|
|
88
|
+
A = distance_to_functional_matrix(
|
|
89
|
+
D,
|
|
90
|
+
kernel=self.kernel,
|
|
91
|
+
beta=self.beta,
|
|
92
|
+
q_low=self.q_low,
|
|
93
|
+
q_high=self.q_high,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
G = build_functional_graph(
|
|
97
|
+
A,
|
|
98
|
+
node_names=node_names,
|
|
99
|
+
threshold=self.graph_threshold,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if detect:
|
|
103
|
+
partition = detect_communities(
|
|
104
|
+
G,
|
|
105
|
+
method=self.community_method,
|
|
106
|
+
seed=self.seed,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
G_with_partition = add_community_attributes(G, partition)
|
|
110
|
+
|
|
111
|
+
G_partitioned = make_partition_weighted_graph(
|
|
112
|
+
G_with_partition,
|
|
113
|
+
partition,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
counts = community_counts(partition)
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
partition = None
|
|
120
|
+
G_partitioned = None
|
|
121
|
+
counts = None
|
|
122
|
+
|
|
123
|
+
return VRFCDResult(
|
|
124
|
+
D=D,
|
|
125
|
+
A=A,
|
|
126
|
+
G=G,
|
|
127
|
+
partition=partition,
|
|
128
|
+
G_partitioned=G_partitioned,
|
|
129
|
+
counts=counts,
|
|
130
|
+
waveforms=waveforms,
|
|
131
|
+
)
|
vrfcd/plotting.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import networkx as nx
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def plot_matrix(
|
|
7
|
+
M,
|
|
8
|
+
title=None,
|
|
9
|
+
colorbar_title=None,
|
|
10
|
+
cmap="inferno",
|
|
11
|
+
figsize=(7, 6),
|
|
12
|
+
xlabel="Neuron index $j$",
|
|
13
|
+
ylabel="Neuron index $i$",
|
|
14
|
+
savepath=None,
|
|
15
|
+
fontsize=18,
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Plot a distance or functional matrix.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
M = np.asarray(M)
|
|
22
|
+
|
|
23
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
24
|
+
|
|
25
|
+
im = ax.imshow(
|
|
26
|
+
M,
|
|
27
|
+
cmap=cmap,
|
|
28
|
+
vmin=np.min(M),
|
|
29
|
+
vmax=np.max(M),
|
|
30
|
+
origin="lower",
|
|
31
|
+
interpolation="nearest",
|
|
32
|
+
aspect="equal",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
cbar = fig.colorbar(im, ax=ax, pad=0.03)
|
|
36
|
+
|
|
37
|
+
if colorbar_title is not None:
|
|
38
|
+
cbar.ax.set_title(colorbar_title, fontsize=fontsize, pad=10)
|
|
39
|
+
|
|
40
|
+
cbar.ax.tick_params(labelsize=fontsize)
|
|
41
|
+
|
|
42
|
+
ax.set_xlabel(xlabel, fontsize=fontsize)
|
|
43
|
+
ax.set_ylabel(ylabel, fontsize=fontsize)
|
|
44
|
+
|
|
45
|
+
ax.tick_params(axis="both", labelsize=fontsize)
|
|
46
|
+
|
|
47
|
+
ax.spines["top"].set_visible(False)
|
|
48
|
+
ax.spines["right"].set_visible(False)
|
|
49
|
+
|
|
50
|
+
if title is not None:
|
|
51
|
+
ax.set_title(title, fontsize=fontsize)
|
|
52
|
+
|
|
53
|
+
plt.tight_layout()
|
|
54
|
+
|
|
55
|
+
if savepath is not None:
|
|
56
|
+
fig.savefig(savepath, dpi=300, bbox_inches="tight")
|
|
57
|
+
|
|
58
|
+
return fig, ax
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def plot_functional_network(
|
|
62
|
+
G,
|
|
63
|
+
partition=None,
|
|
64
|
+
pos=None,
|
|
65
|
+
weight_attr="weight",
|
|
66
|
+
layout_seed=42,
|
|
67
|
+
figsize=(5, 4),
|
|
68
|
+
node_size=500,
|
|
69
|
+
title=r"Network representation of $A(t_R)$",
|
|
70
|
+
savepath=None,
|
|
71
|
+
fontsize=18,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Plot weighted functional network.
|
|
75
|
+
|
|
76
|
+
If partition is supplied, nodes are coloured by community.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
if pos is None:
|
|
80
|
+
pos = nx.spring_layout(G, weight=weight_attr, seed=layout_seed)
|
|
81
|
+
|
|
82
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
83
|
+
|
|
84
|
+
if partition is None:
|
|
85
|
+
node_colors = "lightgray"
|
|
86
|
+
else:
|
|
87
|
+
node_colors = [partition[n] for n in G.nodes()]
|
|
88
|
+
|
|
89
|
+
nx.draw_networkx_nodes(
|
|
90
|
+
G,
|
|
91
|
+
pos,
|
|
92
|
+
node_size=node_size,
|
|
93
|
+
node_color=node_colors,
|
|
94
|
+
edgecolors="black",
|
|
95
|
+
linewidths=1.2,
|
|
96
|
+
cmap=plt.cm.tab10,
|
|
97
|
+
ax=ax,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
widths = [max(0.2, G[u][v].get(weight_attr, 1.0)) for u, v in G.edges()]
|
|
101
|
+
|
|
102
|
+
nx.draw_networkx_edges(
|
|
103
|
+
G,
|
|
104
|
+
pos,
|
|
105
|
+
width=widths,
|
|
106
|
+
alpha=0.8,
|
|
107
|
+
edge_color="black",
|
|
108
|
+
ax=ax,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
ax.set_title(title, fontsize=fontsize)
|
|
112
|
+
ax.axis("off")
|
|
113
|
+
|
|
114
|
+
plt.tight_layout()
|
|
115
|
+
|
|
116
|
+
if savepath is not None:
|
|
117
|
+
fig.savefig(savepath, dpi=300, bbox_inches="tight")
|
|
118
|
+
|
|
119
|
+
return fig, ax, pos
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vrfcd
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: van Rossum distance based functional community detection for spike trains
|
|
5
|
+
Author-email: Indranil Ghosh <indranilg49@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/indrag49/vrfcd
|
|
8
|
+
Project-URL: Repository, https://github.com/indrag49/vrfcd
|
|
9
|
+
Project-URL: Issues, https://github.com/indrag49/vrfcd/issues
|
|
10
|
+
Keywords: computational-neuroscience,spike-trains,van-rossum-distance,functional-connectivity,community-detection,network-science
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: pandas
|
|
17
|
+
Requires-Dist: matplotlib
|
|
18
|
+
Requires-Dist: networkx
|
|
19
|
+
Requires-Dist: joblib
|
|
20
|
+
Requires-Dist: scikit-learn
|
|
21
|
+
Provides-Extra: leiden
|
|
22
|
+
Requires-Dist: igraph; extra == "leiden"
|
|
23
|
+
Requires-Dist: leidenalg; extra == "leiden"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest; extra == "dev"
|
|
26
|
+
Requires-Dist: black; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
Requires-Dist: jupyter; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# vrfcd
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+
|
|
35
|
+
`vrfcd` stands for **van Rossum Functional Community Detection**.
|
|
36
|
+
|
|
37
|
+
This is a `Python` package for constructing functional connectivity networks from neural spike-train data
|
|
38
|
+
using the van Rossum distance, and then detecting functional assemblies in the resulting weighted network.
|
|
39
|
+
The package is designed for computational neuroscience workflows where the input data are spike rasters.
|
|
40
|
+
The goal is to identify groups of neurons with similar temporal spiking structure.
|
|
41
|
+
|
|
42
|
+
## Workflow
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
Given a ```spike train``` as input the workflow is a four step process:
|
|
46
|
+
```
|
|
47
|
+
- Compute the van Rossum distance matrix
|
|
48
|
+
- Transform it into functional adjacency matrix
|
|
49
|
+
- Prepare the weighted `networkX` graph
|
|
50
|
+
- Perform community detection on the graph
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
`vrfcd` provides tools to:
|
|
55
|
+
|
|
56
|
+
- compute a normalised van Rossum distance matrix from spike-train raster datasets,
|
|
57
|
+
- convert the distance matrix into a functional adjacency matrix using different similarity transformations,
|
|
58
|
+
- construct a weighted functional graph from the adjacency matrix,
|
|
59
|
+
- detect communitues using either the Louvain or Leiden community detection algorithm,
|
|
60
|
+
- visualise the distance matrix, functional matrix, and network representation, and
|
|
61
|
+
- run the full analysis using a single pipeline interface.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install vrfcd
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For development:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git clone https://github.com/indranilg49/vrfcd.git
|
|
74
|
+
cd vrfcd
|
|
75
|
+
pip install -e ".[dev]"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For optional Leiden community detection:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install -e ".[dev,leiden]"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Simple usage
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
import numpy as np
|
|
88
|
+
from vrfcd import VRFCDPipeline
|
|
89
|
+
|
|
90
|
+
pipeline = VRFCDPipeline( t_R=0.01, kernel="minmax", community_method="louvain", n_jobs=4, )
|
|
91
|
+
|
|
92
|
+
result = pipeline.fit(spike_matrix, t_axis)
|
|
93
|
+
|
|
94
|
+
D = result.D
|
|
95
|
+
A = result.A
|
|
96
|
+
G = result.G
|
|
97
|
+
|
|
98
|
+
partition = result.partition
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Here `spike_matrix` should have the shape of `n_neurons x n_time_bins`, and `t_axis` should be the corresponding
|
|
102
|
+
time axis. Note that if the time axis comes from experimental data with absolute time stamps, it is a good idea
|
|
103
|
+
to rescale it so that it starts at zero:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
t_axis = t_axis - t_axis[0]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Main functions
|
|
110
|
+
|
|
111
|
+
- ```compute_van_rossum_distance```
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
from vrfcd import compute_van_rossum_distance
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This computes the pairwise van Rossum distance matrix between spike trains.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
D = compute_van_rossum_distance(
|
|
121
|
+
spike_matrix,
|
|
122
|
+
t_axis,
|
|
123
|
+
t_R=0.01,
|
|
124
|
+
traces=False,
|
|
125
|
+
n_jobs=4,
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The parameter `t_R` controls the time scale of the exponential filter used in the van Rossum distance.
|
|
130
|
+
Smaller values of `t_R` make the distance more sensitive to precise spike timing, while larger values smooth
|
|
131
|
+
spike trains over a longer temporal window.
|
|
132
|
+
|
|
133
|
+
If `traces=True`, the function also returns the convolved spike-train waveforms:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
D, waveforms = compute_van_rossum_distance( spike_matrix, t_axis, t_R=0.01, traces=True, )
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- ```distance_to_functional_matrix```
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
from vrfcd import distance_to_functional_matrix
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This converts the van Rossum distance matrix into a functional adjacency matrix.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
A = distance_to_functional_matrix( D, kernel="minmax", )
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
This package currently supports three similarity transformations.
|
|
152
|
+
|
|
153
|
+
```kernel = "clipping"```
|
|
154
|
+
|
|
155
|
+
When using this, the distances are capped at 1 and converted to similarities using: A = 1-D.
|
|
156
|
+
|
|
157
|
+
```kernel = "exponential"```
|
|
158
|
+
|
|
159
|
+
When using this, the distances are rescaled using an exponential kernel: A = exp(-D/beta). The parameter
|
|
160
|
+
`beta` controls how quickly similarity decays with distance.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
A = distance_to_functional_matrix( D, kernel="exponential", beta=0.1, )
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```kernel = "minmax"```
|
|
167
|
+
|
|
168
|
+
When using this, the distances are rescaled using a min-max transformation:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
A = distance_to_functional_matrix(
|
|
172
|
+
D,
|
|
173
|
+
kernel="minmax",
|
|
174
|
+
q_low=0.0,
|
|
175
|
+
q_high=1.0,
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This maps small distances to high functional similarity and large distances to low functional similarity.
|
|
180
|
+
The diagonal of the resulting matrix is set to 0 by default.
|
|
181
|
+
|
|
182
|
+
- ```build_functional_graph```
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
from vrfcd import build_functional_graph
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
This builds a weighted `networkX` graph from the functional adjacency matrix.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
G = build_functional_graph( A, threshold=0.0, )
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
In the graph representation, each neuron is a node, and edges are weighted by the functional similarity values
|
|
195
|
+
in `A`. The `threshold` parameter can be used to remove weak functional connections.
|
|
196
|
+
|
|
197
|
+
- ```detect_communities```
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
from vrfcd import detect_communities
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This function detects communities in the weighted functional graph.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
partition = detect_communities( G, method="louvain", seed=42, )
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The output is a disctionary mapping each node to a community label. The package currently supports two
|
|
210
|
+
methods for community detection:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
method = "louvain"
|
|
214
|
+
method = "leiden"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The Louvain method works with the core dependencies. In order to use Leiden, you will need to install the optional dependencies
|
|
218
|
+
`igraph` and `leidenalg` additionally.
|
|
219
|
+
|
|
220
|
+
## Pipeline interface
|
|
221
|
+
|
|
222
|
+
Here we outline the easiest way to use the package through `VRFCDPipeline`.
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
from vrfcd import VRFCDPipeline
|
|
226
|
+
|
|
227
|
+
pipeline = VRFCDPipeline( t_R=0.01, kernel="minmax", q_low=0.0, q_high=1.0, community_method="louvain", n_jobs=4, traces=True, )
|
|
228
|
+
|
|
229
|
+
result = pipeline.fit(spike_matrix, t_axis)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The pipeline returns a `VRFCDResult` object containing:
|
|
233
|
+
|
|
234
|
+
- `result.D`: van Rossum distance matrix
|
|
235
|
+
- `result.A`: functional adjacency matrix
|
|
236
|
+
- `result.G`: community labels
|
|
237
|
+
- `result.partition`: graph with community attributes
|
|
238
|
+
- `result.G_partitioned`: van Rossum distance matrix
|
|
239
|
+
- `result.counts`: number of nodes in each community
|
|
240
|
+
- `result.waveforms`: convolved spike traces, if `traces=True`
|
|
241
|
+
|
|
242
|
+
This allows the full analysis to be run in one step while still giving access to every intermediate object.
|
|
243
|
+
|
|
244
|
+
## Plotting
|
|
245
|
+
|
|
246
|
+
`vrfcd` also includes helper functions for viusalisation.
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
from vrfcd.plotting import plot_matrix, plot_functional_network
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
To plot the van Rossum distance matrix:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
plot_matrix( result.D, colorbar_title=r"$\tilde{D}(t_R)$", savepath="distance_matrix.pdf", )
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
To plot the adjacency matrix:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
plot_matrix( result.A, colorbar_title=r"$A(t_R)$", savepath="functional_matrix.pdf", )
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
To plot the functional network:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
plot_functional_network( result.G_partitioned, partition=result.partition, savepath="functional_network.pdf", )
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Overview
|
|
271
|
+
|
|
272
|
+
This package is intended for exploratory and research-focused analysis of spike-train data. It is particularly useful when
|
|
273
|
+
one wants to compare neurons based on spike timing, construct a functional network from these similarities,
|
|
274
|
+
and identify groups of neurons with similar temporal spiking patterns. This package can be used with synthetic spike raster,
|
|
275
|
+
simulated spiking network models, and experimental recordings such as Neuropixels spike-train datasets.
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
vrfcd/__init__.py,sha256=4rxSSBS-S3oA7YrS6qsDJDtaIEAj-KS2qhYcdBkLDmE,419
|
|
2
|
+
vrfcd/communities.py,sha256=8-Ado-FMFe9vH-xNJYYUQfTueiz_764JK5fh03XL0XQ,3017
|
|
3
|
+
vrfcd/distance.py,sha256=1ouv4NvqyCMgFtbLMpmPsqXzhlXQO9i8uR3MWN2Yh3A,2665
|
|
4
|
+
vrfcd/graph.py,sha256=lRjtgDdZzvy6d4gYzifN2HCVpUJy6rkePSIpzH7R9Ag,1173
|
|
5
|
+
vrfcd/kernels.py,sha256=AhOI1fr2xxnlGGp19OZDBUdlD23I5su54EurUngN7ww,1710
|
|
6
|
+
vrfcd/pipeline.py,sha256=_etbYix_lki82nHNBkkWhfzzpAVUHnJ0WR1z53ryZz4,3246
|
|
7
|
+
vrfcd/plotting.py,sha256=os53Fzy8_vKqAMZRpTx-6Me7FBdGzHOfZ107RP2sLcM,2489
|
|
8
|
+
vrfcd-0.1.1.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
vrfcd-0.1.1.dist-info/METADATA,sha256=3LGr1-dwsAbWxJvOk4wMUkMaft3_ET1J6LRh6U3DFK0,7980
|
|
10
|
+
vrfcd-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
vrfcd-0.1.1.dist-info/top_level.txt,sha256=j3yae39lvtgTi3UTrID7haeqDTn4iRbD4m4iVlLeT7o,6
|
|
12
|
+
vrfcd-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vrfcd
|