Column-based forward operators in DART #1162
hkershaw-brown
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Column-based forward operators in DART
Current state of
https://github.com/NCAR/DART/blob/main/observations/forward_operators/obs_def_*_mod.f90for forwardoperators (FOs) that need
or
model_interpolaterepeatedly, one level and one quantity at a time.As of DART v11.25.
Projects:
DART The Need for Speed #995.
DART Column Operators #366 #754 #438.
1. The two idioms in use today
1a. "Count levels by interpolating until it fails"
There is no
model_modinterface that reports the number of vertical levels at alocation, so operators discover it by probing
VERTISLEVEL1, 2, 3, … untilmodel_interpolatereturns a non-zeroistatus:COUNTLEVELSQTY_PRESSUREmaxlevels = 10000COUNTLEVELSQTY_PRESSUREmaxlevels = 10000COUNTLEVELSQTY_GEOPOTENTIAL_HEIGHTmaxlayers = 1000CATEGORIESQTY_SEAICE_CATEGORYMAX_CATEGORIES = 100LEVELSQTY_PRESSURE10000(literal)LEVELSQTY_PRESSURE10000(literal)model_levelsQTY_PRESSUREdoLEVELS,FILLINTERFACES,FILLMIDPOINTSQTY_DENSITY_ION_E,QTY_GEOMETRIC_HEIGHT,QTY_PRESSUREMAXLEVELS = 300(compile-time)Consequences of the idiom itself:
model_interpolatethat is expected to fail. Somemodel_mods log/print onfailure, and some fail with
E_ERRon the quantity being probed — e.g.tiegcm/model_mod.f90:308 hard-errors on
QTY_GEOPOTENTIAL_HEIGHT, which is exactly whatget_expected_gnd_gps_vtec/get_expected_O_N2_ratiowant to probe. The commentthere says as much.
for the probe: "Some applications just need to know the number of vertical
levels. This is done by trying to 'interpolate' height on a large number of
levels. When the interpolation fails, you've gone one level too far."
(clm/model_mod.f90:1237).
obs_def_upper_atm_modrequirespreprocess+recompile if the model has more than 300 levels
(:308).
obs_def_cice_modcounts categories at ahard-wired
(0.0, 0.0)(:419-423),which is open ocean in the Gulf of Guinea. It works only because the
quantity exists on the grid there for global models (not true for regional models?)
(
arrays_prealloced), andobs_def_cice_modcachesNcatonce(:167). COSMOS,
cwp, tpw, CO_Nadir and upper_atm re-count on every observation.
1b. "Build the column one
model_interpolatecall per (level, quantity)"Then the column data is fetched the same way. Worst offender, RTTOV
GETLEVELDATA(obs_def_rttov13_mod.f90:3603-3822):42 possible
interpolatecalls inside the level loop, plus ~16 surface callsafter it. With a 55-level model and clouds enabled that's roughly
55 × 9 ≈ 500calls per observation; with the aerosol options on it approaches55 × 42 ≈ 2300.2. Affected forward operators, ranked by cost
Tier 1 — full multi-quantity column per observation
obs_def_rttov_mod,obs_def_rttov13_modget_expected_radiancenlev × (3 … 42)+ ~16 surfaceobs_def_cwp_modget_expected_cwpnlev × (1 … 6)+ 7 surfaceobs_def_CO_Nadir_modget_expected_iasi_co,get_expected_mopitt_conlev(pressure) +n_retrieval_layers × nlevforQTY_COobs_def_TES_nadir_modget_expected_TES_nadir_obsens_size × N_layerstemperature callsobs_def_upper_atm_modget_expected_O_N2_rationlev × 5(interfaces + 4 midpoint quantities)obs_def_upper_atm_modget_expected_gnd_gps_vtecnlev × 2obs_def_tpw_modget_expected_tpwnlev × 2+ 2 surfaceobs_def_COSMOS_modget_expected_neutron_intensitynlayer × 3(count + height + moisture)obs_def_cice_modget_expected_agreg_*Ncat × (1 … 2), ×2 for freeboard/thicknessTwo of these are particularly bad
obs_def_CO_Nadir_mod— the retrieval-layer loopdo i=start_i,end_iwrapsthe model-level loop
do j=1,num_levs, and re-interpolatesQTY_COat modellevel
jinside both(:325-352).
With
mopitt_dim = 10and 32 CAM levels that is ~320QTY_COinterpolationswhere 32 would do — each model level is fetched up to 10 times.
obs_def_TES_nadir_mod—do imem = 1, ens_size / do k = 1, N_layerscallsinterpolatewith the fullens_sizeeach time(:874-885),
so with 80 members and
N_layers = 30that is 2400 full-ensemble interpolationsto fill 30 levels. The
@todoright above it names the problem: "Possibly wecould overload interpolate and set_location to take an array of locations …
For now this is a loop around ensemble_size calling interpolate —
overcommunicating."
Tier 2 — many quantities at one location (repeated horizontal search, no column)
These don't need a column, but they pay the horizontal-location cost N times for
one point, so they benefit from the same fix (locate once, read many):
obs_def_radar_mod—get_expected_radar_ref,get_expected_fall_velocityobs_def_upper_atm_mod—get_expected_upper_atm_density,get_expected_oxygen_ion_densityobs_def_oxygen_ion_density_mod—get_expected_oxygen_ion_valobs_def_land_mod— albedo / biomass / PAR operatorsobs_def_GRACE_mod—get_expected_TWSobs_def_rel_humidity_mod,obs_def_abs_humidity_mod,obs_def_dew_point_mod,obs_def_altimeter_mod,obs_def_wind_speed_modobs_def_GRACE_modis a Tier-1 case in waiting: it hard-codes soil depths0.05/0.25/0.7/1.5 mand thicknesses100/300/600/1000 mmwith!>@todo Determine number of layers and their thicknesses automatically(:171) — i.e.
it wants a column interface and currently fakes it. (It also
E_ERRsunconditionally at :168.)
Tier 3 — ray/path integration (many different locations)
obs_def_gps_mod—get_expected_gpsro_ref, non-local operator(:481-530). The
INTEGRATEloop steps along the ray and callsref_localtwice per step; eachref_localdoes 3interpolatecalls (T,Q,P) at the same new lat/lon/height(:599-607).
This is not a column caching problem,
But the 3 calls at each step are at an identical location, so each step
triggers 3 identical vertical-column constructions in the model (see 3). A
"give me these N quantities at this one location" interface fixes 2/3 of the work,
and a location-array interface would let the whole ray go in one call.
Q. Is a location array and a column array the same thing (interface wise?)
3. Which models are expensive?
The per-call cost splits into a horizontal locate and a vertical locate.
Repeating
model_interpolatedown a column repeats both, even though only thelevel index changed.
3a. Horizontal search repeated per level
MPAS-A —
models/mpas_atm/model_mod.f90. Worst case. Note users reporting slowness with this: #995model_interpolatecallscell_ok_to_interpolate(:1179) →
find_closest_cell_center→
xyz_find_nearestagainst aget_closestructure over allnCells(:6380-6417).
find_trianglethencalls
find_closest_cell_centeragain (:5920).So an unstructured nearest-neighbour search runs ≥2× per
interpolatecall. For anRTTOV column that is ~1000 identical nearest-cell searches for one lat/lon.
highest_obs_pressure_mb > 0adds acompute_pressure_at_locon top, which thecode itself flags: "this is expensive - only do it if users want to reject
observations at the top of the model" (:1266).
CAM-SE —
models/cam-se/model_mod.f90.coord_ind_cssearches the cubed-sphere for the enclosing quad on every call(:592). The code already knows this is
redundant: "SENote2: In the CLASSIC, there is a possibility that the cell_corner
was already found and this call can be skipped. Understand that and implement as
needed." (:587).
CLM —
models/clm/model_mod.f90. Worst case for the land operators.Both
compute_gridcell_valueandget_grid_vertvaldo a linear scan over theentire state range of the variable to find the columns/PFTs in the target
gridcell —
ELEMENTS : do indexi = index1, indexN(:1422,
:1661), and
get_grid_vertvalscanstwice (once to count, once to gather,
:1625). Cost is O(number of state elements
of that variable) per call. The code flags it:
"this is the loop that can exploit the knowledge of what columnids or pftids are
needed for any particular gridcell" (:1411).
COSMOS drives this with
3 × nlayerscalls per observation, and re-counts layersevery observation.
POP / CICE (dipole & tripole grids) —
models/POP/model_mod.f90.Regular-grid lookup table narrows the candidates, then
get_dipole_quaddoes anin_quadtest over every overlapping target quad(:1329-1340). Bounded but not free, and
repeated per category by the
obs_def_cice_modoperators.MOM6 —
models/MOM6/model_mod.f90. The only user ofGRID_QUAD_FULLY_IRREGULAR, whose documented search is "do an exhaustive searchof the target grid quads which overlap that regular grid box"
(quad_utils_mod.f90:40-45).
CAM-FV —
models/cam-fv/model_mod.f90. Horizontal locate is cheap(regular lon/lat), but
interp_handle = get_interp_handle(obs_qty)(:783) *"should this be a pointer?? is
it replicating the internal arrays on assignment?" What is this comment getting at?
WRF —
models/wrf/model_mod.f90. Map projection inverse is closed-form andcheap, but
get_domain_infowalks nested domains highest resolution (I think) outward until thepoint is inside (:6336-6398) — redone per
call, per level.
3b. Vertical column rebuilt per call — the bigger cost when
which_vert /= VERTISLEVELThis is what makes COSMOS (
VERTISHEIGHT), TES nadir (VERTISPRESSURE),GPS non-local (
VERTISHEIGHT), and the pressure-level branches of cwp/tpw soexpensive:
get_quad_valsloopsdo icorner = 1,4callingfind_vertical_levels(:914) — a fullpressure or height column built at 4 corners on every single
model_interpolate.dry_mass_vertical_coordinateandprecise = .true.(thedefault in
model_interpolate, :544),build_dry_mass_pressure_columnsreads specific humidity, cloud liquid andcloud ice at every level to build one pressure column
(:1668-1762) — ~
3 × nlevstatereads per
interpolatecall. The comment concedes it: "This is expensive butprobably necessary to get unbiased forward operators."
get_model_height_profile_distribfetchesPHat 4 corners for allbtlevels (:5387-5479); the pressurepath is equivalent. Rebuilt per call.
So a GPS non-local ray step under CAM-SE costs 3 identical dry-mass column
builds ≈
3 × 3 × 32 ≈ 288state reads to produce 3 numbers. Under CAM-FV it is3 × 4 corners of column construction.
3c. The multiplier that makes it MPI traffic, not just flops
Every leaf state read goes through
get_state(distributed_state_mod.f90:70).
When the owning task is remote it issues a one-sided
get_from_fwd/MPI_Getfor that single element, all copies
(:112).
There is no batching across levels. So a column built one level at a time is
nlevseparate small RMA transfers where one contiguous fetch of the column woulddo. This is the reason the redundancy hurts at scale rather than just being untidy.
3d. Radiance-specific multiplier: per-channel observations
RTTOV brightness temperatures are one observation per channel. Every channel at
the same footprint re-enters
get_expected_radianceand re-runsGETLEVELDATAfrom scratch —
arrays_preallocedcaches only the allocation, never the data.A 10-channel sounder footprint therefore rebuilds the identical 55-level column
10 times: ~5000
interpolatecalls where ~500 would do, and there is no guaranteesame-location obs are even adjacent in the obs sequence.
4. Policy rejections collide with the probe-until-failure idiom
The probe idiom (1a) reads any non-zero
istatusas "the model has no suchlevel". But several
model_mods also return non-zeroistatusto mean"DART policy says don't assimilate here" — the
no_obs_assim_above_level/highest_obs_pressure_mbtop cutoffs. The operator cannot tell the two apart, soa QC setting silently changes the geometry the operator believes the model has.
4a. CAM: the level cutoff is a no-op — accidentally
obs_too_high(cam_common_code_mod.f90:487)has a
VERTISLEVELbranch:no_assim_above_levelis never assigned anywhere in DART. It is declared andinitialized to
-1.0_r8at:144, read once at
:511, and that is the
only other reference in the tree.
init_discard_high_obs(:1266-1332) sets
no_assim_above_pressure,no_assim_above_heightand a localno_assim_above_scaleh— but not the level. Sovert_value < -1.0is never trueand CAM-FV/CAM-SE never reject a
VERTISLEVELlocation, whateverno_obs_assim_above_levelis set to.Two consequences, pulling opposite ways:
terminate on
check_good_levelsreturningmy_status = 8whenvert_value > ref_nlevels(:1152), which is the
genuine model top. They are not truncated by the namelist.
VERTISLEVELis neverdiscarded, contrary to what
no_obs_assim_above_leveladvertises and what theinit_discard_high_obslog message claims ("Discarding observations higher thanmodel level N"). The namelist is honoured for pressure, height and scale-height
obs only. Q. Who cares about obs VERTISLEVEL, OSSEs?
Fixing the 'bug; (
no_assim_above_level = no_obs_assim_above_level) wouldbreak every column operator on CAM: level 1 is the CAM model top, so
vert_value < no_assim_above_levelis true for exactly levels 1 … N-1 so the probeloop would fail on its first call and every column operator would return
numlevels = 0. That coupling has to be resolved before the assignment is added.4b. CAM:
VERTISHEIGHT/VERTISPRESSURErejections do fire for fwd ops #438discarding_high_obsis set true wheneverno_obs_assim_above_level > 0(cam-fv/model_mod.f90:338-341,
cam-se/model_mod.f90:385-388), and the
height/pressure branches of
obs_too_highare live. The operator that gets hurt isnonlocal GPS RO refractivity:
ref_localbuilds its location asVERTISHEIGHT(obs_def_gps_mod.f90:592).
obs_too_highreturns 14 for all ensemble members at once (istatus(:) = status1,cam-fv/model_mod.f90:800).
track_statustherefore setsreturn_now, andget_expected_gpsro_refreturnsimmediately (obs_def_gps_mod.f90:499-501).
So the entire GPS observation is discarded as soon as any single point on its ray
path rises above the cutoff,
ray_topby construction: the converters defaultray_htopto 15,000 m(convert_cosmic_gps_cdf.f90:91)
and 30,000 m
(convert_gpsro_bufr.f90:147).
With a cutoff below
ray_htop, everyGPSRO_REFRACTIVITYobs using theGPSEXC(nonlocal) form is rejected regardless of its own height Q. How easy it is to set ray_htop to "fail" a bunch of obs? Not sure.
The local (
GPSREF) form is unaffected: it makes oneref_localcall at the obslocation itself.
Same mechanism, same
VERTISHEIGHT/VERTISPRESSUREexposure, applies to COSMOS(soil moisture at
VERTISHEIGHT), TES nadir (VERTISPRESSURE), and thenon-
model_levelsbranches of cwp/tpw, but those run on CLM / (and others?), none ofwhich implement a top cutoff - maybe a problem in strongly coupled DA?
4c. MPAS-A: the cutoff does apply to
VERTISLEVEL, and truncates columns silentlyMPAS-A checks
highest_obs_pressure_mbinsidemodel_interpolate(mpas_atm/model_mod.f90:1274-1290).
The guard excludes only
SURFACEandUNDEFINED:@hkershaw-brown check refactored code.
VERTISLEVELis not excluded, so the RTTOVCOUNTLEVELSprobe is subject to it.The module default is
highest_obs_pressure_mb = 100.0(100 mb,:254); the shipped
mpas_atm/work/input.nmlsets1.0. So:&model_nml),COUNTLEVELSstops at ~100 mb instead of the model top. RTTOV then computes radiances from aprofile that is not the whole atmosphere levels. The obs is not rejected; it
returns a wrong value - assuming that you should be using the whole atmospheric column.
numlevelsis computed once and cached globally viaarrays_prealloced(obs_def_rttov13_mod.f90:3537).
The truncated count from whatever the first radiance observation happened to be
is then used for every observation for the rest of the run.
COUNTLEVELSexits onany(this_istatus /= 0), so one ensemble member beingabove the cutoff truncates the column for all of them. Not sure how varied ensemble members are? Could this be several levels different between members.
Cost note:
compute_pressure_at_locfor aVERTISLEVELlocation callsconvert_vert_distrib(… VERTISPRESSURE …)(:4374-4385) — a full barycentric
vertical conversion, with its own cell search on every call in the column
loop, purely to evaluate the cutoff. That roughly doubles the per-level cost
already described in 3a.
4d. Other models
boundsCheck/zlocbad-level path. (It doesallocate(v_h, v_p)of(0:bt, ens_size)unconditionally on everymodel_interpolatecall,:1161 a per-call allocate/deallocate
inside every column loop.) Also see WRF model_mod - Why not allow last level? #366
obs_too_high(copy of cam model_mod)(:3006-3041) with
no_assim_above_level = 13_r8hard-coded at:187 and the opposite inequality
(
vert_value > no_assim_above_level, since pangu level 1 is the surface). It iscurrently inert — the
obs_too_highcall inmodel_interpolateis commented out(:581-582) but it would truncate at
level 13 if re-enabled, and
discarding_high_obsalready defaults to.true.\
4e. Related: unguarded probe loops
obs_def_CO_Nadir_mod'smodel_levels: dois unbounded and writesp_col(:, lev)before testingistatus(:296-305,
:458-467),
with
p_coldimensionedmax_model_levs = 100(:78) and no
bound check. A model with ≥100 levels (WACCM has 70 or 126) overruns the array.
By contrast RTTOV, COSMOS, cice and upper_atm all check their cap and error out.
5. Summary of possible capabilities needed for a column interface:
and the
maxlevels/MAXLEVELS/maxlayerscompile-time caps.(1b) — replaces the
nlev × 1loops in tpw, VTEC, cwp.including the vertical column construction, then read N quantities.
Also fixes GPS
ref_local's three identical column builds.TES_nadirper-member loop, note@todoin the code about this already.says don't assimilate here" (4). Today both come back as a single non-zero
istatus, so a QC namelist setting silently redefines how many levels theoperator thinks the model has (MPAS, 4c), and a cutoff applied to one point on
a ray discards the whole observation (CAM + nonlocal GPS, 4b). Capability 1
("how many levels?") means you can distinguish these.
All reactions