Interpolation of sections along a path

For ClimCanvas v1.02.1

When you choose an "Orientation" of the vertical cross-section marked "interpolated" (along a parallel, along a meridian, along the great circle between two points), ClimCanvas does not simply line up grid-point values: it takes points on the path and bilinearly interpolates the value at each point from the surrounding grid points. This chapter explains what that interpolation actually does. It focuses on grids whose longitude and latitude are 2-D (2-D coordinates: the Lambert grid of a regional model, WRF, ocean-model grids, and so on), but the "along the great circle between two points" section on an ordinary grid with 1-D longitude/latitude uses the same mechanism. Because interpolation changes the values, its use is always reported in the box "Processing applied to this figure" below the figure.

Why interpolation is needed

On a grid with 2-D longitude/latitude, the grid rows and columns do not coincide with parallels and meridians. For example, on the ClimCORE Lambert grid (817 × 661, 5 km), latitude changes by about 5°, from 30.3°N to 35.2°N, along a single row, and longitude changes by nearly 10° along a single column. A section "along a grid row" has exact values without interpolation, but it is no substitute for "the section at 35°N".

Grid rows, a parallel and a great circle
A synthetic grid (60 × 50, 100 km) in the same projection as ClimCORE (standard parallels 30°N and 60°N, central longitude 140°E). The blue grid row drops 5° in latitude at both ends and crosses the orange parallel only at the center. The green great circle is a path drawn independently of the grid; a section along a path interpolates values at points on this line.

To draw a section along a parallel or between any two points, you need to find, for each point on the path, where that point lies in the grid, and interpolate from the surrounding grid-point values. On a grid with 1-D longitude/latitude the position is found in one dimension for each of the latitude and longitude axes, but on a 2-D coordinate grid the grid lines are curved, so the grid index has to be computed back from longitude/latitude. That inversion is the core of this chapter.

Overall flow — three stages

The interpolation proceeds in three stages, path points → fractional grid indices → values, and none of them uses any knowledge of the map projection. Everything is computed from the grid's longitude and latitude values (and the variable values) alone, so it works the same way for files without projection parameters and for grids in any projection.

Stage What it does Function embedded in the reproduction script
1. Take points on the path For a parallel or meridian, divide the longitude (latitude) span evenly; for a great circle, take equally spaced points on the sphere great_circle_points
2. Express the point positions as grid indices Express which cell each point falls in, and where within it, as fractional grid indices (fj, fi) grid_fractional_indices (2-D) / grid_fractional_indices_1d (1-D)
3. Interpolate from the four surrounding grid points Weighted mean of the values at the four cell corners (bilinear interpolation) sample_bilinear
Sampling the path
(a) Points are taken on the path (green) at roughly the grid spacing, and for each point the cell containing it (light green) is found. (b) Seen in grid-index space, the curved grid becomes an array of square cells, and each point can be expressed by fractional grid indices (fj, fi). (c) The position (a, b) of point P within its cell. (d) The value at P is a weighted mean of the four corner values; the weight of each corner is the area of the rectangle on the opposite side of P (the values are examples).

A fractional grid index extends the grid-point index (row j, column i) to a continuous quantity. If P in the figure has (fj, fi) = (26.27, 27.53), it lies in the cell spanning rows 26–27 and columns 27–28, at 0.27 along the row direction and 0.53 along the column direction (the integer part identifies the cell, the fractional part the position within it).

1. Taking points on the path

p(t) = [ sin((1 − t) ω) p1 + sin(t ω) p2 ] / sin ω (t = 0 … 1)
ω = atan2(|p1 × p2|, p1 · p2) (angle between the two points)
distance from the start = t ω R (R = 6371 km)

2. Expressing the point positions as grid indices

Grids with 2-D longitude/latitude

Using only the 2-D arrays of grid longitude and latitude, the fractional grid indices of each point are found in three steps. The figure shows the example of point P, whose true grid index, constructed from the projection, is (23.37, 31.62).

Steps for finding the fractional grid indices
(a) Compare the spherical distances between P and the thinned grid points (blue) to find a coarse nearest point. (b) Search for the true nearest grid point within a window centered on the coarse nearest point, moving the window until its center is the nearest. (c) In grid-index space the cells are squares, and P converges from the nearest grid point to its position within the cell in a single iteration. (d) Projected onto the plane tangent to the sphere at P, the same cell becomes a quadrilateral. The problem is solved on this plane.

Step 1 — coarse nearest point on a thinned grid (a). When longitude and latitude are converted to 3-D unit vectors on the sphere, the spherical distance between two points is shorter the larger their dot product. So the dot products between P and the grid points thinned in both rows and columns (thinning stride = the number of points along the longer side of the grid ÷ 100, rounded up) are taken, and the largest gives the coarse nearest point. For ClimCORE (817 × 661) the stride is 9, so dot products with only 91 × 74 = 6,734 points are needed. Because the comparison is made with 3-D vectors, it is unaffected by the longitude convention (0–360 / −180–180), the dateline, or the poles.

