Repository: Unidata/MetPy

Issue:
hres of interpolate_to_grid not respected
### What went wrong?

When using interpolate.interpolate_to_grid, the output grid spacing is not exactly the one specified by 'hres' parameter.

### Operating System

Linux

### Version

1.2.0

### Python Version

3.7

### Code to Reproduce

```python
from metpy.interpolate import interpolate_to_grid
import numpy as np
import matplotlib.pyplot as plt


def calculate_z(x, y):
    z = []
    for i, _ in enumerate(x):
        z.append(x[i]**2 + y[i]**2)
    return np.array(z)

x = np.random.rand(50) * 10
y = np.random.rand(50) * 10

z = calculate_z(x, y)

grid_x, grid_y, grid_z = interpolate_to_grid(x, y, z, interp_type='linear', hres=1, boundary_coords={'west': 0, 'south': 0, 'east': 10.0, 'north': 10.0})

print(grid_x)
print('\n')
print(grid_y)
```


### Errors, Traceback, and Logs

_No response_

Discussion:

Patch:
diff --git a/src/metpy/interpolate/grid.py b/src/metpy/interpolate/grid.py
index 9de3e71c65..9bfeb4b8d0 100644
--- a/src/metpy/interpolate/grid.py
+++ b/src/metpy/interpolate/grid.py
@@ -18,10 +18,11 @@ def generate_grid(horiz_dim, bbox):
 
     Parameters
     ----------
-    horiz_dim: integer
+    horiz_dim: int or float
         Horizontal resolution
-    bbox: dictionary
-        Dictionary containing coordinates for corners of study area.
+    bbox: dict
+        Dictionary with keys 'east', 'west', 'north', 'south' with the box extents
+        in those directions.
 
     Returns
     -------
@@ -36,9 +37,7 @@ def generate_grid(horiz_dim, bbox):
     grid_x = np.linspace(bbox['west'], bbox['east'], x_steps)
     grid_y = np.linspace(bbox['south'], bbox['north'], y_steps)
 
-    gx, gy = np.meshgrid(grid_x, grid_y)
-
-    return gx, gy
+    return np.meshgrid(grid_x, grid_y)
 
 
 def generate_grid_coords(gx, gy):
@@ -63,8 +62,9 @@ def generate_grid_coords(gx, gy):
 def get_xy_range(bbox):
     r"""Return x and y ranges in meters based on bounding box.
 
-    bbox: dictionary
-        dictionary containing coordinates for corners of study area
+    bbox: dict
+        Dictionary with keys 'east', 'west', 'north', 'south' with the box extents
+        in those directions.
 
     Returns
     -------
@@ -83,9 +83,10 @@ def get_xy_range(bbox):
 def get_xy_steps(bbox, h_dim):
     r"""Return meshgrid spacing based on bounding box.
 
-    bbox: dictionary
-        Dictionary containing coordinates for corners of study area.
-    h_dim: integer
+    bbox: dict
+        Dictionary with keys 'east', 'west', 'north', 'south' with the box extents
+        in those directions.
+    h_dim: int or float
         Horizontal resolution in meters.
 
     Returns
@@ -98,8 +99,8 @@ def get_xy_steps(bbox, h_dim):
     """
     x_range, y_range = get_xy_range(bbox)
 
-    x_steps = np.ceil(x_range / h_dim)
-    y_steps = np.ceil(y_range / h_dim)
+    x_steps = np.ceil(x_range / h_dim) + 1
+    y_steps = np.ceil(y_range / h_dim) + 1
 
     return int(x_steps), int(y_steps)
 
@@ -111,14 +112,14 @@ def get_boundary_coords(x, y, spatial_pad=0):
         x coordinates.
     y: numeric
         y coordinates.
-    spatial_pad: numeric
-        Number of meters to add to the x and y dimensions to reduce
-        edge effects.
+    spatial_pad: int or float
+        Number of meters to add to the x and y dimensions to reduce edge effects.
 
     Returns
     -------
-    bbox: dictionary
-        dictionary containing coordinates for corners of study area
+    bbox: dict
+        Dictionary with keys 'east', 'west', 'north', 'south' with the box extents
+        in those directions.
 
     """
     west = np.min(x) - spatial_pad


