dfm-python 0.5.60__py3-none-any.whl → 0.6.1__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/config/adapter.py +3 -9
- dfm_python/config/constants.py +35 -0
- dfm_python/config/schema/model.py +37 -77
- dfm_python/dataset/dfm_dataset.py +88 -1
- dfm_python/encoder/pca.py +3 -0
- dfm_python/functional/em.py +647 -59
- dfm_python/models/dfm/dfm.py +141 -15
- dfm_python/models/dfm/initialization.py +290 -62
- dfm_python/models/dfm/mixed_freq.py +128 -3
- dfm_python/models/dfm/tent.py +95 -17
- dfm_python/numeric/builder.py +5 -2
- dfm_python/numeric/estimator.py +252 -63
- dfm_python/numeric/stability.py +234 -6
- dfm_python/ssm/kalman.py +499 -54
- {dfm_python-0.5.60.dist-info → dfm_python-0.6.1.dist-info}/METADATA +1 -1
- {dfm_python-0.5.60.dist-info → dfm_python-0.6.1.dist-info}/RECORD +17 -17
- {dfm_python-0.5.60.dist-info → dfm_python-0.6.1.dist-info}/WHEEL +0 -0
dfm_python/config/adapter.py
CHANGED
|
@@ -508,18 +508,16 @@ def make_config_source(
|
|
|
508
508
|
# ============================================================================
|
|
509
509
|
|
|
510
510
|
# Model type constants (exported for use in schema/model.py)
|
|
511
|
-
|
|
511
|
+
# NOTE: KDFM is legacy and has been removed from this codebase.
|
|
512
512
|
MODEL_TYPE_DDFM = 'ddfm'
|
|
513
513
|
MODEL_TYPE_DFM = 'dfm'
|
|
514
514
|
|
|
515
515
|
# Model type detection patterns
|
|
516
|
-
_KDFM_TYPE_ALIASES = {'kdfm', 'kernelized'}
|
|
517
516
|
_DDFM_TYPE_ALIASES = {'ddfm', 'deep'}
|
|
518
|
-
_KDFM_PARAMS = {'ar_order', 'ma_order', 'structural_method', 'structural_reg_weight'}
|
|
519
517
|
_DDFM_PARAMS = {'encoder_layers', 'epochs', 'learning_rate', 'batch_size'}
|
|
520
518
|
|
|
521
519
|
def detect_config_type(data: Dict[str, Any]) -> str:
|
|
522
|
-
"""Detect config type (DFM
|
|
520
|
+
"""Detect config type (DFM or DDFM) from data dictionary.
|
|
523
521
|
|
|
524
522
|
Parameters
|
|
525
523
|
----------
|
|
@@ -529,7 +527,7 @@ def detect_config_type(data: Dict[str, Any]) -> str:
|
|
|
529
527
|
Returns
|
|
530
528
|
-------
|
|
531
529
|
str
|
|
532
|
-
'
|
|
530
|
+
'ddfm' or 'dfm'
|
|
533
531
|
|
|
534
532
|
Raises
|
|
535
533
|
------
|
|
@@ -544,8 +542,6 @@ def detect_config_type(data: Dict[str, Any]) -> str:
|
|
|
544
542
|
model_type = data.get('model_type', '').lower()
|
|
545
543
|
|
|
546
544
|
# Check explicit model type
|
|
547
|
-
if model_type in _KDFM_TYPE_ALIASES:
|
|
548
|
-
return MODEL_TYPE_KDFM
|
|
549
545
|
if model_type in _DDFM_TYPE_ALIASES:
|
|
550
546
|
return MODEL_TYPE_DDFM
|
|
551
547
|
if model_type == MODEL_TYPE_DFM:
|
|
@@ -558,8 +554,6 @@ def detect_config_type(data: Dict[str, Any]) -> str:
|
|
|
558
554
|
|
|
559
555
|
# Check for model-specific parameters
|
|
560
556
|
keys = set(data.keys())
|
|
561
|
-
if keys & _KDFM_PARAMS:
|
|
562
|
-
return MODEL_TYPE_KDFM
|
|
563
557
|
if any(k.startswith('ddfm_') for k in keys) or (keys & _DDFM_PARAMS):
|
|
564
558
|
return MODEL_TYPE_DDFM
|
|
565
559
|
|
dfm_python/config/constants.py
CHANGED
|
@@ -35,6 +35,29 @@ DEFAULT_VARIANCE_FALLBACK = 1.0 # Default variance fallback value for numerical
|
|
|
35
35
|
# Maximum eigenvalues
|
|
36
36
|
MAX_EIGENVALUE = 1e3 # Maximum eigenvalue cap (reduced to 1e3 to improve condition number: 1e3/1e-6 = 1e9)
|
|
37
37
|
|
|
38
|
+
# ============================================================================
|
|
39
|
+
# Performance and Numerical Health Guardrails
|
|
40
|
+
# ============================================================================
|
|
41
|
+
|
|
42
|
+
# Time budgets (hard limits to prevent pathological runs)
|
|
43
|
+
# Single constraint: 10 minutes for all datasets
|
|
44
|
+
MAX_ESTEP_SECONDS = 600.0 # Maximum E-step time (Kalman filter + smoother) - 10 minutes
|
|
45
|
+
MAX_MSTEP_SECONDS = 600.0 # Maximum M-step time (parameter updates) - 10 minutes
|
|
46
|
+
MAX_EM_ITERATION_SECONDS = 1200.0 # Maximum total EM iteration time - 20 minutes
|
|
47
|
+
MAX_FIRST_ITERATION_SECONDS = 600.0 # Maximum first iteration E-step time (diagnostic) - 10 minutes
|
|
48
|
+
|
|
49
|
+
# Numerical health thresholds (fail fast on pathological regimes)
|
|
50
|
+
# Note: Condition number check is more lenient during initialization (first iteration)
|
|
51
|
+
MAX_CONDITION_NUMBER_SMOOTHER = 1e8 # Maximum condition number before refusing to smooth (increased from 1e7)
|
|
52
|
+
MAX_CONDITION_NUMBER_INIT = 1e10 # Maximum condition number during initialization (more lenient)
|
|
53
|
+
MAX_STABILIZATION_AMOUNT = 1e-3 # Maximum stabilization amount before error (increased from 1e-4 to 0.1% diagonal loading)
|
|
54
|
+
MAX_STABILIZATION_AMOUNT_INIT = 5e-3 # Maximum stabilization during initialization (more lenient)
|
|
55
|
+
MAX_STD_UNSCALED = 100.0 # Maximum standard deviation for unscaled data (indicates missing preprocessing)
|
|
56
|
+
MIN_STD_SCALED = 1e-4 # Minimum standard deviation for scaled data (indicates degenerate series)
|
|
57
|
+
|
|
58
|
+
# Fallback tracking
|
|
59
|
+
MAX_FALLBACKS_PER_ITERATION = 0 # Maximum allowed fallbacks per iteration (0 = fail on any fallback)
|
|
60
|
+
|
|
38
61
|
# Eigenvalue stability thresholds
|
|
39
62
|
DEFAULT_EIGENVALUE_MAX_MAGNITUDE = 1.0 # Default maximum eigenvalue magnitude for stability checks
|
|
40
63
|
DEFAULT_EIGENVALUE_WARN_THRESHOLD = 0.99 # Default warning threshold for near-unstable eigenvalues
|
|
@@ -566,5 +589,17 @@ __all__ = [
|
|
|
566
589
|
# Frequency hierarchy and tent kernels
|
|
567
590
|
'FREQUENCY_HIERARCHY',
|
|
568
591
|
'MAX_TENT_SIZE',
|
|
592
|
+
# Performance and numerical health guardrails
|
|
593
|
+
'MAX_ESTEP_SECONDS',
|
|
594
|
+
'MAX_MSTEP_SECONDS',
|
|
595
|
+
'MAX_EM_ITERATION_SECONDS',
|
|
596
|
+
'MAX_FIRST_ITERATION_SECONDS',
|
|
597
|
+
'MAX_CONDITION_NUMBER_SMOOTHER',
|
|
598
|
+
'MAX_CONDITION_NUMBER_INIT',
|
|
599
|
+
'MAX_STABILIZATION_AMOUNT',
|
|
600
|
+
'MAX_STABILIZATION_AMOUNT_INIT',
|
|
601
|
+
'MAX_STD_UNSCALED',
|
|
602
|
+
'MIN_STD_SCALED',
|
|
603
|
+
'MAX_FALLBACKS_PER_ITERATION',
|
|
569
604
|
]
|
|
570
605
|
|
|
@@ -418,16 +418,32 @@ class DFMConfig(BaseModelConfig):
|
|
|
418
418
|
series_ids = list(self.frequency.keys()) if columns is None else columns
|
|
419
419
|
|
|
420
420
|
# Build blocks array from block series lists (N x B matrix)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
421
|
+
# Default global block behavior:
|
|
422
|
+
# If config omitted blocks.series (empty list) and we have runtime columns,
|
|
423
|
+
# interpret a single empty Block_Global as "all series in this block".
|
|
424
|
+
block_series_sets: Dict[str, set] = {}
|
|
425
|
+
default_global_applied = False
|
|
426
|
+
for name in self.block_names:
|
|
427
|
+
series_list = self.blocks[name].get('series', [])
|
|
428
|
+
if (
|
|
429
|
+
columns is not None
|
|
430
|
+
and (series_list is None or len(series_list) == 0)
|
|
431
|
+
and len(self.block_names) == 1
|
|
432
|
+
and name == "Block_Global"
|
|
433
|
+
):
|
|
434
|
+
block_series_sets[name] = set(series_ids)
|
|
435
|
+
default_global_applied = True
|
|
436
|
+
else:
|
|
437
|
+
block_series_sets[name] = set(series_list or [])
|
|
438
|
+
|
|
425
439
|
blocks_list = [
|
|
426
440
|
[1 if series_id in block_series_sets[name] else 0 for name in self.block_names]
|
|
427
441
|
for series_id in series_ids
|
|
428
442
|
]
|
|
429
443
|
|
|
430
444
|
self._cached_blocks = np.array(blocks_list, dtype=int)
|
|
445
|
+
|
|
446
|
+
# Quick sanity on membership counts for the first block (if any)
|
|
431
447
|
return self._cached_blocks
|
|
432
448
|
|
|
433
449
|
@classmethod
|
|
@@ -463,14 +479,14 @@ class DFMConfig(BaseModelConfig):
|
|
|
463
479
|
return result
|
|
464
480
|
|
|
465
481
|
@classmethod
|
|
466
|
-
def from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig'
|
|
467
|
-
"""Create DFMConfig
|
|
482
|
+
def from_dict(cls, data: Dict[str, Any]) -> Union['DFMConfig', 'DDFMConfig']:
|
|
483
|
+
"""Create DFMConfig or DDFMConfig from dictionary.
|
|
468
484
|
|
|
469
485
|
Expected format: {'frequency': {'column_name': 'frequency'}, 'blocks': {...}, ...}
|
|
470
486
|
|
|
471
487
|
Also accepts estimation parameters: threshold, max_iter, etc.
|
|
472
488
|
"""
|
|
473
|
-
from ...config.adapter import detect_config_type, MODEL_TYPE_DDFM,
|
|
489
|
+
from ...config.adapter import detect_config_type, MODEL_TYPE_DDFM, _normalize_blocks_dict
|
|
474
490
|
from ...utils.errors import ConfigurationError
|
|
475
491
|
|
|
476
492
|
# Extract base params (handles frequency conversion from series if needed)
|
|
@@ -482,14 +498,24 @@ class DFMConfig(BaseModelConfig):
|
|
|
482
498
|
if config_type == MODEL_TYPE_DDFM:
|
|
483
499
|
return DDFMConfig(**base_params, **DDFMConfig._extract_ddfm(data))
|
|
484
500
|
|
|
485
|
-
if config_type == MODEL_TYPE_KDFM:
|
|
486
|
-
return KDFMConfig(**base_params, **KDFMConfig._extract_kdfm(data))
|
|
487
|
-
|
|
488
501
|
# Handle blocks for DFM
|
|
489
502
|
from ...config.adapter import _raise_config_error, _is_dict_like
|
|
490
503
|
blocks_dict = data.get('blocks', {})
|
|
491
504
|
if not blocks_dict:
|
|
492
|
-
|
|
505
|
+
# Default behavior: single global block.
|
|
506
|
+
# This enables minimal configs like:
|
|
507
|
+
# clock: d
|
|
508
|
+
# num_factors: 2
|
|
509
|
+
# and lets the model bind series names from the dataset at runtime.
|
|
510
|
+
default_num_factors = int(data.get('num_factors', 1) or 1)
|
|
511
|
+
if default_num_factors < 1:
|
|
512
|
+
_raise_config_error(f"num_factors must be >= 1, got {default_num_factors}")
|
|
513
|
+
blocks_dict = {
|
|
514
|
+
"Block_Global": {
|
|
515
|
+
"num_factors": default_num_factors,
|
|
516
|
+
"series": [], # to be bound from dataset columns
|
|
517
|
+
}
|
|
518
|
+
}
|
|
493
519
|
if not _is_dict_like(blocks_dict):
|
|
494
520
|
_raise_config_error(f"blocks must be a dict, got {type(blocks_dict)}")
|
|
495
521
|
|
|
@@ -600,72 +626,6 @@ class DDFMConfig(BaseModelConfig):
|
|
|
600
626
|
)
|
|
601
627
|
|
|
602
628
|
|
|
603
|
-
@dataclass
|
|
604
|
-
class KDFMConfig(BaseModelConfig):
|
|
605
|
-
"""KDFM configuration dataclass.
|
|
606
|
-
|
|
607
|
-
This dataclass contains all configuration parameters for the KDFM model.
|
|
608
|
-
It inherits from BaseModelConfig and adds KDFM-specific parameters.
|
|
609
|
-
|
|
610
|
-
Note: KDFM does not use blocks structure (unlike DFM). Only frequency dict is needed.
|
|
611
|
-
"""
|
|
612
|
-
# VARMA parameters
|
|
613
|
-
ar_order: int = DEFAULT_KDFM_AR_ORDER # VAR order p
|
|
614
|
-
ma_order: int = DEFAULT_KDFM_MA_ORDER # MA order q (0 = pure VAR)
|
|
615
|
-
|
|
616
|
-
# Structural identification
|
|
617
|
-
structural_method: str = 'cholesky' # 'cholesky', 'full', 'low_rank'
|
|
618
|
-
structural_rank: Optional[int] = None # For low-rank parameterization
|
|
619
|
-
|
|
620
|
-
# Training parameters (use constants for defaults)
|
|
621
|
-
learning_rate: float = DEFAULT_LEARNING_RATE
|
|
622
|
-
max_epochs: int = DEFAULT_MAX_EPOCHS
|
|
623
|
-
batch_size: int = DEFAULT_BATCH_SIZE
|
|
624
|
-
weight_decay: float = DEFAULT_REGULARIZATION_SCALE
|
|
625
|
-
grad_clip_val: float = DEFAULT_GRAD_CLIP_VAL
|
|
626
|
-
|
|
627
|
-
# Regularization
|
|
628
|
-
structural_reg_weight: float = DEFAULT_STRUCTURAL_REG_WEIGHT # Weight for structural loss
|
|
629
|
-
use_regularization: bool = True
|
|
630
|
-
regularization_scale: float = DEFAULT_REGULARIZATION_SCALE
|
|
631
|
-
|
|
632
|
-
# ========================================================================
|
|
633
|
-
# Factory Methods
|
|
634
|
-
# ========================================================================
|
|
635
|
-
|
|
636
|
-
@classmethod
|
|
637
|
-
def _extract_kdfm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
638
|
-
"""Extract KDFM-specific parameters from config dict."""
|
|
639
|
-
# Don't extract base params here - they're already in base_params from from_dict
|
|
640
|
-
kdfm_params = cls._extract_params(data, {
|
|
641
|
-
'ar_order': DEFAULT_KDFM_AR_ORDER,
|
|
642
|
-
'ma_order': DEFAULT_KDFM_MA_ORDER,
|
|
643
|
-
'structural_method': 'cholesky',
|
|
644
|
-
'structural_rank': None,
|
|
645
|
-
'learning_rate': DEFAULT_LEARNING_RATE,
|
|
646
|
-
'max_epochs': DEFAULT_MAX_EPOCHS,
|
|
647
|
-
'batch_size': DEFAULT_BATCH_SIZE,
|
|
648
|
-
'weight_decay': DEFAULT_REGULARIZATION_SCALE,
|
|
649
|
-
'grad_clip_val': DEFAULT_GRAD_CLIP_VAL,
|
|
650
|
-
'structural_reg_weight': DEFAULT_STRUCTURAL_REG_WEIGHT,
|
|
651
|
-
'use_regularization': True,
|
|
652
|
-
'regularization_scale': DEFAULT_REGULARIZATION_SCALE,
|
|
653
|
-
})
|
|
654
|
-
return kdfm_params
|
|
655
|
-
|
|
656
|
-
@classmethod
|
|
657
|
-
def from_dict(cls, data: Dict[str, Any]) -> 'KDFMConfig':
|
|
658
|
-
"""Create KDFMConfig from dictionary (delegates to DFMConfig.from_dict for type detection)."""
|
|
659
|
-
result = DFMConfig.from_dict(data)
|
|
660
|
-
if isinstance(result, KDFMConfig):
|
|
661
|
-
return result
|
|
662
|
-
from ..utils.errors import ConfigurationError
|
|
663
|
-
raise ConfigurationError(
|
|
664
|
-
f"Expected KDFMConfig but got {type(result).__name__}",
|
|
665
|
-
details=f"Result type: {type(result).__name__}, expected: KDFMConfig"
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
|
|
669
629
|
# ============================================================================
|
|
670
630
|
# Validation Functions
|
|
671
631
|
# ============================================================================
|
|
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
|
|
|
22
22
|
|
|
23
23
|
from ..logger import get_logger
|
|
24
24
|
from ..config import DFMConfig
|
|
25
|
+
from ..config.constants import FREQUENCY_HIERARCHY, DEFAULT_HIERARCHY_VALUE
|
|
25
26
|
from ..dataset.time import TimeIndex
|
|
26
27
|
from ..utils.errors import DataValidationError, ConfigurationError
|
|
27
28
|
|
|
@@ -139,9 +140,13 @@ class DFMDataset:
|
|
|
139
140
|
|
|
140
141
|
# Get all available columns (numeric only)
|
|
141
142
|
numeric_columns = list(X_df.select_dtypes(include=[np.number]).columns)
|
|
143
|
+
|
|
144
|
+
# Reorder columns for mixed-frequency models: clock-frequency first, then slower-frequency
|
|
145
|
+
numeric_columns = self._reorder_columns_for_mixed_freq(numeric_columns, X_df)
|
|
146
|
+
|
|
142
147
|
self._columns = numeric_columns
|
|
143
148
|
|
|
144
|
-
# Store DataFrame with time index (numeric columns only)
|
|
149
|
+
# Store DataFrame with time index (numeric columns only, properly ordered)
|
|
145
150
|
self.data_df = X_df[numeric_columns]
|
|
146
151
|
|
|
147
152
|
@property
|
|
@@ -154,6 +159,88 @@ class DFMDataset:
|
|
|
154
159
|
"""Get time index."""
|
|
155
160
|
return self._time_index
|
|
156
161
|
|
|
162
|
+
def _reorder_columns_for_mixed_freq(
|
|
163
|
+
self,
|
|
164
|
+
columns: List[str],
|
|
165
|
+
data_df: pd.DataFrame
|
|
166
|
+
) -> List[str]:
|
|
167
|
+
"""Reorder columns for mixed-frequency models.
|
|
168
|
+
|
|
169
|
+
Ensures columns are ordered as [clock-frequency series | slower-frequency series]
|
|
170
|
+
as required by the mixed-frequency model implementation.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
columns : List[str]
|
|
175
|
+
Current column list
|
|
176
|
+
data_df : pd.DataFrame
|
|
177
|
+
DataFrame with data (for validation)
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
List[str]
|
|
182
|
+
Reordered column list (clock-frequency first, then slower-frequency)
|
|
183
|
+
"""
|
|
184
|
+
# Check if config has frequency mapping
|
|
185
|
+
if not self.config.frequency:
|
|
186
|
+
return columns # No frequency mapping, return as-is
|
|
187
|
+
|
|
188
|
+
# Get clock frequency
|
|
189
|
+
clock = self.config.clock
|
|
190
|
+
|
|
191
|
+
# Build frequency dict from config
|
|
192
|
+
# Handle both grouped format {'w': [series1, ...], 'm': [series2, ...]}
|
|
193
|
+
# and individual format {'series1': 'w', 'series2': 'm', ...}
|
|
194
|
+
frequency_dict = {}
|
|
195
|
+
if isinstance(self.config.frequency, dict):
|
|
196
|
+
for key, value in self.config.frequency.items():
|
|
197
|
+
if isinstance(value, list):
|
|
198
|
+
# Grouped format: {'w': [series1, ...], 'm': [series2, ...]}
|
|
199
|
+
for series in value:
|
|
200
|
+
if series in columns:
|
|
201
|
+
frequency_dict[series] = key
|
|
202
|
+
else:
|
|
203
|
+
# Individual format: {'series1': 'w', 'series2': 'm', ...}
|
|
204
|
+
if key in columns:
|
|
205
|
+
frequency_dict[key] = value
|
|
206
|
+
|
|
207
|
+
# If no frequency mapping found for any columns, return as-is
|
|
208
|
+
if not frequency_dict:
|
|
209
|
+
return columns
|
|
210
|
+
|
|
211
|
+
# Get clock hierarchy value
|
|
212
|
+
clock_hierarchy = FREQUENCY_HIERARCHY.get(clock, DEFAULT_HIERARCHY_VALUE)
|
|
213
|
+
|
|
214
|
+
# Separate columns by frequency
|
|
215
|
+
clock_cols = []
|
|
216
|
+
slower_cols = []
|
|
217
|
+
|
|
218
|
+
for col in columns:
|
|
219
|
+
freq = frequency_dict.get(col)
|
|
220
|
+
if freq is None:
|
|
221
|
+
# Column not in frequency mapping - assume clock frequency
|
|
222
|
+
clock_cols.append(col)
|
|
223
|
+
else:
|
|
224
|
+
freq_hierarchy = FREQUENCY_HIERARCHY.get(freq, DEFAULT_HIERARCHY_VALUE)
|
|
225
|
+
if freq_hierarchy <= clock_hierarchy:
|
|
226
|
+
# Clock frequency or same as clock
|
|
227
|
+
clock_cols.append(col)
|
|
228
|
+
else:
|
|
229
|
+
# Slower frequency
|
|
230
|
+
slower_cols.append(col)
|
|
231
|
+
|
|
232
|
+
# Reorder: clock-frequency first, then slower-frequency
|
|
233
|
+
reordered = clock_cols + slower_cols
|
|
234
|
+
|
|
235
|
+
# Log if reordering occurred
|
|
236
|
+
if reordered != columns:
|
|
237
|
+
_logger.info(
|
|
238
|
+
f"Reordered columns for mixed-frequency model: "
|
|
239
|
+
f"{len(clock_cols)} clock-frequency, {len(slower_cols)} slower-frequency"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return reordered
|
|
243
|
+
|
|
157
244
|
def _validate(self) -> None:
|
|
158
245
|
"""Validate dataset state and data integrity.
|
|
159
246
|
|
dfm_python/encoder/pca.py
CHANGED
|
@@ -56,8 +56,11 @@ def compute_principal_components(
|
|
|
56
56
|
|
|
57
57
|
n_series = cov_matrix.shape[0]
|
|
58
58
|
|
|
59
|
+
|
|
60
|
+
|
|
59
61
|
try:
|
|
60
62
|
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
|
|
63
|
+
|
|
61
64
|
# Sort by absolute value, descending
|
|
62
65
|
sort_idx = np.argsort(np.abs(eigenvalues))[::-1][:n_components]
|
|
63
66
|
eigenvalues_sorted = eigenvalues[sort_idx]
|