We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Log scale and grids
This issue is triggered by https://github.com/gammapy/gammapy/issues/2270#issue-464031097 - there's a test fail with Numpy dev - the issue is that we are making an `nbins` float here:

https://github.com/gammapy/gammapy/blob/7a19c4b3b3e82831c6c683db9799a1d803cf928f/gammapy/utils/energy.py#L34

@adonath - Could you please fix this? I'm not sure if the proper fix is to round up or down to the nearest int.

There's other options as well, e.g. we could just remove the `per_decade` argument or that helper function completely and use this:
https://github.com/gammapy/gammapy/blob/76ab409e6bee09436127ce67e11de1ad5b2d7bb0/gammapy/maps/geom.py#L479

Or introduce an equivalent of `numpy.arange` - Numpy offered a separate function for the case where the caller wants a stepsize instead of a given number of points - maybe we should do that as well to be symmetric, and to keep support for setting up grids of a desired step size easily.

Finally, there's the option to attache the grid-making functionality to the existing scale classes here and to use those throughout Gammapy:
https://github.com/gammapy/gammapy/blob/5a24919d91a9aa601f7a1ee3d677cf4bebfc6b3b/gammapy/utils/interpolation.py#L138

I think https://observablehq.com/@d3/scale-ticks and also matplotlib have the feature to make grids or ticks attached to the scales. Here in Astropy this wasn't needed/added so far http://docs.astropy.org/en/stable/visualization/normalization.html

