Processing Sentinel-5P data on air pollution using Jupyter Notebook on CREODIAS

Prerequisites

No. 1 Hosting

You need a CREODIAS hosting account with access to the Horizon interface: https://horizon.cloudferro.com.

No. 2 Installing Jupyter Notebook

The code in this article runs on Jupyter Notebook and you can install it on your platform of choice by following the offical Jupyter Notebook install page.

One particular way of installing (and by no means being required to follow up this article), is to install it on Kubernetes cluster. If that is the case, this article will be useful:

Installing JupyterHub on Magnum Kubernetes Cluster in CREODIAS Cloud.

PART #1 - Download and prepare data

This notebook aims to present how to process Sentinel-5P data and prepare it for further analysis, using HARP, via CREODIAS platfrom.

In this notebook you will learn how to:

  • create a query via https://explore.creodias.eu/

  • upload data from eodata CREODIAS repository

  • read many of Sentinel-5P images (netCDF)

  • process Sentinel-5P images (tropospheric column of NO2 - NO2 TVCD) using HARP

  • create a single image which is going to be monthly average of NO2 TVCD (netcdf)

  • plot results

Datasets which are going to be used:

HARP https://atmospherictoolbox.org/harp/

Import libraries

import harp
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.io.img_tiles as cimgt
from cmcrameri import cm
import json, requests, os

Upload data using query generated by Data Explorer

Additionally, you can set time of analysis:

start

first day of analysed period

end

last day od analysed period

start = str("2023-03-01") #first day of the analysed period
end = str("2023-03-31")   #last day of the analysed period

#querry generated by Data Explorer
url = f"https://datahub.creodias.eu/odata/v1/Products?$filter=((ContentDate/Start ge {start}T00:00:00.000Z and ContentDate/Start le {end}T23:59:59.999Z) and (Online eq true) and (OData.CSC.Intersects(Footprint=geography'SRID=4326;POLYGON ((13.44866 55.702105, 24.616418 55.287061, 24.505406 48.582106, 13.359851 49.556626, 13.44866 55.702105))')) and (((((((Attributes/OData.CSC.StringAttribute/any(i0:i0/Name eq 'productType' and i0/Value eq 'L2__NO2___')))) and (((Attributes/OData.CSC.StringAttribute/any(i0:i0/Name eq 'processingMode' and i0/Value eq 'OFFL')))) and (Collection/Name eq 'SENTINEL-5P'))))))&$expand=Attributes&$top=1000"
#take a look on result url
"https://datahub.creodias.eu/odata/v1/Products?$filter=((ContentDate/Start ge 2023-03-01T00:00:00.000Z and ContentDate/Start le 2023-03-31T23:59:59.999Z) and (Online eq true) and (OData.CSC.Intersects(Footprint=geography'SRID=4326;POLYGON ((13.44866 55.702105, 24.616418 55.287061, 24.505406 48.582106, 13.359851 49.556626, 13.44866 55.702105))')) and (((((((Attributes/OData.CSC.StringAttribute/any(i0:i0/Name eq 'productType' and i0/Value eq 'L2__NO2___')))) and (((Attributes/OData.CSC.StringAttribute/any(i0:i0/Name eq 'processingMode' and i0/Value eq 'OFFL')))) and (Collection/Name eq 'SENTINEL-5P'))))))&$expand=Attributes&$top=1000"

Define lists of paths of files and files’ names

#get list of products (JSON)
products = json.loads(requests.get(url).text)

path_list = []                           #create an empty list to store the file names
for item in products['value']:
    path_list.append(item['S3Path'])     #append each S3Path (path) to the list
    print(item['S3Path'])                #print each S3Path (path) to the list

