Repository: gammapy/gammapy

Issue:
`LightCurveEstimator` gives bizarre results with `LogParabolaSpectralModel` or `ExpCutoffPowerLawSpectralModel`
### version

present dev / 0.18.1

### Issue

Using the `LightCurveEstimator` with a `LogParabolaSpectralModel` or a `ExpCutoffPowerLawSpectralModel` is giving a bizarre error:

- The unit on the `flux` column vanishes
- Has a shape of `(n,1)` and length `n` (n-number of points in the light curve) instead of the expected shape of `(1)` and length `n`

### To reproduce:
Change to a `LogParabolaSpectralModel` in the `light curve_simulation` notebook


`LightCurveEstimator` gives bizarre results with `LogParabolaSpectralModel` or `ExpCutoffPowerLawSpectralModel`
### version

present dev / 0.18.1

### Issue

Using the `LightCurveEstimator` with a `LogParabolaSpectralModel` or a `ExpCutoffPowerLawSpectralModel` is giving a bizarre error:

- The unit on the `flux` column vanishes
- Has a shape of `(n,1)` and length `n` (n-number of points in the light curve) instead of the expected shape of `(1)` and length `n`

### To reproduce:
Change to a `LogParabolaSpectralModel` in the `light curve_simulation` notebook




Discussion:

Resolving patch:
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
@@ -22,10 +22,7 @@
 def integrate_spectrum(func, energy_min, energy_max, ndecade=100):
     """Integrate 1d function using the log-log trapezoidal rule.
 
-    If scalar values for xmin and xmax are passed an oversampled grid is generated using the
-    ``ndecade`` keyword argument. If xmin and xmax arrays are passed, no
-    oversampling is performed and the integral is computed in the provided
-    grid.
+    Internally an oversampling of the energy bins to "ndecade" is used.
 
     Parameters
     ----------
@@ -37,19 +34,12 @@ def integrate_spectrum(func, energy_min, energy_max, ndecade=100):
         Integration range minimum
     ndecade : int, optional
         Number of grid points per decade used for the integration.
-        Default : 100.
+        Default : 100
     """
-    edges = edges_from_lo_hi(energy_min, energy_max)
-    energy_axis = MapAxis.from_energy_edges(energy_edges=edges)
-
-    factor = np.ceil(ndecade / energy_axis.nbin_per_decade)
-    energy_axis_upsampled = energy_axis.upsample(factor=factor)
-
-    values = func(energy_axis_upsampled.edges)
-    integral = trapz_loglog(values, energy_axis_upsampled.edges)
-
-    indices = energy_axis_upsampled.coord_to_idx(energy_axis.edges[:-1])
-    return np.add.reduceat(integral, indices)
+    num = np.max(ndecade * np.log10(energy_max / energy_min))
+    energy = np.geomspace(energy_min, energy_max, num=int(num), axis=-1)
+    integral = trapz_loglog(func(energy), energy, axis=-1)
+    return integral.sum(axis=0)
 
 
 class SpectralModel(Model):


Test patch:
diff --git a/gammapy/astro/darkmatter/tests/test_spectra.py b/gammapy/astro/darkmatter/tests/test_spectra.py
--- a/gammapy/astro/darkmatter/tests/test_spectra.py
+++ b/gammapy/astro/darkmatter/tests/test_spectra.py
@@ -32,5 +32,5 @@ def test_DMAnnihilation():
     )
     differential_flux = model.evaluate(energy=1 * u.TeV, scale=1).to("cm-2 s-1 TeV-1")
 
-    assert_quantity_allclose(integral_flux.value, 6.19575457e-14, rtol=1e-5)
-    assert_quantity_allclose(differential_flux.value, 2.97506768e-16, rtol=1e-5)
+    assert_quantity_allclose(integral_flux.value, 6.19575457e-14, rtol=1e-3)
+    assert_quantity_allclose(differential_flux.value, 2.97506768e-16, rtol=1e-3)
diff --git a/gammapy/estimators/tests/test_flux_point_estimator.py b/gammapy/estimators/tests/test_flux_point_estimator.py
--- a/gammapy/estimators/tests/test_flux_point_estimator.py
+++ b/gammapy/estimators/tests/test_flux_point_estimator.py
@@ -147,128 +147,182 @@ def fpe_ecpl():
     return create_fpe(ExpCutoffPowerLawSpectralModel(lambda_="1 TeV-1"))
 
 
