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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions