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 +2 -3
- dfm_python/config/adapter.py +34 -56
- dfm_python/config/schema.py +77 -185
- dfm_python/decoder/__init__.py +14 -0
- dfm_python/decoder/linear.py +68 -0
- dfm_python/decoder/mlp.py +130 -0
- dfm_python/encoder/__init__.py +7 -6
- dfm_python/encoder/{vae.py → autoencoder.py} +32 -61
- dfm_python/lightning/ddfm_dm.py +75 -239
- dfm_python/lightning/utils.py +21 -1
- dfm_python/models/__init__.py +3 -4
- dfm_python/models/base.py +63 -7
- dfm_python/models/ddfm.py +381 -224
- dfm_python/models/dfm.py +77 -29
- dfm_python/trainer/__init__.py +2 -0
- dfm_python/{models/mcmc.py → trainer/denoising.py} +50 -52
- dfm_python/utils/__init__.py +3 -3
- {dfm_python-0.5.5.dist-info → dfm_python-0.5.51.dist-info}/METADATA +1 -1
- {dfm_python-0.5.5.dist-info → dfm_python-0.5.51.dist-info}/RECORD +21 -19
- {dfm_python-0.5.5.dist-info → dfm_python-0.5.51.dist-info}/WHEEL +0 -0
- {dfm_python-0.5.5.dist-info → dfm_python-0.5.51.dist-info}/licenses/LICENSE +0 -0
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
|
|
117
|
-
from .models.ddfm import DDFM
|
|
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)
|
dfm_python/config/adapter.py
CHANGED
|
@@ -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=
|
|
214
|
-
frequency=
|
|
215
|
-
transformation
|
|
216
|
-
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'
|
|
230
|
+
series_list.append(SeriesConfig(series_id=series_item, frequency='m'))
|
|
228
231
|
elif isinstance(series_item, dict):
|
|
229
|
-
|
|
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
|
-
|
|
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'
|
|
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
|
-
#
|
|
254
|
-
#
|
|
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
|
-
#
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
652
|
-
blocks
|
|
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
|
dfm_python/config/schema.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
542
|
-
|
|
543
|
-
|
|
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.
|
|
564
|
-
|
|
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
|
-
#
|
|
577
|
-
for
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
613
|
-
|
|
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
|
-
#
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
|
796
|
-
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
|
))
|
dfm_python/decoder/__init__.py
CHANGED
|
@@ -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]")
|