Skip to content

Instantly share code, notes, and snippets.

@chtitux
Created February 2, 2026 11:59
Show Gist options
  • Select an option

  • Save chtitux/67b65d4bfc274141f370e357a2e4d63a to your computer and use it in GitHub Desktop.

Select an option

Save chtitux/67b65d4bfc274141f370e357a2e4d63a to your computer and use it in GitHub Desktop.

Decoding Météo-France BUFR Files

This document provides all the information needed to decode BUFR (Binary Universal Form for the Representation of meteorological data) files produced by Météo-France, particularly for radar products.

Table of Contents

  1. Overview
  2. BUFR Format Basics
  3. Météo-France Specifics
  4. Required Tools and Libraries
  5. Local Descriptor Tables
  6. Radar Products
  7. Decoding Process
  8. Data Interpretation
  9. Code Examples
  10. References and Links

Overview

Météo-France distributes meteorological data in BUFR format, a WMO (World Meteorological Organization) standard binary format. However, Météo-France uses local descriptor tables that are not included in standard BUFR libraries like ecCodes, requiring specific tools or table configurations for proper decoding.

Key Challenges

  • Standard BUFR decoders (ecCodes, pybufrkit) fail with errors like: "unknown descriptor 001192" or "unknown element descriptor 049209"
  • Météo-France uses originating centre 85 with local table version 14
  • Local descriptors in the range 0 XX 192 to 0 XX 255 are Météo-France specific

BUFR Format Basics

BUFR is a binary, table-driven format consisting of 6 sections:

Section Name Content
0 Indicator "BUFR" magic bytes, total message length
1 Identification Originating centre, data category, date/time
2 Optional Local use (optional)
3 Data Description Sequence of descriptors defining data structure
4 Data Encoded binary data
5 End "7777" end marker

Descriptor Format

Descriptors are 16-bit values encoded as F XX YYY:

  • F (2 bits): Type (0=element, 1=replication, 2=operator, 3=sequence)
  • X (6 bits): Class/category
  • Y (8 bits): Entry within class

Example: Descriptor 0 30 4 = Element descriptor, class 30 (Image), entry 4 (Pixel value)


Météo-France Specifics

Originating Centre

  • Centre ID: 85 (Toulouse, Météo-France)
  • Sub-centre: 0
  • Local table version: 14

Data Categories

Category Subcategory Description
6 1 Radar data - Image product
6 2 Radar data - Precipitation

Compression

  • BUFR files are typically gzip compressed (.gz extension)
  • Decompress with gunzip before decoding

Required Tools and Libraries

Option 1: BUFR OPERA (Recommended)

The BUFR OPERA tool from EUMETNET includes Météo-France local tables and is specifically designed for European radar data.

Download: http://eumetnet.eu/wp-content/uploads/2017/04/bufr_opera_mf.zip

Installation:

# Download and extract
curl -L -o bufr_opera_mf.zip "http://eumetnet.eu/wp-content/uploads/2017/04/bufr_opera_mf.zip"
unzip bufr_opera_mf.zip
cd bufr_opera_mf/bufr-opera-mf-1.22