name_list = []                           #create an empty list to store the file names
for item in products['value']:
    name_list.append(item['Name'])       #append each Name (name) to the list
    print(item['Name'])                  #print each Name (name) to the list
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/29/S5P_OFFL_L2__NO2____20230329T113029_20230329T131159_28277_03_020500_20230331T035449
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/29/S5P_OFFL_L2__NO2____20230329T094858_20230329T113029_28276_03_020500_20230331T021341
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/10/S5P_OFFL_L2__NO2____20230310T104321_20230310T122451_28007_03_020400_20230312T025817
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/30/S5P_OFFL_L2__NO2____20230330T093006_20230330T111136_28290_03_020500_20230401T015114
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/07/S5P_OFFL_L2__NO2____20230307T114008_20230307T132138_27965_03_020400_20230309T040214
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/09/S5P_OFFL_L2__NO2____20230309T110217_20230309T124347_27993_03_020400_20230311T031357
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/20/S5P_OFFL_L2__NO2____20230320T091549_20230320T105720_28148_03_020500_20230322T012553
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/13/S5P_OFFL_L2__NO2____20230313T112803_20230313T130934_28050_03_020500_20230316T030602
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/02/S5P_OFFL_L2__NO2____20230302T113316_20230302T131447_27894_03_020400_20230304T041315
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/02/S5P_OFFL_L2__NO2____20230302T095146_20230302T113316_27893_03_020400_20230304T021515
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/05/S5P_OFFL_L2__NO2____20230305T085459_20230305T103629_27935_03_020400_20230307T011244
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/05/S5P_OFFL_L2__NO2____20230305T103629_20230305T121800_27936_03_020400_20230307T030702
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/08/S5P_OFFL_L2__NO2____20230308T093942_20230308T112112_27978_03_020400_20230310T015134
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/14/S5P_OFFL_L2__NO2____20230314T110908_20230314T125038_28064_03_020500_20230317T111227
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/08/S5P_OFFL_L2__NO2____20230308T112112_20230308T130243_27979_03_020400_20230310T033310
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/26/S5P_OFFL_L2__NO2____20230326T104536_20230326T122707_28234_03_020500_20230328T030428
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/26/S5P_OFFL_L2__NO2____20230326T090405_20230326T104536_28233_03_020500_20230328T011028
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/15/S5P_OFFL_L2__NO2____20230315T090841_20230315T105012_28077_03_020500_20230318T003027
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/19/S5P_OFFL_L2__NO2____20230319T093442_20230319T111612_28134_03_020500_20230321T014727
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/31/S5P_OFFL_L2__NO2____20230331T091113_20230331T105243_28304_03_020500_20230402T011756
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/31/S5P_OFFL_L2__NO2____20230331T105243_20230331T123414_28305_03_020500_20230402T031349
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/11/S5P_OFFL_L2__NO2____20230311T102425_20230311T120555_28021_03_020400_20230313T023618
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/04/S5P_OFFL_L2__NO2____20230304T091355_20230304T105525_27921_03_020400_20230306T013254
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/01/S5P_OFFL_L2__NO2____20230301T101042_20230301T115212_27879_03_020400_20230303T023224
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/28/S5P_OFFL_L2__NO2____20230328T114921_20230328T133052_28263_03_020500_20230330T042453
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/14/S5P_OFFL_L2__NO2____20230314T092737_20230314T110908_28063_03_020500_20230317T110733
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/28/S5P_OFFL_L2__NO2____20230328T100751_20230328T114921_28262_03_020500_20230330T023332
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/17/S5P_OFFL_L2__NO2____20230317T101226_20230317T115357_28106_03_020500_20230319T082226
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/17/S5P_OFFL_L2__NO2____20230317T083056_20230317T101226_28105_03_020500_20230319T062640
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/16/S5P_OFFL_L2__NO2____20230316T103118_20230316T121249_28092_03_020500_20230318T193655
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/27/S5P_OFFL_L2__NO2____20230327T084513_20230327T102644_28247_03_020500_20230329T004554
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/04/S5P_OFFL_L2__NO2____20230304T105525_20230304T123655_27922_03_020400_20230306T032707
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/12/S5P_OFFL_L2__NO2____20230312T082359_20230312T100529_28034_03_020500_20230315T154142
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/12/S5P_OFFL_L2__NO2____20230312T100529_20230312T114659_28035_03_020500_20230315T154412
/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/12/S5P_OFFL_L2__NO2____20230312T114659_20230312T132830_28036_03_020500_20230315T155238
S5P_OFFL_L2__NO2____20230329T113029_20230329T131159_28277_03_020500_20230331T035449.nc
S5P_OFFL_L2__NO2____20230329T094858_20230329T113029_28276_03_020500_20230331T021341.nc
S5P_OFFL_L2__NO2____20230310T104321_20230310T122451_28007_03_020400_20230312T025817.nc
S5P_OFFL_L2__NO2____20230330T093006_20230330T111136_28290_03_020500_20230401T015114.nc
S5P_OFFL_L2__NO2____20230307T114008_20230307T132138_27965_03_020400_20230309T040214.nc
S5P_OFFL_L2__NO2____20230309T110217_20230309T124347_27993_03_020400_20230311T031357.nc
S5P_OFFL_L2__NO2____20230320T091549_20230320T105720_28148_03_020500_20230322T012553.nc
S5P_OFFL_L2__NO2____20230313T112803_20230313T130934_28050_03_020500_20230316T030602.nc
S5P_OFFL_L2__NO2____20230302T113316_20230302T131447_27894_03_020400_20230304T041315.nc
S5P_OFFL_L2__NO2____20230302T095146_20230302T113316_27893_03_020400_20230304T021515.nc
S5P_OFFL_L2__NO2____20230305T085459_20230305T103629_27935_03_020400_20230307T011244.nc
S5P_OFFL_L2__NO2____20230305T103629_20230305T121800_27936_03_020400_20230307T030702.nc
S5P_OFFL_L2__NO2____20230308T093942_20230308T112112_27978_03_020400_20230310T015134.nc
S5P_OFFL_L2__NO2____20230314T110908_20230314T125038_28064_03_020500_20230317T111227.nc
S5P_OFFL_L2__NO2____20230308T112112_20230308T130243_27979_03_020400_20230310T033310.nc
S5P_OFFL_L2__NO2____20230326T104536_20230326T122707_28234_03_020500_20230328T030428.nc
S5P_OFFL_L2__NO2____20230326T090405_20230326T104536_28233_03_020500_20230328T011028.nc
S5P_OFFL_L2__NO2____20230315T090841_20230315T105012_28077_03_020500_20230318T003027.nc
S5P_OFFL_L2__NO2____20230319T093442_20230319T111612_28134_03_020500_20230321T014727.nc
S5P_OFFL_L2__NO2____20230331T091113_20230331T105243_28304_03_020500_20230402T011756.nc
S5P_OFFL_L2__NO2____20230331T105243_20230331T123414_28305_03_020500_20230402T031349.nc
S5P_OFFL_L2__NO2____20230311T102425_20230311T120555_28021_03_020400_20230313T023618.nc
S5P_OFFL_L2__NO2____20230304T091355_20230304T105525_27921_03_020400_20230306T013254.nc
S5P_OFFL_L2__NO2____20230301T101042_20230301T115212_27879_03_020400_20230303T023224.nc
S5P_OFFL_L2__NO2____20230328T114921_20230328T133052_28263_03_020500_20230330T042453.nc
S5P_OFFL_L2__NO2____20230314T092737_20230314T110908_28063_03_020500_20230317T110733.nc
S5P_OFFL_L2__NO2____20230328T100751_20230328T114921_28262_03_020500_20230330T023332.nc
S5P_OFFL_L2__NO2____20230317T101226_20230317T115357_28106_03_020500_20230319T082226.nc
S5P_OFFL_L2__NO2____20230317T083056_20230317T101226_28105_03_020500_20230319T062640.nc
S5P_OFFL_L2__NO2____20230316T103118_20230316T121249_28092_03_020500_20230318T193655.nc
S5P_OFFL_L2__NO2____20230327T084513_20230327T102644_28247_03_020500_20230329T004554.nc
S5P_OFFL_L2__NO2____20230304T105525_20230304T123655_27922_03_020400_20230306T032707.nc
S5P_OFFL_L2__NO2____20230312T082359_20230312T100529_28034_03_020500_20230315T154142.nc
S5P_OFFL_L2__NO2____20230312T100529_20230312T114659_28035_03_020500_20230315T154412.nc
S5P_OFFL_L2__NO2____20230312T114659_20230312T132830_28036_03_020500_20230315T155238.nc