-class TestFluxPointsEstimator:
-    @staticmethod
-    def test_str(fpe_pwl):
-        datasets, fpe = fpe_pwl
-        assert "FluxPointsEstimator" in str(fpe)
+def test_str(fpe_pwl):
+    datasets, fpe = fpe_pwl
+    assert "FluxPointsEstimator" in str(fpe)
 
-    @staticmethod
-    @requires_dependency("iminuit")
-    def test_run_pwl(fpe_pwl):
-        datasets, fpe = fpe_pwl
 
-        fp = fpe.run(datasets)
+@requires_dependency("iminuit")
+def test_run_pwl(fpe_pwl):
+    datasets, fpe = fpe_pwl
 
-        actual = fp.table["e_min"].data
-        assert_allclose(actual, [0.316228, 1.0, 10.0], rtol=1e-5)
+    fp = fpe.run(datasets)
 
-        actual = fp.table["e_max"].data
-        assert_allclose(actual, [1.0, 10.0, 31.622777], rtol=1e-5)
+    actual = fp.table["e_min"].data
+    assert_allclose(actual, [0.316228, 1.0, 10.0], rtol=1e-5)
 
-        actual = fp.table["e_ref"].data
-        assert_allclose(actual, [0.562341, 3.162278, 17.782794], rtol=1e-3)
+    actual = fp.table["e_max"].data
+    assert_allclose(actual, [1.0, 10.0, 31.622777], rtol=1e-5)
 
-        actual = fp.table["norm"].data
-        assert_allclose(actual, [1.081434, 0.91077, 0.922176], rtol=1e-3)
+    actual = fp.table["e_ref"].data
+    assert_allclose(actual, [0.562341, 3.162278, 17.782794], rtol=1e-3)
 
-        actual = fp.table["norm_err"].data
-        assert_allclose(actual, [0.066374, 0.061025, 0.179729], rtol=1e-2)
+    actual = fp.table["ref_flux"].quantity
+    desired = [2.162278e-12, 9.000000e-13, 6.837722e-14] * u.Unit("1 / (cm2 s)")
+    assert_allclose(actual, desired, rtol=1e-3)
 
-        actual = fp.table["norm_errn"].data
-        assert_allclose(actual, [0.065803, 0.060403, 0.171376], rtol=1e-2)
+    actual = fp.table["ref_dnde"].quantity
+    desired = [3.162278e-12, 1.000000e-13, 3.162278e-15] * u.Unit("1 / (cm2 s TeV)")
+    assert_allclose(actual, desired, rtol=1e-3)
 
-        actual = fp.table["norm_errp"].data
-        assert_allclose(actual, [0.06695, 0.061652, 0.18839], rtol=1e-2)
+    actual = fp.table["ref_e2dnde"].quantity
+    assert_allclose(actual, 1e-12 * u.Unit("TeV / (cm2 s)"), rtol=1e-3)
 
-        actual = fp.table["counts"].data.squeeze()
-        assert_allclose(actual, [1490, 748, 43])
+    actual = fp.table["ref_eflux"].quantity
+    desired = [1.151293e-12, 2.302585e-12, 1.151293e-12] * u.Unit("TeV / (cm2 s)")
+    assert_allclose(actual, desired, rtol=1e-3)
 
-        actual = fp.table["norm_ul"].data
-        assert_allclose(actual, [1.216227, 1.035472, 1.316878], rtol=1e-2)
+    actual = fp.table["norm"].data
+    assert_allclose(actual, [1.081434, 0.91077, 0.922176], rtol=1e-3)
 