# Compile the decoder (requires zlib)
gcc -o decbufr tests/decbufr.c lib/*.c -I lib -lm -lz

# Run decoder
./decbufr /path/to/bufr_file > decoded_output.txt

Key files in BUFR OPERA:

  • tables/localtabb_85_14.csv - Météo-France local element descriptors
  • tables/localtabd_85_14.csv - Météo-France local sequence descriptors
  • lib/ - BUFR decoding library source code

Option 2: ecCodes with Custom Tables

ecCodes can work if you add Météo-France local tables:

# Install ecCodes
pip install eccodes

# You need to add local tables to:
# $ECCODES_DEFINITION_PATH/bufr/tables/0/local/85/0/14/

Note: This approach requires manually creating table files in ecCodes format, which is more complex than using BUFR OPERA.

Option 3: Custom Parser

For full control, implement a custom BUFR parser using the local table definitions provided below.


Local Descriptor Tables

Météo-France Local Element Descriptors (Table B)

Source: bufr_opera_mf/bufr-opera-mf-1.22/tables/localtabb_85_14.csv

Key local descriptors used in radar products:

Descriptor Name Unit Scale Reference Bits
0 01 192 Type de produit Code table 0 0 8
0 01 193 Sous-type de produit Code table 0 0 8
0 04 192 Temps de parcours de la grille s 0 0 12
0 05 195 Latitude de reference ° 5 -9000000 25
0 06 198 Longitude du meridien parallele a l'axe des Y ° 5 -18000000 26
0 21 192 Code OPERA pour la quantite radar Code table 0 0 8
0 25 192 Indicateur d'utilisation du PVR Code table 0 0 2
0 29 192 Type de projection Code table 0 0 4
0 30 192 Nombre de valeurs possibles de pixel Numeric 0 0 16
0 31 192 Facteur super elargi de repetition differe Numeric 0 0 24
0 48 192 Identification du produit composite CCITT IA5 0 0 80
0 49 192 ...
0 49 209 Code pour l'unite de mesure de la lame d'eau Code table 0 0 4
0 49 210 Numero de PVR Numeric 0 0 8

Rainfall Unit Codes (Descriptor 0 49 209)

Code Unit Description
0 mm Millimeters
1 1/10 mm Tenths of millimeters
2 1/100 mm Hundredths of millimeters (centièmes)
3 1/1000 mm Thousandths of millimeters

Projection Type Codes (Descriptor 0 29 192)

Code Projection
0 Polar stereographic
1 Lambert conformal
2 Mercator
3 Geographic (lat/lon)

Radar Products

LAME_D_EAU (Rainfall Accumulation)

Météo-France provides rainfall accumulation products at various resolutions.

API Endpoint:

https://public-api.meteofrance.fr/public/DPRadar/v1/mosaiques/LAME_D_EAU/{resolution}

Available Resolutions:

  • 250m - Metropolitan France only
  • 1000m - All territories including overseas (Réunion, Antilles, etc.)

Example API Call:

curl -X GET "https://public-api.meteofrance.fr/public/DPRadar/v1/mosaiques/LAME_D_EAU/1000m?bbox=-22,-16,50,57" \
     -H "accept: application/octet-stream" \
     -H "apikey: YOUR_API_KEY" \
     --output radar.bufr.gz

Grid Specifications by Territory

Territory Grid Size (pixels) Resolution Origin (lat, lon)
Metropolitan France 1536×1536 1 km Variable
La Réunion 1150×1000 1 km -16°, 51°
Antilles Variable 1 km Variable

Radar Stations (La Réunion)

Station ID Name Latitude Longitude
61978 Colorado -20.911794 55.421942
61979 Piton Villers -21.190493 55.573419

Decoding Process

Step 1: Download and Decompress

# Download BUFR file (gzip compressed)
curl -X GET "https://public-api.meteofrance.fr/public/DPRadar/v1/mosaiques/LAME_D_EAU/1000m?bbox=-22,-16,50,57" \
     -H "apikey: YOUR_API_KEY" \
     --output radar.bufr.gz

# Decompress
gunzip radar.bufr.gz

Step 2: Decode with BUFR OPERA

./decbufr radar.bufr > decoded.txt

Step 3: Parse Decoded Output

The decoded output contains lines in the format:

 0 30   4     123.0000000            Pixel value

Key fields to extract:

  • Grid dimensions: Descriptors 0 30 21 (columns) and 0 30 22 (rows)
  • Origin coordinates: Descriptors 0 05 1 (latitude) and 0 06 1 (longitude)
  • Pixel values: Descriptor 0 30 4
  • Rainfall unit: Descriptor 0 49 209

Step 4: Reshape Data

Pixel values are stored in row-major order (left to right, top to bottom):

import numpy as np

# After extracting pixel_values list from decoded output
n_cols = 1150  # from descriptor 0 30 21
n_rows = 1000  # from descriptor 0 30 22

data = np.array(pixel_values).reshape((n_rows, n_cols))

Data Interpretation

Missing Values

  • Value 65535 indicates missing data (no radar coverage or invalid measurement)
  • In numpy: data[data == 65535] = np.nan

Unit Conversion

For LAME_D_EAU products, values are typically in centièmes de mm (1/100 mm):

# Convert to millimeters
rainfall_mm = data / 100.0

Z-R Relationship

Météo-France uses the Marshall-Palmer relationship for reflectivity-to-rainfall conversion:

Z = 200 × R^1.6

Where:

  • Z = Radar reflectivity (mm⁶/m³)
  • R = Rain rate (mm/h)

Coordinate System

The grid uses a Lambert conformal or geographic projection:

# Grid parameters
lat_origin = -16.0  # Top-left corner latitude
lon_origin = 51.0   # Top-left corner longitude
resolution_km = 1.0  # 1 km per pixel

# Calculate geographic coordinates for pixel (x, y)
# Note: At ~20°S, 1° longitude ≈ 104 km
km_per_deg_lon = 111.0 * cos(radians(20))  # ~104 km
km_per_deg_lat = 111.0

pixel_lon = lon_origin + (x * resolution_km / km_per_deg_lon)
pixel_lat = lat_origin - (y * resolution_km / km_per_deg_lat)

Code Examples

Complete Python Decoder

#!/usr/bin/env python3
"""
Météo-France BUFR Radar Decoder

Prerequisites:
- BUFR OPERA tool compiled (decbufr binary)
- numpy, matplotlib (for visualization)
"""

import subprocess
import numpy as np
import re
import gzip
from pathlib import Path

def decompress_bufr(input_path: str, output_path: str) -> str:
    """Decompress gzipped BUFR file."""
    with gzip.open(input_path, 'rb') as f_in:
        with open(output_path, 'wb') as f_out:
            f_out.write(f_in.read())
    return output_path

def decode_bufr(bufr_path: str, decbufr_path: str) -> str:
    """Run BUFR OPERA decoder and return output."""
    result = subprocess.run(
        [decbufr_path, bufr_path],
        capture_output=True,
        text=True
    )
    return result.stdout

def parse_decoded_bufr(decoded_text: str) -> dict:
    """Parse decoded BUFR text output."""
    data = {
        'n_cols': None,
        'n_rows': None,
        'lat_origin': None,
        'lon_origin': None,
        'rainfall_unit': None,
        'pixel_values': []
    }

    for line in decoded_text.split('\n'):
        # Grid dimensions
        if '0 30  21' in line and 'Number of pixels per row' in line:
            match = re.search(r'([\d.]+)', line)
            if match:
                data['n_cols'] = int(float(match.group(1)))

        elif '0 30  22' in line and 'Number of pixels per column' in line:
            match = re.search(r'([\d.]+)', line)
            if match:
                data['n_rows'] = int(float(match.group(1)))

        # Origin coordinates (first occurrence)
        elif '0  5   1' in line and 'Latitude' in line and data['lat_origin'] is None:
            match = re.search(r'^\s*([-\d.]+)', line)
            if match:
                data['lat_origin'] = float(match.group(1))

        elif '0  6   1' in line and 'Longitude' in line and data['lon_origin'] is None:
            match = re.search(r'^\s*([-\d.]+)', line)
            if match:
                data['lon_origin'] = float(match.group(1))

        # Rainfall unit
        elif '0 49 209' in line:
            match = re.search(r'^\s*([\d.]+)', line)
            if match:
                data['rainfall_unit'] = int(float(match.group(1)))

        # Pixel values
        elif '0 30   4' in line and 'Pixel value' in line:
            match = re.search(r'^\s*([\d.]+)', line)
            if match:
                data['pixel_values'].append(float(match.group(1)))

    return data

def convert_to_rainfall(pixel_values: np.ndarray, unit_code: int) -> np.ndarray:
    """Convert pixel values to millimeters."""
    rainfall = pixel_values.copy().astype(np.float32)

    # Mark missing values
    rainfall[pixel_values == 65535] = np.nan

    # Convert based on unit code
    unit_divisors = {0: 1, 1: 10, 2: 100, 3: 1000}
    divisor = unit_divisors.get(unit_code, 100)  # Default to centièmes

    valid_mask = ~np.isnan(rainfall)
    rainfall[valid_mask] = rainfall[valid_mask] / divisor

    return rainfall

def main():
    # Configuration
    bufr_gz_path = '/tmp/radar.bufr.gz'
    bufr_path = '/tmp/radar.bufr'
    decbufr_path = '/path/to/bufr_opera/decbufr'

    # Step 1: Decompress
    decompress_bufr(bufr_gz_path, bufr_path)

    # Step 2: Decode
    decoded_text = decode_bufr(bufr_path, decbufr_path)

    # Step 3: Parse
    data = parse_decoded_bufr(decoded_text)

    # Step 4: Convert to numpy array
    pixel_array = np.array(data['pixel_values'])
    pixel_array = pixel_array.reshape((data['n_rows'], data['n_cols']))

    # Step 5: Convert to rainfall in mm
    rainfall_mm = convert_to_rainfall(pixel_array, data['rainfall_unit'])

    # Output statistics
    valid_rain = rainfall_mm[~np.isnan(rainfall_mm)]
    print(f"Grid size: {data['n_cols']} x {data['n_rows']}")
    print(f"Origin: ({data['lat_origin']}°, {data['lon_origin']}°)")
    print(f"Valid pixels: {len(valid_rain)}")
    print(f"Max rainfall: {np.nanmax(rainfall_mm):.2f} mm")
    print(f"Mean rainfall (where > 0): {valid_rain[valid_rain > 0].mean():.2f} mm")

    return rainfall_mm, data

if __name__ == '__main__':
    main()

Visualization Example

import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np

def visualize_radar(rainfall_mm: np.ndarray, data: dict, output_path: str):
    """Create rainfall visualization."""

    # Rainfall colormap
    colors = ['#FFFFFF', '#00FFFF', '#00BFFF', '#0080FF', '#0040FF',
              '#00FF00', '#80FF00', '#FFFF00', '#FFC000', '#FF8000',
              '#FF0000', '#C00000', '#800080']
    levels = [0, 0.1, 0.5, 1, 2, 3, 5, 7, 10, 15, 20, 30, 50]
    cmap = mcolors.ListedColormap(colors)
    norm = mcolors.BoundaryNorm(levels, cmap.N)

    # Calculate extent in geographic coordinates
    km_per_deg_lon = 111.0 * np.cos(np.radians(20))
    km_per_deg_lat = 111.0

    extent = [
        data['lon_origin'],
        data['lon_origin'] + data['n_cols'] / km_per_deg_lon,
        data['lat_origin'] - data['n_rows'] / km_per_deg_lat,
        data['lat_origin']
    ]

    # Create figure
    fig, ax = plt.subplots(figsize=(12, 10))
    im = ax.imshow(rainfall_mm, cmap=cmap, norm=norm, extent=extent)

    plt.colorbar(im, label='Rainfall (mm / 5 min)')
    ax.set_xlabel('Longitude (°E)')
    ax.set_ylabel('Latitude (°)')
    ax.set_title('Météo-France Radar - Rainfall Accumulation')

    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()

References and Links

Official Météo-France Documentation

  1. Technical Description of Radar Public Data

  2. Multipolarized Data Description (PAM)

Tools and Libraries

  1. BUFR OPERA Tool (EUMETNET)

  2. ecCodes (ECMWF)

Standards

  1. WMO BUFR Documentation

  2. OPERA (EUMETNET Radar Network)

API Documentation

  1. Météo-France Public API Portal

Troubleshooting

Common Errors

Error: unknown element descriptor 001192

  • Cause: Standard BUFR library missing Météo-France local tables
  • Solution: Use BUFR OPERA tool or add local tables to ecCodes

Error: undefined reference to 'inflate'

  • Cause: Missing zlib during compilation
  • Solution: Add -lz flag when compiling BUFR OPERA

Error: Empty or corrupted data

  • Cause: File still gzip compressed
  • Solution: Decompress with gunzip before decoding

Validation

To verify correct decoding:

  1. Check grid dimensions match expected values (e.g., 1150×1000 for La Réunion)
  2. Verify total pixel count = rows × columns
  3. Confirm missing value (65535) count is reasonable for the region
  4. Maximum rainfall values should be realistic (typically < 50mm/5min)

Changelog

  • 2024-01-XX: Initial documentation created based on reverse engineering and official Météo-France documentation

This document enables any developer or AI agent to successfully decode Météo-France BUFR radar files by providing the necessary context, tools, code examples, and references.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment