dfm-python 0.5.58__py3-none-any.whl → 0.5.59__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.
@@ -1233,7 +1233,7 @@ def em_step(
1233
1233
  _logger.info(f" E-step: Running Kalman filter + smoother (T={T}, N={N}, m={m}, ~{ops_estimate:.1f}B ops)...")
1234
1234
  _logger.info(f" E-step: Filter is fast (~1s), but smooth may take 5-7 minutes (bottleneck)")
1235
1235
  EZ, V_smooth, VVsmooth, loglik = kalman_filter.filter_and_smooth(X_masked)
1236
-
1236
+
1237
1237
  # Keep float64 for numerical stability (prevents V_smooth accumulation overflow)
1238
1238
  # Do NOT convert to float32 - maintain float64 precision throughout M-step
1239
1239
  # Float64 is critical for large state spaces (m=183) and long time series (T=2135)
dfm_python/models/dfm.py CHANGED
@@ -1319,7 +1319,33 @@ class DFM(BaseFactorModel):
1319
1319
 
1320
1320
  # Run filter and smooth on new data
1321
1321
  y_masked = np.ma.masked_invalid(data_new)
1322
- Z_new, V_smooth_new, _, _ = kalman_new.filter_and_smooth(y_masked)
1322
+
1323
+ # ------------------------------------------------------------------
1324
+ # IMPORTANT (pykalman quirk):
1325
+ # pykalman.KalmanFilter._parse_observations() transposes when input has
1326
+ # shape (1, n_dim_obs), interpreting it as (n_dim_obs, 1). For our
1327
+ # update() case, (1, N) means 1 timestep with N series. This transpose
1328
+ # causes n_dim_obs=1 and breaks CholeskyKalmanFilter internals.
1329
+ #
1330
+ # Workaround: for a single new timestep, use filter_update() directly
1331
+ # with a 1D observation vector (n_dim_obs,), avoiding _parse_observations.
1332
+ # ------------------------------------------------------------------
1333
+ if hasattr(data_new, "shape") and len(data_new.shape) == 2 and int(data_new.shape[0]) == 1:
1334
+ pk = getattr(kalman_new, "_pykalman", None)
1335
+ if pk is None:
1336
+ raise ModelNotTrainedError("Kalman filter not initialized for update()")
1337
+
1338
+ # 1D observation vector for this timestep
1339
+ obs = np.ma.masked_invalid(data_new[0])
1340
+ next_mean, _next_cov = pk.filter_update(
1341
+ filtered_state_mean=Z_last,
1342
+ filtered_state_covariance=V_last,
1343
+ observation=obs,
1344
+ )
1345
+ Z_new = np.asarray(next_mean, dtype=DEFAULT_DTYPE)[np.newaxis, :]
1346
+ V_smooth_new = None
1347
+ else:
1348
+ Z_new, V_smooth_new, _, _ = kalman_new.filter_and_smooth(y_masked)
1323
1349
 
1324
1350
  # Update model state: append new factors and data
1325
1351
  # Concatenate new factors to existing result.Z
@@ -1509,6 +1535,18 @@ class DFM(BaseFactorModel):
1509
1535
  except (ModelNotInitializedError, AttributeError):
1510
1536
  pass
1511
1537
  target_series, target_indices = resolve_target_series(dataset, series_ids, result, self.__class__.__name__)
1538
+
1539
+ # Fallback: If no Dataset is attached, use config.target_series.
1540
+ # This is needed for checkpoints used in external pipelines where dataset.pkl
1541
+ # is missing or not attached to the model instance.
1542
+ if (target_series is None or len(target_series) == 0 or target_indices is None) and getattr(self, "_config", None) is not None:
1543
+ cfg_ts = getattr(self._config, "target_series", None)
1544
+ if cfg_ts is not None:
1545
+ cfg_ts_list = cfg_ts if isinstance(cfg_ts, list) else [cfg_ts]
1546
+ cfg_indices = [series_ids.index(t) for t in cfg_ts_list if series_ids is not None and t in series_ids]
1547
+
1548
+ if cfg_indices:
1549
+ target_series, target_indices = cfg_ts_list, cfg_indices
1512
1550
 
1513
1551
  if target_series is None or len(target_series) == 0:
1514
1552
  raise ValueError(
@@ -1747,19 +1785,36 @@ class DFM(BaseFactorModel):
1747
1785
  raise FileNotFoundError(f"Checkpoint file not found: {path}")
1748
1786
 
1749
1787
  # Try loading with error handling for corrupted files
1788
+ # NOTE: Our training pipeline typically saves with joblib (for speed/compression),
1789
+ # but older checkpoints may be plain pickle. Support both.
1790
+ checkpoint = None
1750
1791
  try:
1751
- with open(path, 'rb') as f:
1752
- checkpoint = pickle.load(f)
1753
- except (pickle.UnpicklingError, EOFError, ValueError) as e:
1754
- raise RuntimeError(
1755
- f"Failed to load checkpoint from {path}. "
1756
- f"The file may be corrupted. Error: {e}. "
1757
- f"Try re-training the model to generate a new checkpoint."
1758
- ) from e
1792
+ import joblib # local import to avoid hard dependency at import-time
1793
+ checkpoint = joblib.load(path)
1794
+ except Exception:
1795
+ # Fallback to plain pickle
1796
+ try:
1797
+ with open(path, 'rb') as f:
1798
+ checkpoint = pickle.load(f)
1799
+ except (pickle.UnpicklingError, EOFError, ValueError) as e:
1800
+ raise RuntimeError(
1801
+ f"Failed to load checkpoint from {path}. "
1802
+ f"The file may be corrupted or saved in an unsupported format. Error: {e}. "
1803
+ f"Try re-training the model to generate a new checkpoint."
1804
+ ) from e
1759
1805
 
1760
1806
  # Use checkpoint config if not provided
1761
1807
  if config is None:
1762
1808
  config = checkpoint.get('config')
1809
+
1810
+ # If target_series was stored separately in the checkpoint (common in our pipeline),
1811
+ # inject it into config so predict() can resolve targets without a Dataset attached.
1812
+ try:
1813
+ ckpt_target_series = checkpoint.get('target_series')
1814
+ if config is not None and getattr(config, 'target_series', None) is None and ckpt_target_series is not None:
1815
+ config.target_series = ckpt_target_series if isinstance(ckpt_target_series, list) else [ckpt_target_series]
1816
+ except Exception:
1817
+ pass
1763
1818
 
1764
1819
  # Get model state (new format preferred, fallback to old format)
1765
1820
  model_state = checkpoint.get('model_state')
dfm_python/ssm/kalman.py CHANGED
@@ -204,7 +204,7 @@ class DFMKalmanFilter:
204
204
  "DFMKalmanFilter parameters not initialized. "
205
205
  "Call update_parameters() first."
206
206
  )
207
-
207
+
208
208
  # Get filtered states first (needed for smoother)
209
209
  transition_offsets = getattr(self._pykalman, 'transition_offsets', None)
210
210
  observation_offsets = getattr(self._pykalman, 'observation_offsets', None)
@@ -241,7 +241,7 @@ class DFMKalmanFilter:
241
241
 
242
242
  # Run filter
243
243
  predicted_state_means, predicted_state_covariances, _, filtered_state_means, filtered_state_covariances = run_filter()
244
-
244
+
245
245
  filter_time = time_module.time() - filter_start
246
246
  _logger.info(f" Filter: Completed in {filter_time:.2f}s ({filter_time/T*1000:.2f}ms/timestep)")
247
247
 
@@ -266,7 +266,7 @@ class DFMKalmanFilter:
266
266
  # This is much cheaper than full eigendecomposition (O(m²) vs O(m³))
267
267
  from ..config.constants import MIN_EIGENVALUE
268
268
  regularization = max(1e-6, MIN_EIGENVALUE * 100) # 1e-4 for stability
269
-
269
+
270
270
  _logger.info(f" Smooth: Stabilizing {len(predicted_state_covariances)} covariance matrices "
271
271
  f"(regularization={regularization:.2e})")
272
272
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.5.58
3
+ Version: 0.5.59
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
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Topic :: Scientific/Engineering
20
20
  Requires-Python: >=3.10
21
+ Requires-Dist: build>=1.4.0
21
22
  Requires-Dist: einops>=0.8.1
22
23
  Requires-Dist: hydra-core>=1.3.2
23
24
  Requires-Dist: matplotlib>=3.5.0
@@ -22,7 +22,7 @@ dfm_python/encoder/pca.py,sha256=C1U0bGOVdr-HQaJYD57MyA7RLaEr70LTXWgmo6P-sUU,105
22
22
  dfm_python/encoder/simple_autoencoder.py,sha256=9y5C6b3MEgNO3oZ0DjA2aX_AnBcX6eqG_9zFJPgHAjk,10717
23
23
  dfm_python/functional/__init__.py,sha256=J9EYUqQnstmY0Wq-g8v3Z3We01gOW99x6M9MxPcSp4w,329
24
24
  dfm_python/functional/dfm_block.py,sha256=lifzLA0fHudj-tnqCK4kMy09zVUtlJMPb2uw635yH4c,21006
25
- dfm_python/functional/em.py,sha256=WGZEuftelHgknSqi1Kb1GTo6rWDGWOkxjvcj1siYfCs,64168
25
+ dfm_python/functional/em.py,sha256=4U6ftX8PWnMdc_ODYuPSaVVD6rqY2ykkV5zBBdO9zU8,64164
26
26
  dfm_python/logger/__init__.py,sha256=ZsUPp5Gd_FTWvgtBweOMafxc14hZRSOdfG8yaYskRgI,1622
27
27
  dfm_python/logger/inference_logger.py,sha256=t6K1Di-QkqIvcbSUGW1sCeWD84ZTgoC27oB2FWaY1SI,10314
28
28
  dfm_python/logger/logger.py,sha256=drTD5o4VPb34YKjux7MaEnI_k5a93AOlhQciMZVTkAE,10379
@@ -30,7 +30,7 @@ dfm_python/logger/train_logger.py,sha256=BGtlJt-FP3WC4u_oGpfNrIYctIXamkB1gVyMhhn
30
30
  dfm_python/models/__init__.py,sha256=NHv79jHILrnJG4Auuo3fspi1FtLLedWfLS9GK9WZx6A,552
31
31
  dfm_python/models/base.py,sha256=igYjuGpcEsaDbsRbz3_8mvYkYuMKGZYdMqVJodylg0E,11024
32
32
  dfm_python/models/ddfm.py,sha256=AjjcCdyFo4LJSfDZjMVd_FNa0ggmI6VnvhZhkFb_tLU,50700
33
- dfm_python/models/dfm.py,sha256=uGG4m0cOK6cjzSA2-9YjRpf-rtMIx_6rMvyfRg_rh14,81261
33
+ dfm_python/models/dfm.py,sha256=36_Z6WabQC48SaCEBu7jSyU8IMSORCv1UanlsVUACVM,84407
34
34
  dfm_python/numeric/builder.py,sha256=zTaZFNqR0B7rb5hPkJs4I7beUYPMu44K9AaTIJniD8s,5852
35
35
  dfm_python/numeric/estimator.py,sha256=OPe7tAiTn0Jj_bL1ywOe47vJ37oeSMC2AtAX2FxLe5s,47534
36
36
  dfm_python/numeric/stability.py,sha256=PIR2XvxHGQ4uD1QuIkGGKuNmkfAasQdhoZlH2BWt56U,43922
@@ -38,7 +38,7 @@ dfm_python/numeric/statistic.py,sha256=Y9rrv18OKBiholy-R5Bz9tzGCkaziMn0Jk4Ca_KS0
38
38
  dfm_python/numeric/tent.py,sha256=tsmmYfj3UbV8caVUpeP-7dzIvslmTVl6p-l5X5--AqU,12229
39
39
  dfm_python/numeric/validator.py,sha256=FPgVC19PsWRgx40S9tDG0x0ccgiAUhnMFyf-IcfxOtA,39927
40
40
  dfm_python/ssm/__init__.py,sha256=AmmiJPlAy1bctM9AVso7eWKdPMrUbCfAZQHgqZv-KLY,784
41
- dfm_python/ssm/kalman.py,sha256=jJcY2AhFJGeELC5pbSRbT5uDN2mR7rC_sV44kNH-CMI,31771
41
+ dfm_python/ssm/kalman.py,sha256=uwkMzUDqQOrhTszxDXalQP3xl0856o00Uizeiry6_2U,31739
42
42
  dfm_python/utils/__init__.py,sha256=WLczOBMPw-_VIvuzoCGHouf-S5dcjhrUnnaTDfNouzQ,5382
43
43
  dfm_python/utils/checkpoint.py,sha256=npdYsNg74KE7wUtU25Wuqs846oXfwYxUJcEzKnh-k6o,11201
44
44
  dfm_python/utils/errors.py,sha256=BKMjYldXjdFg-O_pIBo1-WITInNXvNrHk-6IyFHMf-k,1861
@@ -47,6 +47,6 @@ dfm_python/utils/loss.py,sha256=1KcYwFJmY9l-Uh6Sk-dFpJ2kgMfm0u41Gmgu-5NzaAs,3878
47
47
  dfm_python/utils/metric.py,sha256=AC_TnJ3eIXiF_maNNexSHzXpLv5Hrf1W88X5pJx9C-Q,15327
48
48
  dfm_python/utils/misc.py,sha256=NprRqONFlqYE_LbIbPE6G0CDbYNGXKikaoH80SpVY4E,9596
49
49
  dfm_python/utils/validation.py,sha256=WGosO4AY7HnvUNpzAptzRWHXeu75Uzv-qiZjHSFgn8M,1437
50
- dfm_python-0.5.58.dist-info/METADATA,sha256=qMnsGZT2apMLpxsvv_iTjXR4Zc2P5JCiDwTvc9NNIIc,13103
51
- dfm_python-0.5.58.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
52
- dfm_python-0.5.58.dist-info/RECORD,,
50
+ dfm_python-0.5.59.dist-info/METADATA,sha256=wtA3kxn3Smr8abJUD8bi_sxxuBRlnOp9EJY_MrXIvVk,13131
51
+ dfm_python-0.5.59.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
52
+ dfm_python-0.5.59.dist-info/RECORD,,