dfm-python 0.5.1__py3-none-any.whl → 0.5.3__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 CHANGED
@@ -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.5.0"
68
+ __version__ = "0.5.2"
69
69
 
70
70
  # ============================================================================
71
71
  # PUBLIC API DEFINITION
dfm_python/encoder/vae.py CHANGED
@@ -82,13 +82,27 @@ if _has_torch:
82
82
  # Build layers
83
83
  prev_dim = input_dim
84
84
  for hidden_dim in hidden_dims:
85
- self.layers.append(nn.Linear(prev_dim, hidden_dim))
85
+ layer = nn.Linear(prev_dim, hidden_dim)
86
+ # Initialize weights using Xavier/Kaiming initialization for better training stability
87
+ # Use Kaiming for ReLU, Xavier for tanh/sigmoid
88
+ if activation == 'relu':
89
+ nn.init.kaiming_normal_(layer.weight, mode='fan_in', nonlinearity='relu')
90
+ else:
91
+ nn.init.xavier_normal_(layer.weight, gain=1.0)
92
+ # Initialize bias to small values
93
+ if layer.bias is not None:
94
+ nn.init.constant_(layer.bias, 0.0)
95
+ self.layers.append(layer)
86
96
  if use_batch_norm:
87
97
  self.batch_norms.append(nn.BatchNorm1d(hidden_dim))
88
98
  prev_dim = hidden_dim
89
99
 
90
- # Output layer (no activation, linear)
100
+ # Output layer (linear, no activation)
101
+ # Use smaller initialization for output layer to prevent large initial factors
91
102
  self.output_layer = nn.Linear(prev_dim, output_dim)
103
+ nn.init.xavier_normal_(self.output_layer.weight, gain=0.1) # Smaller gain for output
104
+ if self.output_layer.bias is not None:
105
+ nn.init.constant_(self.output_layer.bias, 0.0)
92
106
 
93
107
  def forward(self, x: torch.Tensor) -> torch.Tensor:
94
108
  """Forward pass through encoder.
@@ -124,7 +124,10 @@ class NoScalingStrategy:
124
124
  # Scaler Factory
125
125
  # ============================================================================
126
126
 
127
- def _create_scaler_transformer(scaler_type: Optional[str]) -> Any:
127
+ def _create_scaler_transformer(
128
+ scaler_type: Optional[str],
129
+ use_robust_as_default: bool = False
130
+ ) -> Any:
128
131
  """Create a raw sklearn scaler for unified scaling.
129
132
 
130
133
  Returns a raw sklearn scaler that can be used directly in TransformerPipeline.
@@ -160,10 +163,13 @@ def _create_scaler_transformer(scaler_type: Optional[str]) -> Any:
160
163
  )
161
164
 
162
165
  if scaler_type is None or scaler_type == 'none':
163
- # Passthrough: no scaling
164
- return FunctionTransformer(func=lambda x: x, inverse_func=lambda x: x)
165
-
166
- scaler_type_lower = scaler_type.lower()
166
+ if use_robust_as_default:
167
+ scaler_type_lower = 'robust'
168
+ else:
169
+ # Passthrough: no scaling
170
+ return FunctionTransformer(func=lambda x: x, inverse_func=lambda x: x)
171
+ else:
172
+ scaler_type_lower = scaler_type.lower()
167
173
 
168
174
  if scaler_type_lower == 'standard':
169
175
  return StandardScaler()
dfm_python/models/base.py CHANGED
@@ -590,11 +590,14 @@ class BaseFactorModel(pl.LightningModule):
590
590
  """
591
591
  pass
592
592
 
593
+ @abstractmethod
593
594
  def update(
594
595
  self,
595
596
  X_std: np.ndarray,
596
597
  *,
597
- kalman_filter: Optional[Any] = None
598
+ history: Optional[int] = None,
599
+ kalman_filter: Optional[Any] = None,
600
+ scaler: Optional[Any] = None
598
601
  ) -> 'BaseFactorModel':
599
602
  """Update factor state with standardized data.
600
603
 
@@ -608,8 +611,16 @@ class BaseFactorModel(pl.LightningModule):
608
611
  Standardized data array (T x N), where T is number of time periods
609
612
  and N is number of series. Data should already be standardized using
610
613
  result.Mx and result.Wx.
614
+ history : int, optional
615
+ Number of recent periods to use for factor state update. If None, uses
616
+ all provided data (default). If specified (e.g., 60), uses only the most
617
+ recent N periods. Initial state (Z_0, V_0) is always estimated from
618
+ full training data, but the update uses only recent history for efficiency.
611
619
  kalman_filter : Any, optional
612
620
  Kalman filter instance. If None, uses default or model's kalman filter.
621
+ scaler : Any, optional
622
+ If provided, attach/replace the model's scaler (e.g., refit on new regime).
623
+ When None, keep the existing scaler from training.
613
624
 
614
625
  Returns
615
626
  -------
@@ -620,48 +631,11 @@ class BaseFactorModel(pl.LightningModule):
620
631
  --------
621
632
  >>> # Update state with new data, then predict
622
633
  >>> model.update(X_std).predict(horizon=1)
623
- >>> # Or update and predict separately
624
- >>> model.update(X_std)
634
+ >>> # Or update with only recent 12 periods
635
+ >>> model.update(X_std, history=12)
625
636
  >>> forecast = model.predict(horizon=6)
626
637
  """
627
- self._check_trained()
628
-
629
- result = self.result # Use property which ensures non-None after _check_trained()
630
-
631
- # Validate input shape
632
- if not isinstance(X_std, np.ndarray):
633
- X_std = np.asarray(X_std)
634
- if X_std.ndim != 2:
635
- raise ValueError(
636
- f"{self.__class__.__name__} update(): X_std must be 2D array (T x N), "
637
- f"got shape {X_std.shape}"
638
- )
639
-
640
- # Handle NaN/Inf values
641
- X_std = np.where(np.isfinite(X_std), X_std, np.nan)
642
-
643
- # Update factor state (model-specific)
644
- if hasattr(self, 'encoder') and self.encoder is not None:
645
- # DDFM: Extract factors via encoder, then use Kalman filter
646
- Z_last_updated = self._update_factor_state_ddfm(
647
- X_std, result, kalman_filter
648
- )
649
- else:
650
- # DFM: Use Kalman filter directly on standardized data
651
- Z_last_updated = self._update_factor_state_dfm(
652
- X_std, result, kalman_filter or getattr(self, 'kalman', None)
653
- )
654
-
655
- # Update result.Z[-1, :] permanently
656
- if Z_last_updated is not None:
657
- result.Z[-1, :] = Z_last_updated
658
- else:
659
- _logger.warning(
660
- f"{self.__class__.__name__} update(): Failed to update factor state, "
661
- f"keeping current state"
662
- )
663
-
664
- return self
638
+ pass
665
639
 
666
640
  def get_result(self) -> BaseResult:
667
641
  """Extract result from trained model.
