Staggered finite volumes in the annulus
MILEVA discretises the Stokes equations on a staggered (marker-and-cell) grid in polar coordinates assemble_stokes in solvers/stokes_assembly.jl; the 3-D assembly is assemble_stokes_3d in solvers/stokes_assembly_3d.jl. Every 2-D Stokes solver (direct, MUMPS, BiCGSTAB, multigrid) calls the same assembly and differs only in how it solves the resulting matrix, so the discrete problem does not depend on the solver choice.
Geometry: one exponent
The meshes themselves, and the four geometries MILEVA runs on (2-D spherical and cylindrical annulus, 3-D chunk and Yin-Yang sphere), are drawn on Grids and geometries.
Both 2-D geometries share the same mesh. They differ only in how the area of a shell grows with radius, geometry_exponent(cfg) in utils/geometry.jl returns that exponent:
cfg.geometry | Measure | Meaning | |
|---|---|---|---|
"spherical_annulus" (default) | 2 | equatorial slice of a spherical shell (Hernlund & Tackley 2008) | |
"cylindrical_annulus" | 1 | cross-section of an infinite cylinder |
Every operator that needs a radial metric asks for "annulus" is refused by validate! in config/SimConfig.jl, because the two geometries differ by a factor of about 1.95 in the core-to-surface area ratio for a Venus-sized body and a silent choice changes the answer.
Use the spherical annulus
The spherical annulus (Hernlund & Tackley 2008) gives the core-mantle boundary the area ratio of a sphere, and published reference values exist for it (Hernlund & Tackley 2008; the "Annulus" column of Fleury et al. 2024). All examples and benchmarks in this documentation use it. The cylindrical setting is kept so older results can be reproduced.

What the one exponent changes. Left: the steady conductive temperature that get_initial_profile returns for the default shell with geometry_area_ratio against docs/scripts/fig_numerics_discretization_sections.jl.
Staggered layout (2-D)
| Field | Array size | Location |
|---|---|---|
| radial faces | ||
| angular faces, cell-centre radii | ||
| cell centres |
The angle v_idx(i,j) = (i-1)*Nt + j), which keeps the periodic wrap-around connection at a bandwidth of about assemble_stokes records that this reduced LU fill-in from about

Left: where the unknowns of one block of cells sit, pressure, temperature and viscosity at cell centres, setup_grid returns for the default shell at amrStretchFactor 2 and 3, which refine both boundary layers at the expense of the mid-mantle (see Radial grid stretching).
Non-dimensional, row-equilibrated system (2-D)
assemble_stokes builds the system in non-dimensional variables, with the layer thickness
Each momentum row is additionally multiplied by the square of its local radial spacing, and after assembly every row is divided by its largest entry (row equilibration). unpack_stokes converts the solution back to SI units with meta.V_scale and meta.P_scale.

Left: the nonzeros of the matrix assemble_stokes returns for a docs/scripts/fig_numerics_discretization_sections.jl.
The viscous operator is ∇·(2η ε̇)
With cfg.useDeviatoricStress = true (the default) the viscous block is factored in _viscous_block_deviatoric:
Here
and the momentum rows are
Because the operator is built as a product,
For incompressible flow useDeviatoricStress = false selects the form

A hot blob beside a stiff inclusion (viscosity raised 1000 times, white contours at 10 and 100 times the background) in a assemble_stokes twice. Left: the speed from the default factored operator useDeviatoricStress = false), up to about a quarter of the peak speed, concentrated where the flow shears past the inclusion; that form returns docs/scripts/fig_numerics_discretization_sections.jl.
How the operator is checked
Two properties hold exactly for any correct Stokes operator and are asserted in tests/test_annulus_operator.jl:
- Work equals dissipation. For a Stokes solution under a body force
,
so
| Viscosity contrast | |
|---|---|
| 1 | 0.995 / 0.999 / 1.000 |
| 10 | 0.996 / 0.999 / 1.000 |
| 1.002 / 1.001 / 1.001 | |
| 1.019 / 1.006 / 1.003 |
- Rigid rotation is a null mode.
for rigid rotation in every geometry. The deviatoric operator annihilates it to round-off in the spherical annulus as well ( , , at = 12, 24, 48).
Rigid rotation alone cannot detect a missing

Both checks recomputed with the assembled operator. Left: docs/scripts/fig_numerics_discretization_sections.jl.
Continuity, pressure and boundary conditions
The continuity row at each cell is
cfg.pPen adds a pressure penalty to this row and is 0 by default. The staggered grid is inf-sup stable and needs no stabilisation; with no penalty the direct solvers return round-off divergence everywhere.
Walls.
at and . Free slip is zero shear traction, , imposed by dropping the wall shear flux. Pressure null space. One pressure DOF near the domain centre is pinned. All 2-D solvers now pass
point_pin=true(a single identity row). The older dense "mean pressure = 0" row is still available forpoint_pin=false; it replaces a continuity row, couples every pressure unknown, and is unusable by a local multigrid smoother.Rotation null space. One
DOF is pinned in the assembly. After every 2-D solve main_SphCon.jlthen removes the net rotation, the rigid rotationthat makes vanish ( _net_rotation_rate, gated bycfg.removeNetRotation, default true). This projection has no effect in 3-D and the code warns if it is requested there.

Left: the discrete divergence of each cell (the continuity row above, in units of pPen = 0 the divergence is round-off, point_pin = true the longest row has 11 entries and the rows of length 1 are the wall and pin identities. Script: docs/scripts/fig_numerics_discretization_sections.jl.
Radial grid stretching
_amr_radial_grid in utils/setup_grid.jl places the
with cfg.amrStretchFactor (default 0, uniform). Cells are smallest at both boundaries and largest at mid-depth; the source quotes
| Field | Size | Meaning |
|---|---|---|
grid.rF | face radii | |
grid.r | cell-centre radii | |
grid.drVec | cell widths | |
grid.drFVec | centre-to-centre spacing at each face, half a cell at the walls | |
grid.dr | scalar | the minimum cell width (used by the CFL limit) |
The 2-D Stokes, diffusion, advection and marker routines all use drVec and drFVec. In 3-D the same radial map is used, and the tangential axes can be stretched in the same way through cfg.amrStretchTheta and cfg.amrStretchPhi (_amr_angular_grid).
On a stretched grid grid.dr is minimum(drVec), so a new radial derivative should use the local widths drVec or drFVec, and radial_cell_index (or searchsortedfirst on grid.r / grid.rF) to locate a radius.
The 2-D lateral spacing is uniform: MutableGrid2D.dth is a scalar and there is no lateral refinement in 2-D.

Left: the face positions _amr_radial_grid returns for docs/scripts/fig_numerics_discretization_sections.jl.
The 3-D operator
assemble_stokes_3d integrates every row over its control volume so that the pressure-gradient block equals minus the transpose of the divergence block, verify_g_minus_bt in the same file checks it on an assembled matrix. Measured on an
Layout.
on radial faces , on colatitude faces , and at , with on the right face of cell . Full stress.
cfg.useCurvatureTerms = true(default) assembles the divergence of the full deviatoric stress with all six spherical strain components. Thefalsebranch is the componentwise scalar Laplacian. Measured on the two branches at= 8 / 12 / 16: rigid-rotation residual / / (true) against / / (false), and = 1.004 / 0.997 / 0.997 against 0.559 / 0.724 / 0.813. Variable viscosity. With a radial viscosity ramp on a Yin-Yang chunk,
at = 8 / 12 / 16 is 0.9961 / 1.0026 / 1.0029 at contrast 1 and 0.9812 / 0.9813 / 0.9841 at , improving under refinement at every contrast ( tests/test_stokes_3d_variable_eta.jl).Face viscosities. With
cfg.useGeometricEta = true(default) viscosities at velocity faces and edges are geometric means,for two cells and for four, following Tackley (2008, §3.2), who advises against arithmetic interpolation at large contrast. This applies to the 3-D assembly; the 2-D deviatoric uses the arithmetic four-cell mean. edges. Each Yin-Yang patch is a non-periodic rectangle in ; the viscous stencil is guarded at and , and the walls take values from the other patch through the Schwarz iteration.

Left: the nonzeros of the matrix assemble_stokes_3d returns for a verify_g_minus_bt); all 3378 pairs agree to the last bit, docs/scripts/fig_numerics_discretization_sections.jl.
Where in the code
| Topic | File | Function |
|---|---|---|
| Radial exponent | utils/geometry.jl | geometry_exponent |
| 2-D grid and stretching | utils/setup_grid.jl | _setup_grid_2d, _amr_radial_grid |
| 2-D Stokes assembly | solvers/stokes_assembly.jl | assemble_stokes, _viscous_block_deviatoric, unpack_stokes |
| 3-D Stokes assembly | solvers/stokes_assembly_3d.jl | assemble_stokes_3d, unpack_stokes_3d, verify_g_minus_bt |
| Net-rotation removal | main_SphCon.jl | _net_rotation_rate |
| Operator tests | tests/ | test_annulus_operator.jl, test_spherical_operator_nullmode.jl, test_stokes_3d_variable_eta.jl, test_energy_consistency.jl |