Create a list of merged paths and names of each image

separator = '/'                                    #set separator between path and name
result_list = [name + separator + path for name,   #create a list of merged paths and names for each image
               path in zip(path_list, name_list)]
print(result_list)
['/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/29/S5P_OFFL_L2__NO2____20230329T113029_20230329T131159_28277_03_020500_20230331T035449/S5P_OFFL_L2__NO2____20230329T113029_20230329T131159_28277_03_020500_20230331T035449.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/29/S5P_OFFL_L2__NO2____20230329T094858_20230329T113029_28276_03_020500_20230331T021341/S5P_OFFL_L2__NO2____20230329T094858_20230329T113029_28276_03_020500_20230331T021341.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/10/S5P_OFFL_L2__NO2____20230310T104321_20230310T122451_28007_03_020400_20230312T025817/S5P_OFFL_L2__NO2____20230310T104321_20230310T122451_28007_03_020400_20230312T025817.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/30/S5P_OFFL_L2__NO2____20230330T093006_20230330T111136_28290_03_020500_20230401T015114/S5P_OFFL_L2__NO2____20230330T093006_20230330T111136_28290_03_020500_20230401T015114.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/07/S5P_OFFL_L2__NO2____20230307T114008_20230307T132138_27965_03_020400_20230309T040214/S5P_OFFL_L2__NO2____20230307T114008_20230307T132138_27965_03_020400_20230309T040214.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/09/S5P_OFFL_L2__NO2____20230309T110217_20230309T124347_27993_03_020400_20230311T031357/S5P_OFFL_L2__NO2____20230309T110217_20230309T124347_27993_03_020400_20230311T031357.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/20/S5P_OFFL_L2__NO2____20230320T091549_20230320T105720_28148_03_020500_20230322T012553/S5P_OFFL_L2__NO2____20230320T091549_20230320T105720_28148_03_020500_20230322T012553.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/13/S5P_OFFL_L2__NO2____20230313T112803_20230313T130934_28050_03_020500_20230316T030602/S5P_OFFL_L2__NO2____20230313T112803_20230313T130934_28050_03_020500_20230316T030602.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/02/S5P_OFFL_L2__NO2____20230302T113316_20230302T131447_27894_03_020400_20230304T041315/S5P_OFFL_L2__NO2____20230302T113316_20230302T131447_27894_03_020400_20230304T041315.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/02/S5P_OFFL_L2__NO2____20230302T095146_20230302T113316_27893_03_020400_20230304T021515/S5P_OFFL_L2__NO2____20230302T095146_20230302T113316_27893_03_020400_20230304T021515.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/05/S5P_OFFL_L2__NO2____20230305T085459_20230305T103629_27935_03_020400_20230307T011244/S5P_OFFL_L2__NO2____20230305T085459_20230305T103629_27935_03_020400_20230307T011244.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/05/S5P_OFFL_L2__NO2____20230305T103629_20230305T121800_27936_03_020400_20230307T030702/S5P_OFFL_L2__NO2____20230305T103629_20230305T121800_27936_03_020400_20230307T030702.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/08/S5P_OFFL_L2__NO2____20230308T093942_20230308T112112_27978_03_020400_20230310T015134/S5P_OFFL_L2__NO2____20230308T093942_20230308T112112_27978_03_020400_20230310T015134.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/14/S5P_OFFL_L2__NO2____20230314T110908_20230314T125038_28064_03_020500_20230317T111227/S5P_OFFL_L2__NO2____20230314T110908_20230314T125038_28064_03_020500_20230317T111227.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/08/S5P_OFFL_L2__NO2____20230308T112112_20230308T130243_27979_03_020400_20230310T033310/S5P_OFFL_L2__NO2____20230308T112112_20230308T130243_27979_03_020400_20230310T033310.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/26/S5P_OFFL_L2__NO2____20230326T104536_20230326T122707_28234_03_020500_20230328T030428/S5P_OFFL_L2__NO2____20230326T104536_20230326T122707_28234_03_020500_20230328T030428.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/26/S5P_OFFL_L2__NO2____20230326T090405_20230326T104536_28233_03_020500_20230328T011028/S5P_OFFL_L2__NO2____20230326T090405_20230326T104536_28233_03_020500_20230328T011028.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/15/S5P_OFFL_L2__NO2____20230315T090841_20230315T105012_28077_03_020500_20230318T003027/S5P_OFFL_L2__NO2____20230315T090841_20230315T105012_28077_03_020500_20230318T003027.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/19/S5P_OFFL_L2__NO2____20230319T093442_20230319T111612_28134_03_020500_20230321T014727/S5P_OFFL_L2__NO2____20230319T093442_20230319T111612_28134_03_020500_20230321T014727.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/31/S5P_OFFL_L2__NO2____20230331T091113_20230331T105243_28304_03_020500_20230402T011756/S5P_OFFL_L2__NO2____20230331T091113_20230331T105243_28304_03_020500_20230402T011756.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/31/S5P_OFFL_L2__NO2____20230331T105243_20230331T123414_28305_03_020500_20230402T031349/S5P_OFFL_L2__NO2____20230331T105243_20230331T123414_28305_03_020500_20230402T031349.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/11/S5P_OFFL_L2__NO2____20230311T102425_20230311T120555_28021_03_020400_20230313T023618/S5P_OFFL_L2__NO2____20230311T102425_20230311T120555_28021_03_020400_20230313T023618.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/04/S5P_OFFL_L2__NO2____20230304T091355_20230304T105525_27921_03_020400_20230306T013254/S5P_OFFL_L2__NO2____20230304T091355_20230304T105525_27921_03_020400_20230306T013254.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/01/S5P_OFFL_L2__NO2____20230301T101042_20230301T115212_27879_03_020400_20230303T023224/S5P_OFFL_L2__NO2____20230301T101042_20230301T115212_27879_03_020400_20230303T023224.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/28/S5P_OFFL_L2__NO2____20230328T114921_20230328T133052_28263_03_020500_20230330T042453/S5P_OFFL_L2__NO2____20230328T114921_20230328T133052_28263_03_020500_20230330T042453.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/14/S5P_OFFL_L2__NO2____20230314T092737_20230314T110908_28063_03_020500_20230317T110733/S5P_OFFL_L2__NO2____20230314T092737_20230314T110908_28063_03_020500_20230317T110733.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/28/S5P_OFFL_L2__NO2____20230328T100751_20230328T114921_28262_03_020500_20230330T023332/S5P_OFFL_L2__NO2____20230328T100751_20230328T114921_28262_03_020500_20230330T023332.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/17/S5P_OFFL_L2__NO2____20230317T101226_20230317T115357_28106_03_020500_20230319T082226/S5P_OFFL_L2__NO2____20230317T101226_20230317T115357_28106_03_020500_20230319T082226.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/17/S5P_OFFL_L2__NO2____20230317T083056_20230317T101226_28105_03_020500_20230319T062640/S5P_OFFL_L2__NO2____20230317T083056_20230317T101226_28105_03_020500_20230319T062640.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/16/S5P_OFFL_L2__NO2____20230316T103118_20230316T121249_28092_03_020500_20230318T193655/S5P_OFFL_L2__NO2____20230316T103118_20230316T121249_28092_03_020500_20230318T193655.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/27/S5P_OFFL_L2__NO2____20230327T084513_20230327T102644_28247_03_020500_20230329T004554/S5P_OFFL_L2__NO2____20230327T084513_20230327T102644_28247_03_020500_20230329T004554.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/04/S5P_OFFL_L2__NO2____20230304T105525_20230304T123655_27922_03_020400_20230306T032707/S5P_OFFL_L2__NO2____20230304T105525_20230304T123655_27922_03_020400_20230306T032707.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/12/S5P_OFFL_L2__NO2____20230312T082359_20230312T100529_28034_03_020500_20230315T154142/S5P_OFFL_L2__NO2____20230312T082359_20230312T100529_28034_03_020500_20230315T154142.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/12/S5P_OFFL_L2__NO2____20230312T100529_20230312T114659_28035_03_020500_20230315T154412/S5P_OFFL_L2__NO2____20230312T100529_20230312T114659_28035_03_020500_20230315T154412.nc', '/eodata/Sentinel-5P/TROPOMI/L2__NO2___/2023/03/12/S5P_OFFL_L2__NO2____20230312T114659_20230312T132830_28036_03_020500_20230315T155238/S5P_OFFL_L2__NO2____20230312T114659_20230312T132830_28036_03_020500_20230315T155238.nc']