dfm_python/models/ddfm.py CHANGED
@@ -308,6 +308,10 @@ class DDFM(BaseFactorModel):
308
308
  decay_learning_rate: bool = True,
309
309
  min_obs_pretrain: int = 50,
310
310
  mult_epoch_pretrain: int = 1,
311
+ loss_function: str = 'mse',
312
+ huber_delta: float = 1.0,
313
+ weight_decay: float = 0.0,
314
+ grad_clip_val: float = 1.0,
311
315
  **kwargs
312
316
  ):
313
317
  """Initialize DDFM instance.
@@ -349,6 +353,19 @@ class DDFM(BaseFactorModel):
349
353
  mult_epoch_pretrain : int, default 1
350
354
  Multiplier for number of epochs during pre-training
351
355
  Display progress every 'disp' iterations
356
+ loss_function : str, default 'mse'
357
+ Loss function for training ('mse', 'huber').
358
+ 'mse': Mean squared error (default, matches original DDFM)
359
+ 'huber': Huber loss (more robust to outliers)
360
+ huber_delta : float, default 1.0
361
+ Delta parameter for Huber loss (only used if loss_function='huber').
362
+ Controls the transition point between quadratic and linear regions.
363
+ weight_decay : float, default 0.0
364
+ Weight decay (L2 regularization) for optimizer. Helps prevent overfitting to linear features.
365
+ Recommended: 1e-5 to 1e-3 for deeper encoders or when encoder collapses to linear behavior.
366
+ grad_clip_val : float, default 1.0
367
+ Maximum gradient norm for gradient clipping. Prevents training instability.
368
+ Set to 0.0 to disable gradient clipping.
352
369
  seed : int, optional
353
370
  Random seed for reproducibility
354
371
  **kwargs
@@ -383,6 +400,22 @@ class DDFM(BaseFactorModel):
383
400
  self.decay_learning_rate = decay_learning_rate
384
401
  self.min_obs_pretrain = min_obs_pretrain
385
402
  self.mult_epoch_pretrain = mult_epoch_pretrain
403
+ self.loss_function = loss_function.lower()
404
+ self.huber_delta = huber_delta
405
+ self.weight_decay = weight_decay
406
+ self.grad_clip_val = grad_clip_val
407
+
408
+ # Validate loss function
409
+ if self.loss_function not in ['mse', 'huber']:
410
+ raise ValueError(
411
+ f"DDFM initialization failed: loss_function must be 'mse' or 'huber', got '{loss_function}'"
412
+ )
413
+
414
+ # Validate gradient clipping value
415
+ if self.grad_clip_val < 0.0:
416
+ raise ValueError(
417
+ f"DDFM initialization failed: grad_clip_val must be >= 0.0, got {grad_clip_val}"
418
+ )
386
419
 
387
420
  # Determine number of factors
388
421
  # DDFM does not use block structure - num_factors is specified directly
@@ -586,7 +619,9 @@ class DDFM(BaseFactorModel):
586
619
 
587
620
  # Clip input data to prevent extreme values that cause NaN
588
621
  # Clip to reasonable range: -10 to 10 standard deviations
589
- data_clipped = torch.clamp(data, min=-10.0, max=10.0)
622
+ # For deeper networks, use slightly tighter clipping to improve stability
623
+ clip_range = 8.0 if len(self.encoder_layers) > 2 else 10.0
624
+ data_clipped = torch.clamp(data, min=-clip_range, max=clip_range)
590
625
 
591
626
  # Forward pass
592
627
  reconstructed = self.forward(data_clipped)
@@ -603,8 +638,23 @@ class DDFM(BaseFactorModel):
603
638
  # Compute loss with missing data masking
604
639
  mask = torch.isfinite(target)
605
640
  target_clean = torch.where(mask, target, torch.zeros_like(target))
606
- squared_diff = (target_clean - reconstructed) ** 2
607
- loss = torch.sum(squared_diff * mask) / (torch.sum(mask) + 1e-8)
641
+
642
+ # Use specified loss function
643
+ if self.loss_function == 'huber':
644
+ # Huber loss: more robust to outliers
645
+ # L_delta(a) = 0.5 * a^2 if |a| <= delta, else delta * (|a| - 0.5 * delta)
646
+ diff = target_clean - reconstructed
647
+ abs_diff = torch.abs(diff)
648
+ huber_loss = torch.where(
649
+ abs_diff <= self.huber_delta,
650
+ 0.5 * diff ** 2,
651
+ self.huber_delta * (abs_diff - 0.5 * self.huber_delta)
652
+ )
653
+ loss = torch.sum(huber_loss * mask) / (torch.sum(mask) + 1e-8)
654
+ else:
655
+ # MSE loss (default)
656
+ squared_diff = (target_clean - reconstructed) ** 2
657
+ loss = torch.sum(squared_diff * mask) / (torch.sum(mask) + 1e-8)
608
658
 
609
659
  # Handle NaN/Inf in loss
610
660
  if not torch.isfinite(loss):
@@ -614,6 +664,9 @@ class DDFM(BaseFactorModel):
614
664
  # Log metrics
615
665
  self.log('train_loss', loss, on_step=True, on_epoch=True, prog_bar=True)
616
666
 
667
+ # Note: Gradient clipping is handled automatically by Lightning trainer if gradient_clip_val is set
668
+ # The grad_clip_val parameter is used in pre_train() and MCMC training for manual training loops
669
+
617
670
  return loss
618
671
 
619
672
 
@@ -697,7 +750,8 @@ class DDFM(BaseFactorModel):
697
750
 
698
751
  optimizer = torch.optim.Adam(
699
752
  list(self.encoder.parameters()) + list(self.decoder.parameters()),
700
- lr=self.learning_rate
753
+ lr=self.learning_rate,
754
+ weight_decay=self.weight_decay
701
755
  )
702
756
 
703
757
  if self.decay_learning_rate:
@@ -750,7 +804,8 @@ class DDFM(BaseFactorModel):
750
804
 
751
805
  optimizer = torch.optim.Adam(
752
806
  list(self.encoder.parameters()) + list(self.decoder.parameters()),
753
- lr=lr
807
+ lr=lr,
808
+ weight_decay=self.weight_decay
754
809
  )
755
810
 
756
811
  return optimizer
@@ -842,7 +897,8 @@ class DDFM(BaseFactorModel):
842
897
  # Create optimizer for pre-training
843
898
  optimizer = torch.optim.Adam(
844
899
  list(self.encoder.parameters()) + list(self.decoder.parameters()),
845
- lr=self.learning_rate
900
+ lr=self.learning_rate,
901
+ weight_decay=self.weight_decay
846
902
  )
847
903
 
848
904
  # Pre-train for epochs * mult_epoch_pretrain
@@ -889,10 +945,11 @@ class DDFM(BaseFactorModel):
889
945
  loss.backward()
890
946
 
891
947
  # Gradient clipping for stability
892
- torch.nn.utils.clip_grad_norm_(
893
- list(self.encoder.parameters()) + list(self.decoder.parameters()),
894
- max_norm=1.0
895
- )
948
+ if self.grad_clip_val > 0.0:
949
+ torch.nn.utils.clip_grad_norm_(
950
+ list(self.encoder.parameters()) + list(self.decoder.parameters()),
951
+ max_norm=self.grad_clip_val
952
+ )
896
953
 
897
954
  optimizer.step()
898
955
 
@@ -1388,12 +1445,120 @@ class DDFM(BaseFactorModel):
1388
1445
  Mx=Mx
1389
1446
  )
1390
1447
 
1448
+ # Convert to numpy (handles torch inputs) and attempt scaler inverse_transform if available
1449
+ X_forecast = np.asarray(
1450
+ X_forecast.detach().cpu().numpy() if hasattr(X_forecast, "detach") else X_forecast
1451
+ )
1452
+ scaler = getattr(self, "scaler", None)
1453
+ if scaler is not None and hasattr(scaler, "inverse_transform"):
1454
+ try:
1455
+ X_forecast = scaler.inverse_transform(X_forecast)
1456
+ except Exception as e:
1457
+ _logger.warning(
1458
+ f"DDFM prediction: scaler.inverse_transform failed, returning unstandardized values. "
1459
+ f"error={e}"
1460
+ )
1461
+
1391
1462
  if return_series and return_factors:
1392
1463
  return X_forecast, Z_forecast
1393
1464
  if return_series:
1394
1465
  return X_forecast
1395
1466
  return Z_forecast
1396
1467
 
1468
+ def update(
1469
+ self,
1470
+ X_std: np.ndarray,
1471
+ *,
1472
+ history: Optional[int] = None,
1473
+ kalman_filter: Optional[Any] = None,
1474
+ scaler: Optional[Any] = None
1475
+ ) -> 'DDFM':
1476
+ """Update factor state with standardized data.
1477
+
1478
+ This method permanently updates the last factor state (result.Z[-1, :])
1479
+ using the provided standardized data. Users should handle all preprocessing
1480
+ (masking, imputation, standardization) before calling this method.
1481
+
1482
+ Parameters
1483
+ ----------
1484
+ X_std : np.ndarray
1485
+ Standardized data array (T x N), where T is number of time periods
1486
+ and N is number of series. Data should already be standardized using
1487
+ result.Mx and result.Wx.
1488
+ history : int, optional
1489
+ Number of recent periods to use for factor state update. If None, uses
1490
+ all provided data (default). If specified (e.g., 60), uses only the most
1491
+ recent N periods. Initial state (Z_0, V_0) is always estimated from
1492
+ full training data, but the update uses only recent history for efficiency.
1493
+ kalman_filter : Any, optional
1494
+ Kalman filter instance. If None, uses default or model's kalman filter.
1495
+
1496
+ Returns
1497
+ -------
1498
+ DDFM
1499
+ Self for method chaining
1500
+
1501
+ Examples
1502
+ --------
1503
+ >>> # Update state with new data, then predict
1504
+ >>> model.update(X_std).predict(horizon=1)
1505
+ >>> # Or update with only recent 12 periods
1506
+ >>> model.update(X_std, history=12)
1507
+ >>> forecast = model.predict(horizon=6)
1508
+ """
1509
+ self._check_trained()
1510
+
1511
+ # Optionally replace scaler (e.g., if refit on new regime)
1512
+ if scaler is not None:
1513
+ self.scaler = scaler
1514
+
1515
+ result = self.result # Use property which ensures non-None after _check_trained()
1516
+
1517
+ # Validate input shape
1518
+ if not isinstance(X_std, np.ndarray):
1519
+ X_std = np.asarray(X_std)
1520
+ if X_std.ndim != 2:
1521
+ raise ValueError(
1522
+ f"DDFM update(): X_std must be 2D array (T x N), "
1523
+ f"got shape {X_std.shape}"
1524
+ )
1525
+
1526
+ # Handle NaN/Inf values
1527
+ X_std = np.where(np.isfinite(X_std), X_std, np.nan)
1528
+
1529
+ # Filter to recent history if specified
1530
+ # Note: Initial state (Z_0, V_0) from result is estimated from full training data,
1531
+ # but we use only recent history for the update step
1532
+ if history is not None and history > 0:
1533
+ if X_std.shape[0] > history:
1534
+ X_recent = X_std[-history:, :]
1535
+ _logger.debug(
1536
+ f"DDFM update(): Using {history} most recent periods out of {X_std.shape[0]} total periods"
1537
+ )
1538
+ else:
1539
+ X_recent = X_std
1540
+ _logger.debug(
1541
+ f"DDFM update(): history={history} specified but data has only {X_std.shape[0]} periods, using all data"
1542
+ )
1543
+ else:
1544
+ X_recent = X_std
1545
+
1546
+ # Update factor state using encoder to extract factors, then Kalman filter
1547
+ Z_last_updated = self._update_factor_state_ddfm(
1548
+ X_recent, result, kalman_filter
1549
+ )
1550
+
1551
+ # Update result.Z[-1, :] permanently
1552
+ if Z_last_updated is not None:
1553
+ result.Z[-1, :] = Z_last_updated
1554
+ else:
1555
+ _logger.warning(
1556
+ f"DDFM update(): Failed to update factor state, "
1557
+ f"keeping current state"
1558
+ )
1559
+
1560
+ return self
1561
+
1397
1562
  @property
1398
1563
  def result(self) -> DDFMResult:
1399
1564
  """Get model result from training state.
