defmodule Mix.Tasks.HrrrNativeDeriveFields do @shortdoc "Compute derived turbulence fields on hrrr_native_profiles" @moduledoc """ Walks all `hrrr_native_profiles` rows that are missing derived fields (bulk_richardson is nil) and populates them from the raw level arrays using the Inversion and ThetaE modules. Idempotent: rows that already have derived fields are skipped. mix hrrr_native_derive_fields # process all pending mix hrrr_native_derive_fields --limit 100 """ use Mix.Task import Ecto.Query alias Microwaveprop.Propagation.Duct alias Microwaveprop.Propagation.Inversion alias Microwaveprop.Repo alias Microwaveprop.Weather.HrrrNativeProfile alias Microwaveprop.Weather.ThetaE @impl Mix.Task def run(argv) do Mix.Task.run("app.start") _ = Oban.pause_all_queues(Oban) {opts, _, _} = OptionParser.parse(argv, switches: [limit: :integer]) limit = Keyword.get(opts, :limit, 10_000) profiles = HrrrNativeProfile |> where([p], is_nil(p.bulk_richardson) and p.level_count > 2) |> limit(^limit) |> Repo.all() Mix.shell().info("Deriving fields for #{length(profiles)} profiles...") count = Enum.count(profiles, fn profile -> derived = derive(profile) {1, _} = HrrrNativeProfile |> where([p], p.id == ^profile.id) |> Repo.update_all(set: derived) true end) Mix.shell().info("Updated #{count} profiles with derived turbulence fields.") end defp derive(profile) do p = %{ heights_m: profile.heights_m, temp_k: profile.temp_k, spfh: profile.spfh, pressure_pa: profile.pressure_pa, u_wind_ms: profile.u_wind_ms, v_wind_ms: profile.v_wind_ms, tke_m2s2: profile.tke_m2s2, level_count: profile.level_count } # Phase 2: turbulence features inversion_fields = case Inversion.find_inversion_top(p) do {:ok, %{height_m: top_h, level_idx: top_idx, base_idx: base_idx}} -> ri = Inversion.bulk_richardson(p, base_idx, top_idx) shear = Inversion.shear_magnitude(p, base_idx, top_idx) theta_e_jump = ThetaE.theta_e_jump(p, base_idx, top_idx) [ inversion_top_m: top_h, bulk_richardson: ri, shear_at_top_ms: shear, theta_e_jump_k: theta_e_jump ] :none -> [ inversion_top_m: nil, bulk_richardson: nil, shear_at_top_ms: nil, theta_e_jump_k: nil ] end # Phase 4: duct geometry duct_result = Duct.analyze(p) duct_fields = [ ducts: duct_result.ducts, best_duct_band_ghz: duct_result.best_duct_band_ghz ] inversion_fields ++ duct_fields end end