"""Generalized Method of Moments, GMM, and Two-Stage Least Squares for
instrumental variables IV2SLS



Issues
------
* number of parameters, nparams, and starting values for parameters
  Where to put them? start was initially taken from global scope (bug)
* When optimal weighting matrix cannot be calculated numerically
  In DistQuantilesGMM, we only have one row of moment conditions, not a
  moment condition for each observation, calculation for cov of moments
  breaks down. iter=1 works (weights is identity matrix)
  -> need method to do one iteration with an identity matrix or an
     analytical weighting matrix given as parameter.
  -> add result statistics for this case, e.g. cov_params, I have it in the
     standalone function (and in calc_covparams which is a copy of it),
     but not tested yet.
  DONE `fitonce` in DistQuantilesGMM, params are the same as in direct call to fitgmm
      move it to GMM class (once it's clearer for which cases I need this.)
* GMM does not know anything about the underlying model, e.g. y = X beta + u or panel
  data model. It would be good if we can reuse methods from regressions, e.g.
  predict, fitted values, calculating the error term, and some result statistics.
  What's the best way to do this, multiple inheritance, outsourcing the functions,
  mixins or delegation (a model creates a GMM instance just for estimation).


Unclear
-------
* dof in Hausman
  - based on rank
  - differs between IV2SLS method and function used with GMM or (IV2SLS)
  - with GMM, covariance matrix difference has negative eigenvalues in iv example, ???
* jtest/jval
  - I'm not sure about the normalization (multiply or divide by nobs) in jtest.
    need a test case. Scaling of jval is irrelevant for estimation.
    jval in jtest looks to large in example, but I have no idea about the size
* bse for fitonce look too large (no time for checking now)
    formula for calc_cov_params for the case without optimal weighting matrix
    is wrong. I do not have an estimate for omega in that case. And I'm confusing
    between weights and omega, which are *not* the same in this case.



Author: josef-pktd
License: BSD (3-clause)

"""

from statsmodels.compat.python import lrange

import numpy as np
from scipy import optimize, stats

from statsmodels.base.model import (
    LikelihoodModel,
    LikelihoodModelResults,
    Model,
)
from statsmodels.regression.linear_model import (
    OLS,
    RegressionResults,
    RegressionResultsWrapper,
)
import statsmodels.stats.sandwich_covariance as smcov
from statsmodels.tools.decorators import cache_readonly
from statsmodels.tools.numdiff import approx_fprime
from statsmodels.tools.tools import _ensure_2d

DEBUG = 0


def maxabs(x):
    """just a shortcut to np.abs(x).max()"""
    return np.abs(x).max()


