ROMS Observation Operators - myroms/roms GitHub Wiki

image

Introduction

The native ROMS 4D-Var observation file and H(x) operators have been refactored to use multiple hierarchical IODA-type enhanced NetCDF-4 files with first-level grouping. The main objective is to create IODA observation files that can be utilized in both the native ROMS 4D-Var drivers and any ROMS-JEDI data assimilation algorithm. The data assimilation output is also stored in IODA files, organized into new groups for diagnostics and post-processing. Unlike the classic native ROMS NetCDF files, the input IODA files remain unmodified during data assimilation.

Important

Please use the C-preprocessing option IODA_OBS to active using multiple IODA observation files and associated H(x) operators. C-preprocessing removes the legacy, native, single classic ROMS 4D-Var NetCDF file processing. It is obsolete, but we keep it for backward compatibility.

The strategy is to have one or more IODA files for each variable in the control vector, such as SSH, SST, SSS, HF radar velocities, temperature, and salinity, as individual files in a particular data assimilation cycle. An IODA file could include multiple variables that represent vectors (u- and v-momentum) and spectral data with specified coordinates (longitude, latitude, depth, and time). It may also contain multiple scalar variables at precise locations, such as temperature (T) and salinity (S) profiles, or biological profiles for various ecosystem state variables. Additionally, secondary variables may be included to compute a control variable using an elaborate operator. This setup provides significant flexibility to apply time-averaged and/or area-averaged H(x) operators to individual observation streams, including altimetry data (SWOT), microwave SST, satellite-derived SSS (SMAP, SMOS), and HF radar velocity (CODAR).

Altimetry ADT SMAP Satellite SSS HF-Radar Velocity
ADT SSS CODAR

The processing of the observations and the application of the H(x) operators, which compute the model values at the observation locations, have been completely rewritten in ROMS using Fortran 2003 object classes:

  • roms_obs.F: Includes two CLASS objects, obs_data and obs_pool, that are used to load and store the input observation data, which are read only once and saved in memory. This reduces I/O overhead and MPI communication in applications running with many processes. The obs_pool object is a vector, pool(:), of TYPE obs_data observations separated by their type, source, or control vector variable. That is, pool(:) may be a vector the size of the number of input IODA files, giving the user flexibility when configuring the data assimilation run. This module can load native (old) or IODA (new) observation NetCDF files for data assimilation.

  • roms_hofx.F: Compute and store $\boldsymbol{\rm{H(X^{s}_{j})}}$ where superscript s indicates background (s=b) or analysis (s=a) state vector. It also computes its transpose HT for adjoint kernel forcing. It has two CLASS objects, obs_filter and obs_hofx. The commands to execute the H(x) operators are compact, using CALL hofx_run (ng, tile, model) and CALL hofx_finalize (ng, model). The MPI parallel exchanges occur once during the kernel's finalize phase because the H(x) vectors are needed only in the minimization algorithm after running ROMS numerical kernels (NLM, TLM, ADM). This substantially reduces MPI communications.

  • roms_vchange.F: It has the CLASS object obs_VarCha to perform the appropriate nonlinear and linear variable changes for the H(x) operators. This capability was not available in legacy processing because all required variable changes were the identity operator.