dfm_python/models/dfm.py CHANGED
@@ -23,11 +23,15 @@ from ..config import (
23
23
  MergedConfigSource,
24
24
  )
25
25
  from ..config.results import DFMResult, FitParams
26
+ from ..config.utils import get_agg_structure, get_tent_weights, FREQUENCY_HIERARCHY, TENT_WEIGHTS_LOOKUP
26
27
  from ..logger import get_logger
27
28
  from ..ssm.em import EMAlgorithm, EMStepParams
28
29
  from ..ssm.kalman import KalmanFilter
29
30
  from .base import BaseFactorModel
30
31
 
32
+ # Frequency to integer mapping for tensor conversion
33
+ _FREQ_TO_INT = {'d': 1, 'w': 2, 'm': 3, 'q': 4, 'sa': 5, 'a': 6}
34
+
31
35
  if TYPE_CHECKING:
32
36
  from omegaconf import DictConfig
33
37
  from ..lightning import DFMDataModule
@@ -89,6 +93,8 @@ class DFM(BaseFactorModel):
89
93
  max_iter: int = 100,
90
94
  nan_method: int = 2,
91
95
  nan_k: int = 3,
96
+ tent_weights_dict: Optional[dict] = None,
97
+ mixed_freq: bool = False,
92
98
  **kwargs
