dfm-python 0.6.13__py3-none-any.whl → 0.6.14__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.
@@ -344,6 +344,47 @@ COMPUTATION_ERROR_TYPES = (RuntimeError, ValueError, TypeError, AttributeError,
344
344
  DEFAULT_KDFM_AR_ORDER = 1 # Default AR order (VAR lag order p) for KDFM
345
345
  DEFAULT_KDFM_MA_ORDER = 0 # Default MA order (MA lag order q) for KDFM (0 = pure VAR)
346
346
 
347
+ # ============================================================================
348
+ # iVDFM Defaults
349
+ # ============================================================================
350
+
351
+ # iVDFM sequence and dimension defaults
352
+ DEFAULT_IVDFM_SEQUENCE_LENGTH = 100 # Default sequence length for sliding windows
353
+ DEFAULT_IVDFM_LATENT_DIM = 3 # Default number of factors
354
+ DEFAULT_IVDFM_AUX_DIM = 1 # Default auxiliary variable dimension
355
+
356
+ # iVDFM network architecture defaults
357
+ DEFAULT_IVDFM_ENCODER_HIDDEN_DIM = 200 # Default encoder hidden dimension
358
+ DEFAULT_IVDFM_ENCODER_N_LAYERS = 3 # Default encoder layers
359
+ DEFAULT_IVDFM_DECODER_HIDDEN_DIM = 200 # Default decoder hidden dimension
360
+ DEFAULT_IVDFM_DECODER_N_LAYERS = 3 # Default decoder layers
361
+ DEFAULT_IVDFM_PRIOR_HIDDEN_DIM = 100 # Default prior network hidden dimension
362
+ DEFAULT_IVDFM_PRIOR_N_LAYERS = 2 # Default prior network layers
363
+
364
+ # iVDFM dynamics and distribution defaults
365
+ DEFAULT_IVDFM_FACTOR_ORDER = 1 # Default AR order for factors
366
+ DEFAULT_IVDFM_INNOVATION_DIST = 'laplace' # Default innovation distribution
367
+ DEFAULT_IVDFM_DECODER_VAR = 0.01 # Default decoder variance
368
+ DEFAULT_IVDFM_ACTIVATION = 'lrelu' # Default activation function
369
+ DEFAULT_IVDFM_SLOPE = 0.1 # Default leaky ReLU slope
370
+
371
+ # iVDFM training defaults
372
+ DEFAULT_IVDFM_BATCH_SIZE = 32 # Default batch size
373
+ DEFAULT_IVDFM_MAX_EPOCHS = 100 # Default maximum epochs
374
+ DEFAULT_IVDFM_AUX_VARIABLE_TYPE = 'time' # Default auxiliary variable type
375
+
376
+ # iVDFM optimizer defaults
377
+ DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY = 0.0 # Default weight decay for optimizer
378
+ DEFAULT_IVDFM_OPTIMIZER_MOMENTUM = 0.9 # Default momentum for SGD
379
+
380
+ # iVDFM scheduler defaults
381
+ DEFAULT_IVDFM_SCHEDULER_TYPE = 'step' # Default scheduler type: 'step', 'plateau', 'cosine', 'exponential', None
382
+ DEFAULT_IVDFM_SCHEDULER_STEP_SIZE = None # Default step_size for StepLR (auto: max_epochs // 3)
383
+ DEFAULT_IVDFM_SCHEDULER_GAMMA = 0.5 # Default gamma for StepLR/ExponentialLR
384
+ DEFAULT_IVDFM_SCHEDULER_PATIENCE = 10 # Default patience for ReduceLROnPlateau
385
+ DEFAULT_IVDFM_SCHEDULER_FACTOR = 0.1 # Default factor for ReduceLROnPlateau
386
+ DEFAULT_IVDFM_SCHEDULER_MIN_LR = 0.0 # Default min_lr for ReduceLROnPlateau
387
+
347
388
  # ============================================================================
348
389
  # Tutorial Defaults
349
390
  # ============================================================================
@@ -564,6 +605,24 @@ __all__ = [
564
605
  # KDFM defaults
565
606
  'DEFAULT_KDFM_AR_ORDER',
566
607
  'DEFAULT_KDFM_MA_ORDER',
608
+ # iVDFM defaults
609
+ 'DEFAULT_IVDFM_SEQUENCE_LENGTH',
610
+ 'DEFAULT_IVDFM_LATENT_DIM',
611
+ 'DEFAULT_IVDFM_AUX_DIM',
612
+ 'DEFAULT_IVDFM_ENCODER_HIDDEN_DIM',
613
+ 'DEFAULT_IVDFM_ENCODER_N_LAYERS',
614
+ 'DEFAULT_IVDFM_DECODER_HIDDEN_DIM',
615
+ 'DEFAULT_IVDFM_DECODER_N_LAYERS',
616
+ 'DEFAULT_IVDFM_PRIOR_HIDDEN_DIM',
617
+ 'DEFAULT_IVDFM_PRIOR_N_LAYERS',
618
+ 'DEFAULT_IVDFM_FACTOR_ORDER',
619
+ 'DEFAULT_IVDFM_INNOVATION_DIST',
620
+ 'DEFAULT_IVDFM_DECODER_VAR',
621
+ 'DEFAULT_IVDFM_ACTIVATION',
622
+ 'DEFAULT_IVDFM_SLOPE',
623
+ 'DEFAULT_IVDFM_BATCH_SIZE',
624
+ 'DEFAULT_IVDFM_MAX_EPOCHS',
625
+ 'DEFAULT_IVDFM_AUX_VARIABLE_TYPE',
567
626
  # Tutorial defaults
568
627
  'TUTORIAL_MAX_PERIODS',
569
628
  'TUTORIAL_MAX_EPOCHS',
@@ -7,13 +7,13 @@ This package contains:
7
7
  Note: Series are specified via frequency dict mapping column names to frequencies.
8
8
  """
9
9
 
10
- from .model import BaseModelConfig, DFMConfig, DDFMConfig
11
- from .results import BaseResult, DFMResult, DDFMResult
12
- from .params import DFMStateSpaceParams, DDFMStateSpaceParams
10
+ from .model import BaseModelConfig, DFMConfig, DDFMConfig, iVDFMConfig
11
+ from .results import BaseResult, DFMResult, DDFMResult, iVDFMResult
12
+ from .params import DFMStateSpaceParams, DDFMStateSpaceParams, iVDFMModelState
13
13
 
14
14
  __all__ = [
15
- 'BaseModelConfig', 'DFMConfig', 'DDFMConfig',
16
- 'BaseResult', 'DFMResult', 'DDFMResult',
17
- 'DFMStateSpaceParams', 'DDFMStateSpaceParams',
15
+ 'BaseModelConfig', 'DFMConfig', 'DDFMConfig', 'iVDFMConfig',
16
+ 'BaseResult', 'DFMResult', 'DDFMResult', 'iVDFMResult',
17
+ 'DFMStateSpaceParams', 'DDFMStateSpaceParams', 'iVDFMModelState',
18
18
  ]
19
19
 
@@ -48,6 +48,14 @@ from ..constants import (
48
48
  DEFAULT_BATCH_SIZE,
49
49
  DEFAULT_DDFM_WINDOW_SIZE,
50
50
  DEFAULT_GRAD_CLIP_VAL,
51
+ DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY,
52
+ DEFAULT_IVDFM_OPTIMIZER_MOMENTUM,
53
+ DEFAULT_IVDFM_SCHEDULER_TYPE,
54
+ DEFAULT_IVDFM_SCHEDULER_STEP_SIZE,
55
+ DEFAULT_IVDFM_SCHEDULER_GAMMA,
56
+ DEFAULT_IVDFM_SCHEDULER_PATIENCE,
57
+ DEFAULT_IVDFM_SCHEDULER_FACTOR,
58
+ DEFAULT_IVDFM_SCHEDULER_MIN_LR,
51
59
  DEFAULT_REGULARIZATION_SCALE,
52
60
  DEFAULT_STRUCTURAL_REG_WEIGHT,
53
61
  DEFAULT_CONVERGENCE_THRESHOLD,
@@ -70,6 +78,23 @@ from ..constants import (
70
78
  DEFAULT_KDFM_MA_ORDER,
71
79
  FREQUENCY_HIERARCHY,
72
80
  DEFAULT_HIERARCHY_VALUE,
81
+ DEFAULT_IVDFM_SEQUENCE_LENGTH,
82
+ DEFAULT_IVDFM_LATENT_DIM,
83
+ DEFAULT_IVDFM_AUX_DIM,
84
+ DEFAULT_IVDFM_ENCODER_HIDDEN_DIM,
85
+ DEFAULT_IVDFM_ENCODER_N_LAYERS,
86
+ DEFAULT_IVDFM_DECODER_HIDDEN_DIM,
87
+ DEFAULT_IVDFM_DECODER_N_LAYERS,
88
+ DEFAULT_IVDFM_PRIOR_HIDDEN_DIM,
89
+ DEFAULT_IVDFM_PRIOR_N_LAYERS,
90
+ DEFAULT_IVDFM_FACTOR_ORDER,
91
+ DEFAULT_IVDFM_INNOVATION_DIST,
92
+ DEFAULT_IVDFM_DECODER_VAR,
93
+ DEFAULT_IVDFM_ACTIVATION,
94
+ DEFAULT_IVDFM_SLOPE,
95
+ DEFAULT_IVDFM_BATCH_SIZE,
96
+ DEFAULT_IVDFM_MAX_EPOCHS,
97
+ DEFAULT_IVDFM_AUX_VARIABLE_TYPE,
73
98
  )
74
99
 
75
100
 
@@ -136,6 +161,7 @@ class BaseModelConfig:
136
161
  # ========================================================================
137
162
  clock: str = 'm' # Required: Base frequency for latent factors (global clock): 'd', 'w', 'm', 'q', 'sa', 'a' (defaults to 'm' for monthly)
138
163
  target_scaler: Optional[ScalerType] = None # Fitted sklearn scaler instance (StandardScaler, RobustScaler, etc.) for target series only. Must be a fitted scaler object (call .fit() on target data first). Pass scaler object directly, not string. Feature series are assumed to be manually preprocessed. If None, target series are assumed to be already in the desired scale.
164
+ scaler: Optional[str] = None # Scaler type as string: 'standard', 'robust', 'minmax', 'maxabs', 'quantile', or null. Applied to targets only (not context/auxiliary variables). For iVDFM, this is the preferred way to specify scaler (like DDFM pattern).
139
165
 
140
166
  def __post_init__(self):
141
167
  """Validate basic model structure.
@@ -626,6 +652,244 @@ class DDFMConfig(BaseModelConfig):
626
652
  )
627
653
 
628
654
 
655
+ @dataclass
656
+ class iVDFMConfig(BaseModelConfig):
657
+ """Identifiable Variational Dynamic Factor Model configuration.
658
+
659
+ Uses 'scaler' (string) pattern like DDFM, not 'target_scaler' (instance).
660
+ Scaler is applied to targets only, not to context/auxiliary variables.
661
+ """
662
+ """Identifiable Variational Dynamic Factor Model configuration.
663
+
664
+ This configuration class extends BaseModelConfig with parameters specific
665
+ to iVDFM models trained using variational inference with identifiable
666
+ innovation priors.
667
+
668
+ Note: iVDFM does NOT use block structure. Use num_factors directly to specify
669
+ the number of factors.
670
+
671
+ The configuration can be built from:
672
+ - Main settings (training parameters) from config files
673
+ - Series definitions via frequency dict (column names -> frequencies)
674
+ """
675
+ # ========================================================================
676
+ # Model Structure
677
+ # ========================================================================
678
+ num_factors: Optional[int] = None # Number of factors (inferred from config if None)
679
+ sequence_length: int = DEFAULT_IVDFM_SEQUENCE_LENGTH # Sequence length for training
680
+ context: Optional[Union[List[str], List[int]]] = None # Column names (DataFrame) or indices (array) for context variables. If None, context_dim is used to generate time-based context.
681
+ context_dim: int = DEFAULT_IVDFM_AUX_DIM # Dimension of context. Used when context is None (generates time-based context) or when context is multivariate.
682
+
683
+ # ========================================================================
684
+ # Neural Network Architecture
685
+ # ========================================================================
686
+ encoder_hidden_dim: Union[int, List[int]] = DEFAULT_IVDFM_ENCODER_HIDDEN_DIM # Encoder architecture
687
+ encoder_n_layers: int = DEFAULT_IVDFM_ENCODER_N_LAYERS # Number of encoder layers
688
+ decoder_hidden_dim: Union[int, List[int]] = DEFAULT_IVDFM_DECODER_HIDDEN_DIM # Decoder architecture
689
+ decoder_n_layers: int = DEFAULT_IVDFM_DECODER_N_LAYERS # Number of decoder layers
690
+ prior_hidden_dim: Union[int, List[int]] = DEFAULT_IVDFM_PRIOR_HIDDEN_DIM # Prior network architecture
691
+ prior_n_layers: int = DEFAULT_IVDFM_PRIOR_N_LAYERS # Number of prior network layers
692
+ activation: str = DEFAULT_IVDFM_ACTIVATION # Activation function
693
+ slope: float = DEFAULT_IVDFM_SLOPE # Leaky ReLU slope
694
+
695
+ # ========================================================================
696
+ # Dynamics and Distribution Parameters
697
+ # ========================================================================
698
+ factor_order: int = DEFAULT_IVDFM_FACTOR_ORDER # AR order for factors
699
+ innovation_distribution: str = DEFAULT_IVDFM_INNOVATION_DIST # Innovation distribution type
700
+ decoder_var: float = DEFAULT_IVDFM_DECODER_VAR # Decoder variance
701
+
702
+ # ========================================================================
703
+ # Training Parameters
704
+ # ========================================================================
705
+ learning_rate: float = DEFAULT_LEARNING_RATE # Learning rate
706
+ optimizer: str = 'Adam' # Optimizer type
707
+ optimizer_weight_decay: float = DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY # Weight decay (L2 regularization)
708
+ optimizer_momentum: float = DEFAULT_IVDFM_OPTIMIZER_MOMENTUM # Momentum for SGD
709
+ batch_size: int = DEFAULT_IVDFM_BATCH_SIZE # Batch size
710
+ max_epochs: int = DEFAULT_IVDFM_MAX_EPOCHS # Maximum epochs
711
+ tolerance: float = DEFAULT_TOLERANCE # Convergence tolerance
712
+ seed: Optional[int] = None # Random seed
713
+
714
+ # ========================================================================
715
+ # Scheduler Parameters
716
+ # ========================================================================
717
+ scheduler_type: Optional[str] = DEFAULT_IVDFM_SCHEDULER_TYPE # Scheduler type: 'step', 'plateau', 'cosine', 'exponential', None
718
+ scheduler_step_size: Optional[int] = DEFAULT_IVDFM_SCHEDULER_STEP_SIZE # Step size for StepLR (None = auto: max_epochs // 3)
719
+ scheduler_gamma: float = DEFAULT_IVDFM_SCHEDULER_GAMMA # Gamma for StepLR/ExponentialLR
720
+ scheduler_patience: int = DEFAULT_IVDFM_SCHEDULER_PATIENCE # Patience for ReduceLROnPlateau
721
+ scheduler_factor: float = DEFAULT_IVDFM_SCHEDULER_FACTOR # Factor for ReduceLROnPlateau
722
+ scheduler_min_lr: float = DEFAULT_IVDFM_SCHEDULER_MIN_LR # Min learning rate for ReduceLROnPlateau
723
+
724
+ def __post_init__(self):
725
+ """Validate iVDFM configuration."""
726
+ super().__post_init__() # Validate base config
727
+
728
+ from ...utils.errors import ConfigurationError
729
+
730
+ # Validate sequence_length
731
+ if self.sequence_length < 1:
732
+ raise ConfigurationError(
733
+ f"sequence_length must be >= 1, got {self.sequence_length}"
734
+ )
735
+
736
+ # Validate context_dim
737
+ if self.context_dim < 1:
738
+ raise ConfigurationError(
739
+ f"context_dim must be >= 1, got {self.context_dim}"
740
+ )
741
+
742
+ # Validate num_factors if provided
743
+ if self.num_factors is not None and self.num_factors < 1:
744
+ raise ConfigurationError(
745
+ f"num_factors must be >= 1, got {self.num_factors}"
746
+ )
747
+
748
+ # Validate factor_order
749
+ if self.factor_order < 1:
750
+ raise ConfigurationError(
751
+ f"factor_order must be >= 1, got {self.factor_order}"
752
+ )
753
+
754
+ # Validate innovation_distribution
755
+ valid_dists = {'laplace', 'gaussian', 'student_t', 'gamma', 'beta', 'exponential'}
756
+ if self.innovation_distribution not in valid_dists:
757
+ raise ConfigurationError(
758
+ f"innovation_distribution must be one of {valid_dists}, got '{self.innovation_distribution}'"
759
+ )
760
+
761
+ # Validate activation
762
+ valid_activations = {'relu', 'lrelu', 'tanh', 'sigmoid'}
763
+ if self.activation not in valid_activations:
764
+ raise ConfigurationError(
765
+ f"activation must be one of {valid_activations}, got '{self.activation}'"
766
+ )
767
+
768
+ # Validate optimizer
769
+ from ..constants import VALID_OPTIMIZERS
770
+ if self.optimizer not in VALID_OPTIMIZERS:
771
+ raise ConfigurationError(
772
+ f"optimizer must be one of {VALID_OPTIMIZERS}, got '{self.optimizer}'"
773
+ )
774
+
775
+ # Validate scheduler_type
776
+ valid_schedulers = {'step', 'plateau', 'cosine', 'exponential', None}
777
+ if self.scheduler_type not in valid_schedulers:
778
+ raise ConfigurationError(
779
+ f"scheduler_type must be one of {valid_schedulers}, got '{self.scheduler_type}'"
780
+ )
781
+
782
+ # Validate scheduler parameters
783
+ if self.scheduler_gamma <= 0 or self.scheduler_gamma > 1:
784
+ raise ConfigurationError(
785
+ f"scheduler_gamma must be in (0, 1], got {self.scheduler_gamma}"
786
+ )
787
+
788
+ if self.scheduler_patience < 0:
789
+ raise ConfigurationError(
790
+ f"scheduler_patience must be >= 0, got {self.scheduler_patience}"
791
+ )
792
+
793
+ if self.scheduler_factor <= 0 or self.scheduler_factor > 1:
794
+ raise ConfigurationError(
795
+ f"scheduler_factor must be in (0, 1], got {self.scheduler_factor}"
796
+ )
797
+
798
+ if self.scheduler_min_lr < 0:
799
+ raise ConfigurationError(
800
+ f"scheduler_min_lr must be >= 0, got {self.scheduler_min_lr}"
801
+ )
802
+
803
+ @classmethod
804
+ def _extract_ivdfm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
805
+ """Extract iVDFM-specific parameters from config dict."""
806
+ from ..constants import (
807
+ DEFAULT_IVDFM_SEQUENCE_LENGTH,
808
+ DEFAULT_IVDFM_LATENT_DIM,
809
+ DEFAULT_IVDFM_AUX_DIM,
810
+ DEFAULT_IVDFM_ENCODER_HIDDEN_DIM,
811
+ DEFAULT_IVDFM_ENCODER_N_LAYERS,
812
+ DEFAULT_IVDFM_DECODER_HIDDEN_DIM,
813
+ DEFAULT_IVDFM_DECODER_N_LAYERS,
814
+ DEFAULT_IVDFM_PRIOR_HIDDEN_DIM,
815
+ DEFAULT_IVDFM_PRIOR_N_LAYERS,
816
+ DEFAULT_IVDFM_FACTOR_ORDER,
817
+ DEFAULT_IVDFM_INNOVATION_DIST,
818
+ DEFAULT_IVDFM_DECODER_VAR,
819
+ DEFAULT_IVDFM_ACTIVATION,
820
+ DEFAULT_IVDFM_SLOPE,
821
+ DEFAULT_IVDFM_BATCH_SIZE,
822
+ DEFAULT_IVDFM_MAX_EPOCHS,
823
+ DEFAULT_IVDFM_AUX_VARIABLE_TYPE,
824
+ DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY,
825
+ DEFAULT_IVDFM_OPTIMIZER_MOMENTUM,
826
+ DEFAULT_IVDFM_SCHEDULER_TYPE,
827
+ DEFAULT_IVDFM_SCHEDULER_STEP_SIZE,
828
+ DEFAULT_IVDFM_SCHEDULER_GAMMA,
829
+ DEFAULT_IVDFM_SCHEDULER_PATIENCE,
830
+ DEFAULT_IVDFM_SCHEDULER_FACTOR,
831
+ DEFAULT_IVDFM_SCHEDULER_MIN_LR,
832
+ )
833
+
834
+ ivdfm_params = cls._extract_params(data, {
835
+ 'num_factors': None,
836
+ 'sequence_length': DEFAULT_IVDFM_SEQUENCE_LENGTH,
837
+ 'context': None,
838
+ 'context_dim': DEFAULT_IVDFM_AUX_DIM,
839
+ 'encoder_hidden_dim': DEFAULT_IVDFM_ENCODER_HIDDEN_DIM,
840
+ 'encoder_n_layers': DEFAULT_IVDFM_ENCODER_N_LAYERS,
841
+ 'decoder_hidden_dim': DEFAULT_IVDFM_DECODER_HIDDEN_DIM,
842
+ 'decoder_n_layers': DEFAULT_IVDFM_DECODER_N_LAYERS,
843
+ 'prior_hidden_dim': DEFAULT_IVDFM_PRIOR_HIDDEN_DIM,
844
+ 'prior_n_layers': DEFAULT_IVDFM_PRIOR_N_LAYERS,
845
+ 'activation': DEFAULT_IVDFM_ACTIVATION,
846
+ 'slope': DEFAULT_IVDFM_SLOPE,
847
+ 'factor_order': DEFAULT_IVDFM_FACTOR_ORDER,
848
+ 'innovation_distribution': DEFAULT_IVDFM_INNOVATION_DIST,
849
+ 'decoder_var': DEFAULT_IVDFM_DECODER_VAR,
850
+ 'learning_rate': DEFAULT_LEARNING_RATE,
851
+ 'optimizer': 'Adam',
852
+ 'optimizer_weight_decay': DEFAULT_IVDFM_OPTIMIZER_WEIGHT_DECAY,
853
+ 'optimizer_momentum': DEFAULT_IVDFM_OPTIMIZER_MOMENTUM,
854
+ 'batch_size': DEFAULT_IVDFM_BATCH_SIZE,
855
+ 'max_epochs': DEFAULT_IVDFM_MAX_EPOCHS,
856
+ 'tolerance': DEFAULT_TOLERANCE,
857
+ 'seed': None,
858
+ 'scheduler_type': DEFAULT_IVDFM_SCHEDULER_TYPE,
859
+ 'scheduler_step_size': DEFAULT_IVDFM_SCHEDULER_STEP_SIZE,
860
+ 'scheduler_gamma': DEFAULT_IVDFM_SCHEDULER_GAMMA,
861
+ 'scheduler_patience': DEFAULT_IVDFM_SCHEDULER_PATIENCE,
862
+ 'scheduler_factor': DEFAULT_IVDFM_SCHEDULER_FACTOR,
863
+ 'scheduler_min_lr': DEFAULT_IVDFM_SCHEDULER_MIN_LR,
864
+ 'scaler': None, # Scaler string: 'standard', 'robust', 'minmax', 'maxabs', 'quantile', or null
865
+ })
866
+
867
+ return ivdfm_params
868
+
869
+ @classmethod
870
+ def from_dict(cls, data: Dict[str, Any]) -> 'iVDFMConfig':
871
+ """Create iVDFMConfig from dictionary.
872
+
873
+ Parameters
874
+ ----------
875
+ data : Dict[str, Any]
876
+ Configuration dictionary
877
+
878
+ Returns
879
+ -------
880
+ iVDFMConfig
881
+ iVDFM configuration instance
882
+ """
883
+ # Extract base params
884
+ base_params = cls._extract_base(data)
885
+
886
+ # Extract iVDFM-specific params
887
+ ivdfm_params = cls._extract_ivdfm(data)
888
+
889
+ # Combine and create config
890
+ return cls(**base_params, **ivdfm_params)
891
+
892
+
629
893
  # ============================================================================
630
894
  # Validation Functions
631
895
  # ============================================================================
@@ -5,6 +5,7 @@ This module defines dataclasses for storing state-space parameters and model str
5
5
 
6
6
  from dataclasses import dataclass
7
7
  import numpy as np
8
+ import torch
8
9
  from typing import Optional, Any
9
10
 
10
11
 
@@ -326,6 +327,188 @@ class DDFMModelState:
326
327
 
327
328
  # Backward compatibility aliases
328
329
  DDFMTrainingState = DDFMModelState
330
+
331
+
332
+ @dataclass
333
+ class iVDFMModelState:
334
+ """iVDFM model state including training state and SSM parameters.
335
+
336
+ Consolidates all iVDFM model state for checkpointing: training state (convergence,
337
+ loss, factors, innovations) and fitted state-space model parameters.
338
+
339
+ Attributes
340
+ ----------
341
+ num_iter : int
342
+ Current training iteration/epoch.
343
+ loss_now : float, optional
344
+ Current training loss value.
345
+ elbo : float, optional
346
+ Final ELBO value.
347
+ converged : bool
348
+ Whether training has converged.
349
+ factors : np.ndarray, optional
350
+ Extracted factors (T x r) or (batch x T x r).
351
+ innovations : np.ndarray, optional
352
+ Innovations (T x r) or (batch x T x r).
353
+ factor_order : int
354
+ AR order for factor dynamics (p in AR(p)).
355
+ A : np.ndarray, optional
356
+ Transition matrix (r x r) for p=1, or AR coefficients (r x p) for p>1.
357
+ B : np.ndarray, optional
358
+ Innovation loading matrix (r x r).
359
+ f0 : np.ndarray, optional
360
+ Initial state (r,).
361
+ """
362
+ # Training state
363
+ num_iter: int = 0
364
+ loss_now: Optional[float] = None
365
+ elbo: Optional[float] = None
366
+ converged: bool = False
367
+ factors: Optional[np.ndarray] = None
368
+ innovations: Optional[np.ndarray] = None
369
+ full_state: Optional[np.ndarray] = None # Augmented state for companion form (T x r*p) for p>1, (T x r) for p=1
370
+
371
+ # SSM parameters
372
+ factor_order: int = 1
373
+ A: Optional[np.ndarray] = None # Transition matrix or AR coefficients
374
+ B: Optional[np.ndarray] = None # Innovation loading matrix
375
+ f0: Optional[np.ndarray] = None # Initial state
376
+
377
+ @classmethod
378
+ def from_model(cls, model: Any) -> 'iVDFMModelState':
379
+ """Create iVDFMModelState from iVDFM model instance.
380
+
381
+ Parameters
382
+ ----------
383
+ model : iVDFM
384
+ iVDFM model instance
385
+
386
+ Returns
387
+ -------
388
+ iVDFMModelState
389
+ Model state dataclass
390
+ """
391
+ # Get training state from model attributes
392
+ factors = getattr(model, 'factors', None)
393
+ innovations = getattr(model, 'innovations', None)
394
+ num_iter = getattr(model, '_num_iter', 0)
395
+ loss_now = getattr(model, 'loss_now', None)
396
+ elbo = getattr(model, '_elbo', None)
397
+ converged = getattr(model, '_converged', False)
398
+
399
+ # Get SSM parameters
400
+ factor_order = getattr(model, 'factor_order', 1)
401
+ A = B = f0 = None
402
+
403
+ # Extract from SSM if available
404
+ if hasattr(model, 'ssm') and model.ssm is not None:
405
+ ssm = model.ssm
406
+ if factor_order == 1:
407
+ # First-order: A is diagonal, stored as (r,)
408
+ if hasattr(ssm, 'A'):
409
+ A = ssm.A.data.cpu().numpy() if hasattr(ssm.A, 'data') else ssm.A
410
+ else:
411
+ # Higher-order: AR coefficients stored as (r, p)
412
+ if hasattr(ssm, 'ar_coeffs'):
413
+ A = ssm.ar_coeffs.data.cpu().numpy() if hasattr(ssm.ar_coeffs, 'data') else ssm.ar_coeffs
414
+
415
+ if hasattr(ssm, 'B'):
416
+ B = ssm.B.data.cpu().numpy() if hasattr(ssm.B, 'data') else ssm.B
417
+
418
+ if hasattr(ssm, 'f0'):
419
+ f0 = ssm.f0.data.cpu().numpy() if hasattr(ssm.f0, 'data') else ssm.f0
420
+
421
+ # Fallback: get from model attributes directly
422
+ if A is None:
423
+ A = getattr(model, 'A', None)
424
+ if A is not None and hasattr(A, 'data'):
425
+ A = A.data.cpu().numpy()
426
+ if B is None:
427
+ B = getattr(model, 'B', None)
428
+ if B is not None and hasattr(B, 'data'):
429
+ B = B.data.cpu().numpy()
430
+ if f0 is None:
431
+ f0 = getattr(model, 'f0', None)
432
+ if f0 is not None and hasattr(f0, 'data'):
433
+ f0 = f0.data.cpu().numpy()
434
+
435
+ # Compute full_state (augmented state for companion form)
436
+ full_state = None
437
+ if factors is not None:
438
+ factors_np = factors
439
+ if isinstance(factors_np, np.ndarray):
440
+ # Average over batch if needed
441
+ if factors_np.ndim == 3:
442
+ factors_np = np.mean(factors_np, axis=0) # (T, r)
443
+
444
+ T, r = factors_np.shape
445
+ if factor_order == 1:
446
+ # For p=1, full_state is just factors
447
+ full_state = factors_np # (T, r)
448
+ else:
449
+ # For p>1, construct augmented state (T, r*p)
450
+ # s_t[i*p : (i+1)*p] = [f_t[i], f_{t-1}[i], ..., f_{t-p+1}[i]]
451
+ full_state = np.zeros((T, r * factor_order), dtype=factors_np.dtype)
452
+ for t in range(T):
453
+ for i in range(r):
454
+ for lag in range(factor_order):
455
+ idx = t - lag
456
+ if idx >= 0:
457
+ full_state[t, i * factor_order + lag] = factors_np[idx, i]
458
+ else:
459
+ # Use f0 for negative indices
460
+ if f0 is not None and i < len(f0):
461
+ full_state[t, i * factor_order + lag] = f0[i]
462
+
463
+ return cls(
464
+ num_iter=num_iter,
465
+ loss_now=loss_now,
466
+ elbo=elbo,
467
+ converged=converged,
468
+ factors=factors,
469
+ innovations=innovations,
470
+ full_state=full_state,
471
+ factor_order=factor_order,
472
+ A=A,
473
+ B=B,
474
+ f0=f0
475
+ )
476
+
477
+ def apply_to_model(self, model: Any) -> None:
478
+ """Apply this state to iVDFM model instance.
479
+
480
+ Parameters
481
+ ----------
482
+ model : iVDFM
483
+ iVDFM model instance to update
484
+ """
485
+ # Apply training state
486
+ if self.factors is not None:
487
+ model.factors = self.factors
488
+ if self.innovations is not None:
489
+ model.innovations = self.innovations
490
+ model._num_iter = self.num_iter
491
+ model.loss_now = self.loss_now
492
+ if self.elbo is not None:
493
+ model._elbo = self.elbo
494
+ model._converged = self.converged
495
+
496
+ # Apply SSM parameters if available
497
+ if self.A is not None and self.B is not None:
498
+ if hasattr(model, 'ssm') and model.ssm is not None:
499
+ ssm = model.ssm
500
+ if self.factor_order == 1:
501
+ if hasattr(ssm, 'A'):
502
+ ssm.A.data = torch.from_numpy(self.A).to(ssm.A.device)
503
+ else:
504
+ if hasattr(ssm, 'ar_coeffs'):
505
+ ssm.ar_coeffs.data = torch.from_numpy(self.A).to(ssm.ar_coeffs.device)
506
+
507
+ if hasattr(ssm, 'B'):
508
+ ssm.B.data = torch.from_numpy(self.B).to(ssm.B.device)
509
+
510
+ if self.f0 is not None and hasattr(ssm, 'f0'):
511
+ ssm.f0.data = torch.from_numpy(self.f0).to(ssm.f0.device)
329
512
  DDFMStateSpaceParams = DDFMModelState
330
513
 
331
514
  # Backward compatibility alias