"""
State Space Representation and Kalman Filter

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

import contextlib
from warnings import warn

import numpy as np

from statsmodels.tools.sm_exceptions import ValueWarning

from . import tools
from .representation import FrozenRepresentation, OptionWrapper, Representation
from .tools import reorder_missing_matrix, reorder_missing_vector

# Define constants
FILTER_CONVENTIONAL = 0x01     # Durbin and Koopman (2012), Chapter 4
FILTER_EXACT_INITIAL = 0x02    # ibid., Chapter 5.6
FILTER_AUGMENTED = 0x04        # ibid., Chapter 5.7
FILTER_SQUARE_ROOT = 0x08      # ibid., Chapter 6.3
FILTER_UNIVARIATE = 0x10       # ibid., Chapter 6.4
FILTER_COLLAPSED = 0x20        # ibid., Chapter 6.5
FILTER_EXTENDED = 0x40         # ibid., Chapter 10.2
FILTER_UNSCENTED = 0x80        # ibid., Chapter 10.3
FILTER_CONCENTRATED = 0x100    # Harvey (1989), Chapter 3.4
FILTER_CHANDRASEKHAR = 0x200   # Herbst (2015)

INVERT_UNIVARIATE = 0x01
SOLVE_LU = 0x02
INVERT_LU = 0x04
SOLVE_CHOLESKY = 0x08
INVERT_CHOLESKY = 0x10

STABILITY_FORCE_SYMMETRY = 0x01

MEMORY_STORE_ALL = 0
MEMORY_NO_FORECAST_MEAN = 0x01
MEMORY_NO_FORECAST_COV = 0x02
MEMORY_NO_FORECAST = MEMORY_NO_FORECAST_MEAN | MEMORY_NO_FORECAST_COV
MEMORY_NO_PREDICTED_MEAN = 0x04
MEMORY_NO_PREDICTED_COV = 0x08
MEMORY_NO_PREDICTED = MEMORY_NO_PREDICTED_MEAN | MEMORY_NO_PREDICTED_COV
MEMORY_NO_FILTERED_MEAN = 0x10
MEMORY_NO_FILTERED_COV = 0x20
MEMORY_NO_FILTERED = MEMORY_NO_FILTERED_MEAN | MEMORY_NO_FILTERED_COV
MEMORY_NO_LIKELIHOOD = 0x40
MEMORY_NO_GAIN = 0x80
MEMORY_NO_SMOOTHING = 0x100
MEMORY_NO_STD_FORECAST = 0x200
MEMORY_CONSERVE = (
    MEMORY_NO_FORECAST_COV | MEMORY_NO_PREDICTED | MEMORY_NO_FILTERED |
    MEMORY_NO_LIKELIHOOD | MEMORY_NO_GAIN | MEMORY_NO_SMOOTHING
)

TIMING_INIT_PREDICTED = 0
TIMING_INIT_FILTERED = 1


class KalmanFilter(Representation):
    r"""
    State space representation of a time series process, with Kalman filter

    Parameters
    ----------
    k_endog : {array_like, int}
        The observed time-series process :math:`y` if array like or the
        number of variables in the process if an integer.
    k_states : int
        The dimension of the unobserved state process.
    k_posdef : int, optional
        The dimension of a guaranteed positive definite covariance matrix
        describing the shocks in the transition equation. Must be less than
        or equal to `k_states`. Default is `k_states`.
    loglikelihood_burn : int, optional
        The number of initial periods during which the loglikelihood is not
        recorded. Default is 0.
    tolerance : float, optional
        The tolerance at which the Kalman filter determines convergence to
        steady-state. Default is 1e-19.
    results_class : class, optional
        Default results class to use to save filtering output. Default is
        `FilterResults`. If specified, class must extend from `FilterResults`.
    **kwargs
        Keyword arguments may be used to provide values for the filter,
        inversion, and stability methods. See `set_filter_method`,
        `set_inversion_method`, and `set_stability_method`.
        Keyword arguments may be used to provide default values for state space
        matrices. See `Representation` for more details.

    See Also
    --------
    FilterResults
    statsmodels.tsa.statespace.representation.Representation

    Notes
    -----
    There are several types of options available for controlling the Kalman
    filter operation. All options are internally held as bitmasks, but can be
    manipulated by setting class attributes, which act like boolean flags. For
    more information, see the `set_*` class method documentation. The options
    are:

    filter_method
        The filtering method controls aspects of which
        Kalman filtering approach will be used.
    inversion_method
        The Kalman filter may contain one matrix inversion: that of the
        forecast error covariance matrix. The inversion method controls how and
        if that inverse is performed.
    stability_method
        The Kalman filter is a recursive algorithm that may in some cases
        suffer issues with numerical stability. The stability method controls
        what, if any, measures are taken to promote stability.
    conserve_memory
        By default, the Kalman filter computes a number of intermediate
        matrices at each iteration. The memory conservation options control
        which of those matrices are stored.
    filter_timing
        By default, the Kalman filter follows Durbin and Koopman, 2012, in
        initializing the filter with predicted values. Kim and Nelson, 1999,
        instead initialize the filter with filtered values, which is
        essentially just a different timing convention.

    The `filter_method` and `inversion_method` options intentionally allow
    the possibility that multiple methods will be indicated. In the case that
    multiple methods are selected, the underlying Kalman filter will attempt to
    select the optional method given the input data.

    For example, it may be that INVERT_UNIVARIATE and SOLVE_CHOLESKY are
    indicated (this is in fact the default case). In this case, if the
    endogenous vector is 1-dimensional (`k_endog` = 1), then INVERT_UNIVARIATE
    is used and inversion reduces to simple division, and if it has a larger
    dimension, the Cholesky decomposition along with linear solving (rather
    than explicit matrix inversion) is used. If only SOLVE_CHOLESKY had been
    set, then the Cholesky decomposition method would *always* be used, even in
    the case of 1-dimensional data.
    """

    filter_methods = [
        "filter_conventional", "filter_exact_initial", "filter_augmented",
        "filter_square_root", "filter_univariate", "filter_collapsed",
        "filter_extended", "filter_unscented", "filter_concentrated",
        "filter_chandrasekhar"
    ]

    filter_conventional = OptionWrapper("filter_method", FILTER_CONVENTIONAL)
    """
    (bool) Flag for conventional Kalman filtering.
    """
    filter_exact_initial = OptionWrapper("filter_method", FILTER_EXACT_INITIAL)
    """
    (bool) Flag for exact initial Kalman filtering. Not implemented.
    """
    filter_augmented = OptionWrapper("filter_method", FILTER_AUGMENTED)
    """
    (bool) Flag for augmented Kalman filtering. Not implemented.
    """
    filter_square_root = OptionWrapper("filter_method", FILTER_SQUARE_ROOT)
    """
    (bool) Flag for square-root Kalman filtering. Not implemented.
    """
    filter_univariate = OptionWrapper("filter_method", FILTER_UNIVARIATE)
    """
    (bool) Flag for univariate filtering of multivariate observation vector.
    """
    filter_collapsed = OptionWrapper("filter_method", FILTER_COLLAPSED)
    """
    (bool) Flag for Kalman filtering with collapsed observation vector.
    """
    filter_extended = OptionWrapper("filter_method", FILTER_EXTENDED)
    """
    (bool) Flag for extended Kalman filtering. Not implemented.
    """
    filter_unscented = OptionWrapper("filter_method", FILTER_UNSCENTED)
    """
    (bool) Flag for unscented Kalman filtering. Not implemented.
    """
    filter_concentrated = OptionWrapper("filter_method", FILTER_CONCENTRATED)
    """
    (bool) Flag for Kalman filtering with concentrated log-likelihood.
    """
    filter_chandrasekhar = OptionWrapper("filter_method", FILTER_CHANDRASEKHAR)
    """
    (bool) Flag for filtering with Chandrasekhar recursions.
    """

    inversion_methods = [
        "invert_univariate", "solve_lu", "invert_lu", "solve_cholesky",
        "invert_cholesky"
    ]

    invert_univariate = OptionWrapper("inversion_method", INVERT_UNIVARIATE)
    """
    (bool) Flag for univariate inversion method (recommended).
    """
    solve_lu = OptionWrapper("inversion_method", SOLVE_LU)
    """
    (bool) Flag for LU and linear solver inversion method.
    """
    invert_lu = OptionWrapper("inversion_method", INVERT_LU)
    """
    (bool) Flag for LU inversion method.
    """
    solve_cholesky = OptionWrapper("inversion_method", SOLVE_CHOLESKY)
    """
    (bool) Flag for Cholesky and linear solver inversion method (recommended).
    """
    invert_cholesky = OptionWrapper("inversion_method", INVERT_CHOLESKY)
    """
    (bool) Flag for Cholesky inversion method.
    """

    stability_methods = ["stability_force_symmetry"]

    stability_force_symmetry = (
        OptionWrapper("stability_method", STABILITY_FORCE_SYMMETRY)
    )
    """
    (bool) Flag for enforcing covariance matrix symmetry
    """

    memory_options = [
        "memory_store_all", "memory_no_forecast_mean",
        "memory_no_forecast_cov", "memory_no_forecast",
        "memory_no_predicted_mean", "memory_no_predicted_cov",
        "memory_no_predicted", "memory_no_filtered_mean",
        "memory_no_filtered_cov", "memory_no_filtered",
        "memory_no_likelihood", "memory_no_gain",
        "memory_no_smoothing", "memory_no_std_forecast", "memory_conserve"
    ]

    memory_store_all = OptionWrapper("conserve_memory", MEMORY_STORE_ALL)
    """
    (bool) Flag for storing all intermediate results in memory (default).
    """
    memory_no_forecast_mean = OptionWrapper(
        "conserve_memory", MEMORY_NO_FORECAST_MEAN)
    """
    (bool) Flag to prevent storing forecasts and forecast errors.
    """
    memory_no_forecast_cov = OptionWrapper(
        "conserve_memory", MEMORY_NO_FORECAST_COV)
    """
    (bool) Flag to prevent storing forecast error covariance matrices.
    """
    @property
    def memory_no_forecast(self):
        """
        (bool) Flag to prevent storing all forecast-related output.
        """
        return self.memory_no_forecast_mean or self.memory_no_forecast_cov

    @memory_no_forecast.setter
    def memory_no_forecast(self, value):
        if bool(value):
            self.memory_no_forecast_mean = True
            self.memory_no_forecast_cov = True
        else:
            self.memory_no_forecast_mean = False
            self.memory_no_forecast_cov = False

    memory_no_predicted_mean = OptionWrapper(
        "conserve_memory", MEMORY_NO_PREDICTED_MEAN)
    """
    (bool) Flag to prevent storing predicted states.
    """
    memory_no_predicted_cov = OptionWrapper(
        "conserve_memory", MEMORY_NO_PREDICTED_COV)
    """
    (bool) Flag to prevent storing predicted state covariance matrices.
    """
    @property
    def memory_no_predicted(self):
        """
        (bool) Flag to prevent storing predicted state and covariance matrices.
        """
        return self.memory_no_predicted_mean or self.memory_no_predicted_cov

    @memory_no_predicted.setter
    def memory_no_predicted(self, value):
        if bool(value):
            self.memory_no_predicted_mean = True
            self.memory_no_predicted_cov = True
        else:
            self.memory_no_predicted_mean = False
            self.memory_no_predicted_cov = False

    memory_no_filtered_mean = OptionWrapper(
        "conserve_memory", MEMORY_NO_FILTERED_MEAN)
    """
    (bool) Flag to prevent storing filtered states.
    """
    memory_no_filtered_cov = OptionWrapper(
        "conserve_memory", MEMORY_NO_FILTERED_COV)
    """
    (bool) Flag to prevent storing filtered state covariance matrices.
    """
    @property
    def memory_no_filtered(self):
        """
        (bool) Flag to prevent storing filtered state and covariance matrices.
        """
        return self.memory_no_filtered_mean or self.memory_no_filtered_cov

    @memory_no_filtered.setter
    def memory_no_filtered(self, value):
        if bool(value):
            self.memory_no_filtered_mean = True
            self.memory_no_filtered_cov = True
        else:
            self.memory_no_filtered_mean = False
            self.memory_no_filtered_cov = False

    memory_no_likelihood = (
        OptionWrapper("conserve_memory", MEMORY_NO_LIKELIHOOD)
    )
    """
    (bool) Flag to prevent storing likelihood values for each observation.
    """
    memory_no_gain = OptionWrapper("conserve_memory", MEMORY_NO_GAIN)
    """
    (bool) Flag to prevent storing the Kalman gain matrices.
    """
    memory_no_smoothing = OptionWrapper("conserve_memory", MEMORY_NO_SMOOTHING)
    """
    (bool) Flag to prevent storing likelihood values for each observation.
    """
    memory_no_std_forecast = (
        OptionWrapper("conserve_memory", MEMORY_NO_STD_FORECAST))
    """
    (bool) Flag to prevent storing standardized forecast errors.
    """
    memory_conserve = OptionWrapper("conserve_memory", MEMORY_CONSERVE)
    """
    (bool) Flag to conserve the maximum amount of memory.
    """

    timing_options = [
        "timing_init_predicted", "timing_init_filtered"
    ]
    timing_init_predicted = OptionWrapper("filter_timing",
                                          TIMING_INIT_PREDICTED)
    """
    (bool) Flag for the default timing convention (Durbin and Koopman, 2012).
    """
    timing_init_filtered = OptionWrapper("filter_timing", TIMING_INIT_FILTERED)
    """
    (bool) Flag for the alternate timing convention (Kim and Nelson, 2012).
    """

    # Default filter options
    filter_method = FILTER_CONVENTIONAL
    """
    (int) Filtering method bitmask.
    """
    inversion_method = INVERT_UNIVARIATE | SOLVE_CHOLESKY
    """
    (int) Inversion method bitmask.
    """
    stability_method = STABILITY_FORCE_SYMMETRY
    """
    (int) Stability method bitmask.
    """
    conserve_memory = MEMORY_STORE_ALL
    """
    (int) Memory conservation bitmask.
    """
    filter_timing = TIMING_INIT_PREDICTED
    """
    (int) Filter timing.
    """

    def __init__(self, k_endog, k_states, k_posdef=None,
                 loglikelihood_burn=0, tolerance=1e-19, results_class=None,
                 kalman_filter_classes=None, **kwargs):
        # Extract keyword arguments to-be-used later
        keys = ["filter_method"] + KalmanFilter.filter_methods
        filter_method_kwargs = {key: kwargs.pop(key) for key in keys
                                if key in kwargs}
        keys = ["inversion_method"] + KalmanFilter.inversion_methods
        inversion_method_kwargs = {key: kwargs.pop(key) for key in keys
                                   if key in kwargs}
        keys = ["stability_method"] + KalmanFilter.stability_methods
        stability_method_kwargs = {key: kwargs.pop(key) for key in keys
                                   if key in kwargs}
        keys = ["conserve_memory"] + KalmanFilter.memory_options
        conserve_memory_kwargs = {key: kwargs.pop(key) for key in keys
                                  if key in kwargs}
        keys = ["alternate_timing"] + KalmanFilter.timing_options
        filter_timing_kwargs = {key: kwargs.pop(key) for key in keys
                                if key in kwargs}

        # Initialize the base class
        super().__init__(
            k_endog, k_states, k_posdef, **kwargs
        )

        # Setup the underlying Kalman filter storage
        self._kalman_filters = {}

        # Filter options
        self.loglikelihood_burn = loglikelihood_burn
        self.results_class = (
            results_class if results_class is not None else FilterResults
        )
        # Options
        self.prefix_kalman_filter_map = (
            kalman_filter_classes
            if kalman_filter_classes is not None
            else tools.prefix_kalman_filter_map.copy())

        self.set_filter_method(**filter_method_kwargs)
        self.set_inversion_method(**inversion_method_kwargs)
        self.set_stability_method(**stability_method_kwargs)
        self.set_conserve_memory(**conserve_memory_kwargs)
        self.set_filter_timing(**filter_timing_kwargs)

        self.tolerance = tolerance

        # Internal flags
        # The _scale internal flag is used because we may want to
        # use a fixed scale, in which case we want the flag to the Cython
        # Kalman filter to indicate that the scale should not be concentrated
        # out, so that self.filter_concentrated = False, but we still want to
        # alert the results object that we are viewing the model as one in
        # which the scale had been concentrated out for e.g. degree of freedom
        # computations.
        # This value should always be None, except within the fixed_scale
        # context, and should not be modified by users or anywhere else.
        self._scale = None

    def _clone_kwargs(self, endog, **kwargs):
        # See Representation._clone_kwargs for docstring
        kwargs = super()._clone_kwargs(endog, **kwargs)

        # Get defaults for options
        kwargs.setdefault("filter_method", self.filter_method)
        kwargs.setdefault("inversion_method", self.inversion_method)
        kwargs.setdefault("stability_method", self.stability_method)
        kwargs.setdefault("conserve_memory", self.conserve_memory)
        kwargs.setdefault("alternate_timing", bool(self.filter_timing))
        kwargs.setdefault("tolerance", self.tolerance)
        kwargs.setdefault("loglikelihood_burn", self.loglikelihood_burn)

        return kwargs

    @property
    def _kalman_filter(self):
        prefix = self.prefix
        if prefix in self._kalman_filters:
            return self._kalman_filters[prefix]
        return None

    def _initialize_filter(self, filter_method=None, inversion_method=None,
                           stability_method=None, conserve_memory=None,
                           tolerance=None, filter_timing=None,
                           loglikelihood_burn=None):
        if filter_method is None:
            filter_method = self.filter_method
        if inversion_method is None:
            inversion_method = self.inversion_method
        if stability_method is None:
            stability_method = self.stability_method
        if conserve_memory is None:
            conserve_memory = self.conserve_memory
        if loglikelihood_burn is None:
            loglikelihood_burn = self.loglikelihood_burn
        if filter_timing is None:
            filter_timing = self.filter_timing
        if tolerance is None:
            tolerance = self.tolerance

        # Make sure we have endog
        if self.endog is None:
            raise RuntimeError("Must bind a dataset to the model before"
                               " filtering or smoothing.")

        # Initialize the representation matrices
        prefix, dtype, create_statespace = self._initialize_representation()

        # Determine if we need to (re-)create the filter
        # (definitely need to recreate if we recreated the _statespace object)
        create_filter = create_statespace or prefix not in self._kalman_filters
        if not create_filter:
            kalman_filter = self._kalman_filters[prefix]

            create_filter = (
                not kalman_filter.conserve_memory == conserve_memory or
                not kalman_filter.loglikelihood_burn == loglikelihood_burn
            )

        # If the dtype-specific _kalman_filter does not exist (or if we need
        # to re-create it), create it
        if create_filter:
            if prefix in self._kalman_filters:
                # Delete the old filter
                del self._kalman_filters[prefix]
            # Setup the filter
            cls = self.prefix_kalman_filter_map[prefix]
            self._kalman_filters[prefix] = cls(
                self._statespaces[prefix], filter_method, inversion_method,
                stability_method, conserve_memory, filter_timing, tolerance,
                loglikelihood_burn
            )
        # Otherwise, update the filter parameters
        else:
            kalman_filter = self._kalman_filters[prefix]
            kalman_filter.set_filter_method(filter_method, False)
            kalman_filter.inversion_method = inversion_method
            kalman_filter.stability_method = stability_method
            kalman_filter.filter_timing = filter_timing
            kalman_filter.tolerance = tolerance
            # conserve_memory and loglikelihood_burn changes always lead to
            # re-created filters

        return prefix, dtype, create_filter, create_statespace

    def set_filter_method(self, filter_method=None, **kwargs):
        r"""
        Set the filtering method

        The filtering method controls aspects of which Kalman filtering
        approach will be used.

        Parameters
        ----------
        filter_method : int, optional
            Bitmask value to set the filter method to. See notes for details.
        **kwargs
            Keyword arguments may be used to influence the filter method by
            setting individual boolean flags. See notes for details.

        Notes
        -----
        The filtering method is defined by a collection of boolean flags, and
        is internally stored as a bitmask. The methods available are:

        FILTER_CONVENTIONAL
            Conventional Kalman filter.
        FILTER_UNIVARIATE
            Univariate approach to Kalman filtering. Overrides conventional
            method if both are specified.
        FILTER_COLLAPSED
            Collapsed approach to Kalman filtering. Will be used *in addition*
            to conventional or univariate filtering.
        FILTER_CONCENTRATED
            Use the concentrated log-likelihood function. Will be used
            *in addition* to the other options.

        Note that only the first method is available if using a Scipy version
        older than 0.16.

        If the bitmask is set directly via the `filter_method` argument, then
        the full method must be provided.

        If keyword arguments are used to set individual boolean flags, then
        the lowercase of the method must be used as an argument name, and the
        value is the desired value of the boolean flag (True or False).

        Note that the filter method may also be specified by directly modifying
        the class attributes which are defined similarly to the keyword
        arguments.

        The default filtering method is FILTER_CONVENTIONAL.

        Examples
        --------
        >>> mod = sm.tsa.statespace.SARIMAX(range(10))
        >>> mod.ssm.filter_method
        1
        >>> mod.ssm.filter_conventional
        True
        >>> mod.ssm.filter_univariate = True
        >>> mod.ssm.filter_method
        17
        >>> mod.ssm.set_filter_method(filter_univariate=False,
        ...                           filter_collapsed=True)
        >>> mod.ssm.filter_method
        33
        >>> mod.ssm.set_filter_method(filter_method=1)
        >>> mod.ssm.filter_conventional
        True
        >>> mod.ssm.filter_univariate
        False
        >>> mod.ssm.filter_collapsed
        False
        >>> mod.ssm.filter_univariate = True
        >>> mod.ssm.filter_method
        17
        """
        if filter_method is not None:
            self.filter_method = filter_method
        for name in KalmanFilter.filter_methods:
            if name in kwargs:
                setattr(self, name, kwargs[name])

    def set_inversion_method(self, inversion_method=None, **kwargs):
        r"""
        Set the inversion method

        The Kalman filter may contain one matrix inversion: that of the
        forecast error covariance matrix. The inversion method controls how and
        if that inverse is performed.

        Parameters
        ----------
        inversion_method : int, optional
            Bitmask value to set the inversion method to. See notes for
            details.
        **kwargs
            Keyword arguments may be used to influence the inversion method by
            setting individual boolean flags. See notes for details.

        Notes
        -----
        The inversion method is defined by a collection of boolean flags, and
        is internally stored as a bitmask. The methods available are:

        INVERT_UNIVARIATE
            If the endogenous time series is univariate, then inversion can be
            performed by simple division. If this flag is set and the time
            series is univariate, then division will always be used even if
            other flags are also set.
        SOLVE_LU
            Use an LU decomposition along with a linear solver (rather than
            ever actually inverting the matrix).
        INVERT_LU
            Use an LU decomposition along with typical matrix inversion.
        SOLVE_CHOLESKY
            Use a Cholesky decomposition along with a linear solver.
        INVERT_CHOLESKY
            Use an Cholesky decomposition along with typical matrix inversion.

        If the bitmask is set directly via the `inversion_method` argument,
        then the full method must be provided.

        If keyword arguments are used to set individual boolean flags, then
        the lowercase of the method must be used as an argument name, and the
        value is the desired value of the boolean flag (True or False).

        Note that the inversion method may also be specified by directly
        modifying the class attributes which are defined similarly to the
        keyword arguments.

        The default inversion method is `INVERT_UNIVARIATE | SOLVE_CHOLESKY`

        Several things to keep in mind are:

        - If the filtering method is specified to be univariate, then simple
          division is always used regardless of the dimension of the endogenous
          time series.
        - Cholesky decomposition is about twice as fast as LU decomposition,
          but it requires that the matrix be positive definite. While this
          should generally be true, it may not be in every case.
        - Using a linear solver rather than true matrix inversion is generally
          faster and is numerically more stable.

        Examples
        --------
        >>> mod = sm.tsa.statespace.SARIMAX(range(10))
        >>> mod.ssm.inversion_method
        1
        >>> mod.ssm.solve_cholesky
        True
        >>> mod.ssm.invert_univariate
        True
        >>> mod.ssm.invert_lu
        False
        >>> mod.ssm.invert_univariate = False
        >>> mod.ssm.inversion_method
        8
        >>> mod.ssm.set_inversion_method(solve_cholesky=False,
        ...                              invert_cholesky=True)
        >>> mod.ssm.inversion_method
        16
        """
        if inversion_method is not None:
            self.inversion_method = inversion_method
        for name in KalmanFilter.inversion_methods:
            if name in kwargs:
                setattr(self, name, kwargs[name])

    def set_stability_method(self, stability_method=None, **kwargs):
        r"""
        Set the numerical stability method

        The Kalman filter is a recursive algorithm that may in some cases
        suffer issues with numerical stability. The stability method controls
        what, if any, measures are taken to promote stability.

        Parameters
        ----------
        stability_method : int, optional
            Bitmask value to set the stability method to. See notes for
            details.
        **kwargs
            Keyword arguments may be used to influence the stability method by
            setting individual boolean flags. See notes for details.

        Notes
        -----
        The stability method is defined by a collection of boolean flags, and
        is internally stored as a bitmask. The methods available are:

        STABILITY_FORCE_SYMMETRY = 0x01
            If this flag is set, symmetry of the predicted state covariance
            matrix is enforced at each iteration of the filter, where each
            element is set to the average of the corresponding elements in the
            upper and lower triangle.

        If the bitmask is set directly via the `stability_method` argument,
        then the full method must be provided.

        If keyword arguments are used to set individual boolean flags, then
        the lowercase of the method must be used as an argument name, and the
        value is the desired value of the boolean flag (True or False).

        Note that the stability method may also be specified by directly
        modifying the class attributes which are defined similarly to the
        keyword arguments.

        The default stability method is `STABILITY_FORCE_SYMMETRY`

        Examples
        --------
        >>> mod = sm.tsa.statespace.SARIMAX(range(10))
        >>> mod.ssm.stability_method
        1
        >>> mod.ssm.stability_force_symmetry
        True
        >>> mod.ssm.stability_force_symmetry = False
        >>> mod.ssm.stability_method
        0
        """
        if stability_method is not None:
            self.stability_method = stability_method
        for name in KalmanFilter.stability_methods:
            if name in kwargs:
                setattr(self, name, kwargs[name])

    def set_conserve_memory(self, conserve_memory=None, **kwargs):
        r"""
        Set the memory conservation method

        By default, the Kalman filter computes a number of intermediate
        matrices at each iteration. The memory conservation options control
        which of those matrices are stored.

        Parameters
        ----------
        conserve_memory : int, optional
            Bitmask value to set the memory conservation method to. See notes
            for details.
        **kwargs
            Keyword arguments may be used to influence the memory conservation
            method by setting individual boolean flags. See notes for details.

        Notes
        -----
        The memory conservation method is defined by a collection of boolean
        flags, and is internally stored as a bitmask. The methods available
        are:

        MEMORY_STORE_ALL
            Store all intermediate matrices. This is the default value.
        MEMORY_NO_FORECAST_MEAN
            Do not store the forecast or forecast errors. If this option is
            used, the `predict` method from the results class is unavailable.
        MEMORY_NO_FORECAST_COV
            Do not store the forecast error covariance matrices.
        MEMORY_NO_FORECAST
            Do not store the forecast, forecast error, or forecast error
            covariance matrices. If this option is used, the `predict` method
            from the results class is unavailable.
        MEMORY_NO_PREDICTED_MEAN
            Do not store the predicted state.
        MEMORY_NO_PREDICTED_COV
            Do not store the predicted state covariance
            matrices.
        MEMORY_NO_PREDICTED
            Do not store the predicted state or predicted state covariance
            matrices.
        MEMORY_NO_FILTERED_MEAN
            Do not store the filtered state.
        MEMORY_NO_FILTERED_COV
            Do not store the filtered state covariance
            matrices.
        MEMORY_NO_FILTERED
            Do not store the filtered state or filtered state covariance
            matrices.
        MEMORY_NO_LIKELIHOOD
            Do not store the vector of loglikelihood values for each
            observation. Only the sum of the loglikelihood values is stored.
        MEMORY_NO_GAIN
            Do not store the Kalman gain matrices.
        MEMORY_NO_SMOOTHING
            Do not store temporary variables related to Kalman smoothing. If
            this option is used, smoothing is unavailable.
        MEMORY_NO_STD_FORECAST
            Do not store standardized forecast errors.
        MEMORY_CONSERVE
            Do not store any intermediate matrices.

        If the bitmask is set directly via the `conserve_memory` argument,
        then the full method must be provided.

        If keyword arguments are used to set individual boolean flags, then
        the lowercase of the method must be used as an argument name, and the
        value is the desired value of the boolean flag (True or False).

        Note that the memory conservation method may also be specified by
        directly modifying the class attributes which are defined similarly to
        the keyword arguments.

        The default memory conservation method is `MEMORY_STORE_ALL`, so that
        all intermediate matrices are stored.

        Examples
        --------
        >>> mod = sm.tsa.statespace.SARIMAX(range(10))
        >>> mod.ssm..conserve_memory
        0
        >>> mod.ssm.memory_no_predicted
        False
        >>> mod.ssm.memory_no_predicted = True
        >>> mod.ssm.conserve_memory
        2
        >>> mod.ssm.set_conserve_memory(memory_no_filtered=True,
        ...                             memory_no_forecast=True)
        >>> mod.ssm.conserve_memory
        7
        """
        if conserve_memory is not None:
