class_docstr: Parameters
  ----------
  level_knot_scale : float
      sigma for level; default to be .1
  level_segments : int
      the number of segments partitioned by the knots of level (trend)
  level_knot_distance : int
      the distance between every two knots of level (trend)
  level_knot_dates : array like
      list of pre-specified dates for the level knots
  seasonality : int, or list of int
      multiple seasonality
  seasonality_fs_order : int, or list of int
      fourier series order for seasonality
  seasonality_segments : int
      the number of segments partitioned by the knots of seasonality; when it is 0, returns to a static coefficients
      of regression used in the seasonality estimation
  seasonal_initial_knot_scale : float
      scale parameter for seasonal regressors initial coefficient knots; default to be 1
  seasonal_knot_scale : float
      scale parameter for seasonal regressors drift of coefficient knots; default to be 0.1.
  degree_of_freedom : int
      degree of freedom for error t-distribution
  date_freq : str
      date frequency; if not supplied, pd.infer_freq will be used to imply the date frequency.
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
      ################################################################
      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
      ]
  
      ################################################################
      # Model Attributes
      ################################################################
      model = deepcopy(posterior_estimates)
      # TODO: adopt torch ?
      # for k, v in model.items():
      #     model[k] = torch.from_numpy(v)
  
      # We can pull any arbitrary value from the 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]
  
      ################################################################
      # Trend Component
      ################################################################
      new_tp = (
          np.arange(start + 1, start + output_len + 1) / trained_len
      )
      if include_error:
          # in-sample knots
          lev_knot_in = model.get(
              BaseSamplingParameters.LEVEL_KNOT.value
          )
          # TODO: hacky way; let's just assume last two knot distance is knots distance for all knots
          lev_knot_width = (
              self.knots_tp_level[-1] - self.knots_tp_level[-2]
          )
          # check whether we need to put new knots for simulation
          if new_tp[-1] > 1:
              # derive knots tp
              if self.knots_tp_level[-1] + lev_knot_width >= new_tp[-1]:
                  knots_tp_level_out = np.array([new_tp[-1]])
              else:
                  knots_tp_level_out = np.arange(
                      self.knots_tp_level[-1] + lev_knot_width,
                      new_tp[-1],
                      lev_knot_width,
                  )
              new_knots_tp_level = np.concatenate(
                  [self.knots_tp_level, knots_tp_level_out]
              )
              lev_knot_out = np.random.laplace(
                  0,
                  self.level_knot_scale,
                  size=(lev_knot_in.shape[0], len(knots_tp_level_out)),
              )
              lev_knot_out = np.cumsum(
                  np.concatenate(
                      [lev_knot_in[:, -1].reshape(-1, 1), lev_knot_out],
                      axis=1,
                  ),
                  axis=1,
              )[:, 1:]
              lev_knot = np.concatenate(
                  [lev_knot_in, lev_knot_out], axis=1
              )
          else:
              new_knots_tp_level = self.knots_tp_level
              lev_knot = lev_knot_in
          kernel_level = sandwich_kernel(new_tp, new_knots_tp_level)
      else:
          lev_knot = model.get(BaseSamplingParameters.LEVEL_KNOT.value)
          kernel_level = sandwich_kernel(new_tp, self.knots_tp_level)
  
      obs_scale = model.get(BaseSamplingParameters.OBS_SCALE.value)
      obs_scale = obs_scale.reshape(-1, 1)
  
      trend = np.matmul(lev_knot, kernel_level.transpose(1, 0))
  
      ################################################################
      # Seasonality Component
      ################################################################
      # init of regression matrix depends on length of response vector
      total_seas_regression = np.zeros(trend.shape, dtype=np.double)
      seas_decomp = {}
      # update seasonal regression matrices
      if self._seasonality and self.regressor_col:
          df = self._make_seasonal_regressors(df, shift=start)
          coef_knot = model.get(
              RegressionSamplingParameters.COEFFICIENTS_KNOT.value
          )
          kernel_coefficients = sandwich_kernel(
              new_tp, self.knots_tp_coefficients
          )
          coef = np.matmul(
              coef_knot, kernel_coefficients.transpose(1, 0)
          )
          pos = 0
          for idx, cols in enumerate(self.regressor_col_grp):
              seasonal_regressor_matrix = df[cols].values
              seas_coef = coef[..., pos : (pos + len(cols)), :]
              seas_regression = np.sum(
                  seas_coef * seasonal_regressor_matrix.transpose(1, 0),
                  axis=-2,
              )
              seas_decomp[
                  "seasonality_{}".format(self._seasonality[idx])
              ] = seas_regression
              pos += len(cols)
              total_seas_regression += seas_regression
      if include_error:
          epsilon = nct.rvs(
              self._degree_of_freedom,
              nc=0,
              loc=0,
              scale=obs_scale,
              size=(num_sample, len(new_tp)),
          )
          pred_array = trend + total_seas_regression + epsilon
      else:
          pred_array = trend + total_seas_regression
  
      out = {
          PredictionKeys.PREDICTION.value: pred_array,
          PredictionKeys.TREND.value: trend,
      }
      out.update(seas_decomp)
  
      return out
  
imports: import numpy
  import pandas
  from copy import deepcopy
  import torch
  from enum import Enum
  import math
  from scipy.stats import nct
  import matplotlib.pyplot
  from ..constants.constants import PredictionKeys, PredictMethod, TrainingMetaKeys, PredictionMetaKeys
  from ..exceptions import IllegalArgument, ModelException
  from .model_template import ModelTemplate
  from ..estimators.stan_estimator import StanEstimatorMAP
  from ..utils.kernels import sandwich_kernel
  from ..utils.features import make_fourier_series_df
  from orbit.constants.palette import OrbitPalette
  from ..utils.knots import get_knot_idx, get_knot_dates
  from ..utils.plot import orbit_style_decorator
label_desc: Use image node_id 11 for calling the KTRLiteModel obj's underlying member method code with example usage: obj.predict(posterior_estimates, df, training_meta, prediction_meta, include_error) and returns: out