Using HARP - Atmospheric Toolbox

Using HARP - bin_spatial

“bin_spatial(A,S,SR,B,W,SR)”

  • A - is the number of latitude edge points, calculates as follows: (N latitude of AoI (55) - S latitude of AOI (49) / C (0.01)) + 1

  • S - is the latitude offset at which to start the grid (S)

  • SR - is the spatial resolution expressed in degrees

  • B - is the number of longitude edge points, calculates as follows: (E longitude of AoI (25) - W longitude of AOI (14) / C (0.01)) + 1

  • W - is the longitude offset at which to start the grid (W)

  • SR - is the spatial resolution expressed in degrees

N = 55.00
S = 49.00
W = 14.00
E = 25.00

SR = 0.01

A = (N-S)/SR + 1
B = (E-W)/SR + 1

operations = ";".join([
    "tropospheric_NO2_column_number_density_validity>75",             #keep pixels wich qa_value > 0.75
    "derive(surface_wind_speed {time} [m/s])",                        #get surafe wind speed expressed in [m/s]
    "surface_wind_speed<5",                                           #keep pixels wich wind_spped < 5 [m/s]
    #keep variables defined below
    "keep(latitude_bounds,longitude_bounds,datetime_start,datetime_length,tropospheric_NO2_column_number_density, surface_wind_speed)",
    "derive(datetime_start {time} [days since 2000-01-01])",          #get start time of the acquisition
    "derive(datetime_stop {time}[days since 2000-01-01])",            #get end time of the acquisition
    "exclude(datetime_length)",                                       #exclude datetime lenght
    f"bin_spatial({int(A)},{int(S)},{float(SR)},{int(B)},{int(W)},{float(SR)})",                       #define bin spatial (details below)
    "derive(tropospheric_NO2_column_number_density [Pmolec/cm2])",    #convert the NO2 units to 10^15 molec/cm^2
    "derive(latitude {latitude})",                                    #get latitude
    "derive(longitude {longitude})",                                  #get longitude
    "count>0"
])