-        actual = fp.table["sqrt_ts"].data
-        assert_allclose(actual, [18.568429, 18.054651, 7.057121], rtol=1e-2)
+    actual = fp.table["norm_err"].data
+    assert_allclose(actual, [0.066374, 0.061025, 0.179729], rtol=1e-2)
 
-        actual = fp.table["norm_scan"][0][[0, 5, -1]]
-        assert_allclose(actual, [0.2, 1, 5])
+    actual = fp.table["norm_errn"].data
+    assert_allclose(actual, [0.065803, 0.060403, 0.171376], rtol=1e-2)
 
-        actual = fp.table["stat_scan"][0][[0, 5, -1]]
-        assert_allclose(actual, [220.368653, 4.301011, 1881.626454], rtol=1e-2)
+    actual = fp.table["norm_errp"].data
+    assert_allclose(actual, [0.06695, 0.061652, 0.18839], rtol=1e-2)
 
-    @staticmethod
-    @requires_dependency("iminuit")
-    @requires_data()
-    def test_run_map_pwl(fpe_map_pwl):
-        datasets, fpe = fpe_map_pwl
-        fp = fpe.run(datasets)
+    actual = fp.table["counts"].data.squeeze()
+    assert_allclose(actual, [1490, 748, 43])
 
-        actual = fp.table["e_min"].data
-        assert_allclose(actual, [0.1, 1.178769, 8.48342], rtol=1e-5)
+    actual = fp.table["norm_ul"].data
+    assert_allclose(actual, [1.216227, 1.035472, 1.316878], rtol=1e-2)
 
-        actual = fp.table["e_max"].data
-        assert_allclose(actual, [1.178769, 8.483429, 100.0], rtol=1e-5)
+    actual = fp.table["sqrt_ts"].data
+    assert_allclose(actual, [18.568429, 18.054651, 7.057121], rtol=1e-2)
 
-        actual = fp.table["e_ref"].data
-        assert_allclose(actual, [0.343332, 3.162278, 29.126327], rtol=1e-5)
+    actual = fp.table["norm_scan"][0][[0, 5, -1]]
+    assert_allclose(actual, [0.2, 1, 5])
 
-        actual = fp.table["norm"].data
-        assert_allclose(actual, [0.974726, 0.96342, 0.994251], rtol=1e-2)
+    actual = fp.table["stat_scan"][0][[0, 5, -1]]
+    assert_allclose(actual, [220.368653, 4.301011, 1881.626454], rtol=1e-2)
 
-        actual = fp.table["norm_err"].data
-        assert_allclose(actual, [0.067637, 0.052022, 0.087059], rtol=3e-2)
 
-        actual = fp.table["counts"].data
-        assert_allclose(actual, [[44611, 0], [1923, 0], [282, 0]])
+@requires_dependency("iminuit")
+def test_run_ecpl(fpe_ecpl):
+    datasets, fpe = fpe_ecpl
 
-        actual = fp.table["norm_ul"].data
-        assert_allclose(actual, [1.111852, 1.07004, 1.17829], rtol=1e-2)
+    fp = fpe.run(datasets)
 
-        actual = fp.table["sqrt_ts"].data
-        assert_allclose(actual, [16.681221, 28.408676, 21.91912], rtol=1e-2)
+    actual = fp.table["ref_flux"].quantity
+    desired = [9.024362e-13, 1.781341e-13, 1.260298e-18] * u.Unit("1 / (cm2 s)")
+    assert_allclose(actual, desired, rtol=1e-3)
 
-        actual = fp.table["norm_scan"][0]
-        assert_allclose(actual, [0.2, 1, 5])
+    actual = fp.table["ref_dnde"].quantity
+    desired = [1.351382e-12, 7.527318e-15, 2.523659e-22] * u.Unit("1 / (cm2 s TeV)")
+    assert_allclose(actual, desired, rtol=1e-3)
 
-        actual = fp.table["stat_scan"][0] - fp.table["stat"][0]
-        assert_allclose(actual, [1.628530e02, 1.436323e-01, 2.007461e03], rtol=1e-2)
+    actual = fp.table["ref_e2dnde"].quantity
+    desired = [4.273446e-13, 7.527318e-14, 7.980510e-20] * u.Unit("TeV / (cm2 s)")
+    assert_allclose(actual, desired, rtol=1e-3)
 
