class_docstr: Parameters
  ----------
  regressor_col : list
      Names of regressor columns, if any
  regressor_sign :  list
      list with values { '+', '-', '=' } such that
      '+' indicates regressor coefficient estimates are constrained to [0, inf).
      '-' indicates regressor coefficient estimates are constrained to (-inf, 0].
      '=' indicates regressor coefficient estimates can be any value between (-inf, inf).
      The length of `regressor_sign` must be the same length as `regressor_col`. If None,
      all elements of list will be set to '='.
  regressor_beta_prior : list
      list of prior float values for regressor coefficient betas. The length of `regressor_beta_prior`
      must be the same length as `regressor_col`. If None, use non-informative priors.
  regressor_sigma_prior : list
      list of prior float values for regressor coefficient sigmas. The length of `regressor_sigma_prior`
      must be the same length as `regressor_col`. If None, use non-informative priors.
  regression_penalty : { 'fixed_ridge', 'lasso', 'auto_ridge' }
      regression penalty method
  lasso_scale : float
      float value between [0, 1], applicable only if `regression_penalty` == 'lasso'
  auto_ridge_scale : float
      float value between [0, 1], applicable only if `regression_penalty` == 'auto_ridge'
  slope_sm_input : float
      float value between [0, 1]. A larger value puts more weight on the current slope.
      If None, the model will estimate this value.
