dfm-python 0.2.7__py3-none-any.whl → 0.2.9__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 +6 -29
- dfm_python/api.py +38 -19
- dfm_python/config.py +13 -14
- dfm_python/config_sources.py +354 -59
- dfm_python/core/diagnostics.py +215 -6
- dfm_python/core/em.py +49 -49
- dfm_python/core/helpers.py +76 -0
- dfm_python/core/numeric.py +7 -7
- dfm_python/data.py +1 -51
- dfm_python/dfm.py +4 -1
- dfm_python/news.py +14 -22
- dfm_python/utils.py +4 -7
- {dfm_python-0.2.7.dist-info → dfm_python-0.2.9.dist-info}/METADATA +4 -10
- dfm_python-0.2.9.dist-info/RECORD +18 -0
- dfm_python-0.2.7.dist-info/RECORD +0 -18
- {dfm_python-0.2.7.dist-info → dfm_python-0.2.9.dist-info}/WHEEL +0 -0
- {dfm_python-0.2.7.dist-info → dfm_python-0.2.9.dist-info}/licenses/LICENSE +0 -0
dfm_python/config_sources.py
CHANGED
|
@@ -10,11 +10,62 @@ This module provides adapters for loading DFMConfig from various sources:
|
|
|
10
10
|
All adapters implement the ConfigSource protocol and return DFMConfig objects.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
import warnings
|
|
14
|
+
from typing import Protocol, Optional, Dict, Any, Union, Tuple
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from dataclasses import is_dataclass, asdict
|
|
16
17
|
|
|
17
18
|
from .config import DFMConfig, SeriesConfig, BlockConfig, DEFAULT_GLOBAL_BLOCK_NAME
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_config_from_defaults(cfg, root_config_dir, config_type: str) -> Optional[dict]:
|
|
23
|
+
"""Load config from defaults or direct path (helper for series/blocks loading).
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
cfg : OmegaConf.DictConfig
|
|
28
|
+
Main config object
|
|
29
|
+
root_config_dir : Path
|
|
30
|
+
Root config directory (contains series/ and blocks/ subdirectories)
|
|
31
|
+
config_type : str
|
|
32
|
+
Type of config to load: 'series' or 'blocks'
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
Optional[dict]
|
|
37
|
+
Loaded config dict or None if not found
|
|
38
|
+
"""
|
|
39
|
+
from omegaconf import OmegaConf
|
|
40
|
+
|
|
41
|
+
config_dict = None
|
|
42
|
+
config_loaded = False
|
|
43
|
+
|
|
44
|
+
# Try loading from defaults
|
|
45
|
+
if 'defaults' in cfg:
|
|
46
|
+
defaults_list = cfg.defaults
|
|
47
|
+
for default_item in defaults_list:
|
|
48
|
+
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
49
|
+
|
|
50
|
+
# Handle dict format: {'series': 'default'} or {'blocks': 'default'}
|
|
51
|
+
if isinstance(default_dict, dict) and config_type in default_dict:
|
|
52
|
+
config_name = default_dict[config_type]
|
|
53
|
+
config_path = root_config_dir / config_type / f'{config_name}.yaml'
|
|
54
|
+
if config_path.exists():
|
|
55
|
+
config_cfg = OmegaConf.load(config_path)
|
|
56
|
+
config_dict = OmegaConf.to_container(config_cfg, resolve=True)
|
|
57
|
+
config_loaded = True
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
# If not loaded from defaults, try direct path
|
|
61
|
+
if not config_loaded:
|
|
62
|
+
config_path = root_config_dir / config_type / 'default.yaml'
|
|
63
|
+
if config_path.exists():
|
|
64
|
+
config_cfg = OmegaConf.load(config_path)
|
|
65
|
+
config_dict = OmegaConf.to_container(config_cfg, resolve=True)
|
|
66
|
+
config_loaded = True
|
|
67
|
+
|
|
68
|
+
return config_dict if config_loaded else None
|
|
18
69
|
|
|
19
70
|
|
|
20
71
|
class ConfigSource(Protocol):
|
|
@@ -55,6 +106,14 @@ class YamlSource:
|
|
|
55
106
|
raise FileNotFoundError(f"Configuration file not found: {configfile}")
|
|
56
107
|
|
|
57
108
|
config_dir = configfile.parent
|
|
109
|
+
# If config file is in a subdirectory (e.g., experiment/), find the root config directory
|
|
110
|
+
# Look for series/ or blocks/ directories to identify config root
|
|
111
|
+
root_config_dir = config_dir
|
|
112
|
+
while root_config_dir.parent != root_config_dir: # Not at filesystem root
|
|
113
|
+
if (root_config_dir / 'series').exists() or (root_config_dir / 'blocks').exists():
|
|
114
|
+
break
|
|
115
|
+
root_config_dir = root_config_dir.parent
|
|
116
|
+
|
|
58
117
|
cfg = OmegaConf.load(configfile)
|
|
59
118
|
cfg_dict = OmegaConf.to_container(cfg, resolve=True)
|
|
60
119
|
|
|
@@ -64,44 +123,28 @@ class YamlSource:
|
|
|
64
123
|
|
|
65
124
|
# Load series from config/series/default.yaml
|
|
66
125
|
series_list = []
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if 'defaults' in cfg:
|
|
71
|
-
defaults_list = cfg.defaults
|
|
72
|
-
for default_item in defaults_list:
|
|
73
|
-
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
74
|
-
|
|
75
|
-
# Handle dict format: {'series': 'default'}
|
|
76
|
-
if isinstance(default_dict, dict) and 'series' in default_dict:
|
|
77
|
-
series_config_name = default_dict['series']
|
|
78
|
-
series_config_path = config_dir / 'series' / f'{series_config_name}.yaml'
|
|
79
|
-
if series_config_path.exists():
|
|
80
|
-
series_cfg = OmegaConf.load(series_config_path)
|
|
81
|
-
series_dict = OmegaConf.to_container(series_cfg, resolve=True)
|
|
82
|
-
series_loaded = True
|
|
83
|
-
break
|
|
84
|
-
|
|
85
|
-
# If not loaded from defaults, try direct path
|
|
86
|
-
if not series_loaded:
|
|
87
|
-
series_config_path = config_dir / 'series' / 'default.yaml'
|
|
88
|
-
if series_config_path.exists():
|
|
89
|
-
series_cfg = OmegaConf.load(series_config_path)
|
|
90
|
-
series_dict = OmegaConf.to_container(series_cfg, resolve=True)
|
|
91
|
-
series_loaded = True
|
|
126
|
+
series_dict = _load_config_from_defaults(cfg, root_config_dir, 'series')
|
|
127
|
+
series_loaded = series_dict is not None
|
|
92
128
|
|
|
93
129
|
# Convert series dict to SeriesConfig objects
|
|
94
130
|
if series_loaded and series_dict is not None:
|
|
95
131
|
for series_id, series_data in series_dict.items():
|
|
96
132
|
if isinstance(series_data, dict):
|
|
133
|
+
# Parse release_date if available
|
|
134
|
+
release_date = series_data.get('release', series_data.get('release_date', None))
|
|
135
|
+
if release_date is not None:
|
|
136
|
+
try:
|
|
137
|
+
release_date = int(release_date)
|
|
138
|
+
except (ValueError, TypeError):
|
|
139
|
+
release_date = None
|
|
140
|
+
|
|
97
141
|
series_list.append(SeriesConfig(
|
|
98
142
|
series_id=series_id,
|
|
99
143
|
series_name=series_data.get('series_name', series_id),
|
|
100
144
|
frequency=series_data.get('frequency', 'm'),
|
|
101
145
|
transformation=series_data.get('transformation', 'lin'),
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
blocks=series_data.get('blocks', [])
|
|
146
|
+
blocks=series_data.get('blocks', []),
|
|
147
|
+
release_date=release_date
|
|
105
148
|
))
|
|
106
149
|
|
|
107
150
|
# If no series loaded from separate files, try to get from main config
|
|
@@ -118,31 +161,8 @@ class YamlSource:
|
|
|
118
161
|
|
|
119
162
|
# Load blocks from config/blocks/default.yaml
|
|
120
163
|
blocks_dict = {}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if 'defaults' in cfg:
|
|
125
|
-
defaults_list = cfg.defaults
|
|
126
|
-
for default_item in defaults_list:
|
|
127
|
-
default_dict = OmegaConf.to_container(default_item, resolve=False) if hasattr(default_item, 'keys') else default_item
|
|
128
|
-
|
|
129
|
-
# Handle dict format: {'blocks': 'default'}
|
|
130
|
-
if isinstance(default_dict, dict) and 'blocks' in default_dict:
|
|
131
|
-
blocks_config_name = default_dict['blocks']
|
|
132
|
-
blocks_config_path = config_dir / 'blocks' / f'{blocks_config_name}.yaml'
|
|
133
|
-
if blocks_config_path.exists():
|
|
134
|
-
blocks_cfg = OmegaConf.load(blocks_config_path)
|
|
135
|
-
blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
|
|
136
|
-
blocks_loaded = True
|
|
137
|
-
break
|
|
138
|
-
|
|
139
|
-
# If not loaded from defaults, try direct path
|
|
140
|
-
if not blocks_loaded:
|
|
141
|
-
blocks_config_path = config_dir / 'blocks' / 'default.yaml'
|
|
142
|
-
if blocks_config_path.exists():
|
|
143
|
-
blocks_cfg = OmegaConf.load(blocks_config_path)
|
|
144
|
-
blocks_dict_raw = OmegaConf.to_container(blocks_cfg, resolve=True)
|
|
145
|
-
blocks_loaded = True
|
|
164
|
+
blocks_dict_raw = _load_config_from_defaults(cfg, root_config_dir, 'blocks')
|
|
165
|
+
blocks_loaded = blocks_dict_raw is not None
|
|
146
166
|
|
|
147
167
|
# Convert blocks dict to BlockConfig objects
|
|
148
168
|
if blocks_loaded and blocks_dict_raw is not None:
|
|
@@ -241,9 +261,6 @@ class SpecCSVSource:
|
|
|
241
261
|
|
|
242
262
|
def load(self) -> DFMConfig:
|
|
243
263
|
"""Load series definitions from spec CSV."""
|
|
244
|
-
import pandas as pd
|
|
245
|
-
from .data import _load_config_from_dataframe
|
|
246
|
-
|
|
247
264
|
specfile = Path(self.spec_path)
|
|
248
265
|
if not specfile.exists():
|
|
249
266
|
raise FileNotFoundError(f"Spec file not found: {specfile}")
|
|
@@ -371,6 +388,190 @@ class MergedConfigSource:
|
|
|
371
388
|
)
|
|
372
389
|
|
|
373
390
|
|
|
391
|
+
def from_spec(
|
|
392
|
+
csv_path: Union[str, Path],
|
|
393
|
+
output_dir: Optional[Union[str, Path]] = None,
|
|
394
|
+
series_filename: Optional[str] = None,
|
|
395
|
+
blocks_filename: Optional[str] = None
|
|
396
|
+
) -> Tuple[Path, Path]:
|
|
397
|
+
"""Convert spec CSV file to YAML configuration files.
|
|
398
|
+
|
|
399
|
+
This function reads a spec CSV file and generates two YAML files:
|
|
400
|
+
- config/series/{basename}.yaml - series definitions
|
|
401
|
+
- config/blocks/{basename}.yaml - block definitions
|
|
402
|
+
|
|
403
|
+
Parameters
|
|
404
|
+
----------
|
|
405
|
+
csv_path : str or Path
|
|
406
|
+
Path to the spec CSV file
|
|
407
|
+
output_dir : str or Path, optional
|
|
408
|
+
Output directory for YAML files. Defaults to config/ directory relative to CSV.
|
|
409
|
+
series_filename : str, optional
|
|
410
|
+
Custom filename for series YAML (without .yaml extension).
|
|
411
|
+
Defaults to CSV basename.
|
|
412
|
+
blocks_filename : str, optional
|
|
413
|
+
Custom filename for blocks YAML (without .yaml extension).
|
|
414
|
+
Defaults to CSV basename.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
Tuple[Path, Path]
|
|
419
|
+
Paths to generated series YAML and blocks YAML files
|
|
420
|
+
|
|
421
|
+
Examples
|
|
422
|
+
--------
|
|
423
|
+
>>> series_path, blocks_path = from_spec('data/sample_spec.csv')
|
|
424
|
+
>>> # Creates config/series/sample_spec.yaml and config/blocks/sample_spec.yaml
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
csv_path = Path(csv_path)
|
|
428
|
+
if not csv_path.exists():
|
|
429
|
+
raise FileNotFoundError(f"CSV file not found: {csv_path}")
|
|
430
|
+
|
|
431
|
+
# Determine output directory
|
|
432
|
+
if output_dir is None:
|
|
433
|
+
# Default: config/ directory relative to CSV file's parent
|
|
434
|
+
output_dir = csv_path.parent.parent / 'config'
|
|
435
|
+
else:
|
|
436
|
+
output_dir = Path(output_dir)
|
|
437
|
+
|
|
438
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
439
|
+
|
|
440
|
+
# Determine filenames
|
|
441
|
+
csv_basename = csv_path.stem
|
|
442
|
+
series_filename = series_filename or csv_basename
|
|
443
|
+
blocks_filename = blocks_filename or csv_basename
|
|
444
|
+
|
|
445
|
+
# Read CSV and convert to DFMConfig
|
|
446
|
+
try:
|
|
447
|
+
df = pd.read_csv(csv_path)
|
|
448
|
+
except Exception as e:
|
|
449
|
+
raise ValueError(f"Failed to read CSV file {csv_path}: {e}")
|
|
450
|
+
|
|
451
|
+
config = _load_config_from_dataframe(df)
|
|
452
|
+
|
|
453
|
+
# Prepare series YAML content
|
|
454
|
+
series_dir = output_dir / 'series'
|
|
455
|
+
series_dir.mkdir(parents=True, exist_ok=True)
|
|
456
|
+
series_path = series_dir / f'{series_filename}.yaml'
|
|
457
|
+
|
|
458
|
+
series_dict = {}
|
|
459
|
+
# Get block names in order (from config.block_names)
|
|
460
|
+
block_names = config.block_names
|
|
461
|
+
|
|
462
|
+
for series in config.series:
|
|
463
|
+
# Convert blocks from indices to block names if needed
|
|
464
|
+
if isinstance(series.blocks, list) and len(series.blocks) > 0:
|
|
465
|
+
if isinstance(series.blocks[0], int):
|
|
466
|
+
# Blocks are indices - convert to block names
|
|
467
|
+
blocks_names = [block_names[i] for i, val in enumerate(series.blocks) if val != 0]
|
|
468
|
+
else:
|
|
469
|
+
# Blocks are already names
|
|
470
|
+
blocks_names = list(series.blocks)
|
|
471
|
+
else:
|
|
472
|
+
blocks_names = ['Block_Global'] # Default
|
|
473
|
+
|
|
474
|
+
series_entry = {
|
|
475
|
+
'series_name': series.series_name,
|
|
476
|
+
'frequency': series.frequency,
|
|
477
|
+
'transformation': series.transformation,
|
|
478
|
+
'blocks': blocks_names
|
|
479
|
+
}
|
|
480
|
+
if series.release_date is not None:
|
|
481
|
+
series_entry['release'] = series.release_date
|
|
482
|
+
series_dict[series.series_id] = series_entry
|
|
483
|
+
|
|
484
|
+
# Prepare blocks YAML content
|
|
485
|
+
blocks_dir = output_dir / 'blocks'
|
|
486
|
+
blocks_dir.mkdir(parents=True, exist_ok=True)
|
|
487
|
+
blocks_path = blocks_dir / f'{blocks_filename}.yaml'
|
|
488
|
+
|
|
489
|
+
blocks_dict = {}
|
|
490
|
+
for block_name, block_config in config.blocks.items():
|
|
491
|
+
block_entry = {
|
|
492
|
+
'factors': block_config.factors,
|
|
493
|
+
'ar_lag': block_config.ar_lag,
|
|
494
|
+
'clock': block_config.clock
|
|
495
|
+
}
|
|
496
|
+
if block_config.notes:
|
|
497
|
+
block_entry['notes'] = block_config.notes
|
|
498
|
+
blocks_dict[block_name] = block_entry
|
|
499
|
+
|
|
500
|
+
# Write YAML files
|
|
501
|
+
try:
|
|
502
|
+
try:
|
|
503
|
+
import yaml
|
|
504
|
+
yaml_lib = yaml
|
|
505
|
+
yaml_kwargs = {'default_flow_style': False, 'sort_keys': False, 'allow_unicode': True}
|
|
506
|
+
except ImportError:
|
|
507
|
+
try:
|
|
508
|
+
from omegaconf import OmegaConf
|
|
509
|
+
yaml_lib = OmegaConf
|
|
510
|
+
yaml_kwargs = {}
|
|
511
|
+
except ImportError:
|
|
512
|
+
raise ImportError(
|
|
513
|
+
"Either PyYAML or omegaconf is required for YAML generation. "
|
|
514
|
+
"Install with: pip install pyyaml or pip install omegaconf"
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Write series YAML
|
|
518
|
+
if hasattr(yaml_lib, 'dump'):
|
|
519
|
+
# PyYAML - add blank lines between series for better readability
|
|
520
|
+
import io
|
|
521
|
+
stream = io.StringIO()
|
|
522
|
+
yaml_lib.dump(series_dict, stream, **yaml_kwargs)
|
|
523
|
+
yaml_content = stream.getvalue()
|
|
524
|
+
|
|
525
|
+
# Add blank line before each series key (except the first one)
|
|
526
|
+
lines = yaml_content.split('\n')
|
|
527
|
+
formatted_lines = []
|
|
528
|
+
for i, line in enumerate(lines):
|
|
529
|
+
# If line starts a new series (starts with a key and colon, not indented)
|
|
530
|
+
if line and not line.startswith(' ') and ':' in line and not line.strip().startswith('#'):
|
|
531
|
+
# Add blank line before this line (except for the first series)
|
|
532
|
+
if i > 0 and formatted_lines and formatted_lines[-1].strip():
|
|
533
|
+
formatted_lines.append('')
|
|
534
|
+
formatted_lines.append(line)
|
|
535
|
+
|
|
536
|
+
with open(series_path, 'w', encoding='utf-8') as f:
|
|
537
|
+
f.write('\n'.join(formatted_lines))
|
|
538
|
+
else:
|
|
539
|
+
# OmegaConf
|
|
540
|
+
series_cfg = OmegaConf.create(series_dict)
|
|
541
|
+
OmegaConf.save(series_cfg, series_path)
|
|
542
|
+
|
|
543
|
+
# Write blocks YAML
|
|
544
|
+
if hasattr(yaml_lib, 'dump'):
|
|
545
|
+
# PyYAML - add blank lines between blocks for better readability
|
|
546
|
+
import io
|
|
547
|
+
stream = io.StringIO()
|
|
548
|
+
yaml_lib.dump(blocks_dict, stream, **yaml_kwargs)
|
|
549
|
+
yaml_content = stream.getvalue()
|
|
550
|
+
|
|
551
|
+
# Add blank line before each block key (except the first one)
|
|
552
|
+
lines = yaml_content.split('\n')
|
|
553
|
+
formatted_lines = []
|
|
554
|
+
for i, line in enumerate(lines):
|
|
555
|
+
# If line starts a new block (starts with a key and colon, not indented)
|
|
556
|
+
if line and not line.startswith(' ') and ':' in line and not line.strip().startswith('#'):
|
|
557
|
+
# Add blank line before this line (except for the first block)
|
|
558
|
+
if i > 0 and formatted_lines and formatted_lines[-1].strip():
|
|
559
|
+
formatted_lines.append('')
|
|
560
|
+
formatted_lines.append(line)
|
|
561
|
+
|
|
562
|
+
with open(blocks_path, 'w', encoding='utf-8') as f:
|
|
563
|
+
f.write('\n'.join(formatted_lines))
|
|
564
|
+
else:
|
|
565
|
+
# OmegaConf
|
|
566
|
+
blocks_cfg = OmegaConf.create(blocks_dict)
|
|
567
|
+
OmegaConf.save(blocks_cfg, blocks_path)
|
|
568
|
+
|
|
569
|
+
except Exception as e:
|
|
570
|
+
raise RuntimeError(f"Failed to write YAML files: {e}")
|
|
571
|
+
|
|
572
|
+
return series_path, blocks_path
|
|
573
|
+
|
|
574
|
+
|
|
374
575
|
def make_config_source(
|
|
375
576
|
source: Optional[Union[str, Path, Dict[str, Any], ConfigSource]] = None,
|
|
376
577
|
*,
|
|
@@ -503,6 +704,101 @@ def make_config_source(
|
|
|
503
704
|
)
|
|
504
705
|
|
|
505
706
|
|
|
707
|
+
# ============================================================================
|
|
708
|
+
# Internal helper: Load config from DataFrame
|
|
709
|
+
# ============================================================================
|
|
710
|
+
|
|
711
|
+
def _load_config_from_dataframe(df: pd.DataFrame) -> DFMConfig:
|
|
712
|
+
"""Load configuration from DataFrame (internal helper).
|
|
713
|
+
|
|
714
|
+
This function converts a DataFrame with series specifications into a DFMConfig.
|
|
715
|
+
It's used internally by SpecCSVSource and other config loaders.
|
|
716
|
+
|
|
717
|
+
Parameters
|
|
718
|
+
----------
|
|
719
|
+
df : pd.DataFrame
|
|
720
|
+
DataFrame with columns: series_id, series_name, frequency, transformation, blocks
|
|
721
|
+
Optional columns: Release (release date)
|
|
722
|
+
Block_* columns: Binary indicators for block membership
|
|
723
|
+
|
|
724
|
+
Returns
|
|
725
|
+
-------
|
|
726
|
+
DFMConfig
|
|
727
|
+
Model configuration object
|
|
728
|
+
"""
|
|
729
|
+
series_list = []
|
|
730
|
+
block_names_set = set()
|
|
731
|
+
|
|
732
|
+
for _, row in df.iterrows():
|
|
733
|
+
# Parse blocks from Block_* columns or 'blocks' column
|
|
734
|
+
blocks = []
|
|
735
|
+
|
|
736
|
+
# First, try to find Block_* columns (CSV format)
|
|
737
|
+
block_cols = [col for col in df.columns
|
|
738
|
+
if (col.startswith('Block_') or col.startswith('Block-'))
|
|
739
|
+
and col not in ['blocks']]
|
|
740
|
+
|
|
741
|
+
if block_cols:
|
|
742
|
+
# CSV format: Block_Global, Block_Consumption, etc. columns with 1/0 values
|
|
743
|
+
for block_col in block_cols:
|
|
744
|
+
block_value = row.get(block_col, 0)
|
|
745
|
+
# Handle various types: int, float, numpy int/float, bool, string "1"/"0"
|
|
746
|
+
# Check numpy types first (numpy.int64, numpy.float64, etc.)
|
|
747
|
+
if hasattr(block_value, 'item'):
|
|
748
|
+
# numpy scalar - convert to Python native type
|
|
749
|
+
block_value = block_value.item()
|
|
750
|
+
|
|
751
|
+
if isinstance(block_value, (int, float)) and block_value != 0:
|
|
752
|
+
blocks.append(block_col)
|
|
753
|
+
elif isinstance(block_value, bool) and block_value:
|
|
754
|
+
blocks.append(block_col)
|
|
755
|
+
elif isinstance(block_value, str) and block_value.strip() in ['1', 'True', 'true']:
|
|
756
|
+
blocks.append(block_col)
|
|
757
|
+
else:
|
|
758
|
+
# Fallback: try 'blocks' column (comma-separated string or list)
|
|
759
|
+
blocks_str = row.get('blocks', 'Block_Global')
|
|
760
|
+
if isinstance(blocks_str, str):
|
|
761
|
+
blocks = [b.strip() for b in blocks_str.split(',')]
|
|
762
|
+
elif isinstance(blocks_str, list):
|
|
763
|
+
blocks = blocks_str
|
|
764
|
+
else:
|
|
765
|
+
blocks = ['Block_Global']
|
|
766
|
+
|
|
767
|
+
# Ensure at least Block_Global
|
|
768
|
+
if not blocks:
|
|
769
|
+
blocks = ['Block_Global']
|
|
770
|
+
|
|
771
|
+
# Track block names
|
|
772
|
+
for block in blocks:
|
|
773
|
+
block_names_set.add(block)
|
|
774
|
+
|
|
775
|
+
# Parse release_date if available
|
|
776
|
+
release_date = row.get('Release', None)
|
|
777
|
+
if release_date is not None:
|
|
778
|
+
try:
|
|
779
|
+
release_date = int(release_date)
|
|
780
|
+
except (ValueError, TypeError):
|
|
781
|
+
release_date = None
|
|
782
|
+
|
|
783
|
+
series_list.append(SeriesConfig(
|
|
784
|
+
series_id=row.get('series_id', f"series_{len(series_list)}"),
|
|
785
|
+
series_name=row.get('series_name', row.get('series_id', f"Series {len(series_list)}")),
|
|
786
|
+
frequency=row.get('frequency', 'm'),
|
|
787
|
+
transformation=row.get('transformation', 'lin'),
|
|
788
|
+
blocks=blocks,
|
|
789
|
+
release_date=release_date
|
|
790
|
+
))
|
|
791
|
+
|
|
792
|
+
# Create default blocks if none specified
|
|
793
|
+
block_names = sorted(block_names_set) if block_names_set else ['Block_Global']
|
|
794
|
+
blocks = {}
|
|
795
|
+
for block_name in block_names:
|
|
796
|
+
blocks[block_name] = BlockConfig(factors=1, ar_lag=1, clock='m')
|
|
797
|
+
|
|
798
|
+
# block_names is derived automatically in DFMConfig.__post_init__ from blocks dict
|
|
799
|
+
return DFMConfig(series=series_list, blocks=blocks)
|
|
800
|
+
|
|
801
|
+
|
|
506
802
|
# ============================================================================
|
|
507
803
|
# Hydra ConfigStore Registration (optional - only if Hydra is available)
|
|
508
804
|
# ============================================================================
|
|
@@ -527,10 +823,9 @@ if HYDRA_AVAILABLE and ConfigStore is not None:
|
|
|
527
823
|
series_id: str
|
|
528
824
|
series_name: str
|
|
529
825
|
frequency: str
|
|
530
|
-
units: str
|
|
531
826
|
transformation: str
|
|
532
|
-
category: str
|
|
533
827
|
blocks: ListType[int]
|
|
828
|
+
units: Optional[str] = None # Optional, for display only
|
|
534
829
|
|
|
535
830
|
@schema_dataclass
|
|
536
831
|
class DFMConfigSchema:
|