-    @staticmethod
-    @requires_dependency("iminuit")
-    @requires_data()
-    def test_run_map_pwl_reoptimize(fpe_map_pwl_reoptimize):
-        datasets, fpe = fpe_map_pwl_reoptimize
-        fpe = fpe.copy()
-        fpe.selection = ["scan"]
+    actual = fp.table["ref_eflux"].quantity
+    desired = [4.770557e-13, 2.787695e-13, 1.371963e-17] * u.Unit("TeV / (cm2 s)")
+    assert_allclose(actual, desired, rtol=1e-3)
 
-        fp = fpe.run(datasets)
+    actual = fp.table["norm"].data
+    assert_allclose(actual, [1.001683, 1.061821, 1.237512e+03], rtol=1e-3)
 
-        actual = fp.table["norm"].data
-        assert_allclose(actual, 0.962368, rtol=1e-2)
+    actual = fp.table["norm_err"].data
+    assert_allclose(actual, [1.386091e-01, 2.394241e-01, 3.259756e+03], rtol=1e-2)
 
-        actual = fp.table["norm_err"].data
-        assert_allclose(actual, 0.051955, rtol=1e-2)
+    actual = fp.table["norm_errn"].data
+    assert_allclose(actual, [1.374962e-01, 2.361246e-01, 2.888978e+03], rtol=1e-2)
 
-        actual = fp.table["sqrt_ts"].data
-        assert_allclose(actual, 28.408426, rtol=1e-2)
+    actual = fp.table["norm_errp"].data
+    assert_allclose(actual, [1.397358e-01, 2.428481e-01, 3.716550e+03], rtol=1e-2)
 
-        actual = fp.table["norm_scan"][0]
-        assert_allclose(actual, 1)
+    actual = fp.table["norm_ul"].data
+    assert_allclose(actual, [1.283433e+00, 1.555117e+00, 9.698645e+03], rtol=1e-2)
 
-        actual = fp.table["stat_scan"][0] - fp.table["stat"][0]
-        assert_allclose(actual, 0.489359, rtol=1e-2)
+    actual = fp.table["sqrt_ts"].data
+    assert_allclose(actual, [7.678454, 4.735691, 0.399243], rtol=1e-2)
 
-    @staticmethod
-    @requires_dependency("iminuit")
-    @requires_data()
-    def test_flux_points_estimator_no_norm_scan(fpe_pwl):
-        datasets, fpe = fpe_pwl
-        fpe.selection_optional = None
 
-        fp = fpe.run(datasets)
+@requires_dependency("iminuit")
+@requires_data()
+def test_run_map_pwl(fpe_map_pwl):
+    datasets, fpe = fpe_map_pwl
+    fp = fpe.run(datasets)
 