Operations for temporal averaging

reduce_operations=";".join([
    "squash(time, (latitude, longitude, latitude_bounds, longitude_bounds))",
    "bin()"
])

Create new image - average NO2 pollution over defined area in specific period

mean_no2 = harp.import_product(result_list, operations, reduce_operations=reduce_operations)

Import and write new created image as a netcdf named “mean_no2_2023_03.nc”

harp.export_product(mean_no2, 'mean_no2_2023_03.nc') #name of varibale, name of new created file with extension

Define variable, longitude, latitude and colormap which are going to be visualized

#select NO2 pollution
no2 = mean_no2.tropospheric_NO2_column_number_density.data
#select desription (name) of variable
no2_description = mean_no2.tropospheric_NO2_column_number_density.description
#select units of variable
no2_units = mean_no2.tropospheric_NO2_column_number_density.unit

#define cooridantes grid
gridlat = np.append(mean_no2.latitude_bounds.data[:,0], mean_no2.latitude_bounds.data[-1,1])
gridlon = np.append(mean_no2.longitude_bounds.data[:,0], mean_no2.longitude_bounds.data[-1,1])

#define legend
colortable = cm.roma_r  #colors
vmin = 0                #min value
vmax = 10               #max value

Create a visualization

boundaries=[14, 25, 49, 55.0]                       #define boundaries (W, E, S, N)

