dfm-python 0.6.0__tar.gz → 0.6.1__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.
Files changed (126) hide show
  1. {dfm_python-0.6.0 → dfm_python-0.6.1}/PKG-INFO +1 -1
  2. {dfm_python-0.6.0 → dfm_python-0.6.1}/config/dfm_macro.yaml +1 -1
  3. dfm_python-0.6.1/models/dfm_macro.pkl +0 -0
  4. {dfm_python-0.6.0 → dfm_python-0.6.1}/pyproject.toml +1 -1
  5. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/models/dfm/dfm.py +37 -7
  6. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/ssm/kalman.py +125 -23
  7. {dfm_python-0.6.0 → dfm_python-0.6.1}/uv.lock +1 -1
  8. {dfm_python-0.6.0 → dfm_python-0.6.1}/.cursor/agents/test-runner.md +0 -0
  9. {dfm_python-0.6.0 → dfm_python-0.6.1}/.git +0 -0
  10. {dfm_python-0.6.0 → dfm_python-0.6.1}/.gitignore +0 -0
  11. {dfm_python-0.6.0 → dfm_python-0.6.1}/.python-version +0 -0
  12. {dfm_python-0.6.0 → dfm_python-0.6.1}/=0.11.0 +0 -0
  13. {dfm_python-0.6.0 → dfm_python-0.6.1}/MANIFEST.in +0 -0
  14. {dfm_python-0.6.0 → dfm_python-0.6.1}/config/ddfm_exchange.yaml +0 -0
  15. {dfm_python-0.6.0 → dfm_python-0.6.1}/config/ddfm_finance.yaml +0 -0
  16. {dfm_python-0.6.0 → dfm_python-0.6.1}/config/ddfm_macro.yaml +0 -0
  17. {dfm_python-0.6.0 → dfm_python-0.6.1}/config/dfm_exchange.yaml +0 -0
  18. {dfm_python-0.6.0 → dfm_python-0.6.1}/config/dfm_finance.yaml +0 -0
  19. {dfm_python-0.6.0 → dfm_python-0.6.1}/data/exchange_rate.csv +0 -0
  20. {dfm_python-0.6.0 → dfm_python-0.6.1}/data/finance.csv +0 -0
  21. {dfm_python-0.6.0 → dfm_python-0.6.1}/data/macro.csv +0 -0
  22. {dfm_python-0.6.0 → dfm_python-0.6.1}/models/dfm_exchange.pkl +0 -0
  23. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/__init__.py +0 -0
  24. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/config/__init__.py +0 -0
  25. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/config/adapter.py +0 -0
  26. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/config/constants.py +0 -0
  27. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/config/schema/__init__.py +0 -0
  28. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/config/schema/block.py +0 -0
  29. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/config/schema/model.py +0 -0
  30. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/config/schema/params.py +0 -0
  31. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/config/schema/results.py +0 -0
  32. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/config/types.py +0 -0
  33. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/dataset/__init__.py +0 -0
  34. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/dataset/ddfm_dataset.py +0 -0
  35. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/dataset/dfm_dataset.py +0 -0
  36. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/dataset/time.py +0 -0
  37. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/decoder/__init__.py +0 -0
  38. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/decoder/base.py +0 -0
  39. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/decoder/linear.py +0 -0
  40. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/decoder/mlp.py +0 -0
  41. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/encoder/__init__.py +0 -0
  42. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/encoder/base.py +0 -0
  43. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/encoder/pca.py +0 -0
  44. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/encoder/simple_autoencoder.py +0 -0
  45. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/functional/__init__.py +0 -0
  46. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/functional/em.py +0 -0
  47. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/logger/__init__.py +0 -0
  48. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/logger/dfm_logger.py +0 -0
  49. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/logger/inference_logger.py +0 -0
  50. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/logger/logger.py +0 -0
  51. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/logger/train_logger.py +0 -0
  52. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/models/__init__.py +0 -0
  53. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/models/base.py +0 -0
  54. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/models/ddfm/ddfm.py +0 -0
  55. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/models/dfm/__init__.py +0 -0
  56. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/models/dfm/initialization.py +0 -0
  57. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/models/dfm/mixed_freq.py +0 -0
  58. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/models/dfm/tent.py +0 -0
  59. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/numeric/builder.py +0 -0
  60. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/numeric/estimator.py +0 -0
  61. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/numeric/stability.py +0 -0
  62. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/numeric/statistic.py +0 -0
  63. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/numeric/validator.py +0 -0
  64. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/ssm/__init__.py +0 -0
  65. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/utils/__init__.py +0 -0
  66. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/utils/checkpoint.py +0 -0
  67. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/utils/errors.py +0 -0
  68. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/utils/helper.py +0 -0
  69. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/utils/loss.py +0 -0
  70. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/utils/metric.py +0 -0
  71. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/utils/misc.py +0 -0
  72. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/dfm_python/utils/validation.py +0 -0
  73. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/__init__.py +0 -0
  74. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/config/__init__.py +0 -0
  75. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/config/schema/__init__.py +0 -0
  76. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/config/schema/test_block.py +0 -0
  77. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/config/schema/test_model.py +0 -0
  78. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/config/schema/test_results.py +0 -0
  79. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/config/test_adapter.py +0 -0
  80. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/config/test_constants.py +0 -0
  81. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/config/test_tent_weights_config.py +0 -0
  82. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/config/test_types.py +0 -0
  83. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/conftest.py +0 -0
  84. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/dataset/test_ddfm_dataset.py +0 -0
  85. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/dataset/test_dfm_dataset.py +0 -0
  86. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/decoder/__init__.py +0 -0
  87. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/decoder/test_linear.py +0 -0
  88. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/decoder/test_mlp.py +0 -0
  89. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/encoder/__init__.py +0 -0
  90. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/encoder/test_base.py +0 -0
  91. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/encoder/test_pca.py +0 -0
  92. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/encoder/test_simple_autoencoder.py +0 -0
  93. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/functional/__init__.py +0 -0
  94. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/functional/test_em.py +0 -0
  95. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/functional/test_em_correctness.py +0 -0
  96. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/functional/test_em_properties.py +0 -0
  97. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/functional/test_invariance.py +0 -0
  98. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/functional/test_macro_overflow.py +0 -0
  99. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/logger/__init__.py +0 -0
  100. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/logger/test_inference_logger.py +0 -0
  101. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/logger/test_logger.py +0 -0
  102. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/logger/test_train_logger.py +0 -0
  103. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/models/__init__.py +0 -0
  104. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/models/test_ddfm.py +0 -0
  105. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/models/test_dfm.py +0 -0
  106. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/numeric/__init__.py +0 -0
  107. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/numeric/test_builder.py +0 -0
  108. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/numeric/test_cov_rowvar_regression.py +0 -0
  109. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/numeric/test_estimator.py +0 -0
  110. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/numeric/test_initialization.py +0 -0
  111. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/numeric/test_stability.py +0 -0
  112. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/numeric/test_validator.py +0 -0
  113. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/ssm/__init__.py +0 -0
  114. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/ssm/test_kalman.py +0 -0
  115. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/ssm/test_kalman_correctness.py +0 -0
  116. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/utils/__init__.py +0 -0
  117. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/utils/test_errors.py +0 -0
  118. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/utils/test_helper.py +0 -0
  119. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/utils/test_metric.py +0 -0
  120. {dfm_python-0.6.0 → dfm_python-0.6.1}/src/test/utils/test_misc.py +0 -0
  121. {dfm_python-0.6.0 → dfm_python-0.6.1}/tutorial/tutorial_exchange_ddfm.py +0 -0
  122. {dfm_python-0.6.0 → dfm_python-0.6.1}/tutorial/tutorial_exchange_dfm.py +0 -0
  123. {dfm_python-0.6.0 → dfm_python-0.6.1}/tutorial/tutorial_finance_ddfm.py +0 -0
  124. {dfm_python-0.6.0 → dfm_python-0.6.1}/tutorial/tutorial_finance_dfm.py +0 -0
  125. {dfm_python-0.6.0 → dfm_python-0.6.1}/tutorial/tutorial_macro_ddfm.py +0 -0
  126. {dfm_python-0.6.0 → dfm_python-0.6.1}/tutorial/tutorial_macro_dfm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Dynamic Factor Model (DFM) estimation and forecasting 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
