Repository: gammapy/gammapy
Pull request #4218 (2022-11-25 11:23:25)

Add interp_scale to parameter scaling
Use the interpolation scale given by Parameter.interp in the transform from the parameter value to the factor seen by the optimizer, as proposed in #4027.
Not sure how to combine this with the scale method, for now I apply interp_scale first and then the scale method.

adonath: Thanks @QRemy! Should deprecate the "old scaling" methods? @registerrier
QRemy: For now they are still applied after the scaling from the .interp argument, not sure if it makes sense. 
adonath: @QRemy I think we never really discussed this PR in our dev call, would you mind setting it on the agenda for this Friday? https://github.com/gammapy/gammapy-meetings/tree/master/dev-meetings/2023/2023-01-27 Thanks!
QRemy: For backward compatibility I added a `scale_interp="lin"` argument instead of using direclty `interp` so the transform is done correctly for previously written models.
codecov[bot]: # [Codecov](https://codecov.io/gh/gammapy/gammapy/pull/4218?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy) Report
> Merging [#4218](https://codecov.io/gh/gammapy/gammapy/pull/4218?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy) (037fc9e) into [main](https://codecov.io/gh/gammapy/gammapy/commit/c7569183c566f1829092f55f57a08c18d6cbcec3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy) (c756918) will **decrease** coverage by `0.04%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main    #4218      +/-   ##
==========================================
- Coverage   94.85%   94.82%   -0.04%     
==========================================
  Files         213      213              
  Lines       30160    30177      +17     
==========================================
+ Hits        28609    28616       +7     
- Misses       1551     1561      +10     
```


| [Impacted Files](https://codecov.io/gh/gammapy/gammapy/pull/4218?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy) | Coverage Δ | |
|---|---|---|
| [gammapy/estimators/map/excess.py](https://codecov.io/gh/gammapy/gammapy/pull/4218?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy#diff-Z2FtbWFweS9lc3RpbWF0b3JzL21hcC9leGNlc3MucHk=) | `99.02% <ø> (ø)` | |
| [gammapy/datasets/evaluator.py](https://codecov.io/gh/gammapy/gammapy/pull/4218?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy#diff-Z2FtbWFweS9kYXRhc2V0cy9ldmFsdWF0b3IucHk=) | `91.22% <100.00%> (+0.06%)` | :arrow_up: |
| [gammapy/datasets/utils.py](https://codecov.io/gh/gammapy/gammapy/pull/4218?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy#diff-Z2FtbWFweS9kYXRhc2V0cy91dGlscy5weQ==) | `88.88% <100.00%> (+4.88%)` | :arrow_up: |
| [gammapy/maps/core.py](https://codecov.io/gh/gammapy/gammapy/pull/4218?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy#diff-Z2FtbWFweS9tYXBzL2NvcmUucHk=) | `85.17% <100.00%> (-1.61%)` | :arrow_down: |
| [gammapy/maps/region/tests/test\_ndmap.py](https://codecov.io/gh/gammapy/gammapy/pull/4218?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy#diff-Z2FtbWFweS9tYXBzL3JlZ2lvbi90ZXN0cy90ZXN0X25kbWFwLnB5) | `100.00% <100.00%> (ø)` | |
| [gammapy/modeling/parameter.py](https://codecov.io/gh/gammapy/gammapy/pull/4218?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy#diff-Z2FtbWFweS9tb2RlbGluZy9wYXJhbWV0ZXIucHk=) | `96.93% <100.00%> (+0.08%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gammapy)

bkhelifi: @QRemy what is the status of this work?
Astro-Kirsty: @QRemy, what is the status here?
Once rebased, ready for review?
QRemy: Now the inverse transform of the covariance is implemented using `propagate` from `jacobi` package
So it's ready for review.
QRemy: One last review before merging this old PR ? 
QRemy: > priors produce similar results since changing the measure of a parameter must be equivalent to setting a prior

priors change the likelihood while this change only how the parameter space is explored
bkhelifi: What is the status here, @QRemy ?
QRemy: > What is the status here, @QRemy ?

Ready

Patch:
diff --git a/gammapy/modeling/covariance.py b/gammapy/modeling/covariance.py
index 155dbb727..6bd867cbd 100644
--- a/gammapy/modeling/covariance.py
+++ b/gammapy/modeling/covariance.py
@@ -72,14 +72,19 @@ class Covariance:
 
         Used in the optimizer interface.
         """
+
         npars = len(parameters)
 
-        if not matrix.shape == (npars, npars):
-            matrix = cls._expand_factor_matrix(matrix, parameters)
+        if npars > 0:
+            if not matrix.shape == (npars, npars):
+                matrix = cls._expand_factor_matrix(matrix, parameters)
 
-        scales = [par.scale for par in parameters]
-        scale_matrix = np.outer(scales, scales)
-        data = scale_matrix * matrix
+            df = np.diag(
+                [p._inverse_transform_derivative(p.factor) for p in parameters]
+            )
+            data = df.T @ matrix @ df
+        else:
+            data = None
 
         return cls(parameters, data=data)
 
diff --git a/gammapy/modeling/models/core.py b/gammapy/modeling/models/core.py
index 459cb5332..78f46bd52 100644
--- a/gammapy/modeling/models/core.py
+++ b/gammapy/modeling/models/core.py
@@ -297,6 +297,7 @@ class ModelBase:
                     "error",
                     "interp",
                     "scale_method",
+                    "scale_transform",
                 ]:
                     default = init[item]
 
diff --git a/gammapy/modeling/parameter.py b/gammapy/modeling/parameter.py
index 28a9a339d..baff1487d 100644
--- a/gammapy/modeling/parameter.py
+++ b/gammapy/modeling/parameter.py
@@ -47,54 +47,56 @@ def _get_parameters_str(parameters):
 class Parameter:
     """A model parameter.
 
-     Note that the parameter value has been split into
-     a factor and scale like this::
-
-         value = factor x scale
-
-     Users should interact with the ``value``, ``quantity``
-     or ``min`` and ``max`` properties and consider the fact
-     that there is a ``factor`` and ``scale`` an implementation detail.
-
-     That was introduced for numerical stability in parameter and error
-     estimation methods, only in the Gammapy optimiser interface do we
-     interact with the ``factor``, ``factor_min`` and ``factor_max`` properties,
-     i.e. the optimiser "sees" the well-scaled problem.
-
-     Parameters
-     ----------
-     name : str
-         Name.
-     value : float or `~astropy.units.Quantity`
-         Value.
-     scale : float, optional
-         Scale (sometimes used in fitting).
-     unit : `~astropy.units.Unit` or str, optional
-         Unit.
-     min : float, str or `~astropy.units.quantity`, optional
-         Minimum (sometimes used in fitting). Default is None, which set the attribute to `np.nan`.
-     max : float, str or `~astropy.units.quantity`, optional
-         Maximum (sometimes used in fitting). Default is None, which set the attribute to `np.nan`.
+    Note that the parameter value has been split into
+    a factor and scale like this::
+
+        value = factor x scale
+
+    Users should interact with the ``value``, ``quantity``
+    or ``min`` and ``max`` properties and consider the fact
+    that there is a ``factor`` and ``scale`` an implementation detail.
+
+    That was introduced for numerical stability in parameter and error
+    estimation methods, only in the Gammapy optimiser interface do we
+    interact with the ``factor``, ``factor_min`` and ``factor_max`` properties,
+    i.e. the optimiser "sees" the well-scaled problem.
+
+    Parameters
+    ----------
+    name : str
+        Name.
+    value : float or `~astropy.units.Quantity`
+        Value.
+    scale : float, optional
+        Scale (sometimes used in fitting).
+    unit : `~astropy.units.Unit` or str, optional
+        Unit.
+    min : float, str or `~astropy.units.quantity`, optional
+        Minimum (sometimes used in fitting). Default is None, which set the attribute to `np.nan`.
+    max : float, str or `~astropy.units.quantity`, optional
+        Maximum (sometimes used in fitting). Default is None, which set the attribute to `np.nan`.
     frozen : bool, optional
-         Frozen (used in fitting).
-     error : float
-         Parameter error.
-     scan_min : float
-         Minimum value for the parameter scan. Overwrites scan_n_sigma.
-     scan_max : float
-         Maximum value for the parameter scan. Overwrites scan_n_sigma.
-     scan_n_values: int
-         Number of values to be used for the parameter scan.
-     scan_n_sigma : int
-         Number of sigmas to scan.
-     scan_values: `numpy.array`
-         Scan values. Overwrites all 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.
-     prior : `~gammapy.modeling.models.Prior`
-         Prior set on the parameter.
+        Frozen (used in fitting).
+    error : float
+        Parameter error.
+    scan_min : float
+        Minimum value for the parameter scan. Overwrites scan_n_sigma.
+    scan_max : float
+        Maximum value for the parameter scan. Overwrites scan_n_sigma.
+    scan_n_values: int
+        Number of values to be used for the parameter scan.
+    scan_n_sigma : int
+        Number of sigmas to scan.
+    scan_values: `numpy.array`
+        Scan values. Overwrites all the scan keywords before.
+    scale_method : {'scale10', 'factor1', None}
+        Method used to set ``factor`` and ``scale``.  Default is "scale10".
+    interp : {"lin", "sqrt", "log"}
+        Parameter scaling to use for the scan.  Default is "lin".
+    prior : `~gammapy.modeling.models.Prior`
+        Prior set on the parameter.
+    scale_transform : {"lin", "sqrt", "log"}
+        Parameter scaling. Default is "lin".
     """
 
     def __init__(
@@ -115,13 +117,17 @@ class Parameter:
         scale_method="scale10",
         interp="lin",
         prior=None,
+        scale_transform="lin",
     ):
         if not isinstance(name, str):
             raise TypeError(f"Name must be string, got '{type(name)}' instead")
 
         self._name = name
         self._link_label_io = None
-        self.scale = scale
+        self._scale_method = scale_method
+        self._scale_transform = scale_transform
+        self.interp = interp
+        self._scale = float(scale)
         self.frozen = frozen
         self._error = error
         self._type = None
@@ -143,8 +149,6 @@ class Parameter:
         self.scan_values = scan_values
         self.scan_n_values = scan_n_values
         self.scan_n_sigma = scan_n_sigma
-        self.interp = interp
-        self.scale_method = scale_method
         self.prior = prior
 
     def __get__(self, instance, owner):
@@ -216,16 +220,13 @@ class Parameter:
     @factor.setter
     def factor(self, val):
         self._factor = float(val)
+        self._value = float(self.inverse_transform(self._factor))
 
     @property
     def scale(self):
         """Scale as a float."""
         return self._scale
 
-    @scale.setter
-    def scale(self, val):
-        self._scale = float(val)
-
     @property
     def unit(self):
         """Unit as a `~astropy.units.Unit` object."""
@@ -248,6 +249,15 @@ class Parameter:
         else:
             self._min = self._set_quantity_str_float(val)
 
+    @property
+    def factor_min(self):
+        """Factor minimum as a float (used by the optimizer).
+
+        By default when no transform is applied, ``factor_min = min / scale``,
+        otherwise ``factor_min = transform(min)``.
+        """
+        return self.transform(self.min)
+
     @property
     def max(self):
         """Maximum as a float."""
@@ -261,6 +271,15 @@ class Parameter:
         else:
             self._max = self._set_quantity_str_float(val)
 
+    @property
+    def factor_max(self):
+        """Factor maximum as a float (used by the optimizer).
+
+        By default when no transform is applied, ``factor_max = max / scale``,
+        otherwise ``factor_max = transform(max)``.
+        """
+        return self.transform(self.max)
+
     def _set_quantity_str_float(self, value):
         """Logics for min and max setter."""
         if isinstance(value, u.Quantity) or isinstance(value, str):
@@ -284,22 +303,6 @@ class Parameter:
         if max is not None:
             self.max = max
 
-    @property
-    def factor_min(self):
-        """Factor minimum as a float.
-
-        This ``factor_min = min / scale`` is for the optimizer interface.
-        """
-        return self.min / self.scale
-
-    @property
-    def factor_max(self):
-        """Factor maximum as a float.
-
-        This ``factor_max = max / scale`` is for the optimizer interface.
-        """
-        return self.max / self.scale
-
     @property
     def scale_method(self):
         """Method used to set ``factor`` and ``scale``."""
@@ -309,8 +312,21 @@ class Parameter:
     def scale_method(self, val):
         if val not in ["scale10", "factor1"] and val is not None:
             raise ValueError(f"Invalid method: {val}")
+        self.reset_autoscale()
         self._scale_method = val
 
+    @property
+    def scale_transform(self):
+        """scale interp : {"lin", "sqrt", "log"}"""
+        return self._scale_transform
+
+    @scale_transform.setter
+    def scale_transform(self, val):
+        if val not in ["lin", "log", "sqrt"]:
+            raise ValueError(f"Invalid transform: {val}")
+        self.reset_autoscale()
+        self._scale_transform = val
+
     @property
     def frozen(self):
         """Frozen (used in fitting) (bool)."""
@@ -327,11 +343,12 @@ class Parameter:
     @property
     def value(self):
         """Value = factor x scale (float)."""
-        return self._factor * self._scale
+        return self._value
 
     @value.setter
     def value(self, val):
-        self.factor = float(val) / self._scale
+        self._value = float(val)
+        self._factor = self.transform(val)
 
     @property
     def quantity(self):
@@ -483,6 +500,7 @@ class Parameter:
             "frozen": self.frozen,
             "interp": self.interp,
             "scale_method": self.scale_method,
+            "scale_transform": self.scale_transform,
         }
 
         if self._link_label_io is not None:
@@ -491,8 +509,8 @@ class Parameter:
             output["prior"] = self.prior.to_dict()["prior"]
         return output
 
-    def autoscale(self):
-        """Autoscale the parameters.
+    def update_scale(self, value):
+        """Update the parameter scale.
 
         Set ``factor`` and ``scale`` according to ``scale_method`` attribute.
 
@@ -508,15 +526,60 @@ class Parameter:
 
         """
         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
+                self._scale = np.power(10.0, exponent)
 
         elif self.scale_method == "factor1":
-            self.factor, self.scale = 1, self.value
+            self._scale = value
+
+    def transform(self, value, update_scale=False):
+        """Tranform from value to factor (used by the optimizer).
+
+        Parameters
+        ----------
+        value : float
+            Parameter value
+        update_scale : bool, optional
+            Update the scaling (used by the autoscale). Default is False.
+        """
+        interp_scale = interpolation_scale(self.scale_transform)
+        transformed_value = interp_scale(value)
+        if update_scale:
+            self.update_scale(transformed_value)
+        return transformed_value / self.scale
+
+    def inverse_transform(self, factor):
+        """Inverse tranform from factor (used by the optimizer) to value.
+
+        Parameters
+        ----------
+        value : float
+            Parameter factor
+        """
+        interp_scale = interpolation_scale(self.scale_transform)
+        value = interp_scale.inverse(self.scale * factor)
+        return value
+
+    def _inverse_transform_derivative(self, factor):
+        """Inverse tranform from factor (used by the optimizer) to value.
+
+        Parameters
+        ----------
+        value : float
+            Parameter factor
+        """
+        interp_scale = interpolation_scale(self.scale_transform)
+        return interp_scale._inverse_deriv(self.scale * factor) * self.scale
+
+    def autoscale(self):
+        "Apply `interpolation_scale' and `scale_method' to the parameter."
+        self.factor = self.transform(self.value, update_scale=True)
+
+    def reset_autoscale(self):
+        "Reset scaling such as factor=value, scale=1."
+        self._factor = self._value
+        self._scale = 1.0
 
 
 class Parameters(collections.abc.Sequence):
@@ -852,12 +915,16 @@ class PriorParameter(Parameter):
         min=np.nan,
         max=np.nan,
         error=0,
+        scale_method="scale10",
+        scale_transform="lin",
     ):
         if not isinstance(name, str):
             raise TypeError(f"Name must be string, got '{type(name)}' instead")
 
         self._name = name
