Repository: gammapy/gammapy

Issue:
Remove parameter scaling?
In preparation of the H.E.S.S. DL3 data tutorial, I found issues with convergence of the model fits both using the `iminuit` and `sherpa` backend. In both cases the optimiser would try positions outside the map already on the first iteration, which currently leads to an error (whether this behaviour is a good one, is a different discussion...). The reason for this weird behaviour is, that we currently scale **all** parameters before the fit and only pass the `factors` to the optimiser.  E.g. if you do a crab analysis in `icrs` coordinates, the optimiser will try a step size in the order of ~10 deg in longitude, which does not make sense. There are now different solutions to fix this behavior:

- Some optimisers can take an initial estimate of the covariance matrix. We could assume plausible default errors for all parameters, but we need a way to build a global covariance matrix form these sub-covariances, e.g. by adding a `Parameters.concatenate()` method.
- We could add a `autoscale` flag to our `Parameter` class, to switch of the scaling for certain parameters (especially position...)
- We could remove the parameter scaling completely. This would actually be my favoured solution. Currently the parameter scaling is only really required (if at all) for the spectral `amplitude` parameter, where the diagonal entries on the covariance matrix can get very small. As we support units for parameters, the scaling can be achieved natively, by moving the scale to the unit of the parameter like `amplitude = u.Quantity("1.34 1e-12 TeV-1 cm-2 s-1")`. This puts a little bit of responsibility on the side of the user, but this can be handled with documentation if needed. Removing the scaling would simplify our `Parameter` class a lot. 

Any opinions @registerrier and @cdeil?



Discussion:
I completely agree that in many cases the scaling is meaningless, in particular for coordinates which are defined only to a closed interval in the real set. 
Technically, it is probably only useful for `amplitude` parameter which can be distributed over several decades. We could alternatively use a log-like representation of the amplitude in the minimizer.  

Often other tools (e.g. sherpa, XSpec) have a `guess` or `norm` function that try to find the good `amplitude` of the model w.r.t. to the data. We could get rid of the amplitude scaling by adding such a functionality, namely `Fit.guess()`. This would require that we have a way to determine 'guessable' parameters, so this is likely not very different from adding an option `autoscale`.

So we might explore your second solution where the only `autoscale=True` parameters would be the amplitudes. If we have all the code there already for scaling, it might be bad to undo everything no?
 
I removed the "bug" label - it there is still a bug, then @adonath please give an example and re-add the bug label. However, I by now also think we should try to remove the parameter scaling (once we have some validation, a week from now), or significantly change the scaling code, so I'm adding a "high-priority" label here.
I've observed this problem in several fits now as well - the minimizer seems to jump outside the FoV already in the first step. To me, this is a clearly a bug!

