defmodule Microwaveprop.Weather.WeatherLayers do @moduledoc "Derives scalar weather map layers from HRRR profile data." alias Microwaveprop.Weather.SoundingParams @doc """ Derives a map of scalar weather fields from a row containing HRRR profile data. Expects a map with keys: `:surface_temp_c`, `:surface_dewpoint_c`, `:surface_pressure_mb`, `:surface_refractivity`, `:profile`, `:duct_characteristics`. Returns a map with derived fields: `:surface_rh`, `:surface_refractivity`, `:temp_850mb`, `:dewpoint_850mb`, `:lapse_rate`, `:inversion_strength`, `:inversion_base_m`, `:duct_base_m`, `:duct_strength`. """ @spec derive(map()) :: map() def derive(row) do sorted = sort_profile(row.profile) %{ surface_rh: compute_rh(row.surface_temp_c, row.surface_dewpoint_c), surface_refractivity: row.surface_refractivity, temp_850mb: level_value(sorted, 850, "tmpc"), dewpoint_850mb: level_value(sorted, 850, "dwpc"), lapse_rate: compute_lapse_rate(sorted), inversion_strength: inversion_strength(sorted), inversion_base_m: inversion_base_m(sorted), duct_base_m: duct_field(row.duct_characteristics, "base"), duct_strength: duct_field(row.duct_characteristics, "strength") } end defp sort_profile(nil), do: [] defp sort_profile([]), do: [] defp sort_profile(profile) do profile |> Enum.filter(fn p -> p["pres"] != nil and p["tmpc"] != nil and p["hght"] != nil end) |> Enum.sort_by(fn p -> p["pres"] end, :desc) end defp compute_rh(t_c, td_c) do 100.0 * SoundingParams.sat_vap_pres(td_c) / SoundingParams.sat_vap_pres(t_c) end defp level_value([], _target_pres, _key), do: nil defp level_value(sorted, target_pres, key) do nearest = Enum.min_by(sorted, fn p -> abs(p["pres"] - target_pres) end) if abs(nearest["pres"] - target_pres) <= 25.0 do nearest[key] end end defp compute_lapse_rate([]), do: nil defp compute_lapse_rate(sorted) when length(sorted) < 2, do: nil defp compute_lapse_rate(sorted) do surface = hd(sorted) top = List.last(sorted) dh_km = (top["hght"] - surface["hght"]) / 1000.0 if dh_km > 0 do (surface["tmpc"] - top["tmpc"]) / dh_km end end defp inversion_strength([]), do: 0.0 defp inversion_strength(sorted) do sorted |> Enum.chunk_every(2, 1, :discard) |> Enum.reduce(0.0, fn [lower, upper], max_str -> dt = upper["tmpc"] - lower["tmpc"] if dt > max_str do dt else max_str end end) end defp inversion_base_m([]), do: nil defp inversion_base_m(sorted) do sfc_hght = hd(sorted)["hght"] result = sorted |> Enum.chunk_every(2, 1, :discard) |> Enum.reduce({0.0, nil}, fn [lower, upper], {max_str, _base} = acc -> dt = upper["tmpc"] - lower["tmpc"] if dt > 0 and dt > max_str do {dt, lower["hght"] - sfc_hght} else acc end end) case result do {str, base} when str > 0 -> base _ -> nil end end defp duct_field(nil, _key), do: nil defp duct_field([], _key), do: nil defp duct_field(ducts, "base") do ducts |> Enum.map(fn d -> d["base"] end) |> Enum.min() end defp duct_field(ducts, "strength") do ducts |> Enum.map(fn d -> d["strength"] end) |> Enum.max() end end