Skip to content

Grids API

For narrative coverage of how the three mesh types fit together, see the narrative Grids page under Concepts.

AtmosTransport.Grids.AbstractFluxTopology Type
julia
AbstractFluxTopology

Supertype for flux topology traits. Each mesh advertises its natural flux representation via flux_topology(mesh).

  • StructuredFluxTopology — directional (x, y, z) face fluxes on a logically rectangular mesh.

  • FaceIndexedFluxTopology — face-indexed connectivity; the natural representation for reduced Gaussian and other unstructured meshes.

source
AtmosTransport.Grids.AbstractHorizontalMesh Type
julia
AbstractHorizontalMesh

Supertype for all horizontal mesh topologies.

Required methods

Any subtype must implement the geometry API defined in GeometryOps.jl:

  • ncells(mesh)

  • nfaces(mesh)

  • cell_area(mesh, c)

  • face_length(mesh, f)

  • face_normal(mesh, f)

  • face_cells(mesh, f) -> (left, right)

  • cell_faces(mesh, c) -> indices

Structured meshes additionally provide:

  • nx(mesh), ny(mesh) — logical grid dimensions
source
AtmosTransport.Grids.AbstractStructuredMesh Type
julia
AbstractStructuredMesh <: AbstractHorizontalMesh

Meshes with a logically rectangular (i, j) structure per region. Both LatLonMesh and CubedSphereMesh are structured. Structured meshes expose nx(mesh), ny(mesh) and support direct (i, j) indexing of areas and face fluxes.

source
AtmosTransport.Grids.AbstractVerticalCoordinate Type
julia
AbstractVerticalCoordinate{FT}

Supertype for vertical coordinate systems.

Required methods

  • n_levels(vc) — number of vertical levels

  • pressure_at_level(vc, k, p_surface) — center pressure

  • pressure_at_interface(vc, k, p_surface) — interface pressure

  • level_thickness(vc, k, p_surface) — layer thickness [Pa]

source
AtmosTransport.Grids.AngularMidpointCenter Type
julia
AngularMidpointCenter <: AbstractCubedSphereCenterLaw

Cell-center law that evaluates the continuous face coordinate map at the midpoint of the logical cell:

xc(i,j)=x(i+1/2,j+1/2).

This matches the historical synthetic/equiangular CubedSphereMesh behavior. It is not how GEOS writes native C180/C720 center coordinates.

source
AtmosTransport.Grids.AtmosGrid Type
julia
AtmosGrid{H, V, Arch, FT}

Composite grid: horizontal mesh + vertical coordinate + architecture.

This is the single grid object passed to all transport operators. Dispatch on H selects mesh-specific geometry; dispatch on Arch selects backend.

Fields

  • horizontal :: H — horizontal mesh (LatLonMesh, CubedSphereMesh, ReducedGaussianMesh)

  • vertical :: V — vertical coordinate (HybridSigmaPressure)

  • arch :: Arch — compute architecture (CPU, GPU)

  • planet :: PlanetParameters{FT} — physical constants [m, m/s², Pa]

source
AtmosTransport.Grids.CubedSphereDefinition Type
julia
CubedSphereDefinition(coordinate_law, center_law, panel_convention;
                      longitude_offset_deg=0, tag=:custom)

Complete horizontal cubed-sphere definition.

The definition is the geometry contract consumed by CubedSphereMesh and all derived helpers. It deliberately separates:

  • coordinate_law: how logical face edges are placed on the gnomonic cube,

  • center_law: how cell centers are derived from the face geometry,

  • panel_convention: how the six physical faces are ordered/oriented in file arrays and halo connectivity,

  • longitude_offset_deg: a final rigid z-axis rotation applied to all panels.

For native GMAO files use GMAOCubedSphereDefinition, which combines GMAOEqualDistanceGnomonic, FourCornerNormalizedCenter, GEOSNativePanelConvention, and the GEOS -10° longitude shift.

source
AtmosTransport.Grids.CubedSphereMesh Type
julia
CubedSphereMesh{FT, C, D} <: AbstractStructuredMesh

Structured cubed-sphere mesh with Nc cells per panel edge and 6 panels.

Cell areas and edge lengths are computed once at construction time from the definition's corner geometry. All 6 panels share identical metrics by symmetry, so only one set is stored.

Fields

  • Nc :: Int — cells per panel edge

  • Hp :: Int — halo padding width (1 for slopes, 3 for PPM)

  • radius :: FT — planet radius [m]

  • definition :: D — coordinate law + center law + panel convention contract

  • convention :: C — panel numbering convention

  • connectivity :: PanelConnectivity — edge-to-edge panel connectivity

  • cell_areas :: Matrix{FT}(Nc, Nc) cell areas [m²] (one panel, all identical)

  • Δx :: Matrix{FT}(Nc, Nc) x-direction cell widths [m]

  • Δy :: Matrix{FT}(Nc, Nc) y-direction cell widths [m]

source
AtmosTransport.Grids.CubedSphereMesh Method
julia
CubedSphereMesh(; Nc, FT=Float64, Hp=1, radius=6.371e6,
                  convention=nothing, definition=nothing)

Construct a cubed-sphere mesh with Nc cells per panel edge.

definition is the authoritative geometry contract. If omitted, the constructor chooses a definition from convention:

  • GnomonicPanelConvention() -> EquiangularCubedSphereDefinition()

  • GEOSNativePanelConvention() -> GMAOCubedSphereDefinition()

This preserves legacy synthetic CS behavior for default meshes while making GEOS-IT/GEOS-FP targets use the actual GMAO equal-distance geometry whenever the GEOS-native panel convention is requested.

Keyword arguments

  • Nc :: Int — cells per panel edge (e.g. 48, 90, 180, 720)

  • FT :: Type — floating-point type (default Float64)

  • Hp :: Int — halo padding width (default 1 for slopes; use 3 for PPM)

  • radius — planet radius [m] (default Earth)

  • convention — panel numbering convention used when definition is omitted

  • definition — full CubedSphereDefinition; overrides convention

source
AtmosTransport.Grids.EquiangularGnomonic Type
julia
EquiangularGnomonic <: AbstractCubedSphereCoordinateLaw

Classical equiangular gnomonic face coordinate law.

For edge index s = 1, ..., Nc + 1, the local angle is

αs=π/4+(s1)π/(2Nc),

and the tangent-plane coordinate used by the gnomonic projection is

ξs=tan(αs).

This law is useful for controlled synthetic targets and legacy binaries. It is not the native GEOS-IT/GEOS-FP cubed-sphere coordinate law.

source
AtmosTransport.Grids.FaceIndexedFluxTopology Type
julia
FaceIndexedFluxTopology <: AbstractFluxTopology

Fluxes stored as a single face-indexed array with explicit connectivity. Natural for reduced Gaussian and other unstructured meshes.

source
AtmosTransport.Grids.GEOSNativePanelConvention Type
julia
GEOSNativePanelConvention <: AbstractCubedSpherePanelConvention

Panel numbering and orientation used by native GEOS-FP / GEOS-IT cubed-sphere files: panels 1-2 are equatorial, 3 is north-pole, 4-5 are equatorial, and 6 is south-pole.

This type describes panel storage/order only. The GEOS -10° longitude shift and GMAO equal-distance coordinate law live in GMAOCubedSphereDefinition.

source
AtmosTransport.Grids.GnomonicPanelConvention Type
julia
GnomonicPanelConvention <: AbstractCubedSpherePanelConvention

Classical gnomonic panel numbering: 1-4 are equatorial panels, 5 is the north-pole panel, 6 is the south-pole panel.

source
AtmosTransport.Grids.HybridSigmaPressure Type
julia
HybridSigmaPressure{FT} <: AbstractVerticalCoordinate{FT}

Hybrid sigma-pressure coordinate: p(k) = A(k) + B(k) * p_surface.

Vectors A and B have length Nz + 1 (level interfaces / half-levels).

source
AtmosTransport.Grids.LatLonMesh Type
julia
LatLonMesh{FT} <: AbstractStructuredMesh

Regular latitude-longitude mesh on the sphere.

Fields

  • Nx, Ny — number of cells in longitude and latitude

  • Δλ, Δφ — uniform spacing [degrees]

  • λᶜ, λᶠ — cell-center and cell-face longitudes [degrees]

  • φᶜ, φᶠ — cell-center and cell-face latitudes [degrees]

  • radius — planet radius [m]

source
AtmosTransport.Grids.LatLonMesh Method
julia
LatLonMesh(; Nx, Ny, longitude=(-180, 180), latitude=(-90, 90), ...)

