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.
274 lines
7.6 KiB
Elixir
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
|