roiextract 0.0.2__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.
roiextract/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ try:
2
+ from importlib.metadata import version
3
+
4
+ __version__ = version("roiextract")
5
+ except Exception:
6
+ __version__ = "Failed to determine the version"
roiextract/analytic.py ADDED
@@ -0,0 +1,73 @@
1
+ import numpy as np
2
+
3
+ from numpy.linalg import norm
4
+ from scipy.linalg import eig
5
+
6
+
7
+ def ctf_optimize_ratio_similarity(
8
+ leadfield, template, mask, lambda_, regA=0.001, regB=0.001
9
+ ):
10
+ """
11
+ Optimize a linear combination of similarity with a template CTF and
12
+ within/outside CTF ratio
13
+
14
+ Parameters
15
+ ----------
16
+ leadfield: array_like
17
+ Lead field matrix for dipoles with fixed orientation with shape
18
+ (channels, voxels).
19
+ template: array_like
20
+ Template CTF pattern for voxels with shape (voxels,). Only values
21
+ within the ROI are used.
22
+ mask: array_like
23
+ Voxel mask of the region of interest with shape (voxels,). Contains
24
+ ones and zeros for voxels within and outside ROI, respectively.
25
+ lambda_: float
26
+ If 0, only ratio is optimized. If 1, only similarity. Values in between
27
+ allow tweaking the balance between optimization for similarity or ratio.
28
+ regA, regB: float
29
+ Regularization parameters to ensure that it is possible to calculate the
30
+ inverse matrices.
31
+
32
+ Returns
33
+ -------
34
+ w: array
35
+ Spatial filter produced by the optimization.
36
+
37
+ Raises
38
+ ------
39
+ ValueError
40
+ If the provided lambda_ value is out of [0, 1] range.
41
+ """
42
+ if lambda_ > 1.0 or lambda_ < 0.0:
43
+ raise ValueError("Value of lambda_ should lie in [0, 1] range")
44
+
45
+ # Make sure w0 is a row vector with unit length
46
+ template = np.atleast_1d(np.squeeze(template))
47
+ if template.ndim > 1:
48
+ raise ValueError(
49
+ f"Template weights should be a vector, got "
50
+ f"{template.ndim} dimensions instead"
51
+ )
52
+ template = template / norm(template)
53
+ template = template[np.newaxis, :]
54
+
55
+ # Split lead field matrix into in and out parts
56
+ L_in = leadfield[:, mask]
57
+ L_out = leadfield[:, ~mask]
58
+
59
+ # Solve the generalized eigenvalue problem
60
+ A_rat = L_out @ L_out.T
61
+ A_sim = L_in @ (template.T @ template) @ L_in.T
62
+ A = (1 - lambda_) * A_rat - lambda_ * A_sim
63
+ A_reg = A + regA * np.trace(A) * np.eye(*A.shape) / A.shape[0]
64
+ B = L_in @ L_in.T
65
+ B_reg = B + regB * np.trace(B) * np.eye(*B.shape) / B.shape[0]
66
+ [eigvals, eigvecs] = eig(A_reg, B_reg)
67
+
68
+ # Get the eigenvector that corresponds to the smallest eigenvalue
69
+ w = eigvecs[:, eigvals.argmin()]
70
+ if np.dot(w @ L_in, template.T) < 0:
71
+ w *= -1
72
+
73
+ return w
roiextract/filter.py ADDED
@@ -0,0 +1,154 @@
1
+ import numpy as np
2
+ import numpy.typing as npt
3
+ import mne
4
+
5
+ from typing import Collection
6
+
7
+ from .utils import (
8
+ _check_input,
9
+ data2stc,
10
+ get_inverse_matrix,
11
+ get_label_mask,
12
+ get_aggregation_weights,
13
+ )
14
+
15
+
16
+ class SpatialFilter:
17
+ """
18
+ Convenience wrapper around a NumPy array that contains the spatial filter.
19
+
20
+ Parameters:
21
+ -----------
22
+ w: array_like, shape (n_channels,)
23
+ The weights of the spatial filter.
24
+ method: str, optional
25
+ Can be used to store the name of the method that was used to obtain the filter.
26
+ method_params: dict, optional
27
+ Can be used to store key parameters of the method for obtaining the filter.
28
+ name: str, optional
29
+ Can be used to add a unique name to the filter (e.g., name of the ROI).
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ w: npt.ArrayLike,
35
+ method: str = "",
36
+ method_params: dict = dict(),
37
+ name: str = "",
38
+ ) -> None:
39
+ self.w = w
40
+ self.method = method
41
+ self.method_params = method_params
42
+ self.name = name
43
+
44
+ def __repr__(self) -> str:
45
+ """
46
+ Generate a short description for the filter in the following form:
47
+ ([x] - only added if present)
48
+
49
+ <SpatialFilter | [name] | [method (method_params)] | XX channels>
50
+ """
51
+ result = "<SpatialFilter"
52
+ if self.name:
53
+ result += f" | {self.name}"
54
+ if self.method:
55
+ params_str = [f"{k}={v}" for k, v in self.method_params.items()]
56
+ params_str = ", ".join(params_str)
57
+ params_str = f" ({params_str})" if params_str else ""
58
+ result += f" | {self.method}{params_str}"
59
+ result += f" | {self.w.size} channels>"
60
+
61
+ return result
62
+
63
+ @classmethod
64
+ def from_inverse(
65
+ cls,
66
+ fwd,
67
+ inv,
68
+ label,
69
+ inv_method,
70
+ lambda2,
71
+ roi_method,
72
+ subject,
73
+ subjects_dir,
74
+ ):
75
+ src = fwd["src"]
76
+ mask = get_label_mask(label, src)
77
+ W = get_inverse_matrix(inv, fwd, inv_method, lambda2)
78
+ w_agg = get_aggregation_weights(
79
+ roi_method, label, src, subject, subjects_dir
80
+ )
81
+ w = w_agg @ W[mask, :]
82
+ return cls(
83
+ np.atleast_1d(np.squeeze(w)),
84
+ method=f"{inv_method}+{roi_method}",
85
+ method_params=dict(lambda2=lambda2),
86
+ name=label.name,
87
+ )
88
+
89
+ def apply(self, data: npt.ArrayLike) -> np.array:
90
+ # TODO: check that the first dimension of data is suitable
91
+ return self.w @ data
92
+
93
+ def apply_raw(self, raw: mne.io.Raw | mne.io.RawArray) -> np.array:
94
+ return self.apply(raw.get_data())
95
+
96
+ def get_ctf(
97
+ self,
98
+ L: npt.ArrayLike,
99
+ mode: str = "power",
100
+ normalize: str | None = "sum",
101
+ ) -> np.array:
102
+ _check_input("mode", mode, ["power", "amplitude"])
103
+ _check_input("normalize", normalize, ["norm", "max", "sum", None])
104
+
105
+ # Estimate the CTF
106
+ ctf = self.w @ L
107
+ if mode == "power":
108
+ ctf = ctf**2
109
+
110
+ # Normalize if needed
111
+ if normalize == "norm":
112
+ ctf /= np.linalg.norm(ctf)
113
+ elif normalize == "max":
114
+ ctf /= np.abs(ctf).max()
115
+ elif normalize == "sum":
116
+ ctf /= np.abs(ctf).sum()
117
+ return ctf
118
+
119
+ def get_ctf_fwd(
120
+ self,
121
+ fwd: mne.Forward,
122
+ mode: str = "power",
123
+ normalize: str = "norm",
124
+ subject: str | None = None,
125
+ ) -> mne.SourceEstimate:
126
+ leadfield = fwd["sol"]["data"]
127
+ src = fwd["src"]
128
+ return data2stc(
129
+ self.get_ctf(leadfield, mode, normalize), src, subject=subject
130
+ )
131
+
132
+ def plot(self, info: mne.Info, **topomap_kwargs):
133
+ # Make sure that the provided info has the correct amount of channels
134
+ n_chans_filter = self.w.size
135
+ n_chans_info = len(info["ch_names"])
136
+ if n_chans_filter != n_chans_info:
137
+ raise ValueError(
138
+ f"The amount of channels in the provided Info object ({n_chans_info})"
139
+ f" does not match the length of the spatial filter ({n_chans_filter})"
140
+ )
141
+
142
+ w = np.squeeze(self.w)
143
+ return mne.viz.plot_topomap(w, info, **topomap_kwargs)
144
+
145
+
146
+ def apply_batch(data, filters: Collection[SpatialFilter]) -> np.array:
147
+ # TODO: check that all filters have the same number of channels
148
+ # TODO: check that the first dimension of data is suitable
149
+ w = np.vstack([sf.w[np.newaxis, :] for sf in filters])
150
+ return w @ data
151
+
152
+
153
+ def apply_batch_raw(raw, filters: Collection[SpatialFilter]) -> np.array:
154
+ return apply_batch(raw.get_data(), filters)
@@ -0,0 +1,82 @@
1
+ import numpy as np
2
+
3
+ from numpy.linalg import norm
4
+ from scipy.optimize import minimize
5
+
6
+
7
+ def _ctf_ratio(w, L, mask):
8
+ # Split the leadfield into within and outside parts
9
+ L_in = L[:, mask]
10
+ L_out = L[:, ~mask]
11
+
12
+ ctf_out = np.squeeze(w @ L_out)
13
+ ctf_in = np.squeeze(w @ L_in)
14
+
15
+ F_out = norm(ctf_out) ** 2
16
+ F_in = norm(ctf_in) ** 2
17
+
18
+ # Function and its gradient for optimization
19
+ F = F_out / F_in
20
+ dF = 2 * w @ (L_out @ L_out.T - F * L_in @ L_in.T) / F_in
21
+
22
+ return F, dF
23
+
24
+
25
+ def _ctf_homogeneity(w, L, P0, mask):
26
+ # Use only voxels within the mask
27
+ L_in = L[:, mask]
28
+
29
+ # Make sure P0 is a row vector with unit length
30
+ P0 = np.squeeze(P0)[np.newaxis, :]
31
+ P0 = P0 / norm(P0)
32
+
33
+ ctf_in = np.squeeze(w @ L_in)
34
+ P_in = ctf_in**2
35
+ dotprod = np.squeeze(P_in @ P0.T)
36
+ L_P = L_in * np.tile(P0, (L.shape[0], 1))
37
+
38
+ F = dotprod**2 / norm(P_in) ** 2
39
+ dF = (
40
+ 4
41
+ / (norm(P_in) ** 2)
42
+ * (dotprod * ctf_in @ L_P.T - F * ctf_in**3 @ L_in.T)
43
+ )
44
+
45
+ # multiply with -1 to minimize
46
+ return (-1) * F, (-1) * dF
47
+
48
+
49
+ def _ctf_compromise(w, L, P0, mask, lambda_):
50
+ F_hom, dF_hom = _ctf_homogeneity(w, L, P0, mask)
51
+ F_rat, dF_rat = _ctf_ratio(w, L, mask)
52
+
53
+ F = lambda_ * F_hom + (1 - lambda_) * F_rat
54
+ dF = lambda_ * dF_hom + (1 - lambda_) * dF_rat
55
+
56
+ return F, dF
57
+
58
+
59
+ def minimize_with_several_guesses(fun, x0s, args, **kwargs):
60
+ solutions = {}
61
+ for x0 in x0s:
62
+ result = minimize(fun, x0, args=args, **kwargs)
63
+ solutions[result.fun] = result
64
+
65
+ best_result = min(list(solutions.keys()))
66
+ return solutions[best_result]
67
+
68
+
69
+ def ctf_optimize_ratio_homogeneity(
70
+ leadfield, template, mask, lambda_, x0s, return_scipy=False, **kwargs
71
+ ):
72
+ result = minimize_with_several_guesses(
73
+ _ctf_compromise,
74
+ x0s,
75
+ args=(leadfield, template, mask, lambda_),
76
+ jac=True,
77
+ **kwargs,
78
+ )
79
+ if return_scipy:
80
+ return result
81
+
82
+ return result.x
roiextract/optimize.py ADDED
@@ -0,0 +1,288 @@
1
+ import numpy as np
2
+
3
+ from functools import partial
4
+
5
+ from .analytic import ctf_optimize_ratio_similarity
6
+ from .filter import SpatialFilter
7
+ from .numerical import ctf_optimize_ratio_homogeneity
8
+ from .quantify import ctf_quantify
9
+ from .utils import (
10
+ get_label_mask,
11
+ resolve_template,
12
+ _check_input,
13
+ _report_props,
14
+ logger,
15
+ )
16
+
17
+
18
+ INITIAL_LAMBDAS = {"rat": 0, "sim": 0.999, "hom": 0.999}
19
+ IS_DECREASING = {"rat": True, "sim": False, "hom": False}
20
+
21
+
22
+ def suggest_lambda(opt_func, quant_func, criteria, threshold, tol=0.001):
23
+ initial_lambda = INITIAL_LAMBDAS[criteria]
24
+ props = quant_func(w=opt_func(lambda_=initial_lambda))
25
+ crit_thresh = threshold * props[criteria]
26
+ logger.info(
27
+ f"Suggesting lambda to obtain at least {threshold:.2g} of max {criteria}"
28
+ )
29
+ logger.info(f"Properties (lambda={initial_lambda}): {_report_props(props)}")
30
+ logger.info(f"Criteria threshold: {crit_thresh:.3g}")
31
+
32
+ left, right = 0, 1
33
+ while right - left > tol:
34
+ mid = (left + right) / 2
35
+ props = quant_func(w=opt_func(lambda_=mid))
36
+ logger.info(f"Properties (lambda={mid:.3g}): {_report_props(props)}")
37
+ if IS_DECREASING[criteria]:
38
+ if props[criteria] > crit_thresh:
39
+ left = mid
40
+ else:
41
+ right = mid
42
+ else:
43
+ if props[criteria] > crit_thresh:
44
+ right = mid
45
+ else:
46
+ left = mid
47
+
48
+ return (left + right) / 2
49
+
50
+
51
+ def ctf_optimize(
52
+ leadfield,
53
+ template,
54
+ mask,
55
+ lambda_,
56
+ mode="similarity",
57
+ criteria="rat",
58
+ threshold=None,
59
+ initial="auto",
60
+ tol=0.001,
61
+ reg=0.001,
62
+ quantify=False,
63
+ name="",
64
+ ):
65
+ """
66
+ Derive a spatial filter that optimizes properties of the CTF for the extracted ROI time series
67
+
68
+ Parameters
69
+ ----------
70
+ leadfield: array_like
71
+ Lead field matrix for dipoles with fixed orientation with shape (channels, voxels).
72
+ template: array_like
73
+ Template CTF pattern for voxels with shape (voxels,). Only values within the ROI are used.
74
+ mask: array_like
75
+ Voxel mask of the region of interest with shape (voxels,). Contains ones and zeros for voxels within and
76
+ outside ROI, respectively.
77
+ lambda_: float
78
+ If 0, only ratio is optimized. If 1, only dot product. Values in between allow tweaking the balance between
79
+ optimization for dot product or ratio.
80
+ mode: str
81
+ Optimize a combination of ratio and similarity (mode="similarity") or ratio and homogeneity (mode="homogeneity").
82
+ reg: float
83
+ Regularization parameter to ensure that it is possible to calculate the inverse matrices.
84
+ quantify: bool
85
+ Whether to calculate CTF properties for the optimized spatial filter.
86
+
87
+ Returns
88
+ -------
89
+ sf: SpatialFilter
90
+ Spatial filter produced by the optimization.
91
+ props: dict, only returned if quantify=True
92
+ Dictionary that contains the estimates of CTF-based properties (ratio, similarity
93
+ and/or homogeneity).
94
+
95
+ Raises
96
+ ------
97
+ ValueError
98
+ If provided lambda_ value is out of [0, 1] range.
99
+ """
100
+ _check_input("mode", mode, ["similarity", "homogeneity"])
101
+ _check_input("initial", initial, ["auto", "ones", "reg"])
102
+ if lambda_ == "auto" and threshold is None:
103
+ raise ValueError("Threshold should be set if lambda_='auto' is used")
104
+
105
+ # Prepare the optimization and quantification functions
106
+ if mode == "similarity":
107
+ opt_func = partial(
108
+ ctf_optimize_ratio_similarity,
109
+ leadfield=leadfield,
110
+ template=template,
111
+ mask=mask,
112
+ regA=reg,
113
+ )
114
+ quant_func = partial(
115
+ ctf_quantify, leadfield=leadfield, mask=mask, w0=template
116
+ )
117
+ else:
118
+ # Setup the initial guess for numerical optimization
119
+ initial = ["ones", "reg"] if initial == "auto" else [initial]
120
+ x0s = []
121
+ if "ones" in initial:
122
+ x0_ones = np.ones((leadfield.shape[0],))
123
+ x0s.append(x0_ones)
124
+ if "reg" in initial:
125
+ # TODO: find some alternative to this formula (change lambda?)
126
+ regA = 0.1 if lambda_ == "auto" else np.exp(10 * lambda_**2 - 10)
127
+ x0_reg = ctf_optimize_ratio_similarity(
128
+ leadfield, template, mask, lambda_=0, regA=regA, regB=reg
129
+ )
130
+ x0s.append(x0_reg)
131
+
132
+ opt_func = partial(
133
+ ctf_optimize_ratio_homogeneity,
134
+ leadfield=leadfield,
135
+ template=template,
136
+ mask=mask,
137
+ x0s=x0s,
138
+ )
139
+ quant_func = partial(
140
+ ctf_quantify, leadfield=leadfield, mask=mask, P0=template
141
+ )
142
+
143
+ # Suggest lambda if needed
144
+ if lambda_ == "auto":
145
+ lambda_ = suggest_lambda(
146
+ opt_func, quant_func, criteria, threshold, tol=tol
147
+ )
148
+ logger.info(
149
+ f"lambda={lambda_:.2g} was selected using the {threshold:2g} threshold"
150
+ )
151
+
152
+ # Optimize the filter, normalize and quantify its properties if needed
153
+ w_opt = opt_func(lambda_=lambda_)
154
+ w_opt = w_opt / np.abs(w_opt).max()
155
+ sf = SpatialFilter(
156
+ w=w_opt,
157
+ method="ctf_optimize",
158
+ method_params=dict(lambda_=lambda_),
159
+ name=name,
160
+ )
161
+ if quantify:
162
+ props = quant_func(w=w_opt)
163
+ return sf, props
164
+
165
+ return sf
166
+
167
+
168
+ def ctf_optimize_label(
169
+ fwd,
170
+ label,
171
+ template,
172
+ lambda_,
173
+ mode="similarity",
174
+ criteria="rat",
175
+ threshold=None,
176
+ initial="auto",
177
+ tol=0.001,
178
+ reg=0.001,
179
+ quantify=False,
180
+ name=None,
181
+ ):
182
+ # Extract data from Forward and Label
183
+ leadfield = fwd["sol"]["data"]
184
+ src = fwd["src"]
185
+ if name is None:
186
+ name = label.name
187
+
188
+ # Create a binary mask for the ROI
189
+ mask = get_label_mask(label, src)
190
+
191
+ # Support pre-defined options for template weights
192
+ template = resolve_template(template, label, src)
193
+
194
+ # Optimize the filter and quantify its properties if needed
195
+ return ctf_optimize(
196
+ leadfield,
197
+ template,
198
+ mask,
199
+ lambda_,
200
+ mode=mode,
201
+ criteria=criteria,
202
+ threshold=threshold,
203
+ initial=initial,
204
+ tol=tol,
205
+ reg=reg,
206
+ quantify=quantify,
207
+ name=name,
208
+ )
209
+
210
+
211
+ def rec_optimize(
212
+ cov_matrix,
213
+ inverse,
214
+ template,
215
+ mask,
216
+ lambda_,
217
+ mode="similarity",
218
+ criteria="rat",
219
+ threshold=None,
220
+ tol=0.001,
221
+ reg=0.001,
222
+ quantify=False,
223
+ name="",
224
+ ):
225
+ return ctf_optimize(
226
+ cov_matrix.T @ inverse,
227
+ template,
228
+ mask,
229
+ lambda_,
230
+ mode=mode,
231
+ criteria=criteria,
232
+ threshold=threshold,
233
+ tol=tol,
234
+ reg=reg,
235
+ quantify=quantify,
236
+ name=name,
237
+ )
238
+
239
+
240
+ def rec_optimize_label(
241
+ cov_matrix,
242
+ fwd,
243
+ inv,
244
+ label,
245
+ template,
246
+ lambda_,
247
+ mode="similarity",
248
+ criteria="rat",
249
+ threshold=None,
250
+ tol=0.001,
251
+ reg=0.001,
252
+ quantify=False,
253
+ name=None,
254
+ **inv_kwargs,
255
+ ):
256
+ from mne.minimum_norm.resolution_matrix import (
257
+ _get_matrix_from_inverse_operator,
258
+ )
259
+
260
+ # Extract data from Forward and Label
261
+ src = fwd["src"]
262
+ if name is None:
263
+ name = label.name
264
+
265
+ # Extract data from InverseOperator
266
+ inverse = _get_matrix_from_inverse_operator(inv, fwd, **inv_kwargs)
267
+
268
+ # Create a binary mask for the ROI
269
+ mask = get_label_mask(label, src)
270
+
271
+ # Support pre-defined options for template weights
272
+ template = resolve_template(template, label, src)
273
+
274
+ # Optimize the filter and quantify its properties if needed
275
+ return rec_optimize(
276
+ cov_matrix,
277
+ inverse,
278
+ template,
279
+ mask,
280
+ lambda_,
281
+ mode=mode,
282
+ criteria=criteria,
283
+ threshold=threshold,
284
+ tol=tol,
285
+ reg=reg,
286
+ quantify=quantify,
287
+ name=name,
288
+ )
roiextract/quantify.py ADDED
@@ -0,0 +1,178 @@
1
+ import numpy as np
2
+
3
+ from numpy.linalg import norm
4
+
5
+ from .utils import resolve_template, get_label_mask
6
+
7
+
8
+ def ctf_ratio(w, L, mask, source_mask=None):
9
+ """
10
+ Calculates ratio of CTF outside/within ROI
11
+ """
12
+
13
+ # Take into account only a subset of sources if source mask is provided
14
+ L_all = L.copy()
15
+ if source_mask is not None:
16
+ mask = np.logical_and(mask, source_mask)
17
+ L_all = L[:, source_mask]
18
+
19
+ # TODO: check that the mask contains at least one source
20
+
21
+ # Split lead field matrix into in and out parts
22
+ L_in = L[:, mask]
23
+
24
+ ctf_all = np.squeeze(w @ L_all)
25
+ ctf_in = np.squeeze(w @ L_in)
26
+
27
+ # Adjusted value in the [0, 1] range
28
+ return norm(ctf_in) / norm(ctf_all)
29
+
30
+
31
+ def ctf_similarity(w, L, w0, mask, source_mask=None):
32
+ """
33
+ Cosine similarity of desired and constructed CTFs within ROI
34
+ """
35
+
36
+ # Take into account only a subset of sources if source mask is provided
37
+ if source_mask is not None:
38
+ within_mask = source_mask[mask]
39
+ mask = np.logical_and(mask, source_mask)
40
+ assert within_mask.sum() > 0
41
+
42
+ # Use only voxels within the mask
43
+ L_in = L[:, mask]
44
+
45
+ # TODO: check that the mask contains at least one source
46
+ assert mask.sum() > 0
47
+
48
+ # Make sure w0 is a row vector with unit length
49
+ if source_mask is not None:
50
+ w0 = w0[within_mask]
51
+ if w0.size > 1:
52
+ w0 = np.squeeze(w0)
53
+ w0 = w0[np.newaxis, :]
54
+ w0 = w0 / norm(w0)
55
+
56
+ dotprod = np.squeeze(w @ L_in @ w0.T)
57
+ ctf = np.squeeze(w @ L_in)
58
+
59
+ # Adjusted value in the [0, 1] range
60
+ return np.abs(dotprod / norm(ctf))
61
+
62
+
63
+ def ctf_homogeneity(w, L, P0, mask, source_mask=None):
64
+ """
65
+ Homogeneity of the CTF within ROI
66
+ """
67
+
68
+ # Take into account only a subset of sources if source mask is provided
69
+ if source_mask is not None:
70
+ within_mask = source_mask[mask]
71
+ mask = np.logical_and(mask, source_mask)
72
+ assert within_mask.sum() > 0
73
+
74
+ # TODO: check that the mask contains at least one source
75
+ assert mask.sum() > 0
76
+
77
+ # Use only voxels within the mask
78
+ L_in = L[:, mask]
79
+
80
+ # Make sure P0 is a row vector with unit length
81
+ if source_mask is not None:
82
+ P0 = P0[within_mask]
83
+ if P0.size > 1:
84
+ P0 = np.squeeze(P0)
85
+ P0 = P0[np.newaxis, :]
86
+ P0 = P0 / norm(P0)
87
+
88
+ ctf2 = (w @ L_in) ** 2
89
+ dotprod = np.squeeze(ctf2 @ P0.T)
90
+
91
+ # Adjusted value in the [0, 1] range
92
+ return np.abs(dotprod / norm(ctf2))
93
+
94
+
95
+ def ctf_quantify(w, leadfield, mask, w0=None, P0=None, source_mask=None):
96
+ """
97
+ Calculate similarity within ROI and in/out ratio for a given filter
98
+
99
+ Parameters
100
+ ----------
101
+ w: array_like
102
+ Spatial filter that needs to be quantified in terms of CTF.
103
+ L: array_like
104
+ Lead field matrix for dipoles with fixed orientation with shape (channels, voxels).
105
+ mask: array_like
106
+ Voxel mask of the region of interest with shape (voxels,). Contains ones and zeros for voxels within and
107
+ outside ROI, respectively.
108
+ w0: array_like
109
+ Template CTF pattern for voxels within ROI with shape (voxels_roi,).
110
+ P0: array_like
111
+ Template power contribution for voxels within ROI with shape (voxels_roi,).
112
+ source_mask: array_like
113
+ Voxel mask with shape (voxels,). Ones correspond to all voxels that should be
114
+ considered when evaluating CTF properties.
115
+
116
+ Returns
117
+ -------
118
+ result: a dictionary with the estimated CTF properties
119
+ 'rat': float
120
+ Ratio of CTF within ROI and total CTF, lies in [0, 1].
121
+ 'sim': float
122
+ Similarity between the actual CTF and the template CTF pattern, lies in [0, 1]. It is only returned if w0 was provided.
123
+ 'hom': float
124
+ Homogeneity of the power contributions of voxels within the ROI, lies in [0, 1]. It is only returned if P0 was provided.
125
+
126
+ """
127
+
128
+ result = dict()
129
+ result["rat"] = ctf_ratio(w, leadfield, mask, source_mask=source_mask)
130
+ if w0 is not None:
131
+ result["sim"] = ctf_similarity(
132
+ w, leadfield, w0, mask, source_mask=source_mask
133
+ )
134
+ if P0 is not None:
135
+ result["hom"] = ctf_homogeneity(
136
+ w, leadfield, P0, mask, source_mask=source_mask
137
+ )
138
+
139
+ return result
140
+
141
+
142
+ def ctf_quantify_label(w, fwd, label, w0=None, P0=None, source_mask=None):
143
+ # Extract data from Forward
144
+ leadfield = fwd["sol"]["data"]
145
+ src = fwd["src"]
146
+
147
+ # Create a binary mask for the ROI
148
+ mask = get_label_mask(label, src)
149
+
150
+ # Support pre-defined options for template weights
151
+ if w0 is not None:
152
+ w0 = resolve_template(w0, label, src)
153
+ if P0 is not None:
154
+ P0 = resolve_template(P0, label, src)
155
+
156
+ return ctf_quantify(
157
+ w, leadfield, mask, w0=w0, P0=P0, source_mask=source_mask
158
+ )
159
+
160
+
161
+ def rec_quantify(w, cov_matrix, inverse, template, mask, source_mask=None):
162
+ return ctf_quantify(
163
+ w, cov_matrix.T @ inverse, template, mask, source_mask=source_mask
164
+ )
165
+
166
+
167
+ def rec_quantify_label(w, fwd, label, template, source_mask=None):
168
+ # Extract data from Forward
169
+ leadfield = fwd["sol"]["data"]
170
+ src = fwd["src"]
171
+
172
+ # Create a binary mask for the ROI
173
+ mask = get_label_mask(label, src)
174
+
175
+ # Support pre-defined options for template weights
176
+ template = resolve_template(template, label, src)
177
+
178
+ return ctf_quantify(w, leadfield, template, mask, source_mask=source_mask)
roiextract/utils.py ADDED
@@ -0,0 +1,77 @@
1
+ import logging
2
+ import numpy as np
3
+ import mne
4
+
5
+
6
+ logger = logging.getLogger("roiextract")
7
+
8
+
9
+ def _check_input(param, value, allowed_values):
10
+ if value not in allowed_values:
11
+ raise ValueError(f"Value {value} is not supported for {param}")
12
+
13
+
14
+ def _report_props(props):
15
+ return ", ".join([f"{k}={v:.2g}" for k, v in props.items()])
16
+
17
+
18
+ def get_label_mask(label, src):
19
+ vertno = [s["vertno"] for s in src]
20
+ nvert = [len(vn) for vn in vertno]
21
+ if label.hemi == "lh":
22
+ this_vertices = np.intersect1d(vertno[0], label.vertices)
23
+ vert = np.searchsorted(vertno[0], this_vertices)
24
+ elif label.hemi == "rh":
25
+ this_vertices = np.intersect1d(vertno[1], label.vertices)
26
+ vert = nvert[0] + np.searchsorted(vertno[1], this_vertices)
27
+ else:
28
+ raise ValueError("label %s has invalid hemi" % label.name)
29
+
30
+ mask = np.zeros((sum(nvert),), dtype=int)
31
+ mask[vert] = 1
32
+ return mask > 0
33
+
34
+
35
+ def resolve_template(template, label, src):
36
+ if isinstance(template, str):
37
+ from mne.label import label_sign_flip
38
+
39
+ _check_input("template", template, ["mean_flip", "mean"])
40
+ signflip = label_sign_flip(label, src)[np.newaxis, :]
41
+
42
+ if template == "mean_flip":
43
+ return signflip
44
+ if template == "mean":
45
+ return np.ones((1, signflip.size))
46
+
47
+ return template
48
+
49
+
50
+ def data2stc(data, src, subject=None):
51
+ vertno = [s["vertno"] for s in src]
52
+ return mne.SourceEstimate(
53
+ data=data, vertices=vertno, tmin=0, tstep=0.01, subject=subject
54
+ )
55
+
56
+
57
+ def get_inverse_matrix(inv, fwd, method, lambda2):
58
+ # TODO: get rid of private dependency
59
+ from mne.minimum_norm.resolution_matrix import (
60
+ _get_matrix_from_inverse_operator,
61
+ )
62
+
63
+ return _get_matrix_from_inverse_operator(inv, fwd, method, lambda2)
64
+
65
+
66
+ def get_aggregation_weights(method, label, src, subject, subjects_dir):
67
+ _check_input("method", method, ["mean", "mean_flip", "centroid"])
68
+ if method in ["mean", "mean_flip"]:
69
+ return resolve_template(method, label, src)
70
+
71
+ # find the center of mass
72
+ hemi_idx = 0 if label.hemi == "lh" else 1
73
+ label_vertices = label.get_vertices_used(src[hemi_idx]["vertno"])
74
+ centroid_idx = label.center_of_mass(
75
+ subject=subject, restrict_vertices=src, subjects_dir=subjects_dir
76
+ )
77
+ return (label_vertices == centroid_idx).astype(int)
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.3
2
+ Name: roiextract
3
+ Version: 0.0.2
4
+ Summary: Data-independent and data-driven optimization of extraction of ROI time series based on M/EEG
5
+ Project-URL: Homepage, https://github.com/ctrltz/roiextract
6
+ Project-URL: Bug Tracker, https://github.com/ctrltz/roiextract/issues
7
+ Author-email: Nikolai Kapralov <kapralov@cbs.mpg.de>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.7
13
+ Requires-Dist: mne
14
+ Requires-Dist: numpy
15
+ Requires-Dist: scipy
16
+ Description-Content-Type: text/markdown
17
+
18
+ # ROIextract
19
+
20
+ Optimization of extraction of ROI time series based on the cross-talk function (CTF) or source reconstruction of spatial patterns (REC). **Work in progress!**
21
+
22
+ ## Usage
23
+
24
+ Obtain a spatial filter that optimizes CTF properties:
25
+
26
+ ```python
27
+ from roiextract import ctf_optimize_label
28
+
29
+ sf = ctf_optimize_label(fwd, label, template, lambda_)
30
+
31
+ sf, props = ctf_optimize_label(fwd, label, template, lambda_, quantify=True)
32
+
33
+ sf = ctf_optimize_label(fwd, label, template, lambda_='auto', threshold=0.95)
34
+ ```
35
+
36
+ Plot the filter as a topomap:
37
+
38
+ ```python
39
+ sf.plot(info)
40
+ ```
41
+
42
+ Apply it to the data to obtain the time course of activity in the ROI/label:
43
+
44
+ ```python
45
+ label_tc = sf.apply(data)
46
+ ```
47
+
48
+ Estimate the CTF for the filter:
49
+
50
+ ```python
51
+ ctf = sf.get_ctf_fwd(fwd) # ctf is an instance of mne.SourceEstimate
52
+ ```
53
+
54
+ Plot the CTF on the brain surface:
55
+
56
+ ```python
57
+ ctf.plot()
58
+ ```
@@ -0,0 +1,11 @@
1
+ roiextract/__init__.py,sha256=_GA_t1v_b8OKJtvhKOcvCbl4A3dcJ4N_AHmPJCW2-pc,159
2
+ roiextract/analytic.py,sha256=WuiQmHZ06Mvtd0MrcasbXiaukDJi1odprJM8oS-YLEI,2390
3
+ roiextract/filter.py,sha256=uVRK0thESf8GsPYZpx9Ljghm_Vt_fI11u4TWwLRT3D4,4661
4
+ roiextract/numerical.py,sha256=BtrBrJGf4d9-vcMLQb_LZbbdGjxvZPDZ1jg3jRzdPhA,1971
5
+ roiextract/optimize.py,sha256=fy8GpCUWi5yGq0rrq5jQJAPMN__DwG6AhEW9FYRykXA,7851
6
+ roiextract/quantify.py,sha256=jFwtGvzL_fvQxXAwRu99Mmfmz0VG7rkhILij1k4K8e0,5389
7
+ roiextract/utils.py,sha256=wTcEv0ewTS17448BThnDuFbrjhdxNCebIFEayd6IMfc,2367
8
+ roiextract-0.0.2.dist-info/METADATA,sha256=_m9pKYx3so-vruvq2dYXZa29yjNAwpsEZYyJdq88yRM,1502
9
+ roiextract-0.0.2.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
10
+ roiextract-0.0.2.dist-info/licenses/LICENSE,sha256=EgvrKcYLEbvwsR_bnQY95QeSwsP2Y0Q_azHcXvri0XM,1073
11
+ roiextract-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.24.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Nikolai Kapralov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.