docstr: Vectorized version of prediction math
code: def predict(
      self,
      posterior_estimates,
      df,
      training_meta,
      prediction_meta,
      include_error=False,
      **kwargs,
  ):
      """Vectorized version of prediction math"""
      ################################################################
      # Prediction Attributes
      ################################################################
      # n_forecast_steps = prediction_meta[PredictionMetaKeys.FUTURE_STEPS.value]
      start = prediction_meta[PredictionMetaKeys.START_INDEX.value]
      trained_len = training_meta[TrainingMetaKeys.NUM_OF_OBS.value]
      output_len = prediction_meta[
          PredictionMetaKeys.PREDICTION_DF_LEN.value
      ]
      full_len = prediction_meta[PredictionMetaKeys.END_INDEX.value]
  
      ################################################################
      # Model Attributes
      ################################################################
      model = deepcopy(posterior_estimates)
      for k, v in model.items():
          model[k] = torch.from_numpy(v)
  
      # We can pull any arbitrary value from teh dictionary because we hold the
      # safe assumption: the length of the first dimension is always the number of samples
      # thus can be safely used to determine `num_sample`. If predict_method is anything
      # other than full, the value here should be 1
      arbitrary_posterior_value = list(model.values())[0]
      num_sample = arbitrary_posterior_value.shape[0]
  
      # seasonality components
      seasonality_levels = model.get(
          SeasonalitySamplingParameters.SEASONALITY_LEVELS.value
      )
      seasonality_smoothing_factor = model.get(
          SeasonalitySamplingParameters.SEASONALITY_SMOOTHING_FACTOR.value
      )
  
      # trend components
      slope_smoothing_factor = model.get(
          BaseSamplingParameters.SLOPE_SMOOTHING_FACTOR.value
      )
      level_smoothing_factor = model.get(
          BaseSamplingParameters.LEVEL_SMOOTHING_FACTOR.value
      )
      local_trend_levels = model.get(
          BaseSamplingParameters.LOCAL_TREND_LEVELS.value
      )
      local_trend_slopes = model.get(
          BaseSamplingParameters.LOCAL_TREND_SLOPES.value
      )
      residual_degree_of_freedom = model.get(
          BaseSamplingParameters.RESIDUAL_DEGREE_OF_FREEDOM.value
      )
      residual_sigma = model.get(
          BaseSamplingParameters.RESIDUAL_SIGMA.value
      )
  
      local_trend_coef = model.get(
          BaseSamplingParameters.LOCAL_TREND_COEF.value
      )
      global_trend_power = model.get(
          BaseSamplingParameters.GLOBAL_TREND_POWER.value
      )
      global_trend_coef = model.get(
          BaseSamplingParameters.GLOBAL_TREND_COEF.value
      )
      local_global_trend_sums = model.get(
          BaseSamplingParameters.LOCAL_GLOBAL_TREND_SUMS.value
      )
  
      # regression components
      regressor_beta = model.get(
          RegressionSamplingParameters.REGRESSION_COEFFICIENTS.value
      )
  
      ################################################################
      # Regression Component
      ################################################################
  
      # calculate regression component
      if self.regressor_col is not None and len(self.regressor_col) > 0:
          regressor_beta = regressor_beta.t()
          if len(regressor_beta.shape) == 1:
              regressor_beta = regressor_beta.unsqueeze(0)
          regressor_matrix = df[self._regressor_col].values
          regressor_torch = torch.from_numpy(regressor_matrix).double()
          regression = torch.matmul(regressor_torch, regressor_beta)
          regression = regression.t()
      else:
          # regressor is always dependent with df. hence, no need to make full size
          regression = torch.zeros(
              (num_sample, output_len), dtype=torch.double
          )
  
      ################################################################
      # Seasonality Component
      ################################################################
  
      # calculate seasonality component
      if self._seasonality > 1:
          if full_len <= seasonality_levels.shape[1]:
              seasonal_component = seasonality_levels[:, :full_len]
          else:
              seasonality_forecast_length = (
                  full_len - seasonality_levels.shape[1]
              )
              seasonality_forecast_matrix = torch.zeros(
                  (num_sample, seasonality_forecast_length),
                  dtype=torch.double,
              )
              seasonal_component = torch.cat(
                  (seasonality_levels, seasonality_forecast_matrix),
                  dim=1,
              )
      else:
          seasonal_component = torch.zeros(
              (num_sample, full_len), dtype=torch.double
          )
  
      ################################################################
      # Trend Component
      ################################################################
  
      # calculate level component.
      # However, if predicted end of period > training period, update with out-of-samples forecast
      if full_len <= trained_len:
          trend_component = local_global_trend_sums[:, :full_len]
  
          # in-sample error are iids
          if include_error:
              error_value = nct.rvs(
                  df=residual_degree_of_freedom.unsqueeze(-1),
                  nc=0,
                  loc=0,
                  scale=residual_sigma.unsqueeze(-1),
                  size=(num_sample, full_len),
              )
  
              error_value = torch.from_numpy(
                  error_value.reshape(num_sample, full_len)
              ).double()
              trend_component += error_value
      else:
          trend_forecast_length = full_len - trained_len
          trend_forecast_init = torch.zeros(
              (num_sample, trend_forecast_length), dtype=torch.double
          )
          trend_component = local_global_trend_sums
          # in-sample error are iids
          if include_error:
              error_value = nct.rvs(
                  df=residual_degree_of_freedom.unsqueeze(-1),
                  nc=0,
                  loc=0,
                  scale=residual_sigma.unsqueeze(-1),
                  size=(num_sample, local_global_trend_sums.shape[1]),
              )
  
              error_value = torch.from_numpy(
                  error_value.reshape(
                      num_sample, local_global_trend_sums.shape[1]
                  )
              ).double()
              trend_component += error_value
  
          trend_component = torch.cat(
              (trend_component, trend_forecast_init), dim=1
          )
  
          last_local_trend_level = local_trend_levels[:, -1]
          last_local_trend_slope = local_trend_slopes[:, -1]
  
          trend_component_zeros = torch.zeros_like(
              trend_component[:, 0]
          )
  
          for idx in range(trained_len, full_len):
              current_local_trend = (
                  local_trend_coef.flatten() * last_local_trend_slope
              )
              global_trend_power_term = torch.pow(
                  torch.abs(last_local_trend_level),
                  global_trend_power.flatten(),
              )
              current_global_trend = (
                  global_trend_coef.flatten() * global_trend_power_term
              )
              trend_component[:, idx] = (
                  last_local_trend_level
                  + current_local_trend
                  + current_global_trend
              )
  
              if include_error:
                  error_value = nct.rvs(
                      df=residual_degree_of_freedom,
                      nc=0,
                      loc=0,
                      scale=residual_sigma,
                      size=num_sample,
                  )
                  error_value = torch.from_numpy(error_value).double()
                  trend_component[:, idx] += error_value
  
              # a 2d tensor of size (num_sample, 2) in which one of the elements
              # is always zero. We can use this to torch.max() across the sample dimensions
              trend_component_augmented = torch.cat(
                  (
                      trend_component[:, idx][:, None],
                      trend_component_zeros[:, None],
                  ),
                  dim=1,
              )
  
              max_value, _ = torch.max(trend_component_augmented, dim=1)
  
              trend_component[:, idx] = max_value
  
              new_local_trend_level = (
                  level_smoothing_factor * trend_component[:, idx]
                  + (1 - level_smoothing_factor)
                  * last_local_trend_level
              )
  
              last_local_trend_slope = (
                  slope_smoothing_factor
                  * (new_local_trend_level - last_local_trend_level)
                  + (1 - slope_smoothing_factor)
                  * last_local_trend_slope
              )
  
              if (
                  self._seasonality > 1
                  and idx + self._seasonality < full_len
              ):
                  seasonal_component[:, idx + self._seasonality] = (
                      seasonality_smoothing_factor.flatten()
                      * (
                          trend_component[:, idx]
                          + seasonal_component[:, idx]
                          - new_local_trend_level
                      )
                      + (1 - seasonality_smoothing_factor.flatten())
                      * seasonal_component[:, idx]
                  )
  
              last_local_trend_level = new_local_trend_level
  
      ################################################################
      # Combine Components
      ################################################################
  
      # trim component with right start index
      trend_component = trend_component[:, start:]
      seasonal_component = seasonal_component[:, start:]
  
      # sum components
      pred_array = trend_component + seasonal_component + regression
  
      pred_array = pred_array.numpy()
      trend_component = trend_component.numpy()
      seasonal_component = seasonal_component.numpy()
      regression = regression.numpy()
  
      out = {
          PredictionKeys.PREDICTION.value: pred_array,
          PredictionKeys.TREND.value: trend_component,
          PredictionKeys.SEASONALITY.value: seasonal_component,
          PredictionKeys.REGRESSION.value: regression,
      }
  
      return out
  
imports: import numpy
  import pandas
  from scipy.stats import nct
  from copy import deepcopy
  import torch
  from enum import Enum
  import warnings
  from ..constants.constants import DEFAULT_REGRESSOR_SIGN, DEFAULT_REGRESSOR_BETA, DEFAULT_REGRESSOR_SIGMA, COEFFICIENT_DF_COLS, PredictMethod, PredictionKeys, TrainingMetaKeys, PredictionMetaKeys
  from ..exceptions import IllegalArgument, ModelException, DataInputException
  from .ets import ETSModel
  from ..estimators.stan_estimator import StanEstimatorMCMC, StanEstimatorMAP
  from ..estimators.pyro_estimator import PyroEstimatorSVI
label_desc: Use image node_id 12 for calling the LGTModel obj's underlying member method code with example usage: obj.predict(posterior_estimates, df, training_meta, prediction_meta, include_error) and returns: out