@@ -6,7 +6,7 @@
6
6
 
7
7
  # EM Algorithm Parameters
8
8
  threshold: 1.0e-05 # EM convergence threshold
9
- max_iter: 50 # Maximum EM iterations
9
+ max_iter: 5 # Maximum EM iterations
10
10
 
11
11
  # Numerical Stability Parameters
12
12
  clip_ar_coefficients: true
Binary file
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dfm-python"
3
- version = "0.6.0"
3
+ version = "0.6.1"
4
4
  description = "Dynamic Factor Model (DFM) estimation and forecasting in Python"
5
5
  requires-python = ">=3.10"
6
6
  license = {text = "MIT"}
@@ -539,13 +539,43 @@ class DFM(BaseFactorModel):
539
539
 
540
540
  # Fallback: compute smoothed factors (should rarely happen if EM completed successfully)
541
541
  _logger.debug("Computing smoothed factors (cache not available)")
542
- kalman_final = self._create_kalman_filter()
543
- y_masked = np.ma.masked_invalid(self.data_processed)
544
- # Use filter_and_smooth() with compute_loglik=False to save time (log-likelihood already known)
545
- # This prevents SVD convergence failures with ill-conditioned covariance matrices
546
- smoothed_state_means, _, _, _ = kalman_final.filter_and_smooth(y_masked, compute_loglik=False)
547
-
548
- return smoothed_state_means
542
+ try:
543
+ kalman_final = self._create_kalman_filter()
544
+ y_masked = np.ma.masked_invalid(self.data_processed)
545
+ # Use filter_and_smooth() with compute_loglik=False to save time (log-likelihood already known)
546
+ # This prevents SVD convergence failures with ill-conditioned covariance matrices
547
+ smoothed_state_means, _, _, _ = kalman_final.filter_and_smooth(y_masked, compute_loglik=False)
548
+ return smoothed_state_means
549
+ except (ValueError, FloatingPointError, OverflowError) as e:
550
+ if "infs or NaNs" in str(e) or "overflow" in str(e).lower() or "not contain infs" in str(e):
551
+ # Parameters are too unstable to recompute factors
552
+ # This can happen if EM converged to numerically unstable parameters
553
+ # Raise a clear error with diagnostic information
554
+ param_status = {}
555
+ if self.training_state is not None:
556
+ for name, param in [
557
+ ('A', self.training_state.A),
558
+ ('C', self.training_state.C),
559
+ ('Q', self.training_state.Q),
560
+ ('R', self.training_state.R),
561
+ ('V_0', self.training_state.V_0),
562
+ ]:
563
+ if param is not None:
564
+ n_inf = np.sum(np.isinf(param))
565
+ n_nan = np.sum(np.isnan(param))
566
+ if n_inf > 0 or n_nan > 0:
567
+ param_status[name] = f"{n_inf} Inf, {n_nan} NaN"
568
+
569
+ error_msg = (
570
+ f"Cannot compute smoothed factors: final parameters contain non-finite values. "
571
+ f"This indicates numerical instability in the fitted model. "
572
+ f"Parameter status: {param_status if param_status else 'all finite'}. "
573
+ f"Consider: (1) reducing model complexity (fewer factors/blocks), "
574
+ f"(2) increasing regularization, (3) checking data scaling."
575
+ )
576
+ raise NumericalError(error_msg, details=f"Original error: {type(e).__name__}: {e}") from e
577
+ else:
578
+ raise
549
579
 