class IV2SLS(LikelihoodModel):
    """
    Instrumental variables estimation using Two-Stage Least-Squares (2SLS)


    Parameters
    ----------
    endog : ndarray
       Endogenous variable, 1-dimensional or 2-dimensional array nobs by 1
    exog : ndarray
       Explanatory variables, 1-dimensional or 2-dimensional array nobs by k
    instrument : ndarray
       Instruments for explanatory variables. Must contain both exog
       variables that are not being instrumented and instruments

    Notes
    -----
    All variables in exog are instrumented in the calculations. If variables
    in exog are not supposed to be instrumented, then these variables
    must also to be included in the instrument array.

    Degrees of freedom in the calculation of the standard errors uses
    `df_resid = (nobs - k_vars)`.
    (This corresponds to the `small` option in Stata's ivreg2.)
    """

    def __init__(self, endog, exog, instrument=None):
        self.instrument, self.instrument_names = _ensure_2d(instrument, True)
        super().__init__(endog, exog)
        # where is this supposed to be handled
        # Note: Greene p.77/78 dof correction is not necessary (because only
        #       asy results), but most packages do it anyway
        self.df_resid = self.exog.shape[0] - self.exog.shape[1]
        # self.df_model = float(self.rank - self.k_constant)
        self.df_model = float(self.exog.shape[1] - self.k_constant)

    def initialize(self):
        self.wendog = self.endog
        self.wexog = self.exog

    def whiten(self, X):
        """Not implemented"""

    def fit(self):
        """estimate model using 2SLS IV regression

        Returns
        -------
        results : instance of RegressionResults
           regression result

        Notes
        -----
        This returns a generic RegressioResults instance as defined for the
        linear models.

        Parameter estimates and covariance are correct, but other results
        have not been tested yet, to see whether they apply without changes.

        """
        # Greene 5th edt., p.78 section 5.4
        # move this maybe
        y, x, z = self.endog, self.exog, self.instrument
        # TODO: this uses "textbook" calculation, improve linalg
        ztz = np.dot(z.T, z)
        ztx = np.dot(z.T, x)
        self.xhatparams = xhatparams = np.linalg.solve(ztz, ztx)
        # print 'x.T.shape, xhatparams.shape', x.shape, xhatparams.shape
        F = xhat = np.dot(z, xhatparams)
        FtF = np.dot(F.T, F)
        self.xhatprod = FtF  # store for Housman specification test
        Ftx = np.dot(F.T, x)
        Fty = np.dot(F.T, y)
        params = np.linalg.solve(FtF, Fty)
        Ftxinv = np.linalg.inv(Ftx)
        self.normalized_cov_params = np.dot(Ftxinv.T, np.dot(FtF, Ftxinv))

        lfit = IVRegressionResults(
            self, params, normalized_cov_params=self.normalized_cov_params
        )

        lfit.exog_hat_params = xhatparams
        lfit.exog_hat = xhat  # TODO: do we want to store this, might be large
        self._results_ols2nd = OLS(y, xhat).fit()

        return RegressionResultsWrapper(lfit)

    # copied from GLS, because I subclass currently LikelihoodModel and not GLS
    def predict(self, params, exog=None):
        """
        Return linear predicted values from a design matrix.

        Parameters
        ----------
        exog : array_like
            Design / exogenous data
        params : array_like, optional after fit has been called
            Parameters of a linear model

        Returns
        -------
        An array of fitted values

        Notes
        -----
        If the model as not yet been fit, params is not optional.
        """
        if exog is None:
            exog = self.exog

        return np.dot(exog, params)


