prop/test/microwaveprop/weather/grib2/simple_packing_test.exs
Graham McIntire b077d4facb
Add batch GRIB2 extraction for multi-point grid queries
Add extract_values/3 to SimplePacking and ComplexPacking for batch
index extraction from a single GRIB2 message. Add extract_grid/2 to
Extractor which takes a list of {lat, lon} points and returns all
variable values for each point, skipping points outside the grid.

This enables extracting weather data for many grid points from a
single HRRR download instead of re-parsing per point.
2026-03-30 16:50:03 -05:00

274 lines
7.6 KiB
Elixir

defmodule Microwaveprop.Weather.Grib2.SimplePackingTest do
use ExUnit.Case, async: true
alias Microwaveprop.Weather.Grib2.SimplePacking
describe "extract_value/3" do
test "basic 16-bit unpacking" do
# value = (R + X * 2^E) * 10^(-D)
# R=0.0, E=0, D=0, N=16 → value = X
params = %{
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 16,
num_data_points: 4
}
# 4 values: 100, 200, 300, 400 as 16-bit big-endian
data = <<100::16-big, 200::16-big, 300::16-big, 400::16-big>>
assert {:ok, 100.0} = SimplePacking.extract_value(params, data, 0)
assert {:ok, 200.0} = SimplePacking.extract_value(params, data, 1)
assert {:ok, 300.0} = SimplePacking.extract_value(params, data, 2)
assert {:ok, 400.0} = SimplePacking.extract_value(params, data, 3)
end
test "applies reference value" do
# R=273.15, E=0, D=0 → value = 273.15 + X
params = %{
reference_value: 273.15,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 16,
num_data_points: 2
}
data = <<10::16-big, 20::16-big>>
assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
assert_in_delta val, 283.15, 0.001
end
test "applies binary scale factor" do
# R=0, E=2, D=0 → value = X * 2^2 = X * 4
params = %{
reference_value: 0.0,
binary_scale: 2,
decimal_scale: 0,
bits_per_value: 16,
num_data_points: 1
}
data = <<25::16-big>>
assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
assert_in_delta val, 100.0, 0.001
end
test "applies decimal scale factor" do
# R=0, E=0, D=2 → value = X * 10^(-2) = X / 100
params = %{
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 2,
bits_per_value: 16,
num_data_points: 1
}
data = <<15_000::16-big>>
assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
assert_in_delta val, 150.0, 0.001
end
test "combined R, E, D" do
# value = (R + X * 2^E) * 10^(-D)
# R=100.0, E=1, D=1 → (100 + X * 2) / 10
params = %{
reference_value: 100.0,
binary_scale: 1,
decimal_scale: 1,
bits_per_value: 16,
num_data_points: 1
}
data = <<50::16-big>>
assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
# (100 + 50 * 2) / 10 = 200 / 10 = 20.0
assert_in_delta val, 20.0, 0.001
end
test "12-bit non-byte-aligned extraction" do
# 3 values of 12 bits each = 36 bits = 4.5 bytes
# Values: 0xABC=2748, 0x123=291, 0xDEF=3567
params = %{
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 12,
num_data_points: 3
}
# Pack 3 12-bit values: 0xABC, 0x123, 0xDEF
# Binary: 1010_1011_1100 | 0001_0010_0011 | 1101_1110_1111
# As bytes: AB C1 23 DE F0 (with 4 bits padding)
data = <<0xAB, 0xC1, 0x23, 0xDE, 0xF0>>
assert {:ok, val0} = SimplePacking.extract_value(params, data, 0)
assert_in_delta val0, 2748.0, 0.001
assert {:ok, val1} = SimplePacking.extract_value(params, data, 1)
assert_in_delta val1, 291.0, 0.001
assert {:ok, val2} = SimplePacking.extract_value(params, data, 2)
assert_in_delta val2, 3567.0, 0.001
end
test "zero bits_per_value returns reference value" do
params = %{
reference_value: 293.5,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 0,
num_data_points: 10
}
# With 0 bits, data is empty
data = <<>>
assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
assert_in_delta val, 293.5, 0.001
assert {:ok, val} = SimplePacking.extract_value(params, data, 5)
assert_in_delta val, 293.5, 0.001
end
test "negative binary scale factor" do
# E=-1 → 2^(-1) = 0.5
params = %{
reference_value: 0.0,
binary_scale: -1,
decimal_scale: 0,
bits_per_value: 16,
num_data_points: 1
}
data = <<200::16-big>>
assert {:ok, val} = SimplePacking.extract_value(params, data, 0)
assert_in_delta val, 100.0, 0.001
end
test "index out of range returns error" do
params = %{
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 16,
num_data_points: 2
}
data = <<100::16-big, 200::16-big>>
assert {:error, :index_out_of_range} = SimplePacking.extract_value(params, data, 5)
end
end
describe "extract_values/3" do
test "extracts multiple values in a single batch call" do
params = %{
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 16,
num_data_points: 4
}
data = <<100::16-big, 200::16-big, 300::16-big, 400::16-big>>
assert {:ok, result} = SimplePacking.extract_values(params, data, [0, 2, 3])
assert map_size(result) == 3
assert_in_delta result[0], 100.0, 0.001
assert_in_delta result[2], 300.0, 0.001
assert_in_delta result[3], 400.0, 0.001
end
test "skips out-of-range indices" do
params = %{
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 16,
num_data_points: 2
}
data = <<100::16-big, 200::16-big>>
assert {:ok, result} = SimplePacking.extract_values(params, data, [0, 5, -1, 1])
assert map_size(result) == 2
assert_in_delta result[0], 100.0, 0.001
assert_in_delta result[1], 200.0, 0.001
end
test "returns all same value when bits_per_value is 0" do
params = %{
reference_value: 293.5,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 0,
num_data_points: 10
}
assert {:ok, result} = SimplePacking.extract_values(params, <<>>, [0, 3, 7])
assert map_size(result) == 3
assert_in_delta result[0], 293.5, 0.001
assert_in_delta result[3], 293.5, 0.001
assert_in_delta result[7], 293.5, 0.001
end
test "applies scaling correctly to batch" do
# value = (R + X * 2^E) * 10^(-D)
# R=100.0, E=1, D=1 -> (100 + X*2) / 10
params = %{
reference_value: 100.0,
binary_scale: 1,
decimal_scale: 1,
bits_per_value: 16,
num_data_points: 2
}
data = <<50::16-big, 75::16-big>>
assert {:ok, result} = SimplePacking.extract_values(params, data, [0, 1])
# (100 + 50*2)/10 = 20.0
assert_in_delta result[0], 20.0, 0.001
# (100 + 75*2)/10 = 25.0
assert_in_delta result[1], 25.0, 0.001
end
test "handles empty indices list" do
params = %{
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 16,
num_data_points: 2
}
data = <<100::16-big, 200::16-big>>
assert {:ok, result} = SimplePacking.extract_values(params, data, [])
assert result == %{}
end
test "handles non-byte-aligned 12-bit batch extraction" do
params = %{
reference_value: 0.0,
binary_scale: 0,
decimal_scale: 0,
bits_per_value: 12,
num_data_points: 3
}
# Pack 3 12-bit values: 0xABC=2748, 0x123=291, 0xDEF=3567
data = <<0xAB, 0xC1, 0x23, 0xDE, 0xF0>>
assert {:ok, result} = SimplePacking.extract_values(params, data, [0, 1, 2])
assert_in_delta result[0], 2748.0, 0.001
assert_in_delta result[1], 291.0, 0.001
assert_in_delta result[2], 3567.0, 0.001
end
end
end