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.
- Overview
- BUFR Format Basics
- Météo-France Specifics
- Required Tools and Libraries
- Local Descriptor Tables
- Radar Products
- Decoding Process
- Data Interpretation
- Code Examples
- References and Links
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.
- 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 192to0 XX 255are Météo-France specific
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 |
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)
- Centre ID: 85 (Toulouse, Météo-France)
- Sub-centre: 0
- Local table version: 14
| Category | Subcategory | Description |
|---|---|---|
| 6 | 1 | Radar data - Image product |
| 6 | 2 | Radar data - Precipitation |
- BUFR files are typically gzip compressed (
.gzextension) - Decompress with
gunzipbefore decoding
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.txtKey files in BUFR OPERA:
tables/localtabb_85_14.csv- Météo-France local element descriptorstables/localtabd_85_14.csv- Météo-France local sequence descriptorslib/- BUFR decoding library source code
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.
For full control, implement a custom BUFR parser using the local table definitions provided below.
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 |
| 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 |
| Code | Projection |
|---|---|
| 0 | Polar stereographic |
| 1 | Lambert conformal |
| 2 | Mercator |
| 3 | Geographic (lat/lon) |
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 only1000m- 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| 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 |
| Station ID | Name | Latitude | Longitude |
|---|---|---|---|
| 61978 | Colorado | -20.911794 | 55.421942 |
| 61979 | Piton Villers | -21.190493 | 55.573419 |
# 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./decbufr radar.bufr > decoded.txtThe 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) and0 30 22(rows) - Origin coordinates: Descriptors
0 05 1(latitude) and0 06 1(longitude) - Pixel values: Descriptor
0 30 4 - Rainfall unit: Descriptor
0 49 209
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))- Value 65535 indicates missing data (no radar coverage or invalid measurement)
- In numpy:
data[data == 65535] = np.nan
For LAME_D_EAU products, values are typically in centièmes de mm (1/100 mm):
# Convert to millimeters
rainfall_mm = data / 100.0Mé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)
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)#!/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()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()-
Technical Description of Radar Public Data
- URL: https://donneespubliques.meteofrance.fr/client/document/descriptiftechnique_radar_donneespubliques_v1-2_20250318_404.pdf
- Content: Grid specifications, product descriptions, BUFR format details
-
Multipolarized Data Description (PAM)
- URL: https://donneespubliques.meteofrance.fr/client/document/descriptif_donnees_multipolarisees_pam_269.pdf
- Content: Encoding tables, BUFR OPERA tool reference, data formats
-
BUFR OPERA Tool (EUMETNET)
- URL: http://eumetnet.eu/wp-content/uploads/2017/04/bufr_opera_mf.zip
- Content: BUFR decoder with Météo-France local tables
-
ecCodes (ECMWF)
- URL: https://confluence.ecmwf.int/display/ECC
- Note: Requires custom tables for Météo-France data
-
WMO BUFR Documentation
- URL: https://community.wmo.int/en/activity-areas/wis/bufr
- Content: Official BUFR format specification
-
OPERA (EUMETNET Radar Network)
- URL: https://www.eumetnet.eu/activities/observations-programme/current-activities/opera/
- Content: European radar data standards
- Météo-France Public API Portal
- URL: https://portail-api.meteofrance.fr/
- Content: API documentation, authentication, endpoints
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
-lzflag when compiling BUFR OPERA
Error: Empty or corrupted data
- Cause: File still gzip compressed
- Solution: Decompress with
gunzipbefore decoding
To verify correct decoding:
- Check grid dimensions match expected values (e.g., 1150×1000 for La Réunion)
- Verify total pixel count = rows × columns
- Confirm missing value (65535) count is reasonable for the region
- Maximum rainfall values should be realistic (typically < 50mm/5min)
- 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.