93
99
  ):
94
100
  """Initialize DFM instance.
@@ -107,6 +113,12 @@ class DFM(BaseFactorModel):
107
113
  Missing data handling method
108
114
  nan_k : int, default 3
109
115
  Spline interpolation order
116
+ tent_weights_dict : dict, optional
117
+ [DEPRECATED] Optional tent weights to control aggregation. Use `mixed_freq` parameter instead.
118
+ Kept for backward compatibility only.
119
+ mixed_freq : bool, default False
120
+ If True, use tent kernels for mixed-frequency data. If False, treat all series as clock frequency.
121
+ When True, raises ValueError if any frequency pair is not in TENT_WEIGHTS_LOOKUP.
110
122
  **kwargs
111
123
  Additional arguments passed to BaseFactorModel
112
124
  """
@@ -119,6 +131,20 @@ class DFM(BaseFactorModel):
119
131
  self.max_iter = max_iter
120
132
  self.nan_method = nan_method
121
133
  self.nan_k = nan_k
134
+ self.mixed_freq = mixed_freq
135
+
136
+ # Legacy tent_weights_dict parameter (deprecated - use mixed_freq instead)
137
+ # Only used when mixed_freq=False for backward compatibility
138
+ self._legacy_tent_weights_dict = tent_weights_dict
139
+
140
+ # Mixed frequency parameters (set during initialize_from_data)
141
+ self._em_R_mat = None
142
+ self._em_q = None
143
+ self._em_nQ = 0
144
+ self._em_tent_weights_dict = None
145
+ self._em_frequencies = None
146
+ self._em_i_idio = None
147
+ self._em_idio_chain_lengths = None
122
148
 
123
149
  # Determine number of factors
124
150
  if num_factors is None:
@@ -178,6 +204,53 @@ class DFM(BaseFactorModel):
178
204
  # Parameters will be initialized during fit_em() or first training step
179
205
  pass
180
206
 
207
+ def _create_em_step_params(
208
+ self,
209
+ y: torch.Tensor,
210
+ device: torch.device,
211
+ dtype: torch.dtype
212
+ ) -> EMStepParams:
213
+ """Create EM step parameters using stored mixed frequency parameters.
214
+
215
+ Parameters
216
+ ----------
217
+ y : torch.Tensor
218
+ Data tensor (N x T)
219
+ device : torch.device
220
+ Device for tensors
221
+ dtype : torch.dtype
222
+ Data type for tensors
223
+
224
+ Returns
225
+ -------
226
+ EMStepParams
227
+ EM step parameters
228
+ """
229
+ clock = getattr(self.config, 'clock', 'm')
230
+ N = y.shape[0]
231
+
232
+ return EMStepParams(
233
+ y=y,
234
+ A=self.A,
235
+ C=self.C,
236
+ Q=self.Q,
237
+ R=self.R,
238
+ Z_0=self.Z_0,
239
+ V_0=self.V_0,
240
+ r=self.r.to(device),
241
+ p=self.p,
242
+ R_mat=self._em_R_mat,
243
+ q=self._em_q,
244
+ nQ=self._em_nQ,
245
+ i_idio=self._em_i_idio if self._em_i_idio is not None else torch.ones(N, device=device, dtype=dtype),
246
+ blocks=self.blocks.to(device),
247
+ tent_weights_dict=self._em_tent_weights_dict,
248
+ clock=clock,
249
+ frequencies=self._em_frequencies,
250
+ idio_chain_lengths=self._em_idio_chain_lengths if self._em_idio_chain_lengths is not None else torch.zeros(N, device=device, dtype=dtype),
251
+ config=self.config
252
+ )
253
+
181
254
  def initialize_from_data(self, X: torch.Tensor) -> None:
182
255
  """Initialize parameters from data using PCA and OLS.
183
256
 
@@ -187,22 +260,100 @@ class DFM(BaseFactorModel):
187
260
  Standardized data (T x N)
188
261
  """
189
262
  opt_nan = {'method': self.nan_method, 'k': self.nan_k}
263
+ clock = getattr(self.config, 'clock', 'm')
190
264
 
