Grid Convergence#

Grid convergence is a spatial coordinate-reference-system (CRS) concept. It describes how the north direction of a projected coordinate grid is rotated relative to geographic true north at a location. The calculation does not depend on any wind-climate workflow.

WindKit exposes this calculation through windkit.spatial.grid_convergence(). There are two useful forms:

  • Natural grid convergence: the angle between true north and the grid north of a dataset’s own CRS.

  • Effective grid convergence: the rotation between the grid north of a source CRS and the grid north of the dataset CRS at the dataset locations.

True North, Grid North, and Sign Convention#

True north, or geographic north, follows a geographic meridian toward the North Pole. Grid north follows the northing axis of a projected coordinate system. These directions are identical in a geographic CRS, but they usually diverge away from a projection’s central meridian.

WindKit uses the PROJ/pyproj sign convention: positive grid convergence means grid north is clockwise, or east, of true north. Negative values mean grid north is counter-clockwise, or west, of true north.

Compute Natural Grid Convergence#

First, create a point dataset in geographic coordinates (EPSG:4326). The coordinates are longitude (west_east) and latitude (south_north).

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
import geopandas as gpd
import windkit as wk

ds = wk.spatial.create_point(
    west_east=[12.0, 12.1],
    south_north=[55.0, 55.1],
    height=[100.0, 100.0],
    crs=4326,
)

print(ds)

gc_natural = wk.spatial.grid_convergence(ds)
print("Natural grid convergence for geographic coordinates:")
print(gc_natural)
<xarray.Dataset> Size: 65B
Dimensions:      (point: 2)
Coordinates:
    height       (point) float64 16B 100.0 100.0
    south_north  (point) float64 16B 55.0 55.1
    west_east    (point) float64 16B 12.0 12.1
    crs          int8 1B 0
Dimensions without coordinates: point
Data variables:
    output       (point) float64 16B 0.0 0.0
Attributes:
    Conventions:  CF-1.8
    history:      2026-09-29T09:45:34+00:00:\twindkit==2.2.0\t"point")
Natural grid convergence for geographic coordinates:
<xarray.DataArray 'grid_convergence' (point: 2)> Size: 16B
array([-0., -0.])
Coordinates:
    height       (point) float64 16B 100.0 100.0
    south_north  (point) float64 16B 55.0 55.1
    west_east    (point) float64 16B 12.0 12.1
    crs          int8 1B 0