Previously (e.g. in v0.12) it was possible to set `fit.datasets.parameters.apply_autoscale = False` and provide step sizes for all parameters by setting `fit.datasets.parameters.covariance`. This, unfortunately, does not seem to work any longer (at least the fit crashes with the same error in the cases I've tested). Has the parameter scaling been changed between v0.12 and v0.14?
@lmohrmann For v0.15 the behaviour is changed such that if the model component moves outside the image it is just ignored in the likelihood evaluation (instead of throwing an error). This seemed the more stable behaviour.

I just checked and indeed there was a small change in v0.14, that the caching of the fit parameters moved from the `Datasets` object to the `Fit` object, where we hide the parameters. So to restore the old behaviour, you have to use the following work-around:

```
fit = Fit([dataset])
fit._parameters.apply_autoscale = False
fit._parameters.covariance = np.diag([1, 1, 1])

result = fit.optimize()
```
In Gammapy v0.15 the covariance will be build "on the fly" from sub-covariances, which already simplifies the definition for complex models. So for v0.15 the issue will be gone.

In general I'm still in favour of removing the parameter scaling completely, but at the coding sprint we agreed, that we will only removed it once we have a high level benchmark with a complex model fit in place. With this benchmark we can measure, what difference the parameter scaling makes in performance / convergence.
We're mixing discussions / issues here.

Whether to remove the `value = scale * factor` split is really a separate question if / how to set initial step sizes for Minuit.
It's mixed in the current implementation, but even if we remove `scale` and `factor` and just have value, the source-position based failing fits where the optimiser takes a bad first step will remain. I don't know if it was on Github or just in person, but basically the solution is to add a default initial step of roughly ~0.02 deg for lat, and `0.02 deg / cos(lat)` for lon, instead of what we do currently.

Having a minimal currently failing test (e.g. very briefly to the left or right of `lon=0 deg`, possibly going to `lat = 88 deg` or so, would be extremely useful as regression test. @lmohrmann or @adonath - can you create one?
@cdeil: I think you're right, the issue that I've been observing is likely connected to badly chosen step sizes for the coordinates, not necessarily the scaling.

@adonath: you can probably create a regression test as @cdeil proposes quicker than I can, but let me know if you don't have time, then I will have a go. Would be really nice to get this fixed for  v0.15 -- I think many testers will encounter this issue. When do you plan to do the release?
There is now the possibility to store the fit trace for debugging: #3074. So this test is rather simple to implement. In general I think removing the parameters scaling is still a good idea...these kind of parameter transformation should happened on the optimisers side, because every optimisers handles it differently...
Just a side note, maybe helpful: the other day I had the issue that I wanted to fit a `NaimaSpectralModel`, which usually have very large values for the amplitude parameter. It seems that the default behaviour was to apply no scaling to the parameter, which had the result that the fit did not change the parameter value at all, and set the error to 2.0 (exactly), while still reporting a successful fit. Turning the scaling on for the parameter fixed this.
Thanks @lmohrmann, my initial proposal for this was to just move the scale to the unit of the parameter. Like `1 * 1e-12 TeV-1 cm-2 s-1` instead of `1e12 TeV-1 cm-2 s-1`. Have you tried this as well? I realise this is less convenient, however we could document e.g. here: https://docs.gammapy.org/0.18.2/tutorials/modeling.html, here https://docs.gammapy.org/0.18.2/howto.html or possibly a "know issues" section.
No, have not tried attaching the scale to the unit. So your proposal would be to remove the `scale` attribute altogether?
Yes, I think so. This linear scaling can be achieved with the unit handling as well. My main conclusion from the issue here, is that neither "scale all parameters" nor "scale no parameter"  works as a reliable default in all cases. However the case, where the amplitude is super small (like for the `NaimaModel`) is much more rare than e.g. the fitting of `lon` and `lat`, where the scaling is problematic. 

We could only make a significant improvement if we add more meta data on the parameters such as type or more advanced scaling such as log or similar and this I is too much work for now. My guess is that the fit convergence without scaling is just fine in most cases, but we have to check of course...

Remaining action items:
- Fully disable the scaling and check convergence on our validation analyses
- Add documentation on the unit handling
- Add documentation on setting an initial covariance  
I quickly tested and removing the parameter scaling by default results in a lot of test failures and convergence issue. So let's keep it for now. However in #3394 I'm adding a `scale_parameters` option which makes the configuration easier...
We could introduce a scaling attribute on the parameter object so we can decide what to do with scaling case by case.

Resolving patch:
diff --git a/gammapy/modeling/fit.py b/gammapy/modeling/fit.py
--- a/gammapy/modeling/fit.py
+++ b/gammapy/modeling/fit.py
@@ -97,8 +97,6 @@ class Fit:
         interval can be adapted by modifying the the upper bound of the interval (``b``) value.
     store_trace : bool
         Whether to store the trace of the fit
-    scale_parameters : bool
-        Whether to scale parameters prior to the fit.
     """
 
     def __init__(
@@ -108,7 +106,6 @@ def __init__(
         covariance_opts=None,
         confidence_opts=None,
         store_trace=False,
-        scale_parameters=True
     ):
         self.store_trace = store_trace
         self.backend = backend
@@ -125,7 +122,6 @@ def __init__(
         self.optimize_opts = optimize_opts
         self.covariance_opts = covariance_opts
         self.confidence_opts = confidence_opts
-        self.scale_parameters = scale_parameters
         self._minuit = None
 
     @property
@@ -136,6 +132,7 @@ def minuit(self):
     @staticmethod
     def _parse_datasets(datasets):
         from gammapy.datasets import Datasets
+
         datasets = Datasets(datasets)
         return datasets, datasets.parameters
 
@@ -157,8 +154,10 @@ def run(self, datasets):
         # TODO: not sure how best to report the results
         # back or how to form the FitResult object.
 
-        return {"optimize_result": optimize_result,
-                "covariance_result": covariance_result}
+        return {
+            "optimize_result": optimize_result,
+            "covariance_result": covariance_result,
+        }
 
     def optimize(self, datasets):
         """Run the optimization.
@@ -176,8 +175,7 @@ def optimize(self, datasets):
         datasets, parameters = self._parse_datasets(datasets=datasets)
         datasets.parameters.check_limits()
 
-        if self.scale_parameters:
-            parameters.autoscale()
+        parameters.autoscale()
 
         kwargs = self.optimize_opts.copy()
         backend = kwargs.pop("backend", self.backend)
@@ -199,7 +197,10 @@ def optimize(self, datasets):
         trace = table_from_row_data(info.pop("trace"))
 
         if self.store_trace:
-            idx = [parameters.index(par) for par in parameters.unique_parameters.free_parameters]
+            idx = [
+                parameters.index(par)
+                for par in parameters.unique_parameters.free_parameters
+            ]
             unique_names = np.array(datasets.models.parameters_unique_names)[idx]
             trace.rename_columns(trace.colnames[1:], list(unique_names))
 
@@ -243,9 +244,7 @@ def covariance(self, datasets):
                 method = ""
 
             factor_matrix, info = compute(
-                parameters=parameters,
-                function=datasets.stat_sum,
-                **kwargs
+                parameters=parameters, function=datasets.stat_sum, **kwargs
             )
 
             datasets.models.covariance = Covariance.from_factor_matrix(
@@ -309,12 +308,7 @@ def confidence(self, datasets, parameter, sigma=1, reoptimize=True):
         result["errn"] *= parameter.scale
         return result
 
-    def stat_profile(
-        self,
-        datasets,
-        parameter,
-        reoptimize=False
-    ):
+    def stat_profile(self, datasets, parameter, reoptimize=False):
         """Compute fit statistic profile.
 
         The method used is to vary one parameter, keeping all others fixed.
@@ -394,8 +388,7 @@ def stat_surface(self, datasets, x, y, reoptimize=False):
 
         with parameters.restore_status():
             for x_value, y_value in progress_bar(
-                itertools.product(x.scan_values, y.scan_values),
-                desc="Trial values"
+                itertools.product(x.scan_values, y.scan_values), desc="Trial values"
             ):
                 x.value, y.value = x_value, y_value
 
diff --git a/gammapy/modeling/models/spectral.py b/gammapy/modeling/models/spectral.py
--- a/gammapy/modeling/models/spectral.py
+++ b/gammapy/modeling/models/spectral.py
@@ -334,9 +334,7 @@ def plot(
             flux_unit = DEFAULT_UNIT[sed_type]
 
         energy_min, energy_max = energy_bounds
-        axis = MapAxis.from_energy_bounds(
-            energy_min, energy_max, n_points, energy_unit
-        )
+        axis = MapAxis.from_energy_bounds(energy_min, energy_max, n_points, energy_unit)
 
         energy = axis.center
         if sed_type == "dnde":
@@ -427,9 +425,7 @@ def plot_error(
         kwargs.setdefault("linewidth", 0)
 
         energy_min, energy_max = energy_bounds
-        axis = MapAxis.from_energy_bounds(
-            energy_min, energy_max, n_points, energy_unit
-        )
+        axis = MapAxis.from_energy_bounds(energy_min, energy_max, n_points, energy_unit)
 
         energy = axis.center
 
@@ -645,8 +641,8 @@ class PowerLawSpectralModel(SpectralModel):
 
     tag = ["PowerLawSpectralModel", "pl"]
     index = Parameter("index", 2.0)
-    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1")
-    reference = Parameter("reference", "1 TeV", frozen=True)
+    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1", scale_method="scale10")
+    reference = Parameter("reference", "1 TeV", frozen=True, scale_method="scale10")
 
     @staticmethod
     def evaluate(energy, index, amplitude, reference):
@@ -763,7 +759,7 @@ class PowerLawNormSpectralModel(SpectralModel):
     tag = ["PowerLawNormSpectralModel", "pl-norm"]
     norm = Parameter("norm", 1, unit="")
     tilt = Parameter("tilt", 0, frozen=True)
-    reference = Parameter("reference", "1 TeV", frozen=True)
+    reference = Parameter("reference", "1 TeV", frozen=True, scale_method="scale10")
 
     @staticmethod
     def evaluate(energy, tilt, norm, reference):
@@ -858,10 +854,10 @@ class PowerLaw2SpectralModel(SpectralModel):
     """
     tag = ["PowerLaw2SpectralModel", "pl-2"]
 
-    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1")
+    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1", scale_method="scale10")
     index = Parameter("index", 2)
-    emin = Parameter("emin", "0.1 TeV", frozen=True)
-    emax = Parameter("emax", "100 TeV", frozen=True)
+    emin = Parameter("emin", "0.1 TeV", frozen=True, scale_method="scale10")
+    emax = Parameter("emax", "100 TeV", frozen=True, scale_method="scale10")
 
     @staticmethod
     def evaluate(energy, amplitude, index, emin, emax):
@@ -940,8 +936,8 @@ class BrokenPowerLawSpectralModel(SpectralModel):
     tag = ["BrokenPowerLawSpectralModel", "bpl"]
     index1 = Parameter("index1", 2.0)
     index2 = Parameter("index2", 2.0)
-    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1")
-    ebreak = Parameter("ebreak", "1 TeV")
+    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1", scale_method="scale10")
+    ebreak = Parameter("ebreak", "1 TeV", scale_method="scale10")
 
     @staticmethod
     def evaluate(energy, index1, index2, amplitude, ebreak):
@@ -982,9 +978,9 @@ class SmoothBrokenPowerLawSpectralModel(SpectralModel):
     tag = ["SmoothBrokenPowerLawSpectralModel", "sbpl"]
     index1 = Parameter("index1", 2.0)
     index2 = Parameter("index2", 2.0)
-    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1")
-    ebreak = Parameter("ebreak", "1 TeV")
-    reference = Parameter("reference", "1 TeV", frozen=True)
+    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1", scale_method="scale10")
+    ebreak = Parameter("ebreak", "1 TeV", scale_method="scale10")
+    reference = Parameter("reference", "1 TeV", frozen=True, scale_method="scale10")
     beta = Parameter("beta", 1, frozen=True)
 
     @staticmethod
@@ -1104,9 +1100,9 @@ class ExpCutoffPowerLawSpectralModel(SpectralModel):
     tag = ["ExpCutoffPowerLawSpectralModel", "ecpl"]
 
     index = Parameter("index", 1.5)
-    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1")
-    reference = Parameter("reference", "1 TeV", frozen=True)
-    lambda_ = Parameter("lambda_", "0.1 TeV-1")
+    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1", scale_method="scale10")
+    reference = Parameter("reference", "1 TeV", frozen=True, scale_method="scale10")
+    lambda_ = Parameter("lambda_", "0.1 TeV-1", scale_method="scale10")
     alpha = Parameter("alpha", "1.0", frozen=True)
 
     @staticmethod
@@ -1161,7 +1157,7 @@ class ExpCutoffPowerLawNormSpectralModel(SpectralModel):
 
     index = Parameter("index", 1.5)
     norm = Parameter("norm", 1, unit="")
-    reference = Parameter("reference", "1 TeV", frozen=True)
+    reference = Parameter("reference", "1 TeV", frozen=True, scale_method="scale10")
     lambda_ = Parameter("lambda_", "0.1 TeV-1")
     alpha = Parameter("alpha", "1.0", frozen=True)
 
@@ -1193,9 +1189,9 @@ class ExpCutoffPowerLaw3FGLSpectralModel(SpectralModel):
 
     tag = ["ExpCutoffPowerLaw3FGLSpectralModel", "ecpl-3fgl"]
     index = Parameter("index", 1.5)
-    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1")
-    reference = Parameter("reference", "1 TeV", frozen=True)
-    ecut = Parameter("ecut", "10 TeV")
+    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1", scale_method="scale10")
+    reference = Parameter("reference", "1 TeV", frozen=True, scale_method="scale10")
+    ecut = Parameter("ecut", "10 TeV", scale_method="scale10")
 
     @staticmethod
     def evaluate(energy, index, amplitude, reference, ecut):
@@ -1231,9 +1227,9 @@ class SuperExpCutoffPowerLaw3FGLSpectralModel(SpectralModel):
     """
 
     tag = ["SuperExpCutoffPowerLaw3FGLSpectralModel", "secpl-3fgl"]
-    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1")
-    reference = Parameter("reference", "1 TeV", frozen=True)
-    ecut = Parameter("ecut", "10 TeV")
+    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1", scale_method="scale10")
+    reference = Parameter("reference", "1 TeV", frozen=True, scale_method="scale10")
+    ecut = Parameter("ecut", "10 TeV", scale_method="scale10")
     index_1 = Parameter("index_1", 1.5)
     index_2 = Parameter("index_2", 2)
 
@@ -1266,9 +1262,9 @@ class SuperExpCutoffPowerLaw4FGLSpectralModel(SpectralModel):
     """
 
     tag = ["SuperExpCutoffPowerLaw4FGLSpectralModel", "secpl-4fgl"]
-    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1")
-    reference = Parameter("reference", "1 TeV", frozen=True)
-    expfactor = Parameter("expfactor", "1e-2")
+    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1", scale_method="scale10")
+    reference = Parameter("reference", "1 TeV", frozen=True, scale_method="scale10")
+    expfactor = Parameter("expfactor", "1e-2", scale_method="scale10")
     index_1 = Parameter("index_1", 1.5)
     index_2 = Parameter("index_2", 2)
 
@@ -1306,8 +1302,8 @@ class LogParabolaSpectralModel(SpectralModel):
 
     """
     tag = ["LogParabolaSpectralModel", "lp"]
-    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1")
-    reference = Parameter("reference", "10 TeV", frozen=True)
+    amplitude = Parameter("amplitude", "1e-12 cm-2 s-1 TeV-1", scale_method="scale10")
+    reference = Parameter("reference", "10 TeV", frozen=True, scale_method="scale10")
     alpha = Parameter("alpha", 2)
     beta = Parameter("beta", 1)
 
@@ -1359,7 +1355,7 @@ class LogParabolaNormSpectralModel(SpectralModel):
     """
     tag = ["LogParabolaNormSpectralModel", "lp-norm"]
     norm = Parameter("norm", 1, unit="")
-    reference = Parameter("reference", "10 TeV", frozen=True)
+    reference = Parameter("reference", "10 TeV", frozen=True, scale_method="scale10")
     alpha = Parameter("alpha", 2)
     beta = Parameter("beta", 1)
 
@@ -1766,6 +1762,9 @@ def __init__(
             parameters.append(Parameter("B", B))
             parameters.append(Parameter("radius", radius, frozen=True))
 
+        for p in parameters:
+            p.scale_method = "scale10"
+
         self.default_parameters = Parameters(parameters)
         super().__init__()
 
diff --git a/gammapy/modeling/parameter.py b/gammapy/modeling/parameter.py
--- a/gammapy/modeling/parameter.py
+++ b/gammapy/modeling/parameter.py
@@ -78,6 +78,8 @@ class Parameter:
         Number of sigmas to scan.
     scan_values: `numpy.array`
         Scan values. Overwrites all of the scan keywords before.
+    scale_method : {'scale10', 'factor1', None}
+        Method used to set ``factor`` and ``scale``
     interp : {"lin", "sqrt", "log"}
         Parameter scaling to use for the scan.
 
@@ -98,6 +100,7 @@ def __init__(
         scan_n_values=11,
         scan_n_sigma=2,
         scan_values=None,
+        scale_method=None,
         interp="lin",
     ):
         self.name = name
@@ -106,6 +109,7 @@ def __init__(
         self.min = min
         self.max = max
         self.frozen = frozen
+        self.scale_method = scale_method
         self._error = error
         self._type = None
 
@@ -232,6 +236,17 @@ def factor_max(self):
         """
         return self.max / self.scale
 
+    @property
+    def scale_method(self):
+        """Method used to set ``factor`` and ``scale``"""
+        return self._scale_method
+
+    @scale_method.setter
+    def scale_method(self, val):
+        if val not in ["scale10", "factor1"] and val is not None:
+            raise ValueError(f"Invalid method: {val}")
+        self._scale_method = val
+
     @property
     def frozen(self):
         """Frozen? (used in fitting) (bool)."""
@@ -239,8 +254,8 @@ def frozen(self):
 
     @frozen.setter
     def frozen(self, val):
-        if val in ['True', 'False']:
-            val=bool(val)
+        if val in ["True", "False"]:
+            val = bool(val)
         if not isinstance(val, bool) and not isinstance(val, np.bool_):
             raise TypeError(f"Invalid type: {val}, {type(val)}")
         self._frozen = val
@@ -264,7 +279,9 @@ def quantity(self, val):
         val = u.Quantity(val)
 
         if not val.unit.is_equivalent(self.unit):
-            raise u.UnitConversionError(f"Unit must be equivalent to {self.unit} for parameter {self.name}")
+            raise u.UnitConversionError(
+                f"Unit must be equivalent to {self.unit} for parameter {self.name}"
+            )
 
         self.value = val.value
         self.unit = val.unit
@@ -345,8 +362,9 @@ def copy(self):
     def update_from_dict(self, data):
         """Update parameters from a dict.
            Protection against changing parameter model, type, name."""
-        keys=["value", "unit", "min", "max", "frozen"]
-        for k in keys: setattr(self, k, data[k])
+        keys = ["value", "unit", "min", "max", "frozen"]
+        for k in keys:
+            setattr(self, k, data[k])
 
     def to_dict(self):
         """Convert to dict."""
@@ -354,46 +372,44 @@ def to_dict(self):
             "name": self.name,
             "value": self.value,
             "unit": self.unit.to_string("fits"),
+            "error": self.error,
             "min": self.min,
             "max": self.max,
             "frozen": self.frozen,
-            "error": self.error,
+            "interp": self.interp
         }
+        if self.scale_method is not None:
+            output["scale_method"] = self.scale_method
 
         if self._link_label_io is not None:
             output["link"] = self._link_label_io
         return output
 
-    def autoscale(self, method="scale10"):
+    def autoscale(self):
         """Autoscale the parameters.
 
-        Set ``factor`` and ``scale`` according to ``method``
+        Set ``factor`` and ``scale`` according to ``scale_method`` attribute
 
-        Available methods:
+        Available ``scale_method``
 
         * ``scale10`` sets ``scale`` to power of 10,
           so that abs(factor) is in the range 1 to 10
         * ``factor1`` sets ``factor, scale = 1, value``
 
         In both cases the sign of value is stored in ``factor``,
-        i.e. the ``scale`` is always positive.
+        i.e. the ``scale`` is always positive. 
+        If ``scale_method`` is None the scaling is ignored.
 
-        Parameters
-        ----------
-        method : {'factor1', 'scale10'}
-            Method to apply
         """
-        if method == "scale10":
+        if self.scale_method == "scale10":
             value = self.value
             if value != 0:
                 exponent = np.floor(np.log10(np.abs(value)))
                 scale = np.power(10.0, exponent)
                 self.factor = value / scale
                 self.scale = scale
-        elif method == "factor1":
+        elif self.scale_method == "factor1":
             self.factor, self.scale = 1, self.value
-        else:
-            raise ValueError(f"Invalid method: {method}")
 
 
 class Parameters(collections.abc.Sequence):
@@ -544,9 +560,12 @@ def to_dict(self):
 
     def to_table(self):
         """Convert parameter attributes to `~astropy.table.Table`."""
-        rows=[]
+        rows = []
         for p in self._parameters:
             d = p.to_dict()
+            for key in ["scale_method", "interp"]:
+                if key in d:
+                    del d[key]
             rows.append({**dict(type=p.type), **d})
         table = table_from_row_data(rows)
 
@@ -581,18 +600,14 @@ def set_parameter_factors(self, factors):
                 parameter.factor = factors[idx]
                 idx += 1
 
-    def autoscale(self, method="scale10"):
+    def autoscale(self):
         """Autoscale all parameters.
 
         See :func:`~gammapy.modeling.Parameter.autoscale`
 
-        Parameters
-        ----------
-        method : {'factor1', 'scale10'}
-            Method to apply
         """
         for par in self._parameters:
-            par.autoscale(method)
+            par.autoscale()
 
     def select(
         self, name=None, type=None, frozen=None,


Test patch:
diff --git a/gammapy/modeling/tests/test_fit.py b/gammapy/modeling/tests/test_fit.py
--- a/gammapy/modeling/tests/test_fit.py
+++ b/gammapy/modeling/tests/test_fit.py
@@ -266,11 +266,11 @@ def test_stat_contour():
     x = result["y"]
     assert_allclose(len(x), 10)
     assert_allclose(x[0], 299, rtol=1e-5)
-    assert_allclose(x[-1], 299.133975, rtol=1e-5)
+    assert_allclose(x[-1], 299.292893, rtol=1e-5)
     y = result["z"]
     assert_allclose(len(y), 10)
     assert_allclose(y[0], 0.04, rtol=1e-5)
-    assert_allclose(y[-1], 0.54, rtol=1e-5)
+    assert_allclose(y[-1], 0.747107, rtol=1e-5)
 
     # Check that original value state wasn't changed
     assert_allclose(dataset.models.parameters["y"].value, 300)
diff --git a/gammapy/modeling/tests/test_parameter.py b/gammapy/modeling/tests/test_parameter.py
--- a/gammapy/modeling/tests/test_parameter.py
+++ b/gammapy/modeling/tests/test_parameter.py
@@ -32,7 +32,10 @@ def test_parameter_outside_limit(caplog):
     par = Parameter("spam", 50, min=0, max=40)
     par.check_limits()
     assert caplog.records[-1].levelname == "WARNING"
-    assert caplog.records[-1].message == "Value 50.0 is outside bounds [0.0, 40.0] for parameter 'spam'"
+    assert (
+        caplog.records[-1].message
+        == "Value 50.0 is outside bounds [0.0, 40.0] for parameter 'spam'"
+    )
 
 
 def test_parameter_scale():
@@ -86,12 +89,12 @@ def test_parameter_to_dict():
         # Regression test for https://github.com/gammapy/gammapy/issues/1883
         ("scale10", 9e35, 9, 1e35),
         # Checks for the simpler method="factor1"
-        ("factor1", 2e10, 2, 1e10),
-        ("factor1", -2e10, -2, 1e10),
+        ("factor1", 2e10, 1, 2e10),
+        ("factor1", -2e10, 1, -2e10),
     ],
 )
 def test_parameter_autoscale(method, value, factor, scale):
-    par = Parameter("", value)
+    par = Parameter("", value, scale_method=method)
     par.autoscale()
     assert_allclose(par.factor, factor)
     assert_allclose(par.scale, scale)
@@ -169,12 +172,26 @@ def test_parameters_set_parameter_factors(pars):
     assert_allclose(pars["ham"].scale, 1)
 
 
-def test_parameters_autoscale():
-    pars = Parameters([Parameter("", 20)])
+def test_parameters_s():
+    pars = Parameters([Parameter("", 20, scale_method="scale10"),
+                       Parameter("", 20, scale_method=None)])
+    pars_dict = pars.to_dict()
     pars.autoscale()
     assert_allclose(pars[0].factor, 2)
     assert_allclose(pars[0].scale, 10)
 
+    assert pars_dict[0]["scale_method"] == "scale10"
+    assert "scale_method" not in pars_dict[1]
+    pars = Parameters.from_dict(pars_dict)
+    pars.autoscale()
+    assert_allclose(pars[0].factor, 2)
+    assert_allclose(pars[0].scale, 10)
+    assert pars[1].scale_method is None
+    pars.autoscale()
+    assert_allclose(pars[1].factor, 20)
+    assert_allclose(pars[1].scale, 1)    
+
+
 
 def test_parameter_scan_values():
     p = Parameter(name="test", value=0, error=1)
@@ -203,17 +220,43 @@ def test_parameter_scan_values():
 
 
 def test_update_from_dict():
-    par = Parameter("test", value=1e-10, min="nan", max="nan", frozen=False, unit="TeV")
+    par = Parameter(
+        "test",
+        value=1e-10,
+        min="nan",
+        max="nan",
+        frozen=False,
+        unit="TeV",
+        scale_method="scale10",
+    )
     par.autoscale()
-    data={"model":"gc", "type":"spectral", "name": "test2", "value":3e-10, "min":0, "max":np.nan, "frozen":True, "unit":"GeV"}
+    data = {
+        "model": "gc",
+        "type": "spectral",
+        "name": "test2",
+        "value": 3e-10,
+        "min": 0,
+        "max": np.nan,
+        "frozen": True,
+        "unit": "GeV",
+    }
     par.update_from_dict(data)
     assert par.name == "test"
     assert par.factor == 3
     assert par.value == 3e-10
     assert par.unit == "GeV"
     assert par.min == 0
-    assert par.max is np.nan    
+    assert par.max is np.nan
     assert par.frozen == True
-    data={"model":"gc", "type":"spectral", "name": "test2", "value":3e-10, "min":0, "max":np.nan, "frozen":'True', "unit":"GeV"}
+    data = {
+        "model": "gc",
+        "type": "spectral",
+        "name": "test2",
+        "value": 3e-10,
+        "min": 0,
+        "max": np.nan,
+        "frozen": "True",
+        "unit": "GeV",
+    }
     par.update_from_dict(data)
     assert par.frozen == True