class IVRegressionResults(RegressionResults):
    """
    Results class for for an OLS model.

    Most of the methods and attributes are inherited from RegressionResults.
    The special methods that are only available for OLS are:

    - get_influence
    - outlier_test
    - el_test
    - conf_int_el

    See Also
    --------
    RegressionResults
    """

    @cache_readonly
    def fvalue(self):
        const_idx = self.model.data.const_idx
        # if constant is implicit or missing, return nan see #2444, #3544
        if const_idx is None:
            return np.nan
        else:
            k_vars = len(self.params)
            restriction = np.eye(k_vars)
            idx_noconstant = lrange(k_vars)
            del idx_noconstant[const_idx]
            fval = self.f_test(restriction[idx_noconstant]).fvalue  # without constant
            return fval

    def spec_hausman(self, dof=None):
        """Hausman's specification test

        See Also
        --------
        spec_hausman : generic function for Hausman's specification test

        """
        # use normalized cov_params for OLS

        endog, exog = self.model.endog, self.model.exog
        resols = OLS(endog, exog).fit()
        normalized_cov_params_ols = resols.model.normalized_cov_params
        # Stata `ivendog` does not use df correction for se
        # se2 = resols.mse_resid #* resols.df_resid * 1. / len(endog)
        se2 = resols.ssr / len(endog)

        params_diff = self.params - resols.params

        cov_diff = np.linalg.pinv(self.model.xhatprod) - normalized_cov_params_ols
        # TODO: the following is very inefficient, solves problem (svd) twice
        # use linalg.lstsq or svd directly
        # cov_diff will very often be in-definite (singular)
        if not dof:
            dof = np.linalg.matrix_rank(cov_diff)
        cov_diffpinv = np.linalg.pinv(cov_diff)
        H = np.dot(params_diff, np.dot(cov_diffpinv, params_diff)) / se2
        pval = stats.chi2.sf(H, dof)

        return H, pval, dof

    # copied from regression results with small changes, no llf
    def summary(self, yname=None, xname=None, title=None, alpha=0.05):
        """Summarize the Regression Results

        Parameters
        ----------
        yname : str, optional
            Default is `y`
        xname : list[str], optional
            Default is `var_##` for ## in p the number of regressors
        title : str, optional
            Title for the top table. If not None, then this replaces the
            default title
        alpha : float
            significance level for the confidence intervals

        Returns
        -------
        smry : Summary instance
            this holds the summary tables and text, which can be printed or
            converted to various output formats.

        See Also
        --------
        statsmodels.iolib.summary.Summary : class to hold summary
            results
        """

        # TODO: import where we need it (for now), add as cached attributes
        from statsmodels.stats.stattools import (
            durbin_watson,
            jarque_bera,
            omni_normtest,
        )

        jb, jbpv, skew, kurtosis = jarque_bera(self.wresid)
        omni, omnipv = omni_normtest(self.wresid)

        # TODO: reuse condno from somewhere else ?
        # condno = np.linalg.cond(np.dot(self.wexog.T, self.wexog))
        wexog = self.model.wexog
        eigvals = np.linalg.eigvalsh(np.dot(wexog.T, wexog))
        eigvals = np.sort(eigvals)  # in increasing order
        condno = np.sqrt(eigvals[-1] / eigvals[0])

        # TODO: check what is valid.
        # box-pierce, breusch-pagan, durbin's h are not with endogenous on rhs
        # use Cumby Huizinga 1992 instead
        self.diagn = dict(
            jb=jb,
            jbpv=jbpv,
            skew=skew,
            kurtosis=kurtosis,
            omni=omni,
            omnipv=omnipv,
            condno=condno,
            mineigval=eigvals[0],
        )

        # TODO not used yet
        # diagn_left_header = ['Models stats']
        # diagn_right_header = ['Residual stats']

        # TODO: requiring list/iterable is a bit annoying
        # need more control over formatting
        # TODO: default do not work if it's not identically spelled

        top_left = [
            ("Dep. Variable:", None),
            ("Model:", None),
            ("Method:", ["Two Stage"]),
            ("", ["Least Squares"]),
            ("Date:", None),
            ("Time:", None),
            ("No. Observations:", None),
            ("Df Residuals:", None),  # [self.df_resid]), # TODO: spelling
            ("Df Model:", None),  # [self.df_model])
        ]

        top_right = [
            ("R-squared:", ["%#8.3f" % self.rsquared]),
            ("Adj. R-squared:", ["%#8.3f" % self.rsquared_adj]),
            ("F-statistic:", ["%#8.4g" % self.fvalue]),
            ("Prob (F-statistic):", ["%#6.3g" % self.f_pvalue]),
            # ('Log-Likelihood:', None), #["%#6.4g" % self.llf]),
            # ('AIC:', ["%#8.4g" % self.aic]),
            # ('BIC:', ["%#8.4g" % self.bic])
        ]

        diagn_left = [
            ("Omnibus:", ["%#6.3f" % omni]),
            ("Prob(Omnibus):", ["%#6.3f" % omnipv]),
            ("Skew:", ["%#6.3f" % skew]),
            ("Kurtosis:", ["%#6.3f" % kurtosis]),
        ]

        diagn_right = [
            ("Durbin-Watson:", ["%#8.3f" % durbin_watson(self.wresid)]),
            ("Jarque-Bera (JB):", ["%#8.3f" % jb]),
            ("Prob(JB):", ["%#8.3g" % jbpv]),
            ("Cond. No.", ["%#8.3g" % condno]),
        ]

        if title is None:
            title = self.model.__class__.__name__ + " " + "Regression Results"

        # create summary table instance
        from statsmodels.iolib.summary import Summary

        smry = Summary()
        smry.add_table_2cols(
            self,
            gleft=top_left,
            gright=top_right,
            yname=yname,
            xname=xname,
            title=title,
        )
        smry.add_table_params(self, yname=yname, xname=xname, alpha=alpha, use_t=True)

        smry.add_table_2cols(
            self,
            gleft=diagn_left,
            gright=diagn_right,
            yname=yname,
            xname=xname,
            title="",
        )

        return smry


