Skip to content

Opening TEMPO L2 ozone data products #13

@pvanlaake

Description

@pvanlaake

TEMPO ozone data are a new product for the monitoring of ozone and air pollution from a geostationary satellite over North America. The L2 products (granules of data from a satellite overpass) have an instrument coordinate system using a non-standard encoding in the netCDF file. As a result, packages like stars and terra cannot successfully extract the data from these files. This issue has been flagged here.

Below is a demonstration of opening TEMPO L2 files with package ncdfCF. Please note that you need package version >=0.8.0.

library(ncdfCF)

ds <- open_ncdf("~/Downloads/tempo_o3prof_l2_v04_20250921t210803z_s012g02/TEMPO_O3PROF_L2_V04_20250921T210803Z_S012G02.nc")
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_profile'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_profile'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_profile_precision'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_profile_precision'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_profile_error'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_profile_error'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_apriori_profile'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_apriori_profile'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_apriori_profile_error'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_apriori_profile_error'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_profile_altitude'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_profile_altitude'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_profile_altitude_bounds'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_profile_altitude_bounds'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_profile_temperature'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_profile_temperature'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_profile_pressure'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_profile_pressure'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_profile_pressure_bounds'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_profile_pressure_bounds'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_pressure' found in
#> variable 'ozone_averaging_kernel'.
#> Warning: Unmatched `coordinates` value 'ozone_profile_altitude' found in
#> variable 'ozone_averaging_kernel'.

Oops. Bad encoding of the "coordinates" attribute in 11 variables. This will not affect the processing but not looking good.

(vars <- ds$var_names)
#>  [1] "/product/ozone_profile"                           
#>  [2] "/product/ozone_profile_precision"                 
#>  [3] "/product/ozone_profile_error"                     
...
#> [51] "/qa_statistics/num_iterations"                    
#> [52] "/qa_statistics/fit_RMS"                           
#> [53] "/qa_statistics/avg_residuals"

The original post was about the "ozone_profile" so that is the first variable, located in the "product" group.

(o3 <- ds[[ vars[1L] ]])
#> <Variable> ozone_profile 
#> Path name: /product/ozone_profile 
#> 
#> Values: (not loaded)
#> 
#> Axes:
#>  name        long_name                  length values       
#>  layer                                   24    [1 ... 24]   
#>  xtrack      pixel index along slit     512    [0 ... 511]  
#>  mirror_step scan mirror position index 132    [132 ... 263]
#> 
#> Auxiliary longitude-latitude grid:
#>  axis name      extent                unit         
#>  X    longitude [-71.712 ... -45.730] degrees_east 
#>  Y    latitude  [17.378 ... 60.636]   degrees_north
#> 
#> Attributes:
#>  name        type     length value                         
#>  comment     NC_CHAR  23     retrieved ozone profile       
#>  units       NC_CHAR   2     DU                            
#>  valid_min   NC_FLOAT  1     -100                          
#>  valid_max   NC_FLOAT  1     100                           
#>  _FillValue  NC_FLOAT  1     -1.26765060022823e+30         
#>  coordinates NC_CHAR  69     time longitude latitude ozo...

Great, here's the data, but note how the axes are called "xtrack" and "mirror_step" and being just index values into the data array, not real coordinates. The data variable does have an "Auxiliary longitude-latitude grid", however, giving latitude-longitude values for each pixel in the data array. We can use the subset() method with those auxiliary coordinates as axis selectors to warp the instrument coordinate system to lat-lon:

# Using a resolution of 0.04 degrees here, the default for TEMPO L3 products.
(o3_ll <- o3$subset(longitude = NA, latitude = NA, layer = 24, .resolution = 0.04))
#> <Variable> ozone_profile 
#> 
#> Values: [-0.156455 ... 13.96814] DU
#>     NA: 542558 (77.3%)
#> 
#> Axes:
#>  axis name      long_name                  length values                      unit
#>  X    longitude pixel index along slit      649   [-71.691716 ... -45.771716] degrees_east 
#>  Y    latitude  scan mirror position index 1081   [17.398387 ... 60.598387]   degrees_north
#>       layer                                   1   [24]                       
#>
#> Attributes:
#>  name         type     length value                         
#>  comment      NC_CHAR  23     retrieved ozone profile       
#>  units        NC_CHAR   2     DU                            
#>  valid_min    NC_FLOAT  1     -100                          
#>  valid_max    NC_FLOAT  1     100                           
#>  _FillValue   NC_FLOAT  1     -1.26765060022823e+30         
#>  coordinates  NC_CHAR  50     time ozone_profile_pressure...
#>  actual_range NC_FLOAT  2     -0.156455, 13.968141

Objects from package ncdfCF can be read by stars:

library(stars)
#> Loading required package: abind
#> Loading required package: sf
#> Linking to GEOS 3.13.0, GDAL 3.8.5, PROJ 9.5.1; sf_use_s2() is TRUE
(o3_stars <- st_as_stars(o3_ll))
#> stars object with 2 dimensions and 1 attribute
#> attribute(s):
#>                          Min.  1st Qu.   Median    Mean  3rd Qu.     Max.   NA's
#> ozone_profile [DU] -0.1564551 6.509749 7.033861 7.18038 8.031126 13.96814 542558
#>                      
#> dimension(s):
#>           from   to offset delta    refsys x/y
#> longitude    1  649 -71.71  0.04 OGC:CRS84 [x]
#> latitude     1 1081  17.38  0.04 OGC:CRS84 [y]

plot(o3_stars)
Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions