Work with JEDI IODA files#
IODA stands for Interface for Observational Data Access.
JEDI can only ingest observatios stored in the IODA format. Therefore, observations from other formats (such as text, PrepBUFR, BUFR, etc.) must first be converted into IODA files before they can be used by JEDI. These observation files are usually referred to as input IODA files.
After data assimilation (DA), JEDI writes both the original observation information and DA-related quantities (e.g., innovations, H(x), quality control flags, and bias corrections) into output IODA files.
In the MPAS and JEDI based wx-workflow (including rrfs-workflow, rtma-workflow, ar-worklfow, etc), these output IODA files are referred to as jdiag files.
This notebook demonstrates how to process both input and output IODA files using the netCDF4 Python package as well as the DAmonitor obsSpace class, which provides a more convenient and concise interface than working directly with the raw netCDF4 API.
0. Environment Setup#
%%time
# autoload external python modules if they changed
%load_ext autoreload
%autoreload 2
import os, sys
pyDAmonitor_ROOT=os.getenv("pyDAmonitor_ROOT")
if pyDAmonitor_ROOT is None:
print("!!! pyDAmonitor_ROOT is NOT set. Run `source ush/load_pyDAmonitor.sh`")
else:
print(f"pyDAmonitor_ROOT={pyDAmonitor_ROOT}\n")
sys.path.insert(0, pyDAmonitor_ROOT)
from DAmonitor.base import query_dataset, query_data, query_obj
pyDAmonitor_ROOT=/ncrc/home2/Guoqing.Ge/pyDAmonitor
CPU times: user 7.26 s, sys: 117 ms, total: 7.38 s
Wall time: 320 ms
1. Use the raw netCDF4 interface to process ioda files#
1.1 Inspecting the Structure of IODA Files#
%%time
from netCDF4 import Dataset
dcObs={
"obs": "aircar",
"jdiag": "t133",
}
print("-------- obs_file --------")
obs_file = f"{pyDAmonitor_ROOT}/data/ioda/ioda_{dcObs['obs']}.nc"
ds = Dataset(obs_file, mode='r')
print(ds)
query_dataset(ds) # query_dataset is a function provided by DAmonitor to loop through and print all groups/variables information of a NetCDF4 dataset
print("\n\n-------- jdiag_file --------")
jdiag_file = f"{pyDAmonitor_ROOT}/data/mpasjedi/jdiag_{dcObs['obs']}_{dcObs['jdiag']}.nc"
ds = Dataset(jdiag_file, mode='r')
print(ds)
query_dataset(ds)
-------- obs_file --------
<class 'netCDF4.Dataset'>
root group (NETCDF4 data model, file format HDF5):
_ioda_layout: ObsGroup
_ioda_layout_version: 0
Orig_obs_file: ioda_aircar.nc
Grid_file: /scratch3/BMC/rtwrfruc/RRFSv2X/code/rrfs-workflow.20260611/fix/conus3km/conus3km.static.nc
Shrink_factor: 0.005
dimensions(sizes): Location(65847)
variables(dimensions): int64 Location(Location)
groups: MetaData, ObsError, ObsType, ObsValue, QualityMarker
*MetaData
aircraftFlightNumber, aircraftFlightPhase, dateTime, dumpReportType, height, latitude, longitude, prepbufrDataLvlCat, prepbufrReportType, pressure, stationElevation, stationIdentification, timeOffset, longitude_latitude_pressure
*ObsError
airTemperature, pressure, relativeHumidity, specificHumidity, windEastward, windNorthward
*ObsType
airTemperature, specificHumidity, windEastward, windNorthward
*ObsValue
airTemperature, dewpointTemperature, specificHumidity, windEastward, windNorthward
*QualityMarker
airTemperature, height, pressure, specificHumidity, windEastward, windNorthward
Location
-------- jdiag_file --------
<class 'netCDF4.Dataset'>
root group (NETCDF4 data model, file format HDF5):
Grid_file: /scratch3/BMC/rtwrfruc/RRFSv2X/code/rrfs-workflow.20260611/fix/conus3km/conus3km.static.nc
Orig_obs_file: ioda_aircar.nc
Shrink_factor: 0.005
_ioda_layout: ObsGroup
_ioda_layout_version: 0
dimensions(sizes): Location(27732)
variables(dimensions): int64 Location(Location)
groups: EffectiveError0, EffectiveError1, EffectiveError2, EffectiveQC0, EffectiveQC1, EffectiveQC2, MetaData, ObsBias0, ObsBias1, ObsBias2, ObsError, ObsType, ObsValue, QualityMarker, TempObsErrorData, hofx0, hofx1, hofx2, innov1, oman, ombg
*EffectiveError0
airTemperature
*EffectiveError1
airTemperature
*EffectiveError2
airTemperature
*EffectiveQC0
airTemperature
*EffectiveQC1
airTemperature
*EffectiveQC2
airTemperature
*MetaData
stationIdentification, aircraftFlightPhase, timeOffset, prepbufrDataLvlCat, longitude, stationElevation, latitude, prepbufrReportType, pressure, longitude_latitude_pressure, dateTime, height, dumpReportType, aircraftFlightNumber
*ObsBias0
airTemperature
*ObsBias1
airTemperature
*ObsBias2
airTemperature
*ObsError
airTemperature, relativeHumidity, specificHumidity, windEastward, pressure, windNorthward
*ObsType
specificHumidity, windEastward, airTemperature, windNorthward
*ObsValue
dewpointTemperature, specificHumidity, airTemperature, windEastward, windNorthward
*QualityMarker
airTemperature, pressure, windEastward, specificHumidity, height, windNorthward
*TempObsErrorData
airTemperature
*hofx0
airTemperature
*hofx1
airTemperature
*hofx2
airTemperature
*innov1
airTemperature
*oman
airTemperature
*ombg
airTemperature
Location
CPU times: user 11.6 ms, sys: 7.99 ms, total: 19.6 ms
Wall time: 221 ms
Understanding the IODA File Structure#
An IODA file is a standard NetCDF4 file and can be opened directly with ds = Dataset(obs_file, mode="r").
Calling print(ds) provides a high-level summary of the file, including its dimensions (e.g., 65,847 locations in the input observation file, and 28,039 locations in the jdiag file), groups, and variables.
From the output above, we can see that an input IODA file (observation file) contains groups such as MetaData, ObsError, ObsType, ObsValue, QualityMarker along with dimensions such as Location (and Channel for satellite observations).
After data assimilation, JEDI writes an output IODA file (commonly called a jdiag file in wx-workflow), which contains the original observation information plus several additional groups that store DA diagnostics, including EffectiveErrorX, EffectiveQCX, ObsBiasX, hofxX, innov1, oman, ombg etc.
These groups have the following meanings:
ombg: observation - background
oman: observation - analysis
innova1: observation - first_guess after the first outer loop
# X = 0, 1, 2, ... indicates the DA outer loops:
# 0: before any outer loop
# 1: after the first outer loop
# 2: after the second outer loop
# ...
EffectiveErrorX: effective obs error used in the inner minimization process, see the above for the meaning of X (0, 1, 2)
EffectiveQCX: effective QC flag
ObsBiasX: observation bias
hofxX: H(x), the non-linear forward operatior result, i.e. the model counterpart to the observation
An input IODA file often contains observations for multiple variables (e.g., t, q, uv, etc.),
whereas a jdiag file typically contains DA diagnostics for a single variable. For example, jdiag_aircar_t133.nc contains diagnostics only for temperature observations.
A jdiag file may also contains diagnostics for multiple variables if they are assimilated within the same ObsSpace. However, in wx-workflow (including rrfs-workflow, rtma-workflow, ar-workflow, etc), the current convention is to configure one ObsSpace for each PrepBUFR observation type (i.e., one entry in the convinfo file) or for each satellite sensor. As a result, most jdiag files produced by these workflows contain diagnostics for only one single observation variable.
1.2 Access the metadata and acutal observation data#
# print the "Location" dimension and its actual content
print(ds.dimensions['Location'])
print(len(ds.variables['Location']))
print(ds.variables['Location'][:])
"<class 'netCDF4.Dimension'>" (unlimited): name = 'Location', size = 27732
27732
[ 0 8555 8557 ... 30767 37440 41184]
# print the ObsValue of t (airTemperature) and other variables
print(ds.groups["ObsValue"].variables["airTemperature"])
print("----------------")
print(ds.groups["ObsValue"].variables["airTemperature"][:]) # actual content
print(ds.groups["ObsValue"].variables["specificHumidity"][:])
<class 'netCDF4.Variable'>
float32 airTemperature(Location)
_FillValue: -3.3687953e+38
coordinates: longitude latitude
long_name: Temperature
units: K
path = /ObsValue
unlimited dimensions: Location
current shape = (27732,)
filling on
----------------
[279.25 273.55 279.25 ... 282.05 292.75 304.25]
[-- -- -- ... -- -- --]
Note#
The file jdiag_aircar_t133.nc contains data assimilation (DA) results only for the aircar_t133 ObsSpace. Consequently, only ObsValue/airTemperature contains valid observation values. All other variables under the ObsValue group contain only missing values (i.e., [-- -- -- ... -- -- --]).
2. Use the DAmonitor class to process ioda files#
As shown above, accessing metadata and variables in an IODA file using the netCDF4 interface requires navigating through the group/variable hierarchy.
For example, to read the air temperature observations, you would write: ds.groups["ObsValue"].variables["airTemperature"][:]
This expression contains 52 characters and can become cumbersome when we access data repeatedly.
The DAmonitor class simplifies this process by flattening the IODA group/variable structure and providing direct access to commonly used metadata and variables.
For example, instead of writing print(ds.groups["ObsValue"].variables["specificHumidity"][:]), we may simply write print(t133.q.ObsValue)
This reduces the access expression from 52 characters to just a few characters, making the code more concise, readable, and easier to maintain.
2.1 DAmonitor Overview#
DAmonitor consists of several modules, including obs, mpas, base, and shapes, each serving a different purpose:
obs: Provides classes and functions for observation-space processing, such asobsSpaceandfit_rate.mpas: Provides classes and functions for model-space processing, including visualization utilities such ashcross_contourandvcross_contour.base: Provides common utilities, includingsource, get_run_directory, get_inv_bkg_ana_files, load_inv_bkg_ana, query_dataset, query_data, query_obj, to_dataframe, load_figs, and aircraft_reject_list_to_df.shapes: Provides geographic features to facilitate plotting, includingcoast_lines, state_lines, counties_gdf, and county_lines.
In this notebook, we focus on the obsSpace class from the obs module.
We can easily load an IODA file into an obsSpace object using obs = obsSpace(filename) and then inspect the contents (attributes, data, etc) of the object using query_obj(obs)
As shown below, an obsSpace object may contain the following attributes (the exact set depends on the type of observations in the IODA file):
Attribute |
Description |
|---|---|
|
The input file path. |
|
The original |
|
The number of locations (records) in the IODA file. |
|
The indices of all locations. |
|
The number of channels for a satellite sensor. |
|
The channel numbers. |
|
The group names contained in the IODA file. |
|
Stores all variables from the original |
|
Observation data for the corresponding variable. These attributes are available only if the variable exists in the IODA file (e.g., |
Observation data attributes |
Each observation data in the above may contain the following attributes: all variables from the |
# use obsSpace to access an ioda file
from DAmonitor.obs import obsSpace
t133=obsSpace(jdiag_file) # t133 is an obsSpace object
print(t133.filepath)
query_obj(t133)
/ncrc/home2/Guoqing.Ge/pyDAmonitor/data/mpasjedi/jdiag_aircar_t133.nc
['ds',
'filepath',
'groups',
'locations',
'metadata',
'nlocs',
'q',
'rh',
't',
'td',
'u',
'v']
# query the data (note: t is an internal object containing a `data` attribute and query_data is intelligent enough to query the `data` attribute directly)
query_data(t133.t)
EffectiveError0, EffectiveError1, EffectiveError2, EffectiveQC0, EffectiveQC1, EffectiveQC2, stationIdentification, aircraftFlightPhase, timeOffset, prepbufrDataLvlCat, longitude, stationElevation, latitude, prepbufrReportType, pressure, dateTime, height, dumpReportType, aircraftFlightNumber, ObsBias0, ObsBias1, ObsBias2, ObsError, ObsType, ObsValue, QualityMarker, TempObsErrorData, hofx0, hofx1, hofx2, innov1, oman, ombg, Location
# access the actual data
print(t133.t.stationIdentification[10])
print(t133.t.aircraftFlightNumber[10])
print(t133.t.aircraftFlightNumber)
print(t133.t.ObsValue)
t133.t.ObsValue
VTTDSIZA
C4GBTSZA
['ISQBOZRA' '0BIDSQRA' '0BIDSQRA' ... 'EPGCFGRA' 'EPGCFGRA' 'EPGCFGRA']
[279.25 273.55 279.25 ... 282.05 292.75 304.25]
masked_array(data=[279.25, 273.55, 279.25, ..., 282.05, 292.75, 304.25],
mask=False,
fill_value=np.float64(1e+20),
dtype=float32)
# check what's inside EffectiveQc and EffectiveError
print(t133.t.EffectiveQC0)
print(t133.t.EffectiveError0)
[0 0 0 ... 0 0 0]
[0.8210167288780212 0.7708320617675781 0.7883864045143127 ...
0.827965259552002 1.0205713510513306 7.202971935272217]
# check whether EffectiveError0, EffectiveError1 and EffectiveError2 are the same
# Note: They are same for this ioda file and this DA configuration. They may be different in other situations.
import numpy as np
a = t133.t.EffectiveError0
b = t133.t.EffectiveError1
c = t133.t.EffectiveError2
print(np.array_equal(a, b))
print(np.array_equal(a, c))
True
True
# check whether EffectiveQC0, EffectiveQC1 and EffectiveQC2 are the same
# Note: They are same for this ioda file and this DA configuration. They may be different in other situations.
import numpy as np
a = t133.t.EffectiveQC0
b = t133.t.EffectiveQC1
c = t133.t.EffectiveQC2
print(np.array_equal(a, b))
print(np.array_equal(a, c))
True
True
3. What method to use? DAmonitor or the raw method?#
DAmonitor is recommended for working with IODA files, as it provides a higher-level interface that simplifies common tasks and improves code readability. However, the raw method introduced in Section 1 is also accepted.