pymc-extras 0.3.1__py3-none-any.whl → 0.4.0__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.
@@ -326,7 +326,7 @@ def find_MAP(
326
326
  )
327
327
 
328
328
  raveled_optimized = RaveledVars(optimizer_result.x, initial_params.point_map_info)
329
- unobserved_vars = get_default_varnames(model.unobserved_value_vars, include_transformed)
329
+ unobserved_vars = get_default_varnames(model.unobserved_value_vars, include_transformed=True)
330
330
  unobserved_vars_values = model.compile_fn(unobserved_vars, mode="FAST_COMPILE")(
331
331
  DictToArrayBijection.rmap(raveled_optimized)
332
332
  )
@@ -335,13 +335,20 @@ def find_MAP(
335
335
  var.name: value for var, value in zip(unobserved_vars, unobserved_vars_values)
336
336
  }
337
337
 
338
- idata = map_results_to_inference_data(optimized_point, frozen_model)
339
- idata = add_fit_to_inference_data(idata, raveled_optimized, H_inv)
338
+ idata = map_results_to_inference_data(
339
+ map_point=optimized_point, model=frozen_model, include_transformed=include_transformed
340
+ )
341
+
342
+ idata = add_fit_to_inference_data(
343
+ idata=idata, mu=raveled_optimized, H_inv=H_inv, model=frozen_model
344
+ )
345
+
340
346
  idata = add_optimizer_result_to_inference_data(
341
- idata, optimizer_result, method, raveled_optimized, model
347
+ idata=idata, result=optimizer_result, method=method, mu=raveled_optimized, model=model
342
348
  )
349
+
343
350
  idata = add_data_to_inference_data(
344
- idata, progressbar=False, model=model, compile_kwargs=compile_kwargs
351
+ idata=idata, progressbar=False, model=model, compile_kwargs=compile_kwargs
345
352
  )
346
353
 
347
354
  return idata
@@ -59,6 +59,7 @@ def make_unpacked_variable_names(names: list[str], model: pm.Model) -> list[str]
59
59
  def map_results_to_inference_data(
60
60
  map_point: dict[str, float | int | np.ndarray],
61
61
  model: pm.Model | None = None,
62
+ include_transformed: bool = True,
62
63
  ):
