Add Wgrib2 module that shells out to wgrib2 binary for fast GRIB2 grid extraction using -lola (nearest-neighbor to regular lat-lon grid). Falls back to pure-Elixir decoder if wgrib2 is not installed. Also: parallel GRIB2 range downloads, merge adjacent byte ranges, skip corrupt messages instead of failing, pressure fetch is optional.
190 lines
5.7 KiB
Elixir
190 lines
5.7 KiB
Elixir
defmodule Microwaveprop.Weather.Grib2.Extractor do
|
|
@moduledoc false
|
|
|
|
alias Microwaveprop.Weather.Grib2.ComplexPacking
|
|
alias Microwaveprop.Weather.Grib2.LambertConformal
|
|
alias Microwaveprop.Weather.Grib2.Section
|
|
alias Microwaveprop.Weather.Grib2.SimplePacking
|
|
|
|
@doc """
|
|
Extract weather values from a GRIB2 binary blob at the given lat/lon.
|
|
|
|
Returns `{:ok, %{"VAR:LEVEL" => float}}` or `{:error, term}`.
|
|
"""
|
|
def extract_points(binary, lat, lon) do
|
|
messages = split_messages(binary)
|
|
|
|
Enum.reduce(messages, {:ok, %{}}, fn
|
|
msg, {:ok, acc} ->
|
|
case extract_single(msg, lat, lon) do
|
|
{:ok, key, value} -> {:ok, Map.put(acc, key, value)}
|
|
{:error, :outside_grid} -> {:error, :outside_grid}
|
|
{:error, _reason} -> {:ok, acc}
|
|
end
|
|
|
|
_msg, error ->
|
|
error
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Extract weather values from a GRIB2 binary blob for multiple lat/lon points.
|
|
|
|
Takes a binary and a list of `{lat, lon}` tuples.
|
|
Returns `{:ok, %{{lat, lon} => %{"VAR:LEVEL" => float}}}` or `{:error, term}`.
|
|
"""
|
|
def extract_grid(binary, points) do
|
|
messages = split_messages(binary)
|
|
|
|
# Pre-compute grid indices once from the first message's grid params.
|
|
# All messages in the same GRIB2 file share the same grid.
|
|
grid_indices =
|
|
case messages do
|
|
[first_msg | _] ->
|
|
case Section.parse_message(first_msg) do
|
|
{:ok, %{grid_params: grid}} -> precompute_indices(grid, points)
|
|
_ -> []
|
|
end
|
|
|
|
[] ->
|
|
[]
|
|
end
|
|
|
|
if grid_indices == [] do
|
|
{:ok, %{}}
|
|
else
|
|
result =
|
|
Enum.reduce(messages, {:ok, init_grid(points)}, fn msg, {:ok, acc} ->
|
|
case extract_grid_with_indices(msg, grid_indices) do
|
|
{:ok, key, values} ->
|
|
merged =
|
|
Enum.reduce(values, acc, fn {point, value}, grid ->
|
|
Map.update!(grid, point, &Map.put(&1, key, value))
|
|
end)
|
|
|
|
{:ok, merged}
|
|
|
|
{:error, _reason} ->
|
|
{:ok, acc}
|
|
end
|
|
end)
|
|
|
|
case result do
|
|
{:ok, grid} ->
|
|
{:ok, Map.reject(grid, fn {_point, values} -> values == %{} end)}
|
|
|
|
error ->
|
|
error
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Split a binary blob into individual GRIB2 messages by scanning for "GRIB" magic bytes
|
|
and reading the total length from the indicator section.
|
|
"""
|
|
def split_messages(binary), do: split_messages(binary, [])
|
|
|
|
@doc """
|
|
Compute linear index from grid (i, j) and scan mode.
|
|
|
|
For HRRR scan_mode=64 (j positive, i positive, i consecutive): index = j * nx + i
|
|
"""
|
|
def linear_index({i, j}, nx, _scan_mode) do
|
|
j * nx + i
|
|
end
|
|
|
|
# --- Private ---
|
|
|
|
defp split_messages(<<>>, acc), do: Enum.reverse(acc)
|
|
|
|
defp split_messages(<<"GRIB", _::32, total_length::64-big, _rest::binary>> = binary, acc) do
|
|
if total_length > byte_size(binary) do
|
|
Enum.reverse(acc)
|
|
else
|
|
msg = binary_part(binary, 0, total_length)
|
|
remaining = binary_part(binary, total_length, byte_size(binary) - total_length)
|
|
split_messages(remaining, [msg | acc])
|
|
end
|
|
end
|
|
|
|
defp split_messages(<<_::8, rest::binary>>, acc) do
|
|
# Skip non-GRIB bytes (padding between messages)
|
|
split_messages(rest, acc)
|
|
end
|
|
|
|
defp init_grid(points) do
|
|
Map.new(points, fn point -> {point, %{}} end)
|
|
end
|
|
|
|
# Pre-compute Lambert Conformal grid indices for all points (done once, reused per message)
|
|
defp precompute_indices(grid, points) do
|
|
Enum.flat_map(points, fn {lat, lon} = point ->
|
|
case LambertConformal.to_grid_index(grid, lat, lon) do
|
|
{:ok, {i, j}} ->
|
|
[{point, linear_index({i, j}, grid.nx, grid.scan_mode)}]
|
|
|
|
{:error, :outside_grid} ->
|
|
[]
|
|
end
|
|
end)
|
|
end
|
|
|
|
# Extract values for all pre-computed indices from a single GRIB message
|
|
defp extract_grid_with_indices(msg, grid_indices) do
|
|
with {:ok, parsed} <- Section.parse_message(msg) do
|
|
%{product: prod, packing_params: packing, data: data} = parsed
|
|
key = "#{prod.var}:#{prod.level}"
|
|
|
|
# For complex packing, decode_all is called once; for simple, each index is independent
|
|
unique_indices = grid_indices |> Enum.map(&elem(&1, 1)) |> Enum.uniq()
|
|
|
|
case batch_unpack(packing, data, unique_indices) do
|
|
{:ok, index_values} ->
|
|
values =
|
|
Enum.flat_map(grid_indices, fn {point, index} ->
|
|
case Map.get(index_values, index) do
|
|
nil -> []
|
|
value -> [{point, value}]
|
|
end
|
|
end)
|
|
|
|
{:ok, key, values}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
rescue
|
|
e -> {:error, "GRIB2 grid extraction failed: #{inspect(e)}"}
|
|
end
|
|
|
|
defp batch_unpack(%{template: 3} = params, data, indices) do
|
|
ComplexPacking.extract_values(params, data, indices)
|
|
end
|
|
|
|
defp batch_unpack(params, data, indices) do
|
|
SimplePacking.extract_values(params, data, indices)
|
|
end
|
|
|
|
defp extract_single(msg, lat, lon) do
|
|
with {:ok, parsed} <- Section.parse_message(msg),
|
|
%{grid_params: grid, product: prod, packing_params: packing, data: data} = parsed,
|
|
key = "#{prod.var}:#{prod.level}",
|
|
{:ok, {i, j}} <- LambertConformal.to_grid_index(grid, lat, lon),
|
|
index = linear_index({i, j}, grid.nx, grid.scan_mode),
|
|
{:ok, value} <- unpack_value(packing, data, index) do
|
|
{:ok, key, value}
|
|
end
|
|
rescue
|
|
e -> {:error, "GRIB2 extraction failed: #{inspect(e)}"}
|
|
end
|
|
|
|
defp unpack_value(%{template: 3} = params, data, index) do
|
|
ComplexPacking.extract_value(params, data, index)
|
|
end
|
|
|
|
defp unpack_value(params, data, index) do
|
|
SimplePacking.extract_value(params, data, index)
|
|
end
|
|
end
|