Construct a regular latitude-longitude mesh.

Coordinate conventions

  • Longitude: cell faces at λᶠ[1..Nx+1] spanning [λ_west, λ_east], cell centers at midpoints λᶜ[i] = (λᶠ[i] + λᶠ[i+1]) / 2. Default range (-180, 180)λᶜ[1] = -178.125° for Nx=96. IMPORTANT: the spectral preprocessor's FFT synthesis (spectral_to_grid!) must use lon_shift_rad = deg2rad(λᶜ[1]) to align its output with this mesh. Using dlon/2 instead causes a 180° shift for (-180, 180) meshes (see 2026-04-11 bug fix in spectral_synthesis.jl).

  • Latitude: cell faces at φᶠ[1..Ny+1] spanning [φ_south, φ_north], cell centers at φᶜ[j]. Default (-90, 90)φᶜ[1] = -88.125° for Ny=48. Latitude is NOT periodic (closed pole boundaries).

Indexing (column-major)

3D arrays on this mesh are (Nx, Ny, Nz) where:

  • i (lon) is the fastest-varying index (column-major, Julia/Fortran style)

  • j (lat) is the middle index

  • k (level) is the slowest-varying index; k=1 is TOA, k=Nz is surface

The flat cell index is c = i + (j-1) × Nx (1-based). Face arrays for the Arakawa C-grid are am[Nx+1, Ny, Nz] (zonal, at west edges) and bm[Nx, Ny+1, Nz] (meridional, at south edges).

source
AtmosTransport.Grids.PanelConnectivity Type
julia
PanelConnectivity

Complete edge-to-edge connectivity for a 6-panel cubed sphere.

neighbors[p][e] gives the PanelEdge for panel p, edge e, where edges are numbered by local index side: 1=+Y, 2=-Y, 3=+X, 4=-X.

source
AtmosTransport.Grids.PanelEdge Type
julia
PanelEdge

A connection from one panel edge to a neighbor panel's edge.

Fields

  • panel :: Int — neighbor panel index (1-6)

  • orientation :: Int — along-edge direction code (0=aligned, 2=reversed)

source
AtmosTransport.Grids.ReducedGaussianMesh Type
julia
ReducedGaussianMesh{FT} <: AbstractHorizontalMesh

Reduced Gaussian mesh with variable longitude count per latitude ring.

Fields

  • latitudes – ring-center latitudes [degrees], ordered south -> north

  • nlon_per_ring – number of cells on each latitude ring

  • ring_offsets – 1-based start indices into the flattened cell array

  • lat_faces – latitude boundaries between rings [degrees]

  • boundary_counts – number of meridional face segments on each ring boundary

  • boundary_offsets – 1-based start offsets within the meridional-face block

  • radius – planet radius [m]

Flattening convention

Cells are flattened ring-by-ring from south to north. Within a ring, cell i is ordered west to east on a periodic longitude partition [0, 360).

Geometry note

The ring boundaries are defined by midpoint latitudes with pole caps at -90 and +90 degrees. This preserves total surface area exactly while the exact Gaussian control-volume boundaries are still being wired in.

source
AtmosTransport.Grids.StructuredFluxTopology Type
julia
StructuredFluxTopology <: AbstractFluxTopology

Fluxes stored as separate directional arrays (am, bm, cm) on a logically rectangular grid.

source
AtmosTransport.Grids.EquiangularCubedSphereDefinition Method
julia
EquiangularCubedSphereDefinition(; convention=GnomonicPanelConvention(),
                                  longitude_offset_deg=0)

Legacy synthetic/equiangular cubed-sphere definition: EquiangularGnomonic corners plus AngularMidpointCenter centers.

source
AtmosTransport.Grids.GEOSIT_C180 Method
julia
GEOSIT_C180(; FT=Float64, Hp=1, radius=6.371e6)
GEOSFP_C720(; FT=Float64, Hp=1, radius=6.371e6)

Convenience constructors for the two native GMAO cubed-sphere targets used for GEOS-IT and GEOS-FP comparisons.

source
AtmosTransport.Grids.GMAOCubedSphereDefinition Method
julia
GMAOCubedSphereDefinition(; convention=GEOSNativePanelConvention(),
                           longitude_offset_deg=-10)