Test patch:
diff --git a/tests/interpolate/test_grid.py b/tests/interpolate/test_grid.py
index a2a621a7fe..573aad2589 100644
--- a/tests/interpolate/test_grid.py
+++ b/tests/interpolate/test_grid.py
@@ -73,8 +73,8 @@ def test_get_xy_steps():
 
     x_steps, y_steps = get_xy_steps(bbox, 3)
 
-    truth_x = 3
-    truth_y = 3
+    truth_x = 4
+    truth_y = 4
 
     assert x_steps == truth_x
     assert y_steps == truth_y
@@ -105,13 +105,15 @@ def test_generate_grid():
 
     gx, gy = generate_grid(3, bbox)
 
-    truth_x = np.array([[0.0, 4.5, 9.0],
-                        [0.0, 4.5, 9.0],
-                        [0.0, 4.5, 9.0]])
+    truth_x = np.array([[0.0, 3.0, 6.0, 9.0],
+                        [0.0, 3.0, 6.0, 9.0],
+                        [0.0, 3.0, 6.0, 9.0],
+                        [0.0, 3.0, 6.0, 9.0]])
 
-    truth_y = np.array([[0.0, 0.0, 0.0],
-                        [4.5, 4.5, 4.5],
-                        [9.0, 9.0, 9.0]])
+    truth_y = np.array([[0.0, 0.0, 0.0, 0.0],
+                        [3.0, 3.0, 3.0, 3.0],
+                        [6.0, 6.0, 6.0, 6.0],
+                        [9.0, 9.0, 9.0, 9.0]])
 
     assert_array_almost_equal(gx, truth_x)
     assert_array_almost_equal(gy, truth_y)
@@ -127,13 +129,20 @@ def test_generate_grid_coords():
     gx, gy = generate_grid(3, bbox)
 
     truth = [[0.0, 0.0],
-             [4.5, 0.0],
+             [3.0, 0.0],
+             [6.0, 0.0],
              [9.0, 0.0],
-             [0.0, 4.5],
-             [4.5, 4.5],
-             [9.0, 4.5],
+             [0.0, 3.0],
+             [3.0, 3.0],
+             [6.0, 3.0],
+             [9.0, 3.0],
+             [0.0, 6.0],
+             [3.0, 6.0],
+             [6.0, 6.0],
+             [9.0, 6.0],
              [0.0, 9.0],
-             [4.5, 9.0],
+             [3.0, 9.0],
+             [6.0, 9.0],
              [9.0, 9.0]]
 
     pts = generate_grid_coords(gx, gy)
@@ -266,7 +275,13 @@ def test_interpolate_to_grid(method, assume_units, test_coords, boundary_coords)
         z = units.Quantity(z, assume_units)
         truth = units.Quantity(truth, assume_units)
 
-    _, _, img = interpolate_to_grid(xp, yp, z, hres=10, interp_type=method, **extra_kw)
+    # Value is tuned to keep the old results working after fixing an off-by-one error
+    # in the grid generation (desired value was 10) See #2319.
+    hres = 10.121
+    xg, yg, img = interpolate_to_grid(xp, yp, z, hres=hres, interp_type=method, **extra_kw)
+
+    assert np.all(np.diff(xg, axis=-1) <= hres)
+    assert np.all(np.diff(yg, axis=0) <= hres)
     assert_array_almost_equal(truth, img)
 
 
diff --git a/tests/interpolate/test_interpolate_tools.py b/tests/interpolate/test_interpolate_tools.py
index e1e367fa80..56a747c827 100644
--- a/tests/interpolate/test_interpolate_tools.py
+++ b/tests/interpolate/test_interpolate_tools.py
@@ -127,4 +127,4 @@ def test_interpolate_to_grid_pandas():
     }, index=[1, 2, 3, 4, 5, 6])
     interpolate_to_grid(
         df['lon'], df['lat'], df['tmp'],
-        interp_type='natural_neighbor', hres=0.5)
+        interp_type='natural_neighbor', hres=0.6)