dfm-python 0.6.12__py3-none-any.whl → 0.6.13__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.
dfm_python/__init__.py CHANGED
@@ -63,7 +63,7 @@ Note: DFMConfig uses frequency dict to specify series (column names -> frequenci
63
63
  For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
64
64
  """
65
65
 
66
- __version__ = "0.5.60"
66
+ __version__ = "0.6.13"
67
67
 
68
68
  # ============================================================================
69
69
  # PUBLIC API DEFINITION
@@ -111,8 +111,10 @@ class DDFMDataset(Dataset):
111
111
 
112
112
  @property
113
113
  def target_indices(self) -> np.ndarray:
114
- """Target series column indices in original data."""
115
- return np.array([self.data_original.columns.get_loc(col) for col in self.target_series])
114
+ """Target series column indices in processed data (self.data, excluding time_idx)."""
115
+ # target_series are column names in the processed data (self.data)
116
+ # self.data contains X + y (or just y), but not time_idx
117
+ return np.array([self.data.columns.get_loc(col) for col in self.target_series])
116
118
 
117
119
  @classmethod
118
120
  def from_dataset(cls, new_data: Union[pd.DataFrame, PolarsDataFrame], dataset: 'DDFMDataset') -> 'DDFMDataset':
@@ -0,0 +1,272 @@
1
+ """Multi-Layer Perceptron (MLP) for common usage across models.
2
+
3
+ This module provides a flexible MLP implementation that can be used
4
+ for encoders, decoders, and other neural network components.
5
+ """
6
+
7
+ from numbers import Number
8
+ from typing import Union, List, Optional
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+
13
+ from ..config.constants import (
14
+ DEFAULT_XAVIER_GAIN,
15
+ DEFAULT_ZERO_VALUE,
16
+ )
17
+ from ..logger import get_logger
18
+ from ..utils.errors import ConfigurationError
19
+
20
+ _logger = get_logger(__name__)
21
+
22
+
23
+ class MLP(nn.Module):
24
+ """Multi-layer perceptron with flexible architecture.
25
+
26
+ Supports various activation functions, custom hidden dimensions,
27
+ and proper weight initialization.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ input_dim: int,
33
+ output_dim: int,
34
+ hidden_dim: Union[int, List[int]] = 200,
35
+ n_layers: int = 3,
36
+ activation: Union[str, List[str]] = 'relu',
37
+ slope: float = 0.1,
38
+ use_bias: bool = True,
39
+ dropout: float = 0.0,
40
+ use_batch_norm: bool = False,
41
+ device: Optional[Union[str, torch.device]] = None,
42
+ seed: Optional[int] = None,
43
+ ):
44
+ """Initialize MLP.
45
+
46
+ Parameters
47
+ ----------
48
+ input_dim : int
49
+ Input dimension
50
+ output_dim : int
51
+ Output dimension
52
+ hidden_dim : Union[int, List[int]]
53
+ Hidden layer dimension(s). If int, all hidden layers use this dimension.
54
+ If list, specifies dimension for each hidden layer.
55
+ n_layers : int
56
+ Total number of layers (including input and output)
57
+ activation : Union[str, List[str]]
58
+ Activation function(s). Options: 'relu', 'lrelu', 'tanh', 'sigmoid', 'none'.
59
+ If str, all layers use this activation. If list, specifies activation per layer.
60
+ slope : float
61
+ Slope for leaky ReLU (negative_slope parameter)
62
+ use_bias : bool
63
+ Whether to use bias in linear layers
64
+ dropout : float
65
+ Dropout probability (0.0 = no dropout)
66
+ use_batch_norm : bool
67
+ Whether to use batch normalization after each hidden layer
68
+ device : Optional[Union[str, torch.device]]
69
+ Device to move model to (None = no move)
70
+ seed : Optional[int]
71
+ Random seed for weight initialization
72
+ """
73
+ super().__init__()
74
+
75
+ self.input_dim = input_dim
76
+ self.output_dim = output_dim
77
+ self.n_layers = n_layers
78
+ self.use_bias = use_bias
79
+ self.dropout = dropout
80
+ self.use_batch_norm = use_batch_norm
81
+
82
+ # Validate and set hidden dimensions
83
+ if isinstance(hidden_dim, Number):
84
+ if n_layers < 2:
85
+ raise ValueError(f"n_layers must be >= 2 when hidden_dim is int, got {n_layers}")
86
+ self.hidden_dim = [int(hidden_dim)] * (n_layers - 1)
87
+ elif isinstance(hidden_dim, list):
88
+ if len(hidden_dim) != n_layers - 1:
89
+ raise ValueError(
90
+ f"hidden_dim list length ({len(hidden_dim)}) must equal n_layers - 1 ({n_layers - 1})"
91
+ )
92
+ self.hidden_dim = [int(d) for d in hidden_dim]
93
+ else:
94
+ raise ValueError(f'Wrong argument type for hidden_dim: {type(hidden_dim)}')
95
+
96
+ # Validate and set activations
97
+ if isinstance(activation, str):
98
+ self.activation = [activation] * (n_layers - 1)
99
+ elif isinstance(activation, list):
100
+ if len(activation) != n_layers - 1:
101
+ raise ValueError(
102
+ f"activation list length ({len(activation)}) must equal n_layers - 1 ({n_layers - 1})"
103
+ )
104
+ self.activation = activation
105
+ else:
106
+ raise ValueError(f'Wrong argument type for activation: {type(activation)}')
107
+
108
+ # Set random seed if provided
109
+ if seed is not None:
110
+ torch.manual_seed(seed)
111
+
112
+ # Build activation functions
113
+ self._act_f = []
114
+ for act in self.activation:
115
+ if act == 'lrelu':
116
+ self._act_f.append(lambda x, s=slope: F.leaky_relu(x, negative_slope=s))
117
+ elif act == 'relu':
118
+ self._act_f.append(F.relu)
119
+ elif act == 'tanh':
120
+ self._act_f.append(torch.tanh)
121
+ elif act == 'sigmoid':
122
+ self._act_f.append(torch.sigmoid)
123
+ elif act == 'none' or act is None:
124
+ self._act_f.append(lambda x: x)
125
+ else:
126
+ raise ValueError(f'Incorrect activation: {act}. Options: relu, lrelu, tanh, sigmoid, none')
127
+
128
+ # Build layers
129
+ self.layers = nn.ModuleList()
130
+ self.batch_norms = nn.ModuleList() if use_batch_norm else None
131
+
132
+ if n_layers == 1:
133
+ # Single layer: input -> output
134
+ layer = nn.Linear(input_dim, output_dim, bias=use_bias)
135
+ self._init_linear(layer)
136
+ self.layers.append(layer)
137
+ else:
138
+ # First hidden layer: input -> hidden[0]
139
+ layer = nn.Linear(input_dim, self.hidden_dim[0], bias=use_bias)
140
+ self._init_linear(layer)
141
+ self.layers.append(layer)
142
+
143
+ if use_batch_norm:
144
+ self.batch_norms.append(nn.BatchNorm1d(self.hidden_dim[0]))
145
+
146
+ # Hidden layers: hidden[i-1] -> hidden[i]
147
+ for i in range(1, len(self.hidden_dim)):
148
+ layer = nn.Linear(self.hidden_dim[i - 1], self.hidden_dim[i], bias=use_bias)
149
+ self._init_linear(layer)
150
+ self.layers.append(layer)
151
+
152
+ if use_batch_norm:
153
+ self.batch_norms.append(nn.BatchNorm1d(self.hidden_dim[i]))
154
+
155
+ # Output layer: hidden[-1] -> output
156
+ layer = nn.Linear(self.hidden_dim[-1], output_dim, bias=use_bias)
157
+ self._init_linear(layer, is_output=True)
158
+ self.layers.append(layer)
159
+
160
+ # Dropout layer
161
+ if dropout > 0.0:
162
+ self.dropout_layer = nn.Dropout(dropout)
163
+ else:
164
+ self.dropout_layer = None
165
+
166
+ # Move to device if specified
167
+ if device is not None:
168
+ if isinstance(device, str):
169
+ device = torch.device(device)
170
+ self.to(device)
171
+
172
+ @staticmethod
173
+ def _init_linear(layer: nn.Linear, is_output: bool = False) -> None:
174
+ """Initialize linear layer weights and bias.
175
+
176
+ Parameters
177
+ ----------
178
+ layer : nn.Linear
179
+ Linear layer to initialize
180
+ is_output : bool
181
+ Whether this is the output layer (uses different initialization)
182
+ """
183
+ # Weight initialization
184
+ if is_output:
185
+ # Output layer: Xavier normal with default gain
186
+ nn.init.xavier_normal_(layer.weight, gain=DEFAULT_XAVIER_GAIN)
187
+ else:
188
+ # Hidden layers: Xavier normal
189
+ nn.init.xavier_normal_(layer.weight, gain=DEFAULT_XAVIER_GAIN)
190
+
191
+ # Bias initialization
192
+ if layer.bias is not None:
193
+ nn.init.constant_(layer.bias, DEFAULT_ZERO_VALUE)
194
+
195
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
196
+ """Forward pass through MLP.
197
+
198
+ Parameters
199
+ ----------
200
+ x : torch.Tensor
201
+ Input tensor, shape (batch, input_dim) or (batch, *, input_dim)
202
+
203
+ Returns
204
+ -------
205
+ torch.Tensor
206
+ Output tensor, shape (batch, output_dim) or (batch, *, output_dim)
207
+ """
208
+ # Handle multi-dimensional input (e.g., sequences)
209
+ original_shape = x.shape
210
+ if x.dim() > 2:
211
+ # Flatten all dimensions except last
212
+ x = x.view(-1, x.shape[-1])
213
+ needs_reshape = True
214
+ else:
215
+ needs_reshape = False
216
+
217
+ # Forward through layers
218
+ h = x
219
+ n_hidden = len(self.layers) - 1 # Number of hidden layers
220
+
221
+ for i, layer in enumerate(self.layers):
222
+ h = layer(h)
223
+
224
+ # Apply activation, batch norm, and dropout (except on output layer)
225
+ if i < n_hidden:
226
+ # Activation
227
+ h = self._act_f[i](h)
228
+
229
+ # Batch normalization
230
+ if self.use_batch_norm and self.batch_norms is not None:
231
+ h = self.batch_norms[i](h)
232
+
233
+ # Dropout
234
+ if self.dropout_layer is not None:
235
+ h = self.dropout_layer(h)
236
+
237
+ # Reshape if needed
238
+ if needs_reshape:
239
+ output_shape = list(original_shape[:-1]) + [self.output_dim]
240
+ h = h.view(*output_shape)
241
+
242
+ return h
243
+
244
+ def get_num_parameters(self) -> int:
245
+ """Get total number of trainable parameters.
246
+
247
+ Returns
248
+ -------
249
+ int
250
+ Number of trainable parameters
251
+ """
252
+ return sum(p.numel() for p in self.parameters() if p.requires_grad)
253
+
254
+ def get_layer_info(self) -> dict:
255
+ """Get information about MLP architecture.
256
+
257
+ Returns
258
+ -------
259
+ dict
260
+ Dictionary with architecture information
261
+ """
262
+ return {
263
+ 'input_dim': self.input_dim,
264
+ 'output_dim': self.output_dim,
265
+ 'hidden_dims': self.hidden_dim,
266
+ 'n_layers': self.n_layers,
267
+ 'activations': self.activation,
268
+ 'use_bias': self.use_bias,
269
+ 'dropout': self.dropout,
270
+ 'use_batch_norm': self.use_batch_norm,
271
+ 'num_parameters': self.get_num_parameters(),
272
+ }
@@ -0,0 +1,304 @@
1
+ """Principal Component Analysis (PCA) utilities.
2
+
3
+ This module provides pure PCA functions for factor extraction.
4
+ Similar to MLP, this is a utility layer without class inheritance.
5
+ """
6
+
7
+ import numpy as np
8
+ from typing import Tuple, Optional, Union, TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ import torch
12
+
13
+ from ..logger import get_logger
14
+ from ..numeric.stability import create_scaled_identity
15
+ from ..config.constants import DEFAULT_IDENTITY_SCALE
16
+ from ..config.types import to_numpy
17
+
18
+ _logger = get_logger(__name__)
19
+
20
+
21
+ def compute_principal_components(
22
+ cov_matrix: Union[np.ndarray, "torch.Tensor"],
23
+ n_components: int,
24
+ block_idx: Optional[int] = None
25
+ ) -> Tuple[np.ndarray, np.ndarray]:
26
+ """Compute top principal components via eigendecomposition (NumPy-based).
27
+
28
+ Accepts both NumPy arrays and PyTorch tensors, but performs all
29
+ computations in NumPy for consistency with the refactored codebase.
30
+
31
+ Parameters
32
+ ----------
33
+ cov_matrix : np.ndarray or torch.Tensor
34
+ Covariance matrix (N x N)
35
+ n_components : int
36
+ Number of principal components to extract
37
+ block_idx : int, optional
38
+ Block index for error messages
39
+
40
+ Returns
41
+ -------
42
+ eigenvalues : np.ndarray
43
+ Eigenvalues (n_components,)
44
+ eigenvectors : np.ndarray
45
+ Eigenvectors (N x n_components)
46
+ """
47
+ # Convert to NumPy if needed
48
+ cov_matrix = to_numpy(cov_matrix)
49
+
50
+ if cov_matrix.size == 1:
51
+ eigenvector = np.array([[DEFAULT_IDENTITY_SCALE]])
52
+ eigenvalue = cov_matrix[0, 0] if np.isfinite(cov_matrix[0, 0]) else DEFAULT_IDENTITY_SCALE
53
+ return np.array([eigenvalue]), eigenvector
54
+
55
+ n_series = cov_matrix.shape[0]
56
+
57
+ try:
58
+ eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
59
+
60
+ # Sort by absolute value, descending
61
+ sort_idx = np.argsort(np.abs(eigenvalues))[::-1][:n_components]
62
+ eigenvalues_sorted = eigenvalues[sort_idx]
63
+ eigenvectors_sorted = eigenvectors[:, sort_idx]
64
+ return np.real(eigenvalues_sorted), np.real(eigenvectors_sorted)
65
+ except (ValueError, np.linalg.LinAlgError) as e:
66
+ if block_idx is not None:
67
+ _logger.warning(
68
+ f"PCA: Eigendecomposition failed for block {block_idx+1}, "
69
+ f"using identity matrix as fallback. Error: {type(e).__name__}"
70
+ )
71
+ else:
72
+ _logger.warning(
73
+ f"PCA: Eigendecomposition failed, using identity matrix as fallback. Error: {type(e).__name__}"
74
+ )
75
+ eigenvectors = create_scaled_identity(n_series, DEFAULT_IDENTITY_SCALE)[:, :n_components]
76
+ eigenvalues = np.ones(n_components)
77
+ return eigenvalues, eigenvectors
78
+
79
+
80
+ def fit_pca(
81
+ X: Union[np.ndarray, "torch.Tensor"],
82
+ n_components: int,
83
+ cov_matrix: Optional[Union[np.ndarray, "torch.Tensor"]] = None,
84
+ block_idx: Optional[int] = None
85
+ ) -> Tuple[np.ndarray, np.ndarray, Optional[np.ndarray], Optional[np.ndarray]]:
86
+ """Fit PCA by computing principal components.
87
+
88
+ If cov_matrix is provided, uses eigendecomposition.
89
+ If X is provided, uses NumPy's SVD for efficiency.
90
+ All computations are performed in NumPy.
91
+
92
+ Parameters
93
+ ----------
94
+ X : np.ndarray or torch.Tensor
95
+ Training data (T x N). If cov_matrix is provided, this is ignored.
96
+ n_components : int
97
+ Number of principal components to extract
98
+ cov_matrix : np.ndarray or torch.Tensor, optional
99
+ Precomputed covariance matrix (N x N). If None, computed from X.
100
+ block_idx : int, optional
101
+ Block index for error messages
102
+
103
+ Returns
104
+ -------
105
+ Tuple[np.ndarray, np.ndarray, Optional[np.ndarray], Optional[np.ndarray]]
106
+ (eigenvalues, eigenvectors, mean, cov_matrix)
107
+ """
108
+ if cov_matrix is not None:
109
+ # Use eigendecomposition for covariance matrix
110
+ eigenvalues, eigenvectors = compute_principal_components(
111
+ cov_matrix, n_components, block_idx=block_idx
112
+ )
113
+ # Store as NumPy array
114
+ cov_matrix_np = to_numpy(cov_matrix)
115
+ return eigenvalues, eigenvectors, None, cov_matrix_np
116
+ else:
117
+ # Convert to NumPy if needed
118
+ X = to_numpy(X)
119
+
120
+ # Center the data
121
+ mean_ = np.mean(X, axis=0, keepdims=True)
122
+ X_centered = X - mean_
123
+
124
+ # Use SVD for efficient low-rank PCA (NumPy equivalent of torch.pca_lowrank)
125
+ try:
126
+ # Check for NaN/Inf values before SVD
127
+ if np.any(~np.isfinite(X_centered)):
128
+ raise ValueError("Input contains non-finite values")
129
+
130
+ # Check for zero variance columns (can cause SVD issues)
131
+ col_stds = np.std(X_centered, axis=0)
132
+ if np.any(col_stds < 1e-10):
133
+ _logger.warning(
134
+ f"PCA: Some columns have near-zero variance (min_std={col_stds.min():.2e}). "
135
+ "This may cause SVD convergence issues. Consider removing constant columns."
136
+ )
137
+
138
+ # Use truncated SVD for efficiency
139
+ U, S, Vt = np.linalg.svd(X_centered, full_matrices=False, hermitian=False)
140
+
141
+ # Validate SVD results
142
+ if not np.all(np.isfinite(S)):
143
+ raise ValueError("SVD produced non-finite singular values")
144
+
145
+ # Take top n_components
146
+ n_components_actual = min(n_components, len(S))
147
+ if n_components_actual < n_components:
148
+ _logger.warning(
149
+ f"PCA: Requested {n_components} components but only {len(S)} available. "
150
+ f"Using {n_components_actual} components."
151
+ )
152
+
153
+ U = U[:, :n_components_actual]
154
+ S = S[:n_components_actual]
155
+ Vt = Vt[:n_components_actual, :]
156
+
157
+ # Vt contains the principal components (eigenvectors) as rows
158
+ # Transpose to get (N x n_components)
159
+ eigenvectors = Vt.T
160
+ # Convert singular values to eigenvalues
161
+ eigenvalues = S ** 2
162
+
163
+ return eigenvalues, eigenvectors, mean_, None
164
+
165
+ except (ValueError, np.linalg.LinAlgError) as e:
166
+ # Fallback: compute covariance and use eigendecomposition
167
+ error_msg = str(e) if str(e) else type(e).__name__
168
+ _logger.warning(
169
+ f"PCA SVD failed ({error_msg}), falling back to eigendecomposition. "
170
+ "This may be due to numerical instability or ill-conditioned data."
171
+ )
172
+
173
+ # Clean data for covariance computation
174
+ X_centered_clean = np.nan_to_num(X_centered, nan=0.0, posinf=0.0, neginf=0.0)
175
+ T = X_centered_clean.shape[0]
176
+
177
+ # Compute covariance with regularization for stability
178
+ cov_raw = (X_centered_clean.T @ X_centered_clean) / max(T - 1, 1)
179
+
180
+ # Add small regularization to diagonal for numerical stability
181
+ reg = 1e-8 * np.eye(cov_raw.shape[0], dtype=cov_raw.dtype)
182
+ cov_matrix_np = cov_raw + reg
183
+
184
+ try:
185
+ eigenvalues, eigenvectors = compute_principal_components(
186
+ cov_matrix_np, n_components, block_idx=block_idx
187
+ )
188
+ return eigenvalues, eigenvectors, mean_, cov_matrix_np
189
+ except (ValueError, np.linalg.LinAlgError) as e2:
190
+ # If eigendecomposition also fails, raise with helpful message
191
+ raise RuntimeError(
192
+ f"PCA failed: Both SVD and eigendecomposition failed. "
193
+ f"Original error: {error_msg}, Eigendecomposition error: {str(e2)}. "
194
+ "This may indicate severely ill-conditioned data. "
195
+ "Consider: (1) Removing constant/near-constant columns, "
196
+ "(2) Checking for outliers, (3) Using fewer components."
197
+ ) from e2
198
+
199
+
200
+ def encode_pca(
201
+ X: Union[np.ndarray, "torch.Tensor"],
202
+ eigenvectors: np.ndarray,
203
+ mean: Optional[np.ndarray] = None
204
+ ) -> np.ndarray:
205
+ """Extract factors using PCA eigenvectors.
206
+
207
+ All computations are performed in NumPy. Returns NumPy array.
208
+
209
+ Parameters
210
+ ----------
211
+ X : np.ndarray or torch.Tensor
212
+ Observed data (T x N)
213
+ eigenvectors : np.ndarray
214
+ PCA eigenvectors (N x n_components) from fit_pca
215
+ mean : Optional[np.ndarray]
216
+ Mean vector (1 x N) from fit_pca. If None, centers using X's mean.
217
+
218
+ Returns
219
+ -------
220
+ factors : np.ndarray
221
+ Extracted factors (T x n_components)
222
+ """
223
+ # Convert to NumPy if needed
224
+ X = to_numpy(X)
225
+
226
+ # Center the data
227
+ if mean is not None:
228
+ X_centered = X - mean
229
+ else:
230
+ X_centered = X - np.mean(X, axis=0, keepdims=True)
231
+
232
+ # Project: X @ eigenvectors
233
+ factors = X_centered @ eigenvectors
234
+
235
+ return factors
236
+
237
+
238
+ # Backward compatibility wrapper class (does not inherit from BaseEncoder)
239
+ class PCAEncoder:
240
+ """Principal Component Analysis encoder for factor extraction (backward compatibility wrapper).
241
+
242
+ This is a wrapper around pure PCA functions for backward compatibility.
243
+ The actual implementation uses pure functions in this module (layer.pca).
244
+
245
+ Parameters
246
+ ----------
247
+ n_components : int
248
+ Number of factors to extract
249
+ block_idx : int, optional
250
+ Block index for error messages
251
+ """
252
+
253
+ def __init__(
254
+ self,
255
+ n_components: int,
256
+ block_idx: Optional[int] = None
257
+ ):
258
+ self.n_components = n_components
259
+ self.block_idx = block_idx
260
+
261
+ # Will be set in fit()
262
+ self.eigenvectors: Optional[np.ndarray] = None
263
+ self.eigenvalues: Optional[np.ndarray] = None
264
+ self.cov_matrix: Optional[np.ndarray] = None
265
+ self.mean_: Optional[np.ndarray] = None
266
+
267
+ def fit(
268
+ self,
269
+ X: Union[np.ndarray, "torch.Tensor"],
270
+ cov_matrix: Optional[Union[np.ndarray, "torch.Tensor"]] = None,
271
+ **kwargs
272
+ ) -> "PCAEncoder":
273
+ """Fit PCA encoder by computing principal components."""
274
+ eigenvalues, eigenvectors, mean, cov_matrix_np = fit_pca(
275
+ X, self.n_components, cov_matrix=cov_matrix, block_idx=self.block_idx
276
+ )
277
+ self.eigenvalues = eigenvalues
278
+ self.eigenvectors = eigenvectors
279
+ self.mean_ = mean
280
+ self.cov_matrix = cov_matrix_np
281
+ return self
282
+
283
+ def encode(
284
+ self,
285
+ X: Union[np.ndarray, "torch.Tensor"],
286
+ **kwargs
287
+ ) -> np.ndarray:
288
+ """Extract factors using fitted PCA encoder."""
289
+ if self.eigenvectors is None:
290
+ from ..utils.errors import ModelNotTrainedError
291
+ raise ModelNotTrainedError(
292
+ "PCAEncoder must be fitted before encoding. Call fit() first.",
293
+ details="The encoder has not been fitted with training data yet."
294
+ )
295
+ return encode_pca(X, self.eigenvectors, mean=self.mean_)
296
+
297
+ def fit_encode(
298
+ self,
299
+ X: Union[np.ndarray, "torch.Tensor"],
300
+ **kwargs
301
+ ) -> np.ndarray:
302
+ """Fit encoder and extract factors in one step."""
303
+ self.fit(X, **kwargs)
304
+ return self.encode(X, **kwargs)
dfm_python/models/base.py CHANGED
@@ -1,9 +1,11 @@
1
- """Abstract base class for factor models."""
1
+ """Abstract base class for factor models and model components."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from typing import Optional, Any, Union, Tuple
5
5
  from pathlib import Path
6
6
  import numpy as np
7
+ import torch
8
+ import torch.nn as nn
7
9
 
8
10
  from ..config import BaseResult
9
11
  from ..utils.errors import ModelNotTrainedError
@@ -71,3 +73,51 @@ class BaseFactorModel(ABC):
71
73
  def load(cls, path: Union[str, Path], *args, **kwargs) -> 'BaseFactorModel':
72
74
  """Load model from file."""
73
75
  raise NotImplementedError("Subclasses must implement load()")
76
+
77
+
78
+ class BaseEncoder(nn.Module, ABC):
79
+ """Abstract base class for model encoders.
80
+
81
+ Model-specific encoder base class. Encoders extract latent factors
82
+ from observed data. This is a PyTorch module for neural encoders.
83
+ """
84
+
85
+ @abstractmethod
86
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
87
+ """Forward pass through encoder.
88
+
89
+ Parameters
90
+ ----------
91
+ x : torch.Tensor
92
+ Input data
93
+
94
+ Returns
95
+ -------
96
+ torch.Tensor
97
+ Encoded representation (factors)
98
+ """
99
+ pass
100
+
101
+
102
+ class BaseDecoder(nn.Module, ABC):
103
+ """Abstract base class for model decoders.
104
+
105
+ Model-specific decoder base class. Decoders map latent factors
106
+ to observed data. This is a PyTorch module for neural decoders.
107
+ """
108
+
109
+ @abstractmethod
110
+ def forward(self, f: torch.Tensor) -> torch.Tensor:
111
+ """Forward pass through decoder.
112
+
113
+ Parameters
114
+ ----------
115
+ f : torch.Tensor
116
+ Latent factors
117
+
118
+ Returns
119
+ -------
120
+ torch.Tensor
121
+ Reconstructed observations
122
+ """
123
+ pass
@@ -0,0 +1,12 @@
1
+ """DDFM model components."""
2
+
3
+ from .encoder import DDFMEncoder, SimpleAutoencoder, extract_decoder_params
4
+ from .decoder import DDFMLinearDecoder, DDFMMLPDecoder
5
+
6
+ __all__ = [
7
+ 'DDFMEncoder',
8
+ 'DDFMLinearDecoder',
9
+ 'DDFMMLPDecoder',
10
+ 'SimpleAutoencoder',
11
+ 'extract_decoder_params',
12
+ ]
@@ -21,7 +21,7 @@ from ...numeric.statistic import (
21
21
  average_3d_array
22
22
  )
23
23
  from ...numeric.builder import build_ddfm_optimizer
24
- from ...encoder.simple_autoencoder import SimpleAutoencoder
24
+ from .encoder import SimpleAutoencoder
25
25
  from .sampling import run_mcmc_iteration
26
26
  from ...config.schema.params import DDFMModelState
27
27
  from ...config.schema.results import DDFMResult