fdnkit 1.0.0__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.
fdnkit/features.py ADDED
@@ -0,0 +1,212 @@
1
+ """Assemble tidy per-trial / per-segment feature tables.
2
+
3
+ Combines the DFA, MFDFA, and FODN analyses into flat dictionaries and pandas
4
+ DataFrames (one row per trial or segment). This is the port and generalization
5
+ of the original feature extractor -- but computed directly from
6
+ arrays rather than by scraping a directory of CSVs.
7
+
8
+ The five "core" features reproduce the set used in Beeram et al. (2026):
9
+ ``MeanAlpha``, ``VarAlpha``, ``LeadingEig``, ``MF_DFA_H``, ``MF_DFA_Hq_mean``.
10
+ :func:`extract_features` additionally returns a richer set (multifractal width,
11
+ sparseness, hub concentration, ...) that callers can opt into.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+
19
+ from .dfa import dfa
20
+ from .fodn import fit_fodn
21
+ from .mfdfa import mfdfa
22
+
23
+ __all__ = [
24
+ "CORE_FEATURES",
25
+ "dfa_features",
26
+ "mfdfa_features",
27
+ "fodn_features",
28
+ "extract_features",
29
+ "feature_table",
30
+ ]
31
+
32
+ CORE_FEATURES = ["MeanAlpha", "VarAlpha", "LeadingEig", "MF_DFA_H", "MF_DFA_Hq_mean"]
33
+
34
+
35
+ def _clean_channels(signals):
36
+ x = np.asarray(signals, dtype=float)
37
+ if x.ndim == 1:
38
+ x = x[None, :]
39
+ if x.ndim != 2:
40
+ raise ValueError("signals must be 1-D or 2-D (n_channels, n_samples)")
41
+ return x
42
+
43
+
44
+ def dfa_features(signals, scales=None, order: int = 1, prefix: str = "DFA") -> dict:
45
+ """Per-channel DFA Hurst, summarized across channels.
46
+
47
+ Returns ``{<prefix>_H_mean, <prefix>_H_std, <prefix>_H_max, <prefix>_H_min}``.
48
+ """
49
+ x = _clean_channels(signals)
50
+ h = np.array([dfa(x[i], scales=scales, order=order).hurst for i in range(x.shape[0])])
51
+ h = h[np.isfinite(h)]
52
+ if h.size == 0:
53
+ return {}
54
+ return {
55
+ f"{prefix}_H_mean": float(h.mean()),
56
+ f"{prefix}_H_std": float(h.std()),
57
+ f"{prefix}_H_max": float(h.max()),
58
+ f"{prefix}_H_min": float(h.min()),
59
+ }
60
+
61
+
62
+ def mfdfa_features(signals, scales=None, q=None, order: int = 1, prefix: str = "MFDFA") -> dict:
63
+ """Per-channel MFDFA, summarized across channels.
64
+
65
+ Returns generalized-Hurst mean/std, mean multifractal width ``delta_h``, and
66
+ the mean ``h(q)`` over all channels and moments.
67
+ """
68
+ x = _clean_channels(signals)
69
+ hq_means, deltas, hq_all = [], [], []
70
+ for i in range(x.shape[0]):
71
+ res = mfdfa(x[i], scales=scales, q=q, order=order)
72
+ finite = res.hq[np.isfinite(res.hq)]
73
+ if finite.size == 0:
74
+ continue
75
+ hq_means.append(float(finite.mean()))
76
+ deltas.append(res.delta_h)
77
+ hq_all.append(finite)
78
+ if not hq_means:
79
+ return {}
80
+ hq_all = np.concatenate(hq_all)
81
+ return {
82
+ f"{prefix}_Hq_mean": float(np.mean(hq_means)),
83
+ f"{prefix}_Hq_std": float(np.std(hq_means)),
84
+ f"{prefix}_delta_h_mean": float(np.nanmean(deltas)),
85
+ f"{prefix}_Hq_grand_mean": float(hq_all.mean()),
86
+ }
87
+
88
+
89
+ def fodn_features(signals, *, n_iter: int = 10, lambda_: float = 0.5, num_fract: int = 50,
90
+ top_k: int = 3, prefix: str = "FODN", **fodn_kwargs) -> dict:
91
+ """FODN network features for a multi-channel segment.
92
+
93
+ Returns fractional-order (alpha) mean/std, leading eigenvalue, network
94
+ sparseness, and the summed hub score of the top-``k`` channels.
95
+ """
96
+ x = _clean_channels(signals)
97
+ if x.shape[0] < 2:
98
+ return {}
99
+ res = fit_fodn(x, n_iter=n_iter, lambda_=lambda_, num_fract=num_fract, **fodn_kwargs)
100
+ alpha = res.alpha[np.isfinite(res.alpha)]
101
+ if alpha.size == 0:
102
+ return {}
103
+ hub = res.dominant_eigvec
104
+ hub = hub / (hub.sum() + 1e-12)
105
+ k = max(1, min(top_k, hub.size))
106
+ top_hub = float(np.sort(hub)[-k:].sum())
107
+ return {
108
+ f"{prefix}_alpha_mean": float(alpha.mean()),
109
+ f"{prefix}_alpha_std": float(alpha.std()),
110
+ f"{prefix}_alpha_var": float(alpha.var()),
111
+ f"{prefix}_leading_eig": float(res.leading_eig),
112
+ f"{prefix}_sparseness": float(res.sparseness),
113
+ f"{prefix}_hub_top{k}": top_hub,
114
+ }
115
+
116
+
117
+ def extract_features(
118
+ signals,
119
+ *,
120
+ do_dfa: bool = True,
121
+ do_mfdfa: bool = True,
122
+ do_fodn: bool = True,
123
+ scales=None,
124
+ q=None,
125
+ order: int = 1,
126
+ fodn_kwargs: dict | None = None,
127
+ include_core_aliases: bool = True,
128
+ ) -> dict:
129
+ """Compute a single tidy feature row for one multi-channel segment.
130
+
131
+ Parameters
132
+ ----------
133
+ signals : array-like, shape (n_channels, n_samples)
134
+ do_dfa, do_mfdfa, do_fodn : bool
135
+ Toggle each analysis family.
136
+ scales, q, order : see the analysis modules.
137
+ fodn_kwargs : dict, optional
138
+ Extra keyword arguments for :func:`fdnkit.fodn.fit_fodn`.
139
+ include_core_aliases : bool
140
+ Also emit the five canonical column names in :data:`CORE_FEATURES`
141
+ (``MeanAlpha`` etc.) so results line up with the reference study.
142
+
143
+ Returns
144
+ -------
145
+ dict
146
+ Feature name -> value.
147
+ """
148
+ feats: dict = {}
149
+ if do_dfa:
150
+ feats.update(dfa_features(signals, scales=scales, order=order))
151
+ if do_mfdfa:
152
+ feats.update(mfdfa_features(signals, scales=scales, q=q, order=order))
153
+ if do_fodn:
154
+ feats.update(fodn_features(signals, **(fodn_kwargs or {})))
155
+
156
+ if include_core_aliases:
157
+ alias = {}
158
+ if "FODN_alpha_mean" in feats:
159
+ alias["MeanAlpha"] = feats["FODN_alpha_mean"]
160
+ if "FODN_alpha_var" in feats:
161
+ alias["VarAlpha"] = feats["FODN_alpha_var"]
162
+ if "FODN_leading_eig" in feats:
163
+ alias["LeadingEig"] = feats["FODN_leading_eig"]
164
+ if "DFA_H_mean" in feats:
165
+ alias["MF_DFA_H"] = feats["DFA_H_mean"]
166
+ if "MFDFA_Hq_grand_mean" in feats:
167
+ alias["MF_DFA_Hq_mean"] = feats["MFDFA_Hq_grand_mean"]
168
+ feats.update(alias)
169
+ return feats
170
+
171
+
172
+ def feature_table(trials, *, id_key="trial_id", group_key="group", label_key="label",
173
+ progress: bool = False, **extract_kwargs) -> pd.DataFrame:
174
+ """Build a per-trial feature DataFrame from an iterable of trial records.
175
+
176
+ Parameters
177
+ ----------
178
+ trials : iterable of dict
179
+ Each record must have a ``"signals"`` array of shape
180
+ ``(n_channels, n_samples)`` and may carry ``trial_id``, ``group``
181
+ (e.g. subject id, for honest CV), and ``label``.
182
+ id_key, group_key, label_key : str
183
+ Keys copied through to identifier columns when present.
184
+ progress : bool
185
+ Print a short progress line per trial.
186
+ **extract_kwargs
187
+ Forwarded to :func:`extract_features`.
188
+
189
+ Returns
190
+ -------
191
+ pandas.DataFrame
192
+ One row per trial; identifier columns first, then features.
193
+ """
194
+ rows = []
195
+ trials = list(trials)
196
+ for i, rec in enumerate(trials):
197
+ if "signals" not in rec:
198
+ raise KeyError("each trial record needs a 'signals' array")
199
+ if progress:
200
+ print(f"[fdnkit] features {i + 1}/{len(trials)}", flush=True)
201
+ feats = extract_features(rec["signals"], **extract_kwargs)
202
+ row = {}
203
+ for key, col in ((id_key, id_key), (group_key, group_key), (label_key, label_key)):
204
+ if key in rec:
205
+ row[col] = rec[key]
206
+ row.update(feats)
207
+ rows.append(row)
208
+
209
+ df = pd.DataFrame(rows)
210
+ id_cols = [c for c in (id_key, group_key, label_key) if c in df.columns]
211
+ other = [c for c in df.columns if c not in id_cols]
212
+ return df[id_cols + other]
fdnkit/fodn.py ADDED
@@ -0,0 +1,346 @@
1
+ """Fractional-Order Dynamical Network (FODN) model.
2
+
3
+ Ports the FODN estimator from the original reference implementation into a
4
+ clean, documented, sklearn-style estimator. The numerical procedure is preserved faithfully:
5
+
6
+ 1. **Fractional order per channel** (:math:`\\alpha_i`) is estimated from the
7
+ variance decay of a Haar wavelet transform across dyadic scales.
8
+ 2. **Grunwald-Letnikov fractional differencing** builds the fractional-derivative
9
+ signal ``z`` for each channel from its ``alpha``.
10
+ 3. **Coupling matrix** ``A`` is fit by regularized least squares
11
+ (``z_k ≈ A x_{k-1}``), then refined by an ADMM-LASSO unknown-input step that
12
+ promotes a sparse directed network.
13
+
14
+ The model underlies the "fractional dynamical network" features in Beeram et al.
15
+ (2026, *Front. Netw. Physiol.* 6:1768476); the underlying method is due to
16
+ Gupta, Pequito & Bogdan (2018) and Xue & Bogdan (2017).
17
+
18
+ The heavy inner loop is unchanged from the validated source; the public surface
19
+ (:class:`FODN`, :func:`fit_fodn`, :class:`FODNResult`) is new.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from dataclasses import dataclass
25
+
26
+ import numpy as np
27
+ import scipy.linalg as LA
28
+ from scipy.special import gamma
29
+
30
+ __all__ = ["HaarWaveletTransform", "FODN", "FODNResult", "fit_fodn"]
31
+
32
+
33
+ class HaarWaveletTransform:
34
+ """Fast in-place Haar wavelet transform of a 1-D signal.
35
+
36
+ Used by the FODN model to estimate a channel's fractional order from the
37
+ variance of detail coefficients across dyadic scales.
38
+ """
39
+
40
+ def __init__(self, x):
41
+ x = np.asarray(x, dtype=float)
42
+ if x.ndim > 1:
43
+ x = np.squeeze(x)
44
+ if x.ndim != 1:
45
+ raise ValueError("HaarWaveletTransform accepts only 1-D signals")
46
+ self.x = x
47
+ self._n = x.size
48
+
49
+ def normalize(self):
50
+ """Subtract the mean in place."""
51
+ self.x = self.x - np.mean(self.x)
52
+
53
+ @staticmethod
54
+ def _dwt_haar(signal):
55
+ n_use = int(np.floor(signal.size / 2))
56
+ c = (signal[: 2 * n_use : 2] + signal[1 : 2 * n_use : 2]) / 2
57
+ s = signal[: 2 * n_use : 2] - c
58
+ c = 2 * c / np.sqrt(2)
59
+ s = -2 * s / np.sqrt(2)
60
+ return c, s
61
+
62
+ def transform(self):
63
+ """Return approximation (``W``) and detail (``D``) coefficient tables."""
64
+ n_by2 = int(np.floor(self._n / 2))
65
+ approx = np.zeros((n_by2, n_by2))
66
+ detail = np.zeros((n_by2, n_by2))
67
+ j = self._n
68
+ signal = self.x
69
+ for i in range(int(np.floor(np.log2(self._n)))):
70
+ j = int(np.floor(j / 2))
71
+ w, d = self._dwt_haar(signal)
72
+ approx[i, :j] = w
73
+ detail[i, :j] = d
74
+ signal = w
75
+ return approx, detail
76
+
77
+
78
+ @dataclass
79
+ class FODNResult:
80
+ """Output of a fitted :class:`FODN` model.
81
+
82
+ Attributes
83
+ ----------
84
+ alpha : numpy.ndarray
85
+ Per-channel fractional orders (length ``n_channels``).
86
+ coupling : numpy.ndarray
87
+ Estimated directed coupling matrix ``A`` (``n_channels x n_channels``).
88
+ eigenvalues : numpy.ndarray
89
+ Eigenvalues of ``A`` (complex).
90
+ leading_eig : float
91
+ Spectral radius: ``max |eigenvalue|`` (network gain / stability proxy).
92
+ dominant_eigvec : numpy.ndarray
93
+ Magnitude of the eigenvector for the largest-real-part eigenvalue;
94
+ a per-channel "hub" score.
95
+ sparseness : float
96
+ Fraction of ``|A|`` entries exceeding ``1e-2`` (network density).
97
+ """
98
+
99
+ alpha: np.ndarray
100
+ coupling: np.ndarray
101
+ eigenvalues: np.ndarray
102
+ leading_eig: float
103
+ dominant_eigvec: np.ndarray
104
+ sparseness: float
105
+
106
+
107
+ class FODN:
108
+ """Fractional-Order Dynamical Network estimator.
109
+
110
+ Parameters
111
+ ----------
112
+ num_inputs : int, optional
113
+ Number of unknown inputs for the LASSO step. Defaults to
114
+ ``floor(n_channels / 2)``.
115
+ num_fract : int
116
+ Truncation length of the Grunwald-Letnikov fractional-difference kernel.
117
+ n_iter : int
118
+ Number of ADMM refinement iterations.
119
+ lambda_ : float
120
+ LASSO sparsity weight for the unknown-input estimate.
121
+ verbose : bool
122
+ Print per-iteration MSE and timing.
123
+
124
+ Attributes (after :meth:`fit`)
125
+ ------------------------------
126
+ alpha_ : numpy.ndarray
127
+ Per-channel fractional orders.
128
+ coupling_ : numpy.ndarray
129
+ Final coupling matrix ``A``.
130
+ """
131
+
132
+ def __init__(self, num_inputs=None, num_fract=50, n_iter=10, lambda_=0.5, verbose=False):
133
+ self.num_inputs = num_inputs
134
+ self.num_fract = num_fract
135
+ self.n_iter = n_iter
136
+ self.lambda_ = lambda_
137
+ self.verbose = verbose
138
+
139
+ # populated during fit
140
+ self._n_ch = None
141
+ self._k = None
142
+ self._order = None
143
+ self._z = None
144
+ self._b = None
145
+ self._a_hist = None
146
+ self._u = None
147
+ self._pre = None
148
+ self.alpha_ = None
149
+ self.coupling_ = None
150
+
151
+ # ---- fractional order estimation ---------------------------------------
152
+ def _fractional_order(self, x):
153
+ num_scales = int(np.floor(np.log2(self._k)))
154
+ log_scales = np.zeros(num_scales)
155
+ scale = np.arange(1, num_scales + 1)
156
+
157
+ wt = HaarWaveletTransform(x)
158
+ wt.normalize()
159
+ _, detail = wt.transform()
160
+ j = int(np.floor(self._k / 2))
161
+ for i in range(num_scales - 1):
162
+ y = detail[i, :j]
163
+ variance = np.var(y, ddof=1)
164
+ if variance <= 0: # guard log2(0)
165
+ variance = 1e-10
166
+ log_scales[i] = np.log2(variance)
167
+ j = int(np.floor(j / 2))
168
+ p = np.polyfit(scale[: num_scales - 1], log_scales[: num_scales - 1], 1)
169
+ return p[0] / 2
170
+
171
+ def _estimate_order(self, x):
172
+ self._order = np.array([self._fractional_order(x[i, :]) for i in range(self._n_ch)])
173
+
174
+ def _update_z(self, x):
175
+ self._z = np.empty((self._n_ch, self._k))
176
+ j = np.arange(0, self.num_fract + 1)
177
+ for i in range(self._n_ch):
178
+ prefactor = gamma(-self._order[i] + j) / gamma(-self._order[i]) / gamma(j + 1)
179
+ y = np.convolve(x[i, :], prefactor)
180
+ self._z[i, :] = y[: self._k]
181
+
182
+ # ---- coupling matrix ----------------------------------------------------
183
+ def _heuristic_b(self, a):
184
+ b = np.zeros((self._n_ch, self._n_ch))
185
+ b[np.abs(a) > 0.01] = a[np.abs(a) > 0.01]
186
+ _, r = LA.qr(b)
187
+ col_ind = np.where(np.abs(np.diag(r)) > 1e-7)
188
+ if np.size(col_ind[0]) < self.num_inputs:
189
+ self._b = np.vstack(
190
+ (np.eye(self.num_inputs), np.zeros((self._n_ch - self.num_inputs, self.num_inputs)))
191
+ )
192
+ else:
193
+ col_ind = col_ind[0][: self.num_inputs]
194
+ self._b = b[:, col_ind]
195
+ if np.linalg.matrix_rank(b) < self.num_inputs:
196
+ # fall back to a well-conditioned selector instead of failing
197
+ self._b = np.vstack(
198
+ (np.eye(self.num_inputs), np.zeros((self._n_ch - self.num_inputs, self.num_inputs)))
199
+ )
200
+
201
+ def _least_squares(self, y, x):
202
+ x_use = np.vstack((np.zeros((1, self._n_ch)), x[:-1, :]))
203
+ reg = 1e-8 * np.eye(x_use.shape[1]) # avoid singular normal equations
204
+ a = np.matmul(np.matmul(y.T, x_use), LA.inv(np.matmul(x_use.T, x_use) + reg))
205
+ mse = LA.norm(y - np.matmul(x_use, a.T), axis=0) ** 2 / self._k
206
+ return a, np.mean(mse)
207
+
208
+ @staticmethod
209
+ def _factor(a, rho):
210
+ m, n = np.shape(a)
211
+ if m >= n:
212
+ lower = LA.cholesky(np.matmul(a.T, a) + rho * np.eye(n), lower=True)
213
+ else:
214
+ lower = LA.cholesky(np.eye(m) + 1 / rho * np.matmul(a, a.T), lower=True)
215
+ return lower, lower.T
216
+
217
+ @staticmethod
218
+ def _shrinkage(x, kappa):
219
+ return np.maximum(0, x - kappa) - np.maximum(0, -x - kappa)
220
+
221
+ class _PreComputed:
222
+ def __init__(self, b, rho):
223
+ self.l, self.u = FODN._factor(b, rho)
224
+ self.l_inv = LA.inv(self.l)
225
+ self.u_inv = LA.inv(self.u)
226
+
227
+ def _lasso(self, b_vec, lambda_):
228
+ a = self._b
229
+ b_vec = np.reshape(b_vec, (np.size(b_vec), 1))
230
+ max_iter, abstol, reltol = 100, 1e-4, 1e-2
231
+ m, n = np.shape(a)
232
+ atb = np.matmul(a.T, b_vec)
233
+ rho = 1 / lambda_
234
+ alpha = 1.0
235
+
236
+ z = np.zeros((n, 1))
237
+ u = np.zeros((n, 1))
238
+ l_inv, u_inv = self._pre.l_inv, self._pre.u_inv
239
+
240
+ for _ in range(max_iter):
241
+ q = atb + rho * (z - u)
242
+ if m >= n:
243
+ x = np.matmul(u_inv, np.matmul(l_inv, q))
244
+ else:
245
+ x = q / rho - np.matmul(
246
+ a.T, np.matmul(LA.inv(u_inv), np.matmul(LA.inv(l_inv), np.matmul(a, q)))
247
+ ) / rho**2
248
+
249
+ z_old = np.array(z)
250
+ x_hat = alpha * x + (1 - alpha) * z_old
251
+ z = self._shrinkage(x_hat + u, lambda_ / rho)
252
+ u += x_hat - z
253
+
254
+ r_norm = LA.norm(x - z)
255
+ s_norm = LA.norm(-rho * (z - z_old))
256
+ eps_pri = np.sqrt(n) * abstol + reltol * np.max((LA.norm(x), LA.norm(-z)))
257
+ eps_dual = np.sqrt(n) * abstol + reltol * LA.norm(rho * u)
258
+ if r_norm < eps_pri and s_norm < eps_dual:
259
+ break
260
+ return np.squeeze(z)
261
+
262
+ # ---- public API ---------------------------------------------------------
263
+ def fit(self, x):
264
+ """Fit the FODN model to a ``(n_channels, n_timepoints)`` array.
265
+
266
+ Returns ``self``; populates :attr:`alpha_` and :attr:`coupling_`.
267
+ """
268
+ import time
269
+
270
+ x = np.asarray(x, dtype=float)
271
+ if x.ndim != 2:
272
+ raise ValueError("x must be 2-D (n_channels, n_timepoints)")
273
+ self._n_ch, self._k = x.shape
274
+ if self._n_ch < 2:
275
+ raise ValueError("FODN needs more than one channel")
276
+ if self._k < self._n_ch:
277
+ raise ValueError("number of timepoints must be >= number of channels")
278
+ if self.num_inputs is None:
279
+ self.num_inputs = int(np.floor(self._n_ch / 2))
280
+ self.num_inputs = max(1, int(self.num_inputs))
281
+
282
+ self._a_hist = np.empty((self.n_iter + 1, self._n_ch, self._n_ch))
283
+ self._u = np.zeros((self.num_inputs, self._k))
284
+
285
+ self._estimate_order(x)
286
+ self._update_z(x)
287
+ self._a_hist[0], mse = self._least_squares(self._z.T, x.T)
288
+ self._heuristic_b(self._a_hist[0])
289
+ self._pre = self._PreComputed(self._b, 1 / self.lambda_)
290
+
291
+ t0 = time.time()
292
+ if self.verbose:
293
+ print(f"beginning mse = {mse:.6f}")
294
+ for it in range(self.n_iter):
295
+ for k in range(1, self._k):
296
+ residual = self._z[:, k] - np.matmul(self._a_hist[it], x[:, k - 1])
297
+ self._u[:, k] = self._lasso(residual, self.lambda_)
298
+ self._a_hist[it + 1], mse = self._least_squares(
299
+ (self._z - np.matmul(self._b, self._u)).T, x.T
300
+ )
301
+ if self.verbose:
302
+ print(f"iter {it}: mse = {mse:.6f}")
303
+ if self.verbose:
304
+ print(f"time taken = {time.time() - t0:.3f}s")
305
+
306
+ self.alpha_ = self._order
307
+ self.coupling_ = self._a_hist[-1]
308
+ return self
309
+
310
+ def result(self) -> FODNResult:
311
+ """Assemble a :class:`FODNResult` (eigen-decomposition + summaries)."""
312
+ if self.coupling_ is None:
313
+ raise RuntimeError("call fit() before result()")
314
+ a = self.coupling_
315
+ w, v = np.linalg.eig(a)
316
+ leading = float(np.max(np.abs(w)))
317
+ dom = np.abs(v[:, int(np.argmax(w.real))])
318
+ sparseness = float(np.count_nonzero(np.abs(a) > 0.01) / a.size)
319
+ return FODNResult(
320
+ alpha=self.alpha_,
321
+ coupling=a,
322
+ eigenvalues=w,
323
+ leading_eig=leading,
324
+ dominant_eigvec=dom,
325
+ sparseness=sparseness,
326
+ )
327
+
328
+
329
+ def fit_fodn(x, *, num_inputs=None, num_fract=50, n_iter=10, lambda_=0.5, verbose=False) -> FODNResult:
330
+ """Functional wrapper: fit a :class:`FODN` and return its :class:`FODNResult`.
331
+
332
+ Parameters
333
+ ----------
334
+ x : array-like, shape (n_channels, n_timepoints)
335
+ Multi-channel signal segment.
336
+ num_inputs, num_fract, n_iter, lambda_, verbose
337
+ Passed through to :class:`FODN`.
338
+ """
339
+ model = FODN(
340
+ num_inputs=num_inputs,
341
+ num_fract=num_fract,
342
+ n_iter=n_iter,
343
+ lambda_=lambda_,
344
+ verbose=verbose,
345
+ ).fit(x)
346
+ return model.result()