517 wtg& operator=(
wtg&&) noexcept = default;
527 static wtg inst{auto_probe_tag{}};
555 return std::move(run_(request, 1, call_seed).front());
582 std::uint64_t call_seed)
const
586 throw std::invalid_argument(std::format(
"weather-generator: days must be 1 to {}",
max_series_days));
588 return run_(request, days, call_seed);
601 check_point_(latitude, longitude);
602 return _cells[locate_(latitude, longitude).cell].normals;
618 const core::batch_options& options)
640 const core::batch_options& options,
641 std::uint64_t batch_seed)
const
643 return core::run_batch(options, batch_seed,
644 [&](std::uint64_t item_seed) {
return get_weather(request, item_seed); });
660 template <
typename Sink>
662 std::uint64_t batch_seed, Sink&& sink)
const
664 core::for_each_batch(options, batch_seed,
665 [&](std::uint64_t item_seed) {
return get_weather(request, item_seed); },
666 std::forward<Sink>(sink));
675 template <
typename Sink>
685 [[nodiscard]]
bool has_data()
const {
return !_cells.empty(); }
689 [[nodiscard]] std::size_t
count()
const {
return _cells.size(); }
701 void load(
const std::filesystem::path& dir)
704 const auto file = dir / data_file_;
705 if (!std::filesystem::is_directory(dir, ec) || !std::filesystem::is_regular_file(file, ec))
709 (void)
extend(core::tsv_document::load(file), core::layer_mode::replace);
710 const auto mask = dir / land_file_;
711 _land = std::filesystem::is_regular_file(mask, ec) ? read_land_(core::tsv_document::load(mask)) : land_mask{};
726 [[nodiscard]]
bool load(dataset tier)
728 const std::string path = std::string{core::tier_name(tier)} +
"/" + std::string{data_file_};
729 auto doc = core::load_resource(path, embedded_files_(), core::find_resources(library_name_, path));
730 if (!doc)
return false;
731 (void)
extend(std::move(*doc), core::layer_mode::replace);
732 const std::string mask = std::string{core::tier_name(tier)} +
"/" + std::string{land_file_};
733 auto land = core::load_resource(mask, embedded_files_(), core::find_resources(library_name_, mask));
734 _land = land ? read_land_(*land) : land_mask{};
753 const core::layer_info&
extend(
const std::filesystem::path& file,
754 core::layer_mode mode = core::layer_mode::override)
756 return extend(core::tsv_document::load(file), mode);
767 core::layer_mode mode = core::layer_mode::override)
769 return extend(core::tsv_document::parse(text, std::move(source)), mode);
777 const core::layer_info&
extend(core::tsv_document doc, core::layer_mode mode = core::layer_mode::override)
780 _table.extend(std::move(doc), mode, [](
const core::tsv_row& row) { (void)cell::parse(row); });
798 auto doc = core::load_resource(std::string{history_file_}, embedded_files_(),
799 core::find_resources(library_name_, std::string{history_file_}));
800 if (!doc)
return false;
809 void load_history(
const std::filesystem::path& file) { read_history_(core::tsv_document::load(file)); }
813 [[nodiscard]]
bool has_history()
const {
return !_history.empty(); }
817 [[nodiscard]]
const std::vector<core::layer_info>&
layers()
const {
return _table.layers(); }
822 [[nodiscard]] std::uint64_t
data_hash()
const {
return _table.data_hash(); }
836 static constexpr double persistence0 = 0.27;
837 static constexpr double persistence1 = 0.00647;
840 static constexpr double shape0 = 1.14578;
841 static constexpr double shape1 = -0.22430;
842 static constexpr double shape_min = 0.4;
843 static constexpr double shape_max = 1.5;
851 static constexpr double wet_mean = 0.016;
852 static constexpr double wet_range = 0.2204;
858 static constexpr std::array<double, 7> spread_mean{-1.21055, 0.05030, 0.15209, 1.31636, -0.19543, -0.04674, 0.03630};
859 static constexpr std::array<double, 7> spread_range{-0.37573, 0.02161, 0.20729, 0.85934, -0.25677, -0.00543, 0.01475};
860 static constexpr double spread_min = 0.2;
861 static constexpr double spread_max = 12.0;
865 static constexpr std::array<double, 4> a{0.69497378, 0.06841554, -0.05222351, 0.30667295};
866 static constexpr std::array<double, 4> b{0.71084994, 0.0, 0.09968858, 0.94429324};
869 static constexpr double wind_shape = 2.902;
870 static constexpr double wind_gamma = 0.89171496;
875 static constexpr double snow0 = 0.4632;
876 static constexpr double snow1 = -0.6058;
877 static constexpr double sleet_above = 1.0;
881 static constexpr std::array<double, 3> thunder{-5.0964, 0.1213, 0.3431};
886 static constexpr double cloud_wet = 0.6;
887 static constexpr double cloud_kappa = 4.0;
890 static constexpr double humidity_wet = 10.0;
891 static constexpr double humidity_sd = 7.0;
898 static constexpr double temperature_month = 0.48;
899 static constexpr double wet_month = 0.32;
900 static constexpr double amount_month = 0.28;
902 static constexpr double lapse_rate = 0.0065;
909 static constexpr double sunset_share = 0.39;
910 static constexpr double warmest_before_sunset = 4.0;
914 static constexpr double wet_hours0 = 2.0;
915 static constexpr double wet_hours1 = 2.5;
916 static constexpr double two_spells = 0.3;
917 static constexpr double thunder_hour = 15.0;
920 static constexpr double wind_swing = 0.25;
921 static constexpr double windiest_hour = 14.0;
922 static constexpr double wind_noise = 0.2;
923 static constexpr double cloud_noise = 1.0;
924 static constexpr double hour_lag = 0.8;
925 static constexpr double wet_hour_cloud = 0.85;
926 static constexpr double wet_hour_humidity = 90.0;
931 static constexpr int window = 48;
936 static constexpr std::string_view library_name_ =
"weather-generator";
937 static constexpr std::string_view data_file_ =
"climate.tsv";
938 static constexpr std::string_view land_file_ =
"land.tsv";
942 static constexpr std::string_view history_file_ =
"history/history.tsv";
943 static constexpr int history_first_year = 1991;
944 static constexpr int history_last_year = 2020;
945 static constexpr int history_months = 12 * (history_last_year - history_first_year + 1);
946 static constexpr int history_rows = 90;
947 static constexpr int history_columns = 180;
950 using history_month = std::array<float, 3>;
954 static constexpr int land_rows = 360;
955 static constexpr int land_columns = 720;
956 static constexpr double sea_limit_km = 100.0;
957 static constexpr double earth_radius_km = 6371.0;
958 using land_mask = std::vector<std::vector<std::pair<int, int>>>;
960 static land_mask read_land_(
const core::tsv_document& doc)
962 land_mask mask(land_rows);
963 for (
const auto& row : doc.rows())
965 const auto i = row.get<
int>(
"row");
966 if (i < 0 || i >= land_rows)
throw row.error(
"row",
"must be 0 to 359");
967 for (
const auto run : row.list(
"columns"))
969 const auto dash = run.find(
'-');
970 const auto a = core::detail::parse_number<int>(run.substr(0, dash));
971 const auto b = dash == std::string_view::npos ? a : core::detail::parse_number<int>(run.substr(dash + 1));
972 if (!a || !b || *a < 0 || *b < *a || *b >= land_columns)
974 throw row.error(
"columns",
"must be runs such as 12-40 of columns 0 to 719");
976 mask[
static_cast<std::size_t
>(i)].emplace_back(*a, *b);
978 std::ranges::sort(mask[
static_cast<std::size_t
>(i)]);
985 static std::size_t history_block_of_(
double latitude,
double longitude)
987 const auto row = std::clamp(
static_cast<int>(std::floor((latitude + 90.0) / 2.0)), 0, history_rows - 1);
989 std::clamp(
static_cast<int>(std::floor((longitude + 180.0) / 2.0)), 0, history_columns - 1);
990 return (
static_cast<std::size_t
>(row) * history_columns) +
static_cast<std::size_t
>(column);
993 void read_history_(
const core::tsv_document& doc)
995 std::vector<std::int32_t> block(
static_cast<std::size_t
>(history_rows * history_columns), -1);
996 std::vector<history_month> months;
997 for (
const auto& row : doc.rows())
1000 const auto key = row.get<std::string>(
"key");
1001 const auto comma = key.find(
',');
1002 const auto lat = core::detail::parse_number<double>(std::string_view{key}.substr(0, comma));
1003 const auto lon = comma == std::string::npos
1005 : core::detail::parse_number<double>(std::string_view{key}.substr(comma + 1));
1006 if (!lat || !lon || outside_(*lat, -90.0, 90.0) || outside_(*lon, -180.0, 180.0))
1008 throw row.error(
"key",
"must be a block centre such as 49.00,3.00");
1010 if (row.get<
int>(
"first_year") != history_first_year)
1012 throw row.error(
"first_year", std::format(
"must be {}", history_first_year));
1014 auto& slot = block[history_block_of_(*lat, *lon)];
1015 if (slot >= 0)
throw row.error(
"key",
"repeats a block");
1016 slot =
static_cast<std::int32_t
>(months.size());
1017 months.resize(months.size() + history_months);
1018 const std::array<std::pair<std::string_view, std::pair<double, double>>, 3> columns{
1019 {{
"tmean", {-40.0, 40.0}}, {
"precipitation", {0.0, 100000.0}}, {
"wet_days", {-31.0, 31.0}}}};
1020 for (std::size_t v = 0; v < columns.size(); ++v)
1022 const auto& [name, range] = columns.at(v);
1023 const auto items = row.list(name);
1024 if (items.size() !=
static_cast<std::size_t
>(history_months))
1026 throw row.error(name, std::format(
"must hold {} values separated by ';'", history_months));
1028 for (std::size_t m = 0; m < items.size(); ++m)
1030 const auto x = core::detail::parse_number<double>(items[m]);
1031 if (!x || outside_(*x, range.first, range.second))
1033 throw row.error(name, std::format(
"values must be {} to {}", range.first, range.second));
1035 months[
static_cast<std::size_t
>(slot) + m].at(v) =
static_cast<float>(*x);
1039 _history_block = std::move(block);
1040 _history = std::move(months);
1041 _history_hash = doc.content_hash();
1044 static core::table_schema schema_()
1046 return {.key = {
"key"},
1047 .required = {
"latitude",
"longitude",
"tmean",
"dtr",
"precipitation",
"wet_days"},
1048 .optional = {
"elevation",
"area",
"humidity",
"cloud",
"wind"}};
1052 static bool outside_(
double x,
double lo,
double hi) {
return !std::isfinite(x) || x < lo || x > hi; }
1057 using months = std::array<double, 12>;
1061 climate_normals normals;
1063 std::array<double, 3> xyz{};
1065 months tmean{}, dtr{}, wet{}, amount{}, humidity{}, cloud{}, wind{};
1066 double annual_range{0.0};
1067 double annual_mean{0.0};
1069 static months read_months_(
const core::tsv_row& row, std::string_view column,
double lo,
double hi)
1071 const auto items = row.list(column);
1072 if (items.size() != 12)
throw row.error(column,
"must hold 12 values separated by ';'");
1074 for (std::size_t i = 0; i < 12; ++i)
1076 const auto v = core::detail::parse_number<double>(items[i]);
1077 if (!v || !std::isfinite(*v))
throw row.error(column,
"holds a value that is not a number");
1078 if (*v < lo || *v > hi)
1080 throw row.error(column, std::format(
"values must be {} to {}", lo, hi));
1087 static months constant_(
double v)
1095 static cell parse(
const core::tsv_row& row)
1098 auto& n = c.normals;
1099 n.cell = row.get<std::string>(
"key");
1100 n.latitude = row.get<
double>(
"latitude");
1101 n.longitude = row.get<
double>(
"longitude");
1102 if (outside_(n.latitude, -90.0, 90.0))
throw row.error(
"latitude",
"must be -90 to 90");
1103 if (outside_(n.longitude, -180.0, 180.0))
throw row.error(
"longitude",
"must be -180 to 180");
1104 n.elevation = row.get_or<
double>(
"elevation", 0.0);
1105 if (outside_(n.elevation, -500.0, 9000.0))
throw row.error(
"elevation",
"must be -500 to 9000");
1106 c.area = row.get_or<
double>(
"area", 0.0);
1107 if (outside_(c.area, 0.0, std::numeric_limits<double>::max()))
1109 throw row.error(
"area",
"must not be negative");
1111 n.tmean = read_months_(row,
"tmean", -80.0, 60.0);
1112 n.dtr = read_months_(row,
"dtr", 0.0, 50.0);
1113 n.precipitation = read_months_(row,
"precipitation", 0.0, 5000.0);
1114 n.wet_days = read_months_(row,
"wet_days", 0.0, 31.0);
1115 n.humidity = row.has(
"humidity") ? read_months_(row,
"humidity", 0.0, 100.0) : constant_(70.0);
1116 n.cloud = row.has(
"cloud") ? read_months_(row,
"cloud", 0.0, 100.0) : constant_(50.0);
1117 n.wind = row.has(
"wind") ? read_months_(row,
"wind", 0.0, 60.0) : constant_(3.5);
1125 double tmean, dtr, wet, amount, humidity, cloud, wind;
1135 std::array<double, 2> e{};
1138 core::layered_table _table{schema_()};
1139 std::vector<cell> _cells;
1140 std::vector<std::uint32_t> _tree;
1141 core::weighted_table<std::size_t> _area;
1144 std::vector<std::int32_t> _history_block;
1145 std::vector<history_month> _history;
1146 std::uint64_t _history_hash{0};
1148 core::rng _engine{core::rng::from_entropy()};
1150 struct auto_probe_tag
1154 explicit wtg(auto_probe_tag )
1156 if (!
load(dataset::lite)) (void)
load(dataset::full);
1161 static std::span<const core::embedded_file> embedded_files_()
1163#ifdef DASMIG_WEATHER_EMBED
1164 return embedded::weathergen::files;
1170 std::uint64_t draw_seed_() {
return _engine(); }
1172 void require_data_()
const
1174 if (_cells.empty())
throw std::runtime_error(
"No weather data loaded. Call load() first.");
1177 static void check_point_(
double latitude,
double longitude)
1179 if (outside_(latitude, -90.0, 90.0))
1181 throw std::invalid_argument(
"weather-generator: latitude must be -90 to 90");
1183 if (outside_(longitude, -180.0, 180.0))
1185 throw std::invalid_argument(
"weather-generator: longitude must be -180 to 180");
1190 static double km_(
const std::array<double, 3>& a,
const std::array<double, 3>& b)
1192 constexpr double rad = std::numbers::pi / 180.0;
1193 return 2.0 * earth_radius_km * core::math::asin_deg(std::min(1.0, std::sqrt(distance2_(a, b)) / 2.0)) * rad;
1196 static std::array<double, 3> unit_vector_(
double latitude,
double longitude)
1198 const auto [slat, clat] = core::math::sin_cos_deg(latitude);
1199 const auto [slon, clon] = core::math::sin_cos_deg(longitude);
1200 return {clat * clon, clat * slon, slat};
1203 static double distance2_(
const std::array<double, 3>& a,
const std::array<double, 3>& b)
1205 const double dx = a[0] - b[0];
1206 const double dy = a[1] - b[1];
1207 const double dz = a[2] - b[2];
1208 return core::math::fenced(dx * dx) + core::math::fenced(dy * dy) + core::math::fenced(dz * dz);
1214 static months controls_(
const months& v)
1217 for (
int it = 0; it < 40; ++it)
1220 for (std::size_t m = 0; m < 12; ++m)
1222 next.at(m) = ((8.0 * v.at(m)) - c.at((m + 11) % 12) - c.at((m + 1) % 12)) / 6.0;
1231 std::vector<cell> cells;
1232 core::weighted_table<std::size_t> area;
1233 cells.reserve(_table.size());
1234 for (
const auto& row : _table.rows())
1236 cell c = cell::parse(row);
1237 const auto& n = c.normals;
1238 c.xyz = unit_vector_(n.latitude, n.longitude);
1243 double total_wet = 0.0;
1244 for (std::size_t m = 0; m < 12; ++m)
1246 const auto ml =
static_cast<double>(month_length_(2001,
static_cast<unsigned>(m + 1)));
1247 wet.at(m) = std::min(n.wet_days.at(m), ml) / ml;
1248 total += n.precipitation.at(m);
1249 total_wet += n.wet_days.at(m);
1250 cloud.at(m) = n.cloud.at(m) / 100.0;
1252 const double annual_amount = total_wet > 0.0 ? total / total_wet : 0.0;
1253 for (std::size_t m = 0; m < 12; ++m)
1255 amount.at(m) = n.wet_days.at(m) >= 0.1 ? n.precipitation.at(m) / n.wet_days.at(m) : annual_amount;
1257 c.tmean = controls_(n.tmean);
1258 c.dtr = controls_(n.dtr);
1259 c.wet = controls_(wet);
1260 c.amount = controls_(amount);
1261 c.humidity = controls_(n.humidity);
1262 c.cloud = controls_(cloud);
1263 c.wind = controls_(n.wind);
1264 const auto [lo, hi] = std::ranges::minmax(n.tmean);
1265 c.annual_range = hi - lo;
1267 for (
const double t : n.tmean) sum += t;
1268 c.annual_mean = sum / 12.0;
1269 cells.push_back(std::move(c));
1270 if (cells.back().area > 0.0) area.add(cells.size() - 1, cells.back().area);
1272 _cells = std::move(cells);
1273 _area = std::move(area);
1281 _tree.resize(_cells.size());
1282 for (std::size_t i = 0; i < _tree.size(); ++i) _tree[i] =
static_cast<std::uint32_t
>(i);
1283 build_tree_(0, _tree.size(), 0);
1288 void build_tree_(std::size_t lo, std::size_t hi,
int axis)
1290 if (hi - lo <= 1)
return;
1291 const std::size_t mid = lo + ((hi - lo) / 2);
1292 const auto a =
static_cast<std::size_t
>(axis);
1293 std::nth_element(_tree.begin() +
static_cast<std::ptrdiff_t
>(lo), _tree.begin() +
static_cast<std::ptrdiff_t
>(mid),
1294 _tree.begin() +
static_cast<std::ptrdiff_t
>(hi),
1295 [&](std::uint32_t x, std::uint32_t y)
1297 const double cx = _cells[x].xyz.at(a);
1298 const double cy = _cells[y].xyz.at(a);
1299 return cx < cy || (cx == cy && x < y);
1301 build_tree_(lo, mid, (axis + 1) % 3);
1302 build_tree_(mid + 1, hi, (axis + 1) % 3);
1307 [[nodiscard]] std::size_t nearest_(
const std::array<double, 3>& q)
const
1309 std::size_t best = _cells.size();
1310 double best_d = std::numeric_limits<double>::infinity();
1311 search_(q, 0, _tree.size(), 0, best, best_d);
1316 void search_(
const std::array<double, 3>& q, std::size_t lo, std::size_t hi,
int axis, std::size_t& best,
1317 double& best_d)
const
1319 if (lo >= hi)
return;
1320 const std::size_t mid = lo + ((hi - lo) / 2);
1321 const std::uint32_t i = _tree[mid];
1322 const double d = distance2_(q, _cells[i].xyz);
1323 if (d < best_d || (d == best_d && i < best))
1328 const auto a =
static_cast<std::size_t
>(axis);
1329 const double diff = q.at(a) - _cells[i].xyz.at(a);
1330 const int next = (axis + 1) % 3;
1331 const bool left_first = diff < 0.0;
1332 if (left_first) search_(q, lo, mid, next, best, best_d);
1333 else search_(q, mid + 1, hi, next, best, best_d);
1334 if (core::math::fenced(diff * diff) <= best_d)
1336 if (left_first) search_(q, mid + 1, hi, next, best, best_d);
1337 else search_(q, lo, mid, next, best, best_d);
1343 static unsigned month_length_(
int year,
unsigned month)
1345 return static_cast<unsigned>(
1346 std::chrono::year_month_day_last{std::chrono::year{year} / std::chrono::month{month} / std::chrono::last}
1352 static std::array<double, 2> month_anomaly_(std::uint64_t month_seed, std::int64_t month_index)
1354 core::rng r{core::rng::child_seed_of(month_seed,
static_cast<std::uint64_t
>(month_index))};
1355 const double t = core::normal(r, 0.0, 1.0);
1356 return {t, core::normal(r, 0.0, 1.0)};
1361 class month_anomalies
1364 explicit month_anomalies(std::uint64_t seed) : _seed{seed} {}
1366 std::array<double, 2> operator()(std::int64_t index)
1368 if (_values.empty()) _first = index;
1369 while (_first +
static_cast<std::int64_t
>(_values.size()) <= index)
1371 _values.push_back(month_anomaly_(_seed, _first +
static_cast<std::int64_t
>(_values.size())));
1373 return _values[
static_cast<std::size_t
>(index - _first)];
1377 std::uint64_t _seed;
1378 std::int64_t _first{0};
1379 std::vector<std::array<double, 2>> _values;
1391 month_history(
const cell& c, std::span<const history_month> months)
1392 : _wet_days{c.normals.wet_days}, _months{months}
1396 std::array<double, 3> operator()(std::int64_t index)
1398 if (_values.empty()) _first = index;
1399 while (_first +
static_cast<std::int64_t
>(_values.size()) <= index)
1401 _values.push_back(control_(_first +
static_cast<std::int64_t
>(_values.size())));
1403 return _values[
static_cast<std::size_t
>(index - _first)];
1407 static constexpr int reach = 12;
1408 static constexpr double root = -0.17157287525380990;
1409 static constexpr double gain = std::numbers::sqrt2;
1412 [[nodiscard]] std::array<double, 3> month_(std::int64_t index)
const
1414 const std::int64_t first =
static_cast<std::int64_t
>(history_first_year) * 12;
1415 if (_months.empty() || index < first || index >= first + history_months)
return {0.0, 0.0, 0.0};
1416 const auto& h = _months[
static_cast<std::size_t
>(index - first)];
1417 const auto m =
static_cast<std::size_t
>(((index % 12) + 12) % 12);
1418 const auto year =
static_cast<int>(index / 12);
1419 const double normal_length = month_length_(2001,
static_cast<unsigned>(m + 1));
1420 const double length = month_length_(year,
static_cast<unsigned>(m + 1));
1421 const double wet = _wet_days.at(m);
1422 const double wet_now = std::max(wet +
static_cast<double>(h[2]), 0.0);
1423 const auto log_odds = [](
double f)
1425 const double g = std::clamp(f, 0.005, 0.995);
1426 return core::math::log(g / (1.0 - g));
1428 const double odds = log_odds(wet_now / length) - log_odds(std::min(wet, normal_length) / normal_length);
1431 double amount = core::math::log(std::max(
static_cast<double>(h[1]) / 100.0, 0.05));
1432 if (wet >= 0.1 && wet_now >= 0.1) amount -= core::math::log(wet_now / wet);
1433 return {
static_cast<double>(h[0]), odds, std::clamp(amount, -3.0, 3.0)};
1436 [[nodiscard]] std::array<double, 3> control_(std::int64_t index)
const
1438 std::array<double, 3> out{};
1439 double weight = gain;
1440 for (
int k = 0; k <= reach; ++k)
1442 const auto a = month_(index + k);
1443 const auto b = k == 0 ? std::array<double, 3>{} : month_(index - k);
1444 for (std::size_t v = 0; v < 3; ++v) out.at(v) += core::math::fenced(weight * (a.at(v) + b.at(v)));
1451 std::span<const history_month> _months;
1452 std::int64_t _first{0};
1453 std::vector<std::array<double, 3>> _values;
1459 static day_params params_(
const cell& c, std::chrono::year_month_day ymd, month_anomalies& anomalies,
1460 month_history* history)
1462 const auto month =
static_cast<unsigned>(ymd.month());
1463 const int year =
static_cast<int>(ymd.year());
1464 const auto length =
static_cast<double>(month_length_(year, month));
1466 const double pos =
static_cast<double>(month - 1) +
1467 ((
static_cast<double>(
static_cast<unsigned>(ymd.day())) - 0.5) / length) - 0.5;
1468 const double lower = std::floor(pos);
1469 const double t = pos - lower;
1470 const std::int64_t index = (
static_cast<std::int64_t
>(year) * 12) +
static_cast<std::int64_t
>(lower);
1471 const auto i =
static_cast<std::size_t
>(((index % 12) + 12) % 12);
1472 const std::size_t j = (i + 1) % 12;
1473 const auto lerp = [&](
const months& v) {
return v.at(i) + core::math::fenced(t * (v.at(j) - v.at(i))); };
1475 if (history !=
nullptr)
return historical_params_(c, *history, index, t, lerp);
1477 const auto a = anomalies(index);
1478 const auto b = anomalies(index + 1);
1479 const double za = a[0] + core::math::fenced(t * (b[0] - a[0]));
1480 const double zp = a[1] + core::math::fenced(t * (b[1] - a[1]));
1482 const double v = core::math::fenced((1.0 - t) * (1.0 - t)) + core::math::fenced(t * t);
1491 const auto factor = [&](
double sigma,
double correction)
1492 {
return core::math::exp(core::math::fenced(sigma * zp) - core::math::fenced(0.5 * correction * v)); };
1493 constexpr double sf = model::wet_month;
1494 constexpr double sa = model::amount_month;
1495 const double f = std::clamp(lerp(c.wet), 0.0, 1.0);
1496 const double odds = factor(sf, sf * sf * (1.0 - (2.0 * f)));
1498 const double wet_odds = core::math::fenced(f * odds);
1501 p.tmean = lerp(c.tmean);
1502 p.dtr = std::max(lerp(c.dtr), 0.0);
1503 p.wet = std::min(wet_odds / (wet_odds + (1.0 - f)), 0.995);
1505 std::max(lerp(c.amount) * factor(sa, (sa * sa) + core::math::fenced(2.0 * sf * sa * (1.0 - f))), 0.1);
1506 p.humidity = std::clamp(lerp(c.humidity), 1.0, 100.0);
1507 p.cloud = std::clamp(lerp(c.cloud), 0.0, 1.0);
1508 p.wind = std::max(lerp(c.wind), 0.0);
1510 p.residual = std::sqrt(1.0 - core::math::fenced(model::temperature_month * model::temperature_month * v));
1517 template <
typename Lerp>
1519 static day_params historical_params_(
const cell& c, month_history& history, std::int64_t index,
double t,
1522 const auto a = history(index);
1523 const auto b = history(index + 1);
1524 const auto at = [&](std::size_t k) {
return a.at(k) + core::math::fenced(t * (b.at(k) - a.at(k))); };
1525 const double v = core::math::fenced((1.0 - t) * (1.0 - t)) + core::math::fenced(t * t);
1526 const double f = std::clamp(lerp(c.wet), 0.0, 1.0);
1527 const double wet_odds = core::math::fenced(f * core::math::exp(at(1)));
1530 p.tmean = lerp(c.tmean) + at(0);
1531 p.dtr = std::max(lerp(c.dtr), 0.0);
1532 p.wet = std::min(wet_odds / (wet_odds + (1.0 - f)), 0.995);
1533 p.amount = std::max(lerp(c.amount) * core::math::exp(at(2)), 0.1);
1534 p.humidity = std::clamp(lerp(c.humidity), 1.0, 100.0);
1535 p.cloud = std::clamp(lerp(c.cloud), 0.0, 1.0);
1536 p.wind = std::max(lerp(c.wind), 0.0);
1538 p.residual = std::sqrt(1.0 - core::math::fenced(model::temperature_month * model::temperature_month * v));
1542 static double persistence_(
double f)
1544 return std::clamp(model::persistence0 + core::math::fenced(model::persistence1 * f), 0.0, 0.9);
1548 static std::pair<double, double> transitions_(
double f)
1550 const double d = persistence_(f);
1551 const double p01 = core::math::fenced(f * (1.0 - d));
1552 return {p01, p01 + d};
1555 static double logistic_(
double z) {
return 1.0 / (1.0 + core::math::exp(-z)); }
1557 static double spread_(
const std::array<double, 7>& c,
const cell& cl,
const day_params& p)
1559 const double season = (p.tmean - cl.annual_mean) / std::max(cl.annual_range / 2.0, 0.5);
1560 const std::array<double, 7> x{1.0, cl.annual_range, p.dtr, p.wet, season, season * cl.annual_range,
1561 std::abs(cl.normals.latitude)};
1563 for (std::size_t i = 0; i < x.size(); ++i) s += core::math::fenced(c.at(i) * x.at(i));
1564 return std::clamp(s, model::spread_min, model::spread_max);
1570 static const std::array<std::array<double, 4>, model::window>& impulse_()
1572 static const auto table = []
1574 std::array<std::array<double, 4>, model::window> out{};
1575 std::array<double, 4> m = model::b;
1579 const auto& a = model::a;
1580 m = {core::math::fenced(a[0] * m[0]) + core::math::fenced(a[1] * m[2]),
1581 core::math::fenced(a[0] * m[1]) + core::math::fenced(a[1] * m[3]),
1582 core::math::fenced(a[2] * m[0]) + core::math::fenced(a[3] * m[2]),
1583 core::math::fenced(a[2] * m[1]) + core::math::fenced(a[3] * m[3])};
1592 double daylight_minutes{};
1593 std::optional<double> rise;
1594 std::optional<double> set;
1601 static sun sun_(std::chrono::sys_days day,
double latitude,
double longitude)
1603 using core::math::fenced;
1604 const double days =
static_cast<double>(day.time_since_epoch().count());
1606 const double jd = days + 2440587.5 + 0.5 - (longitude / 360.0);
1607 const double t = (jd - 2451545.0) / 36525.0;
1608 const auto reduce = [](
double x) {
return x - (360.0 * std::floor(x / 360.0)); };
1609 const double l0 = reduce(280.46646 + fenced(t * (36000.76983 + fenced(t * 0.0003032))));
1610 const double m = reduce(357.52911 + fenced(t * (35999.05029 - fenced(0.0001537 * t))));
1611 const double e = 0.016708634 - fenced(t * (0.000042037 + fenced(0.0000001267 * t)));
1612 const double c = fenced(core::math::sin_deg(m) * (1.914602 - fenced(t * (0.004817 + fenced(0.000014 * t))))) +
1613 fenced(core::math::sin_deg(2.0 * m) * (0.019993 - fenced(0.000101 * t))) +
1614 fenced(core::math::sin_deg(3.0 * m) * 0.000289);
1615 const double omega = reduce(125.04 - fenced(1934.136 * t));
1616 const double lambda = l0 + c - 0.00569 - fenced(0.00478 * core::math::sin_deg(omega));
1618 23.0 + ((26.0 + ((21.448 - fenced(t * (46.815 + fenced(t * (0.00059 - fenced(t * 0.001813)))))) / 60.0)) /
1620 const double eps = eps0 + fenced(0.00256 * core::math::cos_deg(omega));
1621 const double decl = core::math::asin_deg(fenced(core::math::sin_deg(eps) * core::math::sin_deg(lambda)));
1622 const double ty = core::math::sin_deg(eps / 2.0) / core::math::cos_deg(eps / 2.0);
1623 const double y = fenced(ty * ty);
1624 constexpr double deg = 180.0 / std::numbers::pi;
1625 const double eq_time =
1627 (fenced(y * core::math::sin_deg(2.0 * l0)) - fenced(2.0 * e * core::math::sin_deg(m)) +
1628 fenced(4.0 * e * y * core::math::sin_deg(m) * core::math::cos_deg(2.0 * l0)) - fenced(0.5 * y * y * core::math::sin_deg(4.0 * l0)) -
1629 fenced(1.25 * e * e * core::math::sin_deg(2.0 * m)));
1630 const double noon = 720.0 - fenced(4.0 * longitude) - eq_time;
1632 const double denominator = fenced(core::math::cos_deg(latitude) * core::math::cos_deg(decl));
1633 const double numerator = core::math::cos_deg(90.833) - fenced(core::math::sin_deg(latitude) * core::math::sin_deg(decl));
1636 if (std::abs(denominator) < 1e-12 || numerator >= denominator || numerator <= -denominator)
1638 const bool polar_day = (latitude > 0.0) == (decl > 0.0);
1639 return {.daylight_minutes = polar_day ? 1440.0 : 0.0, .rise = std::nullopt, .set = std::nullopt};
1641 const double hour_angle = core::math::acos_deg(numerator / denominator);
1642 return {.daylight_minutes = 8.0 * hour_angle, .rise = noon - (4.0 * hour_angle), .set = noon + (4.0 * hour_angle)};
1648 static double round_to_(
double x,
double per) {
return (std::round(x * per) / per) + 0.0; }
1650 static void check_request_(
const weather_request& request)
1652 if (!request.date.ok())
throw std::invalid_argument(
"weather-generator: date is not a valid date");
1653 if (request.latitude.has_value() != request.longitude.has_value())
1655 throw std::invalid_argument(
"weather-generator: set latitude and longitude together");
1657 if (request.latitude) check_point_(*request.latitude, *request.longitude);
1658 if (request.elevation && outside_(*request.elevation, -500.0, 9000.0))
1660 throw std::invalid_argument(
"weather-generator: elevation must be -500 to 9000 metres");
1662 if (outside_(request.warming, -10.0, 10.0))
1664 throw std::invalid_argument(
"weather-generator: warming must be -10 to 10 °C");
1666 if (request.units != unit_system::metric && request.units != unit_system::imperial)
1668 throw std::invalid_argument(
"weather-generator: units must be metric or imperial");
1676 std::size_t cell{0};
1677 double latitude{0.0};
1678 double longitude{0.0};
1679 double distance_km{0.0};
1686 [[nodiscard]]
double land_km_(
double latitude,
double longitude,
const std::array<double, 3>& q)
const
1688 if (_land.empty())
return std::numeric_limits<double>::infinity();
1689 const auto row_of = [](
double lat) {
return std::clamp(
static_cast<int>(std::floor((lat + 90.0) * 2.0)), 0, land_rows - 1); };
1690 const auto column_of = [](
double lon)
1691 {
return ((
static_cast<int>(std::floor((lon + 180.0) * 2.0)) % land_columns) + land_columns) % land_columns; };
1692 const auto is_land = [&](
int i,
int j)
1694 for (
const auto& [a, b] : _land[static_cast<std::size_t>(i)])
1696 if (j < a)
return false;
1697 if (j <= b)
return true;
1701 const int i0 = row_of(latitude);
1702 const int j0 = column_of(longitude);
1703 if (is_land(i0, j0))
return 0.0;
1705 double best = std::numeric_limits<double>::infinity();
1706 for (
int i = std::max(0, i0 - 2); i <= std::min(land_rows - 1, i0 + 2); ++i)
1708 const double south = -90.0 + (0.5 * i);
1709 const double north = south + 0.5;
1710 const double nearest_lat = std::clamp(latitude, south, north);
1713 const double widest = std::max(std::abs(south), std::abs(north));
1714 const double km_per_degree = std::max(1e-6, core::math::cos_deg(std::min(widest, 89.999)) * 111.19);
1715 const int reach = std::min(land_columns / 2,
static_cast<int>(sea_limit_km / km_per_degree * 2.0) + 2);
1716 for (
int dj = -reach; dj <= reach; ++dj)
1718 const int j = (((j0 + dj) % land_columns) + land_columns) % land_columns;
1719 if (!is_land(i, j))
continue;
1720 const double west = -180.0 + (0.5 * j);
1722 double offset = longitude - west;
1723 offset -= 360.0 * std::floor(offset / 360.0);
1724 double nearest_lon = west;
1725 if (offset <= 0.5) nearest_lon = longitude;
1726 else if (offset < 180.25) nearest_lon = west + 0.5;
1727 best = std::min(best, km_(q, unit_vector_(nearest_lat, nearest_lon)));
1735 [[nodiscard]] place locate_(
double latitude,
double longitude)
const
1737 const auto q = unit_vector_(latitude, longitude);
1738 const std::size_t index = nearest_(q);
1739 const double km = std::min(land_km_(latitude, longitude, q), km_(q, _cells[index].xyz));
1740 if (!(km <= sea_limit_km))
1742 throw std::invalid_argument(std::format(
1743 "weather-generator: ({}, {}) is {} km from land; the data covers land only, without Antarctica",
1744 latitude, longitude, std::isfinite(km) ? std::format(
"{:.0f}", km) : std::string{
"far"}));
1746 return {.cell = index, .latitude = latitude, .longitude = longitude, .distance_km = km};
1749 [[nodiscard]] place place_(
const weather_request& request,
const core::rng& call)
const
1751 if (request.latitude)
return locate_(*request.latitude, *request.longitude);
1752 if (_area.empty())
throw std::invalid_argument(
"weather-generator: no cell has an area to draw a place from");
1753 auto r = call.split(
"place");
1754 const std::size_t index = _area.pick(r);
1755 return {.cell = index,
1756 .latitude = _cells[index].normals.latitude,
1757 .longitude = _cells[index].normals.longitude,
1758 .distance_km = 0.0};
1761 static std::uint64_t day_index_(std::chrono::sys_days day)
1763 return static_cast<std::uint64_t
>(
static_cast<std::int64_t
>(day.time_since_epoch().count()));
1768 static bool wet_on_(
const std::vector<day_draws>& draws,
int k)
1770 const auto at = [&](
int i) ->
const day_draws& {
return draws[
static_cast<std::size_t
>(i)]; };
1773 for (; s > k - model::window && known < 0; --s)
1775 const auto [p01, p11] = transitions_(at(s).p.wet);
1776 if (at(s).u_wet < p01) known = 1;
1777 else if (at(s).u_wet >= p11) known = 0;
1784 wet = at(s).u_wet < at(s).p.wet;
1791 for (
int t = s + 1; t <= k; ++t)
1793 const auto [p01, p11] = transitions_(at(t).p.wet);
1794 wet = at(t).u_wet < (wet ? p11 : p01);
1810 static temperatures temperatures_(
const std::vector<day_draws>& draws,
int k,
bool wet,
const cell& c,
1815 const auto& impulse = impulse_();
1818 for (
int j = model::window - 1; j >= 0; --j)
1820 const auto& e = draws[
static_cast<std::size_t
>(k - j)].e;
1821 const auto& m = impulse.at(
static_cast<std::size_t
>(j));
1822 rm += core::math::fenced(m[0] * e[0]) + core::math::fenced(m[1] * e[1]);
1823 rr += core::math::fenced(m[2] * e[0]) + core::math::fenced(m[3] * e[1]);
1826 const auto& p = draws[
static_cast<std::size_t
>(k)].p;
1827 const double f = p.wet;
1828 const auto wet_shift = [&](
double share)
1829 {
return wet ? -core::math::fenced(share * p.dtr * (1.0 - f)) : core::math::fenced(share * p.dtr * f); };
1830 const double sm = spread_(model::spread_mean, c, p);
1831 const double anomaly =
1832 core::math::fenced(p.residual * rm) + core::math::fenced(model::temperature_month * p.anomaly);
1833 const double mean = p.tmean + wet_shift(model::wet_mean) + core::math::fenced(sm * anomaly) + shift;
1834 const double base_range = std::max(p.dtr + wet_shift(model::wet_range), 0.1);
1835 const double cv = spread_(model::spread_range, c, p) / base_range;
1836 const double s2 = core::math::log(1.0 + core::math::fenced(cv * cv));
1837 const double range = base_range * core::math::exp(core::math::fenced(std::sqrt(s2) * rr) - (0.5 * s2));
1838 return {.mean = mean, .tmax = mean + (0.5 * range), .tmin = mean - (0.5 * range)};
1843 static double precipitation_(weather& w,
const day_params& p,
const temperatures& t,
const core::rng& day)
1845 auto r = day.split(
"amount");
1846 const double shape = std::clamp(model::shape0 + core::math::fenced(model::shape1 * core::math::log(p.amount)),
1847 model::shape_min, model::shape_max);
1848 const double amount = std::max(core::gamma(r, shape, p.amount / shape), 0.1);
1849 auto phase = day.split(
"phase");
1850 const double snow = logistic_(model::snow0 + core::math::fenced(model::snow1 * t.mean));
1851 if (core::uniform01(phase) >= snow) w.type = precipitation_type::rain;
1852 else if (t.mean > model::sleet_above) w.type = precipitation_type::sleet;
1853 else w.type = precipitation_type::snow;
1855 w.precipitation = round_to_(amount, 10.0);
1857 if (w.type == precipitation_type::snow) w.snowfall = w.precipitation;
1858 else if (w.type == precipitation_type::sleet) w.snowfall = round_to_(amount / 2.0, 10.0);
1860 if (w.type != precipitation_type::snow)
1862 auto th = day.split(
"thunder");
1863 const auto& c = model::thunder;
1865 c[0] + core::math::fenced(c[1] * t.tmax) + core::math::fenced(c[2] * core::math::log(amount));
1866 w.thunder = core::uniform01(th) < logistic_(z);
1873 static double sky_(weather& w,
const day_params& p,
bool wet,
const core::rng& day)
1875 const double f = p.wet;
1877 const double cbar = p.cloud;
1878 const double cw = cbar + core::math::fenced(model::cloud_wet * (1.0 - cbar));
1879 const double cd = f < 1.0 ? (cbar - core::math::fenced(f * cw)) / (1.0 - f) : cbar;
1880 const double mean = std::clamp(wet ? cw : cd, 0.02, 0.98);
1881 auto r = day.split(
"cloud");
1882 constexpr double kappa = model::cloud_kappa;
1883 w.cloud_cover = round_to_(core::beta(r, mean * kappa, (1.0 - mean) * kappa), 100.0);
1886 const double hw = core::math::fenced(model::humidity_wet * (1.0 - f));
1887 const double hd = -core::math::fenced(model::humidity_wet * f);
1888 auto r = day.split(
"humidity");
1889 const double h = core::normal(r, p.humidity + (wet ? hw : hd), model::humidity_sd);
1890 w.humidity = round_to_(std::clamp(h, 5.0, 100.0), 1.0);
1893 auto r = day.split(
"wind");
1894 const double wind = core::weibull(r, model::wind_shape, p.wind / model::wind_gamma);
1895 w.wind = round_to_(wind, 10.0);
1909 std::uint64_t seed{0};
1916 [[nodiscard]] std::vector<weather> run_(
const weather_request& request,
int days, std::uint64_t call_seed)
const
1919 check_request_(request);
1920 if (request.historical)
1922 if (_history.empty())
1924 throw std::runtime_error(
"No weather history loaded. Call load_history() first.");
1926 const auto first =
static_cast<int>(request.date.year());
1927 const auto last =
static_cast<int>(
1928 std::chrono::year_month_day{std::chrono::sys_days{request.date} + std::chrono::days{days - 1}}.year());
1929 if (first < history_first_year || last > history_last_year)
1931 throw std::invalid_argument(std::format(
"weather-generator: historical days must fall in {} to {}",
1932 history_first_year, history_last_year));
1935 const int extra = request.hourly ? 1 : 0;
1936 auto sim = simulate_(request, -extra, days + (2 * extra), call_seed);
1937 std::vector<weather> out;
1938 out.reserve(
static_cast<std::size_t
>(days));
1939 const bool imperial = request.units == unit_system::imperial;
1940 for (
int k = extra; k < extra + days; ++k)
1942 const auto at =
static_cast<std::size_t
>(k);
1944 if (request.hourly) d.w.hours = hours_(sim[at - 1], d, sim[at + 1], imperial);
1945 if (imperial) to_imperial_(d);
1946 out.push_back(std::move(d.w));
1954 [[nodiscard]] std::vector<day_result> simulate_(
const weather_request& request,
int offset,
int days,
1955 std::uint64_t call_seed)
const
1957 const core::rng call{call_seed};
1958 const place where = place_(request, call);
1959 const cell& c = _cells[where.cell];
1960 const double elevation = request.elevation.value_or(c.normals.elevation);
1961 const double shift =
1962 request.warming - core::math::fenced(model::lapse_rate * (elevation - c.normals.elevation));
1966 const std::uint64_t place_seed = call.split(c.normals.cell).seed();
1967 month_anomalies anomalies{core::rng{place_seed}.split(
"month").seed()};
1968 std::optional<month_history> history;
1969 if (request.historical)
1971 const auto slot = _history_block[history_block_of_(c.normals.latitude, c.normals.longitude)];
1972 history.emplace(c, slot < 0 ? std::span<const history_month>{}
1973 : std::span{_history}.subspan(
static_cast<std::size_t
>(slot),
1976 const std::uint64_t data =
1979 std::chrono::sys_days{request.date} + std::chrono::days{offset} - std::chrono::days{model::window};
1980 const int span = model::window + days;
1982 std::vector<day_draws> draws(
static_cast<std::size_t
>(span));
1983 for (
int k = 0; k < span; ++k)
1985 const auto day = first + std::chrono::days{k};
1986 const core::rng day_rng{core::rng::child_seed_of(place_seed, day_index_(day))};
1987 auto& d = draws[
static_cast<std::size_t
>(k)];
1988 d.p = params_(c, std::chrono::year_month_day{day}, anomalies, history ? &*history :
nullptr);
1989 auto u = day_rng.split(
"wet");
1990 d.u_wet = core::uniform01(u);
1991 auto e = day_rng.split(
"temperature");
1992 d.e = {core::normal(e, 0.0, 1.0), core::normal(e, 0.0, 1.0)};
1995 std::vector<day_result> out;
1996 out.reserve(
static_cast<std::size_t
>(days));
1997 for (
int k = model::window; k < span; ++k)
1999 const auto day = first + std::chrono::days{k};
2000 const std::uint64_t day_seed = core::rng::child_seed_of(place_seed, day_index_(day));
2001 const core::rng day_rng{day_seed};
2002 const auto& p = draws[
static_cast<std::size_t
>(k)].p;
2003 const bool wet = wet_on_(draws, k);
2004 const auto t = temperatures_(draws, k, wet, c, shift);
2008 result.seed = day_seed;
2009 weather& w = result.w;
2010 w._signature = {.seed = call_seed, .data = data};
2011 w.date = std::chrono::year_month_day{day};
2012 w.cell = c.normals.cell;
2013 w.latitude = where.latitude;
2014 w.longitude = where.longitude;
2015 w.elevation = round_to_(elevation, 1.0);
2016 w.distance_km = round_to_(where.distance_km, 10.0);
2017 if (wet) result.amount = precipitation_(w, p, t, day_rng);
2018 result.wind = sky_(w, p, wet, day_rng);
2019 w.tmin = round_to_(t.tmin, 10.0);
2020 w.tmean = round_to_(t.mean, 10.0);
2021 w.tmax = round_to_(t.tmax, 10.0);
2022 w.frost = w.tmin < 0.0;
2023 const auto sun = sun_(day, where.latitude, where.longitude);
2024 w.daylight_hours = round_to_(sun.daylight_minutes / 60.0, 100.0);
2027 w.sunshine_hours = round_to_((sun.daylight_minutes / 60.0) * (1.0 - w.cloud_cover), 10.0);
2028 const auto instant = [&](
double minutes)
2029 {
return std::chrono::sys_time<std::chrono::minutes>{day} + std::chrono::minutes{std::llround(minutes)}; };
2030 if (sun.rise) w.sunrise = instant(*sun.rise);
2031 if (sun.set) w.sunset = instant(*sun.set);
2032 w.condition = condition_(w);
2034 out.push_back(std::move(result));
2041 static double fahrenheit_(
double celsius) {
return core::math::fenced(celsius * 1.8) + 32.0; }
2042 static constexpr double mm_per_inch = 25.4;
2043 static constexpr double metres_per_second_per_mph = 0.44704;
2046 static void to_imperial_(day_result& d)
2049 w.units = unit_system::imperial;
2050 w.tmin = round_to_(fahrenheit_(d.t.tmin), 10.0);
2051 w.tmean = round_to_(fahrenheit_(d.t.mean), 10.0);
2052 w.tmax = round_to_(fahrenheit_(d.t.tmax), 10.0);
2053 w.precipitation = round_to_(d.amount / mm_per_inch, 100.0);
2055 double snow_cm = 0.0;
2056 if (w.type == precipitation_type::snow) snow_cm = d.amount;
2057 if (w.type == precipitation_type::sleet) snow_cm = d.amount / 2.0;
2058 w.snowfall = round_to_(snow_cm / (mm_per_inch / 10.0), 10.0);
2059 w.wind = round_to_(d.wind / metres_per_second_per_mph, 10.0);
2068 static std::array<long long, 24> allocate_(
long long tenths,
const std::array<double, 24>& weights,
2071 std::array<long long, 24> out{};
2073 long long given = 0;
2074 for (std::size_t h = 0; h < 24; ++h)
2076 if (weights.at(h) > 0.0)
2078 total += weights.at(h);
2083 const long long rest = tenths - given;
2084 if (rest <= 0 || total <= 0.0)
return out;
2085 std::array<double, 24> remainder{};
2086 for (std::size_t h = 0; h < 24; ++h)
2088 const double share =
static_cast<double>(rest) * weights.at(h) / total;
2089 const double whole = std::floor(share);
2090 out.at(h) +=
static_cast<long long>(whole);
2091 given +=
static_cast<long long>(whole);
2092 remainder.at(h) = weights.at(h) > 0.0 ? share - whole : -1.0;
2094 for (
long long left = tenths - given; left > 0; --left)
2096 std::size_t best = 0;
2097 for (std::size_t h = 1; h < 24; ++h)
2099 if (remainder.at(h) > remainder.at(best)) best = h;
2102 remainder.at(best) = -1.0;
2108 static double saturation_(
double celsius)
2110 return 6.112 * core::math::exp((17.62 * celsius) / (243.12 + celsius));
2116 static std::vector<weather_hour> hours_(
const day_result& before,
const day_result& d,
2117 const day_result& after,
bool imperial)
2119 using core::math::fenced;
2120 const weather& w = d.w;
2122 const double solar = 4.0 * w.longitude;
2123 const auto local = [&](
double utc_minutes,
double day_shift)
2124 {
return ((utc_minutes + solar) / 60.0) + day_shift; };
2128 std::array<double, 24> temperature{};
2129 const double tn = w.tmin;
2130 const double tx = w.tmax;
2131 if (d.s.rise && d.s.set)
2133 const double rise = local(*d.s.rise, 0.0);
2134 const double set = local(*d.s.set, 0.0);
2135 const double warmest = std::max(set - model::warmest_before_sunset, (rise + set) / 2.0);
2136 const double set_before = before.s.set ? local(*before.s.set, -24.0) : set - 24.0;
2137 const double rise_after = after.s.rise ? local(*after.s.rise, 24.0) : rise + 24.0;
2138 const double next = after.w.tmin;
2139 const double dusk = tx - fenced(model::sunset_share * (tx - next));
2140 const double dusk_before = before.w.tmax - fenced(model::sunset_share * (before.w.tmax - tn));
2141 for (
int h = 0; h < 24; ++h)
2143 const auto t =
static_cast<double>(h);
2147 const double share = std::max(0.0, t - set_before) / std::max(rise - set_before, 1e-6);
2148 v = dusk_before + fenced((tn - dusk_before) * std::sqrt(std::min(share, 1.0)));
2150 else if (t <= warmest)
2152 v = tn + fenced((tx - tn) * core::math::sin_deg(90.0 * (t - rise) / std::max(warmest - rise, 1e-6)));
2156 const double angle = 90.0 + (90.0 * (t - warmest) / std::max(set - warmest, 1e-6));
2157 v = dusk + fenced((tx - dusk) * core::math::sin_deg(angle));
2161 const double share = (t - set) / std::max(rise_after - set, 1e-6);
2162 v = dusk + fenced((next - dusk) * std::sqrt(std::min(share, 1.0)));
2164 temperature.at(
static_cast<std::size_t
>(h)) = v;
2170 for (
int h = 0; h < 24; ++h)
2172 const double wave = core::math::cos_deg(15.0 * (
static_cast<double>(h) - 15.0));
2173 temperature.at(
static_cast<std::size_t
>(h)) = ((tn + tx) / 2.0) + fenced(((tx - tn) / 2.0) * wave);
2178 std::array<double, 24> daylight{};
2179 for (
int h = 0; h < 24; ++h)
2181 double up = w.daylight_hours >= 24.0 ? 1.0 : 0.0;
2182 if (d.s.rise && d.s.set)
2184 const double from = std::max(
static_cast<double>(h), local(*d.s.rise, 0.0));
2185 const double to = std::min(
static_cast<double>(h + 1), local(*d.s.set, 0.0));
2186 up = std::max(0.0, to - from);
2188 daylight.at(
static_cast<std::size_t
>(h)) = up;
2191 const core::rng stream = core::rng{d.seed}.split(
"hours");
2195 std::array<double, 24> weights{};
2196 std::array<long long, 24> water{};
2197 std::array<long long, 24>
snow{};
2198 if (w.type != precipitation_type::none)
2200 auto r = stream.split(
"precipitation");
2201 const long long tenths = std::llround(w.precipitation * 10.0);
2202 const double mean = (model::wet_hours0 + fenced(model::wet_hours1 * core::math::log(1.0 + w.precipitation))) *
2203 (w.thunder ? 0.5 : 1.0);
2204 const long long drawn = std::llround(core::gamma(r, 2.0, mean / 2.0));
2205 const long long wet_hours = std::clamp<long long>(drawn, 1, std::min<long long>(24, tenths));
2206 const bool two = core::uniform01(r) < model::two_spells && !w.thunder && wet_hours >= 2;
2207 const long long first = two ? 1 +
static_cast<long long>(core::bounded(r,
static_cast<std::uint64_t
>(wet_hours - 1)))
2209 const auto spell = [&](
long long length,
long long start)
2211 for (
long long h = start; h < start + length; ++h)
2213 const double u = 1.0 - core::uniform01(r);
2214 weights.at(
static_cast<std::size_t
>(h)) += -core::math::log(u) + 1e-9;
2217 long long start = 0;
2220 const long long centre = std::llround(core::normal(r, model::thunder_hour, 2.0));
2221 start = std::clamp<long long>(centre - (first / 2), 0, 24 - first);
2225 start = core::uniform_int<long long>(r, 0, 24 - first);
2227 spell(first, start);
2228 if (two) spell(wet_hours - first, core::uniform_int<long long>(r, 0, 24 - (wet_hours - first)));
2229 water = allocate_(tenths, weights, 1);
2230 const long long snow_tenths = std::llround(w.snowfall * 10.0);
2231 snow = w.type == precipitation_type::snow ? water : allocate_(snow_tenths, weights, 0);
2236 auto rc = stream.split(
"cloud");
2237 auto rw = stream.split(
"wind");
2238 const double lag_sd = std::sqrt(1.0 - fenced(model::hour_lag * model::hour_lag));
2239 double xc = core::normal(rc, 0.0, 1.0);
2240 double xw = core::normal(rw, 0.0, 1.0);
2241 const double cover = std::clamp(w.cloud_cover, 0.01, 0.99);
2242 const double cloud_logit = core::math::log(cover / (1.0 - cover));
2243 std::array<double, 24> cloud{};
2244 std::array<double, 24> wind{};
2245 for (std::size_t h = 0; h < 24; ++h)
2249 xc = fenced(model::hour_lag * xc) + fenced(lag_sd * core::normal(rc, 0.0, 1.0));
2250 xw = fenced(model::hour_lag * xw) + fenced(lag_sd * core::normal(rw, 0.0, 1.0));
2252 cloud.at(h) = logistic_(cloud_logit + fenced(model::cloud_noise * xc));
2253 if (water.at(h) > 0) cloud.at(h) = std::max(cloud.at(h), model::wet_hour_cloud);
2254 const double swing =
2255 1.0 + fenced(model::wind_swing *
2256 core::math::cos_deg(15.0 * (
static_cast<double>(h) - model::windiest_hour)));
2257 const double noise = core::math::exp(fenced(model::wind_noise * xw) -
2258 (0.5 * model::wind_noise * model::wind_noise));
2259 wind.at(h) = swing * noise;
2262 double wind_sum = 0.0;
2263 for (
const double x : wind) wind_sum += x;
2264 for (
double& x : wind) x = wind_sum > 0.0 ? d.wind * x * 24.0 / wind_sum : 0.0;
2269 std::array<double, 24> sunshine{};
2271 std::array<bool, 24> full{};
2272 double left = w.sunshine_hours;
2273 for (
int pass = 0; pass < 24 && left > 1e-9; ++pass)
2277 for (std::size_t h = 0; h < 24; ++h)
2279 if (full.at(h))
continue;
2280 clear += fenced(daylight.at(h) * (1.0 - cloud.at(h)));
2281 spare += daylight.at(h);
2283 if (spare <= 0.0)
break;
2284 bool capped =
false;
2285 for (std::size_t h = 0; h < 24; ++h)
2287 if (full.at(h))
continue;
2288 const double share =
clear > 0.0 ? left * fenced(daylight.at(h) * (1.0 - cloud.at(h))) /
clear
2289 : left * daylight.at(h) / spare;
2290 if (share > daylight.at(h))
2293 sunshine.at(h) = daylight.at(h);
2294 left -= daylight.at(h);
2298 if (capped)
continue;
2299 for (std::size_t h = 0; h < 24; ++h)
2301 if (full.at(h))
continue;
2302 sunshine.at(h) =
clear > 0.0 ? left * fenced(daylight.at(h) * (1.0 - cloud.at(h))) /
clear
2303 : left * daylight.at(h) / spare;
2310 const double vapour = (w.humidity / 100.0) * saturation_(w.tmean);
2312 std::vector<weather_hour> out(24);
2313 const auto midnight = std::chrono::sys_time<std::chrono::minutes>{std::chrono::sys_days{w.date}} -
2314 std::chrono::minutes{std::llround(solar)};
2315 for (std::size_t h = 0; h < 24; ++h)
2317 weather_hour& o = out.at(h);
2318 o.hour =
static_cast<int>(h);
2319 o.time = midnight + std::chrono::minutes{60 *
static_cast<long long>(h)};
2320 const double mm =
static_cast<double>(water.at(h)) / 10.0;
2321 const double cm =
static_cast<double>(
snow.at(h)) / 10.0;
2322 if (water.at(h) > 0)
2325 o.thunder = w.thunder;
2327 o.cloud_cover = round_to_(cloud.at(h), 100.0);
2328 double humidity = std::clamp(100.0 * vapour / saturation_(temperature.at(h)), 5.0, 100.0);
2329 if (water.at(h) > 0) humidity = std::max(humidity, model::wet_hour_humidity);
2330 o.humidity = round_to_(humidity, 1.0);
2331 o.daylight_hours = round_to_(daylight.at(h), 100.0);
2332 o.sunshine_hours = round_to_(sunshine.at(h), 100.0);
2333 o.condition = hour_condition_(o, mm);
2336 o.temperature = round_to_(fahrenheit_(temperature.at(h)), 10.0);
2337 o.precipitation = round_to_(mm / mm_per_inch, 100.0);
2338 o.snowfall = round_to_(cm / (mm_per_inch / 10.0), 10.0);
2339 o.wind = round_to_(wind.at(h) / metres_per_second_per_mph, 10.0);
2343 o.temperature = round_to_(temperature.at(h), 10.0);
2344 o.precipitation = mm;
2346 o.wind = round_to_(wind.at(h), 10.0);
2358 case precipitation_type::none:
2359 if (o.cloud_cover < 0.2)
return weather_condition::clear;
2360 if (o.cloud_cover < 0.5)
return weather_condition::partly_cloudy;
2361 if (o.cloud_cover < 0.85)
return weather_condition::mostly_cloudy;
2362 return weather_condition::overcast;
2363 case precipitation_type::snow:
2364 if (mm < 0.5)
return weather_condition::light_snow;
2365 if (mm < 2.0)
return weather_condition::snow;
2366 return weather_condition::heavy_snow;
2367 case precipitation_type::sleet:
2368 return o.thunder ? weather_condition::thunderstorm : weather_condition::sleet;
2369 case precipitation_type::rain:
break;
2371 if (o.thunder)
return weather_condition::thunderstorm;
2372 if (mm < 0.5)
return weather_condition::light_rain;
2373 if (mm < 4.0)
return weather_condition::rain;
2374 return weather_condition::heavy_rain;
2381 case precipitation_type::none:
2382 if (w.cloud_cover < 0.2)
return weather_condition::clear;
2383 if (w.cloud_cover < 0.5)
return weather_condition::partly_cloudy;
2384 if (w.cloud_cover < 0.85)
return weather_condition::mostly_cloudy;
2385 return weather_condition::overcast;
2386 case precipitation_type::snow:
2387 if (w.precipitation < 1.0)
return weather_condition::light_snow;
2388 if (w.precipitation < 10.0)
return weather_condition::snow;
2389 return weather_condition::heavy_snow;
2390 case precipitation_type::sleet:
2391 return w.thunder ? weather_condition::thunderstorm : weather_condition::sleet;
2392 case precipitation_type::rain:
break;
2394 if (w.thunder)
return weather_condition::thunderstorm;
2395 if (w.precipitation < 2.5)
return weather_condition::light_rain;
2396 if (w.precipitation < 10.0)
return weather_condition::rain;
2397 return weather_condition::heavy_rain;