191
- # Use self.em.initialize_parameters() with direct tensor operations (no CPU transfers)
265
+ # Handle mixed_freq parameter
266
+ if self.mixed_freq:
267
+ # Use tent kernels for mixed-frequency data
268
+ agg_structure = get_agg_structure(self.config, clock=clock)
269
+
270
+ # Validate that all required frequency pairs are in TENT_WEIGHTS_LOOKUP
271
+ frequencies_list = [s.frequency for s in self.config.series]
272
+ frequencies_set = set(frequencies_list)
273
+ clock_hierarchy = FREQUENCY_HIERARCHY.get(clock, 3)
274
+
275
+ missing_pairs = []
276
+ for freq in frequencies_set:
277
+ freq_hierarchy = FREQUENCY_HIERARCHY.get(freq, 3)
278
+ if freq_hierarchy > clock_hierarchy:
279
+ # This frequency is slower than clock, needs tent kernel
280
+ tent_w = get_tent_weights(freq, clock)
281
+ if tent_w is None:
282
+ missing_pairs.append((freq, clock))
283
+
284
+ if missing_pairs:
285
+ raise ValueError(
286
+ f"mixed_freq=True but the following frequency pairs are not in TENT_WEIGHTS_LOOKUP: {missing_pairs}. "
287
+ f"Available pairs: {list(TENT_WEIGHTS_LOOKUP.keys())}. "
288
+ f"Either add the missing pairs to TENT_WEIGHTS_LOOKUP or set mixed_freq=False."
289
+ )
290
+
291
+ # Convert tent_weights to torch tensors
292
+ tent_weights_dict = {k: torch.tensor(v, dtype=torch.float32, device=X.device)
293
+ for k, v in agg_structure['tent_weights'].items()}
294
+
295
+ # Get R_mat and q from first structure (if any)
296
+ R_mat = None
297
+ q = None
298
+ if agg_structure['structures']:
299
+ first_structure = list(agg_structure['structures'].values())[0]
300
+ R_mat = torch.tensor(first_structure[0], dtype=torch.float32, device=X.device)
301
+ q = torch.tensor(first_structure[1], dtype=torch.float32, device=X.device)
302
+
303
+ # Create frequencies array
304
+ frequencies_array = np.array(frequencies_list, dtype=object)
305
+
306
+ # Count slower-frequency series
307
+ nQ = sum(1 for freq in frequencies_list
308
+ if FREQUENCY_HIERARCHY.get(freq, 3) > clock_hierarchy)
309
+
310
+ # Compute i_idio (1 for clock frequency, 0 for slower frequencies)
311
+ i_idio = torch.tensor([1 if freq == clock else 0 for freq in frequencies_list],
312
+ dtype=torch.float32, device=X.device)
313
+ else:
314
+ # Treat all series as clock frequency (unified frequency)
315
+ tent_weights_dict = self._legacy_tent_weights_dict
316
+ R_mat = None
317
+ q = None
318
+ frequencies_array = None
319
+ nQ = 0
320
+ i_idio = torch.ones(X.shape[1], dtype=torch.float32, device=X.device)
321
+
322
+ # Convert frequencies to torch tensor
323
+ frequencies_tensor = None
324
+ if frequencies_array is not None:
325
+ frequencies_tensor = torch.tensor(
326
+ [_FREQ_TO_INT.get(f, 3) for f in frequencies_array],
327
+ dtype=torch.int32,
328
+ device=X.device
329
+ )
330
+
331
+ # Store for reuse in EM steps
332
+ self._em_R_mat = R_mat
333
+ self._em_q = q
334
+ self._em_nQ = nQ
335
+ self._em_tent_weights_dict = tent_weights_dict
336
+ self._em_frequencies = frequencies_tensor
337
+ self._em_i_idio = i_idio
338
+
339
+ # Idiosyncratic chain lengths (currently unused, set to zeros)
340
+ self._em_idio_chain_lengths = torch.zeros(X.shape[1], dtype=torch.int32, device=X.device)
341
+
342
+ # Initialize parameters using EM algorithm
192
343
  A, C, Q, R, Z_0, V_0 = self.em.initialize_parameters(
193
344
  X,
194
345
  r=self.r.to(X.device),
195
346
  p=self.p,
196
347
  blocks=self.blocks.to(X.device),
197
348
  opt_nan=opt_nan,
198
- R_mat=None,
199
- q=None,
200
- nQ=0,
201
- i_idio=None,
202
- clock=getattr(self.config, 'clock', 'm'),
203
- tent_weights_dict=None,
204
- frequencies=None,
205
- idio_chain_lengths=None,
349
+ R_mat=R_mat,
350
+ q=q,
351
+ nQ=nQ,
352
+ i_idio=i_idio,
353
+ clock=clock,
354
+ tent_weights_dict=tent_weights_dict,
355
+ frequencies=frequencies_tensor,
356
+ idio_chain_lengths=self._em_idio_chain_lengths,
206
357
  config=self.config
207
358
  )
208
359
 
@@ -264,28 +415,8 @@ class DFM(BaseFactorModel):
264
415
  # EM expects y as (N x T), but data is (T x N)
265
416
  y = data.T # (N x T)
266
417
 
267
- # Create EM step parameters
268
- em_params = EMStepParams(
269
- y=y,
270
- A=self.A,
271
- C=self.C,
272
- Q=self.Q,
273
- R=self.R,
274
- Z_0=self.Z_0,
275
- V_0=self.V_0,
276
- r=self.r.to(y.device),
277
- p=self.p,
278
- R_mat=None,
279
- q=None,
280
- nQ=0,
281
- i_idio=torch.ones(y.shape[0], device=y.device, dtype=y.dtype),
282
- blocks=self.blocks.to(y.device),
283
- tent_weights_dict={},
284
- clock=getattr(self.config, 'clock', 'm'),
285
- frequencies=None,
286
- idio_chain_lengths=torch.zeros(y.shape[0], device=y.device, dtype=y.dtype),
287
- config=self.config
288
- )
418
+ # Create EM step parameters using stored mixed frequency parameters
419
+ em_params = self._create_em_step_params(y, y.device, y.dtype)
289
420
 
290
421
  # Perform EM step - use self.em(...) instead of em_step(...)
291
422
  C_new, R_new, A_new, Q_new, Z_0_new, V_0_new, loglik = self.em(em_params)
@@ -387,28 +518,8 @@ class DFM(BaseFactorModel):
387
518
 
388
519
  # EM loop
389
520
  while num_iter < self.max_iter and not converged:
390
- # Create EM step parameters
391
- em_params = EMStepParams(
392
- y=y,
393
- A=self.A,
394
- C=self.C,
395
- Q=self.Q,
396
- R=self.R,
397
- Z_0=self.Z_0,
398
- V_0=self.V_0,
399
- r=self.r.to(device),
400
- p=self.p,
401
- R_mat=None,
402
- q=None,
403
- nQ=0,
404
- i_idio=torch.ones(y.shape[0], device=device, dtype=dtype),
405
- blocks=self.blocks.to(device),
406
- tent_weights_dict={},
407
- clock=getattr(self.config, 'clock', 'm'),
408
- frequencies=None,
409
- idio_chain_lengths=torch.zeros(y.shape[0], device=device, dtype=dtype),
410
- config=self.config
411
- )
521
+ # Create EM step parameters using stored mixed frequency parameters
522
+ em_params = self._create_em_step_params(y, device, dtype)
412
523
 
413
524
  # Perform EM step - use self.em(...) instead of em_step(...)
414
525
  C_new, R_new, A_new, Q_new, Z_0_new, V_0_new, loglik = self.em(em_params)