Native GMAO/GEOS cubed-sphere definition used by GEOS-IT C180 and GEOS-FP C720 files:

  • GMAOEqualDistanceGnomonic edge/corner law,

  • FourCornerNormalizedCenter (cell_center2) center law,

  • GEOSNativePanelConvention by default,

  • final -10° longitude shift away from the Japan corner.

source
AtmosTransport.Grids.b_diff Method
julia
b_diff(vc::AbstractVerticalCoordinate, k)

Difference of B coefficients across level k: B[k+1] - B[k]. Used for distributing surface pressure tendency across levels.

source
AtmosTransport.Grids.cell_area Function
julia
cell_area(mesh::AbstractHorizontalMesh, c) -> FT

Area [m²] of cell c.

c can be:

  • A flat integer index (column-major: c = i + (j-1)*Nx for structured)

  • A tuple (i, j) for structured meshes

Both forms must be implemented by all concrete mesh types.

source
AtmosTransport.Grids.cell_area Method

Cell area [m²] at local panel indices (i, j). Same on all 6 panels by gnomonic symmetry.

source
AtmosTransport.Grids.cell_area Method

Cell area [m²] from flat cell index c (column-major: c = i + (j-1)*Nx).

source
AtmosTransport.Grids.cell_area Method

Cell area [m²] at structured index (i, j).

source
AtmosTransport.Grids.cell_areas_by_latitude Method

Cell area vector (precomputed, one per latitude band).

source
AtmosTransport.Grids.cell_faces Function
julia
cell_faces(mesh::AbstractHorizontalMesh, c) -> indices

Indices of faces bounding cell c. For structured meshes this returns the 4 (or 6 for hex) face indices; for unstructured meshes it uses the CSR adjacency.

source
AtmosTransport.Grids.cell_faces Method
julia
cell_faces(m::LatLonMesh, c) -> (west, east, south, north)

Indices of the 4 faces bounding cell c. Accepts either a flat cell index (column-major: c = i + (j-1)*Nx) or a tuple (i, j).

Returns face indices consistent with the face_length/face_normal/face_cells numbering: X-faces 1:(Nx+1)_Ny, then Y-faces (Nx+1)_Ny+1 : nfaces.

source
AtmosTransport.Grids.center_law Method

Return the center law carried by mesh.definition.

source
AtmosTransport.Grids.coordinate_law Method

Return the coordinate law carried by mesh.definition.

source
AtmosTransport.Grids.cs_definition Method

Return the full cubed-sphere geometry definition carried by mesh.

source
AtmosTransport.Grids.default_panel_connectivity Method
julia
default_panel_connectivity() -> PanelConnectivity

Return the GEOS-FP native cubed-sphere panel connectivity.

Edge-to-edge connections (Panel p local edge → Panel q local edge): P1 +Y→P3 -X(rev) P1 -Y→P6 +Y(aln) P1 +X→P2 -X(aln) P1 -X→P5 +Y(rev) P2 +Y→P3 -Y(aln) P2 -Y→P6 +X(rev) P2 +X→P4 -Y(rev) P2 -X→P1 +X(aln) P3 +Y→P5 -X(rev) P3 -Y→P2 +Y(aln) P3 +X→P4 -X(aln) P3 -X→P1 +Y(rev) P4 +Y→P5 -Y(aln) P4 -Y→P2 +X(rev) P4 +X→P6 -Y(rev) P4 -X→P3 +X(aln) P5 +Y→P1 -X(rev) P5 -Y→P4 +Y(aln) P5 +X→P6 -X(aln) P5 -X→P3 +Y(rev) P6 +Y→P1 -Y(aln) P6 -Y→P4 +X(rev) P6 +X→P2 -Y(rev) P6 -X→P5 +X(aln)

The table is generated from the same GEOS-native corner geometry as panel_cell_corner_lonlat(mesh, p), so balance, treeify, and NetCDF output share one convention contract.

source
AtmosTransport.Grids.face_cells Function
julia
face_cells(mesh::AbstractHorizontalMesh, f) -> (left, right)

The two cells sharing face f. Convention: flux from left to right is positive. Boundary faces use 0 or a sentinel for the exterior cell.

source
AtmosTransport.Grids.face_cells Method