63
64
  """
64
65
  Add the MAP point to an InferenceData object in the posterior group.
@@ -68,13 +69,13 @@ def map_results_to_inference_data(
68
69
 
69
70
  Parameters
70
71
  ----------
71
- idata: az.InferenceData
72
- An InferenceData object to which the MAP point will be added.
73
72
  map_point: dict
74
73
  A dictionary containing the MAP point estimates for each variable. The keys should be the variable names, and
75
74
  the values should be the corresponding MAP estimates.
76
75
  model: Model, optional
77
76
  A PyMC model. If None, the model is taken from the current model context.
77
+ include_transformed: bool
78
+ Whether to return transformed (unconstrained) variables in the constrained_posterior group. Default is True.
78
79
 
79
80
  Returns
80
81
  -------
@@ -118,7 +119,7 @@ def map_results_to_inference_data(
118
119
  dims=dims,
119
120
  )
120
121
 
121
- if unconstrained_names:
122
+ if unconstrained_names and include_transformed:
122
123
  unconstrained_posterior = az.from_dict(
123
124
  posterior={
124
125
  k: np.expand_dims(v, (0, 1))
@@ -302,7 +302,7 @@ def fit_laplace(
302
302
  ----------
303
303
  model : pm.Model
304
304
  The PyMC model to be fit. If None, the current model context is used.
305
- method : str
305
+ optimize_method : str
306
306
  The optimization method to use. Valid choices are: Nelder-Mead, Powell, CG, BFGS, L-BFGS-B, TNC, SLSQP,
307
307
  trust-constr, dogleg, trust-ncg, trust-exact, trust-krylov, and basinhopping.
308
308
 
@@ -441,9 +441,11 @@ def fit_laplace(
441
441
  .rename({"temp_chain": "chain", "temp_draw": "draw"})
442
442
  )
443
443
 
444
- idata.unconstrained_posterior = unstack_laplace_draws(
445
- new_posterior.laplace_approximation.values, model, chains=chains, draws=draws
446
- )
444
+ if include_transformed:
445
+ idata.unconstrained_posterior = unstack_laplace_draws(
446
+ new_posterior.laplace_approximation.values, model, chains=chains, draws=draws
447
+ )
448
+
447
449
  idata.posterior = new_posterior.drop_vars(
448
450
  ["laplace_approximation", "unpacked_variable_names"]
449
451
  )
@@ -38,16 +38,15 @@ from pymc.blocking import DictToArrayBijection, RaveledVars
38
38
  from pymc.initial_point import make_initial_point_fn
39
39
  from pymc.model import modelcontext
40
40
  from pymc.model.core import Point
41
+ from pymc.progress_bar import CustomProgress, default_progress_theme
41
42
  from pymc.pytensorf import (
42
43
  compile,
43
44
  find_rng_nodes,
44
45
  reseed_rngs,
45
46
  )
46
47
  from pymc.util import (
47
- CustomProgress,
48
48
  RandomSeed,
49
49
  _get_seeds_per_chain,
50
- default_progress_theme,
51
50
  get_default_varnames,
52
51
  )
53
52
  from pytensor.compile.function.types import Function
@@ -0,0 +1,21 @@
1
+ from pymc_extras.statespace.models.structural.components.autoregressive import (
2
+ AutoregressiveComponent,
3
+ )
4
+ from pymc_extras.statespace.models.structural.components.cycle import CycleComponent
5
+ from pymc_extras.statespace.models.structural.components.level_trend import LevelTrendComponent
6
+ from pymc_extras.statespace.models.structural.components.measurement_error import MeasurementError
7
+ from pymc_extras.statespace.models.structural.components.regression import RegressionComponent
8
+ from pymc_extras.statespace.models.structural.components.seasonality import (
9
+ FrequencySeasonality,
10
+ TimeSeasonality,
11
+ )
12
+
13
+ __all__ = [
14
+ "LevelTrendComponent",
15
+ "MeasurementError",
16
+ "AutoregressiveComponent",
17
+ "TimeSeasonality",
18
+ "FrequencySeasonality",
19
+ "RegressionComponent",
20
+ "CycleComponent",
21
+ ]
@@ -0,0 +1,188 @@
1
+ import numpy as np
2
+ import pytensor.tensor as pt
3
+
4
+ from pymc_extras.statespace.models.structural.core import Component
5
+ from pymc_extras.statespace.models.structural.utils import order_to_mask
6
+ from pymc_extras.statespace.utils.constants import AR_PARAM_DIM
7
+
8
+
9
+ class AutoregressiveComponent(Component):
10
+ r"""
11
+ Autoregressive timeseries component
12
+
13
+ Parameters
14
+ ----------
15
+ order: int or sequence of int
16
+
17
+ If int, the number of lags to include in the model.
18
+ If a sequence, an array-like of zeros and ones indicating which lags to include in the model.
19
+
20
+ name: str, default "auto_regressive"
21
+ A name for this autoregressive component. Used to label dimensions and coordinates.
22
+
23
+ observed_state_names: list[str] | None, default None
24
+ List of strings for observed state labels. If None, defaults to ["data"].
25
+
26
+ Notes
27
+ -----
28
+ An autoregressive component can be thought of as a way o introducing serially correlated errors into the model.
29
+ The process is modeled:
30
+
31
+ .. math::
32
+ x_t = \sum_{i=1}^p \rho_i x_{t-i}
33
+
34
+ Where ``p``, the number of autoregressive terms to model, is the order of the process. By default, all lags up to
35
+ ``p`` are included in the model. To disable lags, pass a list of zeros and ones to the ``order`` argumnet. For
36
+ example, ``order=[1, 1, 0, 1]`` would become:
37
+
38
+ .. math::
39
+ x_t = \rho_1 x_{t-1} + \rho_2 x_{t-1} + \rho_4 x_{t-1}
40
+
41
+ The coefficient :math:`\rho_3` has been constrained to zero.
42
+
43
+ .. warning:: This class is meant to be used as a component in a structural time series model. For modeling of
44
+ stationary processes with ARIMA, use ``statespace.BayesianSARIMA``.
45
+
46
+ Examples
47
+ --------
48
+ Model a timeseries as an AR(2) process with non-zero mean:
49
+
50
+ .. code:: python
51
+
52
+ from pymc_extras.statespace import structural as st
53
+ import pymc as pm
54
+ import pytensor.tensor as pt
55
+
56
+ trend = st.LevelTrendComponent(order=1, innovations_order=0)
57
+ ar = st.AutoregressiveComponent(2)
58
+ ss_mod = (trend + ar).build()
59
+
60
+ with pm.Model(coords=ss_mod.coords) as model:
61
+ P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states) * 10, dims=ss_mod.param_dims['P0'])
62
+ intitial_trend = pm.Normal('initial_trend', sigma=10, dims=ss_mod.param_dims['initial_trend'])
63
+ ar_params = pm.Normal('ar_params', dims=ss_mod.param_dims['ar_params'])
64
+ sigma_ar = pm.Exponential('sigma_ar', 1, dims=ss_mod.param_dims['sigma_ar'])
65
+
66
+ ss_mod.build_statespace_graph(data)
67
+ idata = pm.sample(nuts_sampler='numpyro')
68
+
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ order: int = 1,
74
+ name: str = "auto_regressive",
75
+ observed_state_names: list[str] | None = None,
76
+ ):
77
+ if observed_state_names is None:
78
+ observed_state_names = ["data"]
79
+
80
+ k_posdef = k_endog = len(observed_state_names)
81
+
82
+ order = order_to_mask(order)
83
+ ar_lags = np.flatnonzero(order).ravel().astype(int) + 1
84
+ k_states = len(order)
85
+
86
+ self.order = order
87
+ self.ar_lags = ar_lags
88
+
89
+ super().__init__(
90
+ name=name,
91
+ k_endog=k_endog,
92
+ k_states=k_states * k_endog,
93
+ k_posdef=k_posdef,
94
+ measurement_error=True,
95
+ combine_hidden_states=True,
96
+ observed_state_names=observed_state_names,
97
+ obs_state_idxs=np.tile(np.r_[[1.0], np.zeros(k_states - 1)], k_endog),
98
+ )
99
+
100
+ def populate_component_properties(self):
101
+ k_states = self.k_states // self.k_endog # this is also the number of AR lags
102
+
103
+ self.state_names = [
104
+ f"L{i + 1}[{state_name}]"
105
+ for state_name in self.observed_state_names
106
+ for i in range(k_states)
107
+ ]
108
+
109
+ self.shock_names = [f"{self.name}[{obs_name}]" for obs_name in self.observed_state_names]
110
+ self.param_names = [f"params_{self.name}", f"sigma_{self.name}"]
111
+ self.param_dims = {f"params_{self.name}": (f"lag_{self.name}",)}
112
+ self.coords = {f"lag_{self.name}": self.ar_lags.tolist()}
113
+
114
+ if self.k_endog > 1:
115
+ self.param_dims[f"params_{self.name}"] = (
116
+ f"endog_{self.name}",
117
+ AR_PARAM_DIM,
118
+ )
119
+ self.param_dims[f"sigma_{self.name}"] = (f"endog_{self.name}",)
120
+
121
+ self.coords[f"endog_{self.name}"] = self.observed_state_names
122
+
123
+ self.param_info = {
124
+ f"params_{self.name}": {
125
+ "shape": (k_states,) if self.k_endog == 1 else (self.k_endog, k_states),
126
+ "constraints": None,
127
+ "dims": (AR_PARAM_DIM,)
128
+ if self.k_endog == 1
129
+ else (
130
+ f"endog_{self.name}",
131
+ f"lag_{self.name}",
132
+ ),
133
+ },
134
+ f"sigma_{self.name}": {
135
+ "shape": () if self.k_endog == 1 else (self.k_endog,),
136
+ "constraints": "Positive",
137
+ "dims": None if self.k_endog == 1 else (f"endog_{self.name}",),
138
+ },
139
+ }
140
+
141
+ def make_symbolic_graph(self) -> None:
142
+ k_endog = self.k_endog
143
+ k_states = self.k_states // k_endog
144
+ k_posdef = self.k_posdef
145
+
146
+ k_nonzero = int(sum(self.order))
147
+ ar_params = self.make_and_register_variable(
148
+ f"params_{self.name}", shape=(k_nonzero,) if k_endog == 1 else (k_endog, k_nonzero)
149
+ )
150
+ sigma_ar = self.make_and_register_variable(
151
+ f"sigma_{self.name}", shape=() if k_endog == 1 else (k_endog,)
152
+ )
153
+
154
+ if k_endog == 1:
155
+ T = pt.eye(k_states, k=-1)
156
+ ar_idx = (np.zeros(k_nonzero, dtype="int"), np.nonzero(self.order)[0])
157
+ T = T[ar_idx].set(ar_params)
158
+
159
+ else:
160
+ transition_matrices = []
161
+
162
+ for i in range(k_endog):
163
+ T = pt.eye(k_states, k=-1)
164
+ ar_idx = (np.zeros(k_nonzero, dtype="int"), np.nonzero(self.order)[0])
165
+ T = T[ar_idx].set(ar_params[i])
166
+ transition_matrices.append(T)
167
+ T = pt.specify_shape(
168
+ pt.linalg.block_diag(*transition_matrices), (self.k_states, self.k_states)
169
+ )
170
+
171
+ self.ssm["transition", :, :] = T
172
+
173
+ R = np.eye(k_states)
174
+ R_mask = np.full((k_states), False)
175
+ R_mask[0] = True
176
+ R = R[:, R_mask]
177
+
178
+ self.ssm["selection", :, :] = pt.specify_shape(
179
+ pt.linalg.block_diag(*[R for _ in range(k_endog)]), (self.k_states, self.k_posdef)
180
+ )
181
+
182
+ Z = pt.zeros((1, k_states))[0, 0].set(1.0)
183
+ self.ssm["design", :, :] = pt.specify_shape(
184
+ pt.linalg.block_diag(*[Z for _ in range(k_endog)]), (self.k_endog, self.k_states)
185
+ )
186
+
187
+ cov_idx = ("state_cov", *np.diag_indices(k_posdef))
188
+ self.ssm[cov_idx] = sigma_ar**2
@@ -0,0 +1,305 @@
1
+ import numpy as np
2
+
3
+ from pytensor import tensor as pt
4
+ from pytensor.tensor.slinalg import block_diag
5
+
6
+ from pymc_extras.statespace.models.structural.core import Component
7
+ from pymc_extras.statespace.models.structural.utils import _frequency_transition_block
8
+
9
+
10
+ class CycleComponent(Component):
11
+ r"""
12
+ A component for modeling longer-term cyclical effects
13
+
14
+ Supports both univariate and multivariate time series. For multivariate time series,
15
+ each endogenous variable gets its own independent cycle component with separate
16
+ cosine/sine states and optional variable-specific innovation variances.
17
+
18
+ Parameters
19
+ ----------
20
+ name: str
21
+ Name of the component. Used in generated coordinates and state names. If None, a descriptive name will be
22
+ used.
23
+
24
+ cycle_length: int, optional
25
+ The length of the cycle, in the calendar units of your data. For example, if your data is monthly, and you
26
+ want to model a 12-month cycle, use ``cycle_length=12``. You cannot specify both ``cycle_length`` and
27
+ ``estimate_cycle_length``.
28
+
29
+ estimate_cycle_length: bool, default False
30
+ Whether to estimate the cycle length. If True, an additional parameter, ``cycle_length`` will be added to the
31
+ model. You cannot specify both ``cycle_length`` and ``estimate_cycle_length``.
32
+
33
+ dampen: bool, default False
34
+ Whether to dampen the cycle by multiplying by a dampening factor :math:`\rho` at every timestep. If true,
35
+ an additional parameter, ``dampening_factor`` will be added to the model.
36
+
37
+ innovations: bool, default True
38
+ Whether to include stochastic innovations in the strength of the seasonal effect. If True, an additional
39
+ parameter, ``sigma_{name}`` will be added to the model.
40
+ For multivariate time series, this is a vector (variable-specific innovation variances).
41
+
42
+ observed_state_names: list[str], optional
43
+ Names of the observed state variables. For univariate time series, defaults to ``["data"]``.
44
+ For multivariate time series, specify a list of names for each endogenous variable.
45
+
46
+ Notes
47
+ -----
48
+ The cycle component is very similar in implementation to the frequency domain seasonal component, expect that it
49
+ is restricted to n=1. The cycle component can be expressed:
50
+
51
+ .. math::
52
+ \begin{align}
53
+ \gamma_t &= \rho \gamma_{t-1} \cos \lambda + \rho \gamma_{t-1}^\star \sin \lambda + \omega_{t} \\
54
+ \gamma_{t}^\star &= -\rho \gamma_{t-1} \sin \lambda + \rho \gamma_{t-1}^\star \cos \lambda + \omega_{t}^\star \\
55
+ \lambda &= \frac{2\pi}{s}
56
+ \end{align}
57
+
58
+ Where :math:`s` is the ``cycle_length``. [1] recommend that this component be used for longer term cyclical
59
+ effects, such as business cycles, and that the seasonal component be used for shorter term effects, such as
60
+ weekly or monthly seasonality.
61
+
62
+ Unlike a FrequencySeasonality component, the length of a CycleComponent can be estimated.
63
+
64
+ **Multivariate Support:**
65
+ For multivariate time series with k endogenous variables, the component creates:
66
+ - 2k states (cosine and sine components for each variable)
67
+ - Block diagonal transition and selection matrices
68
+ - Variable-specific innovation variances (optional)
69
+ - Proper parameter shapes: (k, 2) for initial states, (k,) for innovation variances
70
+
71
+ Examples
72
+ --------
73
+ **Univariate Example:**
74
+ Estimate a business cycle with length between 6 and 12 years:
75
+
76
+ .. code:: python
77
+
78
+ from pymc_extras.statespace import structural as st
79
+ import pymc as pm
80
+ import pytensor.tensor as pt
81
+ import pandas as pd
82
+ import numpy as np
83
+
84
+ data = np.random.normal(size=(100, 1))
85
+
86
+ # Build the structural model
87
+ grw = st.LevelTrendComponent(order=1, innovations_order=1)
88
+ cycle = st.CycleComponent(
89
+ "business_cycle", cycle_length=12, estimate_cycle_length=False, innovations=True, dampen=True
90
+ )
91
+ ss_mod = (grw + cycle).build()
92
+
93
+ # Estimate with PyMC
94
+ with pm.Model(coords=ss_mod.coords) as model:
95
+ P0 = pm.Deterministic('P0', pt.eye(ss_mod.k_states), dims=ss_mod.param_dims['P0'])
96
+
97
+ initial_level_trend = pm.Normal('initial_level_trend', dims=ss_mod.param_dims['initial_level_trend'])
98
+ sigma_level_trend = pm.HalfNormal('sigma_level_trend', dims=ss_mod.param_dims['sigma_level_trend'])
99
+
100
+ business_cycle = pm.Normal("business_cycle", dims=ss_mod.param_dims["business_cycle"])
101
+ dampening = pm.Beta("dampening_factor_business_cycle", 2, 2)
102
+ sigma_cycle = pm.HalfNormal("sigma_business_cycle", sigma=1)
103
+
104
+ ss_mod.build_statespace_graph(data)
105
+ idata = pm.sample(
106
+ nuts_sampler="nutpie", nuts_sampler_kwargs={"backend": "JAX", "gradient_backend": "JAX"}
107
+ )
108
+
109
+ **Multivariate Example:**
110
+ Model cycles for multiple economic indicators with variable-specific innovation variances:
111
+
112
+ .. code:: python
113
+
114
+ # Multivariate cycle component
115
+ cycle = st.CycleComponent(
116
+ name='business_cycle',
117
+ cycle_length=12,
118
+ estimate_cycle_length=False,
119
+ innovations=True,
120
+ dampen=True,
121
+ observed_state_names=['gdp', 'unemployment', 'inflation']
122
+ )
123
+ ss_mod = cycle.build()
124
+
125
+ with pm.Model(coords=ss_mod.coords) as model:
126
+ P0 = pm.Deterministic("P0", pt.eye(ss_mod.k_states), dims=ss_mod.param_dims["P0"])
127
+ # Initial states: shape (3, 2) for 3 variables, 2 states each
128
+ business_cycle = pm.Normal('business_cycle', dims=ss_mod.param_dims["business_cycle"])
129
+
130
+ # Dampening factor: scalar (shared across variables)
131
+ dampening = pm.Beta("dampening_factor_business_cycle", 2, 2)
132
+
133
+ # Innovation variances: shape (3,) for variable-specific variances
134
+ sigma_cycle = pm.HalfNormal(
135
+ "sigma_business_cycle", dims=ss_mod.param_dims["sigma_business_cycle"]
136
+ )
137
+
138
+ ss_mod.build_statespace_graph(data)
139
+ idata = pm.sample(
140
+ nuts_sampler="nutpie", nuts_sampler_kwargs={"backend": "JAX", "gradient_backend": "JAX"}
141
+ )
142
+
143
+ References
144
+ ----------
145
+ .. [1] Durbin, James, and Siem Jan Koopman. 2012.
146
+ Time Series Analysis by State Space Methods: Second Edition.
147
+ Oxford University Press.
148
+ """
149
+
150
+ def __init__(
151
+ self,
152
+ name: str | None = None,
153
+ cycle_length: int | None = None,
154
+ estimate_cycle_length: bool = False,
155
+ dampen: bool = False,
156
+ innovations: bool = True,
157
+ observed_state_names: list[str] | None = None,
158
+ ):
159
+ if observed_state_names is None:
160
+ observed_state_names = ["data"]
161
+
162
+ if cycle_length is None and not estimate_cycle_length:
163
+ raise ValueError("Must specify cycle_length if estimate_cycle_length is False")
164
+ if cycle_length is not None and estimate_cycle_length:
165
+ raise ValueError("Cannot specify cycle_length if estimate_cycle_length is True")
166
+ if name is None:
167
+ cycle = int(cycle_length) if cycle_length is not None else "Estimate"
168
+ name = f"Cycle[s={cycle}, dampen={dampen}, innovations={innovations}]"
169
+
170
+ self.estimate_cycle_length = estimate_cycle_length
171
+ self.cycle_length = cycle_length
172
+ self.innovations = innovations
173
+ self.dampen = dampen
174
+ self.n_coefs = 1
175
+
176
+ k_endog = len(observed_state_names)
177
+
178
+ k_states = 2 * k_endog
179
+ k_posdef = 2 * k_endog
180
+
181
+ obs_state_idx = np.zeros(k_states)
182
+ obs_state_idx[slice(0, k_states, 2)] = 1
183
+
184
+ super().__init__(
185
+ name=name,
186
+ k_endog=k_endog,
187
+ k_states=k_states,
188
+ k_posdef=k_posdef,
189
+ measurement_error=False,
190
+ combine_hidden_states=True,
191
+ obs_state_idxs=obs_state_idx,
192
+ observed_state_names=observed_state_names,
193
+ )
194
+
195
+ def make_symbolic_graph(self) -> None:
196
+ Z = np.array([1.0, 0.0]).reshape((1, -1))
197
+ design_matrix = block_diag(*[Z for _ in range(self.k_endog)])
198
+ self.ssm["design", :, :] = pt.as_tensor_variable(design_matrix)
199
+
200
+ # selection matrix R defines structure of innovations (always identity for cycle components)
201
+ # when innovations=False, state cov Q=0, hence R @ Q @ R.T = 0
202
+ R = np.eye(2) # 2x2 identity for each cycle component
203
+ selection_matrix = block_diag(*[R for _ in range(self.k_endog)])
204
+ self.ssm["selection", :, :] = pt.as_tensor_variable(selection_matrix)
205
+
206
+ init_state = self.make_and_register_variable(
207
+ f"{self.name}", shape=(self.k_endog, 2) if self.k_endog > 1 else (self.k_states,)
208
+ )
209
+ self.ssm["initial_state", :] = init_state.ravel()
210
+
211
+ if self.estimate_cycle_length:
212
+ lamb = self.make_and_register_variable(f"length_{self.name}", shape=())
213
+ else:
214
+ lamb = self.cycle_length
215
+
216
+ if self.dampen:
217
+ rho = self.make_and_register_variable(f"dampening_factor_{self.name}", shape=())
218
+ else:
219
+ rho = 1
220
+
221
+ T = rho * _frequency_transition_block(lamb, j=1)
222
+ transition = block_diag(*[T for _ in range(self.k_endog)])
223
+ self.ssm["transition"] = pt.specify_shape(transition, (self.k_states, self.k_states))
224
+
225
+ if self.innovations:
226
+ if self.k_endog == 1:
227
+ sigma_cycle = self.make_and_register_variable(f"sigma_{self.name}", shape=())
228
+ self.ssm["state_cov", :, :] = pt.eye(self.k_posdef) * sigma_cycle**2
229
+ else:
230
+ sigma_cycle = self.make_and_register_variable(
231
+ f"sigma_{self.name}", shape=(self.k_endog,)
232
+ )
233
+ state_cov = block_diag(
234
+ *[pt.eye(2) * sigma_cycle[i] ** 2 for i in range(self.k_endog)]
235
+ )
236
+ self.ssm["state_cov"] = pt.specify_shape(state_cov, (self.k_states, self.k_states))
237
+ else:
238
+ # explicitly set state cov to 0 when no innovations
239
+ self.ssm["state_cov", :, :] = pt.zeros((self.k_posdef, self.k_posdef))
240
+
241
+ def populate_component_properties(self):
242
+ self.state_names = [
243
+ f"{f}_{self.name}[{var_name}]" if self.k_endog > 1 else f"{f}_{self.name}"
244
+ for var_name in self.observed_state_names
245
+ for f in ["Cos", "Sin"]
246
+ ]
247
+
248
+ self.param_names = [f"{self.name}"]
249
+
250
+ if self.k_endog == 1:
251
+ self.param_dims = {self.name: (f"state_{self.name}",)}
252
+ self.coords = {f"state_{self.name}": self.state_names}
253
+ self.param_info = {
254
+ f"{self.name}": {
255
+ "shape": (2,),
256
+ "constraints": None,
257
+ "dims": (f"state_{self.name}",),
258
+ }
259
+ }
260
+ else:
261
+ self.param_dims = {self.name: (f"endog_{self.name}", f"state_{self.name}")}
262
+ self.coords = {
263
+ f"state_{self.name}": [f"Cos_{self.name}", f"Sin_{self.name}"],
264
+ f"endog_{self.name}": self.observed_state_names,
265
+ }
266
+ self.param_info = {
267
+ f"{self.name}": {
268
+ "shape": (self.k_endog, 2),
269
+ "constraints": None,
270
+ "dims": (f"endog_{self.name}", f"state_{self.name}"),
271
+ }
272
+ }
273
+
274
+ if self.estimate_cycle_length:
275
+ self.param_names += [f"length_{self.name}"]
276
+ self.param_info[f"length_{self.name}"] = {
277
+ "shape": () if self.k_endog == 1 else (self.k_endog,),
278
+ "constraints": "Positive, non-zero",
279
+ "dims": None if self.k_endog == 1 else f"endog_{self.name}",
280
+ }
281
+
282
+ if self.dampen:
283
+ self.param_names += [f"dampening_factor_{self.name}"]
284
+ self.param_info[f"dampening_factor_{self.name}"] = {
285
+ "shape": () if self.k_endog == 1 else (self.k_endog,),
286
+ "constraints": "0 < x ≤ 1",
287
+ "dims": None if self.k_endog == 1 else (f"endog_{self.name}",),
288
+ }
289
+
290
+ if self.innovations:
291
+ self.param_names += [f"sigma_{self.name}"]
292
+ if self.k_endog == 1:
293
+ self.param_info[f"sigma_{self.name}"] = {
294
+ "shape": (),
295
+ "constraints": "Positive",
296
+ "dims": None,
297
+ }
298
+ else:
299
+ self.param_dims[f"sigma_{self.name}"] = (f"endog_{self.name}",)
300
+ self.param_info[f"sigma_{self.name}"] = {
301
+ "shape": (self.k_endog,),
302
+ "constraints": "Positive",
303
+ "dims": (f"endog_{self.name}",),
304
+ }
305
+ self.shock_names = self.state_names.copy()