#cython: profile=False
#cython: boundscheck=False
#cython: wraparound=False
#cython: cdivision=False
"""
State Space Models

Author: Chad Fulton  
License: Simplified-BSD
"""

{{py:

TYPES = {
    "s": ("np.float32_t", "np.float32", "np.NPY_FLOAT32"),
    "d": ("np.float64_t", "float", "np.NPY_FLOAT64"),
    "c": ("np.complex64_t", "np.complex64", "np.NPY_COMPLEX64"),
    "z": ("np.complex128_t", "complex", "np.NPY_COMPLEX128"),
}

}}

# Typical imports
cimport numpy as np
import numpy as np
from statsmodels.src.math cimport *
cimport scipy.linalg.cython_blas as blas
cimport scipy.linalg.cython_lapack as lapack

from statsmodels.tsa.statespace._kalman_filter cimport (
    MEMORY_NO_LIKELIHOOD, MEMORY_NO_FORECAST_COV, MEMORY_NO_STD_FORECAST,
    MEMORY_NO_SMOOTHING, FILTER_CONCENTRATED, FILTER_CHANDRASEKHAR)

{{for prefix, types in TYPES.items()}}
{{py:cython_type, dtype, typenum = types}}
{{py:
combined_prefix = prefix
combined_cython_type = cython_type
if prefix == 'c':
    combined_prefix = 'z'
    combined_cython_type = 'np.complex128_t'
if prefix == 's':
    combined_prefix = 'd'
    combined_cython_type = 'np.float64_t'
combined_suffix = ''
if combined_prefix == 'z':
    combined_suffix = 'u'
}}

# ### Univariate Kalman filter
#
# The following are the routines as defined in the univariate Kalman filter.
#
# See Durbin and Koopman (2012) Chapter 6.4

cdef int {{prefix}}forecast_univariate({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):

    # Constants
    cdef:
        int i, j, k
        int inc = 1
        int forecast_cov_t = kfilter.t
        int check
        {{cython_type}} forecast_error_cov
        {{cython_type}} forecast_error_cov_inv
        {{cython_type}} forecast_error_cov_inv_prev = 0.0

    if kfilter.conserve_memory & MEMORY_NO_FORECAST_COV > 0:
        forecast_cov_t = 1

    # Initialize the filtered states
    {{prefix}}initialize_filtered(kfilter, model)

    # Make sure the loglikelihood is set to zero if necessary

    # Iterate over the observations at time t
    for i in range(model._k_endog):

        # #### Forecast for time t
        # `forecast` $= Z_{t,i} a_{t,i} + d_{t,i}$
        # Note: $Z_{t,i}$ is a row vector starting at [i,0,t] and ending at
        # [i,k_states,t]
        # Note: zdot and cdot are broken, so have to use gemv for those

        # #### Forecast error for time t
        # `forecast_error` $\equiv v_t = y_t -$ `forecast`
        {{prefix}}forecast_error(kfilter, model, i)

        # #### Forecast error covariance matrix for time t
        # $F_{t,i} \equiv Z_{t,i} P_{t,i} Z_{t,i}' + H_{t,i}$
        # TODO what about Kalman convergence?
        # Note: zdot and cdot are broken, so have to use gemv for those
        if not kfilter.converged:
            forecast_error_cov = {{prefix}}forecast_error_cov(kfilter, model, i)
        else:
            forecast_error_cov = kfilter._forecast_error_cov[i + i*kfilter.k_endog]

        # Handle numerical issues that can cause a very small negative
        # forecast_error_cov
        check = {{prefix}}check1(kfilter, forecast_error_cov)
        if check:
            kfilter._forecast_error_cov[i + i*kfilter.k_endog] = 0
            forecast_error_cov = 0

        # If we have a non-zero variance
        # (if we have a zero-variance then we are done with this iteration)
        check = {{prefix}}check2(kfilter, i, forecast_error_cov)
        if check:
            forecast_error_cov_inv = {{prefix}}forecast_error_cov_inv(kfilter, model, i, forecast_error_cov)

            {{prefix}}standardized_forecast_error(kfilter, model, i, forecast_error_cov_inv)

            # Save temporary array data
            {{prefix}}temp_arrays(kfilter, model, i, forecast_error_cov_inv)

            # #### Filtered state for time t
            # $a_{t,i+1} = a_{t,i} + P_{t,i} Z_{t,i}' F_{t,i}^{-1} v_{t,i}$  
            # Make a new temporary array  
            # K_{t,i} = P_{t,i} Z_{t,i}' F_{t,i}^{-1}
            {{prefix}}filtered_state(kfilter, model, i, forecast_error_cov_inv)

            # Chandrasekhar arrays
            if not kfilter.converged and (kfilter.filter_method & FILTER_CHANDRASEKHAR > 0):
                if kfilter.t > 0:
                    forecast_error_cov_inv_prev = 1.0 / kfilter.forecast_error_cov[i, i, forecast_cov_t - 1]
                {{prefix}}chandrasekhar_recursion(kfilter, model, i, forecast_error_cov, forecast_error_cov_inv, forecast_error_cov_inv_prev)

            # #### Filtered state covariance for time t
            # $P_{t,i+1} = P_{t,i} - P_{t,i} Z_{t,i}' F_{t,i}^{-1} Z_{t,i} P_{t,i}'$
            if not kfilter.converged:
                {{prefix}}filtered_state_cov(kfilter, model, i, forecast_error_cov_inv)

            # #### Loglikelihood
            {{prefix}}loglikelihood(kfilter, model, i, forecast_error_cov, forecast_error_cov_inv)
        else:
            # Otherwise, we need to record that this observation is not associated
            # with a loglikelihood step (so that it can be excluded in the denominator
            # when computing the scale)
            kfilter.nobs_kendog_univariate_singular = kfilter.nobs_kendog_univariate_singular + 1

    {{prefix}}symmetry(kfilter, model)

    return 0

cdef void {{prefix}}initialize_filtered({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    cdef int inc = 1
    blas.{{prefix}}copy(&kfilter.k_states, kfilter._input_state, &inc,
                                           kfilter._filtered_state, &inc)
    if not kfilter.converged:
        blas.{{prefix}}copy(&kfilter.k_states2, kfilter._input_state_cov, &inc,
                                                kfilter._filtered_state_cov, &inc)

cdef int {{prefix}}check1({{prefix}}KalmanFilter kfilter, {{cython_type}} forecast_error_cov):
    if not kfilter.converged:
        return forecast_error_cov{{if combined_prefix == 'z'}}.real{{endif}} < 0
    else:
        return False

cdef int {{prefix}}check2({{prefix}}KalmanFilter kfilter, int i, {{cython_type}} forecast_error_cov):
    if not kfilter.converged:
        kfilter.forecast_error_ipiv[i] = forecast_error_cov{{if combined_prefix == 'z'}}.real{{endif}} > kfilter.tolerance_diffuse
    return kfilter.forecast_error_ipiv[i]

cdef {{cython_type}} {{prefix}}forecast_error_cov_inv({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, int i, {{cython_type}} forecast_error_cov):
    if not kfilter.converged:
        kfilter.forecast_error_fac[i, i] = 1.0 / forecast_error_cov

    return kfilter.forecast_error_fac[i, i]

cdef void {{prefix}}standardized_forecast_error({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, int i, {{cython_type}} forecast_error_cov_inv):
    if not (kfilter.conserve_memory & MEMORY_NO_STD_FORECAST > 0):
            kfilter._standardized_forecast_error[i] = (
                kfilter._forecast_error[i] * forecast_error_cov_inv**0.5)

cdef void {{prefix}}symmetry({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    cdef int j, k
    # Make final filtered_state_cov symmetric (is not currently symmetric
    # due to use of ?syr or ?her)
    if not kfilter.converged:
        for j in range(model._k_states):      # columns
            for k in range(model._k_states):  # rows
                if k > j: # row > column => in lower triangle
                    kfilter._filtered_state_cov[j + k*kfilter.k_states] = kfilter._filtered_state_cov[k + j*kfilter.k_states]

cdef void {{prefix}}forecast_error({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, int i):
    cdef:
        int inc = 1
        {{cython_type}} alpha = 1
        {{cython_type}} beta = 0
        int k_states = model._k_states
    # Adjust for a VAR transition (i.e. design = [#, 0], where the zeros
    # correspond to all states except the first k_posdef states)
    if model.subset_design:
        k_states = model._k_posdef

    # `forecast` $= Z_{t,i} a_{t,i} + d_{t,i}$
    {{if combined_prefix == 'd'}}
    kfilter._forecast[i] = (
        model._obs_intercept[i] +
        blas.{{prefix}}dot(&k_states, &model._design[i], &model._k_endog,
                                      kfilter._filtered_state, &inc)
    )
    {{else}}
    blas.{{prefix}}gemv("N", &inc, &k_states,
                   &alpha, kfilter._filtered_state, &inc,
                           &model._design[i], &model._k_endog,
                   &beta, kfilter._tmp0, &inc)
    kfilter._forecast[i] = model._obs_intercept[i] + kfilter._tmp0[0]
    {{endif}}

    # `forecast_error` $\equiv v_t = y_t -$ `forecast`
    kfilter._forecast_error[i] = model._obs[i] - kfilter._forecast[i]

cdef {{cython_type}} {{prefix}}forecast_error_cov({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, int i):
    cdef:
        int inc = 1
        {{cython_type}} alpha = 1
        {{cython_type}} beta = 0
        {{cython_type}} forecast_error_cov
        int k_states = model._k_states

    # Adjust for a VAR transition (i.e. design = [#, 0], where the zeros
    # correspond to all states except the first k_posdef states)
    if model.subset_design:
        k_states = model._k_posdef

    # *Intermediate calculation* (used just below and then once more)  
    # $M_{t,i} = P_{t,i} Z_{t,i}'$  
    # $(m \times 1) = (m \times m) (1 \times m)'$
    # blas.{{prefix}}gemv("N", &model._k_states, &k_states,
    #       &alpha, kfilter._filtered_state_cov, &kfilter.k_states,
    #               &model._design[i], &model._k_endog,
    #       &beta, &kfilter._M[i*kfilter.k_states], &inc)

    # $F_{t,i} \equiv Z_{t,i} P_{t,i} Z_{t,i}' + H_{t,i}$
    {{if combined_prefix == 'd'}}
    blas.{{prefix}}symv("L", &model._k_states,
          &alpha, kfilter._filtered_state_cov, &kfilter.k_states,
                  &model._design[i], &model._k_endog,
          &beta, &kfilter._M[i*kfilter.k_states], &inc)

    forecast_error_cov = (
        model._obs_cov[i + i*model._k_endog] +
        blas.{{prefix}}dot(&k_states, &model._design[i], &model._k_endog,
                                      &kfilter._M[i*kfilter.k_states], &inc)
    )
    {{else}}
    blas.{{prefix}}symm("R", "L", &inc, &model._k_states,
          &alpha, kfilter._filtered_state_cov, &kfilter.k_states,
                  &model._design[i], &model._k_endog,
          &beta, &kfilter._M[i*kfilter.k_states], &inc)

    blas.{{prefix}}gemv("N", &inc, &k_states,
                   &alpha, &kfilter._M[i*kfilter.k_states], &inc,
                           &model._design[i], &model._k_endog,
                   &beta, kfilter._tmp0, &inc)
    forecast_error_cov = model._obs_cov[i + i*model._k_endog] + kfilter._tmp0[0]
    {{endif}}
    kfilter._forecast_error_cov[i + i*kfilter.k_endog] = forecast_error_cov
    return forecast_error_cov

cdef void {{prefix}}temp_arrays({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, int i, {{cython_type}} forecast_error_cov_inv):
    cdef:
        int k_states = model._k_states
        tmp_1 = 0

    # Adjust for a VAR transition (i.e. design = [#, 0], where the zeros
    # correspond to all states except the first k_posdef states)
    if model.subset_design:
        k_states = model._k_posdef

    # $\\#_1 = P_{t,i} Z_{t,i}'$ - set above
    # $\\#_2 = v_{t,i} / F_{t,i}$
    kfilter._tmp2[i] = kfilter._forecast_error[i] * forecast_error_cov_inv
    # $\\#_3 = Z_{t,i} / F_{t,i}$
    # $\\#_4 = H_{t,i} / F_{t,i}$
    if not kfilter.converged:
        blas.{{prefix}}copy(&k_states, &model._design[i], &model._k_endog,
                                       &kfilter._tmp3[i], &kfilter.k_endog)
        blas.{{prefix}}scal(&k_states, &forecast_error_cov_inv, &kfilter._tmp3[i], &kfilter.k_endog)

        kfilter._tmp4[i + i*kfilter.k_endog] = model._obs_cov[i + i*model._k_endog] * forecast_error_cov_inv
    elif kfilter.conserve_memory & MEMORY_NO_SMOOTHING > 0:
        # If we're converged and we're not storing these arrays, then we
        # already have the converged values and there's nothing more to do
        pass
    else:
        # If we're converged and we are storing these arrays, then we
        # just need to copy them from the previous iteration
        blas.{{prefix}}copy(&k_states, &kfilter.tmp3[i, 0, kfilter.t - 1], &kfilter.k_endog,
                                       &kfilter._tmp3[i], &kfilter.k_endog)
        kfilter._tmp4[i + i*kfilter.k_endog] = kfilter.tmp4[i, i, kfilter.t - 1]

cdef void {{prefix}}filtered_state({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, int i, {{cython_type}} forecast_error_cov_inv):
    cdef int j
    # $a_{t,i+1} = a_{t,i} + P_{t,i} Z_{t,i}' F_{t,i}^{-1} v_{t,i}$  
    for j in range(model._k_states):
        if not kfilter.converged:
            kfilter._kalman_gain[j + i*kfilter.k_states] = kfilter._M[j + i*kfilter.k_states] * forecast_error_cov_inv
        kfilter._filtered_state[j] = (
            kfilter._filtered_state[j] +
            kfilter._forecast_error[i] * kfilter._kalman_gain[j + i*kfilter.k_states]
        )

cdef void {{prefix}}filtered_state_cov({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, int i, {{cython_type}} forecast_error_cov_inv):
    cdef:
        int inc = 1, j, k
        {{cython_type}} scalar = -1.0 * forecast_error_cov_inv
        {{cython_type}} alpha = 1.0
        {{cython_type}} gamma = -1.0
        int k_states = model._k_states
        int k_states1 = model._k_states

    # Adjust for a VAR transition (i.e. design = [#, 0], where the zeros
    # correspond to all states except the first k_posdef states)
    if model.subset_design:
        k_states = model._k_posdef
        if model._k_posdef > model._k_states:
            k_states1 = model._k_posdef + 1

    # $P_{t,i+1} = P_{t,i} - P_{t,i} Z_{t,i}' F_{t,i}^{-1} Z_{t,i} P_{t,i}'$
    # blas.{{prefix}}ger{{combined_suffix}}(&model._k_states, &model._k_states,
    #     &gamma, &kfilter._M[i*kfilter.k_states], &inc,
    #             &kfilter._kalman_gain[i*kfilter.k_states], &inc,
    #     kfilter._filtered_state_cov, &kfilter.k_states
    # )

    {{if combined_prefix == 'd'}}
    blas.{{prefix}}syr("L", &model._k_states,
        &scalar, &kfilter._M[i*kfilter.k_states], &inc,
                 kfilter._filtered_state_cov, &kfilter.k_states
    )
    {{else}}
    blas.{{prefix}}syrk("L", "N", &model._k_states, &inc,
        &scalar, &kfilter._M[i*kfilter.k_states], &kfilter.k_states,
        &alpha, kfilter._filtered_state_cov, &kfilter.k_states)
    {{endif}}

cdef void {{prefix}}chandrasekhar_recursion({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, int i, {{cython_type}} forecast_error_cov, {{cython_type}} forecast_error_cov_inv, {{cython_type}} forecast_error_cov_inv_prev):
    # Constants
    cdef:
        int inc = 1
        int j, k
        {{cython_type}} alpha = 1.0
        {{cython_type}} beta = 0.0
        {{cython_type}} gamma = -1.0

    # Initialization
    if kfilter.t == 0:
        if i == 0:
            kfilter.CM[:] = 0

        # W[:, i:i+1] = T @ (P @ Z[i].T)
        # W[:, i:i+1] = T @ K @ F[i, i]
        # Note: we scale by forecast error cov here b/c kalman_gain was
        # computed above as K = P @ Z[i].T @ (1 / F[i, i])
        blas.{{prefix}}gemv("N", &model._k_states, &model._k_states,
          &forecast_error_cov, model._transition, &model._k_states,
                  &kfilter._kalman_gain[i * kfilter.k_states], &inc,
          &beta, &kfilter.CW[0, i], &inc)
        # M[i, i] = Finv[i, i]
        kfilter.CM[i, i] = -forecast_error_cov_inv
    # Updating
    else:
        # M.T @ W.T. (p x p) (p x m)
        blas.{{prefix}}gemm("T", "T", &model._k_endog, &model._k_states, &model._k_endog,
          &alpha, &kfilter.CM[0, 0], &kfilter.k_endog,
                  &kfilter.CW[0, 0], &kfilter.k_states,
          &beta, &kfilter.CMW[0, 0], &kfilter.k_endog)
        # MW @ Z[i].T (p x m) (m x 1) -> (p x 1)
        blas.{{prefix}}gemv("N", &model._k_endog, &model._k_states,
          &alpha, &kfilter.CMW[0, 0], &kfilter.k_endog,
                  &model._design[i], &model._k_endog,
          &beta, &kfilter.CMWZ[0, 0], &inc)

        # M = M + MWZ @ MWZ.T / F_prev[i, i]
        # Note: syr / syrk only fills in lower triangle here
        {{if combined_prefix == 'd'}}
        blas.{{prefix}}syr("L", &model._k_endog,
            &forecast_error_cov_inv_prev, &kfilter.CMWZ[0, 0], &inc,
                     &kfilter.CM[0, 0], &kfilter.k_endog)
        {{else}}
        blas.{{prefix}}syrk("L", "N", &model._k_endog, &inc,
            &forecast_error_cov_inv_prev, &kfilter.CMWZ[0, 0], &kfilter.k_endog,
            &alpha, &kfilter.CM[0, 0], &kfilter.k_endog)
        {{endif}}

        # Fill in the upper triangle
        for j in range(model._k_endog):      # columns
            for k in range(model._k_endog):  # rows
                if k > j: # row > column => in lower triangle
                    kfilter.CM[j, k] = kfilter.CM[k, j]

        # Compute W
        # W -> tmpW
        blas.{{prefix}}copy(&model._k_endogstates, &kfilter.CW[0, 0], &inc, &kfilter.CtmpW[0, 0], &inc)

        if i == model.k_endog - 1:
            # W = (T - T @ K @ Z[i]) @ W

            # Compute T @ K: (m x m) (m x 1) -> (m x 1)
            # Note: we previously copied CW -> CtmpW, so overwriting CW is okay
            blas.{{prefix}}gemv("N", &model._k_states, &model._k_states,
              &alpha, model._transition, &model._k_states,
                      &kfilter._kalman_gain[i*kfilter.k_states], &inc,
              &beta, &kfilter.CW[0, 0], &inc)
            # T -> tmp00
            blas.{{prefix}}copy(&model._k_states2, model._transition, &inc, kfilter._tmp00, &inc)
            # T - (T @ K) @ Z[i]: (m x 1) (1 x m) -> (m x m)
            blas.{{prefix}}ger{{combined_suffix}}(&model._k_states, &model._k_states,
                &gamma, &kfilter.CW[0, 0], &inc,
                       &model._design[i], &model._k_endog,
                kfilter._tmp00, &kfilter.k_states)

            # (T - T @ K @ Z[i]) @ tmpW -> W
            blas.{{prefix}}gemm("N", "N", &model._k_states, &model._k_endog, &model._k_states,
                  &alpha, kfilter._tmp00, &kfilter.k_states,
                          &kfilter.CtmpW[0, 0], &kfilter.k_states,
                  &beta, &kfilter.CW[0, 0], &kfilter.k_states)
        else:
            # W = (I - I @ K @ Z[i]) @ W
            # K @ Z[i] (m x 1) (1 x m) -> (m x m)
            kfilter.tmp0[:] = 0
            blas.{{prefix}}ger{{combined_suffix}}(&model._k_states, &model._k_states,
                &alpha, &kfilter._kalman_gain[i*kfilter.k_states], &inc,
                        &model._design[i], &model._k_endog,
                kfilter._tmp0, &kfilter.k_states)

            # W = - K @ Z[i] @ W + W
            blas.{{prefix}}gemm("N", "N", &model._k_states, &model._k_endog, &model._k_states,
                  &gamma, kfilter._tmp0, &kfilter.k_states,
                          &kfilter.CtmpW[0, 0], &kfilter.k_states,
                  &alpha, &kfilter.CW[0, 0], &kfilter.k_states)


cdef void {{prefix}}loglikelihood({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, int i, {{cython_type}} forecast_error_cov, {{cython_type}} forecast_error_cov_inv):
    kfilter._loglikelihood[0] = (
        kfilter._loglikelihood[0] - 0.5*(
            {{combined_prefix}}log(2 * NPY_PI * forecast_error_cov)
        )
    )
    if kfilter.filter_method & FILTER_CONCENTRATED:
        kfilter._scale[0] = kfilter._scale[0] + kfilter._forecast_error[i]**2 * forecast_error_cov_inv
    else:
        kfilter._loglikelihood[0] = kfilter._loglikelihood[0] - 0.5 * (kfilter._forecast_error[i]**2 * forecast_error_cov_inv)

cdef int {{prefix}}updating_univariate({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    # the updating step was performed in the forecast_univariate step
    return 0

cdef int {{prefix}}prediction_univariate({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    # Constants
    cdef:
        int inc = 1
        int i, j
        {{cython_type}} alpha = 1.0
        {{cython_type}} beta = 0.0
        {{cython_type}} gamma = -1.0

    # #### Predicted state for time t+1
    # $a_{t+1} = T_t a_{t,n} + c_t$

    # #### Predicted state covariance matrix for time t+1
    # $P_{t+1} = T_t P_{t,n} T_t' + Q_t^*$
    #
    # TODO check behavior during convergence
    if not model.companion_transition:
        {{prefix}}predicted_state(kfilter, model)
        if not kfilter.converged:
            if kfilter.filter_method & FILTER_CHANDRASEKHAR > 0:
                {{prefix}}predicted_state_cov_chandrasekhar(kfilter, model)
            else:
                {{prefix}}predicted_state_cov(kfilter, model)
    else:
        {{prefix}}companion_predicted_state(kfilter, model)
        if not kfilter.converged:
            {{prefix}}companion_predicted_state_cov(kfilter, model)

    # #### Kalman gain for time t
    # $K_t = T_t P_t Z_t' F_t^{-1}$  
    # Kalman gain calculation done in forecasting step.

    return 0

cdef void {{prefix}}predicted_state({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    cdef:
        int inc = 1
        {{cython_type}} alpha = 1.0

    # $a_{t+1} = T_t a_{t,n} + c_t$
    blas.{{prefix}}copy(&model._k_states, model._state_intercept, &inc, kfilter._predicted_state, &inc)
    blas.{{prefix}}gemv("N", &model._k_states, &model._k_states,
          &alpha, model._transition, &model._k_states,
                  kfilter._filtered_state, &inc,
          &alpha, kfilter._predicted_state, &inc)

cdef void {{prefix}}predicted_state_cov({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    cdef:
        int inc = 1
        {{cython_type}} alpha = 1.0
        {{cython_type}} beta = 0.0

    # $P_{t+1} = T_t P_{t,n} T_t' + Q_t^*$
    blas.{{prefix}}copy(&model._k_states2, model._selected_state_cov, &inc, kfilter._predicted_state_cov, &inc)
    # `tmp0` array used here, dimension $(m \times m)$  

    # $\\#_0 = T_t P_{t|t} $

    # $(m \times m) = (m \times m) (m \times m)$
    # blas.{{prefix}}gemm("N", "N", &model._k_states, &model._k_states, &model._k_states,
    #       &alpha, model._transition, &model._k_states,
    #               kfilter._filtered_state_cov, &kfilter.k_states,
    #       &beta, kfilter._tmp0, &kfilter.k_states)
    blas.{{prefix}}symm("R", "L", &model._k_states, &model._k_states,
          &alpha, kfilter._filtered_state_cov, &kfilter.k_states,
                  model._transition, &model._k_states,
          &beta, kfilter._tmp0, &kfilter.k_states)
    # $P_{t+1} = 1.0 \\#_0 T_t' + 1.0 \\#$  
    # $(m \times m) = (m \times m) (m \times m) + (m \times m)$
    blas.{{prefix}}gemm("N", "T", &model._k_states, &model._k_states, &model._k_states,
          &alpha, kfilter._tmp0, &kfilter.k_states,
                  model._transition, &model._k_states,
          &alpha, kfilter._predicted_state_cov, &kfilter.k_states)

cdef void {{prefix}}predicted_state_cov_chandrasekhar({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    # Constants
    cdef:
        int inc = 1
        {{cython_type}} alpha = 1.0
        {{cython_type}} beta = 0.0
    blas.{{prefix}}copy(&model._k_states2, kfilter._input_state_cov, &inc, kfilter._predicted_state_cov, &inc)
    # M @ W.T. (p x p) (p x m)
    blas.{{prefix}}gemm("N", "T", &model._k_endog, &model._k_states, &model._k_endog,
      &alpha, &kfilter.CM[0, 0], &kfilter.k_endog,
              &kfilter.CW[0, 0], &kfilter.k_states,
      &beta, &kfilter.CMW[0, 0], &kfilter.k_endog)
    # P = P + W M W.T
    blas.{{prefix}}gemm("N", "N", &model._k_states, &model._k_states, &model._k_endog,
      &alpha, &kfilter.CW[0, 0], &kfilter.k_states,
              &kfilter.CMW[0, 0], &kfilter.k_endog,
      &alpha, kfilter._predicted_state_cov, &kfilter.k_states)

cdef void {{prefix}}companion_predicted_state({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    cdef:
        int i
        int inc = 1
        {{cython_type}} alpha = 1.0

    # $a_{t+1} = T_t a_{t,n} + c_t$
    blas.{{prefix}}copy(&model._k_states, model._state_intercept, &inc, kfilter._predicted_state, &inc)
    blas.{{prefix}}gemv("N", &model._k_posdef, &model._k_states,
          &alpha, model._transition, &model._k_states,
                  kfilter._filtered_state, &inc,
          &alpha, kfilter._predicted_state, &inc)

    for i in range(model._k_posdef, model._k_states):
        kfilter._predicted_state[i] = kfilter._predicted_state[i] + kfilter._filtered_state[i - model._k_posdef]

cdef void {{prefix}}companion_predicted_state_cov({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    cdef:
        int i, j, idx
        int inc = 1
        {{cython_type}} alpha = 1.0
        {{cython_type}} beta = 0.0
        {{cython_type}} tmp

    # $P_{t+1} = T_t P_{t,n} T_t' + Q_t^*$

    # `tmp0` array used here, dimension $(p \times m)$  
    # $\\#_0 = \phi_t P_{t|t} $

    # $(p \times m) = (p \times m) (m \times m)$
    # TODO: symm?
    blas.{{prefix}}gemm("N", "N", &model._k_posdef, &model._k_states, &model._k_states,
          &alpha, model._transition, &model._k_states,
                  kfilter._filtered_state_cov, &kfilter.k_states,
          &beta, kfilter._tmp0, &kfilter.k_states)
                
    # $P_{t+1} = 1.0 \\#_0 \phi_t' + 1.0 \\#$  
    # $(m \times m) = (p \times m) (m \times p) + (m \times m)$
    blas.{{prefix}}gemm("N", "T", &model._k_posdef, &model._k_posdef, &model._k_states,
          &alpha, kfilter._tmp0, &kfilter.k_states,
                  model._transition, &model._k_states,
          &beta, kfilter._predicted_state_cov, &kfilter.k_states)

    # Fill in the basic matrix blocks
    for i in range(kfilter.k_states):      # columns
        for j in range(kfilter.k_states):  # rows
            idx = j + i*kfilter.k_states

            # Add the Q matrix to the upper-left block
            if i < model._k_posdef and j < model._k_posdef:
                kfilter._predicted_state_cov[idx] = (
                    kfilter._predicted_state_cov[idx] + 
                    model._state_cov[j + i*model._k_posdef]
                )

            # Set the upper-right block to be the first m-p columns of
            # \phi _t P_{t|t}, and the lower-left block to the its transpose
            elif i >= model._k_posdef and j < model._k_posdef:
                tmp = kfilter._tmp0[j + (i-model._k_posdef)*kfilter.k_states]
                kfilter._predicted_state_cov[idx] = tmp
                kfilter._predicted_state_cov[i + j*model._k_states] = tmp

            # Set the lower-right block 
            elif i >= model._k_posdef and j >= model._k_posdef:
                kfilter._predicted_state_cov[idx] = (
                    kfilter._filtered_state_cov[(j - model._k_posdef) + (i - model._k_posdef)*kfilter.k_states]
                )

cdef {{cython_type}} {{prefix}}inverse_noop_univariate({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, {{cython_type}} determinant) except *:
    return -np.inf

cdef {{cython_type}} {{prefix}}loglikelihood_univariate({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model, {{cython_type}} determinant):
    return 0

cdef {{cython_type}} {{prefix}}scale_univariate({{prefix}}KalmanFilter kfilter, {{prefix}}Statespace model):
    return 0

{{endfor}}