# classes for Generalized Method of Moments GMM

_gmm_options = """\

Options for GMM
---------------

Type of GMM
~~~~~~~~~~~

 - one-step
 - iterated
 - CUE : not tested yet

weight matrix
~~~~~~~~~~~~~

 - `weights_method` : str, defines method for robust
   Options here are similar to :mod:`statsmodels.stats.robust_covariance`
   default is heteroscedasticity consistent, HC0

   currently available methods are

   - `cov` : HC0, optionally with degrees of freedom correction
   - `hac` :
   - `iid` : untested, only for Z*u case, IV cases with u as error indep of Z
   - `ac` : not available yet
   - `cluster` : not connected yet
   - others from robust_covariance

other arguments:

 - `wargs` : tuple or dict, required arguments for weights_method

   - `centered` : bool,
     indicates whether moments are centered for the calculation of the weights
     and covariance matrix, applies to all weight_methods
   - `ddof` : int
     degrees of freedom correction, applies currently only to `cov`
   - maxlag : int
     number of lags to include in HAC calculation , applies only to `hac`
   - others not yet, e.g. groups for cluster robust

covariance matrix
~~~~~~~~~~~~~~~~~

The same options as for weight matrix also apply to the calculation of the
estimate of the covariance matrix of the parameter estimates.
The additional option is

 - `has_optimal_weights`: If true, then the calculation of the covariance
   matrix assumes that we have optimal GMM with :math:`W = S^{-1}`.
   Default is True.
   TODO: do we want to have a different default after `onestep`?


"""


