How to identify fields in European air-quality forecast grib files

The CAMS European air quality forecast can be retrieved from the Atmosphere Data Store (ADS) in either GRIB or NetCDF format. When requesting multiple models and/or variables in GRIB format however, it may not be clear which fields are which. The model that a field belongs to is identified by its “centre” and “subCentre” keys, and the variable by its “parameterNumber” and “constituentType” keys.


The tables below show the mapping. Below that is some example Python code showing how this information can be used with ecCodes to identify the model and variable of each field in a GRIB file.

Model centre subCentre
CHIMERE 85 200
DEHM 85 203
EMEP 88 0
Ensemble median 85 2
EURAD-IM 85 201
GEM-AQ 85 204
LOTOS-EUROS 99 0
MATCH 82 98
MOCAGE 85 1
SILAM 86 0
MINNI 85 205
MONARCH 85 206

Variable parameterNumber constituentType
Alder pollen 59 62100
Ammonia 0 9
Birch pollen 59 62101
Carbon monoxide 0 4
Dust 0 62001
Formaldehyde 0 7
Glyoxal 0 10038
Grass pollen 59 62300
Mugwort pollen 59 62201
Nitrogen dioxide 0 5
Nitrogen monoxide 0 11
Non-methane VOCs 0 60013
Olive pollen 59 64002
Ozone 0 0
Particulate matter < 10um (PM10) 0 40008
Particulate matter < 2.5um (PM2.5) 0 40009
Peroxyacyl nitrates (PANs) 0 60018
PM2.5, total organic matter only 0 62010
PM10, sea salt (dry) only 0 62008
PM10, wildfires only 0 62096
Ragweed pollen 59 62200
Residential elementary carbon 0 62094
Secondary inorganic aerosol (SIA) 0 62099
Sulphur dioxide 0 8
Total elementary carbon 0 62095
from eccodes import (
    codes_grib_new_from_file, codes_get, codes_release, codes_write)

# Dictionary matching ADS API key/value pairs to grib key/value pairs
definitions = {
    'variable': {
        'ammonia': {
            'parameterNumber': 0,   # Mass density (kg/m3)
            'constituentType': 9},
        'carbon_monoxide': {
            'parameterNumber': 0,
            'constituentType': 4},
        'dust': {
            'parameterNumber': 0,
            'constituentType': 62001},
        'formaldehyde': {
            'parameterNumber': 0,
            'constituentType': 7},
        'glyoxal': {
            'parameterNumber': 0,
            'constituentType': 10038}, 
        'non_methane_vocs': {
            'parameterNumber': 0,
            'constituentType': 60013},
        'nitrogen_monoxide': {
            'parameterNumber': 0,
            'constituentType': 11},
        'nitrogen_dioxide': {
            'parameterNumber': 0,
            'constituentType': 5},
        'ozone': {
            'parameterNumber': 0,
            'constituentType': 0},
        'peroxyacyl_nitrates': {
            'parameterNumber': 0,
            'constituentType': 60018},
        'particulate_matter_10um': {
            'parameterNumber': 0,
            'constituentType': 40008},
        'particulate_matter_2.5um': {
            'parameterNumber': 0,
            'constituentType': 40009},
        'pm2.5_anthropogenic_wood_burning_carbon': {
            'parameterNumber': 0,
            'constituentType': 62098},
        'pm2.5_anthropogenic_fossil_fuel_carbon': {
            'parameterNumber': 0,
            'constituentType': 62097},
	    'pm2.5_total_organic_matter': {
            'parameterNumber': 0,
            'constituentType': 62010},
        'pm10_sea_salt_dry': {
            'parameterNumber': 0,
            'constituentType': 62008},
        'pm10_wildfires': {
            'parameterNumber': 0,
            'constituentType': 62096},
        'residential_elementary_carbon': {
            'parameterNumber': 0,
            'constituentType': 62094},
        'secondary_inorganic_aerosol': {
            'parameterNumber': 0,
            'constituentType': 62099},
        'sulphur_dioxide': {
            'parameterNumber': 0,
            'constituentType': 8},
        'total_elementary_carbon': {
            'parameterNumber': 0,
            'constituentType': 62095},
        'alder_pollen': {
            'parameterNumber': 59,     # Number Concentration (1/m3)
            'constituentType': 62100},
        'birch_pollen': {
            'parameterNumber': 59,
            'constituentType': 62101},
        'grass_pollen': {
            'parameterNumber': 59,
            'constituentType': 62300},
        'mugwort_pollen': {
            'parameterNumber': 59,
            'constituentType': 62201},
        'olive_pollen': {
            'parameterNumber': 59,
            'constituentType': 64002},
        'ragweed_pollen': {
            'parameterNumber': 59,
            'constituentType': 62200}
    },
    'model': {
        'chimere': {
            'centre': 85,      # France
            'subCentre': 200},
        'dehm': {
            'centre': 85,
            'subCentre': 203},
        'emep': {
            'centre': 88,      # Oslo
            'subCentre': 0},
        'ensemble': {
            'centre': 85,      # France
            'subCentre': 2},
        'euradim': {
            'centre': 85,      # France
            'subCentre': 201},
        'gemaq': {
            'centre': 85,
            'subCentre': 204},
        'lotos': {
            'centre': 99,
            'subCentre': 0},
        'match': {
            'centre': 82,      # Norrkoping
            'subCentre': 98},
        'mocage': {
            'centre': 85,      # Meteo France
            'subCentre': 1},
        'silam': {
            'centre': 86,      # Helsinki
            'subCentre': 0},
        'minni': {
            'centre': 85,
            'subCentre': 205},
        'monarch': {
            'centre': 85,
            'subCentre': 206}
    }
}


def identify_field(msg):
    """Return a dict identifying the input grib message"""

    field = {}
    for api_key, value_to_grib in definitions.items():
        for api_value, grib_defn in value_to_grib.items():

            # Does this message match the key/value pairs in grib_defn?
            match = True
            for grib_key, grib_value in grib_defn.items():
                msg_value = codes_get(msg, grib_key,
                                      ktype=type(grib_value))
                if msg_value != grib_value:
                    match = False
                    break
            if match:
                field[api_key] = api_value

        if api_key not in field:
            raise Exception('Unrecognised field')

    return field


def main(grib_file, output_filename_template=None):
    """Read all fields in the GRIB file, identify the model and variable of
       each, and optionally write them to output files, split by model or
       variable or both.

       Any instance of "<model>" or "<variable>" in output_filename_template
       will be replaced by the associated model or variable string for the
       field, e.g. output_filename_template='<model>.<variable>.grib'
    """

    fields_written = {}

    # Loop over messages in a grib file, identifying all fields
    count = 0
    with open(grib_file) as fin:
        while True:
            msg = codes_grib_new_from_file(fin)
            if msg is None:
                break
            count += 1

            try:
                field = identify_field(msg)
                print('Field ' + str(count) + ' is: ' + repr(field))

                # Write the field to an output file?
                if output_filename_template:
                    name = output_filename_template
                    for k, v in field.items():
                        name = name.replace(f'<{k}>', v)
                    if name not in fields_written:
                        fields_written[name] = 0
                        mode = 'w'
                    else:
                        mode = 'a'
                    with open(name, mode + 'b') as fout:
                        codes_write(msg, fout)
                    fields_written[name] += 1

            finally:
                codes_release(msg)

    for name, n in fields_written.items():
        print(f'Wrote {n} fields to {name}')


if __name__ == '__main__':
    main('mydata.grib',
         output_filename_template='<model>.<variable>.grib')