Repository: statsmodels/statsmodels

Issue:
BUG: statespace ExponentialSmoothing does not work with all parameters fixed
#### Describe the bug

Fixing both parameters in a Simple ES model raises an error

#### Code Sample, a copy-pastable example if possible


```python
import numpy as np
x = np.cumsum(np.random.standard_normal(1000))
from statsmodels.tsa.statespace.exponential_smoothing import ExponentialSmoothing
ses = ExponentialSmoothing(x, initial_level=x[0])
with ses.fix_params({'smoothing_level': 0.5}):
    res = ses.fit()
```
I see
```
c:\git\statsmodels\statsmodels\tsa\statespace\mlemodel.py in fit(self, start_params, transformed, includes_fixed, cov_type, cov_kwds, method, maxiter, full_output, disp, callback, return_params, optim_score, optim_complex_step, optim_hessian, flags, low_memory, **kwargs)
    685                 func = self.smooth
    686             res = func(mlefit.params, transformed=False, includes_fixed=False,
--> 687                        cov_type=cov_type, cov_kwds=cov_kwds)

c:\git\statsmodels\statsmodels\tsa\statespace\exponential_smoothing.py in smooth(self, params, cov_type, cov_kwds, return_ssm, results_class, results_wrapper_class, *args, **kwargs)
    596             params, cov_type=cov_type, cov_kwds=cov_kwds,
    597             return_ssm=return_ssm, results_class=results_class,
--> 598             results_wrapper_class=results_wrapper_class, *args, **kwargs)

c:\git\statsmodels\statsmodels\tsa\statespace\mlemodel.py in smooth(self, params, transformed, includes_fixed, complex_step, cov_type, cov_kwds, return_ssm, results_class, results_wrapper_class, **kwargs)
    847         return self._wrap_results(params, result, return_ssm, cov_type,
    848                                   cov_kwds, results_class,
--> 849                                   results_wrapper_class)

c:\git\statsmodels\statsmodels\tsa\statespace\mlemodel.py in _wrap_results(self, params, result, return_raw, cov_type, cov_kwds, results_class, wrapper_class)
    744                 wrapper_class = self._res_classes['fit'][1]
    745 
--> 746             res = results_class(self, params, result, **result_kwargs)

c:\git\statsmodels\statsmodels\tsa\statespace\exponential_smoothing.py in __init__(self, model, params, filter_results, cov_type, **kwargs)
    606     def __init__(self, model, params, filter_results, cov_type=None,
    607                  **kwargs):
--> 608         super().__init__(model, params, filter_results, cov_type, **kwargs)

c:\git\statsmodels\statsmodels\tsa\statespace\mlemodel.py in __init__(self, model, params, results, cov_type, cov_kwds, **kwargs)
   2259             self._rank = None
   2260             self._get_robustcov_results(cov_type=cov_type, use_self=True,
-> 2261                                         **cov_kwds)

c:\git\statsmodels\statsmodels\tsa\statespace\mlemodel.py in _get_robustcov_results(self, cov_type, **kwargs)
   2490                                                 approx_type=approx_type_str)
   2491         elif self.cov_type == 'opg':
-> 2492             res.cov_params_default = res.cov_params_opg

pandas\_libs\properties.pyx in pandas._libs.properties.CachedProperty.__get__()

c:\git\statsmodels\statsmodels\tsa\statespace\mlemodel.py in cov_params_opg(self)
   2612         """
   2613         return self._cov_params_opg(self._cov_approx_complex_step,
-> 2614                                     self._cov_approx_centered)

c:\git\statsmodels\statsmodels\tsa\statespace\mlemodel.py in _cov_params_opg(self, approx_complex_step, approx_centered)
   2594         if len(self.fixed_params) > 0:
   2595             mask = np.ix_(self._free_params_index, self._free_params_index)
-> 2596             (tmp, singular_values) = pinv_extended(evaluated_hessian[mask])

c:\git\statsmodels\statsmodels\tools\tools.py in pinv_extended(X, rcond)

--> 393     cutoff = rcond * np.maximum.reduce(s)

ValueError: zero-size array to reduction operation maximum which has no identity
```
If the issue has not been resolved, please file it in the issue tracker.

