def _filter_xs(self, s_0, s_ts, pi_0, sigma_0, As, Cs, Qs, Rs):
        """
        Computes x_1|1, P_1|1, ..., x_T|T, P_T|T given observations, parameters and values of switching
        variable at each time.

        :returns:
        -n_LF x T numpy array
            Smoothed means
        -T x n_LF x n_LF numpy array
            Smoothed covariances
        -T-1 x n_LF x n_LF numpy array
            Smoothed lagged covariances (ie, cov[x_t, x_t-1])
        """
        # Initialize Kalman filter using values of parameters at t = 0, even though they're never used
        kf = Kalman(mu_0=pi_0.copy(), sigma_0=sigma_0.copy(), A=As[s_0], B=None, C=Cs[s_0], D=None,
                    Q=Qs[s_0], R=Rs[s_0])

        # x_t|t, P_t|t, t = 1, ..., T
        x_filts = np.zeros([self.T, self.n_LF])
        P_filts = np.zeros([self.T, self.n_LF, self.n_LF])

        # x_t|t-1, P_t|t-1. t = 1, ..., T. Indexed by t.
        x_pred = np.zeros([self.T, self.n_LF])
        P_pred = np.zeros([self.T, self.n_LF, self.n_LF])

        # Filtering step
        for t in range(0, self.T): # corresponds to t = 1, ..., T
            # Change parameters. Never need to use A_{s_0}, etc.
            kf.A = As[s_ts[t]]
            kf.C = Cs[s_ts[t]]
            kf.Q = Qs[s_ts[t]]
            kf.R = Rs[s_ts[t]]

            # Compute x_{t|t-1}, P_{t|t-1}
            kf.predict()
            x_pred[t] = kf.mu
            P_pred[t] = kf.sigma

            # Compute x_{t|t}, P_{t|t}
            kf.update(self.y_ts[t])
            x_filts[t] = kf.mu
            P_filts[t] = kf.sigma

        # TODO: run smoother to fill in missing data!!!

        return x_filts, P_filts, x_pred, P_pred
def kalman_smooth(Y, U, V, pi0, sigma0, A, B, C, D, Q, R, n_LF):
    """
    Runs Kalman filtering and smoothing step of dynamic factor model estimator.

    Arguments
    -Y: N x T
        Observations
    -U: L x T
        State controls
    -V: P x T
        Observation controls
    -pi0: n_LF x 1
    -sigma0: n_LF x n_LF
    -A: n_LF x n_LF
    -B: n_LF x L
    -C: N x n_LF
    -D: N x M
    -Q: n_LF x n_LF
    -R: N x N

    Returns
    -n_LF x T numpy array
        Smoothed means
    -T x n_LF x n_LF numpy array
        Smoothed covariances
    -T-1 x n_LF x n_LF numpy array
        Smoothed lagged covariances (ie, cov[x_t, x_t-1])
    """
    N, T = Y.shape

    # Initialize Kalman filter
    kf = Kalman(mu_0=pi0.copy(),
                sigma_0=sigma0.copy(),
                A=A,
                B=B,
                C=C,
                D=D,
                Q=Q,
                R=R)

    # sigma_t|t, mu_t|t
    sigma_filt = np.zeros([T, n_LF, n_LF])
    sigma_filt[0] = sigma0.copy()
    mu_filt = np.zeros([T, n_LF])
    mu_filt[0] = pi0.copy()

    # sigma_t|t-1, mu_t|t-1. NOTE: indexed by t-1!!!
    sigma_pred = np.zeros([T - 1, n_LF, n_LF])
    mu_pred = np.zeros([T - 1, n_LF])

    # Filtering step
    for t in range(1, T):
        ### Prediction step
        kf.predict(U[:, t])

        # Save mu_t|t-1 and sigma_t|t-1
        sigma_pred[t - 1, :, :] = kf.sigma
        mu_pred[t - 1] = kf.mu

        ### Update step
        kf.C = C.copy()
        kf.D = D.copy()
        kf.R = R.copy()

        # Need to change observation, observation control and observation noise matrices if observation is
        # incomplete! See Shumway and Stoffer page 314.
        # First, figure out which sensors don't have observations:
        nan_sensors = np.where(np.isnan(Y[:, t]))[0]
        # If some observations were present, go through with the update step
        if nan_sensors.size < N:
            # Zero those observations
            y_t = np.nan_to_num(Y[:, t])

            # Modify parameters
            kf.C[nan_sensors, :] = 0.0
            kf.D[nan_sensors, :] = 0.0
            kf.R[nan_sensors, :] = 0.0
            kf.R[:, nan_sensors] = 0.0
            kf.R[nan_sensors, nan_sensors] = 1.0

            kf.update(y_t, V[:, t])

        # Save filtered mean, covariance
        sigma_filt[t, :, :] = kf.sigma
        mu_filt[t] = kf.mu

    # sigma_t|T, mu_t|T
    sigma_smooth = np.zeros((T, n_LF, n_LF))
    mu_smooth = np.zeros((T, n_LF))

    # Initialize: sigma_T|T = sigma_T|T(filtered)
    sigma_smooth[-1] = sigma_filt[-1]
    # mu_T|T = mu_T|T(filtered)
    mu_smooth[-1] = mu_filt[-1]

    # Lagged covariance. Indexed by t-1.
    sigma_lag_smooth = np.zeros((T - 1, n_LF, n_LF))
    # sigmaLag_{T,T-1} = (1 - K_T C) A V_{T-1|T-1}, where K_T is Kalman gain at last timestep.
    # TODO: unclear what to do here if last observation contains missing components
    K_T = np.dot(sigma_pred[-1], np.dot(kf.C.T, np.linalg.pinv(np.dot(kf.C, \
                                                                    np.dot(sigma_pred[-1], kf.C.T)) + kf.R)))
    sigma_lag_smooth[-1] = np.dot(
        np.dot((np.identity(n_LF) - np.dot(K_T, kf.C)), kf.A), sigma_filt[-2])

    # Backwards Kalman gain
    J = np.zeros((T - 1, n_LF, n_LF))

    # Smoothing step. Runs from t=T-1 to t=0.
    for t in range(T - 2, -1, -1):
        # Backward Kalman gain matrix
        J[t] = np.dot(np.dot(sigma_filt[t], kf.A.T),
                      np.linalg.pinv(sigma_pred[t]))

        # Smoothed mean
        mu_smooth[t] = mu_filt[t] + np.dot(J[t], mu_smooth[t + 1] - mu_pred[t])

        # Smoothed covariance. This is explicity symmetric.
        sigma_smooth[t, :, :] = sigma_filt[t] + np.dot(
            np.dot(J[t], sigma_smooth[t + 1] - sigma_pred[t]), J[t].T)

    # Lagged smoothed covariance (NOT SYMMETRIC!)
    for t in range(T - 3, -1, -1):
        sigma_lag_smooth[t, :, :] = np.dot(sigma_filt[t+1], J[t].T) \
                    + np.dot(np.dot(J[t+1], (sigma_lag_smooth[t+1] - np.dot(kf.A, sigma_filt[t+1]))), J[t].T)

    # Fill in missing Y values
    Y_imp = Y.copy()
    nanRows, nanCols = np.where(np.isnan(Y_imp))
    for s, t in zip(nanRows, nanCols):
        Y_imp[s,
              t] = np.dot(C[s, :], mu_smooth[t, :]) + np.dot(D[s, :], V[:, t])

    return mu_smooth.T, sigma_smooth, sigma_lag_smooth, Y_imp, sigma_filt