-        self.scale = scale
+        self._scale_method = scale_method
+        self._scale_transform = scale_transform
+        self._scale = float(scale)
         self.min = min
         self.max = max
         self._error = error
@@ -868,6 +935,7 @@ class PriorParameter(Parameter):
         else:
             self.factor = value
             self.unit = unit
+
         self._type = "prior"
 
     def to_dict(self):
diff --git a/gammapy/utils/interpolation.py b/gammapy/utils/interpolation.py
index 3c032b071..4f0d285ef 100644
--- a/gammapy/utils/interpolation.py
+++ b/gammapy/utils/interpolation.py
@@ -185,6 +185,13 @@ class InterpolationScale:
         else:
             return values
 
+    def _inverse_deriv(self, values):
+        values = self._inverse_derivative(values)
+        if hasattr(self, "_unit"):
+            return u.Quantity(values, self._unit, copy=COPY_IF_NEEDED)
+        else:
+            return values
+
 
 class LogScale(InterpolationScale):
     """Logarithmic scaling."""
@@ -200,6 +207,10 @@ class LogScale(InterpolationScale):
         output = np.exp(values)
         return np.where(abs(output) - cls.tiny <= cls.tiny, 0, output)
 
+    @classmethod
+    def _inverse_derivative(cls, values):
+        return cls._inverse(values)
+
 
 class SqrtScale(InterpolationScale):
     """Square root scaling."""
