Render the HRRR pressure-level profile as a full skew-T log-P diagram on the contact detail page: skewed isotherms, isobars, dry adiabats, saturation mixing ratio lines, plus the T and Td traces. Math lives in MicrowavepropWeb.SkewT (Magnus formula, dry adiabat potential temperature, log-P projection) and is exercised by a dedicated test module. The atmospheric profile section now expands by default so the chart is visible without an extra click.
128 lines
4.6 KiB
Elixir
128 lines
4.6 KiB
Elixir
defmodule MicrowavepropWeb.SkewTTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias MicrowavepropWeb.SkewT
|
|
|
|
describe "saturation_vapor_pressure/1 (Magnus)" do
|
|
test "reference values are within 1% of textbook" do
|
|
# At 0°C the Magnus formula gives ≈ 6.112 hPa.
|
|
assert_in_delta SkewT.saturation_vapor_pressure(0.0), 6.112, 0.05
|
|
|
|
# At 20°C es ≈ 23.37 hPa.
|
|
assert_in_delta SkewT.saturation_vapor_pressure(20.0), 23.37, 0.2
|
|
|
|
# At -20°C es ≈ 1.254 hPa.
|
|
assert_in_delta SkewT.saturation_vapor_pressure(-20.0), 1.254, 0.05
|
|
end
|
|
end
|
|
|
|
describe "temperature_from_mixing_ratio/2" do
|
|
test "round-trips mixing ratios" do
|
|
# Pick a pressure and temperature, compute ws, invert, expect the original.
|
|
p = 850.0
|
|
|
|
for t <- [-10.0, 0.0, 15.0, 25.0] do
|
|
es = SkewT.saturation_vapor_pressure(t)
|
|
ws_g_kg = 622.0 * es / (p - es)
|
|
recovered = SkewT.temperature_from_mixing_ratio(ws_g_kg, p)
|
|
assert_in_delta recovered, t, 0.05
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "dry_adiabat_temperature/2" do
|
|
test "θ == T at 1000 mb" do
|
|
# At the reference pressure the potential temperature equals the
|
|
# actual temperature — so θ = 300 K ⇒ T(1000) = 300 - 273.15 ≈ 26.85°C.
|
|
assert_in_delta SkewT.dry_adiabat_temperature(300.0, 1000.0), 26.85, 0.02
|
|
end
|
|
|
|
test "a parcel lifted dry-adiabatically cools" do
|
|
# Lifting from 1000 mb (θ = 290 K, T ≈ 16.85°C) to 700 mb should
|
|
# drop the temperature by ~26°C (roughly 9.8°C/km, ~3 km).
|
|
t_surface = SkewT.dry_adiabat_temperature(290.0, 1000.0)
|
|
t_700 = SkewT.dry_adiabat_temperature(290.0, 700.0)
|
|
assert t_surface - t_700 > 20.0
|
|
end
|
|
end
|
|
|
|
describe "project/3" do
|
|
setup do
|
|
chart = SkewT.chart_config(width: 400, height: 500)
|
|
%{chart: chart}
|
|
end
|
|
|
|
test "p_bot lands at the chart bottom and p_top at the top", %{chart: chart} do
|
|
{_x_bot, y_bot} = SkewT.project(0.0, chart.p_bot, chart)
|
|
{_x_top, y_top} = SkewT.project(0.0, chart.p_top, chart)
|
|
|
|
# SVG y grows downward, so p_bot (bottom) has a larger y than p_top.
|
|
assert y_bot > y_top
|
|
assert_in_delta y_bot, chart.padding_top + chart.plot_height, 0.5
|
|
assert_in_delta y_top, chart.padding_top, 0.5
|
|
end
|
|
|
|
test "isotherms skew right as pressure decreases", %{chart: chart} do
|
|
# Same temperature at two pressures should have a larger x at
|
|
# the lower pressure (top of chart) because of the skew.
|
|
{x_bot, _} = SkewT.project(0.0, chart.p_bot, chart)
|
|
{x_top, _} = SkewT.project(0.0, chart.p_top, chart)
|
|
assert x_top > x_bot
|
|
end
|
|
|
|
test "temperature range spans the bottom of the chart", %{chart: chart} do
|
|
# At p_bot, t_min lands at the left edge of the data area.
|
|
{x_min, _} = SkewT.project(chart.t_min, chart.p_bot, chart)
|
|
{x_max, _} = SkewT.project(chart.t_max, chart.p_bot, chart)
|
|
assert_in_delta x_min, chart.padding_left, 0.5
|
|
assert_in_delta x_max, chart.padding_left + chart.plot_width, 0.5
|
|
end
|
|
end
|
|
|
|
describe "build/2" do
|
|
test "with no profile returns nil" do
|
|
assert SkewT.build(nil) == nil
|
|
assert SkewT.build([]) == nil
|
|
end
|
|
|
|
test "returns a map of rendering primitives for a valid profile" do
|
|
profile = [
|
|
%{"pres" => 1000.0, "hght" => 100.0, "tmpc" => 25.0, "dwpc" => 18.0},
|
|
%{"pres" => 850.0, "hght" => 1500.0, "tmpc" => 15.0, "dwpc" => 10.0},
|
|
%{"pres" => 700.0, "hght" => 3100.0, "tmpc" => 5.0, "dwpc" => -2.0},
|
|
%{"pres" => 500.0, "hght" => 5600.0, "tmpc" => -15.0, "dwpc" => -25.0}
|
|
]
|
|
|
|
chart = SkewT.build(profile)
|
|
|
|
assert is_map(chart)
|
|
assert chart.width > 0
|
|
assert chart.height > 0
|
|
# Background grid
|
|
assert length(chart.isobars) > 0
|
|
assert length(chart.isotherms) > 0
|
|
assert length(chart.dry_adiabats) > 0
|
|
assert length(chart.mixing_ratio_lines) > 0
|
|
# Traces
|
|
assert chart.t_points != []
|
|
assert chart.td_points != []
|
|
# The first plotted t_point should correspond to the highest pressure
|
|
# (surface-most) profile entry.
|
|
assert length(chart.t_points) == 4
|
|
end
|
|
|
|
test "filters out profile levels with missing temperature or dewpoint independently" do
|
|
profile = [
|
|
%{"pres" => 1000.0, "tmpc" => 25.0, "dwpc" => 18.0},
|
|
%{"pres" => 900.0, "tmpc" => nil, "dwpc" => 15.0},
|
|
%{"pres" => 850.0, "tmpc" => 15.0, "dwpc" => nil},
|
|
%{"pres" => 700.0, "tmpc" => 5.0, "dwpc" => -2.0}
|
|
]
|
|
|
|
chart = SkewT.build(profile)
|
|
# 3 levels have tmpc, 3 have dwpc — the traces are independent.
|
|
assert length(chart.t_points) == 3
|
|
assert length(chart.td_points) == 3
|
|
end
|
|
end
|
|
end
|