Dimensions without coordinates: point
Attributes:
    description:        Grid convergence angle (positive for a clockwise rota...
    height_dim_policy:  forbidden
    long_name:          Grid convergence angle
    short_name:         grid_convergence
    units:              degree
    grid_mapping:       crs

In a projected CRS the same locations can have non-zero natural convergence. Here the points are reprojected into EPSG:3035, the European Lambert Azimuthal Equal Area projection.

ds_projected = wk.spatial.reproject(ds, 3035)
gc_projected = wk.spatial.grid_convergence(ds_projected)
print("Natural grid convergence after reprojection to EPSG:3035:")
print(gc_projected)
Natural grid convergence after reprojection to EPSG:3035:
<xarray.DataArray 'grid_convergence' (point: 2)> Size: 16B
array([1.62325563, 1.70611737])
Coordinates:
    height       (point) float64 16B 100.0 100.0
    west_east    (point) float64 16B 4.449e+06 4.455e+06
    south_north  (point) float64 16B 3.546e+06 3.557e+06
    crs          int8 1B 0
Dimensions without coordinates: point
Attributes:
    description:        Grid convergence angle (positive for a clockwise rota...
    height_dim_policy:  forbidden
    long_name:          Grid convergence angle
    short_name:         grid_convergence
    units:              degree
    grid_mapping:       crs

Spatial Variation Across a Projection#

Grid convergence varies over a projected map. To visualize this, create a grid of points over Europe in EPSG:3035, calculate the natural convergence at each point, and plot the result.

Positive values (red/warm) mean grid north tilts clockwise (east) relative to true north, while negative values (blue/cool) mean grid north tilts counter-clockwise (west). At the central meridian (10 degrees east), grid convergence is zero. Values grow in magnitude farther east or west because projected grid lines no longer follow geographic meridians.

# Define a grid over Europe in EPSG:3035 coordinate space
x = np.linspace(2.5e6, 5.5e6, 100)
y = np.linspace(1.5e6, 4.5e6, 100)
xx, yy = np.meshgrid(x, y)

# Create a WindKit point dataset with the grid locations
ds_grid = wk.spatial.create_point(
    west_east=xx.ravel(),
    south_north=yy.ravel(),
    height=np.ones_like(xx.ravel()) * 100.0,
    crs=3035,
)

# Compute natural grid convergence
gc_grid = wk.spatial.grid_convergence(ds_grid)

# Reshape back to the 2D grid shape for plotting
gc_2d = gc_grid.values.reshape(xx.shape)
Natural Grid Convergence in EPSG:3035 across Europe

Effective Grid Convergence Close to the Pole#

The source= argument computes the convergence between two CRS frames. The value is evaluated at the locations of the first argument (ds_32632 below) and represents the frame rotation from the source CRS grid north into the dataset CRS grid north.

A high-latitude example makes the distinction clear. Consider longitude 15.0 degrees east, latitude 78.0 degrees north, near Svalbard. Compare:

  1. EPSG:3035 (Lambert Azimuthal Equal Area, central meridian 10 degrees east)

  2. EPSG:32632 (UTM Zone 32N, central meridian 9 degrees east)

At 15.0 degrees east, both points lie east of their respective central meridians. Therefore, both projections have positive natural grid convergence angles. The natural angles can be large, while the effective convergence between the two projected frames is much smaller because the frames are similarly rotated.

# Create a high-latitude point close to the pole in geographic coordinates
ds_high_lat = wk.spatial.create_point(
    west_east=[15.0],
    south_north=[78.0],
    height=[100.0],
    crs=4326,
)

# Project the point into both coordinate systems
ds_3035 = wk.spatial.reproject(ds_high_lat, 3035)
ds_32632 = wk.spatial.reproject(ds_high_lat, 32632)

# Compute natural grid convergence for both
gc_natural_3035 = wk.spatial.grid_convergence(ds_3035)
gc_natural_32632 = wk.spatial.grid_convergence(ds_32632)

# Compute the effective grid convergence between them. This tells us the frame
# rotation from EPSG:3035 grid north to EPSG:32632 grid north, evaluated at the
# EPSG:32632 locations.
gc_effective_from_crs = wk.spatial.grid_convergence(ds_32632, source=3035)

# Note that you can pass a dataset as the source and the projection will be inferred.
gc_effective_from_ds = wk.spatial.grid_convergence(ds_32632, source=ds_3035)

print("Latitude: 78 degrees north, Longitude: 15 degrees east")
print(f"Natural GC in EPSG:3035 (LAEA): {gc_natural_3035.values[0]:.4f} degrees")
print(f"Natural GC in EPSG:32632 (UTM 32N): {gc_natural_32632.values[0]:.4f} degrees")
print(
    "Effective GC (source dataset 3035 to dataset CRS 32632): "
    f"{gc_effective_from_ds.values[0]:.4f} degrees"
)
Latitude: 78 degrees north, Longitude: 15 degrees east
Natural GC in EPSG:3035 (LAEA): 5.0267 degrees
Natural GC in EPSG:32632 (UTM 32N): 5.8698 degrees
Effective GC (source dataset 3035 to dataset CRS 32632): 0.8431 degrees

Bin True-North Time Series into a Grid Frame#

When a time series is known to be true-north-relative, pass a wind_dir_crs to bwc_from_tswc. If the TSWC already carries attrs["wind_dir_crs"], WindKit treats the directions as already expressed in that frame and avoids rotating them again. A projected CRS rotates directions into that grid frame; a geographic CRS retains the true-north frame. WindKit records the resolved CRS WKT on the BWC.

tswc = wk.create_tswc(
    ds_high_lat,
    date_range=pd.date_range("2001-01-01", periods=24, freq="h"),
)

# Mark the input as already expressed in EPSG:32632 grid north.
tswc_marked = tswc.assign_attrs(wind_dir_crs=32632)

bwc_marked = wk.bwc_from_tswc(tswc_marked)
bwc_same_frame = wk.bwc_from_tswc(tswc_marked, wind_dir_crs=32632)
bwc_other_frame = wk.bwc_from_tswc(tswc_marked, wind_dir_crs=3035)

print(bwc_marked.attrs["wind_dir_crs"])
print(bwc_same_frame.attrs["wind_dir_crs"])
print(bwc_other_frame.attrs["wind_dir_crs"])
PROJCRS["WGS 84 / UTM zone 32N",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],MEMBER["World Geodetic System 1984 (G2296)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["UTM zone 32N",METHOD["Transverse Mercator",ID["EPSG",9807]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",9,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["Scale factor at natural origin",0.9996,SCALEUNIT["unity",1],ID["EPSG",8805]],PARAMETER["False easting",500000,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["(E)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["(N)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Navigation and medium accuracy spatial referencing."],AREA["Between 6°E and 12°E, northern hemisphere between equator and 84°N, onshore and offshore. Algeria. Austria. Cameroon. Denmark. Equatorial Guinea. France. Gabon. Germany. Italy. Libya. Liechtenstein. Monaco. Netherlands. Niger. Nigeria. Norway. Sao Tome and Principe. Svalbard. Sweden. Switzerland. Tunisia. Vatican City State."],BBOX[0,6,84,12]],ID["EPSG",32632]]
PROJCRS["WGS 84 / UTM zone 32N",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],MEMBER["World Geodetic System 1984 (G2296)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["UTM zone 32N",METHOD["Transverse Mercator",ID["EPSG",9807]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",9,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["Scale factor at natural origin",0.9996,SCALEUNIT["unity",1],ID["EPSG",8805]],PARAMETER["False easting",500000,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["(E)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["(N)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Navigation and medium accuracy spatial referencing."],AREA["Between 6°E and 12°E, northern hemisphere between equator and 84°N, onshore and offshore. Algeria. Austria. Cameroon. Denmark. Equatorial Guinea. France. Gabon. Germany. Italy. Libya. Liechtenstein. Monaco. Netherlands. Niger. Nigeria. Norway. Sao Tome and Principe. Svalbard. Sweden. Switzerland. Tunisia. Vatican City State."],BBOX[0,6,84,12]],ID["EPSG",32632]]
PROJCRS["ETRS89-extended / LAEA Europe",BASEGEOGCRS["ETRS89",ENSEMBLE["European Terrestrial Reference System 1989 ensemble",MEMBER["European Terrestrial Reference Frame 1989"],MEMBER["European Terrestrial Reference Frame 1990"],MEMBER["European Terrestrial Reference Frame 1991"],MEMBER["European Terrestrial Reference Frame 1992"],MEMBER["European Terrestrial Reference Frame 1993"],MEMBER["European Terrestrial Reference Frame 1994"],MEMBER["European Terrestrial Reference Frame 1996"],MEMBER["European Terrestrial Reference Frame 1997"],MEMBER["European Terrestrial Reference Frame 2000"],MEMBER["European Terrestrial Reference Frame 2005"],MEMBER["European Terrestrial Reference Frame 2014"],MEMBER["European Terrestrial Reference Frame 2020"],ELLIPSOID["GRS 1980",6378137,298.257222101,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[0.1]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4258]],CONVERSION["Europe Equal Area 2001",METHOD["Lambert Azimuthal Equal Area",ID["EPSG",9820]],PARAMETER["Latitude of natural origin",52,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",10,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",4321000,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",3210000,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["northing (Y)",north,ORDER[1],LENGTHUNIT["metre",1]],AXIS["easting (X)",east,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Statistical analysis."],AREA["Europe - European Union (EU) countries and candidates. Europe - onshore and offshore: Albania; Andorra; Austria; Belgium; Bosnia and Herzegovina; Bulgaria; Croatia; Cyprus; Czechia; Denmark; Estonia; Faroe Islands; Finland; France; Germany; Gibraltar; Greece; Hungary; Iceland; Ireland; Italy; Kosovo; Latvia; Liechtenstein; Lithuania; Luxembourg; Malta; Monaco; Montenegro; Netherlands; North Macedonia; Norway including Svalbard and Jan Mayen; Poland; Portugal including Madeira and Azores; Romania; San Marino; Serbia; Slovakia; Slovenia; Spain including Canary Islands; Sweden; Switzerland; Türkiye (Turkey); United Kingdom (UK) including Channel Islands and Isle of Man; Vatican City State."],BBOX[24.6,-35.58,84.73,44.83]],ID["EPSG",3035]]

When to Use This#

Use grid_convergence() when reasoning about CRS frames, diagnosing map projection behavior, or checking direction reference frames. If you are using PyWAsP wind climates, see the PyWAsP meridian-convergence tutorial for how these CRS rotations affect BWC, GWC, and WWC workflows.

Total running time of the script: (0 minutes 0.869 seconds)

Gallery generated by Sphinx-Gallery