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.
119 lines
3.9 KiB
Elixir
119 lines
3.9 KiB
Elixir
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
|