In principle having energy-specific scale and grid functions for log isn't needed or good design,  on the other hand it's the only case (currently, but later?) where we need a log scale, so just fixing up the existing helper function is not too bad.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `gammapy/utils/nddata.py`
Content:
```
1 # Licensed under a 3-clause BSD style license - see LICENSE.rst
2 """Utility functions and classes for n-dimensional data and axes."""
3 import numpy as np
4 from astropy.units import Quantity
5 from .array import array_stats_str
6 from .interpolation import ScaledRegularGridInterpolator
7 
8 __all__ = ["NDDataArray", "sqrt_space"]
9 
10 
11 class NDDataArray:
12     """ND Data Array Base class
13 
14     Parameters
15     ----------
16     axes : list
17         List of `~gammapy.utils.nddata.DataAxis`
18     data : `~astropy.units.Quantity`
19         Data
20     meta : dict
21         Meta info
22     interp_kwargs : dict
23         TODO
24     """
25 
26     default_interp_kwargs = dict(bounds_error=False, values_scale="lin")
27     """Default interpolation kwargs used to initialize the
28     `scipy.interpolate.RegularGridInterpolator`.  The interpolation behaviour
29     of an individual axis ('log', 'linear') can be passed to the axis on
30     initialization."""
31 
32     def __init__(self, axes, data=None, meta=None, interp_kwargs=None):
33         self._axes = axes
34         if data is not None:
35             self.data = data
36         self.meta = meta or {}
37         self.interp_kwargs = interp_kwargs or self.default_interp_kwargs
38 
39         self._regular_grid_interp = None
40 
41     def __str__(self):
42         ss = "NDDataArray summary info\n"
43         for axis in self.axes:
44             ss += str(axis)
45         ss += array_stats_str(self.data, "Data")
46         return ss
47 
48     @property
49     def axes(self):
50         """Array holding the axes in correct order"""
51         return self._axes
52 
53     def axis(self, name):
54         """Return axis by name"""
55         try:
56             idx = [_.name for _ in self.axes].index(name)
57         except ValueError:
58             raise ValueError(f"Axis {name} not found")
59         return self.axes[idx]
60 
61     @property
62     def data(self):
63         """Array holding the n-dimensional data."""
64         return self._data
65 
66     @data.setter
67     def data(self, data):
68         """Set data.
69 
70         Some sanity checks are performed to avoid an invalid array.
71         Also, the interpolator is set to None to avoid unwanted behaviour.
72 
73         Parameters
74         ----------
75         data : `~astropy.units.Quantity`, array-like
76             Data array
77         """
78         data = Quantity(data)
79         dimension = len(data.shape)
80         if dimension != self.dim:
81             raise ValueError(
82                 "Overall dimensions to not match. "
83                 "Data: {}, Hist: {}".format(dimension, self.dim)
84             )
85 
86         for dim in np.arange(self.dim):
87             axis = self.axes[dim]
88             if axis.nbin != data.shape[dim]:
89                 msg = "Data shape does not match in dimension {d}\n"
90                 msg += "Axis {n} : {sa}, Data {sd}"
91                 raise ValueError(
92                     msg.format(d=dim, n=axis.name, sa=axis.nbin, sd=data.shape[dim])
93                 )
94         self._regular_grid_interp = None
95         self._data = data
96 
97     @property
98     def dim(self):
99         """Dimension (number of axes)"""
100         return len(self.axes)
101 
102     def evaluate(self, method=None, **kwargs):
103         """Evaluate NDData Array
104 
105         This function provides a uniform interface to several interpolators.
106         The evaluation nodes are given as ``kwargs``.
107 
108         Currently available:
109         `~scipy.interpolate.RegularGridInterpolator`, methods: linear, nearest
110 
111         Parameters
112         ----------
113         method : str {'linear', 'nearest'}, optional
114             Interpolation method
115         kwargs : dict
116             Keys are the axis names, Values the evaluation points
117 
118         Returns
119         -------
120         array : `~astropy.units.Quantity`
121             Interpolated values, axis order is the same as for the NDData array
122         """
123         values = []
124         for idx, axis in enumerate(self.axes):
125             # Extract values for each axis, default: nodes
126             shape = [1] * len(self.axes)
127             shape[idx] = -1
128             default = axis.center.reshape(tuple(shape))
129             temp = Quantity(kwargs.pop(axis.name, default))
130             values.append(np.atleast_1d(temp))
131 
132         # This is to catch e.g. typos in axis names
133         if kwargs != {}:
134             raise ValueError(f"Input given for unknown axis: {kwargs}")
135 
136         if self._regular_grid_interp is None:
137             self._add_regular_grid_interp()
138 
139         return self._regular_grid_interp(values, method=method, **kwargs)
140 
141     def _add_regular_grid_interp(self, interp_kwargs=None):
142         """Add `~scipy.interpolate.RegularGridInterpolator`
143 
144         http://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.interpolate.RegularGridInterpolator.html
145 
146         Parameters
147         ----------
148         interp_kwargs : dict, optional
149             Interpolation kwargs
150         """
151         if interp_kwargs is None:
152             interp_kwargs = self.interp_kwargs
153 
154         points = [a.center for a in self.axes]
155         points_scale = [a.interp for a in self.axes]
156         self._regular_grid_interp = ScaledRegularGridInterpolator(
157             points, self.data, points_scale=points_scale, **interp_kwargs
158         )
159 
160 
161 def sqrt_space(start, stop, num):
162     """Return numbers spaced evenly on a square root scale.
163 
164     This function is similar to `numpy.linspace` and `numpy.logspace`.
165 
166     Parameters
167     ----------
168     start : float
169         start is the starting value of the sequence
170     stop : float
171         stop is the final value of the sequence
172     num : int
173         Number of samples to generate.
174 
175     Returns
176     -------
177     samples : `~numpy.ndarray`
178         1D array with a square root scale
179 
180     Examples
181     --------
182     >>> from gammapy.utils.nddata import sqrt_space
183     >>> sqrt_space(0, 2, 5)
184     array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ])
185     """
186     return np.sqrt(np.linspace(start ** 2, stop ** 2, num))
187
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/gammapy/utils/nddata.py b/gammapy/utils/nddata.py
--- a/gammapy/utils/nddata.py
+++ b/gammapy/utils/nddata.py
@@ -5,7 +5,7 @@
 from .array import array_stats_str
 from .interpolation import ScaledRegularGridInterpolator
 
-__all__ = ["NDDataArray", "sqrt_space"]
+__all__ = ["NDDataArray"]
 
 
 class NDDataArray:
@@ -156,31 +156,3 @@
         self._regular_grid_interp = ScaledRegularGridInterpolator(
             points, self.data, points_scale=points_scale, **interp_kwargs
         )
-
-
-def sqrt_space(start, stop, num):
-    """Return numbers spaced evenly on a square root scale.
-
-    This function is similar to `numpy.linspace` and `numpy.logspace`.
-
-    Parameters
-    ----------
-    start : float
-        start is the starting value of the sequence
-    stop : float
-        stop is the final value of the sequence
-    num : int
-        Number of samples to generate.
-
-    Returns
-    -------
-    samples : `~numpy.ndarray`
-        1D array with a square root scale
-
-    Examples
-    --------
-    >>> from gammapy.utils.nddata import sqrt_space
-    >>> sqrt_space(0, 2, 5)
-    array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ])
-    """
-    return np.sqrt(np.linspace(start ** 2, stop ** 2, num))

{"golden_diff": "diff --git a/gammapy/utils/nddata.py b/gammapy/utils/nddata.py\n--- a/gammapy/utils/nddata.py\n+++ b/gammapy/utils/nddata.py\n@@ -5,7 +5,7 @@\n from .array import array_stats_str\n from .interpolation import ScaledRegularGridInterpolator\n \n-__all__ = [\"NDDataArray\", \"sqrt_space\"]\n+__all__ = [\"NDDataArray\"]\n \n \n class NDDataArray:\n@@ -156,31 +156,3 @@\n         self._regular_grid_interp = ScaledRegularGridInterpolator(\n             points, self.data, points_scale=points_scale, **interp_kwargs\n         )\n-\n-\n-def sqrt_space(start, stop, num):\n-    \"\"\"Return numbers spaced evenly on a square root scale.\n-\n-    This function is similar to `numpy.linspace` and `numpy.logspace`.\n-\n-    Parameters\n-    ----------\n-    start : float\n-        start is the starting value of the sequence\n-    stop : float\n-        stop is the final value of the sequence\n-    num : int\n-        Number of samples to generate.\n-\n-    Returns\n-    -------\n-    samples : `~numpy.ndarray`\n-        1D array with a square root scale\n-\n-    Examples\n-    --------\n-    >>> from gammapy.utils.nddata import sqrt_space\n-    >>> sqrt_space(0, 2, 5)\n-    array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ])\n-    \"\"\"\n-    return np.sqrt(np.linspace(start ** 2, stop ** 2, num))\n", "issue": "Log scale and grids\nThis issue is triggered by https://github.com/gammapy/gammapy/issues/2270#issue-464031097 - there's a test fail with Numpy dev - the issue is that we are making an `nbins` float here:\r\n\r\nhttps://github.com/gammapy/gammapy/blob/7a19c4b3b3e82831c6c683db9799a1d803cf928f/gammapy/utils/energy.py#L34\r\n\r\n@adonath - Could you please fix this? I'm not sure if the proper fix is to round up or down to the nearest int.\r\n\r\nThere's other options as well, e.g. we could just remove the `per_decade` argument or that helper function completely and use this:\r\nhttps://github.com/gammapy/gammapy/blob/76ab409e6bee09436127ce67e11de1ad5b2d7bb0/gammapy/maps/geom.py#L479\r\n\r\nOr introduce an equivalent of `numpy.arange` - Numpy offered a separate function for the case where the caller wants a stepsize instead of a given number of points - maybe we should do that as well to be symmetric, and to keep support for setting up grids of a desired step size easily.\r\n\r\nFinally, there's the option to attache the grid-making functionality to the existing scale classes here and to use those throughout Gammapy:\r\nhttps://github.com/gammapy/gammapy/blob/5a24919d91a9aa601f7a1ee3d677cf4bebfc6b3b/gammapy/utils/interpolation.py#L138\r\n\r\nI think https://observablehq.com/@d3/scale-ticks and also matplotlib have the feature to make grids or ticks attached to the scales. Here in Astropy this wasn't needed/added so far http://docs.astropy.org/en/stable/visualization/normalization.html\r\n\r\nIn principle having energy-specific scale and grid functions for log isn't needed or good design,  on the other hand it's the only case (currently, but later?) where we need a log scale, so just fixing up the existing helper function is not too bad.\n", "before_files": [{"content": "# Licensed under a 3-clause BSD style license - see LICENSE.rst\n\"\"\"Utility functions and classes for n-dimensional data and axes.\"\"\"\nimport numpy as np\nfrom astropy.units import Quantity\nfrom .array import array_stats_str\nfrom .interpolation import ScaledRegularGridInterpolator\n\n__all__ = [\"NDDataArray\", \"sqrt_space\"]\n\n\nclass NDDataArray:\n    \"\"\"ND Data Array Base class\n\n    Parameters\n    ----------\n    axes : list\n        List of `~gammapy.utils.nddata.DataAxis`\n    data : `~astropy.units.Quantity`\n        Data\n    meta : dict\n        Meta info\n    interp_kwargs : dict\n        TODO\n    \"\"\"\n\n    default_interp_kwargs = dict(bounds_error=False, values_scale=\"lin\")\n    \"\"\"Default interpolation kwargs used to initialize the\n    `scipy.interpolate.RegularGridInterpolator`.  The interpolation behaviour\n    of an individual axis ('log', 'linear') can be passed to the axis on\n    initialization.\"\"\"\n\n    def __init__(self, axes, data=None, meta=None, interp_kwargs=None):\n        self._axes = axes\n        if data is not None:\n            self.data = data\n        self.meta = meta or {}\n        self.interp_kwargs = interp_kwargs or self.default_interp_kwargs\n\n        self._regular_grid_interp = None\n\n    def __str__(self):\n        ss = \"NDDataArray summary info\\n\"\n        for axis in self.axes:\n            ss += str(axis)\n        ss += array_stats_str(self.data, \"Data\")\n        return ss\n\n    @property\n    def axes(self):\n        \"\"\"Array holding the axes in correct order\"\"\"\n        return self._axes\n\n    def axis(self, name):\n        \"\"\"Return axis by name\"\"\"\n        try:\n            idx = [_.name for _ in self.axes].index(name)\n        except ValueError:\n            raise ValueError(f\"Axis {name} not found\")\n        return self.axes[idx]\n\n    @property\n    def data(self):\n        \"\"\"Array holding the n-dimensional data.\"\"\"\n        return self._data\n\n    @data.setter\n    def data(self, data):\n        \"\"\"Set data.\n\n        Some sanity checks are performed to avoid an invalid array.\n        Also, the interpolator is set to None to avoid unwanted behaviour.\n\n        Parameters\n        ----------\n        data : `~astropy.units.Quantity`, array-like\n            Data array\n        \"\"\"\n        data = Quantity(data)\n        dimension = len(data.shape)\n        if dimension != self.dim:\n            raise ValueError(\n                \"Overall dimensions to not match. \"\n                \"Data: {}, Hist: {}\".format(dimension, self.dim)\n            )\n\n        for dim in np.arange(self.dim):\n            axis = self.axes[dim]\n            if axis.nbin != data.shape[dim]:\n                msg = \"Data shape does not match in dimension {d}\\n\"\n                msg += \"Axis {n} : {sa}, Data {sd}\"\n                raise ValueError(\n                    msg.format(d=dim, n=axis.name, sa=axis.nbin, sd=data.shape[dim])\n                )\n        self._regular_grid_interp = None\n        self._data = data\n\n    @property\n    def dim(self):\n        \"\"\"Dimension (number of axes)\"\"\"\n        return len(self.axes)\n\n    def evaluate(self, method=None, **kwargs):\n        \"\"\"Evaluate NDData Array\n\n        This function provides a uniform interface to several interpolators.\n        The evaluation nodes are given as ``kwargs``.\n\n        Currently available:\n        `~scipy.interpolate.RegularGridInterpolator`, methods: linear, nearest\n\n        Parameters\n        ----------\n        method : str {'linear', 'nearest'}, optional\n            Interpolation method\n        kwargs : dict\n            Keys are the axis names, Values the evaluation points\n\n        Returns\n        -------\n        array : `~astropy.units.Quantity`\n            Interpolated values, axis order is the same as for the NDData array\n        \"\"\"\n        values = []\n        for idx, axis in enumerate(self.axes):\n            # Extract values for each axis, default: nodes\n            shape = [1] * len(self.axes)\n            shape[idx] = -1\n            default = axis.center.reshape(tuple(shape))\n            temp = Quantity(kwargs.pop(axis.name, default))\n            values.append(np.atleast_1d(temp))\n\n        # This is to catch e.g. typos in axis names\n        if kwargs != {}:\n            raise ValueError(f\"Input given for unknown axis: {kwargs}\")\n\n        if self._regular_grid_interp is None:\n            self._add_regular_grid_interp()\n\n        return self._regular_grid_interp(values, method=method, **kwargs)\n\n    def _add_regular_grid_interp(self, interp_kwargs=None):\n        \"\"\"Add `~scipy.interpolate.RegularGridInterpolator`\n\n        http://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.interpolate.RegularGridInterpolator.html\n\n        Parameters\n        ----------\n        interp_kwargs : dict, optional\n            Interpolation kwargs\n        \"\"\"\n        if interp_kwargs is None:\n            interp_kwargs = self.interp_kwargs\n\n        points = [a.center for a in self.axes]\n        points_scale = [a.interp for a in self.axes]\n        self._regular_grid_interp = ScaledRegularGridInterpolator(\n            points, self.data, points_scale=points_scale, **interp_kwargs\n        )\n\n\ndef sqrt_space(start, stop, num):\n    \"\"\"Return numbers spaced evenly on a square root scale.\n\n    This function is similar to `numpy.linspace` and `numpy.logspace`.\n\n    Parameters\n    ----------\n    start : float\n        start is the starting value of the sequence\n    stop : float\n        stop is the final value of the sequence\n    num : int\n        Number of samples to generate.\n\n    Returns\n    -------\n    samples : `~numpy.ndarray`\n        1D array with a square root scale\n\n    Examples\n    --------\n    >>> from gammapy.utils.nddata import sqrt_space\n    >>> sqrt_space(0, 2, 5)\n    array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ])\n    \"\"\"\n    return np.sqrt(np.linspace(start ** 2, stop ** 2, num))\n", "path": "gammapy/utils/nddata.py"}], "after_files": [{"content": "# Licensed under a 3-clause BSD style license - see LICENSE.rst\n\"\"\"Utility functions and classes for n-dimensional data and axes.\"\"\"\nimport numpy as np\nfrom astropy.units import Quantity\nfrom .array import array_stats_str\nfrom .interpolation import ScaledRegularGridInterpolator\n\n__all__ = [\"NDDataArray\"]\n\n\nclass NDDataArray:\n    \"\"\"ND Data Array Base class\n\n    Parameters\n    ----------\n    axes : list\n        List of `~gammapy.utils.nddata.DataAxis`\n    data : `~astropy.units.Quantity`\n        Data\n    meta : dict\n        Meta info\n    interp_kwargs : dict\n        TODO\n    \"\"\"\n\n    default_interp_kwargs = dict(bounds_error=False, values_scale=\"lin\")\n    \"\"\"Default interpolation kwargs used to initialize the\n    `scipy.interpolate.RegularGridInterpolator`.  The interpolation behaviour\n    of an individual axis ('log', 'linear') can be passed to the axis on\n    initialization.\"\"\"\n\n    def __init__(self, axes, data=None, meta=None, interp_kwargs=None):\n        self._axes = axes\n        if data is not None:\n            self.data = data\n        self.meta = meta or {}\n        self.interp_kwargs = interp_kwargs or self.default_interp_kwargs\n\n        self._regular_grid_interp = None\n\n    def __str__(self):\n        ss = \"NDDataArray summary info\\n\"\n        for axis in self.axes:\n            ss += str(axis)\n        ss += array_stats_str(self.data, \"Data\")\n        return ss\n\n    @property\n    def axes(self):\n        \"\"\"Array holding the axes in correct order\"\"\"\n        return self._axes\n\n    def axis(self, name):\n        \"\"\"Return axis by name\"\"\"\n        try:\n            idx = [_.name for _ in self.axes].index(name)\n        except ValueError:\n            raise ValueError(f\"Axis {name} not found\")\n        return self.axes[idx]\n\n    @property\n    def data(self):\n        \"\"\"Array holding the n-dimensional data.\"\"\"\n        return self._data\n\n    @data.setter\n    def data(self, data):\n        \"\"\"Set data.\n\n        Some sanity checks are performed to avoid an invalid array.\n        Also, the interpolator is set to None to avoid unwanted behaviour.\n\n        Parameters\n        ----------\n        data : `~astropy.units.Quantity`, array-like\n            Data array\n        \"\"\"\n        data = Quantity(data)\n        dimension = len(data.shape)\n        if dimension != self.dim:\n            raise ValueError(\n                \"Overall dimensions to not match. \"\n                \"Data: {}, Hist: {}\".format(dimension, self.dim)\n            )\n\n        for dim in np.arange(self.dim):\n            axis = self.axes[dim]\n            if axis.nbin != data.shape[dim]:\n                msg = \"Data shape does not match in dimension {d}\\n\"\n                msg += \"Axis {n} : {sa}, Data {sd}\"\n                raise ValueError(\n                    msg.format(d=dim, n=axis.name, sa=axis.nbin, sd=data.shape[dim])\n                )\n        self._regular_grid_interp = None\n        self._data = data\n\n    @property\n    def dim(self):\n        \"\"\"Dimension (number of axes)\"\"\"\n        return len(self.axes)\n\n    def evaluate(self, method=None, **kwargs):\n        \"\"\"Evaluate NDData Array\n\n        This function provides a uniform interface to several interpolators.\n        The evaluation nodes are given as ``kwargs``.\n\n        Currently available:\n        `~scipy.interpolate.RegularGridInterpolator`, methods: linear, nearest\n\n        Parameters\n        ----------\n        method : str {'linear', 'nearest'}, optional\n            Interpolation method\n        kwargs : dict\n            Keys are the axis names, Values the evaluation points\n\n        Returns\n        -------\n        array : `~astropy.units.Quantity`\n            Interpolated values, axis order is the same as for the NDData array\n        \"\"\"\n        values = []\n        for idx, axis in enumerate(self.axes):\n            # Extract values for each axis, default: nodes\n            shape = [1] * len(self.axes)\n            shape[idx] = -1\n            default = axis.center.reshape(tuple(shape))\n            temp = Quantity(kwargs.pop(axis.name, default))\n            values.append(np.atleast_1d(temp))\n\n        # This is to catch e.g. typos in axis names\n        if kwargs != {}:\n            raise ValueError(f\"Input given for unknown axis: {kwargs}\")\n\n        if self._regular_grid_interp is None:\n            self._add_regular_grid_interp()\n\n        return self._regular_grid_interp(values, method=method, **kwargs)\n\n    def _add_regular_grid_interp(self, interp_kwargs=None):\n        \"\"\"Add `~scipy.interpolate.RegularGridInterpolator`\n\n        http://docs.scipy.org/doc/scipy-0.14.0/reference/generated/scipy.interpolate.RegularGridInterpolator.html\n\n        Parameters\n        ----------\n        interp_kwargs : dict, optional\n            Interpolation kwargs\n        \"\"\"\n        if interp_kwargs is None:\n            interp_kwargs = self.interp_kwargs\n\n        points = [a.center for a in self.axes]\n        points_scale = [a.interp for a in self.axes]\n        self._regular_grid_interp = ScaledRegularGridInterpolator(\n            points, self.data, points_scale=points_scale, **interp_kwargs\n        )\n", "path": "gammapy/utils/nddata.py"}]}