Nonuniform dz - #79
Conversation
| __device__ | ||
| inline int height_to_int(const Float z, const Float* z_lev, const int* z_lut, const Float lut_dz, const int lut_size, const int ntot_max) | ||
| { | ||
| int k = z_lut[min(max(static_cast<int>(z / lut_dz), 0), lut_size-1)]; |
There was a problem hiding this comment.
here we are finding which physical layer the possible collision occurs. k is an index for z_lev such that z_lev[k] <= z < z_lev[k+1]
| while (k > 0 && z < z_lev[k]) | ||
| --k; | ||
| while (k < ntot_max-1 && z >= z_lev[k+1]) | ||
| ++k; |
There was a problem hiding this comment.
this is a correction, a bin in the lookup table can overlap with at most two layers. the LUT stores the layer of the bin's lower edge so it's a possible upward correction.
| inline int height_to_int(const Float z, const Float* z_lev, const int* z_lut, const Float lut_dz, const int lut_size, const int ntot_max) | ||
| { | ||
| int k = z_lut[min(max(static_cast<int>(z / lut_dz), 0), lut_size-1)]; | ||
| while (k > 0 && z < z_lev[k]) |
There was a problem hiding this comment.
for floating point safety
| dz_min = std::min(dz_min, z_lev({k+2}) - z_lev({k+1})); | ||
|
|
||
| const Float zsize = z_lev({nz+1}); | ||
| const int lut_size = static_cast<int>(std::ceil(zsize/dz_min)); |
There was a problem hiding this comment.
we use the thinnest physical grid spacing (dz_min) to set the table vertical resolution
| const Raytracer_definitions::Vector<int> grid_cells, | ||
| const Raytracer_definitions::Vector<Float> grid_d, | ||
| const Raytracer_definitions::Vector<int> kn_grid, | ||
| const Array_gpu<Float,1>& z_lev, |
There was a problem hiding this comment.
z_lev: physical grid interface heights
kn_z_lev: null-cell boundary heights (kn_ktot+1 vals), a subset of z_lev, so each null cell holds a whole number of layers (~ktot/kn_ktot each).
z_lut: height bin (k) to physical layer index table
kn_z_lut: height bin (k) to null cell index table
most of the code changes are just carrying these around. the ray tracer kernels can no longer derive k_ext and k_null from a single dz, each lookup needs the two tables. Once built, it's fairly cheap: per photon cost is one read, one compare.
| const Float kext_tot = tau_tot[idx] / grid_d.z; | ||
| const Float kext_cld = tau_cld[idx] / grid_d.z; | ||
| const Float kext_aer = tau_aer[idx] / grid_d.z; | ||
| const Float dz = z_lev[iz+1] - z_lev[iz]; |
There was a problem hiding this comment.
switching to a local dz
| const Float kext_tot = tau_tot[idx] / grid_d.z; | ||
| const Float kext_cld = tau_cld[idx] / grid_d.z; | ||
| const Float kext_aer = tau_aer[idx] / grid_d.z; | ||
| const Float dz = z_lev[iz+1] - z_lev[iz]; |
|
|
||
| const int km = k - 1; | ||
| photon.position.z = (km + rng()) * grid_d.z; | ||
| photon.position.z = z_lev[km] + rng() * (z_lev[km+1] - z_lev[km]); |
There was a problem hiding this comment.
again using local not fixed dz
| Status::print_message("Storing the bw raytracer output."); | ||
|
|
||
| auto nc_radiance = output_nc.add_variable<Float>("radiance" , {"ny", "nx"}); | ||
| auto nc_radiance = output_nc.add_variable<Float>("radiance" , {"py", "px"}); |
There was a problem hiding this comment.
unrelated to this PR, but I think this is a bug
There was a problem hiding this comment.
radiance is camera output, so it is sized by the camera pixel counts (py, px), not the domain (ny, nx). The old dims only work when the two happen to match. It breaks for a camera whose pixel count differs from the domain size. test_rte_rrtmgp_bw already writes radiance with py/px, so this just makes the two drivers consistent.
|
|
||
| Array<int,1> z_lut({lut_size}); | ||
| Array<int,1> kn_z_lut({lut_size}); | ||
| for (int i=0; i<lut_size; ++i) |
There was a problem hiding this comment.
Example: layers of 20, 25, 60, 200 m. LUT spacing 20 m.
Bin 0 (0 - 20 m) corresponds to layer 0 (0 - 20 m), LUT stores k = 0.
Bin 1 (20–40 m) lies inside layer 1 (20- 45 m), LUT stores k = 1
Bin 2 (40–60 m) includes the layers 1 and 2. LUT stores the layer of its lower edge: k = 1. A possible collision that falls within bin 2 would be eligible for a correction (++k) in the height_to_int func if z >= z_lev[k+1]
Bin 3 covers 60–80 m, which lies inside layer 2 (45–105 m), LUT stores k =2.
and so on
The lw/sw solve_gpu built a uniform vertical grid from grid_d.z instead of the input interface heights, and the lw plane-parallel fallback divided by a scalar dz; both broke the 3D absorption output on non-uniform grids.
2a5a777 to
3c06c15
Compare
| cudaMemcpy(&max_kext_gas, max_kext_gas_g, sizeof(Float), cudaMemcpyDeviceToHost); | ||
|
|
||
| const Float lowest_gas_mean_free_path = grid_d.z / max_tau_gas; | ||
| const Float lowest_gas_mean_free_path = Float(1.) / max_kext_gas; |
There was a problem hiding this comment.
Before the mean-free-path criterion divided a fixed spacing by the maximum layer tau, which was fine for uniform grid but for stretched grid biases the estimate low by dz/dz_min. Now we use the maximum of tau/dz, the largest extinction coefficient in the domain, which is grid-independent.
|
Update: I added the constant-dz fast path Menno suggested. The kernels take a dz_constant template argument like independent_column. The path is chosen at runtime. The solver checks whether the interface heights are equally spaced, and if so it runs the original equidistant arithmetic, including the original null-cell boundaries at uniform spacing for any kn_ktot. This simplifies what I wrote in the Cabauw test section. On uniform grids the new code now always uses the old null-cell boundaries, so the non-divisible case is also bit identical and the second test figure no longer applies. |
| const int mie_phase_table_size = mie_phase_ang.size(); | ||
|
|
||
| // Constant dz implies the old uniform null-cell walls, valid for any kn_grid. | ||
| const bool dz_constant = vertical_spacing_is_constant(z_lev, grid_cells.z); |
There was a problem hiding this comment.
check if there is a uniform grid to set the argument passed to the ray tracer kernels
MennoVeerman
left a comment
There was a problem hiding this comment.
Code works like a charm, thanks for adding support for nonuniform dz grids!
Overview
The Monte Carlo ray tracer required an evenly spaced vertical grid. This PR removes that restriction (enables arbitrary, non-uniform vertical grids) for the forward shortwave and longwave tracers, the backward tracer, and the test drivers.
Changes
The simplest change is we replace the constant dz multiplies within the tau to k_ext and count to flux conversion calculations with a local dz.
The null collision grid requires changes too:
Longwave emission positions are now sampled from the interface heights; the emission energy weighting is tau based and needs no change.
Tests
Cabauw, uniform grid, old code vs this PR
Same null grid boundaries:

Different null grid boundaries:When the layer count does not divide the null cell count, the old code placed boundaries at fixed heights inside layers. With the constant-dz fast path the new code defaults back to those same boundaries on uniform grids, so this case is also bit identical.
RCEMIP on a uniform and stretched grid
SW surface fluxes (one timeslice):

LW heating rates and surface flux (one timeslice):

The spike in the heating rate difference sits at the cloud boundary: the mass conserving remap smears the cloud edge within one 40 m cell and slightly shifts the location of peak heating rate. The difference cancels in the column.
Pixel-wise comparison (12 timeslices):

Performance
On uniform grids the radiation step costs 3.8% more.On uniform grids performance is unchanged (uses constant-dz path). Stretched grids need far fewer levels which improves overall performance. The RCEMIP scenes here use 144 levels where the equivalent uniform grid needs 806, a 5.6x reduction in cells, and the ray tracer runs ~1.2x faster on the same scene.