#### Expected Output

Model works in the sense that I can get forecasts from the results.

#### Output of ``import statsmodels.api as sm; sm.show_versions()``

<details>

INSTALLED VERSIONS
------------------
Python: 3.7.7.final.0

statsmodels
===========

Installed: v0.12.0.dev0+243.gf6d0bf623 (c:\git\statsmodels\statsmodels)
</details>



Discussion:
@ChadFulton I think it needs to skip covariance estimation when there are no parameters.

Resolving patch:
diff --git a/statsmodels/tools/tools.py b/statsmodels/tools/tools.py
--- a/statsmodels/tools/tools.py
+++ b/statsmodels/tools/tools.py
@@ -377,16 +377,16 @@ def isestimable(c, d):
     return True
 
 
-def pinv_extended(X, rcond=1e-15):
+def pinv_extended(x, rcond=1e-15):
     """
     Return the pinv of an array X as well as the singular values
     used in computation.
 
     Code adapted from numpy.
     """
-    X = np.asarray(X)
-    X = X.conjugate()
-    u, s, vt = np.linalg.svd(X, 0)
+    x = np.asarray(x)
+    x = x.conjugate()
+    u, s, vt = np.linalg.svd(x, False)
     s_orig = np.copy(s)
     m = u.shape[0]
     n = vt.shape[1]
diff --git a/statsmodels/tsa/statespace/mlemodel.py b/statsmodels/tsa/statespace/mlemodel.py
--- a/statsmodels/tsa/statespace/mlemodel.py
+++ b/statsmodels/tsa/statespace/mlemodel.py
@@ -2591,7 +2591,10 @@ def _cov_params_opg(self, approx_complex_step=True, approx_centered=False):
             approx_complex_step=approx_complex_step,
             approx_centered=approx_centered)
 
-        if len(self.fixed_params) > 0:
+        if self._free_params_index is not None and len(self._free_params_index) == 0:
+            neg_cov = np.zeros_like(evaluated_hessian) * np.nan
+            singular_values = np.empty(0)
+        elif len(self.fixed_params) > 0:
             mask = np.ix_(self._free_params_index, self._free_params_index)
             (tmp, singular_values) = pinv_extended(evaluated_hessian[mask])
             neg_cov = np.zeros_like(evaluated_hessian) * np.nan
@@ -2601,7 +2604,10 @@ def _cov_params_opg(self, approx_complex_step=True, approx_centered=False):
 
         self.model.update(self.params, transformed=True, includes_fixed=True)
         if self._rank is None:
-            self._rank = np.linalg.matrix_rank(np.diag(singular_values))
+            if self._free_params_index is not None and len(self._free_params_index) == 0:
+                self._rank = 0
+            else:
+                self._rank = np.linalg.matrix_rank(np.diag(singular_values))
         return -neg_cov
 
     @cache_readonly


Test patch:
diff --git a/statsmodels/tsa/statespace/tests/test_exponential_smoothing.py b/statsmodels/tsa/statespace/tests/test_exponential_smoothing.py
--- a/statsmodels/tsa/statespace/tests/test_exponential_smoothing.py
+++ b/statsmodels/tsa/statespace/tests/test_exponential_smoothing.py
@@ -877,3 +877,13 @@ def test_invalid():
                        match='ExponentialSmoothing does not support `exog`.'):
         mod = ExponentialSmoothing(aust)
         mod.clone(aust, exog=air)
+
+
+def test_parameterless_model(reset_randomstate):
+    # GH 6687
+    x = np.cumsum(np.random.standard_normal(1000))
+    ses = ExponentialSmoothing(x, initial_level=x[0], initialization_method="known")
+    with ses.fix_params({'smoothing_level': 0.5}):
+        res = ses.fit()
+    assert np.isnan(res.bse).all()
+    assert res.fixed_params == ["smoothing_level"]