@@ -832,6 +943,21 @@ class DFM(BaseFactorModel):
832
943
  f"(max deviation: {max_deviation:.1f} std devs). "
833
944
  f"Possible numerical instability."
834
945
  )
946
+
947
+ # Convert to numpy (handles torch inputs) and attempt scaler inverse_transform if available
948
+ X_forecast = np.asarray(
949
+ X_forecast.detach().cpu().numpy() if hasattr(X_forecast, "detach") else X_forecast
950
+ )
951
+ scaler = getattr(self, "scaler", None)
952
+ if scaler is not None and hasattr(scaler, "inverse_transform"):
953
+ try:
954
+ X_forecast = scaler.inverse_transform(X_forecast)
955
+ except Exception as e:
956
+ _logger.warning(
957
+ f"DFM prediction: scaler.inverse_transform failed, returning unstandardized values. "
958
+ f"error={e}"
959
+ )
960
+
835
961
  if return_factors and np.any(~np.isfinite(Z_forecast)):
836
962
  nan_count = np.sum(~np.isfinite(Z_forecast))
837
963
  raise ValueError(
@@ -846,6 +972,100 @@ class DFM(BaseFactorModel):
846
972
  return X_forecast
847
973
  return Z_forecast
848
974
 
975
+ def update(
976
+ self,
977
+ X_std: np.ndarray,
978
+ *,
979
+ history: Optional[int] = None,
980
+ kalman_filter: Optional[Any] = None,
981
+ scaler: Optional[Any] = None
982
+ ) -> 'DFM':
983
+ """Update factor state with standardized data.
984
+
985
+ This method permanently updates the last factor state (result.Z[-1, :])
986
+ using the provided standardized data. Users should handle all preprocessing
987
+ (masking, imputation, standardization) before calling this method.
988
+
989
+ Parameters
990
+ ----------
991
+ X_std : np.ndarray
992
+ Standardized data array (T x N), where T is number of time periods
993
+ and N is number of series. Data should already be standardized using
994
+ result.Mx and result.Wx.
995
+ history : int, optional
996
+ Number of recent periods to use for factor state update. If None, uses
997
+ all provided data (default). If specified (e.g., 60), uses only the most
998
+ recent N periods. Initial state (Z_0, V_0) is always estimated from
999
+ full training data, but the update uses only recent history for efficiency.
1000
+ kalman_filter : Any, optional
1001
+ Kalman filter instance. If None, uses default or model's kalman filter.
1002
+
1003
+ Returns
1004
+ -------
1005
+ DFM
1006
+ Self for method chaining
1007
+
1008
+ Examples
1009
+ --------
1010
+ >>> # Update state with new data, then predict
1011
+ >>> model.update(X_std).predict(horizon=1)
1012
+ >>> # Or update with only recent 12 periods
1013
+ >>> model.update(X_std, history=12)
1014
+ >>> forecast = model.predict(horizon=6)
1015
+ """
1016
+ self._check_trained()
1017
+
1018
+ # Optionally replace scaler (e.g., if refit on new regime)
1019
+ if scaler is not None:
1020
+ self.scaler = scaler
1021
+
1022
+ result = self.result # Use property which ensures non-None after _check_trained()
1023
+
1024
+ # Validate input shape
1025
+ if not isinstance(X_std, np.ndarray):
1026
+ X_std = np.asarray(X_std)
1027
+ if X_std.ndim != 2:
1028
+ raise ValueError(
1029
+ f"DFM update(): X_std must be 2D array (T x N), "
1030
+ f"got shape {X_std.shape}"
1031
+ )
1032
+
1033
+ # Handle NaN/Inf values
1034
+ X_std = np.where(np.isfinite(X_std), X_std, np.nan)
1035
+
1036
+ # Filter to recent history if specified
1037
+ # Note: Initial state (Z_0, V_0) from result is estimated from full training data,
1038
+ # but we use only recent history for the update step
1039
+ if history is not None and history > 0:
1040
+ if X_std.shape[0] > history:
1041
+ X_recent = X_std[-history:, :]
1042
+ _logger.debug(
1043
+ f"DFM update(): Using {history} most recent periods out of {X_std.shape[0]} total periods"
1044
+ )
1045
+ else:
1046
+ X_recent = X_std
1047
+ _logger.debug(
1048
+ f"DFM update(): history={history} specified but data has only {X_std.shape[0]} periods, using all data"
1049
+ )
1050
+ else:
1051
+ X_recent = X_std
1052
+
1053
+ # Update factor state using Kalman filter directly on standardized data
1054
+ Z_last_updated = self._update_factor_state_dfm(
1055
+ X_recent, result, kalman_filter or getattr(self, 'kalman', None)
1056
+ )
1057
+
1058
+ # Update result.Z[-1, :] permanently
1059
+ if Z_last_updated is not None:
1060
+ result.Z[-1, :] = Z_last_updated
1061
+ else:
1062
+ _logger.warning(
1063
+ f"DFM update(): Failed to update factor state, "
1064
+ f"keeping current state"
1065
+ )
1066
+
1067
+ return self
1068
+
849
1069
  @property
850
1070
  def result(self) -> DFMResult:
851
1071
  """Get model result from training state.
dfm_python/models/mcmc.py CHANGED
@@ -347,10 +347,11 @@ class DDFMMCMCTrainer:
347
347
  loss = torch.sum(squared_diff) / (torch.sum(mask) + 1e-8)
348
348
  loss.backward()
349
349
  # Gradient clipping to prevent NaN and improve stability
350
- torch.nn.utils.clip_grad_norm_(
351
- list(self.model.encoder.parameters()) + list(self.model.decoder.parameters()),
352
- max_norm=1.0
353
- )
350
+ if self.model.grad_clip_val > 0.0:
351
+ torch.nn.utils.clip_grad_norm_(
352
+ list(self.model.encoder.parameters()) + list(self.model.decoder.parameters()),
353
+ max_norm=self.model.grad_clip_val
354
+ )
354
355
  optimizer.step()
355
356
  # Extract factors from this sample
356
357
  x_sample_tensor = torch.tensor(x_sim_den[i, :, :], device=device, dtype=dtype)
dfm_python/ssm/em.py CHANGED
@@ -570,7 +570,7 @@ class EMAlgorithm(nn.Module):
570
570
  resNaN = x_clean.clone()
571
571
  resNaN[indNaN] = torch.nan
572
572
 
573
- # Determine tent kernel size (pC) for quarterly-monthly aggregation
573
+ # Determine tent kernel size (pC) for slower-frequency aggregation
574
574
  pC = 5 # Default: quarterly to monthly uses 5 periods [1,2,3,2,1]
575
575
  if R_mat is not None:
576
576
  pC = R_mat.shape[1]
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dfm-python
3
- Version: 0.5.1
3
+ Version: 0.5.3
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
7
7
  Project-URL: Repository, https://github.com/yourusername/dfm-python
8
8
  Project-URL: Issues, https://github.com/yourusername/dfm-python/issues
9
- Author: DFM Python Contributors
9
+ Author: Minkey Chang
10
10
  License: MIT
11
11
  License-File: LICENSE
12
12
  Keywords: dfm,dynamic-factor-model,econometrics,forecasting,nowcasting,time-series
@@ -59,25 +59,25 @@ Description-Content-Type: text/markdown
59
59
  [![PyPI version](https://img.shields.io/pypi/v/dfm-python.svg)](https://pypi.org/project/dfm-python/)
60
60
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
61
61
 
62
- A comprehensive Python implementation of **Dynamic Factor Models (DFM)** and **Deep Dynamic Factor Models (DDFM)** for forecasting high-dimensional time series. Built with PyTorch Lightning for production-ready training and inference.
62
+ A comprehensive Python implementation of **Dynamic Factor Models (DFM)** and **Deep Dynamic Factor Models (DDFM)** for forecasting high-dimensional time series. Built with PyTorch Lightning.
63
63
 
64
64
  ## Features
65
65
 
66
66
  ### Core Capabilities
67
- - ✅ **Mixed-frequency data**: Monthly, quarterly, semi-annual, annual series in one model
68
- - ✅ **Clock-based framework**: All factors evolve at a common clock frequency
69
- - ✅ **Block structure**: Flexible factor organization (global + sector-specific factors)
70
- - ✅ **Idiosyncratic components**: Per-series state augmentation for better fit
71
- - ✅ **Preprocessed data**: Users handle preprocessing (imputation, scaling) using sktime or other tools
72
- - ✅ **Forecasting**: Generate predictions for any horizon
73
- - ✅ **Nowcasting**: Estimate current period values using incomplete data
74
- - ✅ **Deep DFM (DDFM)**: Nonlinear encoder with PyTorch for capturing complex factor structures
67
+ - **Mixed-frequency data**: Monthly, quarterly, semi-annual, annual series in one model
68
+ - **Clock-based framework**: All factors evolve at a common clock frequency
69
+ - **Block structure**: Flexible factor organization (global + sector-specific factors)
70
+ - **Idiosyncratic components**: Per-series state augmentation for better fit
71
+ - **Preprocessed data**: Users handle preprocessing (imputation, scaling) using sktime or other tools
72
+ - **Forecasting**: Generate predictions for any horizon
73
+ - **Nowcasting**: Estimate current period values using incomplete data
74
+ - **Deep DFM (DDFM)**: Nonlinear encoder with PyTorch for capturing complex factor structures
75
75
 
76
76
  ### Technical Features
77
- - ✅ **PyTorch Lightning**: Standard training interface with DataModule and Trainer
78
- - ✅ **Multiple configuration methods**: YAML files, Python dictionaries, or Hydra
79
- - ✅ **Advanced numerical stability**: Adaptive regularization, spectral radius capping, variance floors
80
- - ✅ **Production-ready**: Comprehensive error handling, extensive testing, well-documented
77
+ - **PyTorch Lightning**: Standard training interface with DataModule and Trainer
78
+ - **Multiple configuration methods**: YAML files, Python dictionaries, or Hydra
79
+ - **Advanced numerical stability**: Adaptive regularization, spectral radius capping, variance floors
80
+ - **Error handling**: Comprehensive error handling, extensive testing, well-documented
81
81
 
82
82
  ## Installation
83
83
 
@@ -391,17 +391,17 @@ pytest src/test/test_trainer.py -v
391
391
  ## Project Status
392
392
 
393
393
  **Version**: 0.5.1
394
- **Status**: Stable and production-ready
395
- **Python**: 3.10+
394
+ **Status**: Stable
395
+ **Python**: 3.10+
396
396
 
397
397
  ### What's New in 0.5.1
398
398
 
399
- - ✨ **New `update()` method**: Replaces legacy `nowcast()` with flexible `update().predict()` pattern
400
- - 🔧 **Improved API**: Users now control all preprocessing (masking, imputation, standardization)
401
- - 🧹 **Code cleanup**: Removed legacy code and overengineering (~200+ lines removed)
402
- - 🐛 **Bug fixes**: Fixed DDFM `update()` method for single-factor models
403
- - 📚 **Updated tutorials**: All 4 tutorials rewritten with new pattern and VAR(1) configuration
404
- - ✅ **VAR(1) only**: Simplified to VAR(1) factor dynamics throughout
399
+ - **New `update()` method**: Replaces legacy `nowcast()` with flexible `update().predict()` pattern
400
+ - **Improved API**: Users now control all preprocessing (masking, imputation, standardization)
401
+ - **Code cleanup**: Removed legacy code and overengineering (~200+ lines removed)
402
+ - **Bug fixes**: Fixed DDFM `update()` method for single-factor models
403
+ - **Updated tutorials**: All 4 tutorials rewritten with new pattern and VAR(1) configuration
404
+ - **VAR(1) only**: Simplified to VAR(1) factor dynamics throughout
405
405
 
406
406
  ## License
407
407
 
@@ -1,4 +1,4 @@
1
- dfm_python/__init__.py,sha256=C_3NmIr6G7CyDoWB87Osp5b8XZR65LpHmflxMthue74,5683
1
+ dfm_python/__init__.py,sha256=NU9ZxIsniiU9q1IVL8AuGVej9_ROM7nyIFD5dh4MfT4,5683
2
2
  dfm_python/config/__init__.py,sha256=NrTwjNzQRcYlk5Ht0hTF8o-6CCl5e4S_JxQAp1mNyCs,1507
3
3
  dfm_python/config/adapter.py,sha256=X-mjWlg6ebWp-VAxjAUssgHnmr8AXOvSxhTTVSHOIIk,27475
4
4
  dfm_python/config/results.py,sha256=hzXL6MwaIxzA4uzouh6Bc_57lbpedphN9ErTMgfkpeE,15967
@@ -11,22 +11,22 @@ dfm_python/data/utils.py,sha256=5aJFrQ09I3oda4KNut1dXVt-9GYMGouT5lDUpfUEb8Q,1330
11
11
  dfm_python/encoder/__init__.py,sha256=ljPTXq04cpOsGXNnma2EJ447kPFlytT6_mToLyKok7w,850
12
12
  dfm_python/encoder/base.py,sha256=MOSn_bsQP-GVonGprJymQq-_Q6tmCQex8VOWUJjyxNA,3029
13
13
  dfm_python/encoder/pca.py,sha256=FsvCqXD0AuASxymV2-5oAM2QVv_syhSyL3CmxVOXxgM,10584
14
- dfm_python/encoder/vae.py,sha256=nKgt327-oQ1KT6q3hdABI9z_1K26cKSLVXC1Criy0KA,21250
14
+ dfm_python/encoder/vae.py,sha256=dmM243XrLedzwZ-4CdI3W8A9LH591JnlBjd72eHvXvs,22122
15
15
  dfm_python/lightning/__init__.py,sha256=_2DFZnMX3BOybuZZ76VJJ8gmMLZ7tFyO3yyN-A4_ECI,1247
16
16
  dfm_python/lightning/data_module.py,sha256=cZuBdGbk6meaeiw9ZPGkcsUW4v25ZP_km-u0Pkoh5bg,34815
17
- dfm_python/lightning/scaling.py,sha256=PrHCCNS7Z06xkq7_AiQv2bru2Cvra5mejQk6tAzr_hw,16844
17
+ dfm_python/lightning/scaling.py,sha256=-8zYrBm3y4_Ke0ODh1-SJnC0pWef3ZJT6GRv8owo2o8,16997
18
18
  dfm_python/logger/__init__.py,sha256=2GwtfOG2ttbtJ5CrO6338R6GIao3YuY0HLiBeCoC0Ns,1066
19
19
  dfm_python/logger/inference_logger.py,sha256=TmXiWl703boJ3mIR-WgGpy6hfeF8a68OW9vrZppTTCE,10091
20
20
  dfm_python/logger/logger.py,sha256=1YzsUnvAnAK9CTcfttdSIMtgFvylO13LjQPMEsSs9xc,5187
21
21
  dfm_python/logger/train_logger.py,sha256=9VYhzwNYH1Rn7IQipKTY5wmVJcX1n6HDgnUZH0hChPY,17732
22
22
  dfm_python/models/__init__.py,sha256=3JsYZOqDGkl1eG8Zrc4-5gf7Sw47PzzDg2XBhsudB04,724
23
- dfm_python/models/base.py,sha256=-mKK2onDilvPLVRRrW3W7GYsMZxLMHjZWj6n6gL_xnw,30041
24
- dfm_python/models/ddfm.py,sha256=yZe7pxc81ZOsCwzoJ78UHXhEu5adRxkdTEvq-L17Sho,59415
25
- dfm_python/models/dfm.py,sha256=r4MnjhGv9bPB2spiluF6cWKw0cuwR_qq7pfw5oz8dlM,33675
26
- dfm_python/models/mcmc.py,sha256=QXBVxeBHREkc9K_xCGR1Y4sIFUkmX_RbRcgNktojuEc,23978
23
+ dfm_python/models/base.py,sha256=WIx1d38bsaRkpfA4iYVG0_x7R1N8zVfi922LAlXqsEg,29261
24
+ dfm_python/models/ddfm.py,sha256=oMHZTUMu7Dy1_8O1IK33prx_B15s0ex83Hq0lWP-91w,66657
25
+ dfm_python/models/dfm.py,sha256=AK4gdwi5a_oEX8FqovyE19bCWN2GpL6SWiEx6-HsGqw,43318
26
+ dfm_python/models/mcmc.py,sha256=IyQYyfNV5ou48Q3SfStPt3k07N74lSQE_u8W8QX93ro,24070
27
27
  dfm_python/models/utils.py,sha256=LAtvBqUYxnnl4dIaWl6MC-NHSuoyeNkHZppfWB1lCxQ,20193
28
28
  dfm_python/ssm/__init__.py,sha256=dFpt8_3jb_foISDNhma_JUAzch0VrKZcaeWP9zwpwoM,1161
29
- dfm_python/ssm/em.py,sha256=yRYA930YAqOn4B4oxsPER_-vzO8hDPzMYA_eGnn4Sec,63430
29
+ dfm_python/ssm/em.py,sha256=sUiwuBVLTQBNHlBJ9YwemVY0wJYMTUqkuhyjPohENNw,63429
30
30
  dfm_python/ssm/kalman.py,sha256=KC6Q71VxM2NSdBVB5107QsNUm-_kE59MiyqrzImWz7E,45931
31
31
  dfm_python/ssm/utils.py,sha256=SYsu4E2zHAXXLfR2iQWNX-ywuzmwZj5kT4owHlyfvjU,13637
32
32
  dfm_python/trainer/__init__.py,sha256=gc8jRjM-y2XN911LldwYUYHAAuFEvzUJJvXhG6B3Sv4,25654
@@ -38,7 +38,7 @@ dfm_python/utils/diagnostics.py,sha256=UwrJfnn8VACATfQEkTcWw9eEJlTik-VCgTZ1dJABZ
38
38
  dfm_python/utils/helpers.py,sha256=iOVwkP5pti-q-HKOAkp6bUPAp7BPV7BBgh8jL76g9go,13049
39
39
  dfm_python/utils/statespace.py,sha256=P-GQiCP9HzI2d9PgYst4nmRuocEXr9gG1yzGYsYWkMg,44159
40
40
  dfm_python/utils/time.py,sha256=o6NRPuk4Ynk8kPaUnzR6xq3pPAO6aLUTR9N_3hn_N88,35651
41
- dfm_python-0.5.1.dist-info/METADATA,sha256=pf5lWV4TFMr4lKSNeUrtpHV_-ym3h_EaslIrAiioH8U,12159
42
- dfm_python-0.5.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
43
- dfm_python-0.5.1.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
44
- dfm_python-0.5.1.dist-info/RECORD,,
41
+ dfm_python-0.5.3.dist-info/METADATA,sha256=6yDzvYrImsw5VC1FF2XyRGy7HLxlAs4c-jKU5cKMsw0,12007
42
+ dfm_python-0.5.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
43
+ dfm_python-0.5.3.dist-info/licenses/LICENSE,sha256=6e87br6LCDHjy2hdIR1-sdBQy351qdmSEbOg6jQ7ZXY,1063
44
+ dfm_python-0.5.3.dist-info/RECORD,,