@@ -213,6 +224,10 @@ class SqrtScale(InterpolationScale):
     def _inverse(cls, values):
         return np.power(values, 2)
 
+    @classmethod
+    def _inverse_derivative(cls, values):
+        return 2.0 * values
+
 
 class StatProfileScale(InterpolationScale):
     """Square root profile scaling."""
@@ -241,6 +256,10 @@ class LinearScale(InterpolationScale):
     def _inverse(cls, values):
         return values
 
+    @classmethod
+    def _inverse_derivative(cls, values):
+        return 1.0
+
 
 def interpolate_profile(x, y, interp_scale="sqrt", extrapolate=False):
     """Helper function to interpolate one-dimensional profiles.


Test patch:
diff --git a/gammapy/modeling/tests/test_covariance.py b/gammapy/modeling/tests/test_covariance.py
index d28a0360f..b94226cc5 100644
--- a/gammapy/modeling/tests/test_covariance.py
+++ b/gammapy/modeling/tests/test_covariance.py
@@ -1,5 +1,6 @@
 # Licensed under a 3-clause BSD style license - see LICENSE.rst
 """Unit tests for the Covariance class"""
+
 import pytest
 import numpy as np
 from numpy.testing import assert_allclose
@@ -60,3 +61,15 @@ def test_scipy_mvn(covariance):
 def test_plot_correlation(covariance_diagonal):
     with mpl_plot_check():
         covariance_diagonal.plot_correlation()
+
+
+def test_from_factor_matrix():
+    covar = Covariance.from_factor_matrix([], None)
+    assert covar.data.shape == (0, 0)
+
+    x = Parameter("x", 1)
+    y = Parameter("y", 2)
+    parameters = Parameters([x, y])
+    covar = Covariance.from_factor_matrix(parameters, np.ones((2, 2)))
+    assert covar.data.shape == (2, 2)
+    assert_allclose(covar.data, 1)
diff --git a/gammapy/modeling/tests/test_fit.py b/gammapy/modeling/tests/test_fit.py
index f49c6c5b1..8a5ea0764 100644
--- a/gammapy/modeling/tests/test_fit.py
+++ b/gammapy/modeling/tests/test_fit.py
@@ -130,6 +130,91 @@ def test_run(backend):
     assert_allclose(pars["z"].error, 1, rtol=1e-7)
 
 
+def test_run_scale_transform_change_sqrt():
+    dataset = MyDataset()
+    fit = Fit(backend="minuit")
+    stat_ref = dataset.stat_sum()
+    for par in dataset.models.parameters:
+        par.scale_transform = "sqrt"
+    dataset.stat_sum()
+    assert_allclose(dataset.stat_sum(), stat_ref)
+
+    result = fit.run([dataset])
+    pars = dataset.models.parameters
+
+    assert fit._minuit is not None
+    assert result.success
+    assert result.optimize_result.method == "migrad"
+    assert result.covariance_result.method == "hesse"
+    assert result.covariance_result.success
+
+    assert_allclose(pars["x"].value, 2, rtol=1e-3)
+    assert_allclose(pars["y"].value, 3e2, rtol=1e-3)
+    assert_allclose(pars["z"].value, 4.07e-2, rtol=1e-2)
+
+    assert_allclose(pars["x"].error, 1, rtol=1e-4)
+    assert_allclose(pars["y"].error, 1, rtol=1e-4)
+    assert_allclose(pars["z"].error, 1, rtol=1e-2)
+
+    correlation = dataset.models.covariance.correlation
+    assert_allclose(correlation[0, 1], 0, atol=1e-7)
+    assert_allclose(correlation[0, 2], 0, atol=1e-7)
+    assert_allclose(correlation[1, 2], 0, atol=1e-7)
+
+    # Verify that the fit result models are independent of the dataset ones
+    pars["x"].value = 3
+    assert_allclose(result.parameters["x"].value, 2, rtol=1e-3)
+
+    # check parameters from the result object
+    pars = result.parameters
+    assert_allclose(pars["x"].error, 1, rtol=1e-4)
+    assert_allclose(pars["y"].error, 1, rtol=1e-4)
+    assert_allclose(pars["z"].error, 1, rtol=1e-2)
+
+
+def test_run_scale_transform_change_log():
+    dataset = MyDataset()
+    fit = Fit(backend="minuit")
+    stat_ref = dataset.stat_sum()
+    for par in dataset.models.parameters:
+        par.scale_method = "factor1"
+        par.scale_transform = "log"
+    dataset.stat_sum()
+    assert_allclose(dataset.stat_sum(), stat_ref)
+
+    result = fit.run([dataset])
+    pars = dataset.models.parameters
+    print(result)
+    assert fit._minuit is not None
+    assert result.success
+    assert result.optimize_result.method == "migrad"
+    assert result.covariance_result.method == "hesse"
+    assert result.covariance_result.success
+
+    assert_allclose(pars["x"].value, 2, rtol=1e-2)
+    assert_allclose(pars["y"].value, 3e2, rtol=1e-2)
+    assert_allclose(pars["z"].value, 4.07e-2, rtol=2e-1)
+
+    assert_allclose(pars["x"].error, 1, rtol=1e-2)
+    assert_allclose(pars["y"].error, 1, rtol=1e-2)
+    assert_allclose(pars["z"].error, 1, rtol=1e-1)
+
+    correlation = dataset.models.covariance.correlation
+    assert_allclose(correlation[0, 1], 0, atol=1e-2)
+    assert_allclose(correlation[0, 2], 0, atol=1e-1)
+    assert_allclose(correlation[1, 2], 0, atol=1e-1)
+
+    # Verify that the fit result models are independent of the dataset ones
+    pars["x"].value = 3
+    assert_allclose(result.parameters["x"].value, 2, rtol=1e-2)
+
+    # check parameters from the result object
+    pars = result.parameters
+    assert_allclose(pars["x"].error, 1, rtol=1e-2)
+    assert_allclose(pars["y"].error, 1, rtol=1e-2)
+    assert_allclose(pars["z"].error, 1, rtol=1e-1)
+
+
 def test_run_no_free_parameters():
     dataset = MyDataset()
     for par in dataset.models.parameters.free_parameters:
diff --git a/gammapy/modeling/tests/test_parameter.py b/gammapy/modeling/tests/test_parameter.py
index ba71fbb93..89a500052 100644
--- a/gammapy/modeling/tests/test_parameter.py
+++ b/gammapy/modeling/tests/test_parameter.py
@@ -58,6 +58,7 @@ def test_parameter_scale():
 
     assert par.value == 420
     assert par.min == 400
+    assert_allclose(par.scale, 10)
     assert_allclose(par.factor_min, 40)
     assert par.max == 500
     assert_allclose(par.factor_max, 50)
@@ -105,6 +106,8 @@ def test_parameter_to_dict():
         # Checks for the simpler method="factor1"
         ("factor1", 2e10, 1, 2e10),
         ("factor1", -2e10, 1, -2e10),
+        # Check no scaling
+        (None, 2e10, 2e10, 1),
     ],
 )
 def test_parameter_autoscale(method, value, factor, scale):
@@ -115,6 +118,39 @@ def test_parameter_autoscale(method, value, factor, scale):
     assert isinstance(par.scale, float)
 
 
+def test_parameter_scale_method_change():
+    value = 2e10
+    par = Parameter("", value, scale_method="scale10")
+    par.autoscale()
+    assert_allclose(par.factor, 2)
+    assert_allclose(par.scale, 1e10)
+    par.scale_method = "factor1"
+    assert par.scale_method == "factor1"
+    assert_allclose(par.factor, value)
+    assert_allclose(par.scale, 1)
+    par.autoscale()
+    assert_allclose(par.factor, 1)
+    assert_allclose(par.scale, value)
+
+
+def test_parameter_scale_transform_change():
+    value = 100
+    par = Parameter("", value, scale_method=None, scale_transform="log")
+    par.autoscale()
+    assert_allclose(par.factor, np.log(value))
+    assert_allclose(par.scale, 1)
+    par.scale_transform = "sqrt"
+    assert par.scale_transform == "sqrt"
+    assert_allclose(par.factor, value)
+    assert_allclose(par.scale, 1)
+    par.autoscale()
+    assert_allclose(par.factor, 10)
+    assert_allclose(par.scale, 1)
+
+    with pytest.raises(ValueError):
+        par.scale_transform = "invalid"
+
+
 @pytest.fixture()
 def pars():
     return Parameters(