Eliminates the external wgrib2 C tool dependency that blocked HRRR processing. Implements Lambert Conformal projection, simple packing (Template 5.0), complex packing with spatial differencing (Template 5.3), and GRIB2 section parsing — enough to extract point values from HRRR grid data using only Elixir.
265 lines
7.1 KiB
Elixir
265 lines
7.1 KiB
Elixir
defmodule Microwaveprop.Weather.Grib2.SectionTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Weather.Grib2.Section
|
|
|
|
describe "parse_message/1" do
|
|
test "parses a synthetic GRIB2 message" do
|
|
msg = build_synthetic_grib2()
|
|
assert {:ok, parsed} = Section.parse_message(msg)
|
|
|
|
# Grid params from Section 3
|
|
assert parsed.grid_params.nx == 1799
|
|
assert parsed.grid_params.ny == 1059
|
|
assert_in_delta parsed.grid_params.la1, 21.138123, 0.000001
|
|
assert_in_delta parsed.grid_params.lo1, 237.280472, 0.000001
|
|
assert_in_delta parsed.grid_params.dx, 3000.0, 0.1
|
|
assert_in_delta parsed.grid_params.dy, 3000.0, 0.1
|
|
assert_in_delta parsed.grid_params.latin1, 38.5, 0.000001
|
|
assert_in_delta parsed.grid_params.latin2, 38.5, 0.000001
|
|
assert parsed.grid_params.scan_mode == 64
|
|
|
|
# Product from Section 4
|
|
assert parsed.product.var == "TMP"
|
|
assert parsed.product.level == "2 m above ground"
|
|
|
|
# Packing params from Section 5
|
|
assert parsed.packing_params.num_data_points == 100
|
|
assert parsed.packing_params.bits_per_value == 16
|
|
|
|
# Data from Section 7
|
|
assert is_binary(parsed.data)
|
|
|
|
# Bitmap indicator
|
|
assert parsed.bitmap == :none
|
|
end
|
|
|
|
test "rejects non-GRIB2 binary" do
|
|
assert {:error, _} = Section.parse_message(<<"NOT_GRIB", 0::64-big>>)
|
|
end
|
|
|
|
test "rejects GRIB edition 1" do
|
|
indicator = <<"GRIB", 0::16, 0::8, 1::8, 100::64-big>>
|
|
assert {:error, _} = Section.parse_message(indicator)
|
|
end
|
|
end
|
|
|
|
describe "product identification" do
|
|
test "identifies all supported variables" do
|
|
# discipline 0 (meteorological)
|
|
assert Section.identify_var(0, 0, 0) == "TMP"
|
|
assert Section.identify_var(0, 0, 6) == "DPT"
|
|
assert Section.identify_var(0, 3, 0) == "PRES"
|
|
assert Section.identify_var(0, 3, 5) == "HGT"
|
|
assert Section.identify_var(0, 3, 18) == "HPBL"
|
|
assert Section.identify_var(0, 1, 3) == "PWAT"
|
|
end
|
|
|
|
test "returns category:number for unknown variables" do
|
|
assert Section.identify_var(0, 99, 99) == "99:99"
|
|
end
|
|
end
|
|
|
|
describe "level identification" do
|
|
test "surface" do
|
|
assert Section.identify_level(1, 0) == "surface"
|
|
end
|
|
|
|
test "pressure level in mb" do
|
|
# Stored in Pa, displayed in mb
|
|
assert Section.identify_level(100, 100_000) == "1000 mb"
|
|
assert Section.identify_level(100, 85_000) == "850 mb"
|
|
end
|
|
|
|
test "meters above ground" do
|
|
assert Section.identify_level(103, 2) == "2 m above ground"
|
|
assert Section.identify_level(103, 10) == "10 m above ground"
|
|
end
|
|
|
|
test "entire atmosphere" do
|
|
assert Section.identify_level(200, 0) ==
|
|
"entire atmosphere (considered as a single layer)"
|
|
end
|
|
|
|
test "unknown level type" do
|
|
assert Section.identify_level(255, 0) == "unknown:255:0"
|
|
end
|
|
end
|
|
|
|
describe "sign_magnitude_16/1" do
|
|
test "positive value" do
|
|
assert Section.sign_magnitude_16(<<0::1, 5::15>>) == 5
|
|
end
|
|
|
|
test "negative value" do
|
|
assert Section.sign_magnitude_16(<<1::1, 5::15>>) == -5
|
|
end
|
|
|
|
test "zero" do
|
|
assert Section.sign_magnitude_16(<<0::16>>) == 0
|
|
end
|
|
end
|
|
|
|
# --- Helpers ---
|
|
|
|
defp build_synthetic_grib2 do
|
|
# Section 1 (Identification) - minimal
|
|
sec1_body = <<
|
|
# Originating center, subcenter, etc.
|
|
0::16-big,
|
|
0::16-big,
|
|
# Master/local tables
|
|
2::8,
|
|
1::8,
|
|
# Significance of reference time
|
|
1::8,
|
|
# Year, month, day, hour, minute, second
|
|
2026::16-big,
|
|
3::8,
|
|
28::8,
|
|
18::8,
|
|
0::8,
|
|
0::8,
|
|
# Production status, type
|
|
0::8,
|
|
1::8
|
|
>>
|
|
|
|
sec1 = section_wrap(1, sec1_body)
|
|
|
|
# Section 3 (Grid Definition) - Template 3.30 (Lambert Conformal)
|
|
sec3_body = <<
|
|
# Source of grid definition
|
|
0::8,
|
|
# Number of data points
|
|
1_905_141::32-big,
|
|
# Number of octets for optional list
|
|
0::8,
|
|
# Interpretation
|
|
0::8,
|
|
# Grid Definition Template Number = 30 (Lambert Conformal)
|
|
30::16-big,
|
|
# Shape of earth (6 = spherical, R=6371229)
|
|
6::8,
|
|
# Scale factor/value of radius (not used for shape 6)
|
|
0::8,
|
|
0::32-big,
|
|
# Scale factor/value of major axis
|
|
0::8,
|
|
0::32-big,
|
|
# Scale factor/value of minor axis
|
|
0::8,
|
|
0::32-big,
|
|
# Nx, Ny
|
|
1799::32-big,
|
|
1059::32-big,
|
|
# La1 in microdegrees
|
|
21_138_123::32-big,
|
|
# Lo1 in microdegrees
|
|
237_280_472::32-big,
|
|
# Resolution and component flags
|
|
0b00111000::8,
|
|
# LaD in microdegrees
|
|
38_500_000::32-big,
|
|
# LoV in microdegrees
|
|
262_500_000::32-big,
|
|
# Dx in millimetres
|
|
3_000_000::32-big,
|
|
# Dy in millimetres
|
|
3_000_000::32-big,
|
|
# Projection centre flag
|
|
0::8,
|
|
# Scanning mode
|
|
64::8,
|
|
# Latin1 in microdegrees
|
|
38_500_000::32-big,
|
|
# Latin2 in microdegrees
|
|
38_500_000::32-big,
|
|
# Latitude/longitude of southern pole
|
|
0::32-big,
|
|
0::32-big
|
|
>>
|
|
|
|
sec3 = section_wrap(3, sec3_body)
|
|
|
|
# Section 4 (Product Definition) - Template 4.0
|
|
sec4_body = <<
|
|
# Number of coordinate values
|
|
0::16-big,
|
|
# Product Definition Template Number = 0
|
|
0::16-big,
|
|
# Parameter category (0 = Temperature)
|
|
0::8,
|
|
# Parameter number (0 = Temperature)
|
|
0::8,
|
|
# Type of generating process
|
|
2::8,
|
|
# Background + forecast generating process
|
|
0::8,
|
|
0::8,
|
|
# Hours/minutes of observational data cutoff
|
|
0::16-big,
|
|
0::8,
|
|
# Indicator of unit of time range (1=hour)
|
|
1::8,
|
|
# Forecast time
|
|
0::32-big,
|
|
# Type of first fixed surface (103 = above ground)
|
|
103::8,
|
|
# Scale factor and scaled value of first fixed surface
|
|
0::8,
|
|
2::32-big,
|
|
# Type of second fixed surface (255 = missing)
|
|
255::8,
|
|
0::8,
|
|
0::32-big
|
|
>>
|
|
|
|
sec4 = section_wrap(4, sec4_body)
|
|
|
|
# Section 5 (Data Representation) - Template 5.0 (Simple packing)
|
|
# Reference value as IEEE 754 float32
|
|
ref_val = <<0.0::float-32-big>>
|
|
|
|
sec5_body = <<
|
|
# Number of data points
|
|
100::32-big,
|
|
# Data Representation Template Number = 0
|
|
0::16-big,
|
|
# Reference value (IEEE 754)
|
|
ref_val::binary,
|
|
# Binary scale factor (sign-magnitude 16-bit)
|
|
0::16-big,
|
|
# Decimal scale factor (sign-magnitude 16-bit)
|
|
0::16-big,
|
|
# Number of bits per value
|
|
16::8
|
|
>>
|
|
|
|
sec5 = section_wrap(5, sec5_body)
|
|
|
|
# Section 6 (Bitmap) - no bitmap
|
|
sec6_body = <<255::8>>
|
|
sec6 = section_wrap(6, sec6_body)
|
|
|
|
# Section 7 (Data) - 100 values of 16 bits = 200 bytes
|
|
data_bytes = :binary.copy(<<42::16-big>>, 100)
|
|
sec7 = section_wrap(7, data_bytes)
|
|
|
|
# Section 8 (End)
|
|
sec8 = "7777"
|
|
|
|
body = sec1 <> sec3 <> sec4 <> sec5 <> sec6 <> sec7 <> sec8
|
|
total_length = 16 + byte_size(body)
|
|
|
|
# Section 0 (Indicator)
|
|
sec0 = <<"GRIB", 0::16, 0::8, 2::8, total_length::64-big>>
|
|
|
|
sec0 <> body
|
|
end
|
|
|
|
defp section_wrap(section_number, body) do
|
|
length = 5 + byte_size(body)
|
|
<<length::32-big, section_number::8, body::binary>>
|
|
end
|
|
end
|