dfm-python 0.5.5__py3-none-any.whl → 0.5.51__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
@@ -113,8 +113,8 @@ from .lightning import (
113
113
  from .models.base import BaseFactorModel
114
114
  from .models.dfm import DFM
115
115
 
116
- # DDFM high-level API and low-level model (PyTorch is mandatory)
117
- from .models.ddfm import DDFM, DDFMModel
116
+ # DDFM high-level API (PyTorch is mandatory)
117
+ from .models.ddfm import DDFM
118
118
 
119
119
  __all__ = [
120
120
  # Core classes
@@ -133,7 +133,6 @@ __all__ = [
133
133
  # DDFM high-level API (PyTorch is mandatory)
134
134
  __all__.extend([
135
135
  'DDFM', # High-level API class
136
- 'DDFMModel', # Low-level implementation (used internally)
137
136
  ])
138
137
 
139
138
  # Lightning modules (mandatory dependency)
@@ -208,13 +208,16 @@ class YamlSource:
208
208
  except (ValueError, TypeError):
209
209
  release_date = None
210
210
 
211
+ # Remove transformation and blocks if present (not in SeriesConfig)
212
+ series_data_clean = {k: v for k, v in series_data.items()
213
+ if k not in ['transformation', 'blocks']}
211
214
  series_list.append(SeriesConfig(
212
215
  series_id=series_id,
213
- series_name=series_data.get('series_name', series_id),
214
- frequency=series_data.get('frequency', 'm'),
215
- transformation=series_data.get('transformation', 'lin'),
216
- blocks=series_data.get('blocks', []),
217
- release_date=release_date
216
+ series_name=series_data_clean.get('series_name', series_id),
217
+ frequency=series_data_clean.get('frequency', 'm'),
218
+ # transformation removed - handled by preprocessing pipeline
219
+ # blocks removed - defined in DFMConfig
220
+ release_date=series_data_clean.get('release_date', release_date)
218
221
  ))
219
222
 
220
223
  # If no series loaded from separate files, try to get from main config
@@ -224,17 +227,23 @@ class YamlSource:
224
227
  for series_item in series_data:
225
228
  if isinstance(series_item, str):
226
229
  # Series is just a string ID - create minimal config
227
- series_list.append(SeriesConfig(series_id=series_item, frequency='m', transformation='lin', blocks=[1]))
230
+ series_list.append(SeriesConfig(series_id=series_item, frequency='m'))
228
231
  elif isinstance(series_item, dict):
229
- series_list.append(SeriesConfig(**series_item))
232
+ # Remove transformation and blocks if present (not in SeriesConfig)
233
+ series_item_clean = {k: v for k, v in series_item.items()
234
+ if k not in ['transformation', 'blocks']}
235
+ series_list.append(SeriesConfig(**series_item_clean))
230
236
  elif isinstance(series_data, dict):
231
237
  for series_id, series_item in series_data.items():
232
238
  if isinstance(series_item, dict):
233
239
  series_item['series_id'] = series_id
234
- series_list.append(SeriesConfig(**series_item))
240
+ # Remove transformation and blocks if present (not in SeriesConfig)
241
+ series_item_clean = {k: v for k, v in series_item.items()
242
+ if k not in ['transformation', 'blocks']}
243
+ series_list.append(SeriesConfig(**series_item_clean))
235
244
  elif isinstance(series_item, str):
236
245
  # Series value is just a string - create minimal config
237
- series_list.append(SeriesConfig(series_id=series_id, frequency='m', transformation='lin', blocks=[1]))
246
+ series_list.append(SeriesConfig(series_id=series_id, frequency='m'))
238
247
 
239
248
  # Load block properties from model config (factors, ar_lag, clock)
240
249
  # Model config defines all blocks and their properties
@@ -250,28 +259,8 @@ class YamlSource:
250
259
  if blocks_dict_raw is not None:
251
260
  blocks_properties_from_config.update(blocks_dict_raw)
252
261
 
253
- # Derive blocks from series configs (new pattern: blocks discovered from series)
254
- # Collect all unique block names from series
255
- block_names_from_series = set()
256
- for series in series_list:
257
- if hasattr(series, 'blocks') and series.blocks:
258
- # Handle both list of strings and list of ints
259
- if isinstance(series.blocks, list):
260
- for block in series.blocks:
261
- if isinstance(block, str):
262
- block_names_from_series.add(block)
263
-
264
- # If no blocks specified in series, use default single block
265
- # Use first block from model config if available, otherwise create default
266
- if not block_names_from_series:
267
- # Check if model config defines blocks
268
- if blocks_properties_from_config:
269
- # Use first block from model config
270
- first_block_name = list(blocks_properties_from_config.keys())[0]
271
- block_names_from_series.add(first_block_name)
272
- else:
273
- # Create default generic block
274
- block_names_from_series.add(DEFAULT_BLOCK_NAME)
262
+ # Note: SeriesConfig no longer contains blocks information.
263
+ # Blocks are defined in DFMConfig, not in SeriesConfig.
275
264
 
276
265
  # Create block dicts for each block
277
266
  # Priority: 1) Model config properties, 2) Defaults from main_settings
@@ -296,24 +285,13 @@ class YamlSource:
296
285
  'clock': default_clock
297
286
  }
298
287
 
299
- # Then, ensure all blocks referenced by series exist
300
- for block_name in block_names_from_series:
301
- if block_name not in blocks_dict:
302
- # Create block with defaults if not in model config
303
- blocks_dict[block_name] = {
304
- 'factors': 1,
305
- 'ar_lag': default_ar_lag,
306
- 'clock': default_clock
307
- }
308
-
309
- # Convert block names to list for from_dict() if series use integer block indices
310
- # Check if series use integer indices instead of block names
311
- block_names_list = None
312
- if series_list:
313
- first_series_blocks = series_list[0].blocks if hasattr(series_list[0], 'blocks') else []
314
- if first_series_blocks and isinstance(first_series_blocks[0], int):
315
- # Series use integer indices, need block_names for from_dict()
316
- block_names_list = list(blocks_dict.keys())
288
+ # If no blocks specified in config, use default single block
289
+ if not blocks_dict:
290
+ blocks_dict[DEFAULT_BLOCK_NAME] = {
291
+ 'factors': 1,
292
+ 'ar_lag': default_ar_lag,
293
+ 'clock': default_clock
294
+ }
317
295
 
318
296
  # Build dictionary for from_dict() which handles type detection
319
297
  # Include all main_settings (which may contain DDFM parameters from model config)
@@ -323,10 +301,6 @@ class YamlSource:
323
301
  **main_settings
324
302
  }
325
303
 
326
- # Add block_names if series use integer indices
327
- if block_names_list is not None:
328
- config_dict['block_names'] = block_names_list
329
-
330
304
  # Use from_dict() which now correctly detects DDFM configs
331
305
  # The detection bug has been fixed in schema.py
332
306
  return DFMConfig.from_dict(config_dict)
@@ -644,12 +618,16 @@ if HYDRA_AVAILABLE and ConfigStore is not None:
644
618
 
645
619
  @schema_dataclass
646
620
  class SeriesConfigSchema:
647
- """Schema for SeriesConfig validation in Hydra."""
621
+ """Schema for SeriesConfig validation in Hydra.
622
+
623
+ Note: transformation is handled by preprocessing pipeline, not in SeriesConfig.
624
+ Note: blocks are defined in DFMConfig, not in SeriesConfig.
625
+ """
648
626
  series_id: str
649
627
  series_name: str
650
628
  frequency: str
651
- transformation: str
652
- blocks: ListType[int]
629
+ # transformation removed - handled by preprocessing pipeline
630
+ # blocks removed - defined in DFMConfig
653
631
  units: Optional[str] = None # Optional, for display only
654
632
 
655
633
  @schema_dataclass
@@ -36,7 +36,7 @@ except ImportError:
36
36
  DEFAULT_BLOCK_NAME = 'Block_0'
37
37
 
38
38
  # Import validation functions from utils
39
- from .utils import validate_frequency, validate_transformation
39
+ from .utils import validate_frequency
40
40
 
41
41
 
42
42
  # ============================================================================
@@ -93,14 +93,17 @@ def _infer_blocks(
93
93
  series_list: List['SeriesConfig'],
94
94
  data: Dict[str, Any]
95
95
  ) -> Dict[str, Dict[str, Any]]:
96
- """Infer blocks from series when blocks not explicitly provided.
96
+ """Infer blocks from configuration data when blocks not explicitly provided.
97
+
98
+ Note: SeriesConfig no longer contains blocks information.
99
+ Blocks are defined in DFMConfig, not in SeriesConfig.
97
100
 
98
101
  Parameters
99
102
  ----------
100
103
  series_list : List[SeriesConfig]
101
- List of series configurations
104
+ List of series configurations (blocks information not used)
102
105
  data : Dict[str, Any]
103
- Configuration data (for clock default)
106
+ Configuration data (for clock default and block_names)
104
107
 
105
108
  Returns
106
109
  -------
@@ -108,31 +111,17 @@ def _infer_blocks(
108
111
  Dictionary mapping block names to block config dicts
109
112
  """
110
113
  blocks_dict = {}
111
- block_names_set = set()
112
-
113
- # Collect all unique block names from series
114
- for s in series_list:
115
- if isinstance(s, dict):
116
- blocks = s.get('blocks', [])
117
- else:
118
- blocks = s.blocks
119
-
120
- if isinstance(blocks, list) and len(blocks) > 0:
121
- if isinstance(blocks[0], str):
122
- # Block names as strings
123
- block_names_set.update(blocks)
124
- else:
125
- # Block indices - need block_names to map
126
- if 'block_names' in data:
127
- block_names_list = data['block_names']
128
- for idx in blocks:
129
- if isinstance(idx, int) and idx < len(block_names_list):
130
- block_names_set.add(block_names_list[idx])
131
114
 
132
- # Create default blocks for each unique block name
133
- clock = data.get('clock', 'm')
134
- for block_name in sorted(block_names_set):
135
- blocks_dict[block_name] = {'factors': 1, 'ar_lag': 1, 'clock': clock}
115
+ # Try to get block_names from data
116
+ if 'block_names' in data:
117
+ block_names_list = data['block_names']
118
+ clock = data.get('clock', 'm')
119
+ for block_name in block_names_list:
120
+ blocks_dict[block_name] = {'factors': 1, 'ar_lag': 1, 'clock': clock}
121
+ else:
122
+ # Default: create default block if no blocks specified
123
+ clock = data.get('clock', 'm')
124
+ blocks_dict[DEFAULT_BLOCK_NAME] = {'factors': 1, 'ar_lag': 1, 'clock': clock}
136
125
 
137
126
  return blocks_dict
138
127
 
@@ -179,15 +168,13 @@ class SeriesConfig:
179
168
  This is a generic DFM configuration - no API or database-specific fields.
180
169
  For API/database integration, implement adapters in your application layer.
181
170
 
171
+ Note: Transformation is handled by preprocessing pipeline, not in SeriesConfig.
172
+ Note: Blocks are defined in DFMConfig, not in SeriesConfig.
173
+
182
174
  Attributes
183
175
  ----------
184
176
  frequency : str
185
177
  Series frequency: 'm' (monthly), 'q' (quarterly), 'sa' (semi-annual), 'a' (annual)
186
- transformation : str
187
- Transformation code: 'lin', 'pch', 'pca', etc.
188
- blocks : List[str] or List[int]
189
- Block names (strings) or block indices (ints) this series loads on.
190
- Must include the global block (first block).
191
178
  series_id : str, optional
192
179
  Unique identifier (auto-generated if None)
193
180
  series_name : str, optional
@@ -203,8 +190,6 @@ class SeriesConfig:
203
190
  """
204
191
  # Required fields (no defaults)
205
192
  frequency: str
206
- transformation: str
207
- blocks: Union[List[str], List[int]] # Can be block names (str) or indices (int)
208
193
  # Optional fields (with defaults - must come after required fields)
209
194
  series_id: Optional[str] = None # Auto-generated if None: "series_0", "series_1", etc.
210
195
  series_name: Optional[str] = None # Optional metadata for display
@@ -214,65 +199,9 @@ class SeriesConfig:
214
199
  def __post_init__(self):
215
200
  """Validate fields after initialization."""
216
201
  self.frequency = validate_frequency(self.frequency)
217
- self.transformation = validate_transformation(self.transformation)
218
202
  # Auto-generate series_name if not provided
219
203
  if self.series_name is None and self.series_id:
220
204
  self.series_name = self.series_id
221
-
222
- def to_block_indices(self, block_names: List[str]) -> List[int]:
223
- """Convert block names to indices.
224
-
225
- Parameters
226
- ----------
227
- block_names : List[str]
228
- List of block names in order
229
-
230
- Returns
231
- -------
232
- List[int]
233
- Block indices (0 or 1) for each block
234
- """
235
- if not self.blocks:
236
- raise ValueError(
237
- f"Series '{self.series_id}' validation failed: no blocks specified. "
238
- f"Please specify blocks for series '{self.series_id}'."
239
- )
240
-
241
- # If already integers, validate and return
242
- if isinstance(self.blocks[0], int):
243
- if len(self.blocks) != len(block_names):
244
- raise ValueError(
245
- f"Series '{self.series_id}' validation failed: has {len(self.blocks)} block indices "
246
- f"but {len(block_names)} blocks defined. "
247
- f"Please ensure series '{self.series_id}' has exactly {len(block_names)} block indices."
248
- )
249
- return list(self.blocks)
250
-
251
- # Convert block names to indices
252
- # Normalize block names (handle both underscore and hyphen variants)
253
- block_names_normalized = {name.replace('-', '_'): name for name in block_names}
254
- blocks_normalized = {name.replace('-', '_'): name for name in self.blocks}
255
-
256
- block_indices = [0] * len(block_names)
257
- for block_name in self.blocks:
258
- block_name_norm = block_name.replace('-', '_')
259
- if block_name_norm not in block_names_normalized:
260
- # Try exact match first
261
- if block_name in block_names:
262
- block_indices[block_names.index(block_name)] = 1
263
- else:
264
- raise ValueError(
265
- f"Series '{self.series_id}' validation failed: references block '{block_name}' "
266
- f"which is not in block_names: {block_names}. "
267
- f"Block names are case-sensitive and must match exactly. "
268
- f"Please use one of the defined block names: {block_names}."
269
- )
270
- else:
271
- # Use normalized name to find index
272
- actual_block_name = block_names_normalized[block_name_norm]
273
- block_indices[block_names.index(actual_block_name)] = 1
274
-
275
- return block_indices
276
205
 
277
206
 
278
207
  @dataclass
@@ -538,63 +467,37 @@ class DFMConfig(BaseModelConfig):
538
467
  f"Please set ar_lag to 1 (VAR(1)) or 2 (VAR(2)) for block '{block_name}'."
539
468
  )
540
469
 
541
- # Auto-generate series_id if not provided and convert blocks to indices
542
- n_blocks = len(self.block_names)
543
- for i, s in enumerate(self.series):
544
- # Convert block names to indices if needed
545
- if isinstance(s.blocks, list) and len(s.blocks) > 0:
546
- if isinstance(s.blocks[0], str):
547
- # Convert block names to indices
548
- block_indices = s.to_block_indices(self.block_names)
549
- object.__setattr__(s, 'blocks', block_indices)
550
-
551
- # Validate all series have correct number of blocks
552
- for i, s in enumerate(self.series):
553
- if len(s.blocks) != n_blocks:
554
- raise ValueError(
555
- f"Series {i} ('{s.series_id}') validation failed: has {len(s.blocks)} block loadings, "
556
- f"but expected {n_blocks} (from block_names: {self.block_names}). "
557
- f"Each series must specify a loading (0 or 1) for each block. "
558
- f"Please ensure series '{s.series_id}' has exactly {n_blocks} block loadings."
559
- )
560
-
561
- # Check first column (global block) is all 1s
470
+ # Auto-generate series_id if not provided
471
+ # Note: Blocks are defined in DFMConfig, not in SeriesConfig.
472
+ # Series-to-block mapping is handled separately (e.g., via series_to_blocks dict or default to global block).
562
473
  for i, s in enumerate(self.series):
563
- if s.blocks[0] != 1:
564
- raise ValueError(
565
- f"Series {i} ('{s.series_id}') validation failed: must load on the global block "
566
- f"(first block '{self.block_names[0]}'). "
567
- f"All series must have blocks[0] = 1. "
568
- f"Current value: {s.blocks[0]}. "
569
- f"Please set blocks[0] = 1 for series '{s.series_id}'."
570
- )
474
+ if s.series_id is None:
475
+ object.__setattr__(s, 'series_id', f"series_{i}")
571
476
 
572
477
  # Validate block clock constraints: series frequency <= block clock
478
+ # All series load on all blocks by default (can be customized via series_to_blocks mapping)
573
479
  for i, s in enumerate(self.series):
574
480
  series_freq_hierarchy = FREQUENCY_HIERARCHY.get(s.frequency, 3)
575
481
 
576
- # Find which blocks this series loads on
577
- for block_idx, loads_on_block in enumerate(s.blocks):
578
- if loads_on_block == 1:
579
- block_name = self.block_names[block_idx]
580
- block_cfg = self.blocks[block_name]
581
- block_clock = block_cfg.get('clock', self.clock)
582
- block_clock_hierarchy = FREQUENCY_HIERARCHY.get(block_clock, 3)
583
-
584
- # Series frequency must be <= block clock (slower or equal)
585
- if series_freq_hierarchy < block_clock_hierarchy:
586
- # Suggest valid frequencies for the series
587
- valid_freqs = [freq for freq, hier in FREQUENCY_HIERARCHY.items()
588
- if hier >= block_clock_hierarchy]
589
- valid_freqs_str = ', '.join(sorted(valid_freqs))
590
- raise ValueError(
591
- f"Series '{s.series_id}' has frequency '{s.frequency}' which is faster than "
592
- f"block '{block_name}' clock '{block_clock}'. "
593
- f"Series in a block must have frequency <= block clock. "
594
- f"Suggested fix: change series frequency to one of [{valid_freqs_str}] "
595
- f"(slower or equal to block clock '{block_clock}'), "
596
- f"or set block clock to '{s.frequency}' or faster."
597
- )
482
+ # Check against all blocks (series can load on multiple blocks)
483
+ for block_name, block_cfg in self.blocks.items():
484
+ block_clock = block_cfg.get('clock', self.clock)
485
+ block_clock_hierarchy = FREQUENCY_HIERARCHY.get(block_clock, 3)
486
+
487
+ # Series frequency must be <= block clock (slower or equal)
488
+ if series_freq_hierarchy < block_clock_hierarchy:
489
+ # Suggest valid frequencies for the series
490
+ valid_freqs = [freq for freq, hier in FREQUENCY_HIERARCHY.items()
491
+ if hier >= block_clock_hierarchy]
492
+ valid_freqs_str = ', '.join(sorted(valid_freqs))
493
+ raise ValueError(
494
+ f"Series '{s.series_id}' has frequency '{s.frequency}' which is faster than "
495
+ f"block '{block_name}' clock '{block_clock}'. "
496
+ f"Series in a block must have frequency <= block clock. "
497
+ f"Suggested fix: change series frequency to one of [{valid_freqs_str}] "
498
+ f"(slower or equal to block clock '{block_clock}'), "
499
+ f"or set block clock to '{s.frequency}' or faster."
500
+ )
598
501
 
599
502
  # Validate factors_per_block
600
503
  if any(f < 1 for f in self.factors_per_block):
@@ -607,10 +510,26 @@ class DFMConfig(BaseModelConfig):
607
510
  )
