amica-python 0.1.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.
amica/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from . import datasets, utils
2
+ from ._sklearn_interface import AMICA
3
+ from .core import fit_amica
4
+
5
+ __all__ = ['fit_amica', 'AMICA', 'datasets', 'utils']
amica/_batching.py ADDED
@@ -0,0 +1,194 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from typing import Union
5
+ from warnings import warn
6
+
7
+ import numpy as np
8
+ import psutil
9
+ import torch
10
+
11
+ ArrayLike2D = Union[np.ndarray, "np.typing.NDArray[np.floating]"]
12
+
13
+
14
+ class BatchLoader:
15
+ """Iterate over an array in fixed-size batches of data along a chosen axis.
16
+
17
+ We hand rolled this instead of using DataLoader because 1) we want to yield
18
+ slices of input array (i.e. a view), and 2) return the indices as
19
+ a slice object. DataLoader would internally convert the slice into a tensor
20
+ of indices.
21
+
22
+ Example (AMICA shape):
23
+ X: (n_samples, n_features)
24
+ it = BatchLoader(X, axis=0, batch_size=4096)
25
+ for X_blk, sl in it:
26
+ # X_blk is X[sl, :] where sl is slice(start, end)
27
+ ...
28
+ """
29
+
30
+ def __init__(self, X: ArrayLike2D, axis: int, batch_size: int | None = None):
31
+ # Validate inputs
32
+ cls_name = self.__class__.__name__
33
+ if not isinstance(X, torch.Tensor):
34
+ raise TypeError(f"{cls_name} expects a torch.Tensor") # pragma: no cover
35
+ if X.ndim < 1:
36
+ raise ValueError(
37
+ f"{cls_name} expects an array with at least 1 dimension"
38
+ ) # pragma: no cover
39
+ self.X = X
40
+ self.axis = axis
41
+
42
+ if self.axis < 0:
43
+ self.axis += X.ndim
44
+ if not (0 <= self.axis < X.ndim):
45
+ raise ValueError(
46
+ f"axis {self.axis} out of bounds for array with ndim={X.ndim}"
47
+ )
48
+
49
+ # Determine batching parameters
50
+ n = X.shape[self.axis]
51
+ start = 0
52
+ stop = n
53
+ if batch_size is None:
54
+ # Treat as single chunk spanning [start:stop]
55
+ batch_size = stop
56
+
57
+ # Validate parameters
58
+ assert (0 <= start <= n), f"start {start} out of range [0, {n}]"
59
+ assert (0 <= stop <= n), f"stop {stop} out of range [0, {n}]"
60
+ assert start <= stop, f"start {start} must be <= stop {stop}"
61
+ if batch_size < 0:
62
+ raise ValueError(f"batch_size must be positive. Got {batch_size}.")
63
+ if batch_size > X.shape[self.axis]:
64
+ raise ValueError(
65
+ f"batch_size {batch_size} exceeds data size {X.shape[self.axis]} "
66
+ f"along axis {self.axis}."
67
+ )
68
+
69
+ # Store parameters
70
+ self.start = start
71
+ self.stop = stop
72
+ self.batch_size = int(batch_size)
73
+
74
+ def __getitem__(self, idx: int) -> torch.Tensor:
75
+ start = self.start + idx * self.batch_size
76
+ stop = min(start + self.batch_size, self.stop)
77
+
78
+
79
+ idx = [slice(None)] * self.X.ndim
80
+ idx[self.axis] = slice(start, stop)
81
+ return self.X[tuple(idx)]
82
+
83
+ def __iter__(self) -> Iterator[tuple[np.ndarray, slice]]:
84
+ axis = self.axis
85
+ start = self.start
86
+ stop = self.stop
87
+ step = self.batch_size
88
+
89
+ idx = [slice(None)] * self.X.ndim
90
+ assert -((stop - start) // -step) == len(self) # sanity check
91
+ for s in range(start, stop, step):
92
+ e = min(s + step, stop)
93
+ batch_slice = slice(s, e)
94
+ idx[axis] = batch_slice
95
+ yield self.X[tuple(idx)], batch_slice
96
+
97
+ def __len__(self) -> int:
98
+ return (self.X.shape[self.axis] + self.batch_size - 1) // self.batch_size
99
+
100
+ def __repr__(self) -> str:
101
+ return (
102
+ f"{self.__class__.__name__}(Data shape: {self.X.shape}, "
103
+ f"Batched axis: {self.axis}, batch_size: {self.batch_size}, "
104
+ f"n_batches: {len(self)})"
105
+ )
106
+
107
+ def choose_batch_size(
108
+ *,
109
+ N: int,
110
+ n_comps: int,
111
+ n_mix: int,
112
+ n_models: int = 1,
113
+ dtype: np.dtype = np.float64,
114
+ memory_fraction: float = 0.25, # use up to 25% of available memory
115
+ memory_cap: float = 1.5 * 1024**3, # 1.5 GB absolute ceiling
116
+ ) -> int:
117
+ """
118
+ Choose batch size for processing data in chunks.
119
+
120
+ Parameters
121
+ ----------
122
+ N : int
123
+ Total number of samples.
124
+ n_comps : int
125
+ Number of components to be learned in the model, e.g. size of the n_components
126
+ dimension of the data.
127
+ n_mix : int
128
+ Number of mixture components per source/component to be learned in the model.
129
+ dtype : np.dtype, optional
130
+ Data type of the input data, by default np.float64.
131
+ memory_cap : float, optional
132
+ Maximum memory (in bytes) to be used for processing, by default
133
+ ``1.5 * 1024**3`` (1.5 GB).
134
+
135
+ Notes
136
+ -----
137
+ The batch size is primarily determined by the estimated size of hot buffers (e.g.
138
+ y, z, fp, ufp), which scale with the size of n_samples:
139
+ - One array of shape (N,):
140
+ - loglik
141
+ - Two arrays of shape (N, n_models):
142
+ - modloglik
143
+ - v (model responsibilities)
144
+ - Two arrays of shape (N, n_comps)
145
+ - b
146
+ - g
147
+ - Five arrays of shape (N, n_comps, n_mix): u, y, z, fp, ufp
148
+ - u (mixture responsibilities)
149
+ - y
150
+ - z
151
+ - fp
152
+ - ufp
153
+ """
154
+ dtype_size = np.dtype(dtype).itemsize
155
+ # per-sample cost across pre-allocated buffers
156
+ bytes_per_sample = (
157
+ 1 # loglik
158
+ + 2 * n_models # modloglik, v
159
+ + 2 * n_comps # b, g
160
+ + 5 * n_comps * n_mix # fp, u, ufp, y, z,
161
+ ) * dtype_size
162
+ # Plus small headroom for intermediates
163
+ bytes_per_sample = int(bytes_per_sample * 1.2)
164
+
165
+ # Pick memory budget
166
+ try:
167
+ hard_cap = 4 * 1024**3 # 4 GiB (avoid runaway memory use)
168
+ avail_mem = psutil.virtual_memory().available
169
+ mem_cap = min(avail_mem * memory_fraction, hard_cap)
170
+ except Exception:
171
+ mem_cap = memory_cap # fallback to user-specified cap
172
+
173
+ max_batch_size = mem_cap // bytes_per_sample
174
+
175
+ # Ensure at least 1 sample. This should only trigger if n_comps and n_mix are huge.
176
+ if max_batch_size < 1:
177
+ raise MemoryError(
178
+ f"Cannot fit even 1 sample within memory cap of "
179
+ f"{mem_cap / 1024**3:.2f} GiB. "
180
+ f"Per-sample memory cost is {bytes_per_sample / 1024**3:.2f} GB."
181
+ )
182
+ batch_size = int(min(N, max_batch_size))
183
+
184
+ # Heuristic floor, we don't want absurdly small chunks or chunks that are too
185
+ # small relative to the model complexity (n_comps)
186
+ # This heuristic works well for typical ICA regimes, where n_comps is < 256
187
+ min_batch_size = max(8192, n_comps * 32) # at least 32 samples per component
188
+ min_batch_size = min(min_batch_size, N) # Cannot exceed N
189
+ if batch_size < min_batch_size:
190
+ warn(
191
+ f"Warning: To stay within the memory cap, batch size is {batch_size} "
192
+ f"samples, which is below the recommended minimum of {min_batch_size}."
193
+ )
194
+ return batch_size
amica/_newton.py ADDED
@@ -0,0 +1,77 @@
1
+ """Quasi-Newton related computations for estimation of gradient curvature terms."""
2
+ import torch
3
+
4
+
5
+ def compute_newton_terms(*, config, accumulators, mu):
6
+ """Compute second-order curvature or scaling terms for Hessian approximation.
7
+
8
+ Parameters
9
+ ----------
10
+ config : amica.state.Config
11
+ Configuration object containing model parameters. Specifically, it should have
12
+ ``n_components`` and ``dtype`` attributes.
13
+ accumulators : amica.state.Accumulators
14
+ Accumulators instance containing intermediate tensors needed for Newton term
15
+ calculations. It should have tensors stored in the following attributes:
16
+
17
+ - ``dbaralpha_numer`` (n_components, n_mixtures)
18
+ - ``dbaralpha_denom`` (n_components, n_mixtures)
19
+ - ``dsigma2_numer`` (n_components,)
20
+ - ``dsigma2_denom`` (n_components,)
21
+ - ``dkappa_numer`` (n_components, n_mixtures)
22
+ - ``dkappa_denom`` (n_components, n_mixtures)
23
+ - ``dlambda_numer`` (n_components, n_mixtures)
24
+ - ``dlambda_denom`` (n_components, n_mixtures)
25
+
26
+ mu : torch.Tensor
27
+ Tensor of shape (n_components, n_mixtures) representing the mean estimates for
28
+ each component and mixture. This is typically stored in the state object
29
+ (``state.mu``).
30
+
31
+ Returns
32
+ -------
33
+ dict
34
+ A dictionary containing the computed Newton terms:
35
+ - ``baralpha``: Tensor of shape (n_components, n_mixtures)
36
+ - ``sigma2``: Tensor of shape (n_components,)
37
+ - ``kappa``: Tensor of shape (n_components,)
38
+ - ``lambda_``: Tensor of shape (n_components,)
39
+ """
40
+ #--------------------------FORTRAN CODE--------------------------------------------
41
+ # baralpha = dbaralpha_numer / dbaralpha_denom
42
+ # sigma2 = dsigma2_numer / dsigma2_denom
43
+ # kappa = dble(0.0)
44
+ # lambda = dble(0.0)
45
+ #-----------------------------------------------------------------------------------
46
+ # weighting factor: how much mixture j contributes to source i
47
+ baralpha = accumulators.newton.dbaralpha_numer / accumulators.newton.dbaralpha_denom
48
+ # variance estimate for source i
49
+ sigma2 = accumulators.newton.dsigma2_numer / accumulators.newton.dsigma2_denom
50
+ # curvature terms
51
+ kappa = torch.zeros(
52
+ (config.n_components,), dtype=config.dtype, device=config.device
53
+ )
54
+ lambda_ = torch.zeros(
55
+ (config.n_components,), dtype=config.dtype, device=config.device
56
+ )
57
+
58
+ # Calculate dkap for all mixtures
59
+ dkap = accumulators.newton.dkappa_numer / accumulators.newton.dkappa_denom
60
+ kappa += torch.sum(baralpha * dkap, dim=1)
61
+
62
+ #--------------------------FORTRAN CODE-------------------------
63
+ # lambda(i,h) = lambda(i,h) + ...
64
+ # baralpha(j,i,h) * ( dlambda_numer(j,i,h)/dlambda_denom(j,i,h) + ...
65
+ #---------------------------------------------------------------
66
+ lambda_inner_term = (
67
+ (accumulators.newton.dlambda_numer / accumulators.newton.dlambda_denom)
68
+ + (dkap * mu**2)
69
+ )
70
+ lambda_ += torch.sum(baralpha * lambda_inner_term, dim=1)
71
+
72
+ return {
73
+ "baralpha": baralpha, # (n_components, n_mixtures)
74
+ "sigma2": sigma2, # (n_components,)
75
+ "kappa": kappa, # (n_components,)
76
+ "lambda_": lambda_, # (n_components,)
77
+ }
@@ -0,0 +1,387 @@
1
+ """Scikit-learn class wrapper for AMICA."""
2
+ import warnings
3
+
4
+ import numpy as np
5
+ from sklearn.base import BaseEstimator, TransformerMixin
6
+ from sklearn.utils.validation import check_is_fitted, validate_data
7
+
8
+ from .core import fit_amica
9
+
10
+ CHECK_ARRAY_KWARGS = {
11
+ "dtype": [np.float64, np.float32],
12
+ "ensure_min_samples": 2, # safeguard accidental (n_features, n_samples) inputs
13
+ "ensure_min_features": 2,
14
+ }
15
+
16
+ class AMICA(TransformerMixin, BaseEstimator):
17
+ """AMICA: adaptive Mixture algorithm for Independent Component Analysis.
18
+
19
+ Implements the AMICA algorithm as described in :footcite:t:`palmer2012` and
20
+ :footcite:t:`palmer2008`, and originally implemented in :footcite:t:`amica`.
21
+
22
+ Parameters
23
+ ----------
24
+ n_components : int, default=None
25
+ Number of components to use. If ``None``, then ``n_components == n_features``.
26
+
27
+ .. note::
28
+ If the data are rank deficient, then the effective number of components
29
+ will be lower than ``n_components``, as the number of components will be
30
+ set to the data rank.
31
+
32
+ n_mixtures : int, default=3
33
+ Number of mixtures components to use in the Gaussian Mixture Model (GMM) for
34
+ each component's source density. default is ``3``.
35
+ n_models : int, default=1
36
+ Number of ICA decompositions to run. Only ``1`` is supported currently.
37
+ batch_size : int, optional
38
+ Batch size for processing data in chunks along the samples axis. If ``None``,
39
+ batching is chosen automatically to keep peak memory under ~1.5 GB, and
40
+ warns if the batch size is below ~8k samples. If the input data is small enough
41
+ to process in one shot, no batching is used. If you want to enforce no batching
42
+ even when the data is very large, set ``batch_size`` to ``X.shape[0]``
43
+ to process all samples at once, but note that this may lead to
44
+ high memory usage for large datasets.
45
+ device : str, default='cpu'
46
+ Device to run the computations on. Can be either 'cpu' or 'cuda' for GPU
47
+ acceleration. Note that using 'cuda' requires a compatible NVIDIA GPU and
48
+ the appropriate CUDA drivers installed.
49
+ mean_center : bool, default=True
50
+ If ``True``, the data is mean-centered before whitening and fitting. This is
51
+ equivalent to ``do_mean=1`` in the Fortran AMICA program.
52
+ whiten : str {"zca", "pca", "variance"}, default="zca"
53
+ whitening strategy.
54
+
55
+ - ``"zca"``: Data is whitened with the inverse of the symmetric square
56
+ root of the covariance matrix. if ``n_components`` < ``n_features``, then
57
+ approximate sphering is done by multiplying by the eigenvectors of a reduced
58
+ dimensional subset of the principle component (eigenvector) subspace. This
59
+ is equivalent to ``do_sphere=1`` + ``do_approx_sphere=1`` in the
60
+ Fortran AMICA program. In EEBLAB's AMICA GUI, this is called
61
+ "Symmetric sphering".
62
+ - ``"pca"``: Data is whitened using only eigenvector projection and scaling to
63
+ do sphering (not symmetric or approximately symmetric). This is equivalent
64
+ to ``do_sphere=1`` + ``do_approx_sphere=0`` in the Fortran AMICA program.
65
+ In EEBLAB's AMICA GUI, this is called "Principle Components (Eigenvectors)".
66
+ - ``"variance"``: Diagonal Normalization. Each feature is scaled by the variance
67
+ across features. This is equivalent to ``do_sphere=0`` in the Fortran AMICA
68
+ program. In EEBLAB's AMICA GUI, this is called "No sphering transformation".
69
+
70
+ max_iter : int, default=500
71
+ Maximum number of iterations during fit.
72
+ tol : float, default=1e-7
73
+ Tolerance for stopping criteria. A positive scalar giving the tolerance at which
74
+ the un-mixing matrix is considered to have converged. The default is ``1e-7``.
75
+ Fortran AMICA program contained tunable tolerance parameters for two
76
+ different stopping criteria ``min_dll`` and ``min_grad_norm``. We only expose
77
+ one parameter, which is applied to both criteria.
78
+ lrate : float, default=0.05
79
+ Initial learning rate for the natural gradient. The Fortran AMICA program
80
+ exposed 2 tunable learning rate parameters, ``lrate`` and ``rholrate`` (initial
81
+ lrate for shape parameters) but we expose only one for simplicity, which is
82
+ applied to both.
83
+ pdftype : int, default=0
84
+ Type of source density model to use. Currently only ``0`` is supported,
85
+ which corresponds to the Gaussian Mixture Model (GMM) density.
86
+ do_newton : bool, default=True
87
+ If ``True``, the optimization method will switch from Stochastic Gradient
88
+ Descent (SGD) to newton updates after ``newt_start`` iterations. If ``False``,
89
+ only SGD updates are used.
90
+ newt_start : int, default=50
91
+ Number of iterations before switching to Newton updates if ``do_newton`` is
92
+ ``True``.
93
+ newtrate : float, default=1.0
94
+ lrate for newton iterations.
95
+ w_init : ndarray of shape (``n_components``, ``n_components``), default=``None``
96
+ Initial un-mixing array. If ``None``, then an array of values drawn from a
97
+ normal distribution is used.
98
+ sbeta_init : ndarray of shape (``n_components``, ``n_mixtures``), default=None
99
+ Initial scale parameters for the mixture components. If ``None``, then an array
100
+ of values drawn from a uniform distribution is used.
101
+ mu_init : ndarray of shape (``n_components``, ``n_mixtures``), default=None
102
+ Initial location parameters for the mixture components. If ``None``, then an
103
+ array of values drawn from a normal distribution is used.
104
+ random_state : int or None, default=None
105
+ Used to initialize ``w_init`` when not specified, with a
106
+ normal distribution. Pass an int for reproducible results
107
+ across multiple function calls. Note that unlike scikit-learn's FastICA, you
108
+ **cannot** pass a :class:`~numpy.random.BitGenerator` instance via
109
+ :func:`~numpy.random.default_rng`.
110
+ verbose : int, default=1
111
+ Output mode during optimization:
112
+
113
+ - ``0``: silent
114
+ - ``1``: progress bar
115
+ - ``2``: per-iteration FORTRAN-style
116
+
117
+ Attributes
118
+ ----------
119
+ components_ : ndarray of shape (``n_components``, ``n_features``)
120
+ The linear operator to apply to the data to get the independent
121
+ sources. This is equal to ``np.matmul(unmixing_matrix, self.whitening_)`` when
122
+ ``whiten`` is ``"zca"`` or ``"pca"``.
123
+ mixing_ : ndarray of shape (``n_features``, ``n_components``)
124
+ The pseudo-inverse of ``components_``. It is the linear operator
125
+ that maps independent sources to the data (in feature space).
126
+ mean_ : ndarray of shape(``n_features``,)
127
+ The mean over features. Only set if `self.whiten` is ``True``.
128
+ whitening_ : ndarray of shape (``n_components``, ``n_features``)
129
+ Only set if whiten is ``True``. This is the pre-whitening matrix
130
+ that projects data onto the first ``n_components`` principal components.
131
+ n_features_in_ : int
132
+ Number of features seen during :meth:`~AMICA.fit`.
133
+ n_iter_ : int
134
+ Number of iterations taken to converge during fit.
135
+ ll_ : ndarray of shape (``n_iter_``,)
136
+ The per-iteration Log-likelihood values of model fit.
137
+ mu_ : ndarray of shape (``n_components``, ``n_mixtures``)
138
+ Learned location parameters for each source-mixture component.
139
+ sbeta_ : ndarray of shape (``n_components``, ``n_mixtures``)
140
+ Learned scale parameters for each source-mixture component.
141
+ rho_ : ndarray of shape (``n_components``, ``n_mixtures``)
142
+ Learned shape parameters for each source-mixture component.
143
+ alpha_ : ndarray of shape (``n_components``, ``n_mixtures``)
144
+ Learned per-source mixture weights.
145
+ c_ : ndarray of shape (``n_components``,)
146
+ Learned source bias/offset terms.
147
+ locations_ : ndarray of shape (``n_components``, ``n_mixtures``)
148
+ Alias for ``mu_``.
149
+ scales_ : ndarray of shape (``n_components``, ``n_mixtures``)
150
+ Alias for ``sbeta_``.
151
+ shapes_ : ndarray of shape (``n_components``, ``n_mixtures``)
152
+ Alias for ``rho_``.
153
+ mixture_weights_ : ndarray of shape (``n_components``, ``n_mixtures``)
154
+ Alias for ``alpha_``.
155
+
156
+ References
157
+ ----------
158
+ .. footbibliography::
159
+
160
+
161
+ Examples
162
+ --------
163
+ >>> from sklearn.datasets import load_digits
164
+ >>> from amica import AMICA
165
+ >>> X, _ = load_digits(return_X_y=True)
166
+ >>> transformer = AMICA(n_components=7, random_state=0)
167
+ >>> X_transformed = transformer.fit_transform(X)
168
+ >>> X_transformed.shape
169
+ (1797, 7)
170
+ """
171
+
172
+ def __init__(
173
+ self,
174
+ n_components=None,
175
+ *,
176
+ n_mixtures=3,
177
+ batch_size=None,
178
+ device='cpu',
179
+ n_models=1,
180
+ mean_center=True,
181
+ whiten="zca",
182
+ max_iter=500,
183
+ tol=1e-7,
184
+ lrate=0.05,
185
+ pdftype=0,
186
+ do_newton=True,
187
+ newt_start=50,
188
+ newtrate=1.0,
189
+ w_init=None,
190
+ sbeta_init=None,
191
+ mu_init=None,
192
+ random_state=None,
193
+ verbose=1,
194
+ ):
195
+ super().__init__()
196
+ self.n_components = n_components
197
+ self.n_mixtures = n_mixtures
198
+ self.n_models = n_models
199
+ self.mean_center = mean_center
200
+ self.whiten = whiten
201
+ self._whiten = whiten # for compatibility
202
+ self.max_iter = max_iter
203
+ self.tol = tol
204
+ self.lrate = lrate
205
+ self.pdftype = pdftype
206
+ self.do_newton = do_newton
207
+ self.newt_start = newt_start
208
+ self.newtrate = newtrate
209
+ self.w_init = w_init
210
+ self.sbeta_init = sbeta_init
211
+ self.mu_init = mu_init
212
+ self.random_state = random_state
213
+ self.verbose = verbose
214
+ self.batch_size = batch_size
215
+ self.device = device
216
+
217
+ def fit(self, X, y=None, verbose=None):
218
+ """Fit the AMICA model to the data X.
219
+
220
+ Parameters
221
+ ----------
222
+ X : array-like of shape (n_samples, ``n_features``)
223
+ Training data, where ``n_samples`` is the number of samples
224
+ and ``n_features`` is the number of features.
225
+ y : Ignored
226
+ Not used, present here for API consistency by convention.
227
+ verbose : int or None, default=None
228
+ Per-call override for estimator verbosity.
229
+ If ``None``, uses the estimator's ``self.verbose`` setting.
230
+
231
+ Returns
232
+ -------
233
+ self : object
234
+ Fitted estimator.
235
+ """
236
+ # Validate input data
237
+ X = validate_data(
238
+ self, X=X,
239
+ reset=True,
240
+ **CHECK_ARRAY_KWARGS
241
+ )
242
+ # Fit the model
243
+ fit_dict = fit_amica(
244
+ X,
245
+ n_components=self.n_components,
246
+ n_mixtures=self.n_mixtures,
247
+ batch_size=self.batch_size,
248
+ device=self.device,
249
+ mean_center=self.mean_center,
250
+ whiten=self.whiten,
251
+ max_iter=self.max_iter,
252
+ tol=self.tol,
253
+ lrate=self.lrate,
254
+ pdftype=self.pdftype,
255
+ do_newton=self.do_newton,
256
+ newt_start=self.newt_start,
257
+ newtrate=self.newtrate,
258
+ w_init=self.w_init,
259
+ sbeta_init=self.sbeta_init,
260
+ mu_init=self.mu_init,
261
+ random_state=self.random_state,
262
+ verbose=self.verbose if verbose is None else verbose,
263
+ )
264
+
265
+ # Set attributes
266
+ if self.mean_center:
267
+ self.mean_ = fit_dict['mean']
268
+ self.n_features_in_ = X.shape[1]
269
+ ll_full = np.asarray(fit_dict["LL"])
270
+ self.n_iter_ = int(np.count_nonzero(ll_full))
271
+ self.ll_ = ll_full[:self.n_iter_].copy()
272
+ self.whitening_ = fit_dict["S"][:self.n_components, :]
273
+ self._unmixing = fit_dict['W']
274
+ self.components_ = self._unmixing @ self.whitening_
275
+ self.mixing_ = np.linalg.pinv(self.whitening_) @ fit_dict["A"]
276
+ self.mu_ = np.asarray(fit_dict["mu"])
277
+ self.sbeta_ = np.asarray(fit_dict["sbeta"])
278
+ self.rho_ = np.asarray(fit_dict["rho"])
279
+ self.alpha_ = np.asarray(fit_dict["alpha"])
280
+ self.c_ = np.asarray(fit_dict["c"])
281
+ self.locations_ = self.mu_
282
+ self.scales_ = self.sbeta_
283
+ self.shapes_ = self.rho_
284
+ self.mixture_weights_ = self.alpha_
285
+ return self
286
+
287
+ def transform(self, X, copy=True):
288
+ """Recover the sources from X (apply the unmixing matrix).
289
+
290
+ Parameters
291
+ ----------
292
+ X : array-like of shape (n_samples, ``n_features``)
293
+ Data to transform, where ``n_samples`` is the number of samples
294
+ and ``n_features`` is the number of features.
295
+ copy : bool, default=True
296
+ If False, data passed to fit can be overwritten. Defaults to True.
297
+
298
+ Returns
299
+ -------
300
+ X_new : ndarray of shape (n_samples, ``n_components``)
301
+ Estimated sources obtained by transforming the data with the estimated
302
+ unmixing matrix.
303
+ """
304
+ check_is_fitted(self)
305
+ X = validate_data(
306
+ self, X=X, reset=False, copy=copy, dtype=[np.float64, np.float32]
307
+ )
308
+
309
+ Xc = X - self.mean_ if hasattr(self, "mean_") and self.mean_ is not None else X
310
+ return Xc @ self.components_.T
311
+
312
+ def fit_transform(self, X, y=None):
313
+ """Fit the model to the data and transform it.
314
+
315
+ Parameters
316
+ ----------
317
+ X : array-like of shape (n_samples, ``n_features``)
318
+ Training data, where n_samples is the number of samples
319
+ and ``n_features`` is the number of features.
320
+ y : Ignored
321
+ Not used, present here for API consistency by convention.
322
+
323
+ Returns
324
+ -------
325
+ X_new : ndarray of shape (n_samples, ``n_components``)
326
+ Estimated sources obtained by transforming the data with the estimated
327
+ unmixing matrix.
328
+ """
329
+ # Here you would implement the AMICA fitting logic
330
+ X = validate_data(self, X=X, y=None, reset=False)
331
+ n_components = self.n_components
332
+ if self.whiten == "variance" and n_components is not None:
333
+ n_components = None
334
+ warnings.warn("Ignoring n_components with whiten='variance'.")
335
+ # For now, we just call the parent class's method
336
+ self.fit(X)
337
+ return self.transform(X)
338
+
339
+ def inverse_transform(self, X):
340
+ """Reconstruct data from its independent components.
341
+
342
+ Parameters
343
+ ----------
344
+ X : array-like of shape (``n_samples``, ``n_components``)
345
+ Independent components to invert.
346
+
347
+ Returns
348
+ -------
349
+ X_reconstructed : ndarray of shape (n_samples, ``n_features``)
350
+ Reconstructed data.
351
+
352
+ Examples
353
+ --------
354
+ >>> from sklearn.datasets import load_digits
355
+ >>> from amica import AMICA
356
+ >>> X, _ = load_digits(return_X_y=True)
357
+ >>> transformer = AMICA(n_components=7, random_state=0)
358
+ >>> X_transformed = transformer.fit_transform(X)
359
+ >>> X_reconstructed = transformer.inverse_transform(X_transformed)
360
+ >>> X_reconstructed.shape
361
+ (1797, 64)
362
+ """
363
+ check_is_fitted(self)
364
+ X = validate_data(
365
+ self, X=X, reset=False, dtype=[np.float64, np.float32]
366
+ )
367
+ X_rec = X @ self.mixing_.T
368
+ if hasattr(self, "mean_") and self.mean_ is not None:
369
+ X_rec += self.mean_
370
+ return X_rec
371
+
372
+ def to_mne(self, info):
373
+ """Convert a fitted AMICA instance to an MNE-Python ICA instance.
374
+
375
+ Parameters
376
+ ----------
377
+ info : instance of mne.Info
378
+ Channel metadata to attach to the ICA object.
379
+
380
+ Returns
381
+ -------
382
+ ica : instance of mne.preprocessing.ICA
383
+ ICA object compatible with MNE-Python tooling.
384
+ """
385
+ from .utils.mne import to_mne
386
+
387
+ return to_mne(self, info)