-        assert fp.sed_type == "dnde"
-        assert "norm_scan" not in fp.table.colnames
+    actual = fp.table["e_min"].data
+    assert_allclose(actual, [0.1, 1.178769, 8.48342], rtol=1e-5)
+
+    actual = fp.table["e_max"].data
+    assert_allclose(actual, [1.178769, 8.483429, 100.0], rtol=1e-5)
+
+    actual = fp.table["e_ref"].data
+    assert_allclose(actual, [0.343332, 3.162278, 29.126327], rtol=1e-5)
+
+    actual = fp.table["norm"].data
+    assert_allclose(actual, [0.974726, 0.96342, 0.994251], rtol=1e-2)
+
+    actual = fp.table["norm_err"].data
+    assert_allclose(actual, [0.067637, 0.052022, 0.087059], rtol=3e-2)
+
+    actual = fp.table["counts"].data
+    assert_allclose(actual, [[44611, 0], [1923, 0], [282, 0]])
+
+    actual = fp.table["norm_ul"].data
+    assert_allclose(actual, [1.111852, 1.07004, 1.17829], rtol=1e-2)
+
+    actual = fp.table["sqrt_ts"].data
+    assert_allclose(actual, [16.681221, 28.408676, 21.91912], rtol=1e-2)
+
+    actual = fp.table["norm_scan"][0]
+    assert_allclose(actual, [0.2, 1, 5])
+
+    actual = fp.table["stat_scan"][0] - fp.table["stat"][0]
+    assert_allclose(actual, [1.628530e02, 1.436323e-01, 2.007461e03], rtol=1e-2)
+
+
+@requires_dependency("iminuit")
+@requires_data()
+def test_run_map_pwl_reoptimize(fpe_map_pwl_reoptimize):
+    datasets, fpe = fpe_map_pwl_reoptimize
+    fpe = fpe.copy()
+    fpe.selection = ["scan"]
+
+    fp = fpe.run(datasets)
+
+    actual = fp.table["norm"].data
+    assert_allclose(actual, 0.962368, rtol=1e-2)
+
+    actual = fp.table["norm_err"].data
+    assert_allclose(actual, 0.051955, rtol=1e-2)
+
+    actual = fp.table["sqrt_ts"].data
+    assert_allclose(actual, 28.408426, rtol=1e-2)
+
+    actual = fp.table["norm_scan"][0]
+    assert_allclose(actual, 1)
+
+    actual = fp.table["stat_scan"][0] - fp.table["stat"][0]
+    assert_allclose(actual, 0.489359, rtol=1e-2)
+
+
+@requires_dependency("iminuit")
+@requires_data()
+def test_flux_points_estimator_no_norm_scan(fpe_pwl):
+    datasets, fpe = fpe_pwl
+    fpe.selection_optional = None
+
+    fp = fpe.run(datasets)
+
+    assert fp.sed_type == "dnde"
+    assert "norm_scan" not in fp.table.colnames
 
 
 def test_no_likelihood_contribution():
diff --git a/gammapy/modeling/models/tests/test_spectral.py b/gammapy/modeling/models/tests/test_spectral.py
--- a/gammapy/modeling/models/tests/test_spectral.py
+++ b/gammapy/modeling/models/tests/test_spectral.py
@@ -279,8 +279,8 @@ def table_model():
             energy=[1, 3, 7, 10] * u.TeV, norms=[1, 5, 3, 0.5] * u.Unit(""),
         ),
         val_at_2TeV=u.Quantity(2.76058404, ""),
-        integral_1_10TeV=u.Quantity(24.757573885411876, "TeV"),
-        eflux_1_10TeV=u.Quantity(117.74087966682515, "TeV2"),
+        integral_1_10TeV=u.Quantity(24.758255, "TeV"),
+        eflux_1_10TeV=u.Quantity(117.745068, "TeV2"),
     ),
 ]
 
@@ -492,7 +492,8 @@ def test_ecpl_integrate():
     # regression test to check the numerical integration for small energy bins
     ecpl = ExpCutoffPowerLawSpectralModel()
     value = ecpl.integral(1 * u.TeV, 1.1 * u.TeV)
-    assert_quantity_allclose(value, 8.380761e-14 * u.Unit("s-1 cm-2"))
+    assert value.isscalar
+    assert_quantity_allclose(value, 8.380714e-14 * u.Unit("s-1 cm-2"))
 
 
 def test_pwl_pivot_energy():
@@ -638,8 +639,8 @@ def test_synchrotron(self):
         model = NaimaSpectralModel(radiative_model)
 
         val_at_2TeV = 1.0565840392550432e-24 * u.Unit("cm-2 s-1 TeV-1")
-        integral_1_10TeV = 4.449303e-13 * u.Unit("cm-2 s-1")
-        eflux_1_10TeV = 4.594242e-13 * u.Unit("TeV cm-2 s-1")
+        integral_1_10TeV = 4.449186e-13 * u.Unit("cm-2 s-1")
+        eflux_1_10TeV = 4.594121e-13 * u.Unit("TeV cm-2 s-1")
 
         value = model(self.energy)
         assert_quantity_allclose(value, val_at_2TeV)