For structured LatLon, faces are implicitly indexed:

  • X-faces: index f in 1:(Nx+1)*Ny → face between (i-1,j) and (i,j), periodic in x

  • Y-faces: index f in 1:Nx*(Ny+1) → face between (i,j-1) and (i,j), bounded in y

Direct (i,j,k) array access is preferred over these for performance.

source
AtmosTransport.Grids.face_cells Method
julia
face_cells(m, f) -> (left, right)

Return the two cell indices adjacent to face f. For the face-indexed transport operator, flux through face f goes from left to right (positive flux = transfer from left to right).

Face types

Faces 1..ncells are zonal (X) faces within each ring:

  • Face f sits between the western cell left_i = i-1 (periodic wrap) and the eastern cell right_i = i in the same ring j.

  • These faces are always interior (both indices > 0).

Faces ncells+1..nfaces are meridional (Y) boundary faces between adjacent latitude rings (or at the poles):

  • South pole cap (boundary 1): left = 0 (pole singularity), right = cell on ring 1. A face_left=0 tells the transport kernel to treat this as a boundary — no flux accumulation on the left side.

  • North pole cap (boundary nrings+1): left = cell on last ring, right = 0 (pole singularity).

  • Interior boundaries (boundary b, between ring b-1 and ring b): the boundary has lcm(nlon[b-1], nlon[b]) segments. Segment seg maps to cell south_i in ring b-1 and cell north_i in ring b via integer division: cell_i = ((seg-1) × nlon_ring) ÷ nseg + 1. This distributes boundary segments evenly among cells.

source
AtmosTransport.Grids.face_length Function
julia
face_length(mesh::AbstractHorizontalMesh, f) -> FT

Length [m] of face f (or area of the face cross-section for 3D).

source
AtmosTransport.Grids.face_length Method
julia
face_length(m::LatLonMesh, f) -> FT

Length [m] of face f using the universal face-indexed API.

X-faces (f ∈ 1:(Nx+1)_Ny) return the meridional edge length. Y-faces (f ∈ (Nx+1)_Ny+1 : nfaces) return the zonal edge length.

source
AtmosTransport.Grids.face_normal Function
julia
face_normal(mesh::AbstractHorizontalMesh, f) -> (nx, ny)

Unit normal of face f in logical coordinates (not geographic).

For structured meshes, components are in the (i, j) index directions:

  • X-faces → (1, 0) (positive = increasing i = eastward on LatLon)

  • Y-faces → (0, 1) (positive = increasing j = northward on LatLon)

For unstructured meshes, implementations should return components in a local tangent-plane frame defined per face (e.g., east/north or panel-local). The frame convention MUST be documented by each mesh type so that flux-signing is unambiguous.

The transport operators multiply face_normal by face_length and the face flux to compute the signed contribution to cell convergence. Consistent normal orientation between face_cells (left → right = positive) and face_normal is required.

source
AtmosTransport.Grids.face_normal Method
julia
face_normal(m::LatLonMesh, f) -> (nx, ny)

Unit normal of face f. X-faces → (1, 0), Y-faces → (0, 1).

source
AtmosTransport.Grids.gnomonic_panel_connectivity Method
julia
gnomonic_panel_connectivity() -> PanelConnectivity

Return the classical gnomonic cubed-sphere panel connectivity.

Panels 1-4 are the equatorial belt (+x, +y, -x, -y faces); panel 5 is the north polar cap (+z face); panel 6 is the south polar cap (-z face).

This matches the gnomonic ordering used by the ERA5-CS preprocessor and all CS transport binaries written by open_streaming_cs_transport_binary.

Edge-to-edge connections (P→Q with orientation 0=aligned, 2=reversed): P1 N→P5_S(0) P1 S→P6_N(0) P1 E→P2_W(0) P1 W→P4_E(0) P2 N→P5_E(0) P2 S→P6_E(2) P2 E→P3_W(0) P2 W→P1_E(0) P3 N→P5_N(2) P3 S→P6_S(2) P3 E→P4_W(0) P3 W→P2_E(0) P4 N→P5_W(2) P4 S→P6_W(0) P4 E→P1_W(0) P4 W→P3_E(0) P5 N→P3_N(2) P5 S→P1_N(0) P5 E→P2_N(0) P5 W→P4_N(2) P6 N→P1_S(0) P6 S→P3_S(2) P6 E→P2_S(2) P6 W→P4_S(0)