Step 2 — moving the window to the true nearest grid point (b). Dot products are taken with all grid points in a window of one stride around the coarse nearest point (center ± stride). If the nearest point in the window differs from the center, the window is moved there and the search repeats; once the center is the nearest point in the window, it is final. In the figure the window moves once, from the coarse nearest point (25, 30) to the nearest grid point (23, 32), and stops.

Step 3 — Newton's method on the plane tangent at P (c, d). The grid points around the nearest grid point are projected gnomonically onto the plane tangent to the sphere at P. A unit vector v maps as follows, where p is the unit vector of P and e, n are the eastward and northward unit vectors at P. On this plane P is at the origin.

w = v / (v · p), x = w · e, y = w · n

A square cell in grid-index space (c) becomes a quadrilateral on the plane (d). From the plane coordinates of the four cell corners x00, x01, x10, x11 (and likewise for y), consider the bilinear map that sends the position (a, b) within the cell (a along rows, b along columns, both 0–1) to a point on the plane, and solve for the (a, b) at which X = Y = 0 (the position of P) by Newton's method.

X(a, b) = x00 (1 − a)(1 − b) + x01 (1 − a) b + x10 a (1 − b) + x11 a b (same for Y)

Grids with 1-D longitude/latitude

On an ordinary grid with 1-D longitude/latitude, the index is found independently for each of the latitude and longitude axes. The coordinate values are sorted in ascending order, the index corresponding to the point's value is interpolated linearly (numpy's interp), and values outside the range are set to missing.

Grid Point Column index fi
0–357.5°, 2.5° spacing (144 columns) 10° 4.0
Same −1° (= 359°) 143.6 (between the 357.5° column and the 0° column)
Regional grid 100–160° −200° (= 160°) 24.0 (eastern edge)
Same 170° missing (outside the region)

Expanding to a 2-D grid and using the method above gives almost the same indices (a difference of 2.7e-3 cells on a 2.5° grid). Because they do not agree exactly, the 1-D method is used for ordinary grids.

3. Bilinear interpolation from the four surrounding grid points

The value at the fractional grid index (fj, fi) = (j0 + a, i0 + b) is a weighted mean of the values at the four cell corners. f00 is the value at corner (j0, i0), f01 at (j0, i0 + 1), f10 at (j0 + 1, i0), and f11 at (j0 + 1, i0 + 1).

f = f00 (1 − a)(1 − b) + f01 (1 − a) b + f10 a (1 − b) + f11 a b

The weight of each corner equals the area of the rectangle on the opposite side of the point (figure a below). If the point coincides with a corner, the result is that corner's value itself; if it lies on a cell edge, it is the linear interpolation of the values at the two ends of the edge.

Bilinear weights and the missing-value rule
(a) The weight of each corner is the area of the rectangle on the opposite side of the point. (b) The region (orange) in which a missing grid point (×) makes the interpolation missing is only the interior of the four cells that have that point as a corner, plus the grid lines through that point. On the outer boundary of the four cells (where the missing corner has weight 0) values are produced.

Limits and notes

Checking in the reproduction script

The functions used for the interpolation are embedded verbatim at the top of the reproduction script (only those used: great_circle_points for a great circle, grid_fractional_indices for a 2-D coordinate grid, grid_fractional_indices_1d for a 1-D grid, always sample_bilinear for a section along a path, and lonlat_tick_label when longitude/latitude are shown alongside the ticks). The in-app drawing and the reproduction script interpolate with literally the same code, so the figures match, and the script runs with numpy and xarray alone. For a great-circle section on a 2-D coordinate grid, the relevant part looks like this.

path_lon, path_lat, path_x = great_circle_points((118.0, 27.0), (160.0, 44.0), 60)
path_fj, path_fi = grid_fractional_indices(ds0['lon'].transpose('y', 'x').values,
                                           ds0['lat'].transpose('y', 'x').values,
                                           path_lon, path_lat)
da_fill = ds0['t'].sel(time='2024-01-01T06:00:00')
da_fill = sample_bilinear(da_fill, 'y', 'x', path_fj, path_fi, 'path', wrap_x=False)
da_fill = da_fill.assign_coords({'path': path_x, 'path_lon': ('path', path_lon),
                                 'path_lat': ('path', path_lat)})

The start and end points and the number of points are written as literals, so you can move the section by editing them in the script. Reading the function bodies lets you follow the steps of this chapter directly.

What the tests verify

The ClimCanvas tests check the following about this interpolation automatically (tests/test_section_path.py and others).