608
511
 
609
512
  def get_blocks_array(self) -> np.ndarray:
610
- """Get blocks as numpy array (snake_case - recommended, cached)."""
513
+ """Get blocks as numpy array (snake_case - recommended, cached).
514
+
515
+ Note: SeriesConfig no longer contains blocks information.
516
+ By default, all series load on all blocks (all 1s).
517
+ Custom series-to-block mapping can be provided via series_to_blocks attribute.
518
+ """
611
519
  if self._cached_blocks is None:
612
- blocks_list = [s.blocks for s in self.series]
613
- self._cached_blocks = np.array(blocks_list, dtype=int)
520
+ n_series = len(self.series)
521
+ n_blocks = len(self.block_names)
522
+ # Default: all series load on all blocks (all 1s)
523
+ # Custom mapping can be provided via series_to_blocks attribute if needed
524
+ if hasattr(self, 'series_to_blocks') and self.series_to_blocks is not None:
525
+ blocks_list = []
526
+ for s in self.series:
527
+ series_blocks = self.series_to_blocks.get(s.series_id, [1] * n_blocks)
528
+ blocks_list.append(series_blocks)
529
+ self._cached_blocks = np.array(blocks_list, dtype=int)
530
+ else:
531
+ # Default: all series load on all blocks
532
+ self._cached_blocks = np.ones((n_series, n_blocks), dtype=int)
614
533
  return self._cached_blocks