550
580
  def get_result(self) -> DFMResult:
551
581
  """Extract DFMResult from trained model.
@@ -384,44 +384,146 @@ class DFMKalmanFilter:
384
384
  f"observation_offsets shape mismatch: expected ({N},), got {observation_offsets_final.shape}"
385
385
  )
386
386
 
387
+ # Validate parameters for NaN/Inf before calling _filter (prevents cryptic errors)
388
+ A = self._pykalman.transition_matrices
389
+ C = self._pykalman.observation_matrices
390
+ Q = self._pykalman.transition_covariance
391
+ R = self._pykalman.observation_covariance
392
+ Z_0 = self._pykalman.initial_state_mean
393
+ V_0 = self._pykalman.initial_state_covariance
394
+
395
+ # Check for non-finite values in parameters
396
+ param_checks = {
397
+ 'A (transition)': A,
398
+ 'C (observation)': C,
399
+ 'Q (process noise)': Q,
400
+ 'R (observation noise)': R,
401
+ 'Z_0 (initial state mean)': Z_0,
402
+ 'V_0 (initial state cov)': V_0,
403
+ }
404
+ non_finite_params = []
405
+ for name, param in param_checks.items():
406
+ if param is not None and not np.isfinite(param).all():
407
+ n_inf = np.sum(np.isinf(param)) if param is not None else 0
408
+ n_nan = np.sum(np.isnan(param)) if param is not None else 0
409
+ non_finite_params.append(f"{name}: {n_inf} Inf, {n_nan} NaN")
410
+
411
+ if non_finite_params:
412
+ # Try to recover by stabilizing parameters
413
+ _logger.warning(
414
+ f" Filter: Non-finite values detected in parameters: {', '.join(non_finite_params)}. "
415
+ f"Attempting to stabilize before filtering..."
416
+ )
417
+ # Stabilize all covariance matrices
418
+ if Q is not None and not np.isfinite(Q).all():
419
+ Q = ensure_symmetric(Q)
420
+ Q = np.where(np.isfinite(Q), Q, 0.0)
421
+ Q = cap_max_eigenval(Q, max_eigenval=MAX_EIGENVALUE, symmetric=True, warn=False)
422
+ if R is not None and not np.isfinite(R).all():
423
+ R = ensure_symmetric(R)
424
+ R = np.where(np.isfinite(R), R, np.diag(np.diag(R))) # Preserve diagonal structure
425
+ # Ensure R diagonal is positive
426
+ R_diag = np.diag(R)
427
+ R_diag = np.where(np.isfinite(R_diag) & (R_diag > 0), R_diag, 1.0) # Fallback to 1.0
428
+ np.fill_diagonal(R, R_diag)
429
+ R = cap_max_eigenval(R, max_eigenval=MAX_EIGENVALUE, symmetric=True, warn=False)
430
+ if V_0 is not None and not np.isfinite(V_0).all():
431
+ V_0 = ensure_symmetric(V_0)
432
+ V_0 = np.where(np.isfinite(V_0), V_0, 0.0)
433
+ V_0 = cap_max_eigenval(V_0, max_eigenval=MAX_EIGENVALUE / 10, symmetric=True, warn=False)
434
+ if A is not None and not np.isfinite(A).all():
435
+ A = np.where(np.isfinite(A), A, 0.0)
436
+ if C is not None and not np.isfinite(C).all():
437
+ C = np.where(np.isfinite(C), C, 0.0)
438
+ if Z_0 is not None and not np.isfinite(Z_0).all():
439
+ Z_0 = np.where(np.isfinite(Z_0), Z_0, 0.0)
440
+
387
441
  # Option 4: Try filter with current parameters, retry with more aggressive stabilization if overflow
388
442
  try:
389
443
  return _filter(
390
- self._pykalman.transition_matrices,
391
- self._pykalman.observation_matrices,
392
- self._pykalman.transition_covariance,
393
- self._pykalman.observation_covariance,
444
+ A,
445
+ C,
446
+ Q,
447
+ R,
394
448
  transition_offsets_final,
395
449
  observation_offsets_final,
396
- self._pykalman.initial_state_mean,
397
- self._pykalman.initial_state_covariance,
450
+ Z_0,
451
+ V_0,
398
452
  observations
399
453
  )
400
454
  except (ValueError, FloatingPointError, OverflowError) as e:
401
455
  if "infs or NaNs" in str(e) or "overflow" in str(e).lower():
402
456
  _logger.warning(
403
457
  f" Filter: Overflow detected in _filter ({type(e).__name__}: {e}). "
404
- f"Retrying with more aggressively stabilized V_0..."
458
+ f"Retrying with more aggressively stabilized parameters..."
405
459
  )
406
- # Retry with more aggressively stabilized V_0
407
- V_0_retry = self._pykalman.initial_state_covariance.copy()
408
- # Cap at smaller value for retry
460
+ # Retry with more aggressively stabilized parameters
461
+ V_0_retry = V_0.copy() if V_0 is not None else self._pykalman.initial_state_covariance.copy()
462
+ V_0_retry = ensure_symmetric(V_0_retry)
463
+ V_0_retry = np.where(np.isfinite(V_0_retry), V_0_retry, 0.0)
409
464
  V_0_retry = cap_max_eigenval(V_0_retry, max_eigenval=MAX_EIGENVALUE / 10, symmetric=True, warn=False)
410
- # Also ensure Q is well-bounded
411
- Q_retry = self._pykalman.transition_covariance.copy()
465
+
466
+ Q_retry = Q.copy() if Q is not None else self._pykalman.transition_covariance.copy()
467
+ Q_retry = ensure_symmetric(Q_retry)
468
+ Q_retry = np.where(np.isfinite(Q_retry), Q_retry, 0.0)
412
469
  Q_retry = cap_max_eigenval(Q_retry, max_eigenval=MAX_EIGENVALUE, symmetric=True, warn=False)
413
470
 
414
- return _filter(
415
- self._pykalman.transition_matrices,
416
- self._pykalman.observation_matrices,
417
- Q_retry,
418
- self._pykalman.observation_covariance,
419
- transition_offsets_final,
420
- observation_offsets_final,
421
- self._pykalman.initial_state_mean,
422
- V_0_retry,
423
- observations
424
- )
471
+ # Also stabilize R (critical: R appears in predicted_observation_covariance = C P C^T + R)
472
+ R_retry = R.copy() if R is not None else self._pykalman.observation_covariance.copy()
473
+ R_retry = ensure_symmetric(R_retry)
474
+ R_retry = np.where(np.isfinite(R_retry), R_retry, np.diag(np.diag(R_retry)))
475
+ # Ensure R diagonal is positive and finite
476
+ R_diag = np.diag(R_retry)
477
+ R_diag = np.where(np.isfinite(R_diag) & (R_diag > 0), R_diag, 1.0)
478
+ np.fill_diagonal(R_retry, R_diag)
479
+ R_retry = cap_max_eigenval(R_retry, max_eigenval=MAX_EIGENVALUE, symmetric=True, warn=False)
480
+
481
+ # Also stabilize A if it has eigenvalues > 1 (causes unbounded growth in prediction step)
482
+ # P_{t|t-1} = A P_{t-1|t-1} A^T + Q grows unbounded if A has eigenvalues > 1
483
+ A_retry = A.copy() if A is not None else self._pykalman.transition_matrices.copy()
484
+ if A_retry is not None:
485
+ A_retry = np.where(np.isfinite(A_retry), A_retry, 0.0)
486
+ # Check if A has eigenvalues > 1 (unstable)
487
+ try:
488
+ eigvals = np.linalg.eigvals(A_retry)
489
+ max_eig = np.max(np.abs(eigvals)) # Use absolute value for complex eigenvalues
490
+ if max_eig > 0.99: # Close to or above 1.0
491
+ _logger.warning(
492
+ f" Filter: A has max |eigenvalue| {max_eig:.2e} > 0.99, capping to 0.99 to prevent unbounded growth"
493
+ )
494
+ A_retry = cap_max_eigenval(A_retry, max_eigenval=0.99, symmetric=False, warn=False)
495
+ except (np.linalg.LinAlgError, ValueError):
496
+ # If eigendecomposition fails, just ensure finite and scale down
497
+ _logger.warning(" Filter: Could not compute A eigenvalues, scaling A by 0.9 as safety measure")
498
+ A_retry = A_retry * 0.9
499
+ else:
500
+ A_retry = A
501
+
502
+ try:
503
+ return _filter(
504
+ A_retry,
505
+ C,
506
+ Q_retry,
507
+ R_retry,
508
+ transition_offsets_final,
509
+ observation_offsets_final,
510
+ Z_0,
511
+ V_0_retry,
512
+ observations
513
+ )
514
+ except (ValueError, FloatingPointError, OverflowError) as e2:
515
+ # Even retry failed - parameters are too unstable
516
+ _logger.error(
517
+ f" Filter: Retry also failed with {type(e2).__name__}: {e2}. "
518
+ f"Parameters are too unstable to filter. This indicates severe numerical issues."
519
+ )
520
+ raise NumericalError(
521
+ "Kalman filter failed even after parameter stabilization. "
522
+ "Model parameters contain extreme values that cannot be filtered. "
523
+ "This typically indicates: (1) model is too complex for the data, "
524
+ "(2) data scaling issues, (3) numerical instability in EM convergence.",
525
+ details=f"Original error: {type(e).__name__}: {e}, Retry error: {type(e2).__name__}: {e2}"
526
+ ) from e2
425
527
  else:
426
528
  raise
427
529
 
@@ -875,7 +875,7 @@ wheels = [
875
875
 
876
876
  [[package]]
877
877
  name = "dfm-python"
878
- version = "0.6.0"
878
+ version = "0.6.1"
879
879
  source = { editable = "." }
880
880
  dependencies = [
881
881
  { name = "build" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes