|
| 1 | +(omi:developer-guide)= |
| 2 | +# Developer Guide: Adding a New Observation Type to the OMI Interface |
| 3 | + |
| 4 | +This page describes how to add a new observation type — generically |
| 5 | +called C below — to the TSMP-PDAF OMI (Observation Model Interface) |
| 6 | +framework. It covers the architecture, the role of each source file, |
| 7 | +and the changes required, using the existing GRACE-DA and SM-DA |
| 8 | +implementations as reference. |
| 9 | + |
| 10 | +## Architecture Overview |
| 11 | + |
| 12 | +Observation handling sits between the PDAF library and the eCLM model |
| 13 | +state in three layers: |
| 14 | + |
| 15 | +``` |
| 16 | +PDAF library (src/) |
| 17 | + │ calls callback routines at each analysis step |
| 18 | + ▼ |
| 19 | +callback_obs_pdafomi.F90 ← single routing hub |
| 20 | + │ dispatches to one routine per observation type |
| 21 | + ▼ |
| 22 | +obs_GRACE_pdafomi.F90 ← one module per observation type |
| 23 | +obs_SM_pdafomi.F90 |
| 24 | +obs_C_pdafomi.F90 (new) |
| 25 | + │ read observations, build H, apply H to state vector |
| 26 | + ▼ |
| 27 | +eCLM state vector (via enkf_clm_mod, mod_assimilation, …) |
| 28 | +``` |
| 29 | + |
| 30 | +Each observation-type module is self-contained. The only coupling |
| 31 | +between modules is through the central routing file and the shared |
| 32 | +`mod_assimilation` module. |
| 33 | + |
| 34 | +## Files Involved |
| 35 | + |
| 36 | +To add a new observation type `C`, touch exactly **eight** things: |
| 37 | + |
| 38 | +| File | Action | |
| 39 | +|------------------------------------------------|-----------------------------------------------------------------------| |
| 40 | +| `interface/framework/obs_C_pdafomi.F90` | **Create** (new file, modelled on the template) | |
| 41 | +| `interface/framework/Makefile` | Add `obs_C_pdafomi.o` to `MOD_USER_PDAFOMI` | |
| 42 | +| `interface/framework/callback_obs_pdafomi.F90` | Add calls for C to every callback routine | |
| 43 | +| `interface/framework/mod_assimilation.F90` | Declare `cradius_C`, `sradius_C`, and any other C-specific parameters | |
| 44 | +| `interface/framework/mod_read_obs.F90` | Add `'C'` case to `update_obs_type` | |
| 45 | +| `interface/model/eclm/enkf_clm_mod_5.F90` | Add `clmupdate_C` flag (parallel to `clmupdate_tws`, `clmupdate_swc`) | |
| 46 | +| `interface/framework/init_pdaf.F90` | Import `assim_C` and set it from `clmupdate_C` | |
| 47 | +| `interface/framework/init_pdaf_parse.F90` | Import `rms_obs_C` and add a `parse` call for it | |
| 48 | + |
| 49 | +The template at `templates/omi/obs_OBSTYPE_pdafomi_TEMPLATE.F90` is a |
| 50 | +good starting skeleton. The existing SM and GRACE modules show how the |
| 51 | +template is applied in this codebase. |
| 52 | + |
| 53 | +--- |
| 54 | + |
| 55 | +## Step 1 — Create `obs_C_pdafomi.F90` |
| 56 | + |
| 57 | +### Module-level variables |
| 58 | + |
| 59 | +At minimum, declare `assim_C` (a logical switch controlled by |
| 60 | +`callback_obs_pdafomi`), `rms_obs_C` (a constant fallback error |
| 61 | +standard deviation), and the PDAFomi types `thisobs` (of type `obs_f`, |
| 62 | +thread-safe) and `thisobs_l` (of type `obs_l`, declared |
| 63 | +`THREADPRIVATE`). |
| 64 | + |
| 65 | +`assim_C` and `rms_obs_C` are wired up externally: |
| 66 | +- **`init_pdaf.F90`** imports `assim_C` and sets it from `clmupdate_C` |
| 67 | + (i.e. `assim_C = (clmupdate_C /= 0)`), so the observation type is |
| 68 | + enabled automatically when the corresponding model-state update flag |
| 69 | + is active. Commented-out placeholders mark the insertion point. |
| 70 | +- **`init_pdaf_parse.F90`** imports `rms_obs_C` and populates it via a |
| 71 | + `parse` call that reads `rms_obs_C` from `enkfpf.par`. Commented-out |
| 72 | + placeholders mark the insertion point here as well. |
| 73 | + |
| 74 | +Additional module-level allocatable arrays — e.g. a cached |
| 75 | +climatological mean needed for an anomaly operator — follow the same |
| 76 | +pattern as in `obs_GRACE_pdafomi.F90`. |
| 77 | + |
| 78 | +### The `obs_f` type |
| 79 | + |
| 80 | +`obs_f` carries all observation metadata and is populated inside |
| 81 | +`init_dim_obs_C`. Mandatory fields: |
| 82 | + |
| 83 | +| Field | Type | Meaning | |
| 84 | +|-------------------------|------------------------|-------------------------------------------------------| |
| 85 | +| `thisobs%doassim` | `INTEGER` | 1 = assimilate, 0 = skip | |
| 86 | +| `thisobs%disttype` | `INTEGER` | distance metric for localization (see below) | |
| 87 | +| `thisobs%ncoord` | `INTEGER` | number of spatial coordinates (usually 2) | |
| 88 | +| `thisobs%id_obs_p(:,:)` | `INTEGER, ALLOCATABLE` | state-vector index for each process-local observation | |
| 89 | + |
| 90 | +Frequently useful optional fields: |
| 91 | +- `icoeff_p`: bilinear interpolation weights |
| 92 | +- `obs_err_type`: 0 = Gaussian, 1 = Laplace |
| 93 | +- `inno_omit`: innovation outlier rejection threshold |
| 94 | + |
| 95 | +**`disttype` choices:** |
| 96 | +- `0` — Cartesian distance; coordinates in any consistent unit (GRACE |
| 97 | + uses integer grid-cell indices, so `cradius_GRACE` is in number of |
| 98 | + grid cells). |
| 99 | +- `3` — geographic distance; coordinates in degrees, radii in km (used |
| 100 | + by SM). |
| 101 | + |
| 102 | +Use `disttype=3` for point observations distributed geographically (C, |
| 103 | +SM) and `disttype=0` for products already mapped to grid-cell indices |
| 104 | +(e.g. GRACE). |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +### Routine 1 — `init_dim_obs_C` |
| 109 | + |
| 110 | +Called once per analysis step before the filter loop. Responsibilities: |
| 111 | + |
| 112 | +1. Set the mandatory `thisobs` fields. |
| 113 | +2. Read the global observation vector from the NetCDF file using |
| 114 | + `read_obs_nc_type` from `mod_read_obs`. |
| 115 | +3. Handle the `dim_obs == 0` case (no observations scheduled for this |
| 116 | + step): allocate dummy arrays of size 1 and call |
| 117 | + `PDAFomi_gather_obs` before returning. |
| 118 | +4. For each global observation that falls within the PE-local domain, |
| 119 | + record the state-vector index in `thisobs%id_obs_p` and populate |
| 120 | + the local arrays `obs_p`, `ivar_obs_p`, and `ocoord_p`. |
| 121 | +5. Call `PDAFomi_gather_obs` to finalize observation bookkeeping across |
| 122 | + MPI ranks. |
| 123 | + |
| 124 | +**Reading observations:** Both SM and GRACE use `read_obs_nc_type`, |
| 125 | +which checks the `type_clm` NetCDF variable and returns `dim_obs = 0` |
| 126 | +if the file does not contain observations of the requested type. |
| 127 | + |
| 128 | +**Who reads the file:** SM reads only on `mype_filter==0` and |
| 129 | +broadcasts; GRACE reads on all filter processes because the spatial |
| 130 | +averaging operator needs each PE to know its contribution. For point |
| 131 | +observations like C, the SM pattern (read + broadcast) is simpler. |
| 132 | + |
| 133 | +**Snapping observations to the state vector:** For point observations |
| 134 | +(SM, C), loop over all global observations and all PE-local gridcells |
| 135 | +and record the state-vector index for matching pairs. For GRACE the |
| 136 | +logic is inverted: each observation aggregates multiple gridcells, so |
| 137 | +the loop iterates over gridcells and maps each one to the observation |
| 138 | +it contributes to. |
| 139 | + |
| 140 | +--- |
| 141 | + |
| 142 | +### Routine 2 — `obs_op_C` |
| 143 | + |
| 144 | +Applies the observation operator H, mapping the PE-local model state to |
| 145 | +the observation space. For a simple point observation (SM pattern): |
| 146 | +allocate a local output array, extract `state_p(obs_index_p(i))` for |
| 147 | +each local observation, then call `PDAFomi_gather_obsstate` to assemble |
| 148 | +the global result in `ostate`. |
| 149 | + |
| 150 | +For aggregating operators (GRACE pattern), sum all gridcell |
| 151 | +contributions mapping to the same observation, apply any transformation |
| 152 | +(e.g. subtract temporal mean for TWS anomaly), and then call |
| 153 | +`PDAFomi_gather_obsstate`. Follow the SM pattern for C unless your |
| 154 | +observations represent spatial averages over a large footprint. |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +### Routines 3 & 4 — Localization (required for LESTKF/LETKF) |
| 159 | + |
| 160 | +**`init_dim_obs_l_C`** counts observations within the localization |
| 161 | +radius of the current local analysis domain. It is a thin wrapper |
| 162 | +around `PDAFomi_init_dim_obs_l`, passing `thisobs_l`, `thisobs`, the |
| 163 | +local domain coordinates `coords_l` (in the same units and `disttype` |
| 164 | +as `ocoord_p`), and the localization parameters `cradius_C` and |
| 165 | +`sradius_C`. |
| 166 | + |
| 167 | +**`localize_covar_C`** is required only for the LEnKF. It is an |
| 168 | +equally thin wrapper around `PDAFomi_localize_covar` with the same |
| 169 | +localization parameters. |
| 170 | + |
| 171 | +--- |
| 172 | + |
| 173 | +## Step 2 — Update `callback_obs_pdafomi.F90` |
| 174 | + |
| 175 | +This file dispatches PDAF callbacks to each observation module. Add C |
| 176 | +in the same pattern as GRACE and SM. The commented-out |
| 177 | +`!USE obs_C_pdafomi` placeholders show exactly where to insert new |
| 178 | +entries. |
| 179 | + |
| 180 | +For each of the eight callback subroutines, add a `USE obs_C_pdafomi` |
| 181 | +statement and one call to the corresponding C routine: |
| 182 | + |
| 183 | +- **`init_dim_obs_pdafomi`**: set `assim_C = .true.`, call |
| 184 | + `init_dim_obs_C`, and add `dim_obs_C` to the running total. The |
| 185 | + `assim_*` flags exist to disable an observation type for debugging; |
| 186 | + keep `assim_C = .true.` here because the module itself returns |
| 187 | + `dim_obs = 0` when no observations are present. (This logic is |
| 188 | + planned to be complemented in the future by setting `assim_C` from |
| 189 | + the EnKF input file) |
| 190 | +- **`obs_op_pdafomi`**: call `obs_op_C`. Call order does not affect |
| 191 | + correctness because offsets in `ostate` are determined by the order |
| 192 | + of `init_dim_obs_*` calls above. |
| 193 | +- **`init_dim_obs_l_pdafomi`**: call `init_dim_obs_l_C`. The variable |
| 194 | + `dim_obs_l` is incremented inside each call; PDAF initializes it to |
| 195 | + 0 before invoking the callback. |
| 196 | +- **`localize_covar_pdafomi`**: call `localize_covar_C`. |
| 197 | +- **Remaining callbacks** (`add_obs_err`, `init_obscovar`, `prodRinvA`, |
| 198 | + `prodRinvA_l`, `deallocate_obs`): each needs a corresponding C |
| 199 | + call, all of which are thin wrappers around the matching PDAFomi |
| 200 | + generic routine, identical in structure to the existing GRACE and SM |
| 201 | + entries. |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## Step 3 — Add Parameters to `mod_assimilation.F90` |
| 206 | + |
| 207 | +Declare `cradius_C` and `sradius_C` next to the existing GRACE and |
| 208 | +SM radius parameters. These are read from the `enkfpf.par` input file |
| 209 | +via the existing namelist mechanism; add corresponding entries to the |
| 210 | +`[DA]` section parser in the same module. |
| 211 | + |
| 212 | +--- |
| 213 | + |
| 214 | +## Step 4 — Register the New Type in `mod_read_obs.F90` |
| 215 | + |
| 216 | +The routine `update_obs_type` reads the `type_clm` string from the |
| 217 | +observation file and sets the `clmupdate_*` flags for the next |
| 218 | +analysis cycle. Add a `'C'` case that sets the appropriate flags. If |
| 219 | +no existing `clmupdate_*` flag covers your new variable, declare a new |
| 220 | +`clmupdate_C` in `enkf_clm_mod.F90` and wire it to the state-vector |
| 221 | +fill and scatter logic in the eCLM interface routines (`eclm/`). |
| 222 | + |
| 223 | +--- |
| 224 | + |
| 225 | +## Key Design Differences Between GRACE-DA and SM-DA |
| 226 | + |
| 227 | +### Localization distance units |
| 228 | + |
| 229 | +| Type | `disttype` | Coordinate arrays | Radius unit | |
| 230 | +|-------|---------------|----------------------------|-------------| |
| 231 | +| GRACE | 0 (Cartesian) | integer grid-cell indices | grid cells | |
| 232 | +| SM | 3 (haversine) | degrees longitude/latitude | km | |
| 233 | + |
| 234 | +### Observation operator complexity |
| 235 | + |
| 236 | +- **SM**: direct extraction from `state_p` using a precomputed index. |
| 237 | +- **GRACE**: aggregating — all gridcell values within the GRACE |
| 238 | + footprint are summed and averaged, then the temporal mean is |
| 239 | + subtracted to produce a TWS anomaly. `thisobs%id_obs_p(1, g)` maps |
| 240 | + each gridcell `g` to its observation (inverse of the usual |
| 241 | + convention). |
| 242 | + |
| 243 | +Follow the SM pattern for C point observations; follow the GRACE |
| 244 | +pattern if observations represent spatial averages over a large |
| 245 | +footprint. |
| 246 | + |
| 247 | +### Coverage checks and error specification |
| 248 | + |
| 249 | +GRACE discards observations where fewer than a minimum number of |
| 250 | +gridcells lie within the support radius (important near coastlines). SM |
| 251 | +and C point observations do not need this, but consider rejecting |
| 252 | +observations over water bodies or outside the CLM domain. |
| 253 | + |
| 254 | +Error specification is controlled by `multierr`: constant |
| 255 | +(`multierr=0`), per-observation from `obserr_clm` (`multierr=1`), or |
| 256 | +full covariance matrix from `obscov_clm` (`multierr=2`). |
| 257 | + |
| 258 | +--- |
| 259 | + |
| 260 | +## Assimilating Multiple Observation Types at the Same Timestep |
| 261 | + |
| 262 | +The framework generates a state vector for each type individually before the assimilation, some things would need to be adapted when mutliple observation types are assimilated at the same timestep. Currently, one observation file only consists of one observation type. As SM observations are usually assimilated at noon and GRACE observations are assimilated at the end of the month at midnight, this should not provide any problems. |
0 commit comments