class GMM(Model):
    """
    Class for estimation by Generalized Method of Moments

    needs to be subclassed, where the subclass defined the moment conditions
    `momcond`

    Parameters
    ----------
    endog : ndarray
        endogenous variable, see notes
    exog : ndarray
        array of exogenous variables, see notes
    instrument : ndarray
        array of instruments, see notes
    nmoms : None or int
        number of moment conditions, if None then it is set equal to the
        number of columns of instruments. Mainly needed to determine the shape
        or size of start parameters and starting weighting matrix.
    kwds : anything
        this is mainly if additional variables need to be stored for the
        calculations of the moment conditions

    Attributes
    ----------
    results : instance of GMMResults
        currently just a storage class for params and cov_params without it's
        own methods
    bse : property
        return bse



    Notes
    -----
    The GMM class only uses the moment conditions and does not use any data
    directly. endog, exog, instrument and kwds in the creation of the class
    instance are only used to store them for access in the moment conditions.
    Which of this are required and how they are used depends on the moment
    conditions of the subclass.

    Warning:

    Options for various methods have not been fully implemented and
    are still missing in several methods.


    TODO:
    currently onestep (maxiter=0) still produces an updated estimate of bse
    and cov_params.

    """

    results_class = "GMMResults"

    def __init__(
        self,
        endog,
        exog,
        instrument,
        k_moms=None,
        k_params=None,
        missing="none",
        **kwds,
    ):
        """
        maybe drop and use mixin instead

        TODO: GMM does not really care about the data, just the moment conditions
        """
        instrument = self._check_inputs(instrument, endog)  # attaches if needed
        super().__init__(endog, exog, missing=missing, instrument=instrument)
        #         self.endog = endog
        #         self.exog = exog
        #         self.instrument = instrument
        self.nobs = endog.shape[0]
        if k_moms is not None:
            self.nmoms = k_moms
        elif instrument is not None:
            self.nmoms = instrument.shape[1]
        else:
            self.nmoms = np.nan

        if k_params is not None:
            self.k_params = k_params
        elif instrument is not None:
            self.k_params = exog.shape[1]
        else:
            self.k_params = np.nan

        self.__dict__.update(kwds)
        self.epsilon_iter = 1e-6

    def _check_inputs(self, instrument, endog):
        if instrument is not None:
            offset = np.asarray(instrument)
            if offset.shape[0] != endog.shape[0]:
                raise ValueError("instrument is not the same length as endog")
        return instrument

    def _fix_param_names(self, params, param_names=None):
        # TODO: this is a temporary fix, need
        xnames = self.data.xnames

        if param_names is not None:
            if len(params) == len(param_names):
                self.data.xnames = param_names
            else:
                raise ValueError("param_names has the wrong length")

        elif len(params) < len(xnames):
            # cut in front for poisson multiplicative
            self.data.xnames = xnames[-len(params) :]
        elif len(params) > len(xnames):
            # use generic names
            self.data.xnames = ["p%2d" % i for i in range(len(params))]

    def set_param_names(self, param_names, k_params=None):
        """set the parameter names in the model

        Parameters
        ----------
        param_names : list[str]
            param_names should have the same length as the number of params
        k_params : None or int
            If k_params is None, then the k_params attribute is used, unless
            it is None.
            If k_params is not None, then it will also set the k_params
            attribute.
        """
        if k_params is not None:
            self.k_params = k_params
        else:
            k_params = self.k_params

        if k_params == len(param_names):
            self.data.xnames = param_names
        else:
            raise ValueError("param_names has the wrong length")

    def fit(
        self,
        start_params=None,
        maxiter=10,
        inv_weights=None,
        weights_method="cov",
        wargs=(),
        has_optimal_weights=True,
        optim_method="bfgs",
        optim_args=None,
    ):
        """
        Estimate parameters using GMM and return GMMResults

        TODO: weight and covariance arguments still need to be made consistent
        with similar options in other models,
        see RegressionResult.get_robustcov_results

        Parameters
        ----------
        start_params : array (optional)
            starting value for parameters ub minimization. If None then
            fitstart method is called for the starting values.
        maxiter : int or 'cue'
            Number of iterations in iterated GMM. The onestep estimate can be
            obtained with maxiter=0 or 1. If maxiter is large, then the
            iteration will stop either at maxiter or on convergence of the
            parameters (TODO: no options for convergence criteria yet.)
            If `maxiter == 'cue'`, the the continuously updated GMM is
            calculated which updates the weight matrix during the minimization
            of the GMM objective function. The CUE estimation uses the onestep
            parameters as starting values.
        inv_weights : None or ndarray
            inverse of the starting weighting matrix. If inv_weights are not
            given then the method `start_weights` is used which depends on
            the subclass, for IV subclasses `inv_weights = z'z` where `z` are
            the instruments, otherwise an identity matrix is used.
        weights_method : str, defines method for robust
            Options here are similar to :mod:`statsmodels.stats.robust_covariance`
            default is heteroscedasticity consistent, HC0

            currently available methods are

            - `cov` : HC0, optionally with degrees of freedom correction
            - `hac` :
            - `iid` : untested, only for Z*u case, IV cases with u as error indep of Z
            - `ac` : not available yet
            - `cluster` : not connected yet
            - others from robust_covariance

        wargs` : tuple or dict,
            required and optional arguments for weights_method

            - `centered` : bool,
              indicates whether moments are centered for the calculation of the weights
              and covariance matrix, applies to all weight_methods
            - `ddof` : int
              degrees of freedom correction, applies currently only to `cov`
            - `maxlag` : int
              number of lags to include in HAC calculation , applies only to `hac`
            - others not yet, e.g. groups for cluster robust

        has_optimal_weights: If true, then the calculation of the covariance
              matrix assumes that we have optimal GMM with :math:`W = S^{-1}`.
              Default is True.
              TODO: do we want to have a different default after `onestep`?
        optim_method : str, default is 'bfgs'
            numerical optimization method. Currently not all optimizers that
            are available in LikelihoodModels are connected.
        optim_args : dict
            keyword arguments for the numerical optimizer.

        Returns
        -------
        results : instance of GMMResults
            this is also attached as attribute results

        Notes
        -----

        Warning: One-step estimation, `maxiter` either 0 or 1, still has
        problems (at least compared to Stata's gmm).
        By default it uses a heteroscedasticity robust covariance matrix, but
        uses the assumption that the weight matrix is optimal.
        See options for cov_params in the results instance.

        The same options as for weight matrix also apply to the calculation of
        the estimate of the covariance matrix of the parameter estimates.

        """
        # TODO: add check for correct wargs keys
        #       currently a misspelled key is not detected,
        #       because I'm still adding options

        # TODO: check repeated calls to fit with different options
        #       arguments are dictionaries, i.e. mutable
        #       unit test if anything  is stale or spilled over.

        # bug: where does start come from ???
        start = start_params  # alias for renaming
        if start is None:
            start = self.fitstart()  # TODO: temporary hack

        if optim_args is None:
            optim_args = {}
        if "disp" not in optim_args:
            optim_args["disp"] = 1

        if maxiter == 0 or maxiter == "cue":
            if inv_weights is not None:
                weights = np.linalg.pinv(inv_weights)
            else:
                # let start_weights handle the inv=False for maxiter=0
                weights = self.start_weights(inv=False)

            params = self.fitgmm(
                start, weights=weights, optim_method=optim_method, optim_args=optim_args
            )
        else:
            params, weights = self.fititer(
                start,
                maxiter=maxiter,
                start_invweights=inv_weights,
                weights_method=weights_method,
                wargs=wargs,
                optim_method=optim_method,
                optim_args=optim_args,
            )
            # TODO weights returned by fititer is inv_weights - not true anymore
            # weights_ currently not necessary and used anymore
            np.linalg.pinv(weights)

        if maxiter == "cue":
            # we have params from maxiter= 0 as starting value
            # TODO: need to give weights options to gmmobjective_cu
            params = self.fitgmm_cu(
                params, optim_method=optim_method, optim_args=optim_args
            )
            # weights is stored as attribute
            weights = self._weights_cu

        # TODO: use Bunch instead ?
        options_other = {
            "weights_method": weights_method,
            "has_optimal_weights": has_optimal_weights,
            "optim_method": optim_method,
        }

        # check that we have the right number of xnames
        self._fix_param_names(params, param_names=None)
        results = results_class_dict[self.results_class](
            model=self,
            params=params,
            weights=weights,
            wargs=wargs,
            options_other=options_other,
            optim_args=optim_args,
        )

        self.results = results  # FIXME: remove, still keeping it temporarily
        return results

    def fitgmm(self, start, weights=None, optim_method="bfgs", optim_args=None):
        """estimate parameters using GMM

        Parameters
        ----------
        start : array_like
            starting values for minimization
        weights : ndarray
            weighting matrix for moment conditions. If weights is None, then
            the identity matrix is used


        Returns
        -------
        paramest : ndarray
            estimated parameters

        Notes
        -----
        todo: add fixed parameter option, not here ???

        uses scipy.optimize.fmin

        """
        #        if not fixed is None:  # fixed not defined in this version
        #            raise NotImplementedError

        # TODO: should start_weights only be in `fit`
        if weights is None:
            weights = self.start_weights(inv=False)

        if optim_args is None:
            optim_args = {}

        if optim_method == "nm":
            optimizer = optimize.fmin
        elif optim_method == "bfgs":
            optimizer = optimize.fmin_bfgs
            # TODO: add score
            optim_args["fprime"] = (
                self.score
            )  # lambda params: self.score(params, weights)
        elif optim_method == "ncg":
            optimizer = optimize.fmin_ncg
            optim_args["fprime"] = self.score
        elif optim_method == "cg":
            optimizer = optimize.fmin_cg
            optim_args["fprime"] = self.score
        elif optim_method == "fmin_l_bfgs_b":
            optimizer = optimize.fmin_l_bfgs_b
            optim_args["fprime"] = self.score
        elif optim_method == "powell":
            optimizer = optimize.fmin_powell
        elif optim_method == "slsqp":
            optimizer = optimize.fmin_slsqp
        else:
            raise ValueError("optimizer method not available")

        if DEBUG:
            print(np.linalg.det(weights))

        # TODO: add other optimization options and results
        return optimizer(self.gmmobjective, start, args=(weights,), **optim_args)

    def fitgmm_cu(self, start, optim_method="bfgs", optim_args=None):
        """estimate parameters using continuously updating GMM

        Parameters
        ----------
        start : array_like
            starting values for minimization

        Returns
        -------
        paramest : ndarray
            estimated parameters

        Notes
        -----
        todo: add fixed parameter option, not here ???

        uses scipy.optimize.fmin

        """
        #        if not fixed is None:  # fixed not defined in this version
        #            raise NotImplementedError

        if optim_args is None:
            optim_args = {}

        if optim_method == "nm":
            optimizer = optimize.fmin
        elif optim_method == "bfgs":
            optimizer = optimize.fmin_bfgs
            optim_args["fprime"] = self.score_cu
        elif optim_method == "ncg":
            optimizer = optimize.fmin_ncg
        else:
            raise ValueError("optimizer method not available")

        # TODO: add other optimization options and results
        return optimizer(self.gmmobjective_cu, start, args=(), **optim_args)

    def start_weights(self, inv=True):
        """Create identity matrix for starting weights"""
        return np.eye(self.nmoms)

    def gmmobjective(self, params, weights):
        """
        objective function for GMM minimization

        Parameters
        ----------
        params : ndarray
            parameter values at which objective is evaluated
        weights : ndarray
            weighting matrix

        Returns
        -------
        jval : float
            value of objective function

        """
        moms = self.momcond_mean(params)
        return np.dot(np.dot(moms, weights), moms)
        # moms = self.momcond(params)
        # return np.dot(np.dot(moms.mean(0),weights), moms.mean(0))

    def gmmobjective_cu(self, params, weights_method="cov", wargs=()):
        """
        objective function for continuously updating  GMM minimization

        Parameters
        ----------
        params : ndarray
            parameter values at which objective is evaluated

        Returns
        -------
        jval : float
            value of objective function

        """
        moms = self.momcond(params)
        inv_weights = self.calc_weightmatrix(
            moms, weights_method=weights_method, wargs=wargs
        )
        weights = np.linalg.pinv(inv_weights)
        self._weights_cu = weights  # store if we need it later
        return np.dot(np.dot(moms.mean(0), weights), moms.mean(0))

    def fititer(
        self,
        start,
        maxiter=2,
        start_invweights=None,
        weights_method="cov",
        wargs=(),
        optim_method="bfgs",
        optim_args=None,
    ):
        """iterative estimation with updating of optimal weighting matrix

        stopping criteria are maxiter or change in parameter estimate less
        than self.epsilon_iter, with default 1e-6.

        Parameters
        ----------
        start : ndarray
            starting value for parameters
        maxiter : int
            maximum number of iterations
        start_weights : array (nmoms, nmoms)
            initial weighting matrix; if None, then the identity matrix
            is used
        weights_method : {'cov', ...}
            method to use to estimate the optimal weighting matrix,
            see calc_weightmatrix for details

        Returns
        -------
        params : ndarray
            estimated parameters
        weights : ndarray
            optimal weighting matrix calculated with final parameter
            estimates

        Notes
        -----




        """
        self.history = []
        momcond = self.momcond

        if start_invweights is None:
            w = self.start_weights(inv=True)
        else:
            w = start_invweights

        # call fitgmm function
        # args = (self.endog, self.exog, self.instrument)
        # args is not used in the method version
        winv_new = w
        for it in range(maxiter):
            winv = winv_new
            w = np.linalg.pinv(winv)
            # this is still calling function not method
            #            resgmm = fitgmm(momcond, (), start, weights=winv, fixed=None,
            #                            weightsoptimal=False)
            resgmm = self.fitgmm(
                start, weights=w, optim_method=optim_method, optim_args=optim_args
            )

            moms = momcond(resgmm)
            # the following is S = cov_moments
            winv_new = self.calc_weightmatrix(
                moms, weights_method=weights_method, wargs=wargs, params=resgmm
            )

            if it > 2 and maxabs(resgmm - start) < self.epsilon_iter:
                # check rule for early stopping
                # TODO: set has_optimal_weights = True
                break

            start = resgmm
        return resgmm, w

    def calc_weightmatrix(self, moms, weights_method="cov", wargs=(), params=None):
        """
        calculate omega or the weighting matrix

        Parameters
        ----------
