Skip to main content

grib_core/
parameter.rs

1//! WMO parameter tables (Code Table 4.2) and local table overlays for GRIB2.
2//!
3//! Maps (discipline, parameter_category, parameter_number) to human-readable names.
4
5use crate::metadata::{Parameter, ParameterTableSource};
6
7/// A user-authored or built-in GRIB2 local table entry.
8///
9/// `subcenter_id` and `local_table_version` may be `None` to match any value
10/// for a center, but correctness-sensitive built-in entries should prefer exact
11/// local table versions when known.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct LocalParameterEntry<'a> {
14    pub center_id: u16,
15    pub subcenter_id: Option<u16>,
16    pub local_table_version: Option<u8>,
17    pub discipline: u8,
18    pub category: u8,
19    pub number: u8,
20    pub short_name: &'a str,
21    pub description: &'a str,
22}
23
24impl LocalParameterEntry<'_> {
25    fn matches(
26        &self,
27        discipline: u8,
28        category: u8,
29        number: u8,
30        center_id: u16,
31        subcenter_id: u16,
32        local_table_version: u8,
33    ) -> bool {
34        self.center_id == center_id
35            && self
36                .subcenter_id
37                .map_or(true, |expected| expected == subcenter_id)
38            && self
39                .local_table_version
40                .map_or(true, |expected| expected == local_table_version)
41            && self.discipline == discipline
42            && self.category == category
43            && self.number == number
44    }
45}
46
47/// Built-in local table entries shipped with the crate.
48pub const BUILTIN_LOCAL_PARAMETERS: &[LocalParameterEntry<'static>] = &[LocalParameterEntry {
49    center_id: 7,
50    subcenter_id: None,
51    local_table_version: Some(1),
52    discipline: 0,
53    category: 16,
54    number: 196,
55    short_name: "REFC",
56    description: "Maximum/Composite radar reflectivity",
57}];
58
59/// Look up a GRIB1 parameter short name.
60pub fn grib1_parameter_name(table_version: u8, number: u8) -> &'static str {
61    match (table_version, number) {
62        (_, 1) => "PRES",
63        (_, 2) => "PRMSL",
64        (_, 7) => "HGT",
65        (_, 11) => "TMP",
66        (_, 17) => "DPT",
67        (_, 33) => "UGRD",
68        (_, 34) => "VGRD",
69        (_, 39) => "VVEL",
70        (_, 52) => "RH",
71        (_, 54) => "PWAT",
72        (_, 61) => "APCP",
73        (_, 71) => "TCDC",
74        _ => "unknown",
75    }
76}
77
78/// Look up a GRIB1 parameter description.
79pub fn grib1_parameter_description(table_version: u8, number: u8) -> &'static str {
80    match (table_version, number) {
81        (_, 1) => "Pressure",
82        (_, 2) => "Pressure reduced to mean sea level",
83        (_, 7) => "Geopotential height",
84        (_, 11) => "Temperature",
85        (_, 17) => "Dew point temperature",
86        (_, 33) => "U-component of wind",
87        (_, 34) => "V-component of wind",
88        (_, 39) => "Vertical velocity",
89        (_, 52) => "Relative humidity",
90        (_, 54) => "Precipitable water",
91        (_, 61) => "Total precipitation",
92        (_, 71) => "Total cloud cover",
93        _ => "Unknown parameter",
94    }
95}
96
97/// Look up a parameter short name from WMO code tables.
98pub fn parameter_name(discipline: u8, category: u8, number: u8) -> &'static str {
99    wmo_parameter_definition(discipline, category, number)
100        .map(|(short_name, _description)| short_name)
101        .unwrap_or("unknown")
102}
103
104/// Look up a human-readable parameter description from WMO code tables.
105pub fn parameter_description(discipline: u8, category: u8, number: u8) -> &'static str {
106    wmo_parameter_definition(discipline, category, number)
107        .map(|(_short_name, description)| description)
108        .unwrap_or("Unknown parameter")
109}
110
111/// Look up a GRIB2 parameter using WMO tables plus built-in local tables.
112pub fn lookup_parameter(
113    discipline: u8,
114    category: u8,
115    number: u8,
116    center_id: u16,
117    subcenter_id: u16,
118    local_table_version: u8,
119) -> Parameter {
120    lookup_parameter_with_local_entries(
121        discipline,
122        category,
123        number,
124        center_id,
125        subcenter_id,
126        local_table_version,
127        &[],
128    )
129}
130
131/// Look up a GRIB2 parameter using WMO tables plus user-provided local entries.
132///
133/// The WMO table is always checked first for standard parameters. User-provided
134/// local entries are checked before built-in local entries, allowing callers to
135/// add or override center-specific local-use definitions without changing the
136/// global WMO table.
137pub fn lookup_parameter_with_local_entries(
138    discipline: u8,
139    category: u8,
140    number: u8,
141    center_id: u16,
142    subcenter_id: u16,
143    local_table_version: u8,
144    local_entries: &[LocalParameterEntry<'_>],
145) -> Parameter {
146    if let Some((short_name, description)) = wmo_parameter_definition(discipline, category, number)
147    {
148        return Parameter::new_grib2_with_source(
149            discipline,
150            category,
151            number,
152            short_name,
153            description,
154            ParameterTableSource::Wmo,
155        );
156    }
157
158    if is_local_parameter_code(category) || is_local_parameter_code(number) {
159        if local_table_version != 0 {
160            if let Some(entry) = local_entries
161                .iter()
162                .chain(BUILTIN_LOCAL_PARAMETERS.iter())
163                .find(|entry| {
164                    entry.matches(
165                        discipline,
166                        category,
167                        number,
168                        center_id,
169                        subcenter_id,
170                        local_table_version,
171                    )
172                })
173            {
174                return Parameter::new_grib2_with_source(
175                    discipline,
176                    category,
177                    number,
178                    entry.short_name.to_owned(),
179                    entry.description.to_owned(),
180                    ParameterTableSource::Local {
181                        center_id,
182                        subcenter_id,
183                        local_table_version,
184                    },
185                );
186            }
187        }
188
189        return Parameter::new_grib2_with_source(
190            discipline,
191            category,
192            number,
193            "unknown",
194            "Unknown parameter",
195            ParameterTableSource::UnknownLocal {
196                center_id,
197                subcenter_id,
198                local_table_version,
199            },
200        );
201    }
202
203    Parameter::new_grib2_with_source(
204        discipline,
205        category,
206        number,
207        "unknown",
208        "Unknown parameter",
209        ParameterTableSource::Unknown,
210    )
211}
212
213fn is_local_parameter_code(code: u8) -> bool {
214    (192..=254).contains(&code)
215}
216
217fn wmo_parameter_definition(
218    discipline: u8,
219    category: u8,
220    number: u8,
221) -> Option<(&'static str, &'static str)> {
222    match (discipline, category, number) {
223        // Discipline 0: Meteorological products
224        // Category 0: Temperature
225        (0, 0, 0) => Some(("TMP", "Temperature")),
226        (0, 0, 1) => Some(("VTMP", "Virtual temperature")),
227        (0, 0, 2) => Some(("POT", "Potential temperature")),
228        (0, 0, 4) => Some(("TMAX", "Maximum temperature")),
229        (0, 0, 5) => Some(("TMIN", "Minimum temperature")),
230        (0, 0, 6) => Some(("DPT", "Dew point temperature")),
231        // Category 1: Moisture
232        (0, 1, 0) => Some(("SPFH", "Specific humidity")),
233        (0, 1, 1) => Some(("RH", "Relative humidity")),
234        (0, 1, 3) => Some(("PWAT", "Precipitable water")),
235        (0, 1, 8) => Some(("APCP", "Total precipitation")),
236        // Category 2: Momentum
237        (0, 2, 0) => Some(("WDIR", "Wind direction")),
238        (0, 2, 1) => Some(("WIND", "Wind speed")),
239        (0, 2, 2) => Some(("UGRD", "U-component of wind")),
240        (0, 2, 3) => Some(("VGRD", "V-component of wind")),
241        (0, 2, 22) => Some(("GUST", "Wind gust")),
242        // Category 3: Mass
243        (0, 3, 0) => Some(("PRES", "Pressure")),
244        (0, 3, 1) => Some(("PRMSL", "Pressure reduced to MSL")),
245        (0, 3, 5) => Some(("HGT", "Geopotential height")),
246        // Category 4: Short-wave radiation
247        (0, 4, 7) => Some(("DSWRF", "Downward short-wave radiation flux")),
248        // Category 5: Long-wave radiation
249        (0, 5, 3) => Some(("DLWRF", "Downward long-wave radiation flux")),
250        // Category 6: Cloud
251        (0, 6, 1) => Some(("TCDC", "Total cloud cover")),
252        // Category 7: Thermodynamic stability
253        (0, 7, 6) => Some(("CAPE", "Convective available potential energy")),
254        (0, 7, 7) => Some(("CIN", "Convective inhibition")),
255        // Category 16: Forecast radar imagery
256        (0, 16, 5) => Some(("REFC", "Composite reflectivity")),
257        // Category 19: Physical atmospheric properties
258        (0, 19, 1) => Some(("ALBDO", "Albedo")),
259
260        // Discipline 10: Oceanographic products
261        // Category 0: Waves
262        (10, 0, 3) => Some((
263            "HTSGW",
264            "Significant height of combined wind waves and swell",
265        )),
266        (10, 0, 4) => Some(("WVDIR", "Direction of wind waves")),
267        (10, 0, 5) => Some(("WVPER", "Mean period of wind waves")),
268        // Category 3: Surface properties
269        (10, 3, 0) => Some(("WTMP", "Water temperature")),
270
271        _ => None,
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn known_parameters() {
281        assert_eq!(grib1_parameter_name(2, 11), "TMP");
282        assert_eq!(grib1_parameter_name(2, 33), "UGRD");
283        assert_eq!(parameter_name(0, 0, 0), "TMP");
284        assert_eq!(parameter_name(0, 2, 2), "UGRD");
285        assert_eq!(parameter_name(0, 3, 5), "HGT");
286        assert_eq!(parameter_name(0, 16, 5), "REFC");
287        assert_eq!(parameter_name(0, 19, 1), "ALBDO");
288        assert_eq!(parameter_name(10, 0, 3), "HTSGW");
289        assert_eq!(parameter_description(0, 0, 2), "Potential temperature");
290        assert_eq!(parameter_description(0, 16, 5), "Composite reflectivity");
291        assert_eq!(parameter_description(0, 19, 1), "Albedo");
292        assert_eq!(parameter_description(10, 3, 0), "Water temperature");
293    }
294
295    #[test]
296    fn unknown_parameter() {
297        assert_eq!(parameter_name(255, 255, 255), "unknown");
298        assert_eq!(parameter_name(0, 16, 196), "unknown");
299        assert_eq!(parameter_description(0, 16, 196), "Unknown parameter");
300    }
301
302    #[test]
303    fn ncep_refc_uses_local_table_context() {
304        let parameter = lookup_parameter(0, 16, 196, 7, 0, 1);
305
306        assert_eq!(parameter.short_name, "REFC");
307        assert_eq!(
308            parameter.description,
309            "Maximum/Composite radar reflectivity"
310        );
311        assert_eq!(
312            parameter.source,
313            ParameterTableSource::Local {
314                center_id: 7,
315                subcenter_id: 0,
316                local_table_version: 1,
317            }
318        );
319    }
320
321    #[test]
322    fn unknown_local_parameter_remains_local_unknown() {
323        let parameter = lookup_parameter(0, 16, 196, 8, 0, 1);
324
325        assert_eq!(parameter.short_name, "unknown");
326        assert_eq!(parameter.description, "Unknown parameter");
327        assert_eq!(
328            parameter.source,
329            ParameterTableSource::UnknownLocal {
330                center_id: 8,
331                subcenter_id: 0,
332                local_table_version: 1,
333            }
334        );
335    }
336
337    #[test]
338    fn local_table_version_must_match_local_entry() {
339        let parameter = lookup_parameter(0, 16, 196, 7, 0, 2);
340
341        assert_eq!(parameter.short_name, "unknown");
342        assert_eq!(parameter.description, "Unknown parameter");
343        assert_eq!(
344            parameter.source,
345            ParameterTableSource::UnknownLocal {
346                center_id: 7,
347                subcenter_id: 0,
348                local_table_version: 2,
349            }
350        );
351    }
352
353    #[test]
354    fn user_local_entries_resolve_local_use_codes() {
355        let entries = [LocalParameterEntry {
356            center_id: 42,
357            subcenter_id: Some(5),
358            local_table_version: Some(3),
359            discipline: 0,
360            category: 192,
361            number: 1,
362            short_name: "XFOO",
363            description: "Example local parameter",
364        }];
365
366        let parameter = lookup_parameter_with_local_entries(0, 192, 1, 42, 5, 3, &entries);
367
368        assert_eq!(parameter.short_name, "XFOO");
369        assert_eq!(parameter.description, "Example local parameter");
370        assert_eq!(
371            parameter.source,
372            ParameterTableSource::Local {
373                center_id: 42,
374                subcenter_id: 5,
375                local_table_version: 3,
376            }
377        );
378    }
379
380    #[test]
381    fn wmo_parameters_win_over_local_entries() {
382        let entries = [LocalParameterEntry {
383            center_id: 7,
384            subcenter_id: None,
385            local_table_version: Some(1),
386            discipline: 0,
387            category: 0,
388            number: 0,
389            short_name: "BADTMP",
390            description: "Bad local temperature",
391        }];
392
393        let parameter = lookup_parameter_with_local_entries(0, 0, 0, 7, 0, 1, &entries);
394
395        assert_eq!(parameter.short_name, "TMP");
396        assert_eq!(parameter.description, "Temperature");
397        assert_eq!(parameter.source, ParameterTableSource::Wmo);
398    }
399}