The processing of all the observations is reported at the beginning of the data assimilation run, which facilitates checking if they are processed correctly:

  ROMS_OBS         - Processing: ../Data/OBS/usec3km_obs_adt_20190827.nc4
                      Group: MetaData::dateTime                           Count = 87710
                      Group: MetaData::latitude                           Count = 87710
                      Group: MetaData::longitude                          Count = 87710
                      Group: MetaData::provenance                         Count = 87710
                      Group: MetaData::surveyIndex                        Count = 649
                      Group: MetaData::surveyTime                         Count = 649
                      Group: MetaData::x_grid                             Count = 87710
                      Group: MetaData::y_grid                             Count = 87710
                      Group: ObsError::absoluteDynamicTopography          Count = 87710  ID = 1
                      Group: ObsValue::absoluteDynamicTopography          Count = 87710  ID = 1

                    - absoluteDynamicTopography
                      (ValMin = -3.01195E+00 ValMax =  2.15674E+00 ValAvg =  3.73152E-01 CheckSum = 1775003)
                      (ErrMin =  2.77778E+02 ErrMax =  2.77778E+02 ErrAvg =  2.77778E+02)
                      DateTime Range:   2019-08-27 00:42:45.00 to 2019-08-29 23:58:19.00
                      SurveyTime Range: 2019-08-27 00:42:45.00 to 2019-08-29 23:58:19.00

  ROMS_OBS         - Processing: ../Data/OBS/usec3km_obs_sst_20190827.nc4
                      Group: MetaData::dateTime                           Count = 174310
                      Group: MetaData::latitude                           Count = 174310
                      Group: MetaData::longitude                          Count = 174310
                      Group: MetaData::provenance                         Count = 174310
                      Group: MetaData::surveyIndex                        Count = 77
                      Group: MetaData::surveyTime                         Count = 77
                      Group: MetaData::x_grid                             Count = 174310
                      Group: MetaData::y_grid                             Count = 174310
                      Group: ObsError::seaSurfaceTemperature              Count = 174310 ID = 6
                      Group: ObsValue::seaSurfaceTemperature              Count = 174310 ID = 6

                    - seaSurfaceTemperature
                      (ValMin =  7.90500E+00 ValMax =  3.08168E+01 ValAvg =  2.31506E+01 CheckSum = 2504327)
                      (ErrMin =  5.10204E-01 ErrMax =  2.61102E+01 ErrAvg =  1.96736E+00)
                      DateTime Range:   2019-08-27 01:06:45.00 to 2019-08-29 22:30:19.00
                      SurveyTime Range: 2019-08-27 01:06:45.00 to 2019-08-29 22:30:19.00

  ROMS_OBS         - Processing: ../Data/OBS/usec3km_obs_sss_20190827.nc4
                      Group: MetaData::dateTime                           Count = 52
                      Group: MetaData::latitude                           Count = 52
                      Group: MetaData::longitude                          Count = 52
                      Group: MetaData::provenance                         Count = 52
                      Group: MetaData::surveyIndex                        Count = 50
                      Group: MetaData::surveyTime                         Count = 50
                      Group: MetaData::x_grid                             Count = 52
                      Group: MetaData::y_grid                             Count = 52
                      Group: ObsError::seaSurfaceSalinity                 Count = 52     ID = 7
                      Group: ObsValue::seaSurfaceSalinity                 Count = 52     ID = 7

                    - seaSurfaceSalinity
                      (ValMin =  3.17402E+01 ValMax =  3.58166E+01 ValAvg =  3.30407E+01 CheckSum = 642)
                      (ErrMin =  3.18633E+01 ErrMax =  5.07306E+01 ErrAvg =  4.64497E+01)
                      DateTime Range:   2019-08-27 01:06:45.00 to 2019-08-29 22:30:19.00
                      SurveyTime Range: 2019-08-27 01:06:45.00 to 2019-08-29 22:30:19.00

  ROMS_OBS         - Processing: ../Data/OBS/usec3km_obs_temp_20190827.nc4
                      Group: MetaData::dateTime                           Count = 12286
                      Group: MetaData::depth                              Count = 12286
                      Group: MetaData::latitude                           Count = 12286
                      Group: MetaData::longitude                          Count = 12286
                      Group: MetaData::provenance                         Count = 12286
                      Group: MetaData::surveyIndex                        Count = 409
                      Group: MetaData::surveyTime                         Count = 409
                      Group: MetaData::x_grid                             Count = 12286
                      Group: MetaData::y_grid                             Count = 12286
                      Group: MetaData::z_grid                             Count = 12286
                      Group: ObsError::waterTemperature                   Count = 12286  ID = 6
                      Group: ObsValue::waterTemperature                   Count = 12286  ID = 6

                    - waterTemperature
                      (ValMin =  3.33530E+00 ValMax =  2.93459E+01 ValAvg =  1.30433E+01 CheckSum = 168795)
                      (ErrMin =  6.88618E-02 ErrMax =  1.23457E+02 ErrAvg =  1.08630E+01)
                      DateTime Range:   2019-08-27 00:00:00.00 to 2019-08-29 23:54:19.00
                      SurveyTime Range: 2019-08-27 00:00:00.00 to 2019-08-29 23:54:19.00

  ROMS_OBS         - Processing: ../Data/OBS/usec3km_obs_salt_20190827.nc4
                      Group: MetaData::dateTime                           Count = 12197
                      Group: MetaData::depth                              Count = 12197
                      Group: MetaData::latitude                           Count = 12197
                      Group: MetaData::longitude                          Count = 12197
                      Group: MetaData::provenance                         Count = 12197
                      Group: MetaData::surveyIndex                        Count = 406
                      Group: MetaData::surveyTime                         Count = 406
                      Group: MetaData::x_grid                             Count = 12197
                      Group: MetaData::y_grid                             Count = 12197
                      Group: MetaData::z_grid                             Count = 12197
                      Group: ObsError::salinity                           Count = 12197  ID = 7
                      Group: ObsValue::salinity                           Count = 12197  ID = 7

                    - salinity
                      (ValMin =  2.92491E+01 ValMax =  3.68814E+01 ValAvg =  3.36789E+01 CheckSum = 155723)
                      (ErrMin =  1.77741E+00 ErrMax =  4.00000E+02 ErrAvg =  9.50594E+01)
                      DateTime Range:   2019-08-27 00:00:00.00 to 2019-08-29 23:54:19.00
                      SurveyTime Range: 2019-08-27 00:00:00.00 to 2019-08-29 23:54:19.00

  ROMS_OBS         - Processing: ../Data/OBS/usec3km_obs_uv_codar_20190827.nc4
                      Group: MetaData::dateTime                           Count = 10332
                      Group: MetaData::depth                              Count = 10332
                      Group: MetaData::latitude                           Count = 10332
                      Group: MetaData::longitude                          Count = 10332
                      Group: MetaData::provenance                         Count = 10332
                      Group: MetaData::surveyIndex                        Count = 72
                      Group: MetaData::surveyTime                         Count = 72
                      Group: MetaData::x_grid                             Count = 10332
                      Group: MetaData::y_grid                             Count = 10332
                      Group: MetaData::z_grid                             Count = 10332
                      Group: ObsError::waterZonalVelocity                 Count = 10332  ID = 4
                      Group: ObsError::waterMeridionalVelocity            Count = 10332  ID = 5
                      Group: ObsValue::waterZonalVelocity                 Count = 10332  ID = 4
                      Group: ObsValue::waterMeridionalVelocity            Count = 10332  ID = 5

                    - waterZonalVelocity
                      (ValMin = -4.73497E-01 ValMax =  1.07178E+00 ValAvg = -3.81463E-02 CheckSum = 203326)
                      (ErrMin =  2.50000E+01 ErrMax =  2.50000E+03 ErrAvg =  3.67621E+02)
                      DateTime Range:   2019-08-27 00:00:00.00 to 2019-08-29 22:54:19.00
                      SurveyTime Range: 2019-08-27 00:00:00.00 to 2019-08-29 22:54:19.00
                    - waterMeridionalVelocity
                      (ValMin = -1.03206E+00 ValMax =  1.58777E+00 ValAvg = -6.93601E-02 CheckSum = 203532)
                      (ErrMin =  2.50000E+01 ErrMax =  2.50000E+03 ErrAvg =  3.67621E+02)
                      DateTime Range:   2019-08-27 00:00:00.00 to 2019-08-29 22:54:19.00
                      SurveyTime Range: 2019-08-27 00:00:00.00 to 2019-08-29 22:54:19.00

  OBS_POOL         - NC4_CLONE: creating H(x) file,    Grid 01: usec3km_obs_adt_20190827_hofx.nc4
  OBS_POOL         - NC4_CLONE: creating H(x) file,    Grid 01: usec3km_obs_sst_20190827_hofx.nc4
  OBS_POOL         - NC4_CLONE: creating H(x) file,    Grid 01: usec3km_obs_sss_20190827_hofx.nc4
  OBS_POOL         - NC4_CLONE: creating H(x) file,    Grid 01: usec3km_obs_temp_20190827_hofx.nc4
  OBS_POOL         - NC4_CLONE: creating H(x) file,    Grid 01: usec3km_obs_salt_20190827_hofx.nc4
  OBS_POOL         - NC4_CLONE: creating H(x) file,    Grid 01: usec3km_obs_uv_codar_20190827_hofx.nc4

Specify all IODA observation files in the input rbl4dvar.in script:

! Input observation filename(s), [1:Ngrids].

       NOBSFILES == 6

         OBSname == ../Data/OBS/usec3km_obs_adt_20190827.nc4     \
                    ../Data/OBS/usec3km_obs_sst_20190827.nc4     \
                    ../Data/OBS/usec3km_obs_sss_20190827.nc4     \
                    ../Data/OBS/usec3km_obs_temp_20190827.nc4    \
                    ../Data/OBS/usec3km_obs_salt_20190827.nc4    \
                    ../Data/OBS/usec3km_obs_uv_codar_20190827.nc4

If using an s4dvarin template, we have:

! Input observation filename(s), [1:Ngrids].

      NOBSFILES == MyNobsFiles
        OBSname == roms_obs.nc

The execution bash script sets and adds the IODA files to use:

## Set observations NetCDF filename(s) in a bash array. Usually, the "_area"
## and "_time" labels refer to area-averaged and time-averaged observations.
## Here, FprefixF='eccofs3km', FprefixC='eccofs6km', and Fsuffix='20190101'.

  echo
  echo "   Observation File(s):"
  echo

  if [[ ${IODAobs} -eq 1 ]]; then
    OBSnameF[${#OBSnameF[@]}]="${ObsDir}/${FprefixF}_obs_adt_${Fsuffix}.nc4"
    OBSnameF[${#OBSnameF[@]}]="${ObsDir}/${FprefixF}_obs_sst_${Fsuffix}.nc4"
    OBSnameF[${#OBSnameF[@]}]="${ObsDir}/${FprefixF}_obs_sss_${Fsuffix}.nc4"
    OBSnameF[${#OBSnameF[@]}]="${ObsDir}/${FprefixF}_obs_temp_${Fsuffix}.nc4"
    OBSnameF[${#OBSnameF[@]}]="${ObsDir}/${FprefixF}_obs_salt_${Fsuffix}.nc4"
#   OBSnameF[${#OBSnameF[@]}]="${ObsDir}/${FprefixF}_obs_uv_codar_${Fsuffix}.nc4"

    OBSnameC[${#OBSnameC[@]}]="${ObsDir}/${FprefixC}_obs_adt_${Fsuffix}.nc4"
    OBSnameC[${#OBSnameC[@]}]="${ObsDir}/${FprefixC}_obs_sst_${Fsuffix}.nc4"
    OBSnameC[${#OBSnameC[@]}]="${ObsDir}/${FprefixC}_obs_sss_${Fsuffix}.nc4"
    OBSnameC[${#OBSnameC[@]}]="${ObsDir}/${FprefixC}_obs_temp_${Fsuffix}.nc4"
    OBSnameC[${#OBSnameC[@]}]="${ObsDir}/${FprefixC}_obs_salt_${Fsuffix}.nc4"
#   OBSnameC[${#OBSnameC[@]}]="${ObsDir}/${FprefixC}_obs_uv_codar_${Fsuffix}.nc4"
  else
    OBSnameF="${FprefixF}_roms_obs_${Fsuffix}.nc4"
    OBSnameC="${FprefixC}_roms_obs_${Fsuffix}.nc4"
  fi

  NobsFilesF=${#OBSnameF[@]}
  NobsFilesC=${#OBSnameC[@]}

  echo "     Fine Resolution Grid, NobsFiles = ${NobsFilesF}"
  echo
  for obs in ${OBSnameF[@]}; do
    obsfile=$(basename "$obs")
    obspath="${HereDir%/*}/Data/OBS/${obsfile}"
    if [ -f "${obspath}" ]; then
      echo "     OBSnameF: $obs"
    else
      echo "     file: ${obspath} cannot be found."
      exit 1
    fi
  done

  echo
  echo "     Course Resolution Grid, NobsFiles = ${NobsFilesC}"
  echo
  for obs in ${OBSnameC[@]}; do
    obsfile=$(basename "$obs")
    obspath="${HereDir%/*}/Data/OBS/${obsfile}"
    if [ -f "${obspath}" ]; then
      echo "     OBSnameC: $obs"
    else
      echo "     file: ${obspath} cannot be found."
      exit 1
    fi
  done

Then the substitute Perl script replaces the template values with the correct IODA files. We must use the latest version of ROMS/Bin/susbtitude Perl script.

 $SUBSTITUTE $Inp4DVAR MyNobsFiles   ${NobsFiles}
 $SUBSTITUTE $Inp4DVAR roms_obs.nc   ${OBSname[@]}

We can check the log report when running submit_mixres_rbl4dvar.sh in DRYRUN mode (DRYRUN=1):

Running 4D-Var System:  Cycle = 1   Outer = 0   Phase = background

   Creating 4D-Var Input Script from Template: rbl4dvar_nl.in   Outer = 0  Phase = background
     (Resolution = 3 km, Fprefix = eccofs3km, Fsuffix = 20190101)

   Multi-scale configuration, MultiScale = 1

     SVCname  = ../../Data/GRD/eccofs3km_Bcorr_xy.nc4

     NRMnameM = ../../Data/NRM/eccofs3km_roms_nrm_multiscale_i.nc4
     NRMnameI = ../../Data/NRM/eccofs3km_roms_nrm_multiscale_i.nc4
     NRMnameB = ../../Data/NRM/eccofs3km_roms_nrm_multiscale_b.nc4
     NRMnameF = ../../Data/NRM/eccofs3km_roms_nrm_multiscale_f.nc4


   Processing 5 observation files:

     OBSname: ../../Data/OBS/eccofs3km_obs_adt_20190101.nc4
     OBSname: ../../Data/OBS/eccofs3km_obs_sst_20190101.nc4
     OBSname: ../../Data/OBS/eccofs3km_obs_sss_20190101.nc4
     OBSname: ../../Data/OBS/eccofs3km_obs_temp_20190101.nc4
     OBSname: ../../Data/OBS/eccofs3km_obs_salt_20190101.nc4

   Copying and editing ../s4dvar.in  into  rbl4dvar_nl.in

   mpirun -np 12 romsM_nl roms_nl_eccofs_era5_20190101.in

The rb4dvar_nl.in file generated from the template will have:

! Input observation filename(s), [1:Ngrids].

      NOBSFILES == 5
        OBSname == ../../Data/OBS/eccofs3km_obs_adt_20190101.nc4 \
                   ../../Data/OBS/eccofs3km_obs_sst_20190101.nc4 \
                   ../../Data/OBS/eccofs3km_obs_sss_20190101.nc4 \
                   ../../Data/OBS/eccofs3km_obs_temp_20190101.nc4 \
                   ../../Data/OBS/eccofs3km_obs_salt_20190101.nc4

I know it is a little bit complicated, but everything is scripted to avoid confusion and errors.


IODA Observations

The Interface for Observation Data Access (IODA) is a standardized data assimilation format that includes input observation and output diagnostic files. The Joint Center for Satellite Data Assimilation (JCSDA) developed it as part of its Joint Effort for Data asimilation Integration (JEDI) framework to facilitate processing of various observation data streams. IODA files are structured as hierarchical NetCDF-4 files that use the underlying HDF5 data model. This structure allows organization similar to subdirectories within a file.

Different groups within an IODA file can contain variables with the same name without conflict. This feature lets you store various data properties, including observation values, observation errors, quality control flags, model H(x) values, background errors, innovations, increments, residuals, and more.


In IODA, an enhanced NetCDF-4 file with extension .nc4, consists of several components, including: dimensions, dimension variables, global attributes, groups, and group variables:

netcdf app_obs_xxx_yyyymmdd.nc4 {

dimensions:
	Location = 10332 ;
	nvars = 2 ;
	survey = 72 ;
variables:
  ...

// global attributes:
		:_ioda_layout = "ObsGroup" ;
		...

group: MetaData {
  variables:
    ...
  } // group MetaData

group: ObsError {
  variables:
    ...
  } // group ObsError

group: ObsValue {
  variables:
    ...
  } // group ObsValue

group: PreQC {
  variables:
    ...
  } // group PreQC

group: ??? {
  variables:
    ...
  } // group ???

}

The MetaData Group contains variables that describe each observation's multidimensional coordinates and other characteristics. This group is the top level of the hierarchical structure in the IODA file. Additional variables provide unique functionality in native ROMS 4D-Var and ROMS-JEDI applications without affecting compatibility with the general JEDI building blocks.

MetaData Variables Algorithms Description
dateTime JEDI/native Time for each observation (seconds since reference date-time)
depth JEDI/native Depth below sea level for each observation (meter; negative)
latitude JEDI/native Latitude for each observation (degrees_north)
longitude JEDI/native Longitude for each observation (degrees_east)
sequenceNumber JEDI Original sequence order to unmap parallel Round Robin IODA processing
variables_name JEDI UFO/IODA observation standard name (string)
dateTimeAverageBegin native ROMS Observation start of time-averaged window (seconds since reference date-time)
dateTimeAverageEnd native ROMS Observation end of time-averaged window (seconds since reference date-time)
provenance native ROMS Observation origin identifier (integer)
spatialAverage native ROMS Observation spatial-averaged radius for H(x) operator
stateID native ROMS Internal ROMS state vector variable indentifier associtated with observation (integer)
surveyIndex native ROMS Observation survey time indices (integer) as they appear in dateTime
surveyTime native ROMS Observation survey time (seconds since reference date-time)
x_grid native ROMS Fractional X-grid location for each observation (floating-point)
y_grid native ROMS Fractional Y-grid location for each observation (floating-point)
y_grid native ROMS Fractional Z-grid location for each observation (floating-point)

Here is an example of the MetaData schema demonstrating the current group variables. The design is expandable, allowing you to add variables as needed for new observational data streams and complex H(x) operators.

group: MetaData {
  variables:
  	int64 dateTime(Location) ;
  		dateTime:long_name = "elapsed observation time since reference" ;
  		dateTime:units = "seconds since 2019-08-27T00:00:00Z" ;
  		dateTime:_FillValue = -2147483643LL ;
  	int64 dateTimeAverageBegin(Location) ;
  		dateTimeAverageBegin:long_name = "start of time averaging filter" ;
  		dateTimeAverageBegin:units = "seconds since 2019-08-27T00:00:00Z" ;
  		dateTimeAverageBegin:filter = "12-hour half-length averaging" ;
  		dateTimeAverageBegin:_FillValue = -2147483643LL ;
  	int64 dateTimeAverageEnd(Location) ;
  		dateTimeAverageEnd:long_name = "end of time averaging filter" ;
  		dateTimeAverageEnd:units = "seconds since 2019-08-27T00:00:00Z" ;
  		dateTimeAverageEnd:filter = "12-hour half-length averaging" ;
  		dateTimeAverageEnd:_FillValue = -2147483643LL ;
  	float depth(Location) ;
  		depth:long_name = "observation depth below sea level" ;
  		depth:units = "meter" ;
  		depth:negative = "downwards" ;
  		depth:_FillValue = 1.e+37f ;
  	float latitude(Location) ;
  		latitude:long_name = "observation latitude" ;
  		latitude:units = "degrees_north" ;
  		latitude:_FillValue = 1.e+37f ;
  	float longitude(Location) ;
  		longitude:long_name = "observation longitude" ;
  		longitude:units = "degrees_east" ;
  		longitude:_FillValue = 1.e+37f ;
  	int provenance(Location) ;
  		provenance:long_name = "observation origin identifier" ;
  		provenance:_FillValue = 0 ;
  	int sequenceNumber(Location) ;
  		sequenceNumber:long_name = "observation sequence number" ;
  		sequenceNumber:_FillValue = -1 ;
  	float spatialAverage(nvars) ;
  		spatialAverage:long_name = "spatial averaging radius" ;
  		spatialAverage:units = "meter" ;
  		spatialAverage:_FillValue = 1.e+37f ;
  	int stateID(nvars) ;
  		stateID:long_name = "state variable index" ;
  		stateID:_FillValue = -1 ;
  	int surveyIndex(survey) ;
  		surveyIndex:long_name = "observation survey time indices as they appear in dateTime" ;
  		surveyIndex:_FillValue = -1 ;
  	int64 surveyTime(survey) ;
  		surveyTime:long_name = "observation survey time" ;
  		surveyTime:units = "seconds since 2019-08-27T00:00:00Z" ;
  		surveyTime:_FillValue = -2147483643LL ;
  	string variables_name(nvars) ;
  		variables_name:long_name = "observation UFO/IODA standard name" ;
  		string variables_name:_FillValue = "MISSING" ;
  	float x_grid(Location) ;
  		x_grid:long_name = "observation fractional x-grid location" ;
  		x_grid:valid_min = 0.5 ;
  		x_grid:valid_max = 559.5 ;
  		x_grid:_FillValue = 1.e+37f ;
  	float y_grid(Location) ;
  		y_grid:long_name = "observation fractional y-grid location" ;
  		y_grid:valid_min = 0.5 ;
  		y_grid:valid_max = 241.5 ;
  		y_grid:_FillValue = 1.e+37f ;
  	float z_grid(Location) ;
  		z_grid:long_name = "observation fractional z-grid location" ;
  		z_grid:valid_min = 1.f ;
  		z_grid:valid_max = 50.f ;
  		z_grid:_FillValue = 1.e+37f ;
  } // group MetaData

Note

  • Each observation is characterized by its location in space and time (longitude, latitude, depth, dateTime).
  • All the variables in the MetaData Group are organized and declared in alphabetical order.
  • All the observations are assumed to be located at RHO-points, grid cell center (A-grid).
  • ROMS active state tracer variables are practical salinity (nondimensional, PSS-78) and potential temperature (Celsius). Keep this in mind when processing the raw data. Modern observations may use absolute salinity (g/kg; TEOS10) and in insitu temperature (Celsius). A practical salinity value of 35 equals an absolute salinity value of about 35.165 g/kg. Absolute salinity values are generally about 0.5 percent higher than practical salinity values. The ROMS equation of state for seawater is based on Jacket and McDougall (1995) and not on TEOS10.
  • The data uses single precision (float), so there's no need for double precision since the observations aren't measured with that accuracy. As a result, the files will be significantly smaller.
  • Time is measured in seconds from the reference date. It is an integer (int64, see variables dateTime, dateTimeAverageBegin, dateTimeAverageEnd and surveyTime above). You don't need to store time data as double precision, and using integers avoids rounding errors. The timestep in ROMS data assimilation is not in microseconds anyway. As a result, the file will be smaller and easier to transfer.
  • ROMS allows multiple time references. I haven't seen any benefit in using, for example, days since 2006-01-01 00:00:00, as we have done with our USEC or ECCOFS applications. It is prone to many rounding errors. We could use seconds from the start of the simulation year or, better yet, seconds since the data-assimilation cycle date to create smaller files and improve compression. It doesn't matter which reference date we choose for the observation files. ROMS will set it correctly!
  • Usually the IODA filenames are in the form: PREFIX_SOURCE_YYYYMMDD.nc4, where the PREFIX is an identifier for the application like eccofs3km_obs, usec6km_obs, wc13_obs, and so on. The SOURCE is a keyword for the observation type, such as adt, sst, sss, temp, salt, uv_codar, and many other observation labels. We recommend enhancing the SOURCE label with time- and area-averaged attributes, such as adt_time18_area60 for 18-hour and 60 km averaging. Finally, YYYYMMDD is the start of the data assimilation cycle using ISO 8601 guidelines for representing calendar dates (four-digit year, two-digit month, and two-digit day).
  • Repetitive observations should have their provenance multiplied by -1 so we can quickly identify them during processing. Usually, we use repetitive altimetry data that is added at different times but with different (larger) observation errors. This strategy has been deprecated and replaced with area- and time-averaging H(x) operators. The roms2ioda.m MATLAB script will assign negative provenance to repetitive altimetry data. It will remove these repetitive observations if you specify the area- and time-averaging parameters during conversion.
  • If the variables dateTimeAverageBegin and dateTimeAverageEnd are present in the MetaData Group, ROMS will apply the time-averaged H(x) operator for each observation location in space and time coordinates.
  • If the variable spatialAverage is present in the MetaData Group, ROMS will apply the area-averaged H(x) operator for each observation location in space and time coordinates. It is restricted to surface observations such as ADT, SST, SSS, and HF-Radar velocity.
  • The idea is to store observation data in a container with user-defined divisions. For example, we could have containers for annual observations. Then, we can create a script that extracts and generates the necessary IODA files for a specific data assimilation cycle. The user decides whether to save the IODA files. The user or the operational environment determines these protocols.

The IODA-type input and output files consist of multiple hierarchical groups that organize various fields related to observations, H(x) computations, and data assimilation diagnostics. The table below summarizes all the groups (⬇️ input, ⬆️ output, or ↕️ both input/output) in the native ROMS 4D-Var and ROMS-JEDI data assimilation algorithms.

Data Group DA System Description
↕️ ObsError native ROMS, JEDI Stores the error associated with each observation
↕️ ObsValue native ROMS, JEDI Holds observational data
↕️ PreQC native ROMS, JEDI Stores quality control information flag
⬆️ EffectiveError0 JEDI Stores initial observation effective error after QC steps
⬆️ EffectiveError1 JEDI Stores final observation effective error after QC steps
⬆️ EffectiveQC0 JEDI Stores initial QC value given by QCflags enumeration object
⬆️ EffectiveQC1 JEDI Stores final QC value given by QCflags enumeration object
⬆️ ObsBias0 JEDI Stores information about initial/prior observation bias correction
⬆️ ObsBias1 JEDI Stores information about final/analysis observation bias correction
⬆️ hofx0 JEDI Stores first guess prior at the observation locations, H(Xb) operator
⬆️ hofx1 JEDI Stores data assimilation analysis at the observation locations, H(Xa) operator
⬆️ oman JEDI Stores observation minus analysis, residual, y - H(Xa)
⬆️ ombg JEDI Stores observation minus background/prior, innovation, y - H(Xb)
⬆️ BackgroundError native ROMS Stores background/prior error at the observation locations
⬆️ HofxInitial native ROMS Stores first guess prior at the observation locations, H(Xb) operator
⬆️ HofxFinal native ROMS Stores data assimilation analysis at the observation locations, H(Xa) operator
⬆️ Innovation native ROMS Stores observation minus background/prior, y - H(Xb)
⬆️ Increment native ROMS Stores analysis minus background/prior, H(Xa) - H(Xb)
⬆️ Residual native ROMS Stores observation minus analysis, y - H(Xa)

Below is an example of an input HF-Radar velocity observations IODA file designed for the native ROMS 4D-Var. This file includes. The CODAR velocities are located at a depth of -2 m and at RHO-points (the grid cell center). Its vector components are in the geographical EAST and North directions. The new ROMS H(x) operators will interpolate the model to a depth of -2 m and apply the required variable changes for staggered C-grid locations and rotation to curvilinear coordinates.

usec6km_obs_uv_codar_20190830.nc4 {
dimensions:
	Location = 10332 ;
	nvars = 2 ;
	survey = 72 ;
variables:
	int Location(Location) ;
		Location:suggested_chunk_dim = 512 ;
		Location:_FillValue = -1 ;
	int nvars(nvars) ;
		nvars:suggested_chunk_dim = 100 ;
		nvars:_FillValue = -1 ;
	int survey(survey) ;
		survey:suggested_chunk_dim = 100 ;
		survey:_FillValue = -1 ;

// global attributes:
		:_ioda_layout = "ObsGroup" ;
		:_ioda_layout_version = 3 ;
		:odb_version = 1 ;
		:roms_grid = 279, 120, 50 ;
		:date_time = 2019082700. ;
		:datetimeReference = "2019-08-27T00:00:00Z" ;
		:description = "Native ROMS 4D-Var observations file converted to IODA" ;
		:sourceFiles = "OBS/usec6km_roms_obs_20190827.nc4" ;
		:history = "Created from Matlab script: create_ioda_obs on Wednesday - August 12, 2026 - 5:55:8.1567 PM" ;

group: MetaData {
  variables:
  	int64 dateTime(Location) ;
  		dateTime:long_name = "elapsed observation time since reference" ;
  		dateTime:units = "seconds since 2019-08-27T00:00:00Z" ;
  		dateTime:_FillValue = -2147483643LL ;
  	float depth(Location) ;
  		depth:long_name = "observation depth below sea level" ;
  		depth:units = "meter" ;
  		depth:negative = "downwards" ;
  		depth:_FillValue = 1.e+37f ;
  	float latitude(Location) ;
  		latitude:long_name = "observation latitude" ;
  		latitude:units = "degrees_north" ;
  		latitude:_FillValue = 1.e+37f ;
  	float longitude(Location) ;
  		longitude:long_name = "observation longitude" ;
  		longitude:units = "degrees_east" ;
  		longitude:_FillValue = 1.e+37f ;
  	int provenance(Location) ;
  		provenance:long_name = "observation origin identifier" ;
  		provenance:_FillValue = 0 ;
  	int sequenceNumber(Location) ;
  		sequenceNumber:long_name = "observation sequence number" ;
  		sequenceNumber:_FillValue = -1 ;
  	int stateID(nvars) ;
  		stateID:long_name = "state variable index" ;
  		stateID:_FillValue = -1 ;
  	int surveyIndex(survey) ;
  		surveyIndex:long_name = "observation survey time indices as they appear in dateTime" ;
  		surveyIndex:_FillValue = -1 ;
  	int64 surveyTime(survey) ;
  		surveyTime:long_name = "observation survey time" ;
  		surveyTime:units = "seconds since 2019-08-27T00:00:00Z" ;
  		surveyTime:_FillValue = -2147483643LL ;
  	string variables_name(nvars) ;
  		variables_name:long_name = "observation UFO/IODA standard name" ;
  		string variables_name:_FillValue = "MISSING" ;
  	float x_grid(Location) ;
  		x_grid:long_name = "observation fractional x-grid location" ;
  		x_grid:valid_min = 0.5 ;
  		x_grid:valid_max = 279.5 ;
  		x_grid:_FillValue = 1.e+37f ;
  	float y_grid(Location) ;
  		y_grid:long_name = "observation fractional y-grid location" ;
  		y_grid:valid_min = 0.5 ;
  		y_grid:valid_max = 120.5 ;
  		y_grid:_FillValue = 1.e+37f ;
  	float z_grid(Location) ;
  		z_grid:long_name = "observation fractional z-grid location" ;
  		z_grid:valid_min = 1.f ;
  		z_grid:valid_max = 50.f ;
  		z_grid:_FillValue = 1.e+37f ;
  } // group MetaData

group: ObsError {
  variables:
  	float waterZonalVelocity(Location) ;
  		waterZonalVelocity:long_name = "observation error standard deviation" ;
  		waterZonalVelocity:units = "m s-1" ;
  		waterZonalVelocity:coordinates = "longitude, latitude, depth, dateTime" ;
  		waterZonalVelocity:_FillValue = 1.e+37f ;
  	float waterMeridionalVelocity(Location) ;
  		waterMeridionalVelocity:long_name = "observation error standard deviation" ;
  		waterMeridionalVelocity:units = "m s-1" ;
  		waterMeridionalVelocity:coordinates = "longitude, latitude, depth, dateTime";
  		waterMeridionalVelocity:_FillValue = 1.e+37f ;
  } // group ObsError

group: ObsValue {
  variables:
  	float waterZonalVelocity(Location) ;
  		waterZonalVelocity:long_name = "observation value" ;
  		waterZonalVelocity:units = "m s-1" ;
  		waterZonalVelocity:coordinates = "longitude, latitude, depth, dateTime" ;
  		waterZonalVelocity:_FillValue = 1.e+37f ;
  	float waterMeridionalVelocity(Location) ;
  		waterMeridionalVelocity:long_name = "observation value" ;
  		waterMeridionalVelocity:units = "m s-1" ;
  		waterMeridionalVelocity:coordinates = "longitude, latitude, depth, dateTime";
  		waterMeridionalVelocity:_FillValue = 1.e+37f ;
  } // group ObsValue

group: PreQC {
  variables:
  	int waterZonalVelocity(Location) ;
  		waterZonalVelocity:long_name = "observation preset quality control filter identifier" ;
  		waterZonalVelocity:coordinates = "longitude, latitude, depth, dateTime" ;
  		waterZonalVelocity:_FillValue = -1 ;
  	int waterMeridionalVelocity(Location) ;
  		waterMeridionalVelocity:long_name = "observation preset quality control filter identifier" ;
  		waterMeridionalVelocity:coordinates = "longitude, latitude, depth, dateTime";
  		waterMeridionalVelocity:_FillValue = -1 ;
  } // group PreQC
}

Caution

In IODA, the observation error is represented by its standard deviation and has the same units as the observation value. In contrast, in the legacy native ROMS 4D-Var file, the observation error is expressed as variance in squared units.


The table below displays the names of native ROMS 4D-Var and ROMS-JEDI observations in input and output IODA files and their relationship to the ROMS control vector. Note that the State ID has a zero digit, as in 60 and 70, indicating a surface value for a 3D variable. In ROMS-JEDI, these variables are defined in test/Utility/obsop_name_map.yaml.

Short Name State ID Standard Name IODA Variable Name
ssh, SSH 1 sea_surface_height_above_geoid absoluteDynamicTopography
u, uocn 4 eastward_sea_water_velocity waterZonalVelocity
v, vocn 5 northward_sea_water_velocity waterMeridionalVelocity
sst, SST 6, 60 sea_surface_temperature seaSurfaceTemperature
tocn, temp 6 sea_water_temperature waterTemperature
sss, SSS 7, 70 sea_surface_salinity seaSurfaceSalinity
socn, salt 7 sea_water_salinity salinity

Integrating sea surface height (SSH) into ocean models is complex because it requires specialized processing operators. The zeta (𝜁) state variable in ROMS represents the free surface. This variable is a time-varying, physical boundary condition at the top of the numerical grid and is influenced by waves, tides, and currents. It can be interpreted as the sea-level anomaly relative to an unspecified geopotential datum. Conversely, SSH is an instantaneous measurement typically obtained from satellite altimeters. This measurement is relative to a reference ellipsoid and provides essential information about the Geoid, ocean dynamics, and seawater steric effects resulting from variations in temperature and salinity (density).

The Geoid is an undulating surface that represents the level the ocean would reach if it were influenced only by Earth's gravity and rotation, without effects from currents or winds. The SSH can also be calculated with respect to the Geoid, referred to as Absolute Dynamic Topography (ADT), as shown in the diagram below.

image

Therefore, the evolution of 𝜁(x,y,t) represents the instantaneous SSH, but several corrections to altimetry data are required to assimilate it correctly in a particular application.

In native ROMS 4D-Var, the Sea Level Anomaly (SLA) derived from altimetry represents the change in sea level after removing tidal effects. The Absolute Dynamic Topography (ADT) is calculated using ADT = MDT + SLA, where MDT stands for Mean Dynamic Topography. This value excludes tidal signals because it represents the average sea surface elevation above the Geoid.

For regional applications, such as along the U.S. East Coast, MDT is computed using the method described by Levin et al., 2018. During the data assimilation cycle in ROMS, tidal forcing is incorporated as lateral boundary conditions. Previously, ADT tracks were repeated to create an average representing a specific area and time, as illustrated below. However, this approach was suboptimal. It has since been improved by utilizing the spatialAverage, dateTimeAverageBegin, and dateTimeAverageEnd variables in the IODA observation file.

image

Native to IODA Conversion

Several MATLAB scripts in the roms_matlab repository process IODA files and convert a legacy, single native ROMS 4D-Var file into multiple IODA files. These scripts are well documented, so use the help command for any function below to see its arguments.

  • create_ioda_obs.m: It creates an IODA enhanced NetCDF-4 file from provided observation data structure.

  • ioda_add_filter.m: It duplicates an input IODA enhanced NetCDF-4 observation file and adds the required variables in the MetaData Group for area- and/or time-averaging processing by the H(x) operators in a ROMS data assimilation application.

  • ioda_ijkpos.m: It calculates IODA MetaData observation fractional grid locations (x_grid, y_grid, z_grid) in terms of ROMS (I, J, K) coordinates. The purpose of this computation is to facilitate the processing of H(x) operators in ROMS for curvilinear applications.

  • ioda_ijpos.m: It calculates IODA MetaData observation fractional grid locations (x_grid, y_grid) in terms of ROMS (I, J) coordinates. The purpose of this computation is to facilitate the processing of H(x) operators in ROMS for curvilinear applications.

  • ioda_metadata.m: It initializes the metadata structure, M, for IODA enhanced NetCDF-4 files. It sets the names of the data assimilation control variables, the area-averaged, and the time-averaged parameters to empty values. Users must assign these values later. This function is designed to be used before calling the ioda_add_filter.m and roms2ioda.m converters, as the resulting structure M is passed as an argument to those functions.

  • ioda_read.m: It reads the IODA observation NetCDF-4 file and stores all the variables in a structure, S.

  • ioda_superobs.m: It checks the provided IODA observation data and creates super-observations when there is more than one measurement of the same state variable per grid cell. At input, Sinp is either an IODA observation NetCDF-4 file or data structure.

  • ioda_write.m: It writes an observation data structure into an IODA observation NetCDF-4 file.

  • plot_ioda.m: It plots data fields from an IODA NetCDF-4 file.

  • roms2ioda.m: It converts a legacy, single, native ROMS 4D-Var file into multiple IODA enhanced NetCDF-4 files.

Important

The MATLAB script roms2ioda relies on several additional functions, including get_roms_grid, obs_k2z, and obs_read, which are part of the official ROMS MATLAB repository. To avoid redundancy, these secondary functions are not included in this directory. However, you can find all the necessary scripts in the subdirectory ioda within the official repository, available here. We recommend using the official ROMS MATLAB repository, as the functions needed for the conversion will be automatically included in your MATLAB path when you launch the application.


To convert from a legacy, single, native 4D-Var* file into multiple IODA enhanced NetCDF-4 files, follow these steps:

  1. Use the Matlab script ioda_metadata to set up the IODA metadata structure, denoted as M. This structure should contain information regarding the DA cycle length (cycle_length, hours), the area-averaged parameter (radius, in kilometers), the time-averaged parameter (time_window, half-value in hours), the NetCDF-4 names of the data assimilation control variables (ioda_vname), and the standard name (standard_name). Initially, the averaging parameters are empty.
>> M=ioda_metadata(72, true);                              % 72-hours (3-days) data assimilation cycle and reporting

IODA NetCDF-4 file Metadata Structure:

           name: 'SSH'
   cycle_length: 72
         radius: []
    time_window: []
     ioda_vname: 'absoluteDynamicTopography'
  standard_name: 'absolute_dynamic_topography'

           name: 'SST'
   cycle_length: 72
         radius: []
    time_window: []
     ioda_vname: 'seaSurfaceTemperature'
  standard_name: 'sea_surface_temperature'

           name: 'SSS'
   cycle_length: 72
         radius: []
    time_window: []
     ioda_vname: 'seaSurfaceSalinity'
  standard_name: 'sea_surface_salinity'

           name: 'uv_CODAR'
   cycle_length: 72
         radius: []
    time_window: []
     ioda_vname: {'waterZonalVelocity'  'waterMeridionalVelocity'}
  standard_name: {'eastward_sea_water_velocity'  'meridional_sea_water_velocity'}

           name: 'ptemp'
   cycle_length: 72
         radius: []
    time_window: []
     ioda_vname: 'waterPotentialTemperature'
  standard_name: 'sea_water_potential_temperature'

           name: 'temp'
   cycle_length: 72
         radius: []
    time_window: []
     ioda_vname: 'waterTemperature'
  standard_name: 'sea_water_temperature'

           name: 'salt'
   cycle_length: 72
         radius: []
    time_window: []
     ioda_vname: 'salinity'
  standard_name: 'sea_water_salinity'


      name        cycle_length       radius       time_window              ioda_vname                         standard_name           
  ____________    ____________    ____________    ____________    _____________________________    ___________________________________

  {'SSH'     }         72         {0×0 double}    {0×0 double}    {'absoluteDynamicTopography'}    {'absolute_dynamic_topography'    }
  {'SST'     }         72         {0×0 double}    {0×0 double}    {'seaSurfaceTemperature'    }    {'sea_surface_temperature'        }
  {'SSS'     }         72         {0×0 double}    {0×0 double}    {'seaSurfaceSalinity'       }    {'sea_surface_salinity'           }
  {'uv_CODAR'}         72         {0×0 double}    {0×0 double}    {1×2 cell                   }    {1×2 cell                         }
  {'ptemp'   }         72         {0×0 double}    {0×0 double}    {'waterPotentialTemperature'}    {'sea_water_potential_temperature'}
  {'temp'    }         72         {0×0 double}    {0×0 double}    {'waterTemperature'         }    {'sea_water_temperature'          }
  {'salt'    }         72         {0×0 double}    {0×0 double}    {'salinity'                 }    {'sea_water_salinity'             }
  1. Execute the roms2ioda script to convert from native to multiple IODA files. Note that it creates the potential temperature file using the sw_press and sw_ptmp functions with a constant salinity of 35.0. This file is not used, but it is created so the user knows the ROMS state variable is potential temperature, not insitu temperature.
>> ObsData = 'USEC/Data/OBS/usec3km_roms_obs_20190827.nc';     % single native classic NetCDF file 
>> HisName = 'USEC/Data/INI/usec3km_roms_ini_20190827.nc';     % ROMS history NetCDF for geometry information

>> roms2ioda(ObsData, HisName, 'usec3km_obs', '20190827', M);
 
*** Creating observations file:  usec3km_obs_adt_20190827.nc4
*** Writing  observations file:  usec3km_obs_adt_20190827.nc4
 
*** Creating observations file:  usec3km_obs_sst_20190827.nc4
*** Writing  observations file:  usec3km_obs_sst_20190827.nc4
 
*** Creating observations file:  usec3km_obs_sss_20190827.nc4
*** Writing  observations file:  usec3km_obs_sss_20190827.nc4
 
*** Creating observations file:  usec3km_obs_temp_20190827.nc4
*** Writing  observations file:  usec3km_obs_temp_20190827.nc4
 
*** Creating observations file:  usec3km_obs_ptemp_20190827.nc4
*** Writing  observations file:  usec3km_obs_ptemp_20190827.nc4
 
*** Creating observations file:  usec3km_obs_salt_20190827.nc4
*** Writing  observations file:  usec3km_obs_salt_20190827.nc4
 
*** Creating observations file:  usec3km_obs_uv_codar_20190827.nc4
*** Writing  observations file:  usec3km_obs_uv_codar_20190827.nc4
  1. To create a specific IODA file with information about area-averaging and/or time-averaging for the respective H(x) averaging operators, use the function ioda_add_filter.m. Here, we specify the half-length averaging time in hours for each datum. Thus, we recommend that such an IODA file contain a single control variable. Time averaging will be truncated for observations near the start or end of the data assimilation cycle.
>> M(strcmp({M.name}, 'SSH')).radius = 30;           % 30 km averaging radius
>> M(strcmp({M.name}, 'SSH')).time_window = 18;      % 36 hours averaging time-window centered at observation time
                                                     %             |<------------ 36 h ------------->|
                                                     %             |---------- dateTime(n) ----------|
                                                     %             |<-----18 h----->|<-----18 h----->|

>> disp( struct2table(M) );                          % Check that parameters in the metadata structure are correct

        name        cycle_length       radius       time_window              ioda_vname                         standard_name           
    ____________    ____________    ____________    ____________    _____________________________    ___________________________________

    {'SSH'     }         72         {[      30]}    {[      18]}    {'absoluteDynamicTopography'}    {'absolute_dynamic_topography'    }
    {'SST'     }         72         {0×0 double}    {0×0 double}    {'seaSurfaceTemperature'    }    {'sea_surface_temperature'        }
    {'SSS'     }         72         {0×0 double}    {0×0 double}    {'seaSurfaceSalinity'       }    {'sea_surface_salinity'           }
    {'uv_CODAR'}         72         {0×0 double}    {0×0 double}    {1×2 cell                   }    {1×2 cell                         }
    {'ptemp'   }         72         {0×0 double}    {0×0 double}    {'waterPotentialTemperature'}    {'sea_water_potential_temperature'}
    {'temp'    }         72         {0×0 double}    {0×0 double}    {'waterTemperature'         }    {'sea_water_temperature'          }
    {'salt'    }         72         {0×0 double}    {0×0 double}    {'salinity'                 }    {'sea_water_salinity'             }

>> ioda_add_filter('usec3km_obs_adt_20190827.nc4', 'usec3km_obs_adt_area30_time36_20190827.nc4',M);
 
*** Creating observations file:  usec3km_obs_adt_area30_time36_20190827.nc4
*** Writing IODA observations file:  usec3km_obs_adt_area30_time36_20190827.nc4

Check your IODA file for the correct averaging parameters before running the data assimilation system:

% ncdump -v spatialAverage usec3km_adt_area30_time36_20190827.nc4

netcdf usec3km_obs_adt_area30_time36_20190827 {
  ...

group: MetaData {
  variables:
  	int64 dateTime(Location) ;
  		dateTime:long_name = "elapsed observation time since reference" ;
  		dateTime:units = "seconds since 2019-08-27T00:00:00Z" ;
  		dateTime:_FillValue = -2147483643LL ;
  	int64 dateTimeAverageBegin(Location) ;
  		dateTimeAverageBegin:long_name = "start of time averaging filter" ;
  		dateTimeAverageBegin:units = "seconds since 2019-08-27T00:00:00Z" ;
  		dateTimeAverageBegin:filter = "18-hour half-length averaging" ;
  		dateTimeAverageBegin:_FillValue = -2147483643LL ;
  	int64 dateTimeAverageEnd(Location) ;
  		dateTimeAverageEnd:long_name = "end of time averaging filter" ;
  		dateTimeAverageEnd:units = "seconds since 2019-08-27T00:00:00Z" ;
  		dateTimeAverageEnd:filter = "18-hour half-length averaging" ;
  		dateTimeAverageEnd:_FillValue = -2147483643LL ;
  	float latitude(Location) ;
  		latitude:long_name = "observation latitude" ;
  		latitude:units = "degrees_north" ;
  		latitude:_FillValue = 1.e+37f ;
  	float longitude(Location) ;
  		longitude:long_name = "observation longitude" ;
  		longitude:units = "degrees_east" ;
  		longitude:_FillValue = 1.e+37f ;
  	int provenance(Location) ;
  		provenance:long_name = "observation origin identifier" ;
  		provenance:negative = "repetitive observation at different times and error" ;
  		provenance:_FillValue = 0 ;
  	int sequenceNumber(Location) ;
  		sequenceNumber:long_name = "observation sequence number" ;
  		sequenceNumber:_FillValue = -1 ;
  	float spatialAverage(nvars) ;
  		spatialAverage:long_name = "spatial averaging radius" ;
  		spatialAverage:units = "meter" ;
  		spatialAverage:_FillValue = 1.e+37f ;
  	int stateID(nvars) ;
  		stateID:long_name = "state variable index" ;
  		stateID:_FillValue = -1 ;
  	int surveyIndex(survey) ;
  		surveyIndex:long_name = "observation survey time indices as they appear in dateTime" ;
  		surveyIndex:_FillValue = -1 ;
  	int64 surveyTime(survey) ;
  		surveyTime:long_name = "observation survey time" ;
  		surveyTime:units = "seconds since 2019-08-27T00:00:00Z" ;
  		surveyTime:_FillValue = -2147483643LL ;
  	string variables_name(nvars) ;
  		variables_name:long_name = "observation UFO/IODA standard name" ;
  		string variables_name:_FillValue = "MISSING" ;
  	float x_grid(Location) ;
  		x_grid:long_name = "observation fractional x-grid location" ;
  		x_grid:valid_min = 0.5 ;
  		x_grid:valid_max = 560 ;
  		x_grid:_FillValue = 1.e+37f ;
  	float y_grid(Location) ;
  		y_grid:long_name = "observation fractional y-grid location" ;
  		y_grid:valid_min = 0.5 ;
  		y_grid:valid_max = 242 ;
  		y_grid:_FillValue = 1.e+37f ;
  data:

   spatialAverage = 30000 ;

  } // group MetaData
⚠️ **GitHub.com Fallback** ⚠️