Coordinate systems

Available geometries

AGILE supports five coordinate settings: Cartesian (x, y, z), spherical (r, θ, φ), cylindrical (r, z, φ), and the two logarithmic-radius variants logSpherical and logCylindrical (the same two curvilinear systems with a radial mesh that is uniform in ln(r) rather than in r -- see Logarithmically stretched radius below). All are always 3D, so a curvilinear run is the full 3D system, never an axisymmetric or 1D/2D reduction.

The coordinate system is a compile-time choice: the finite-volume kernels are specialized for it, so that the (much cheaper) Cartesian kernel stays free of curvilinear bookkeeping. It is selected via geometry in &meshlist of the parameter file:

&meshlist
   geometry = 'spherical'   ! or 'cylindrical', 'logSpherical',
                            ! 'logCylindrical', or 'Cartesian' (the default)
/

logSpherical/logCylindrical are a spherical/cylindrical build (GEOM, and the runtime coordinate, stay 'spherical'/'cylindrical') plus a LOG_RADIUS flag; every geometry-specific branch stays correct untouched and only the radial spacing differs.

Conventions to know when setting up a curvilinear case:

  • Angular domain bounds are given in units of 2π. So for spherical, xprobmin2 = 0.125d0, xprobmax2 = 0.375d0 places θ between π/4 and 3π/4. For cylindrical, only the φ bound (xprobmin3/xprobmax3) gets this treatment; the z bound (xprobmin2/xprobmax2) is a plain length, exactly like r.
  • Axis order is (r, θ, φ) for spherical and (r, z, φ) for cylindrical -- in both cases φ is the third coordinate direction.
  • Under logSpherical/logCylindrical, xprobmin1/xprobmax1 are physical radii in the parameter file and are converted to the logical ln-radius coordinate by read_par_files. A case's mod_usr.fpp that reads those bounds for a physical radius must map them back with r_of_s from mod_geometry (not by hand -- the map is only a plain exponential when log_r0 = 0).
  • Cell positions ps(igrid)%x are the volume barycentre of the cell, not the midpoint of its faces -- so x ± dx/2 is not a cell face. See Cell positions are volume barycentres below.

Cell positions are volume barycentres

ps(igrid)%x -- which every case's mod_usr.fpp reads -- holds the volume barycentre of the cell. A cell average equals the point value at the barycentre to second order (the linear term of the Taylor expansion integrates to zero only about that point), so the barycentre is where the state, the initial condition and any position-dependent source term belong. Only the directions with a non-uniform volume weight are affected: r in both systems, and θ in spherical. φ in both, and z in cylindrical, carry uniform weight, so their barycentres are their midpoints. The offset is about 1% of a cell width radially on the test grids and grows to dθ/6 -- a sixth of a cell, pushed away from the axis -- for the first polar cell off θ = 0.

r_L r_R less volume more volume r_c = 1/2(r_L + r_R) face midpoint — the old stored x r-bar — volume barycentre, +O(h²) outward spherical: r-bar = 3/4·(r_R⁴ − r_L⁴)/(r_R³ − r_L³) shift ≈ 1% of dr radially · grows to dθ/6 for the first cell off θ = 0
The point moves outward to the centre of volume. Only directions with a non-uniform volume weight are affected: the radius in both systems, and θ in spherical. φ (both) and cylindrical z carry uniform weight, so their barycentres already were their midpoints. fill_geometry_device evaluates the offset forms with the common factor cancelled analytically, because computing an O(h²) quantity as the difference of two O(1) ones would throw away most of the mantissa on a fine grid.

fill_geometry_device is written face-first: the radial-cell macro produces the two faces, the midpoint and the extent, and every area and volume is then written in terms of those so the expressions stay algebraically exact for any face pair. Three places in the tree that assumed x ± dx/2 was a face -- the face coordinates handed to the Riemann solver in mod_finite_volume, the VTU corner grid in calc_x, and fill_geometry_device itself -- rebuild the face from rnode instead. bgeo%ds deliberately stays on the cell midpoint: it is a geometric extent that setdt turns into the CFL length, and following the barycentre there would quietly relax the timestep.

Why curvilinear coordinates need extra source terms

