Files

267 lines
11 KiB
C#

using TheLivingWorld.Core.Ecs;
using TheLivingWorld.Core.Simulation;
namespace TheLivingWorld.Tests;
public sealed class WeatherModelTests
{
private const double Warsaw = 52.23;
private const double Sydney = -33.87;
private static readonly DateTime JanuaryNoon = new(2012, 1, 15, 12, 0, 0, DateTimeKind.Unspecified);
private static readonly DateTime JulyNoon = new(2012, 7, 15, 12, 0, 0, DateTimeKind.Unspecified);
[Fact]
public void Seasons_run_the_other_way_below_the_equator()
{
Assert.True(WeatherModel.SeasonPhase(JulyNoon, Warsaw) > 0.8f);
Assert.True(WeatherModel.SeasonPhase(JanuaryNoon, Warsaw) < -0.8f);
Assert.True(WeatherModel.SeasonPhase(JulyNoon, Sydney) < -0.8f);
Assert.True(WeatherModel.SeasonPhase(JanuaryNoon, Sydney) > 0.8f);
}
[Fact]
public void The_day_peaks_in_the_afternoon_and_bottoms_out_before_dawn()
{
var afternoon = new DateTime(2012, 6, 1, 15, 0, 0, DateTimeKind.Unspecified);
var beforeDawn = new DateTime(2012, 6, 1, 3, 0, 0, DateTimeKind.Unspecified);
Assert.Equal(1f, WeatherModel.DiurnalPhase(afternoon), 3);
Assert.Equal(-1f, WeatherModel.DiurnalPhase(beforeDawn), 3);
}
[Fact]
public void Siberia_is_brutal_in_January_and_pleasant_in_July()
{
var climate = ClimateCatalog.Siberian;
var winter = Calm(climate, 62.03, JanuaryNoon);
var summer = Calm(climate, 62.03, JulyNoon);
Assert.InRange(winter.TemperatureC, -45f, -20f);
Assert.InRange(summer.TemperatureC, 8f, 30f);
}
[Fact]
public void The_equator_barely_notices_the_calendar()
{
var climate = ClimateCatalog.Equatorial;
var january = Calm(climate, 1.35, JanuaryNoon);
var july = Calm(climate, 1.35, JulyNoon);
Assert.InRange(MathF.Abs(january.TemperatureC - july.TemperatureC), 0f, 3f);
Assert.InRange(january.TemperatureC, 20f, 36f);
}
[Fact]
public void A_year_of_a_climate_averages_out_to_its_stated_mean()
{
foreach (var climate in ClimateCatalog.All)
{
var total = 0.0;
var samples = 0;
// Every six hours through a year, so both the seasonal and the daily curve are covered evenly.
for (var hours = 0; hours < 365 * 24; hours += 6)
{
var moment = new DateTime(2012, 1, 1, 0, 0, 0, DateTimeKind.Unspecified).AddHours(hours);
total += Calm(climate, Warsaw, moment).TemperatureC;
samples++;
}
var mean = total / samples;
Assert.True(
Math.Abs(mean - climate.MeanTemperatureC) < 1.5,
$"{climate.Label} averaged {mean:F1} °C against a stated mean of {climate.MeanTemperatureC} °C.");
}
}
[Fact]
public void A_deep_low_clouds_over_and_rains_while_a_high_stays_clear()
{
var climate = ClimateCatalog.CentralEuropean;
var low = WeatherModel.Sample(climate, Warsaw, JulyNoon, anomalyHpa: -22f, 0f, 0f);
var high = WeatherModel.Sample(climate, Warsaw, JulyNoon, anomalyHpa: 14f, 0f, 0f);
Assert.True(low.CloudCover > high.CloudCover);
Assert.True(low.PrecipitationMmH > 0f);
Assert.Equal(0f, high.PrecipitationMmH);
Assert.True(low.PressureHpa < high.PressureHpa);
}
[Fact]
public void Precipitation_falls_as_snow_once_it_is_freezing()
{
var winterNight = new DateTime(2012, 1, 15, 2, 0, 0, DateTimeKind.Unspecified);
var sample = WeatherModel.Sample(ClimateCatalog.Siberian, 62.03, winterNight, -25f, 0f, 0f);
Assert.True(sample.TemperatureC < 0f);
Assert.Contains(
sample.Condition,
new[] { WeatherCondition.Snow, WeatherCondition.HeavySnow, WeatherCondition.Blizzard });
}
[Fact]
public void A_dry_climate_under_a_gale_raises_a_sandstorm_rather_than_rain()
{
// A steep gradient with no depth to it: lots of wind, not enough convergence to cloud over.
var sample = WeatherModel.Sample(ClimateCatalog.HotDesert, 30.05, JulyNoon, 2f, 30f, 0f);
Assert.True(sample.WindSpeedMs > 11f);
Assert.Equal(0f, sample.PrecipitationMmH);
Assert.Equal(WeatherCondition.Sandstorm, sample.Condition);
}
[Fact]
public void Wind_runs_along_the_isobars_and_the_hemisphere_decides_which_way()
{
// Pressure rising to the north. The along-isobar component flips between hemispheres, so the wind
// arrives from the east in the north and from the west in the south. (They are not exactly opposite:
// the friction term drags both towards the low regardless of hemisphere.)
var north = WeatherModel.Sample(ClimateCatalog.Oceanic, Warsaw, JulyNoon, 0f, 0f, 20f);
var south = WeatherModel.Sample(ClimateCatalog.Oceanic, Sydney, JulyNoon, 0f, 0f, 20f);
Assert.InRange(north.WindDirectionDeg, 0f, 180f);
Assert.InRange(south.WindDirectionDeg, 180f, 360f);
}
[Fact]
public void A_steeper_gradient_means_a_stronger_wind()
{
var calm = WeatherModel.Sample(ClimateCatalog.Oceanic, Warsaw, JulyNoon, 0f, 0f, 0f);
var breezy = WeatherModel.Sample(ClimateCatalog.Oceanic, Warsaw, JulyNoon, 0f, 0f, 10f);
var gale = WeatherModel.Sample(ClimateCatalog.Oceanic, Warsaw, JulyNoon, 0f, 0f, 40f);
Assert.Equal(ClimateCatalog.Oceanic.WindSpeedMs, calm.WindSpeedMs, 3);
Assert.True(gale.WindSpeedMs > breezy.WindSpeedMs);
Assert.True(breezy.WindSpeedMs > calm.WindSpeedMs);
}
[Fact]
public void Wind_chill_bites_in_the_cold_and_humidity_bites_in_the_heat()
{
var freezing = WeatherModel.Sample(ClimateCatalog.Tundra, 68, JanuaryNoon, -10f, 15f, 0f);
Assert.True(freezing.FeelsLikeC < freezing.TemperatureC);
var muggy = WeatherModel.Sample(ClimateCatalog.Equatorial, 1.35, JulyNoon, -12f, 0f, 0f);
Assert.True(muggy.TemperatureC > 26f);
Assert.True(muggy.FeelsLikeC > muggy.TemperatureC);
}
[Fact]
public void An_overcast_sky_flattens_the_daily_temperature_swing()
{
var climate = ClimateCatalog.ColdSteppe;
var afternoon = new DateTime(2012, 7, 15, 15, 0, 0, DateTimeKind.Unspecified);
var beforeDawn = new DateTime(2012, 7, 15, 3, 0, 0, DateTimeKind.Unspecified);
var clearSwing = Calm(climate, Warsaw, afternoon).TemperatureC - Calm(climate, Warsaw, beforeDawn).TemperatureC;
var cloudyDay = WeatherModel.Sample(climate, Warsaw, afternoon, -20f, 0f, 0f).TemperatureC;
var cloudyNight = WeatherModel.Sample(climate, Warsaw, beforeDawn, -20f, 0f, 0f).TemperatureC;
Assert.True(clearSwing > cloudyDay - cloudyNight);
}
[Fact]
public void The_wet_season_sits_where_the_preset_says_it_does()
{
// The monsoon peaks just after midsummer; the Mediterranean does its raining in winter.
var monsoonSummer = WeatherModel.WetSeasonFactor(ClimateCatalog.TropicalMonsoon, JulyNoon, 19.08);
var monsoonWinter = WeatherModel.WetSeasonFactor(ClimateCatalog.TropicalMonsoon, JanuaryNoon, 19.08);
Assert.True(monsoonSummer > monsoonWinter);
var medSummer = WeatherModel.WetSeasonFactor(ClimateCatalog.Mediterranean, JulyNoon, 41.39);
var medWinter = WeatherModel.WetSeasonFactor(ClimateCatalog.Mediterranean, JanuaryNoon, 41.39);
Assert.True(medWinter > medSummer);
}
[Fact]
public void A_pressure_system_pulls_the_field_towards_itself_and_fades_at_the_edges()
{
PressureSystem[] systems =
[
new(X: 0.5f, Y: 0.5f, VelocityX: 0f, VelocityY: 0f,
IntensityHpa: -20f, Radius: 0.3f, AgeHours: 10f, LifetimeHours: 40f),
];
var centre = WeatherModel.SampleField(systems, 0.5f, 0.5f);
var edge = WeatherModel.SampleField(systems, 1.5f, 0.5f);
Assert.InRange(centre.Anomaly, -21f, -19f);
Assert.InRange(edge.Anomaly, -0.5f, 0f);
// Pressure climbs as you leave the low, so the gradient east of centre points east.
var offCentre = WeatherModel.SampleField(systems, 0.65f, 0.5f);
Assert.True(offCentre.GradientX > 0f);
}
[Fact]
public void Systems_fade_in_and_out_instead_of_popping()
{
var born = new PressureSystem(0.5f, 0.5f, 0f, 0f, -20f, 0.3f, AgeHours: 0f, LifetimeHours: 40f);
var grown = born with { AgeHours = 20f };
var dying = born with { AgeHours = 40f };
Assert.Equal(0f, WeatherModel.Envelope(born), 3);
Assert.Equal(1f, WeatherModel.Envelope(grown), 3);
Assert.Equal(0f, WeatherModel.Envelope(dying), 3);
}
[Fact]
public void Snow_piles_up_below_freezing_and_melts_above_it()
{
var afterAnHour = WeatherModel.UpdateSnowDepth(0f, temperatureC: -4f, precipitationMmH: 2f, 1f);
Assert.Equal(20f, afterAnHour, 1);
// Rain at the same rate leaves nothing lying.
Assert.Equal(0f, WeatherModel.UpdateSnowDepth(0f, 6f, 2f, 1f), 1);
var thawed = WeatherModel.UpdateSnowDepth(afterAnHour, temperatureC: 8f, precipitationMmH: 0f, 2f);
Assert.True(thawed < afterAnHour);
}
[Fact]
public void Snow_depth_never_leaves_its_bounds()
{
Assert.Equal(0f, WeatherModel.UpdateSnowDepth(5f, temperatureC: 30f, 0f, elapsedHours: 100f));
Assert.Equal(
WeatherModel.MaxSnowDepthMm,
WeatherModel.UpdateSnowDepth(0f, temperatureC: -20f, precipitationMmH: 40f, elapsedHours: 100f));
// A zero-length step still normalises a value that arrived out of range from storage.
Assert.Equal(0f, WeatherModel.UpdateSnowDepth(-5f, -10f, 0f, 0f));
}
[Fact]
public void A_world_opened_in_deep_winter_already_has_snow_on_the_ground()
{
var siberianWinter = WeatherModel.SeasonalSnowDepth(ClimateCatalog.Siberian, 62.03, JanuaryNoon);
var siberianSummer = WeatherModel.SeasonalSnowDepth(ClimateCatalog.Siberian, 62.03, JulyNoon);
Assert.True(siberianWinter > WeatherModel.FullCoverDepthMm);
Assert.Equal(0f, siberianSummer);
// Nowhere warm ever starts under snow, whatever the month.
Assert.Equal(0f, WeatherModel.SeasonalSnowDepth(ClimateCatalog.Equatorial, 1.35, JanuaryNoon));
Assert.Equal(0f, WeatherModel.SeasonalSnowDepth(ClimateCatalog.HotDesert, 30.05, JanuaryNoon));
}
[Fact]
public void The_seasonal_snow_line_follows_the_hemisphere()
{
// Same climate, opposite hemispheres: the snow is on the ground in opposite months.
var north = WeatherModel.SeasonalSnowDepth(ClimateCatalog.Tundra, 68, JanuaryNoon);
var south = WeatherModel.SeasonalSnowDepth(ClimateCatalog.Tundra, -68, JanuaryNoon);
Assert.True(north > 0f);
Assert.True(north > south);
}
private static WeatherSample Calm(ClimatePreset climate, double latitude, DateTime moment) =>
WeatherModel.Sample(climate, latitude, moment, anomalyHpa: 0f, gradientX: 0f, gradientY: 0f);
}