dfm-python 0.4.7__tar.gz → 0.4.8__tar.gz
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-0.4.7 → dfm_python-0.4.8}/PKG-INFO +1 -1
- {dfm_python-0.4.7 → dfm_python-0.4.8}/pyproject.toml +1 -1
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/__init__.py +1 -1
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/ssm/em.py +67 -14
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/ssm/kalman.py +50 -1
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/ssm/utils.py +45 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/.git +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/.gitignore +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/.python-version +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/LICENSE +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/MANIFEST.in +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/README.md +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/experiment/test_finance.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/experiment/test_macro.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/model/ddfm.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/model/dfm.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KO3YEARC.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBFDSAAF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBFINAAF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBFXSAAF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSESI_R.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSIBCEQ.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSIBCOQ.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSIBNER.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSIBNMR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSIEMAR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSIHAPR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSIIMER.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSIIMPR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBSIXMER.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOBUSINVR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCALL_.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCNFBUSR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCNFCONR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCNGOV_D.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCNPER_D.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCOBUSIR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCONPMTH.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCONPRCF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCONSTRP.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCPCOREF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCPEXFEF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCREU__A.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSECDMR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSECEMR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSEMEMR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSEOHDR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSEOHSR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSHHEMR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSHHLMR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSMTTOA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSPHIMR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCSPSPMR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOCYLEADR.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KODBLNEQA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KODBLNHSA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEMPCONF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEMPMANF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEMPTOTO.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEMPWREP.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEQUIMAE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEQUIPTE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEQUITTE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEXCHINA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEXNGS_D.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEXPGDSA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOEXPPRCF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOGCFCNSD.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOGCFFNVD.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOGDP___D.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOGFCF__D.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOHWRWEMP.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOIMNGS_D.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOIMPCONA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOIMPGDDA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOIMPGDSA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOIMPPRCF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOIPALL_G.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOIPMIMAG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOLRLTCO.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOLRLTHH.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOMPCAPGG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOMPCONDG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOMPCONGG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOMPCONNG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOMPRI30G.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOMSCAPGG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOMSCONGG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KONETTRDF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KONEWCONA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOPICAPGG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOPICONGG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOPIMANUG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOPOPACTE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOPPSRM_F.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOPROPRCF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOPSMANUG.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KORCNST.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KORELEC.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KORFBEV.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KORMACH.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KORMCMP.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOROCOINF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KORWHLS.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOSHRPRCF.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOSIAHOTE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOSIAI__E.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOSIARRLE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOSIAWRTE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOSSBFMBE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOUN_TOTQ.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOUS____.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOVALCONA.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOWRCCNSE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOWRCDURE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOWRCNODE.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/config/series/KOWRCSEME.yaml +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/data/finance.csv +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/data/macro.csv +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/config/__init__.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/config/adapter.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/config/results.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/config/schema.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/config/utils.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/data/__init__.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/data/dataloader.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/data/dataset.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/data/utils.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/encoder/__init__.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/encoder/base.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/encoder/pca.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/encoder/vae.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/lightning/__init__.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/lightning/data_module.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/lightning/scaling.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/logger/__init__.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/logger/inference_logger.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/logger/logger.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/logger/train_logger.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/models/__init__.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/models/base.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/models/ddfm.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/models/dfm.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/ssm/__init__.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/trainer/__init__.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/trainer/ddfm.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/trainer/dfm.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/utils/__init__.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/utils/data.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/utils/diagnostics.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/utils/helpers.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/utils/statespace.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/dfm_python/utils/time.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/test/test_config.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/test/test_helpers.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/test/test_logger.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/test/test_models.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/test/test_nowcast.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/test/test_sktime_integration.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/test/test_ssm.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/test/test_trainer.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/src/test/test_utils.py +0 -0
- {dfm_python-0.4.7 → dfm_python-0.4.8}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dfm-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: Dynamic Factor Model (DFM) estimation and nowcasting in Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/yourusername/dfm-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/yourusername/dfm-python#readme
|
|
@@ -65,7 +65,7 @@ Note: DFMConfig and SeriesConfig are internal implementation details.
|
|
|
65
65
|
For detailed documentation, see the README.md file and the tutorial notebooks/scripts.
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
|
-
__version__ = "0.4.
|
|
68
|
+
__version__ = "0.4.8"
|
|
69
69
|
|
|
70
70
|
# ============================================================================
|
|
71
71
|
# PUBLIC API DEFINITION
|
|
@@ -48,7 +48,7 @@ from typing import Tuple, Optional, Dict, Any
|
|
|
48
48
|
from dataclasses import dataclass
|
|
49
49
|
from ..logger import get_logger
|
|
50
50
|
from .kalman import KalmanFilter
|
|
51
|
-
from .utils import ensure_positive_definite, ensure_symmetric
|
|
51
|
+
from .utils import ensure_positive_definite, ensure_symmetric, cap_max_eigenval
|
|
52
52
|
|
|
53
53
|
_logger = get_logger(__name__)
|
|
54
54
|
|
|
@@ -108,6 +108,10 @@ class EMAlgorithm(nn.Module):
|
|
|
108
108
|
self.kalman = kalman
|
|
109
109
|
self.register_buffer('regularization_scale', torch.tensor(regularization_scale))
|
|
110
110
|
|
|
111
|
+
def _cap_max_eigenval(self, M: torch.Tensor, max_eigenval: float = 1e6) -> torch.Tensor:
|
|
112
|
+
"""Cap maximum eigenvalue of matrix to prevent numerical explosion."""
|
|
113
|
+
return cap_max_eigenval(M, max_eigenval=max_eigenval, warn=False)
|
|
114
|
+
|
|
111
115
|
def forward(
|
|
112
116
|
self,
|
|
113
117
|
params: EMStepParams
|
|
@@ -241,8 +245,27 @@ class EMAlgorithm(nn.Module):
|
|
|
241
245
|
# Transpose y to (T, N) for batch operations
|
|
242
246
|
# Use y_for_mstep (NaN replaced with 0) for M-step
|
|
243
247
|
sum_yEZ = torch.sum(y_for_mstep.T[:, :, None] * EZ[:, None, :], dim=0) # (N, m)
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
|
|
249
|
+
# CRITICAL FIX: Remove NaN/Inf from EZZ before computing sum_EZZ
|
|
250
|
+
# This prevents ill-conditioned sum_EZZ caused by corrupted EZZ from smoother
|
|
251
|
+
# MATLAB doesn't check for NaN/Inf, but we need to handle it explicitly
|
|
252
|
+
EZZ_clean = EZZ.clone()
|
|
253
|
+
nan_inf_mask = torch.isnan(EZZ_clean) | torch.isinf(EZZ_clean)
|
|
254
|
+
if torch.any(nan_inf_mask):
|
|
255
|
+
# Replace NaN/Inf with zero (exclude corrupted time steps from sum)
|
|
256
|
+
EZZ_clean[nan_inf_mask] = 0.0
|
|
257
|
+
corrupted_count = torch.sum(nan_inf_mask).item()
|
|
258
|
+
total_count = EZZ_clean.numel()
|
|
259
|
+
_logger.warning(
|
|
260
|
+
f"EM algorithm: EZZ contains {corrupted_count}/{total_count} NaN/Inf values "
|
|
261
|
+
f"({corrupted_count/total_count:.1%}). Excluding corrupted time steps from sum_EZZ calculation."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Compute sum_EZZ = sum_t E[Z_t Z_t^T] (only from valid time steps)
|
|
265
|
+
sum_EZZ = torch.sum(EZZ_clean, dim=0)
|
|
266
|
+
|
|
267
|
+
# Cap maximum eigenvalue to prevent condition number explosion
|
|
268
|
+
sum_EZZ = self._cap_max_eigenval(sum_EZZ, max_eigenval=1e6)
|
|
246
269
|
|
|
247
270
|
# ROOT CAUSE FIX: Adaptive regularization based on condition number
|
|
248
271
|
# Fixed regularization (1e-6) becomes insufficient as condition number grows
|
|
@@ -268,22 +291,38 @@ class EMAlgorithm(nn.Module):
|
|
|
268
291
|
reg_scale = base_reg_scale
|
|
269
292
|
else:
|
|
270
293
|
# All eigenvalues are near-zero, use stronger regularization
|
|
271
|
-
|
|
294
|
+
# Ensure minimum regularization (1e-3) for numerical stability
|
|
295
|
+
reg_scale = max(base_reg_scale * 100, 1e-3)
|
|
272
296
|
_logger.warning(
|
|
273
297
|
f"EM algorithm: sum_EZZ has no valid eigenvalues, "
|
|
274
|
-
f"using strong regularization: {reg_scale:.2e}"
|
|
298
|
+
f"using strong regularization: {reg_scale:.2e} (min=1e-3, base={base_reg_scale:.2e})"
|
|
275
299
|
)
|
|
276
300
|
except (RuntimeError, ValueError) as e:
|
|
277
|
-
# Eigendecomposition failed, use stronger regularization
|
|
278
|
-
|
|
301
|
+
# CRITICAL FIX: Eigendecomposition failed, use stronger regularization
|
|
302
|
+
# Ensure minimum regularization (1e-3) even if base_reg_scale * 10 is smaller
|
|
303
|
+
# This is the Ridge Regression approach: eigendecomposition failure means
|
|
304
|
+
# the matrix is too ill-conditioned, so we need strong regularization
|
|
305
|
+
reg_scale = max(base_reg_scale * 10, 1e-3)
|
|
279
306
|
_logger.warning(
|
|
280
307
|
f"EM algorithm: Failed to compute condition number for sum_EZZ ({e}), "
|
|
281
|
-
f"using
|
|
308
|
+
f"using strong regularization: {reg_scale:.2e} (min=1e-3, base={base_reg_scale:.2e})"
|
|
282
309
|
)
|
|
283
310
|
|
|
284
311
|
# Apply adaptive regularization
|
|
285
312
|
sum_EZZ_reg = sum_EZZ + torch.eye(m, device=device, dtype=dtype) * reg_scale
|
|
286
|
-
|
|
313
|
+
|
|
314
|
+
# CRITICAL FIX: Use pseudo-inverse as fallback when solve fails
|
|
315
|
+
# Original MATLAB uses pinv() in smoother (line 1039, 1064)
|
|
316
|
+
# This handles singular/ill-conditioned matrices more robustly
|
|
317
|
+
try:
|
|
318
|
+
C_new = torch.linalg.solve(sum_EZZ_reg.T, sum_yEZ.T).T
|
|
319
|
+
except RuntimeError as e:
|
|
320
|
+
# Fallback to pseudo-inverse (original MATLAB: pinv() in smoother)
|
|
321
|
+
_logger.warning(
|
|
322
|
+
f"EM algorithm: torch.linalg.solve failed for C matrix update ({e}), "
|
|
323
|
+
f"using pseudo-inverse fallback (original MATLAB: pinv() in smoother)"
|
|
324
|
+
)
|
|
325
|
+
C_new = (torch.linalg.pinv(sum_EZZ_reg.T) @ sum_yEZ.T).T
|
|
287
326
|
|
|
288
327
|
# Check for NaN in C_new after solve (indicates numerical instability)
|
|
289
328
|
if torch.any(torch.isnan(C_new)):
|
|
@@ -294,11 +333,25 @@ class EMAlgorithm(nn.Module):
|
|
|
294
333
|
f"after solve operation. This indicates numerical instability. "
|
|
295
334
|
f"Possible causes: singular matrix, extreme values, or insufficient regularization."
|
|
296
335
|
)
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
336
|
+
# CRITICAL FIX: Preserve previous iteration values instead of setting to zero
|
|
337
|
+
# This allows gradual improvement and preserves information from previous iterations
|
|
338
|
+
nan_mask = torch.isnan(C_new)
|
|
339
|
+
if torch.any(nan_mask):
|
|
340
|
+
# Check if previous C matrix is valid (no NaN)
|
|
341
|
+
if not torch.any(torch.isnan(params.C)):
|
|
342
|
+
C_new[nan_mask] = params.C[nan_mask]
|
|
343
|
+
# Only log if significant NaN ratio to reduce log spam
|
|
344
|
+
if nan_ratio > 0.1: # Only log if >10% NaN
|
|
345
|
+
_logger.warning(
|
|
346
|
+
f"Preserved {torch.sum(nan_mask).item()} NaN values in C matrix from previous iteration "
|
|
347
|
+
f"(total NaN: {nan_count}/{C_new.numel()}, {nan_ratio:.1%})"
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
# If previous C also has NaN, set to zero as last resort
|
|
351
|
+
C_new[nan_mask] = 0.0
|
|
352
|
+
_logger.warning(
|
|
353
|
+
f"Previous C matrix also contains NaN. Set {torch.sum(nan_mask).item()} NaN values to zero."
|
|
354
|
+
)
|
|
302
355
|
|
|
303
356
|
# Normalize C columns (factor loadings)
|
|
304
357
|
for j in range(m):
|
|
@@ -36,6 +36,7 @@ from .utils import (
|
|
|
36
36
|
clean_matrix,
|
|
37
37
|
safe_inverse,
|
|
38
38
|
safe_determinant,
|
|
39
|
+
cap_max_eigenval,
|
|
39
40
|
)
|
|
40
41
|
|
|
41
42
|
_logger = get_logger(__name__)
|
|
@@ -343,6 +344,8 @@ class KalmanFilter(nn.Module):
|
|
|
343
344
|
# Fallback: use previous covariance with regularization
|
|
344
345
|
V = Vu + torch.eye(V.shape[0], device=device, dtype=dtype) * 1e-6
|
|
345
346
|
|
|
347
|
+
# Cap maximum eigenvalue to prevent explosion
|
|
348
|
+
V = self._cap_max_eigenval(V, max_eigenval=1e6)
|
|
346
349
|
# Ensure V is real, symmetric, and positive semi-definite (MATLAB: V = 0.5 * (V+V'))
|
|
347
350
|
V = ensure_real_and_symmetric(V)
|
|
348
351
|
V = self._ensure_cov_stable(V, min_eigenval=self.min_eigenval.item(), ensure_real=True)
|
|
@@ -356,13 +359,24 @@ class KalmanFilter(nn.Module):
|
|
|
356
359
|
Zu = Z
|
|
357
360
|
Vu = V
|
|
358
361
|
else:
|
|
359
|
-
# CRITICAL FIX: Adaptive regularization based on missing data ratio
|
|
362
|
+
# CRITICAL FIX: Adaptive regularization based on missing data ratio and n_obs
|
|
360
363
|
# When data is sparse (many masked values), increase regularization to prevent numerical instability
|
|
361
364
|
missing_ratio = 1.0 - (len(Y_t) / k) # Ratio of missing observations
|
|
362
365
|
# Scale regularization: base regularization * (1 + missing_ratio * 10)
|
|
363
366
|
# This means: 0% missing -> base reg, 50% missing -> 6x reg, 100% missing -> 11x reg
|
|
364
367
|
adaptive_reg = self.inv_regularization.item() * (1.0 + missing_ratio * 10.0)
|
|
365
368
|
|
|
369
|
+
# CRITICAL FIX: Additional regularization based on n_obs (number of observed series)
|
|
370
|
+
# Log analysis shows F matrix inversion becomes unstable when n_obs >= 30
|
|
371
|
+
# Scale regularization: n_obs >= 30 -> additional factor (more generic threshold)
|
|
372
|
+
n_obs = len(Y_t)
|
|
373
|
+
if n_obs >= 30:
|
|
374
|
+
n_obs_factor = 1.0 + (n_obs - 29) * 0.1 # 30 -> 1.1x, 33 -> 1.4x, 39 -> 2.0x
|
|
375
|
+
adaptive_reg = adaptive_reg * n_obs_factor
|
|
376
|
+
# Only log once per iteration (first time step) to reduce log spam
|
|
377
|
+
if t == 0:
|
|
378
|
+
_logger.debug(f"kalman_filter_forward: n_obs={n_obs} >= 30, applying n_obs_factor={n_obs_factor:.2f}, adaptive_reg={adaptive_reg:.2e}")
|
|
379
|
+
|
|
366
380
|
# Steps for variance and population regression coefficients:
|
|
367
381
|
# Var(c_t*Z_t + e_t) = c_t Var(Z) c_t' + Var(e) = c_t*V*c_t' + R
|
|
368
382
|
VC = V @ C_t.T
|
|
@@ -378,6 +392,26 @@ class KalmanFilter(nn.Module):
|
|
|
378
392
|
diag_R_t = torch.clamp(diag_R_t, min=self.min_diagonal_variance.item() * (1.0 + missing_ratio * 5.0))
|
|
379
393
|
R_t = torch.diag(diag_R_t)
|
|
380
394
|
|
|
395
|
+
# ROOT CAUSE ANALYSIS: Compute and log F matrix condition number before capping
|
|
396
|
+
# This helps identify when F becomes ill-conditioned
|
|
397
|
+
try:
|
|
398
|
+
eigenvals_F = torch.linalg.eigvalsh(F)
|
|
399
|
+
eigenvals_F = eigenvals_F[eigenvals_F > 1e-12]
|
|
400
|
+
if len(eigenvals_F) > 0:
|
|
401
|
+
max_eig_F = torch.max(eigenvals_F).item()
|
|
402
|
+
min_eig_F = torch.min(eigenvals_F).item()
|
|
403
|
+
cond_F = max_eig_F / min_eig_F if min_eig_F > 1e-12 else float('inf')
|
|
404
|
+
# Only log first 5 steps or when severely ill-conditioned to reduce log spam
|
|
405
|
+
if cond_F > 1e10 or t < 5:
|
|
406
|
+
_logger.debug(
|
|
407
|
+
f"kalman_filter_forward: t={t}, n_obs={n_obs}, F condition={cond_F:.2e}, "
|
|
408
|
+
f"eigenvals=[{min_eig_F:.2e}, {max_eig_F:.2e}], adaptive_reg={adaptive_reg:.2e}"
|
|
409
|
+
)
|
|
410
|
+
except (RuntimeError, ValueError):
|
|
411
|
+
pass # Skip if eigendecomposition fails
|
|
412
|
+
|
|
413
|
+
# Cap maximum eigenvalue to prevent condition number explosion
|
|
414
|
+
F = self._cap_max_eigenval(F, max_eigenval=1e6)
|
|
381
415
|
# Ensure F is real, symmetric, and positive semi-definite
|
|
382
416
|
# Use adaptive minimum eigenvalue based on missing data ratio
|
|
383
417
|
min_eig_adaptive = self.min_eigenval.item() * (1.0 + missing_ratio * 5.0)
|
|
@@ -394,6 +428,15 @@ class KalmanFilter(nn.Module):
|
|
|
394
428
|
# This handles GPU numerical stability issues (singular matrix errors)
|
|
395
429
|
iF = self._safe_inv(F, regularization=adaptive_reg, use_pinv_fallback=True)
|
|
396
430
|
|
|
431
|
+
# ROOT CAUSE ANALYSIS: Check if inversion was successful (only log warnings, not every occurrence)
|
|
432
|
+
if not self._check_finite(iF, f"iF (inverse of F) at t={t}"):
|
|
433
|
+
# Only log first occurrence to reduce log spam
|
|
434
|
+
if t < 5:
|
|
435
|
+
_logger.warning(
|
|
436
|
+
f"kalman_filter_forward: iF contains NaN/Inf at t={t}, n_obs={n_obs}, "
|
|
437
|
+
f"adaptive_reg={adaptive_reg:.2e}, missing_ratio={missing_ratio:.2f}"
|
|
438
|
+
)
|
|
439
|
+
|
|
397
440
|
# Matrix of population regression coefficients (Kalman gain)
|
|
398
441
|
VCF = VC @ iF
|
|
399
442
|
|
|
@@ -431,6 +474,8 @@ class KalmanFilter(nn.Module):
|
|
|
431
474
|
|
|
432
475
|
# MATLAB-style: Ensure symmetry immediately after update (V = 0.5 * (V+V'))
|
|
433
476
|
Vu = ensure_real_and_symmetric(Vu)
|
|
477
|
+
# Cap maximum eigenvalue to prevent explosion
|
|
478
|
+
Vu = self._cap_max_eigenval(Vu, max_eigenval=1e6)
|
|
434
479
|
|
|
435
480
|
# Clean NaN/Inf before stabilization
|
|
436
481
|
if not self._check_finite(Vu, f"Vu at t={t}"):
|
|
@@ -830,6 +875,10 @@ class KalmanFilter(nn.Module):
|
|
|
830
875
|
min_eigenval = self.min_eigenval.item()
|
|
831
876
|
return ensure_covariance_stable(M, min_eigenval=min_eigenval, ensure_real=ensure_real)
|
|
832
877
|
|
|
878
|
+
def _cap_max_eigenval(self, M: torch.Tensor, max_eigenval: float = 1e6) -> torch.Tensor:
|
|
879
|
+
"""Cap maximum eigenvalue of matrix to prevent numerical explosion."""
|
|
880
|
+
return cap_max_eigenval(M, max_eigenval=max_eigenval, warn=False)
|
|
881
|
+
|
|
833
882
|
def _clean_matrix(
|
|
834
883
|
self,
|
|
835
884
|
M: torch.Tensor,
|
|
@@ -97,6 +97,51 @@ def ensure_symmetric(tensor: "torch.Tensor") -> "torch.Tensor":
|
|
|
97
97
|
return 0.5 * (tensor + tensor.T)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
+
def cap_max_eigenval(
|
|
101
|
+
M: "torch.Tensor",
|
|
102
|
+
max_eigenval: float = 1e6,
|
|
103
|
+
warn: bool = True
|
|
104
|
+
) -> "torch.Tensor":
|
|
105
|
+
"""Cap maximum eigenvalue of matrix to prevent numerical explosion.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
M : torch.Tensor
|
|
110
|
+
Matrix to cap (square matrix)
|
|
111
|
+
max_eigenval : float, default 1e6
|
|
112
|
+
Maximum allowed eigenvalue
|
|
113
|
+
warn : bool, default True
|
|
114
|
+
Whether to log warnings
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
torch.Tensor
|
|
119
|
+
Matrix with capped eigenvalues
|
|
120
|
+
"""
|
|
121
|
+
if M.numel() == 0 or M.shape[0] == 0:
|
|
122
|
+
return M
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
eigenvals = torch.linalg.eigvalsh(M)
|
|
126
|
+
max_eig = torch.max(eigenvals)
|
|
127
|
+
|
|
128
|
+
if max_eig > max_eigenval:
|
|
129
|
+
# Scale matrix to cap maximum eigenvalue
|
|
130
|
+
scale_factor = max_eigenval / max_eig
|
|
131
|
+
M = M * scale_factor
|
|
132
|
+
M = ensure_symmetric(M)
|
|
133
|
+
if warn:
|
|
134
|
+
_logger.warning(
|
|
135
|
+
f"Matrix maximum eigenvalue capped: {max_eig:.2e} -> {max_eigenval:.2e} "
|
|
136
|
+
f"(scale_factor={scale_factor:.2e})"
|
|
137
|
+
)
|
|
138
|
+
except (RuntimeError, ValueError):
|
|
139
|
+
# If eigendecomposition fails, return matrix as-is
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
return M
|
|
143
|
+
|
|
144
|
+
|
|
100
145
|
def ensure_real_and_symmetric(tensor: "torch.Tensor") -> "torch.Tensor":
|
|
101
146
|
"""Ensure matrix is real and symmetric.
|
|
102
147
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|