AGILE discretizes the generic conservation law

(see Physics modules and equations) with a finite-volume update that divides the net flux through a cell's faces by the cell volume. In spherical or cylindrical coordinates the face areas and cell volume depend on position (e.g. a spherical shell's outer face is larger than its inner face), so this ratio is what actually implements the curvilinear divergence for scalar quantities -- no extra term is needed for the mass, energy, or (for ffhd) the field-aligned quantities, since these are advected as genuine scalars and Gauss's theorem then guarantees the finite volume update alone reproduces correctly in any coordinate system.

Momentum (and, for mhd, the magnetic field) is different: it is a vector quantity, and its flux is a tensor built from a position-dependent (r, θ, φ) or (r, z, φ) basis. Writing the vector Laplacian/divergence out in curvilinear coordinates leaves over extra terms beyond the flux-divergence that the finite-volume update captures -- physically, these account for how the basis vectors themselves change from one face of a cell to the other. AGILE adds these leftover terms explicitly as a per-cell geometric source, in the addsource_geometry routine of each physics module (src/{hd,mhd,srhd,ffhd}/mod_*_templates.fpp); a physics module that does not implement one aborts at startup rather than silently omitting these terms. The explicit expressions below are exactly what these routines evaluate (rewritten into physical notation matching Physics modules and equations; see the source files for the exact discretization, in particular how the isotropic-pressure-like prefactor is evaluated from the actual discrete face areas rather than plugging in the continuous coordinate factor directly, so that a uniform pressure produces no net force to the same precision the code otherwise resolves -- for cylindrical this recovers the continuous prefactor exactly, since the radial face area of a cylindrical cell is linear in r). Because those discrete factors are the exact volume averages of the continuous 2/r, cot(θ)/r (spherical) or 1/r (cylindrical) prefactors for any face pair, no curvature term depends on where in the cell the stored position sits, and addsource_geometry takes no position argument -- which is what keeps it correct now that ps%x is the volume barycentre rather than the face midpoint.

Geometric source terms

The equations below give the additional source term that a curvilinear run adds to the corresponding equation from Physics modules and equations; everywhere else the equations are unchanged. ffhd needs none, for any geometry (see above), so it is omitted below beyond noting .

Spherical (r, θ, φ)

Hydrodynamics: hd

Magnetohydrodynamics: mhd

With the total (thermal + magnetic) pressure :

where ψ is the GLM field (see Divergence B source treatments); ψ itself has no geometric source.

Special relativistic hydrodynamics: srhd

Structurally identical to the hd case, with the momentum density (rather than ) and using the spatial four-velocity components (the primitive momentum-slot variables; ), so that :

with as in Physics modules and equations.

Cylindrical (r, z, φ)

Only the / (and, for mhd, /) components pick up a geometric source; / do not, since a cylindrical volume element's z-extent does not depend on r.

Hydrodynamics: hd

Magnetohydrodynamics: mhd

With :

Special relativistic hydrodynamics: srhd

As for spherical, with and :

Logarithmically stretched radius

geometry = 'logSpherical' and geometry = 'logCylindrical' place the radial mesh uniformly in ln(r + r0) rather than in r. The offset r0 is the log_r0 entry of &meshlist and defaults to zero, the plain ln(r): the cell width then grows in proportion to r, so the relative resolution dr/r is constant -- the one stretching astrophysics usually wants, and the only one supported.

The design principle is that taking the logical radial coordinate to be ξ = ln(r + r0) leaves the mesh uniform in ξ, which is exactly what the geometry kernel already assumes -- an analytic function of the block corner and a constant spacing. The tree, the AMR level ladder, rnode, prolongation, coarsening and the ghost exchange all keep working unchanged in ξ; only the step from logical faces to physical ones takes an exp. It needs none of upstream MPI-AMRVAC's general stretched-grid machinery (qstretch, dxfirst, per-level stretch tables) -- which AGILE has removed -- and refinement and log stretching commute for free.