fig = plt.figure(figsize=(10,10))                   #size of the figure
bmap=cimgt.Stamen(style='toner-lite')               #set background
ax = plt.axes(projection=bmap.crs)
ax.set_extent(boundaries,crs = ccrs.PlateCarree())

zoom = 10
ax.add_image(bmap, zoom)

img = plt.pcolormesh(gridlon, gridlat,
                     no2[0,:,:], vmin=vmin, vmax=vmax,
                     cmap=colortable,
                     transform=ccrs.PlateCarree(),alpha = 0.55)
ax.coastlines()

cbar = fig.colorbar(img, ax=ax,orientation='horizontal', fraction=0.04, pad=0.1)
cbar.set_label(f'{no2_description} [{no2_units}]')
cbar.ax.tick_params(labelsize=14)

plt.show()
../_images/image200.png

PART #2 - Conversion and zonal statistics

This part aims to present how to convert Sentinel-5P netCDF data into GeoTiff image and calculate zonal statistics, via CREODIAS platfrom

In this part you will learn how to:

  • convert netCDF into GeoTiff

  • calculate mean tropospheric column of NO2 (NO2 TVCD) for each Poland’s district and save results as a new-created shapefile

  • plot netCDF - NO2 TVCD values, for area of interest

  • plot mean NO2 TVCD for each Poland’s district as a choropleth

  • export new-created choropleth to a .jpg

  • put mean NO2 TVCD for each Poland’s district to a data frame and export it as a .csv