615
534
 
616
535
  # ========================================================================
@@ -747,53 +666,26 @@ class DDFMConfig(BaseModelConfig):
747
666
  block_names = data.get('block_names', [])
748
667
  factors_per_block = data.get('factors_per_block', None)
749
668
 
750
- # If block_names is still empty, try to infer from series blocks
751
- if not block_names and 'series' in data and isinstance(data['series'], dict):
752
- # Collect all unique block names from series
753
- all_blocks = set()
754
- for series_cfg in data['series'].values():
755
- if isinstance(series_cfg, dict):
756
- series_blocks = series_cfg.get('blocks', [])
757
- if isinstance(series_blocks, list):
758
- all_blocks.update(series_blocks)
759
- if all_blocks:
760
- # Ensure first block is used as default
761
- block_names = []
762
- if all_blocks:
763
- # Use first block as global
764
- first_block = sorted(all_blocks)[0]
765
- block_names.append(first_block)
766
- all_blocks.remove(first_block)
767
- block_names.extend(sorted(all_blocks))
768
- factors_per_block = [1] * len(block_names)
769
- # Create default blocks dict for later use
770
- blocks_dict = {name: {'factors': 1, 'clock': 'm'} for name in block_names}
771
-
772
- # Parse series dict: {series_id: {frequency: ..., blocks: [block_names], ...}}
669
+ # Note: SeriesConfig no longer contains blocks information.
670
+ # Blocks must be defined in DFMConfig, not inferred from series.
671
+ # If block_names is still empty, create default block
672
+ if not block_names:
673
+ block_names = [DEFAULT_BLOCK_NAME]
674
+ factors_per_block = [1]
675
+ blocks_dict = {DEFAULT_BLOCK_NAME: {'factors': 1, 'ar_lag': 1, 'clock': data.get('clock', 'm')}}
676
+
677
+ # Parse series dict: {series_id: {frequency: ..., ...}}
678
+ # Note: transformation is handled by preprocessing pipeline, not in SeriesConfig
679
+ # Note: blocks are defined in DFMConfig, not in SeriesConfig
773
680
  series_list = []
774
681
  for series_id, series_cfg in data['series'].items():
775
682
  if isinstance(series_cfg, dict):
776
- # Convert block names to binary array (0/1 for each block)
777
- series_blocks_names = series_cfg.get('blocks', [])
778
- if isinstance(series_blocks_names, list) and series_blocks_names:
779
- # Create binary array: 1 if series loads on block, 0 otherwise
780
- # First block (Global) must always be 1
781
- series_blocks = [0] * len(block_names)
782
- series_blocks[0] = 1 # Global block is always 1
783
- for block_name in series_blocks_names:
784
- if block_name in block_names:
785
- block_idx = block_names.index(block_name)
786
- series_blocks[block_idx] = 1
787
- else:
788
- # Default: only global block
789
- series_blocks = [1] + [0] * (len(block_names) - 1)
790
-
791
683
  series_list.append(SeriesConfig(
792
684
  series_id=series_id,
793
685
  series_name=series_cfg.get('series_name', series_id),
794
686
  frequency=series_cfg.get('frequency', 'm'),
795
- transformation=series_cfg.get('transformation', 'lin'),
796
- blocks=series_blocks,
687
+ # transformation removed - handled by preprocessing pipeline
688
+ # blocks removed - defined in DFMConfig
797
689
  units=series_cfg.get('units', None), # Optional, for display only
798
690
  release_date=series_cfg.get('release_date', None) # Optional, for nowcasting
799
691
  ))
@@ -0,0 +1,14 @@
1
+ """Decoder modules for DDFM.
2
+
3
+ This package provides decoder implementations for reconstructing observations
4
+ from latent factors in the Deep Dynamic Factor Model (DDFM).
5
+ """
6
+
7
+ from .linear import Decoder
8
+ from .mlp import MLPDecoder
9
+
10
+ __all__ = [
11
+ 'Decoder',
12
+ 'MLPDecoder',
13
+ ]
14
+
@@ -0,0 +1,68 @@
1
+ """Linear decoder network for DDFM.
2
+
3
+ Maps latent factors f_t back to observed variables X_t using a linear transformation.
4
+ This preserves interpretability and allows Kalman filtering.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ import torch
11
+ import torch.nn as nn
12
+ else:
13
+ torch = None
14
+ nn = None
15
+
16
+ try:
17
+ import torch
18
+ import torch.nn as nn
19
+ _has_torch = True
20
+ except ImportError:
21
+ _has_torch = False
22
+ torch = None
23
+ nn = None
24
+
25
+
26
+ if _has_torch:
27
+ class Decoder(nn.Module):
28
+ """Linear decoder network for DDFM.
29
+
30
+ Maps latent factors f_t back to observed variables X_t using a linear transformation.
31
+ This preserves interpretability and allows Kalman filtering.
32
+ """
33
+
34
+ def __init__(self, input_dim: int, output_dim: int, use_bias: bool = True):
35
+ """Initialize linear decoder.
36
+
37
+ Parameters
38
+ ----------
39
+ input_dim : int
40
+ Number of factors (input dimension)
41
+ output_dim : int
42
+ Number of series (output dimension)
43
+ use_bias : bool
44
+ Whether to use bias term
45
+ """
46
+ super().__init__()
47
+ self.decoder = nn.Linear(input_dim, output_dim, bias=use_bias)
48
+
49
+ def forward(self, f: "torch.Tensor") -> "torch.Tensor":
50
+ """Forward pass through decoder.
51
+
52
+ Parameters
53
+ ----------
54
+ f : torch.Tensor
55
+ Factors (batch_size x input_dim)
56
+
57
+ Returns
58
+ -------
59
+ torch.Tensor
60
+ Reconstructed observations (batch_size x output_dim)
61
+ """
62
+ return self.decoder(f)
63
+ else:
64
+ # Placeholder class when PyTorch is not available
65
+ class Decoder:
66
+ """Placeholder Decoder class when PyTorch is not available."""
67
+ def __init__(self, *args, **kwargs):
68
+ raise ImportError("PyTorch is required for DDFM. Install with: pip install dfm-python[deep]")