dfm-python 0.2.3__py3-none-any.whl → 0.2.5__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 +10 -53
- dfm_python/api.py +4 -4
- dfm_python/config.py +235 -558
- dfm_python/config_sources.py +558 -0
- dfm_python/core/__init__.py +2 -46
- dfm_python/core/diagnostics.py +162 -5
- dfm_python/core/em.py +1014 -742
- dfm_python/core/helpers.py +977 -35
- dfm_python/core/numeric.py +420 -137
- dfm_python/data.py +609 -0
- dfm_python/dfm.py +289 -248
- dfm_python/kalman.py +64 -62
- dfm_python/news.py +1 -1
- dfm_python/{utils/aggregation.py → utils.py} +148 -2
- dfm_python-0.2.5.dist-info/METADATA +783 -0
- dfm_python-0.2.5.dist-info/RECORD +18 -0
- dfm_python/core/grouping.py +0 -54
- dfm_python/core/results.py +0 -70
- dfm_python/data_loader.py +0 -798
- dfm_python/utils/__init__.py +0 -25
- dfm_python/utils/data_utils.py +0 -198
- dfm_python-0.2.3.dist-info/METADATA +0 -312
- dfm_python-0.2.3.dist-info/RECORD +0 -21
- {dfm_python-0.2.3.dist-info → dfm_python-0.2.5.dist-info}/WHEEL +0 -0
- {dfm_python-0.2.3.dist-info → dfm_python-0.2.5.dist-info}/licenses/LICENSE +0 -0
dfm_python/config.py
CHANGED
|
@@ -1,48 +1,35 @@
|
|
|
1
|
-
"""Configuration models and
|
|
1
|
+
"""Configuration models and factory methods for DFM nowcasting.
|
|
2
2
|
|
|
3
|
-
This module provides
|
|
3
|
+
This module provides the core configuration system for Dynamic Factor Models:
|
|
4
4
|
- Configuration dataclasses (DFMConfig, SeriesConfig, BlockConfig, Params)
|
|
5
|
-
-
|
|
6
|
-
- Factory functions for flexible config loading
|
|
5
|
+
- Factory methods for creating DFMConfig from dictionaries and Hydra configs
|
|
7
6
|
|
|
8
|
-
The configuration
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- Dictionary configurations
|
|
13
|
-
- Merging multiple configuration sources
|
|
7
|
+
The configuration dataclasses define:
|
|
8
|
+
- Model structure (series, blocks, factors)
|
|
9
|
+
- Estimation parameters (EM algorithm settings)
|
|
10
|
+
- Numerical stability controls (regularization, clipping, damping)
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
Factory methods support:
|
|
13
|
+
- Dictionary configurations (legacy format, new format, Hydra format)
|
|
14
|
+
- Hydra DictConfig objects (via from_hydra())
|
|
15
|
+
|
|
16
|
+
For loading configurations from files (YAML, Spec CSV) or other sources,
|
|
17
|
+
see the config_sources module which provides source adapters.
|
|
18
|
+
|
|
19
|
+
Note: Source adapter classes (YamlSource, DictSource, etc.) are re-exported
|
|
20
|
+
from config_sources for backward compatibility.
|
|
17
21
|
"""
|
|
18
22
|
|
|
19
23
|
import numpy as np
|
|
20
24
|
from typing import List, Optional, Dict, Any, Union
|
|
21
|
-
from
|
|
22
|
-
import warnings
|
|
23
|
-
import logging
|
|
24
|
-
from dataclasses import dataclass, field, is_dataclass, asdict
|
|
25
|
+
from dataclasses import dataclass, field
|
|
25
26
|
|
|
26
27
|
try:
|
|
27
28
|
from typing import Protocol
|
|
28
29
|
except ImportError:
|
|
29
30
|
from typing_extensions import Protocol
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
from hydra.core.config_store import ConfigStore
|
|
33
|
-
HYDRA_AVAILABLE = True
|
|
34
|
-
except ImportError:
|
|
35
|
-
HYDRA_AVAILABLE = False
|
|
36
|
-
ConfigStore = None
|
|
37
|
-
|
|
38
|
-
# Valid frequency codes
|
|
39
|
-
_VALID_FREQUENCIES = {'d', 'w', 'm', 'q', 'sa', 'a'}
|
|
40
|
-
|
|
41
|
-
# Valid transformation codes
|
|
42
|
-
_VALID_TRANSFORMATIONS = {
|
|
43
|
-
'lin', 'chg', 'ch1', 'pch', 'pc1', 'pca',
|
|
44
|
-
'cch', 'cca', 'log'
|
|
45
|
-
}
|
|
32
|
+
# Validation functions are defined at the end of this file
|
|
46
33
|
|
|
47
34
|
# Default global block name (can be overridden in config)
|
|
48
35
|
DEFAULT_GLOBAL_BLOCK_NAME = 'Block_Global'
|
|
@@ -61,20 +48,6 @@ _TRANSFORM_UNITS_MAP = {
|
|
|
61
48
|
}
|
|
62
49
|
|
|
63
50
|
|
|
64
|
-
def validate_frequency(frequency: str) -> str:
|
|
65
|
-
"""Validate frequency code."""
|
|
66
|
-
if frequency not in _VALID_FREQUENCIES:
|
|
67
|
-
raise ValueError(f"Invalid frequency: {frequency}. Must be one of {_VALID_FREQUENCIES}")
|
|
68
|
-
return frequency
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def validate_transformation(transformation: str) -> str:
|
|
72
|
-
"""Validate transformation code."""
|
|
73
|
-
if transformation not in _VALID_TRANSFORMATIONS:
|
|
74
|
-
warnings.warn(f"Unknown transformation code: {transformation}. Will use untransformed data.")
|
|
75
|
-
return transformation
|
|
76
|
-
|
|
77
|
-
|
|
78
51
|
@dataclass
|
|
79
52
|
class BlockConfig:
|
|
80
53
|
"""Configuration for a single factor block.
|
|
@@ -224,9 +197,12 @@ class Params:
|
|
|
224
197
|
max_iter : int
|
|
225
198
|
Maximum EM iterations (default: 5000)
|
|
226
199
|
nan_method : int
|
|
227
|
-
Missing data handling method (1-5, default: 2 = spline interpolation)
|
|
200
|
+
Missing data handling method (1-5, default: 2 = spline interpolation).
|
|
201
|
+
This preprocessing step is followed by Kalman Filter-based missing data
|
|
202
|
+
handling during DFM estimation, following standard practice in state-space
|
|
203
|
+
models (Mariano & Murasawa 2003, Harvey 1989).
|
|
228
204
|
nan_k : int
|
|
229
|
-
Spline parameter for NaN interpolation (default: 3)
|
|
205
|
+
Spline parameter for NaN interpolation (default: 3 for cubic spline)
|
|
230
206
|
clock : str
|
|
231
207
|
Base frequency for all latent factors (default: 'm' for monthly)
|
|
232
208
|
clip_ar_coefficients : bool
|
|
@@ -259,6 +235,14 @@ class Params:
|
|
|
259
235
|
Damping factor: 0.8 = 80% new, 20% old (default: 0.8)
|
|
260
236
|
warn_on_damped_update : bool
|
|
261
237
|
Warn when damped updates are used (default: True)
|
|
238
|
+
augment_idio : bool
|
|
239
|
+
Enable state augmentation with idiosyncratic components (default: True)
|
|
240
|
+
augment_idio_slow : bool
|
|
241
|
+
Enable tent-length chains for slower-frequency series (default: True)
|
|
242
|
+
idio_rho0 : float
|
|
243
|
+
Initial AR coefficient for idiosyncratic components (default: 0.1)
|
|
244
|
+
idio_min_var : float
|
|
245
|
+
Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
|
|
262
246
|
|
|
263
247
|
Examples
|
|
264
248
|
--------
|
|
@@ -297,6 +281,12 @@ class Params:
|
|
|
297
281
|
damping_factor: float = 0.8
|
|
298
282
|
warn_on_damped_update: bool = True
|
|
299
283
|
|
|
284
|
+
# Idiosyncratic component augmentation
|
|
285
|
+
augment_idio: bool = True # Enable state augmentation with idiosyncratic components (default: True)
|
|
286
|
+
augment_idio_slow: bool = True # Enable tent-length chains for slower-frequency series (default: True)
|
|
287
|
+
idio_rho0: float = 0.1 # Initial AR coefficient for idiosyncratic components (default: 0.1)
|
|
288
|
+
idio_min_var: float = 1e-8 # Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
|
|
289
|
+
|
|
300
290
|
def __post_init__(self):
|
|
301
291
|
"""Validate parameters."""
|
|
302
292
|
self.clock = validate_frequency(self.clock)
|
|
@@ -340,8 +330,8 @@ class DFMConfig:
|
|
|
340
330
|
ar_lag: int = 1 # Number of lags in AR transition equation (lookback window)
|
|
341
331
|
threshold: float = 1e-5 # EM convergence threshold
|
|
342
332
|
max_iter: int = 5000 # Maximum EM iterations
|
|
343
|
-
nan_method: int = 2 # Missing data handling method (1-5)
|
|
344
|
-
nan_k: int = 3 # Spline parameter for NaN interpolation
|
|
333
|
+
nan_method: int = 2 # Missing data handling method (1-5). Preprocessing step before Kalman Filter-based handling
|
|
334
|
+
nan_k: int = 3 # Spline parameter for NaN interpolation (cubic spline)
|
|
345
335
|
clock: str = 'm' # Base frequency for nowcasting (global clock): 'd', 'w', 'm', 'q', 'sa', 'a' (defaults to 'm' for monthly)
|
|
346
336
|
|
|
347
337
|
# ========================================================================
|
|
@@ -370,6 +360,12 @@ class DFMConfig:
|
|
|
370
360
|
damping_factor: float = 0.8 # Damping factor (0.8 = 80% new, 20% old)
|
|
371
361
|
warn_on_damped_update: bool = True # Warn when damped updates are used
|
|
372
362
|
|
|
363
|
+
# Idiosyncratic Component Augmentation
|
|
364
|
+
augment_idio: bool = True # Enable state augmentation with idiosyncratic components (default: True)
|
|
365
|
+
augment_idio_slow: bool = True # Enable tent-length chains for slower-frequency series (default: True)
|
|
366
|
+
idio_rho0: float = 0.1 # Initial AR coefficient for idiosyncratic components (default: 0.1)
|
|
367
|
+
idio_min_var: float = 1e-8 # Minimum variance for idiosyncratic innovation covariance (default: 1e-8)
|
|
368
|
+
|
|
373
369
|
# ========================================================================
|
|
374
370
|
# Internal cache (not user-configurable)
|
|
375
371
|
# ========================================================================
|
|
@@ -394,7 +390,7 @@ class DFMConfig:
|
|
|
394
390
|
indicating what needs to be fixed.
|
|
395
391
|
"""
|
|
396
392
|
# Import frequency hierarchy for validation
|
|
397
|
-
from .utils
|
|
393
|
+
from .utils import FREQUENCY_HIERARCHY
|
|
398
394
|
|
|
399
395
|
if not self.series:
|
|
400
396
|
raise ValueError(
|
|
@@ -443,7 +439,9 @@ class DFMConfig:
|
|
|
443
439
|
if block_clock_hierarchy < global_clock_hierarchy:
|
|
444
440
|
raise ValueError(
|
|
445
441
|
f"Block '{block_name}' has clock '{block_cfg.clock}' which is faster than "
|
|
446
|
-
f"global clock '{self.clock}'. Block clocks must be >= global clock."
|
|
442
|
+
f"global clock '{self.clock}'. Block clocks must be >= global clock. "
|
|
443
|
+
f"Suggested fix: change block '{block_name}' clock to '{self.clock}' or slower, "
|
|
444
|
+
f"or set global clock to '{block_cfg.clock}' or faster."
|
|
447
445
|
)
|
|
448
446
|
|
|
449
447
|
# Auto-generate series_id if not provided and convert blocks to indices
|
|
@@ -493,11 +491,17 @@ class DFMConfig:
|
|
|
493
491
|
|
|
494
492
|
# Series frequency must be <= block clock (slower or equal)
|
|
495
493
|
if series_freq_hierarchy < block_clock_hierarchy:
|
|
494
|
+
# Suggest valid frequencies for the series
|
|
495
|
+
valid_freqs = [freq for freq, hier in FREQUENCY_HIERARCHY.items()
|
|
496
|
+
if hier >= block_clock_hierarchy]
|
|
497
|
+
valid_freqs_str = ', '.join(sorted(valid_freqs))
|
|
496
498
|
raise ValueError(
|
|
497
499
|
f"Series '{s.series_id}' has frequency '{s.frequency}' which is faster than "
|
|
498
500
|
f"block '{block_name}' clock '{block_cfg.clock}'. "
|
|
499
501
|
f"Series in a block must have frequency <= block clock. "
|
|
500
|
-
f"
|
|
502
|
+
f"Suggested fix: change series frequency to one of [{valid_freqs_str}] "
|
|
503
|
+
f"(slower or equal to block clock '{block_cfg.clock}'), "
|
|
504
|
+
f"or set block clock to '{s.frequency}' or faster."
|
|
501
505
|
)
|
|
502
506
|
|
|
503
507
|
# Validate factors_per_block
|
|
@@ -535,6 +539,106 @@ class DFMConfig:
|
|
|
535
539
|
self._cached_blocks = np.array(blocks_list, dtype=int)
|
|
536
540
|
return self._cached_blocks
|
|
537
541
|
|
|
542
|
+
def validate_and_report(self) -> Dict[str, Any]:
|
|
543
|
+
"""Validate configuration and return structured report with issues and suggestions.
|
|
544
|
+
|
|
545
|
+
This method performs validation checks without raising exceptions, returning
|
|
546
|
+
a structured report that can be used for debugging and user guidance.
|
|
547
|
+
|
|
548
|
+
Returns
|
|
549
|
+
-------
|
|
550
|
+
Dict[str, Any]
|
|
551
|
+
Report dictionary with keys:
|
|
552
|
+
- 'valid': bool - Whether configuration is valid
|
|
553
|
+
- 'errors': List[str] - List of error messages
|
|
554
|
+
- 'warnings': List[str] - List of warning messages
|
|
555
|
+
- 'suggestions': List[str] - List of actionable suggestions
|
|
556
|
+
|
|
557
|
+
Examples
|
|
558
|
+
--------
|
|
559
|
+
>>> config = DFMConfig(...)
|
|
560
|
+
>>> report = config.validate_and_report()
|
|
561
|
+
>>> if not report['valid']:
|
|
562
|
+
... print("Errors:", report['errors'])
|
|
563
|
+
... print("Suggestions:", report['suggestions'])
|
|
564
|
+
"""
|
|
565
|
+
from .utils import FREQUENCY_HIERARCHY
|
|
566
|
+
|
|
567
|
+
report = {
|
|
568
|
+
'valid': True,
|
|
569
|
+
'errors': [],
|
|
570
|
+
'warnings': [],
|
|
571
|
+
'suggestions': []
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# Check for empty series
|
|
575
|
+
if not self.series:
|
|
576
|
+
report['valid'] = False
|
|
577
|
+
report['errors'].append("DFM configuration must contain at least one series.")
|
|
578
|
+
report['suggestions'].append("Add series definitions to your configuration.")
|
|
579
|
+
return report
|
|
580
|
+
|
|
581
|
+
# Check for empty blocks
|
|
582
|
+
if not self.blocks:
|
|
583
|
+
report['valid'] = False
|
|
584
|
+
report['errors'].append("DFM configuration must contain at least one block.")
|
|
585
|
+
report['suggestions'].append("Add block definitions to your configuration.")
|
|
586
|
+
return report
|
|
587
|
+
|
|
588
|
+
# Check frequency constraints
|
|
589
|
+
global_clock_hierarchy = FREQUENCY_HIERARCHY.get(self.clock, 3)
|
|
590
|
+
for i, s in enumerate(self.series):
|
|
591
|
+
series_freq_hierarchy = FREQUENCY_HIERARCHY.get(s.frequency, 3)
|
|
592
|
+
|
|
593
|
+
for block_idx, loads_on_block in enumerate(s.blocks):
|
|
594
|
+
if loads_on_block == 1:
|
|
595
|
+
if block_idx < len(self.block_names):
|
|
596
|
+
block_name = self.block_names[block_idx]
|
|
597
|
+
block_cfg = self.blocks[block_name]
|
|
598
|
+
block_clock_hierarchy = FREQUENCY_HIERARCHY.get(block_cfg.clock, 3)
|
|
599
|
+
|
|
600
|
+
if series_freq_hierarchy < block_clock_hierarchy:
|
|
601
|
+
valid_freqs = [freq for freq, hier in FREQUENCY_HIERARCHY.items()
|
|
602
|
+
if hier >= block_clock_hierarchy]
|
|
603
|
+
valid_freqs_str = ', '.join(sorted(valid_freqs))
|
|
604
|
+
report['valid'] = False
|
|
605
|
+
report['errors'].append(
|
|
606
|
+
f"Series '{s.series_id}' has frequency '{s.frequency}' which is faster than "
|
|
607
|
+
f"block '{block_name}' clock '{block_cfg.clock}'."
|
|
608
|
+
)
|
|
609
|
+
report['suggestions'].append(
|
|
610
|
+
f"For series '{s.series_id}': change frequency to one of [{valid_freqs_str}], "
|
|
611
|
+
f"or set block '{block_name}' clock to '{s.frequency}' or faster."
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Check block clock constraints
|
|
615
|
+
for block_name, block_cfg in self.blocks.items():
|
|
616
|
+
block_clock_hierarchy = FREQUENCY_HIERARCHY.get(block_cfg.clock, 3)
|
|
617
|
+
if block_clock_hierarchy < global_clock_hierarchy:
|
|
618
|
+
report['valid'] = False
|
|
619
|
+
report['errors'].append(
|
|
620
|
+
f"Block '{block_name}' has clock '{block_cfg.clock}' which is faster than "
|
|
621
|
+
f"global clock '{self.clock}'."
|
|
622
|
+
)
|
|
623
|
+
report['suggestions'].append(
|
|
624
|
+
f"Change block '{block_name}' clock to '{self.clock}' or slower, "
|
|
625
|
+
f"or set global clock to '{block_cfg.clock}' or faster."
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Check factors_per_block
|
|
629
|
+
if any(f < 1 for f in self.factors_per_block):
|
|
630
|
+
invalid_blocks = [i for i, f in enumerate(self.factors_per_block) if f < 1]
|
|
631
|
+
report['valid'] = False
|
|
632
|
+
report['errors'].append(
|
|
633
|
+
f"factors_per_block must contain positive integers (>= 1). "
|
|
634
|
+
f"Invalid values found at block indices {invalid_blocks}."
|
|
635
|
+
)
|
|
636
|
+
report['suggestions'].append(
|
|
637
|
+
f"Set factors_per_block[{invalid_blocks[0]}] to at least 1 for block '{self.block_names[invalid_blocks[0]]}'."
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
return report
|
|
641
|
+
|
|
538
642
|
# Note: Legacy PascalCase properties were removed to keep the API clean and generic.
|
|
539
643
|
# Use the snake_case helper methods above.
|
|
540
644
|
|
|
@@ -567,7 +671,12 @@ class DFMConfig:
|
|
|
567
671
|
'warn_on_regularization': data.get('warn_on_regularization', True),
|
|
568
672
|
'use_damped_updates': data.get('use_damped_updates', True),
|
|
569
673
|
'damping_factor': data.get('damping_factor', 0.8),
|
|
570
|
-
'warn_on_damped_update': data.get('warn_on_damped_update', True)
|
|
674
|
+
'warn_on_damped_update': data.get('warn_on_damped_update', True),
|
|
675
|
+
# Idiosyncratic component augmentation
|
|
676
|
+
'augment_idio': data.get('augment_idio', True),
|
|
677
|
+
'augment_idio_slow': data.get('augment_idio_slow', True),
|
|
678
|
+
'idio_rho0': data.get('idio_rho0', 0.1),
|
|
679
|
+
'idio_min_var': data.get('idio_min_var', 1e-8)
|
|
571
680
|
}
|
|
572
681
|
|
|
573
682
|
@classmethod
|
|
@@ -825,536 +934,104 @@ class DFMConfig:
|
|
|
825
934
|
return cls.from_dict(cfg)
|
|
826
935
|
|
|
827
936
|
|
|
828
|
-
# Register with Hydra ConfigStore (optional - only if Hydra is available)
|
|
829
|
-
if HYDRA_AVAILABLE and ConfigStore is not None:
|
|
830
|
-
try:
|
|
831
|
-
cs = ConfigStore.instance()
|
|
832
|
-
if cs is not None:
|
|
833
|
-
from dataclasses import dataclass as schema_dataclass
|
|
834
|
-
|
|
835
|
-
@schema_dataclass
|
|
836
|
-
class SeriesConfigSchema:
|
|
837
|
-
"""Schema for SeriesConfig validation in Hydra."""
|
|
838
|
-
series_id: str
|
|
839
|
-
series_name: str
|
|
840
|
-
frequency: str
|
|
841
|
-
units: str
|
|
842
|
-
transformation: str
|
|
843
|
-
category: str
|
|
844
|
-
blocks: List[int]
|
|
845
|
-
|
|
846
|
-
@schema_dataclass
|
|
847
|
-
class DFMConfigSchema:
|
|
848
|
-
"""Schema for unified DFMConfig validation in Hydra."""
|
|
849
|
-
series: List[SeriesConfigSchema]
|
|
850
|
-
block_names: List[str]
|
|
851
|
-
factors_per_block: Optional[List[int]] = None
|
|
852
|
-
ar_lag: int = 1
|
|
853
|
-
threshold: float = 1e-5
|
|
854
|
-
max_iter: int = 5000
|
|
855
|
-
nan_method: int = 2
|
|
856
|
-
nan_k: int = 3
|
|
857
|
-
clock: str = 'm'
|
|
858
|
-
|
|
859
|
-
# Register schemas
|
|
860
|
-
cs.store(group="dfm", name="base_dfm_config", node=DFMConfigSchema)
|
|
861
|
-
cs.store(group="model", name="base_model_config", node=DFMConfigSchema)
|
|
862
|
-
cs.store(name="dfm_config_schema", node=DFMConfigSchema)
|
|
863
|
-
cs.store(name="model_config_schema", node=DFMConfigSchema)
|
|
864
|
-
|
|
865
|
-
except Exception as e:
|
|
866
|
-
warnings.warn(f"Could not register Hydra structured config schemas: {e}. "
|
|
867
|
-
f"Configs will still work via from_dict() but without schema validation.")
|
|
868
|
-
|
|
869
|
-
|
|
870
937
|
# ============================================================================
|
|
871
938
|
# Configuration Source Adapters
|
|
872
939
|
# ============================================================================
|
|
873
940
|
|
|
874
|
-
|
|
941
|
+
# Import ConfigSource classes from separate module
|
|
942
|
+
from .config_sources import (
|
|
943
|
+
ConfigSource,
|
|
944
|
+
YamlSource,
|
|
945
|
+
DictSource,
|
|
946
|
+
SpecCSVSource,
|
|
947
|
+
HydraSource,
|
|
948
|
+
MergedConfigSource,
|
|
949
|
+
make_config_source,
|
|
950
|
+
)
|
|
875
951
|
|
|
952
|
+
# Re-export for backward compatibility
|
|
953
|
+
__all__ = [
|
|
954
|
+
'DFMConfig', 'SeriesConfig', 'BlockConfig', 'Params',
|
|
955
|
+
'DEFAULT_GLOBAL_BLOCK_NAME',
|
|
956
|
+
'ConfigSource', 'YamlSource', 'DictSource', 'SpecCSVSource',
|
|
957
|
+
'HydraSource', 'MergedConfigSource', 'make_config_source',
|
|
958
|
+
]
|
|
876
959
|
|
|
877
|
-
class
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
Any object that implements a `load()` method returning a DFMConfig
|
|
881
|
-
can be used as a configuration source.
|
|
882
|
-
"""
|
|
883
|
-
def load(self) -> DFMConfig:
|
|
884
|
-
"""Load and return a DFMConfig object."""
|
|
885
|
-
...
|
|
960
|
+
# Legacy class definitions removed - now in config_sources.py
|
|
961
|
+
# For backward compatibility, classes are imported and re-exported above
|
|
886
962
|
|
|
887
963
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
Supports Hydra-style configs with defaults for series and blocks.
|
|
892
|
-
"""
|
|
893
|
-
def __init__(self, yaml_path: Union[str, Path]):
|
|
894
|
-
"""Initialize YAML source.
|
|
895
|
-
|
|
896
|
-
Parameters
|
|
897
|
-
----------
|
|
898
|
-
yaml_path : str or Path
|
|
899
|
-
Path to YAML configuration file
|
|
900
|
-
"""
|
|
901
|
-
self.yaml_path = Path(yaml_path)
|
|
902
|
-
|
|
903
|
-
def load(self) -> DFMConfig:
|
|
904
|
-
"""Load configuration from YAML file."""
|
|
905
|
-
try:
|
|
906
|
-
from omegaconf import OmegaConf
|
|
907
|
-
except ImportError:
|
|
908
|
-
raise ImportError("omegaconf is required for YAML config loading. Install with: pip install omegaconf")
|
|
909
|
-
|
|
910
|
-
configfile = Path(self.yaml_path)
|
|
911
|
-
if not configfile.exists():
|
|
912
|
-
raise FileNotFoundError(f"Configuration file not found: {configfile}")
|
|
913
|
-
|
|
914
|
-
config_dir = configfile.parent
|
|
915
|
-
cfg = OmegaConf.load(configfile)
|
|
916
|
-
cfg_dict = OmegaConf.to_container(cfg, resolve=True)
|
|
917
|
-
|
|
918
|
-
# Extract main settings (estimation parameters)
|
|
919
|
-
excluded_keys = {'defaults', '_target_', '_recursive_', '_convert_'}
|
|
920
|
-
main_settings = {k: v for k, v in cfg_dict.items() if k not in excluded_keys}
|
|
921
|
-
|
|
922
|
-
# Load series from config/series/default.yaml
|
|
923
|
-
series_list = []
|
|
924
|
-
series_loaded = False
|
|
925
|
-
series_dict = None
|
|
926
|
-
|
|
927
|
-
if 'defaults' in cfg:
|
|
928
|
-
defaults_list = cfg.defaults
|
|
929
|
-
for default_item in defaults_list:
|
|
930
|
-
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
931
|
-
|
|
932
|
-
# Handle dict format: {'series': 'default'}
|
|
933
|
-
if isinstance(default_dict, dict) and 'series' in default_dict:
|
|
934
|
-
series_config_name = default_dict['series']
|
|
935
|
-
series_config_path = config_dir / 'series' / f'{series_config_name}.yaml'
|
|
936
|
-
if series_config_path.exists():
|
|
937
|
-
series_cfg = OmegaConf.load(series_config_path)
|
|
938
|
-
series_dict = OmegaConf.to_container(series_cfg, resolve=True)
|
|
939
|
-
series_loaded = True
|
|
940
|
-
break
|
|
941
|
-
|
|
942
|
-
# If not loaded from defaults, try direct path
|
|
943
|
-
if not series_loaded:
|
|
944
|
-
series_config_path = config_dir / 'series' / 'default.yaml'
|
|
945
|
-
if series_config_path.exists():
|
|
946
|
-
series_cfg = OmegaConf.load(series_config_path)
|
|
947
|
-
series_dict = OmegaConf.to_container(series_cfg, resolve=True)
|
|
948
|
-
series_loaded = True
|
|
949
|
-
|
|
950
|
-
# Convert series dict to SeriesConfig objects
|
|
951
|
-
if series_loaded and series_dict is not None:
|
|
952
|
-
for series_id, series_data in series_dict.items():
|
|
953
|
-
if isinstance(series_data, dict):
|
|
954
|
-
series_list.append(SeriesConfig(
|
|
955
|
-
series_id=series_id,
|
|
956
|
-
series_name=series_data.get('series_name', series_id),
|
|
957
|
-
frequency=series_data.get('frequency', 'm'),
|
|
958
|
-
transformation=series_data.get('transformation', 'lin'),
|
|
959
|
-
category=series_data.get('category', ''),
|
|
960
|
-
units=series_data.get('units', ''),
|
|
961
|
-
blocks=series_data.get('blocks', [])
|
|
962
|
-
))
|
|
963
|
-
|
|
964
|
-
# If no series loaded from separate files, try to get from main config
|
|
965
|
-
if not series_loaded and 'series' in cfg_dict:
|
|
966
|
-
series_data = cfg_dict['series']
|
|
967
|
-
if isinstance(series_data, list):
|
|
968
|
-
for series_item in series_data:
|
|
969
|
-
series_list.append(SeriesConfig(**series_item))
|
|
970
|
-
elif isinstance(series_data, dict):
|
|
971
|
-
for series_id, series_item in series_data.items():
|
|
972
|
-
if isinstance(series_item, dict):
|
|
973
|
-
series_item['series_id'] = series_id
|
|
974
|
-
series_list.append(SeriesConfig(**series_item))
|
|
975
|
-
|
|
976
|
-
# Load blocks from config/blocks/default.yaml
|
|
977
|
-
blocks_dict = {}
|
|
978
|
-
blocks_loaded = False
|
|
979
|
-
blocks_dict_raw = None
|
|
980
|
-
|
|
981
|
-
if 'defaults' in cfg:
|
|
982
|
-
defaults_list = cfg.defaults
|
|
983
|
-
for default_item in defaults_list:
|
|
984
|
-
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
985
|
-
|
|
986
|
-
# Handle dict format: {'blocks': 'default'}
|
|
987
|
-
if isinstance(default_dict, dict) and 'blocks' in default_dict:
|
|
988
|
-
blocks_config_name = default_dict['blocks']
|
|
989
|
-
blocks_config_path = config_dir / 'blocks' / f'{blocks_config_name}.yaml'
|
|
990
|
-
if blocks_config_path.exists():
|
|
991
|
-
blocks_cfg = OmegaConf.load(blocks_config_path)
|
|
992
|
-
blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
|
|
993
|
-
blocks_loaded = True
|
|
994
|
-
break
|
|
995
|
-
|
|
996
|
-
# If not loaded from defaults, try direct path
|
|
997
|
-
if not blocks_loaded:
|
|
998
|
-
blocks_config_path = config_dir / 'blocks' / 'default.yaml'
|
|
999
|
-
if blocks_config_path.exists():
|
|
1000
|
-
blocks_cfg = OmegaConf.load(blocks_config_path)
|
|
1001
|
-
blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
|
|
1002
|
-
blocks_loaded = True
|
|
1003
|
-
|
|
1004
|
-
# Convert blocks dict to BlockConfig objects
|
|
1005
|
-
if blocks_loaded and blocks_dict_raw is not None:
|
|
1006
|
-
for block_name, block_data in blocks_dict_raw.items():
|
|
1007
|
-
if isinstance(block_data, dict):
|
|
1008
|
-
blocks_dict[block_name] = BlockConfig(
|
|
1009
|
-
factors=block_data.get('factors', 1),
|
|
1010
|
-
ar_lag=block_data.get('ar_lag', 1),
|
|
1011
|
-
clock=block_data.get('clock', 'm'),
|
|
1012
|
-
notes=block_data.get('notes', None)
|
|
1013
|
-
)
|
|
1014
|
-
|
|
1015
|
-
# If no blocks loaded from separate files, try to get from main config
|
|
1016
|
-
if not blocks_loaded and 'blocks' in cfg_dict:
|
|
1017
|
-
blocks_data = cfg_dict['blocks']
|
|
1018
|
-
if isinstance(blocks_data, dict):
|
|
1019
|
-
for block_name, block_item in blocks_data.items():
|
|
1020
|
-
if isinstance(block_item, dict):
|
|
1021
|
-
blocks_dict[block_name] = BlockConfig(**block_item)
|
|
1022
|
-
|
|
1023
|
-
# Ensure at least one block exists
|
|
1024
|
-
if not blocks_dict:
|
|
1025
|
-
blocks_dict[DEFAULT_GLOBAL_BLOCK_NAME] = BlockConfig(factors=1, clock=main_settings.get('clock', 'm'))
|
|
1026
|
-
|
|
1027
|
-
return DFMConfig(
|
|
1028
|
-
series=series_list,
|
|
1029
|
-
blocks=blocks_dict,
|
|
1030
|
-
**main_settings
|
|
1031
|
-
)
|
|
964
|
+
# ============================================================================
|
|
965
|
+
# Configuration validation functions
|
|
966
|
+
# ============================================================================
|
|
1032
967
|
|
|
968
|
+
# Valid frequency codes
|
|
969
|
+
_VALID_FREQUENCIES = {'d', 'w', 'm', 'q', 'sa', 'a'}
|
|
1033
970
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
- New format (list): {'series': [{'series_id': ..., ...}], ...}
|
|
1040
|
-
- Hydra format: {'series': {'series_id': {...}}, 'blocks': {...}}
|
|
1041
|
-
"""
|
|
1042
|
-
def __init__(self, mapping: Dict[str, Any]):
|
|
1043
|
-
"""Initialize dictionary source.
|
|
1044
|
-
|
|
1045
|
-
Parameters
|
|
1046
|
-
----------
|
|
1047
|
-
mapping : dict
|
|
1048
|
-
Dictionary containing configuration data
|
|
1049
|
-
"""
|
|
1050
|
-
self.mapping = mapping
|
|
1051
|
-
|
|
1052
|
-
def load(self) -> DFMConfig:
|
|
1053
|
-
"""Load configuration from dictionary.
|
|
1054
|
-
|
|
1055
|
-
If the dictionary is partial (e.g., only max_iter, threshold),
|
|
1056
|
-
it will be merged with a minimal default config.
|
|
1057
|
-
"""
|
|
1058
|
-
# Check if this is a partial config (missing series or blocks)
|
|
1059
|
-
has_series = 'series' in self.mapping and self.mapping['series']
|
|
1060
|
-
has_blocks = 'blocks' in self.mapping and self.mapping['blocks']
|
|
1061
|
-
|
|
1062
|
-
if not has_series or not has_blocks:
|
|
1063
|
-
# This is a partial config - create a minimal default and merge
|
|
1064
|
-
minimal_default = {
|
|
1065
|
-
'series': [],
|
|
1066
|
-
'blocks': {},
|
|
1067
|
-
'clock': 'm',
|
|
1068
|
-
'max_iter': 5000,
|
|
1069
|
-
'threshold': 1e-5
|
|
1070
|
-
}
|
|
1071
|
-
# Merge: mapping takes precedence
|
|
1072
|
-
merged = {**minimal_default, **self.mapping}
|
|
1073
|
-
return DFMConfig.from_dict(merged)
|
|
1074
|
-
|
|
1075
|
-
return DFMConfig.from_dict(self.mapping)
|
|
971
|
+
# Valid transformation codes
|
|
972
|
+
_VALID_TRANSFORMATIONS = {
|
|
973
|
+
'lin', 'chg', 'ch1', 'pch', 'pc1', 'pca',
|
|
974
|
+
'cch', 'cca', 'log'
|
|
975
|
+
}
|
|
1076
976
|
|
|
1077
977
|
|
|
1078
|
-
|
|
1079
|
-
"""
|
|
1080
|
-
|
|
1081
|
-
This source reads series definitions and block memberships from a CSV.
|
|
1082
|
-
It constructs a valid DFMConfig with sensible defaults for other
|
|
1083
|
-
parameters (e.g., clock='m', threshold=1e-5, max_iter=5000, factors=1).
|
|
1084
|
-
|
|
1085
|
-
For custom main settings (threshold, max_iter, clock, per-block factors),
|
|
1086
|
-
you can still merge with a base config via MergedConfigSource or
|
|
1087
|
-
dfm.load_config(base=..., override=spec_path).
|
|
1088
|
-
"""
|
|
1089
|
-
def __init__(self, spec_path: Union[str, Path]):
|
|
1090
|
-
"""Initialize spec CSV source.
|
|
1091
|
-
|
|
1092
|
-
Parameters
|
|
1093
|
-
----------
|
|
1094
|
-
spec_path : str or Path
|
|
1095
|
-
Path to spec CSV file
|
|
1096
|
-
"""
|
|
1097
|
-
self.spec_path = Path(spec_path)
|
|
978
|
+
def validate_frequency(frequency: str) -> str:
|
|
979
|
+
"""Validate frequency code.
|
|
1098
980
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
specfile = Path(self.spec_path)
|
|
1105
|
-
if not specfile.exists():
|
|
1106
|
-
raise FileNotFoundError(f"Spec file not found: {specfile}")
|
|
981
|
+
Parameters
|
|
982
|
+
----------
|
|
983
|
+
frequency : str
|
|
984
|
+
Frequency code to validate
|
|
1107
985
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
986
|
+
Returns
|
|
987
|
+
-------
|
|
988
|
+
str
|
|
989
|
+
Validated frequency code (same as input if valid)
|
|
1112
990
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
"""Load configuration from a Hydra DictConfig or dict.
|
|
1118
|
-
|
|
1119
|
-
This adapter handles Hydra's composed configuration objects,
|
|
1120
|
-
converting them to DFMConfig format.
|
|
1121
|
-
"""
|
|
1122
|
-
def __init__(self, cfg: Union[Dict[str, Any], 'DictConfig']):
|
|
1123
|
-
"""Initialize Hydra source.
|
|
991
|
+
Raises
|
|
992
|
+
------
|
|
993
|
+
ValueError
|
|
994
|
+
If frequency is not in the set of valid frequencies
|
|
1124
995
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
def load(self) -> DFMConfig:
|
|
1133
|
-
"""Load configuration from Hydra DictConfig/dict."""
|
|
1134
|
-
return DFMConfig.from_hydra(self.cfg)
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
class MergedConfigSource:
|
|
1138
|
-
"""Merge multiple configuration sources.
|
|
1139
|
-
|
|
1140
|
-
This allows combining configurations from different sources,
|
|
1141
|
-
e.g., base YAML config + series from Spec CSV.
|
|
1142
|
-
|
|
1143
|
-
The merge strategy:
|
|
1144
|
-
- Base config provides main settings (threshold, max_iter, clock, blocks)
|
|
1145
|
-
- Override config provides series definitions (replaces base series)
|
|
1146
|
-
- Block definitions are merged (override takes precedence)
|
|
996
|
+
Examples
|
|
997
|
+
--------
|
|
998
|
+
>>> validate_frequency('m')
|
|
999
|
+
'm'
|
|
1000
|
+
>>> validate_frequency('invalid')
|
|
1001
|
+
ValueError: Invalid frequency: invalid. Must be one of {'d', 'w', 'm', 'q', 'sa', 'a'}
|
|
1147
1002
|
"""
|
|
1148
|
-
|
|
1149
|
-
"
|
|
1150
|
-
|
|
1151
|
-
Parameters
|
|
1152
|
-
----------
|
|
1153
|
-
base : ConfigSource
|
|
1154
|
-
Base configuration (provides main settings)
|
|
1155
|
-
override : ConfigSource
|
|
1156
|
-
Override configuration (provides series/block overrides)
|
|
1157
|
-
"""
|
|
1158
|
-
self.base = base
|
|
1159
|
-
self.override = override
|
|
1160
|
-
|
|
1161
|
-
def load(self) -> DFMConfig:
|
|
1162
|
-
"""Load and merge configurations."""
|
|
1163
|
-
from dataclasses import fields
|
|
1164
|
-
|
|
1165
|
-
base_cfg = self.base.load()
|
|
1166
|
-
|
|
1167
|
-
# Check if override is a partial config (DictSource with partial dict)
|
|
1168
|
-
override_is_partial = False
|
|
1169
|
-
if isinstance(self.override, DictSource):
|
|
1170
|
-
has_series = 'series' in self.override.mapping and self.override.mapping['series']
|
|
1171
|
-
has_blocks = 'blocks' in self.override.mapping and self.override.mapping['blocks']
|
|
1172
|
-
override_is_partial = not (has_series and has_blocks)
|
|
1173
|
-
|
|
1174
|
-
if override_is_partial:
|
|
1175
|
-
# Handle partial override: merge fields directly without loading full DFMConfig
|
|
1176
|
-
override_dict = self.override.mapping
|
|
1177
|
-
override_cfg = base_cfg # Use base as template
|
|
1178
|
-
else:
|
|
1179
|
-
override_cfg = self.override.load()
|
|
1180
|
-
|
|
1181
|
-
# Merge blocks: override takes precedence
|
|
1182
|
-
if override_is_partial and 'blocks' in override_dict:
|
|
1183
|
-
# Merge block dicts
|
|
1184
|
-
merged_blocks = {**base_cfg.blocks, **override_dict['blocks']}
|
|
1185
|
-
else:
|
|
1186
|
-
merged_blocks = {**base_cfg.blocks, **override_cfg.blocks}
|
|
1187
|
-
|
|
1188
|
-
# Use override's series if provided and non-empty, otherwise use base's series
|
|
1189
|
-
if override_is_partial:
|
|
1190
|
-
if 'series' in override_dict and override_dict['series']:
|
|
1191
|
-
merged_series = override_dict['series']
|
|
1192
|
-
else:
|
|
1193
|
-
merged_series = base_cfg.series
|
|
1194
|
-
else:
|
|
1195
|
-
merged_series = override_cfg.series if (override_cfg.series and len(override_cfg.series) > 0) else base_cfg.series
|
|
1196
|
-
|
|
1197
|
-
# Get all config fields (excluding derived/computed fields)
|
|
1198
|
-
excluded_fields = {'series', 'blocks', 'block_names', 'factors_per_block', '_cached_blocks'}
|
|
1199
|
-
base_settings = {
|
|
1200
|
-
field.name: getattr(base_cfg, field.name)
|
|
1201
|
-
for field in fields(DFMConfig)
|
|
1202
|
-
if field.name not in excluded_fields
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
# Override settings from override_cfg or override_dict
|
|
1206
|
-
if override_is_partial:
|
|
1207
|
-
override_settings = {
|
|
1208
|
-
field.name: override_dict.get(field.name, getattr(base_cfg, field.name))
|
|
1209
|
-
for field in fields(DFMConfig)
|
|
1210
|
-
if field.name not in excluded_fields
|
|
1211
|
-
}
|
|
1212
|
-
else:
|
|
1213
|
-
override_settings = {
|
|
1214
|
-
field.name: getattr(override_cfg, field.name)
|
|
1215
|
-
for field in fields(DFMConfig)
|
|
1216
|
-
if field.name not in excluded_fields
|
|
1217
|
-
and hasattr(override_cfg, field.name)
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
# Merge settings: base + override (override takes precedence)
|
|
1221
|
-
merged_settings = {**base_settings, **override_settings}
|
|
1222
|
-
|
|
1223
|
-
# Create merged config: merged settings + merged series + merged blocks
|
|
1224
|
-
return DFMConfig(
|
|
1225
|
-
series=merged_series,
|
|
1226
|
-
blocks=merged_blocks,
|
|
1227
|
-
**merged_settings
|
|
1228
|
-
)
|
|
1003
|
+
if frequency not in _VALID_FREQUENCIES:
|
|
1004
|
+
raise ValueError(f"Invalid frequency: {frequency}. Must be one of {_VALID_FREQUENCIES}")
|
|
1005
|
+
return frequency
|
|
1229
1006
|
|
|
1230
1007
|
|
|
1231
|
-
def
|
|
1232
|
-
|
|
1233
|
-
*,
|
|
1234
|
-
yaml: Optional[Union[str, Path]] = None,
|
|
1235
|
-
mapping: Optional[Union[Dict[str, Any], Any]] = None,
|
|
1236
|
-
spec: Optional[Union[str, Path]] = None,
|
|
1237
|
-
hydra: Optional[Union[Dict[str, Any], 'DictConfig']] = None,
|
|
1238
|
-
) -> ConfigSource:
|
|
1239
|
-
"""Create a ConfigSource adapter from various input formats.
|
|
1240
|
-
|
|
1241
|
-
This factory function automatically selects the appropriate adapter
|
|
1242
|
-
based on the input type or explicit keyword arguments.
|
|
1008
|
+
def validate_transformation(transformation: str) -> str:
|
|
1009
|
+
"""Validate transformation code.
|
|
1243
1010
|
|
|
1244
1011
|
Parameters
|
|
1245
1012
|
----------
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
If str/Path, treated as YAML file path.
|
|
1249
|
-
If dict, treated as dictionary config.
|
|
1250
|
-
yaml : str or Path, optional
|
|
1251
|
-
Explicit YAML file path
|
|
1252
|
-
mapping : dict, optional
|
|
1253
|
-
Explicit dictionary config
|
|
1254
|
-
spec : str or Path, optional
|
|
1255
|
-
Explicit spec CSV file path
|
|
1256
|
-
hydra : DictConfig or dict, optional
|
|
1257
|
-
Explicit Hydra config
|
|
1013
|
+
transformation : str
|
|
1014
|
+
Transformation code to validate
|
|
1258
1015
|
|
|
1259
1016
|
Returns
|
|
1260
1017
|
-------
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1018
|
+
str
|
|
1019
|
+
Validated transformation code (same as input, even if unknown)
|
|
1020
|
+
|
|
1021
|
+
Notes
|
|
1022
|
+
-----
|
|
1023
|
+
Unknown transformation codes trigger a warning but are not rejected,
|
|
1024
|
+
allowing for extensibility. The code will be used as-is, and the
|
|
1025
|
+
transformation logic should handle unknown codes appropriately.
|
|
1026
|
+
|
|
1264
1027
|
Examples
|
|
1265
1028
|
--------
|
|
1266
|
-
>>>
|
|
1267
|
-
|
|
1268
|
-
>>>
|
|
1269
|
-
|
|
1270
|
-
>>> source = make_config_source({'series': [...], 'clock': 'm'})
|
|
1271
|
-
>>>
|
|
1272
|
-
>>> # Explicit keyword arguments
|
|
1273
|
-
>>> source = make_config_source(yaml='config/default.yaml')
|
|
1274
|
-
>>> source = make_config_source(spec='data/spec.csv')
|
|
1275
|
-
>>>
|
|
1276
|
-
>>> # Merge YAML base + Spec CSV series
|
|
1277
|
-
>>> base = make_config_source(yaml='config/default.yaml')
|
|
1278
|
-
>>> override = make_config_source(spec='data/spec.csv')
|
|
1279
|
-
>>> merged = MergedConfigSource(base, override)
|
|
1029
|
+
>>> validate_transformation('lin')
|
|
1030
|
+
'lin'
|
|
1031
|
+
>>> validate_transformation('unknown') # Issues warning but returns value
|
|
1032
|
+
'unknown'
|
|
1280
1033
|
"""
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
f"Only one of yaml, mapping, spec, or hydra can be specified. "
|
|
1286
|
-
f"Got: {', '.join(explicit_kwargs)}. "
|
|
1287
|
-
f"For merging configs, use MergedConfigSource."
|
|
1288
|
-
)
|
|
1289
|
-
|
|
1290
|
-
# Helper: coerce arbitrary object with attributes into dict
|
|
1291
|
-
def _coerce_to_mapping(obj: Any) -> Dict[str, Any]:
|
|
1292
|
-
if isinstance(obj, dict):
|
|
1293
|
-
return obj
|
|
1294
|
-
if is_dataclass(obj):
|
|
1295
|
-
return asdict(obj)
|
|
1296
|
-
if hasattr(obj, "__dict__"):
|
|
1297
|
-
try:
|
|
1298
|
-
return dict(vars(obj))
|
|
1299
|
-
except Exception:
|
|
1300
|
-
pass
|
|
1301
|
-
raise TypeError(
|
|
1302
|
-
f"Unsupported mapping type: {type(obj)}. "
|
|
1303
|
-
f"Provide a dict, dataclass instance, or an object with attributes."
|
|
1304
|
-
)
|
|
1305
|
-
|
|
1306
|
-
# Handle explicit keyword arguments
|
|
1307
|
-
if yaml is not None:
|
|
1308
|
-
return YamlSource(yaml)
|
|
1309
|
-
if mapping is not None:
|
|
1310
|
-
return DictSource(_coerce_to_mapping(mapping))
|
|
1311
|
-
if spec is not None:
|
|
1312
|
-
return SpecCSVSource(spec)
|
|
1313
|
-
if hydra is not None:
|
|
1314
|
-
return HydraSource(hydra)
|
|
1315
|
-
|
|
1316
|
-
# Infer from source argument
|
|
1317
|
-
if source is None:
|
|
1318
|
-
raise ValueError(
|
|
1319
|
-
"No configuration source provided. "
|
|
1320
|
-
"Specify source, yaml, mapping, spec, or hydra."
|
|
1321
|
-
)
|
|
1322
|
-
|
|
1323
|
-
# If already a ConfigSource, return as-is
|
|
1324
|
-
if hasattr(source, 'load') and callable(getattr(source, 'load')):
|
|
1325
|
-
return source # type: ignore
|
|
1326
|
-
|
|
1327
|
-
# Infer type from source
|
|
1328
|
-
if isinstance(source, DFMConfig):
|
|
1329
|
-
# Wrap DFMConfig in a simple adapter
|
|
1330
|
-
class DFMConfigAdapter:
|
|
1331
|
-
def __init__(self, cfg: DFMConfig):
|
|
1332
|
-
self._cfg = cfg
|
|
1333
|
-
def load(self) -> DFMConfig:
|
|
1334
|
-
return self._cfg
|
|
1335
|
-
return DFMConfigAdapter(source)
|
|
1336
|
-
|
|
1337
|
-
if isinstance(source, (str, Path)):
|
|
1338
|
-
path = Path(source)
|
|
1339
|
-
suffix = path.suffix.lower()
|
|
1340
|
-
if suffix in ['.yaml', '.yml']:
|
|
1341
|
-
return YamlSource(path)
|
|
1342
|
-
elif suffix == '.csv':
|
|
1343
|
-
return SpecCSVSource(path)
|
|
1344
|
-
else:
|
|
1345
|
-
# Default to YAML if extension unclear
|
|
1346
|
-
return YamlSource(path)
|
|
1347
|
-
|
|
1348
|
-
if isinstance(source, dict):
|
|
1349
|
-
return DictSource(source)
|
|
1350
|
-
# Accept objects that can be coerced into dict (dataclass or attribute bag)
|
|
1351
|
-
try:
|
|
1352
|
-
coerced = _coerce_to_mapping(source)
|
|
1353
|
-
return DictSource(coerced)
|
|
1354
|
-
except Exception:
|
|
1355
|
-
pass
|
|
1356
|
-
|
|
1357
|
-
raise TypeError(
|
|
1358
|
-
f"Unsupported source type: {type(source)}. "
|
|
1359
|
-
f"Expected str, Path, dict, ConfigSource, or DFMConfig."
|
|
1360
|
-
)
|
|
1034
|
+
import warnings
|
|
1035
|
+
if transformation not in _VALID_TRANSFORMATIONS:
|
|
1036
|
+
warnings.warn(f"Unknown transformation code: {transformation}. Will use untransformed data.")
|
|
1037
|
+
return transformation
|