Datasets which are going to be used:

Import libraries
#Import libraries

import pandas as pd
import geopandas as gpd
import xarray as xr
import numpy as np
import os
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import rasterstats
import rasterio
import rioxarray
from rasterio.plot import show
from rasterio.transform import from_origin

Export image as a GeoTiff and show results

#open the netcdf file
r1 = xr.open_dataset('mean_no2_2023_03.nc')

#extract the tropospheric_NO2_column_number_density variable
no2 = r1['tropospheric_NO2_column_number_density'].values[0,:,:]
#reverse order of the values within the array
no2 = no2[::-1]

#get the latitude and longitude coordinates
y = r1['latitude'].values
x = r1['longitude'].values

#reverse the latitude and longitude coordinates
y = y[::-1]
x = x[::-1]

#convert the DataArray to a rioxarray DataArray
da = xr.DataArray(no2,
                  coords={'y': y, 'x': x},
                  dims=('y', 'x'),
                  attrs={'crs': 'EPSG:4326'})

#save the rioxarray DataArray to a GeoTIFF file with CRS 4326
da.rio.to_raster('mean_no2_2023_03.tif', crs='EPSG:4326')

#open new-crtead raster
src = rasterio.open("mean_no2_2023_03.tif")

#plot the new-created raster
show(src)
../_images/image201.png

Calculate mean NO2 TVCD for each Poland’s district and save results as a new-created shapefile

# -*- coding: utf-8 -*-
#read shapefile of districts in Poland
shapefile_path = 'poland_pow.shp'
poland_pow = gpd.read_file(shapefile_path)

#define the affine transformation (pixel size and coordinates of the top-left corner of the raster)
affine = rasterio.transform.from_bounds(min(x), min(y),
                                        max(x), max(y),
                                        len(da.x), len(da.y))

#loop over the polygons in the poland_pow GeoDataFrame and calculate the mean NO2 TVCD for each polygon
mean_no2 = []
for i, row in poland_pow.iterrows():
    #extract the geometry of the polygon
    geometry = row.geometry
    #calculate the mean NO2 TVCD for the polygon
    mean_no2p = rasterstats.zonal_stats(geometry,
                                        da.values,
                                        affine=affine,
                                        nodata=-999,
                                        stats=['mean'],
                                        nan='ignore')[0]['mean']
    #add the mean NO2 TVCD value to the poland_pow GeoDataFrame properties
    poland_pow.loc[i, 'mean_no2'] = mean_no2p

#encode names of districts to UTF-8
poland_pow['JPT_NAZWA_'] = poland_pow['JPT_NAZWA_'].apply(lambda x: x.encode('latin-1').decode('utf-8'))
#save the GeoDataFrame with the mean NO2 TVCD values to a new shapefile
poland_pow.to_file('nuts_2023_03.shp', encoding='utf-8')

Plot netCDF - NO2 TVCD values, for area of interest - Poland

#open the netCDF file
nc_file = xr.open_dataset('mean_no2_2023_03.nc')

#read the tropospheric_NO2_column_number_density variable
no2_tvcd = nc_file['tropospheric_NO2_column_number_density'].values[0,:,]

#get the latitude and longitude coordinates from nc_file
lat = nc_file['latitude'].values
lon = nc_file['longitude'].values

#create the figure and Basemap instance
fig = plt.figure(figsize=(20,20))

