prop/lib/microwaveprop/weather/grib2/lambert_conformal.ex
Graham McIntire 5a9359191f
Replace wgrib2 with pure Elixir GRIB2 decoder for HRRR pipeline
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.
2026-03-29 18:43:43 -05:00

84 lines
2.1 KiB
Elixir

defmodule Microwaveprop.Weather.Grib2.LambertConformal do
@moduledoc false
@r_earth 6_371_229.0
@doc """
Convert a lat/lon to grid indices (i, j) on a Lambert Conformal Conic grid.
Returns `{:ok, {i, j}}` or `{:error, :outside_grid}`.
"""
def to_grid_index(grid, lat, lon) do
%{
nx: nx,
ny: ny,
la1: la1,
lo1: lo1,
dx: dx,
dy: dy,
latin1: latin1,
lov: lov
} = grid
# Normalize longitudes to 0..360
lo1 = normalize_lon(lo1)
lon = normalize_lon(lon)
lov = normalize_lon(lov)
latin1_rad = deg_to_rad(latin1)
la1_rad = deg_to_rad(la1)
lat_rad = deg_to_rad(lat)
# Cone constant (for HRRR, latin1 == latin2 == 38.5, so n = sin(latin1))
n = :math.sin(latin1_rad)
# F factor
f =
:math.cos(latin1_rad) *
:math.pow(:math.tan(:math.pi() / 4 + latin1_rad / 2), n) / n
# rho for a given latitude
rho_origin = rho(la1_rad, n, f)
rho_target = rho(lat_rad, n, f)
# Theta angles (difference from central meridian, scaled by cone constant)
theta_origin = n * deg_to_rad(lo1 - lov)
theta_target = n * deg_to_rad(adjusted_lon_diff(lon, lov))
# Grid coordinates
x_origin = rho_origin * :math.sin(theta_origin)
y_origin = rho_origin * :math.cos(theta_origin)
x_target = rho_target * :math.sin(theta_target)
y_target = rho_target * :math.cos(theta_target)
i = round((x_target - x_origin) / dx)
j = round((y_origin - y_target) / dy)
if i >= 0 and i < nx and j >= 0 and j < ny do
{:ok, {i, j}}
else
{:error, :outside_grid}
end
end
defp rho(lat_rad, n, f) do
@r_earth * f / :math.pow(:math.tan(:math.pi() / 4 + lat_rad / 2), n)
end
defp deg_to_rad(deg), do: deg * :math.pi() / 180.0
defp normalize_lon(lon) when lon < 0, do: lon + 360.0
defp normalize_lon(lon), do: lon + 0.0
# Compute longitude difference handling wrap-around
defp adjusted_lon_diff(lon, lov) do
diff = lon - lov
cond do
diff > 180.0 -> diff - 360.0
diff < -180.0 -> diff + 360.0
true -> diff
end
end
end