prop/test/microwaveprop_web/skew_t_property_test.exs
Graham McIntire 4c1013cdbd
test(property): add property tests for terrain + atmospheric math
Cover the pure-math invariants of the ITU-R P.526-16 diffraction
helpers, the haversine metric on Geo, and the Magnus / dry-adiabat
helpers in the skew-T renderer. 29 properties, one invariant each.
2026-04-21 13:56:39 -05:00

119 lines
3.9 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule MicrowavepropWeb.SkewTPropertyTest do
@moduledoc """
StreamData property tests for `MicrowavepropWeb.SkewT`'s atmospheric
math helpers.
Invariants covered:
* Magnus saturation vapor pressure is positive and strictly
monotonic in temperature.
* `temperature_from_mixing_ratio/2` is the inverse of the
ws = 622·es / (p es) forward relation.
* Along a dry adiabat, temperature decreases monotonically as
pressure falls (lift a parcel → it cools).
* At the 1000 mb reference pressure, dry-adiabat T equals θ 273.15
(definition of potential temperature).
* `project/3` reproduces the chart corners and preserves pressure
ordering on the y-axis.
"""
use ExUnit.Case, async: true
use ExUnitProperties
alias MicrowavepropWeb.SkewT
describe "saturation_vapor_pressure/1" do
property "is strictly positive across the troposphere" do
check all(t_c <- float(min: -80.0, max: 50.0)) do
assert SkewT.saturation_vapor_pressure(t_c) > 0
end
end
property "is strictly monotonic in temperature" do
check all(
t_c <- float(min: -80.0, max: 50.0),
delta <- float(min: 0.001, max: 10.0)
) do
lo = SkewT.saturation_vapor_pressure(t_c)
hi = SkewT.saturation_vapor_pressure(t_c + delta)
assert hi > lo
end
end
end
describe "temperature_from_mixing_ratio/2" do
property "round-trips T → ws → T for realistic (T, p) pairs" do
check all(
t_c <- float(min: -30.0, max: 35.0),
p_mb <- float(min: 300.0, max: 1050.0)
) do
es = SkewT.saturation_vapor_pressure(t_c)
# Only test where ws is well-defined (p > es).
if p_mb > es + 1.0 do
ws_g_kg = 622.0 * es / (p_mb - es)
recovered = SkewT.temperature_from_mixing_ratio(ws_g_kg, p_mb)
assert_in_delta recovered, t_c, 0.01
end
end
end
end
describe "dry_adiabat_temperature/2" do
property "equals θ 273.15 at the 1000 mb reference pressure" do
check all(theta_k <- float(min: 200.0, max: 400.0)) do
t_c = SkewT.dry_adiabat_temperature(theta_k, 1000.0)
assert_in_delta t_c, theta_k - 273.15, 1.0e-6
end
end
property "temperature decreases as pressure decreases along an adiabat" do
check all(
theta_k <- float(min: 250.0, max: 360.0),
p_hi <- float(min: 500.0, max: 1050.0),
drop <- float(min: 10.0, max: 400.0)
) do
p_lo = p_hi - drop
if p_lo > 50.0 do
t_hi = SkewT.dry_adiabat_temperature(theta_k, p_hi)
t_lo = SkewT.dry_adiabat_temperature(theta_k, p_lo)
assert t_hi > t_lo
end
end
end
end
describe "project/3" do
setup do
%{chart: SkewT.chart_config([])}
end
property "projects p_bot to the plot bottom and p_top to the plot top",
%{chart: chart} do
check all(t_c <- float(min: chart.t_min, max: chart.t_max)) do
{_x_bot, y_bot} = SkewT.project(t_c, chart.p_bot, chart)
{_x_top, y_top} = SkewT.project(t_c, chart.p_top, chart)
# SVG y grows downward: the bottom of the chart has a larger y.
assert y_bot > y_top
assert_in_delta y_bot, chart.padding_top + chart.plot_height, 1.0e-6
assert_in_delta y_top, chart.padding_top, 1.0e-6
end
end
property "pressure ordering is inverted on the y-axis (higher p → larger y)",
%{chart: chart} do
check all(
p_hi <- float(min: 200.0, max: 1050.0),
p_lo <- float(min: 100.0, max: 1000.0),
t_c <- float(min: -20.0, max: 20.0)
) do
if p_hi > p_lo + 1.0 do
{_x1, y_hi} = SkewT.project(t_c, p_hi, chart)
{_x2, y_lo} = SkewT.project(t_c, p_lo, chart)
assert y_hi > y_lo
end
end
end
end
end