source
AtmosTransport.Grids.ncells Function
julia
ncells(mesh::AbstractHorizontalMesh) -> Int

Total number of horizontal cells in the mesh.

source
AtmosTransport.Grids.ncells Method

Total number of cells across all 6 panels: 6 × Nc².

source
AtmosTransport.Grids.nfaces Function
julia
nfaces(mesh::AbstractHorizontalMesh) -> Int

Total number of horizontal faces in the mesh.

source
AtmosTransport.Grids.nfaces Method

Total number of faces across all 6 panels: 6 × 2 × Nc × (Nc + 1) (x + y faces per panel).

source
AtmosTransport.Grids.nx Function
julia
nx(mesh::AbstractStructuredMesh) -> Int
ny(mesh::AbstractStructuredMesh) -> Int

Logical grid dimensions for structured meshes.

source
AtmosTransport.Grids.nx Method

Cells per panel edge (same in both x and y by construction).

source
AtmosTransport.Grids.panel_cell_center_lonlat Method
julia
panel_cell_center_lonlat(Nc, panel, FT) -> (lons, lats)
panel_cell_center_lonlat(mesh::CubedSphereMesh, panel) -> (lons, lats)

Return (Nc, Nc) arrays of cell-center longitudes and latitudes in degrees for the given panel.

The Nc method is the classical gnomonic convention. The mesh method honors panel_convention(mesh), including GEOS-native panel ordering and orientation.

source
AtmosTransport.Grids.panel_cell_corner_lonlat Method
julia
panel_cell_corner_lonlat(Nc, panel, FT) -> (lons, lats)
panel_cell_corner_lonlat(mesh::CubedSphereMesh, panel) -> (lons, lats)

Return (Nc+1, Nc+1) arrays of cell-corner longitudes and latitudes in degrees. The mesh method honors panel_convention(mesh).

source
AtmosTransport.Grids.panel_cell_local_tangent_basis Method
julia
panel_cell_local_tangent_basis(mesh, panel)
    -> (x_east, x_north, y_east, y_north)

Return four (Nc, Nc) matrices describing the local panel-coordinate unit vectors at cell centers in geographic (east, north) components.

For each cell, (x_east[i,j], x_north[i,j]) is the unit vector for increasing local X (i) and (y_east[i,j], y_north[i,j]) is the unit vector for increasing local Y (j). The helper honors panel_convention(mesh), including GEOS-native Y-reversed panels, and is the geometry contract used by preprocessing wind rotation.

source
AtmosTransport.Grids.panel_connectivity_for Method
julia
panel_connectivity_for(convention) -> PanelConnectivity

Return the PanelConnectivity that matches the given panel-numbering convention. Dispatches to gnomonic_panel_connectivity() or default_panel_connectivity() so that the mesh and all downstream code automatically use the right edge table.

source
AtmosTransport.Grids.panel_convention Method

Return the panel-numbering convention struct (Gnomonic or GEOSNative).

source
AtmosTransport.Grids.panel_count Method

Number of panels (always 6 for a cubed sphere).

source
AtmosTransport.Grids.panel_labels Method
julia
panel_labels(convention) -> NTuple{6, Symbol}

Return symbolic labels for each panel under the given convention.

  • Gnomonic: (:x_plus, :y_plus, :x_minus, :y_minus, :north_pole, :south_pole) — panels 1-4 are equatorial (centred on ±x, ±y axes), 5 and 6 are polar.

  • GEOS native: (:equatorial_1, ..., :north_pole, ..., :south_pole) — panels 1-2 and 4-5 are equatorial, 3 is north pole, 6 is south pole. This matches the file-panel ordering in GEOS-FP/GEOS-IT NetCDF variables.

source
AtmosTransport.Grids.reciprocal_edge Method
julia
reciprocal_edge(conn, p, e) -> Int

Find which edge of the neighbor panel connects back to panel p edge e.

source
AtmosTransport.Grids.ring_longitudes Method
julia
ring_longitudes(m, j) -> Vector{FT}

Return the cell-center longitudes for ring j, in [0°, 360°) convention with half-cell offset: lon[i] = (i − 0.5) × (360° / nlon).

For nlon = 96: [1.875, 5.625, ..., 358.125].

This convention matches the spectral synthesis spectral_to_ring! which uses lon_shift_rad = π / nlon to place FFT output at cell centers.

source