A positive log_r0 moves the singularity of the map from r = 0 out to r = −r0, making the mesh genuinely uniform (cell width r0·dξ) for r ≪ r0 and logarithmic for r ≫ r0. That is what lets a stretched radial grid reach r = 0: with the default log_r0 = 0 a log-radial grid can never reach the axis, so xprobmin1 ≤ 0 is rejected unless log_r0 > 0. logCylindrical with log_r0 > 0 therefore supports the cylindrical axis; logSpherical still has no usable origin (nothing fills the ghost cells across r = 0).

uniform in r r=0 uniform in ln(r) r=0 unreachable r_min > 0 s = ln(1 + r/r0) s = 0 = r = 0 (exactly) mirror: odd about s = 0 uniform for r ≪ r0 · logarithmic for r ≫ r0
Reaching the axis. With the default log_r0 = 0 the map is plain ln(r) and cannot reach r = 0. A positive log_r0 moves the singularity out to r = −r0, so the mesh is genuinely uniform near the axis and logarithmic far out. Three properties of that form are deliberate: the pivot is the axis (s = 0 is r = 0 exactly, so rnode's corner is exactly zero); the map is odd, so the pole copy fills a ghost cell from an interior cell of mirrored volume; and the odd branch is confined to the ghost layer, so the physical domain still uses the cheaper log_ra·exp(s) + log_rb and a plain log_r0 = 0 build is bit-for-bit unchanged.

Two limitations of the stretching:

  • The reconstruction stays uniform-stencil. The MUSCL reconstruction and the limiters assume equal cells, so the second-order constant degrades as the stretch ratio grows. Keep it modest -- the test cases run at ratios around 1.04 to 1.09 per cell. A positive log_r0 helps here, since the mesh is genuinely uniform in the region next to the axis where the stencil assumption would otherwise be worst.
  • The timestep tightens. dr = (r + r0)·dξ, so a small log_r0 chosen for near-axis resolution buys a CFL restriction on top of the one the polar axis already imposes.

The polar axis

A spherical domain may span the full θ from 0 to π, and a cylindrical one may start at r = 0. The axis is not a boundary: a ghost cell just past the axis at azimuth φ is the interior cell at φ + π, so it is filled from the block half a revolution away (a "pi-periodic" treatment) rather than by a boundary condition. Setting it up takes three things in &meshlist/&boundlist, and check_pole_setup stops the run with a specific message if any is missing:

  • 'pole' on the axis face(s) -- typeboundary_min2/max2 in spherical, typeboundary_min1 in cylindrical; it is all-or-nothing per face.
  • φ periodic over exactly one full turn (xprobmin3 = 0, xprobmax3 = 1 in 2π units, periodic at both φ faces).
  • an even number of level-1 blocks across φ.

The sign rule is the same in both systems: the component along the mirrored direction flips, the φ component flips, everything else is symmetric. Note that this makes the radial component odd at the cylindrical axis (m_r, B_r as well as m_φ, B_φ) -- upstream MPI-AMRVAC marks only the φ component antisymmetric there, which is wrong.

The polar axis is supported for hd, mhd, srhd and ffhd. Two things to know:

  • The timestep collapses near the axis. ds(φ) = r·sin(θ)·dφ → 0 there, so setdt gives a severe CFL restriction. No axis filtering or ring averaging is implemented; keep pole cases at modest resolution.
  • Volume averages cannot validate the pole copy -- a cell on the axis has vanishing volume and a zero-area axis face, so a wrong ghost value barely moves mean(w). The pole test cases therefore assert on the ghost cells directly against the analytic state at it = 0.

Current limitations

  • AMR across curvilinear refinement levels is only lightly tested. Prolongation and coarsening volume-weight correctly, but fix_conserve is disabled for all geometries in src/mod_advance.fpp.
  • General grid stretching is unavailable. A stretch_dim that varied the cell spacing within a block could not be expressed by the analytic metric derivation, so upstream MPI-AMRVAC's stretched-grid machinery was removed and the stretch_dim / qstretch_baselevel / nstretchedblocks_baselevel / stretch_uncentered keys are no longer read from &meshlist. The logarithmic radius above is the one supported non-uniform mesh, precisely because it is still uniform in ξ = ln(r + r0).
  • divvector/curlvector in src/mod_geometry.fpp still assume a face lies midway between two cell centres, which is untrue for the volume barycentre. Neither routine is on a path this fork currently exercises.