From 65f405cce7fd90276a9bcaf6b1c7f979e39894b1 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 5 Aug 2026 17:33:54 -0500 Subject: [PATCH] fix(weather): read atom-keyed ducts in duct base/strength layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit duct_field/2 did d["base"] || d["base_m"], but ProfilesFile.read/1 atomizes both keys via its @mp_atom_keys whitelist. String indexing an atom-keyed map returns nil, so the Duct Base and Duct Strength layers were always nil on the cold-derive path and in point-detail popups — despite the comment directly above claiming both shapes were handled. Normalize keys before lookup, the way duct_min_freq/1 already did, and share that normalization between the two. --- lib/microwaveprop/weather/weather_layers.ex | 20 ++++++++-- .../weather/weather_layers_test.exs | 38 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/lib/microwaveprop/weather/weather_layers.ex b/lib/microwaveprop/weather/weather_layers.ex index eba9ca34..977601e2 100644 --- a/lib/microwaveprop/weather/weather_layers.ex +++ b/lib/microwaveprop/weather/weather_layers.ex @@ -54,7 +54,7 @@ defmodule Microwaveprop.Weather.WeatherLayers do end defp duct_min_freq(duct) when is_map(duct) do - d = Map.new(duct, fn {k, v} -> {to_string(k), v} end) + d = stringify_keys(duct) case d["min_freq_ghz"] do f when is_number(f) and f > 0 -> f * 1.0 @@ -198,15 +198,29 @@ defmodule Microwaveprop.Weather.WeatherLayers do # wider frequency range, so it's the most meaningful scalar. defp duct_field(ducts, "base") do ducts - |> Enum.map(fn d -> d["base"] || d["base_m"] end) + |> Enum.map(&duct_value(&1, ["base", "base_m"])) |> Enum.reject(&is_nil/1) |> Enum.min(fn -> nil end) end defp duct_field(ducts, "strength") do ducts - |> Enum.map(fn d -> d["strength"] || d["thickness_m"] end) + |> Enum.map(&duct_value(&1, ["strength", "thickness_m"])) |> Enum.reject(&is_nil/1) |> Enum.max(fn -> nil end) end + + # First present key wins. Keys are normalized to strings first because + # a duct read back through `ProfilesFile.read/1` carries atom keys + # (`:base_m`) while a `Msgpax`-decoded or sounding-derived one carries + # strings — indexing an atom-keyed map with a string silently returns + # nil, which blanks the layer. + defp duct_value(duct, keys) when is_map(duct) do + d = stringify_keys(duct) + Enum.find_value(keys, fn key -> d[key] end) + end + + defp duct_value(_duct, _keys), do: nil + + defp stringify_keys(map), do: Map.new(map, fn {k, v} -> {to_string(k), v} end) end diff --git a/test/microwaveprop/weather/weather_layers_test.exs b/test/microwaveprop/weather/weather_layers_test.exs index f9e24a19..45ec3c30 100644 --- a/test/microwaveprop/weather/weather_layers_test.exs +++ b/test/microwaveprop/weather/weather_layers_test.exs @@ -154,6 +154,44 @@ defmodule Microwaveprop.Weather.WeatherLayersTest do assert result.duct_base_m == nil assert result.duct_strength == nil end + + # HrrrNativeClient and the Rust f00 pipeline emit `:base_m` / + # `:thickness_m`, which `ProfilesFile.read/1` atomizes via its + # `@mp_atom_keys` whitelist. String-key access on an atom-keyed map + # returns nil, which blanks the Duct Base / Duct Strength overlays + # on every cold-derive read. + test "extracts base and strength from atom-keyed native ducts" do + ducts = [ + %{base_m: 500.0, top_m: 800.0, thickness_m: 300.0}, + %{base_m: 200.0, top_m: 400.0, thickness_m: 200.0} + ] + + result = WeatherLayers.derive(sample_row(%{duct_characteristics: ducts})) + + assert result.duct_base_m == 200.0 + assert result.duct_strength == 300.0 + end + + test "extracts base and strength from string-keyed native ducts" do + ducts = [ + %{"base_m" => 500.0, "top_m" => 800.0, "thickness_m" => 300.0}, + %{"base_m" => 200.0, "top_m" => 400.0, "thickness_m" => 200.0} + ] + + result = WeatherLayers.derive(sample_row(%{duct_characteristics: ducts})) + + assert result.duct_base_m == 200.0 + assert result.duct_strength == 300.0 + end + + test "extracts base and strength from atom-keyed sounding ducts" do + ducts = [%{base: 500.0, strength: 12.5}, %{base: 200.0, strength: 8.0}] + + result = WeatherLayers.derive(sample_row(%{duct_characteristics: ducts})) + + assert result.duct_base_m == 200.0 + assert result.duct_strength == 12.5 + end end describe "derive/1 duct cutoff frequency" do