#create a basemap with the desired projection and latitude/longitude bounds - Poland
m = Basemap(projection='cyl', llcrnrlat=49, urcrnrlat=55,
            llcrnrlon=13, urcrnrlon=26, resolution='c')

#plot NO2 TVCD on basemap
x, y = m(lon, lat)
m.pcolormesh(x, y, no2_tvcd, cmap='RdYlGn_r', vmin=0, vmax=10)

#add coordinates grid
m.drawparallels(np.arange(49, 55, 1), labels=[1,0,0,0], fontsize=20)
m.drawmeridians(np.arange(14, 25, 1), labels=[0,0,0,1], fontsize=20)

#read shapefile of districts in Poland
shapefile_path = 'poland_pow.shp'               #define path
poland_nuts = gpd.read_file(shapefile_path)     #read file containing districts of Poland

#plot districts on map
poland_nuts.plot(ax=plt.gca(), facecolor='none', edgecolor='black', linewidth=1)

#add colorbar
cbar = plt.colorbar(fraction=0.03, pad=0.02)
cbar.ax.set_ylabel('NO$_2$ (Pmolec cm$^{-2}$)', fontsize=24)
cbar.ax.tick_params(labelsize=16)

#plot title
plt.title('Mean tropospheric NO$_2$ column number density in March 2023', fontsize=24)

#show plot
plt.show()
../_images/image202.png

Plot mean NO2 TVCD for each Poland’s district - create choropleth

#read shapefile of NUTS3 regions in Poland
shapefile_path = 'nuts_2023_03.shp'          #define path
poland_nuts = gpd.read_file(shapefile_path)  #read file containing districts of Poland

#create a figure
fig, ax1 = plt.subplots(ncols=1, figsize=(12, 6))

#plot choropleth based on mean NO2 TVCD iver Poland
poland_nuts.plot(column='mean_no2',                  #mean_no2 is a column contains mean NO2 TVCD for each polygon
                 cmap='RdYlGn_r', ax=ax1)            #cmap - color ramp from green to red
ax1.set_xlim(14, 25)                                 #W-E extent of Poland
ax1.set_ylim(49, 55)                                 #S-N extent of Poland
ax1.set_title('Mean NO$_2$ TVCD in March 2023 over Poland')    #title of plot

#add a colorbar reffering to the the NO2 TVCD
vmin, vmax = [0, 10]                           #min and max value on colorbar
cbar = plt.cm.ScalarMappable(cmap='RdYlGn_r', norm=plt.Normalize(vmin=vmin, vmax=vmax))
plt.colorbar(cbar, ax=ax1, shrink=0.5, aspect=10).set_label('NO$_2$ TVCD [Pmolec/cm2]', fontsize=12)

#show the plot
plt.show()
../_images/image203.png

Export image of mean NO2 TVCD for each Poland’s district to a .jpg

#save a plot as a .jpg
fig.savefig('POLAND_NO2_2023_03.jpg', dpi=300)

Put mean NO2 TVCD for each Poland’s district to a data frame

#get the mean NO2 TVCD values for each polygon in the Poland shapefile and round to two decimal places
no2_means = round(poland_nuts['mean_no2'],2)

#create a Pandas DataFrame with the mean NO2 TVCD values and their corresponding polygon names
NO2_df = pd.DataFrame({'Polygon': poland_nuts['JPT_NAZWA_'], 'Mean NO2': no2_means})

#sort the Poland's districts in descending order by the mean NO2 values values
NO2_df = NO2_df.sort_values(by='Mean NO2', ascending=False)

#print the DataFrame
print(NO2_df)
                         Polygon  Mean NO2
143              powiat Katowice     10.76
139           powiat Ruda Śląska     10.70
356        powiat Świętochłowice     10.66
142               powiat Chorzów     10.64
85   powiat Siemianowice Śląskie     10.29
..                           ...       ...
225            powiat chodzieski      0.52
19              powiat obornicki      0.51
329             powiat chojnicki      0.48
174             powiat złotowski      0.42
328               powiat drawski      0.36

[380 rows x 2 columns]

Export mean NO2 TVCD for each Poland’s district to a .csv file

NO2_df.to_csv('NO2_df.csv')