Weather Generator 1.1.0
Daily weather for any place and date from climate normals for C++23
Loading...
Searching...
No Matches
weathergen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Diego Dasso Migotto
3
4#ifndef DASMIG_WEATHERGEN_HPP
5#define DASMIG_WEATHERGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <array>
10#include <chrono>
11#include <cmath>
12#include <cstddef>
13#include <cstdint>
14#include <filesystem>
15#include <format>
16#include <limits>
17#include <numbers>
18#include <optional>
19#include <ostream>
20#include <span>
21#include <stdexcept>
22#include <string>
23#include <string_view>
24#include <system_error>
25#include <utility>
26#include <vector>
27
28#ifdef DASMIG_WEATHER_EMBED
29#include "weathergen_data.hpp"
30#endif
31
32/// @file weathergen.hpp
33/// @brief Weather Generator library — daily weather for any place and date
34/// from climate normals for C++23.
35/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
36/// @see See doc/usage.md for the narrative tutorial and doc/data.md for the
37/// data and the model.
38
39DASMIG_CORE_REQUIRE(1, 3);
40
41namespace dasmig
42{
43
44/// @brief The form precipitation falls in.
45enum class precipitation_type : std::uint8_t
46{
47 none, ///< A dry day.
48 rain, ///< Rain.
49 sleet, ///< Rain and snow mixed.
50 snow ///< Snow.
51};
52
53/// @brief The day's weather in one word or two.
54enum class weather_condition : std::uint8_t
55{
56 clear, ///< Dry, cloud cover below 20%.
57 partly_cloudy, ///< Dry, cloud cover 20% to 50%.
58 mostly_cloudy, ///< Dry, cloud cover 50% to 85%.
59 overcast, ///< Dry, cloud cover 85% or more.
60 light_rain, ///< Rain below 2.5 mm.
61 rain, ///< Rain from 2.5 mm to 10 mm.
62 heavy_rain, ///< Rain of 10 mm or more (a heavy precipitation day).
63 thunderstorm, ///< Rain or sleet with thunder.
64 sleet, ///< Rain and snow mixed.
65 light_snow, ///< Snow below 1 mm of water (about 1 cm).
66 snow, ///< Snow from 1 mm to 10 mm of water.
67 heavy_snow ///< Snow of 10 mm of water (about 10 cm) or more.
68};
69
70/// @brief The key of a precipitation type (`none`, `rain`, `sleet`, `snow`).
71/// @param t The type.
72/// @return The key.
73[[nodiscard]] constexpr std::string_view to_string(precipitation_type t) noexcept
74{
75 switch (t)
76 {
77 case precipitation_type::rain: return "rain";
78 case precipitation_type::sleet: return "sleet";
79 case precipitation_type::snow: return "snow";
80 case precipitation_type::none: break;
81 }
82 return "none";
83}
84
85/// @brief The key of a condition (`clear`, `partly_cloudy`, ...,
86/// `heavy_snow`).
87/// @param c The condition.
88/// @return The key.
89[[nodiscard]] constexpr std::string_view to_string(weather_condition c) noexcept
90{
91 switch (c)
92 {
93 case weather_condition::partly_cloudy: return "partly_cloudy";
94 case weather_condition::mostly_cloudy: return "mostly_cloudy";
95 case weather_condition::overcast: return "overcast";
96 case weather_condition::light_rain: return "light_rain";
97 case weather_condition::rain: return "rain";
98 case weather_condition::heavy_rain: return "heavy_rain";
99 case weather_condition::thunderstorm: return "thunderstorm";
100 case weather_condition::sleet: return "sleet";
101 case weather_condition::light_snow: return "light_snow";
102 case weather_condition::snow: return "snow";
103 case weather_condition::heavy_snow: return "heavy_snow";
104 case weather_condition::clear: break;
105 }
106 return "clear";
107}
108
109/// @brief The English label of a condition (`clear`, `partly cloudy`, ...).
110/// @param c The condition.
111/// @return The label.
112[[nodiscard]] constexpr std::string_view label(weather_condition c) noexcept
113{
114 switch (c)
115 {
116 case weather_condition::partly_cloudy: return "partly cloudy";
117 case weather_condition::mostly_cloudy: return "mostly cloudy";
118 case weather_condition::overcast: return "overcast";
119 case weather_condition::light_rain: return "light rain";
120 case weather_condition::rain: return "rain";
121 case weather_condition::heavy_rain: return "heavy rain";
122 case weather_condition::thunderstorm: return "thunderstorm";
123 case weather_condition::sleet: return "sleet";
124 case weather_condition::light_snow: return "light snow";
125 case weather_condition::snow: return "snow";
126 case weather_condition::heavy_snow: return "heavy snow";
127 case weather_condition::clear: break;
128 }
129 return "clear";
130}
131
132/// @brief The units of a result's weather values.
133enum class unit_system : std::uint8_t
134{
135 metric, ///< °C, mm of precipitation, cm of snow, m/s.
136 imperial ///< °F, inches of precipitation and of snow, mph.
137};
138
139/// @brief The key of a unit system (`metric`, `imperial`).
140/// @param u The unit system.
141/// @return The key.
142[[nodiscard]] constexpr std::string_view to_string(unit_system u) noexcept
143{
144 return u == unit_system::imperial ? "imperial" : "metric";
145}
146
147/// @cond INTERNAL
148namespace weather_detail
149{
150
151// An instant as ISO 8601 UTC to the minute (`2024-01-15T07:42Z`).
152inline std::string time_string(std::chrono::sys_time<std::chrono::minutes> t)
153{
154 const auto day = std::chrono::floor<std::chrono::days>(t);
155 const std::chrono::year_month_day ymd{day};
156 const auto minutes = (t - day).count();
157 return std::format("{:04d}-{:02d}-{:02d}T{:02d}:{:02d}Z", static_cast<int>(ymd.year()),
158 static_cast<unsigned>(ymd.month()), static_cast<unsigned>(ymd.day()), minutes / 60,
159 minutes % 60);
160}
161
162} // namespace weather_detail
163/// @endcond
164
165/// @brief What a weather call asks for.
166///
167/// Every generation call takes one; `weather_request{}` asks for the
168/// defaults (a random place on land, on the default date). Choices are never
169/// stored on the generator, so one call cannot change the next.
171{
172 /// @brief The default date, 2023-07-01 (birth-generator's, user-generator's
173 /// and device-generator's reference date); the library never reads
174 /// the clock.
175 static constexpr std::chrono::year_month_day default_date{
176 std::chrono::year{2023}, std::chrono::month{7}, std::chrono::day{1}};
177
178 /// @brief Degrees north, -90 to 90. Set with longitude, or leave both
179 /// unset to draw a place on land by area.
180 std::optional<double> latitude{}; // NOLINT(readability-redundant-member-init)
181
182 /// @brief Degrees east, -180 to 180. Set with latitude.
183 std::optional<double> longitude{}; // NOLINT(readability-redundant-member-init)
184
185 /// @brief Metres above sea level, -500 to 9000. Temperatures move by the
186 /// standard lapse rate (6.5 °C per km) from the climate cell's mean
187 /// elevation; unset uses the cell's.
188 std::optional<double> elevation{}; // NOLINT(readability-redundant-member-init)
189
190 /// @brief The day (any valid date). A series starts here.
191 std::chrono::year_month_day date{default_date};
192
193 /// @brief Degrees Celsius added to every temperature, -10 to 10. The
194 /// normals are 1991-2020 means (centred on 2005); land has warmed
195 /// about 0.3 °C a decade since.
196 double warming{0.0};
197
198 /// @brief The units of the weather values (metric by default). The
199 /// place keeps metres and km either way, and `warming` and
200 /// `elevation` are always in °C and metres.
201 unit_system units{unit_system::metric};
202
203 /// @brief Also give each day's 24 hours in weather::hours. The days
204 /// themselves are the same with or without them.
205 bool hourly{false};
206
207 /// @brief Draw each month around the month as it was (CRU TS, January
208 /// 1991 to December 2020) instead of around the normals: a cold
209 /// winter is cold, a drought month is dry. Every day asked for
210 /// must fall in 1991-2020, and load_history() must have loaded
211 /// the months.
212 bool historical{false};
213};
214
215/// @brief One hour of a day's weather (weather::hours, when the request asks
216/// for them).
217///
218/// The hours are those of the place's mean solar day: hour 0 starts at local
219/// mean midnight, `4 * longitude` minutes before 00:00 UTC (east is
220/// earlier), so the day's lowest temperature comes near sunrise and the
221/// highest in the afternoon wherever the place is. In metric units the
222/// precipitation and snowfall of the 24 hours add up to the day's exactly.
223/// Values are rounded like the day's.
225{
226 std::chrono::sys_time<std::chrono::minutes> time; ///< Start of the hour, UTC.
227 int hour{}; ///< Hour of the local mean solar day, 0 to 23.
228 double temperature{}; ///< Temperature at the start of the hour, °C (°F).
229 double precipitation{}; ///< Precipitation in the hour, mm (in).
230 double snowfall{}; ///< Fresh snow in the hour, cm (in).
231 precipitation_type type{precipitation_type::none}; ///< The form it falls in (none in a dry hour).
232 bool thunder{}; ///< Thunder in the hour.
233 double cloud_cover{}; ///< Cloud cover, 0 to 1.
234 double humidity{}; ///< Relative humidity, %.
235 double wind{}; ///< Mean wind speed at 10 m, m/s (mph).
236 double daylight_hours{}; ///< The part of the hour the sun is up, 0 to 1.
237 double sunshine_hours{}; ///< Bright sunshine in the hour, 0 to 1.
238 weather_condition condition{weather_condition::clear}; ///< The hour in a word.
239
240 /// @brief Write this hour as a JSON object.
241 /// @param w Writer positioned where a value is allowed.
242 void to_json(core::json_writer& w) const
243 {
244 w.begin_object()
245 .field("time", weather_detail::time_string(time))
246 .field("hour", hour)
247 .field("condition", dasmig::to_string(condition))
248 .field("temperature", temperature)
249 .field("precipitation", precipitation)
250 .field("snowfall", snowfall)
251 .field("type", dasmig::to_string(type))
252 .field("thunder", thunder)
253 .field("cloud_cover", cloud_cover)
254 .field("humidity", humidity)
255 .field("wind", wind)
256 .field("daylight_hours", daylight_hours)
257 .field("sunshine_hours", sunshine_hours)
258 .end_object();
259 }
260};
261
262/// @brief The monthly climate normals of a place (climate_at()).
263///
264/// Twelve values per element, January to December, as loaded: the means the
265/// daily weather is drawn around.
267{
268 std::string cell; ///< The climate cell's key (its block centre, or a layer's own key).
269 double latitude{}; ///< The cell's point, degrees north (a block's land centroid).
270 double longitude{}; ///< The cell's point, degrees east.
271 double elevation{}; ///< The cell's mean elevation, metres.
272 std::array<double, 12> tmean{}; ///< Mean temperature, °C.
273 std::array<double, 12> dtr{}; ///< Mean diurnal range (Tmax - Tmin), °C.
274 std::array<double, 12> precipitation{}; ///< Precipitation, mm per month.
275 std::array<double, 12> wet_days{}; ///< Days with at least 0.1 mm.
276 std::array<double, 12> humidity{}; ///< Mean relative humidity, %.
277 std::array<double, 12> cloud{}; ///< Cloud cover, %.
278 std::array<double, 12> wind{}; ///< Mean wind speed at 10 m, m/s.
279
280 /// @brief Write the normals as a JSON object.
281 /// @param w Writer positioned where a value is allowed.
282 void to_json(core::json_writer& w) const
283 {
284 w.begin_object()
285 .field("cell", cell)
286 .field("latitude", latitude)
287 .field("longitude", longitude)
288 .field("elevation", elevation);
289 const auto months = [&](std::string_view name, const std::array<double, 12>& v)
290 {
291 w.key(name).begin_array();
292 for (const double x : v) w.value(x);
293 w.end_array();
294 };
295 months("tmean", tmean);
296 months("dtr", dtr);
297 months("precipitation", precipitation);
298 months("wet_days", wet_days);
299 months("humidity", humidity);
300 months("cloud", cloud);
301 months("wind", wind);
302 w.end_object();
303 }
304
305 /// @brief The normals as a JSON document.
306 /// @return Compact JSON.
307 [[nodiscard]] std::string to_json() const
308 {
309 core::json_writer w;
310 to_json(w);
311 return w.str();
312 }
313};
314
315/// @brief One day's weather at one place.
316///
317/// Values are rounded for output: temperatures, precipitation, snowfall and
318/// wind to 0.1 (inches of precipitation to 0.01), cloud cover to 0.01,
319/// humidity to whole percent. The units are metric unless the request asked
320/// for imperial ones. Supports implicit conversion to std::string (a
321/// one-line summary), streaming via operator<< and JSON output via
322/// to_json().
324{
325 public:
326 std::chrono::year_month_day date{}; ///< The day.
327 std::string cell; ///< Key of the climate cell drawn from.
328 double latitude{}; ///< Degrees north: the requested point, or the drawn cell's.
329 double longitude{}; ///< Degrees east: the requested point, or the drawn cell's.
330 double elevation{}; ///< Metres: the requested elevation, or the cell's.
331 double distance_km{}; ///< Distance from the point to land, km (0 on land; at most 100).
332 double tmin{}; ///< Lowest temperature, °C (°F).
333 double tmean{}; ///< Mean temperature, °C (°F): halfway between tmin and tmax.
334 double tmax{}; ///< Highest temperature, °C (°F).
335 bool frost{}; ///< tmin below 0 °C (32 °F).
336 double precipitation{}; ///< Precipitation, mm (in) of water (0 on a dry day).
337 double snowfall{}; ///< Fresh snow, cm (in): 10 cm per 10 mm of water.
338 precipitation_type type{precipitation_type::none}; ///< The form it falls in.
339 bool thunder{}; ///< Thunder heard (on rain or sleet days only).
340 double cloud_cover{}; ///< Mean cloud cover, 0 to 1.
341 double humidity{}; ///< Mean relative humidity, %.
342 double wind{}; ///< Mean wind speed at 10 m, m/s (mph).
343 double daylight_hours{}; ///< Time from sunrise to sunset, hours (0 in polar night, 24 in polar day).
344 double sunshine_hours{}; ///< Bright sunshine, hours: the daylight not covered by cloud.
345 /// @brief Sunrise, UTC (unset in polar day and polar night). It can fall
346 /// on the date before in UTC, far east of Greenwich.
347 std::optional<std::chrono::sys_time<std::chrono::minutes>> sunrise{}; // NOLINT(readability-redundant-member-init)
348 /// @brief Sunset, UTC (unset in polar day and polar night).
349 std::optional<std::chrono::sys_time<std::chrono::minutes>> sunset{}; // NOLINT(readability-redundant-member-init)
350 weather_condition condition{weather_condition::clear}; ///< The day in a word.
351 unit_system units{unit_system::metric}; ///< The units of the values above.
352 /// @brief The day's 24 hours, when the request set `hourly` (empty
353 /// otherwise).
354 std::vector<weather_hour> hours;
355
356 /// @brief Retrieve the random seed used to generate this weather.
357 /// @return The call seed; pass it to get_weather(request, seed) with the
358 /// same request and data to replay this day.
359 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
360
361 /// @brief The seed and the data this weather was generated from.
362 /// @return The replay signature; `data` is wtg::data_hash() at the
363 /// time of generation.
364 [[nodiscard]] const core::seed_signature& signature() const { return _signature; }
365
366 /// @brief ISO 8601 date string (YYYY-MM-DD).
367 /// @return The date.
368 [[nodiscard]] std::string date_string() const
369 {
370 return std::format("{:04d}-{:02d}-{:02d}", static_cast<int>(date.year()),
371 static_cast<unsigned>(date.month()), static_cast<unsigned>(date.day()));
372 }
373
374 /// @brief An instant as ISO 8601 UTC to the minute (`2024-01-15T07:42Z`).
375 /// @param t The instant.
376 /// @return The string.
377 [[nodiscard]] static std::string time_string(std::chrono::sys_time<std::chrono::minutes> t)
378 {
379 return weather_detail::time_string(t);
380 }
381
382 /// @brief Human-readable summary string.
383 /// @return For example `2023-07-01: light rain, 14.2 to 21.9 °C, 1.3 mm`
384 /// (`57.6 to 71.4 °F, 0.05 in` in imperial units).
385 [[nodiscard]] std::string to_string() const
386 {
387 const bool imperial = units == unit_system::imperial;
388 std::string s = std::format("{}: {}, {:.1f} to {:.1f} {}", date_string(), label(condition), tmin, tmax,
389 imperial ? "°F" : "°C");
390 if (type == precipitation_type::snow || type == precipitation_type::sleet)
391 {
392 s += std::format(", {:.1f} {} of snow", snowfall, imperial ? "in" : "cm");
393 }
394 else if (type == precipitation_type::rain)
395 {
396 s += imperial ? std::format(", {:.2f} in", precipitation) : std::format(", {:.1f} mm", precipitation);
397 }
398 return s;
399 }
400
401 /// @brief Write this weather as a JSON object.
402 /// @param w Writer positioned where a value is allowed.
403 void to_json(core::json_writer& w) const
404 {
405 w.begin_object().field("date", date_string());
406 // Metric days have no key, so their JSON is the same as 1.0's.
407 if (units == unit_system::imperial) w.field("units", dasmig::to_string(units));
408 w.key("place")
409 .begin_object()
410 .field("cell", cell)
411 .field("latitude", latitude)
412 .field("longitude", longitude)
413 .field("elevation", elevation)
414 .field("distance_km", distance_km)
415 .end_object()
416 .field("condition", dasmig::to_string(condition))
417 .field("tmin", tmin)
418 .field("tmean", tmean)
419 .field("tmax", tmax)
420 .field("frost", frost)
421 .field("precipitation", precipitation)
422 .field("snowfall", snowfall)
423 .field("type", dasmig::to_string(type))
424 .field("thunder", thunder)
425 .field("cloud_cover", cloud_cover)
426 .field("humidity", humidity)
427 .field("wind", wind)
428 .field("daylight_hours", daylight_hours)
429 .field("sunshine_hours", sunshine_hours);
430 const auto instant = [&](std::string_view name, const auto& t)
431 {
432 w.key(name);
433 if (t) w.value(time_string(*t));
434 else w.value(nullptr);
435 };
436 instant("sunrise", sunrise);
437 instant("sunset", sunset);
438 if (!hours.empty())
439 {
440 w.key("hours").begin_array();
441 for (const auto& h : hours) h.to_json(w);
442 w.end_array();
443 }
444 w.key("signature");
445 _signature.to_json(w);
446 w.end_object();
447 }
448
449 /// @brief This weather as a JSON document.
450 /// @return Compact JSON, for example `{"date":"2023-07-01","place":{...},
451 /// "condition":"light_rain","tmin":14.2,...,"signature":{...}}`.
452 [[nodiscard]] std::string to_json() const
453 {
454 core::json_writer w;
455 to_json(w);
456 return w.str();
457 }
458
459 /// @brief Implicit conversion to std::string.
460 /// @return Summary string.
461 operator std::string() const // NOLINT(hicpp-explicit-conversions)
462 {
463 return to_string();
464 }
465
466 /// @brief Stream the summary to an output stream.
467 /// @param os Output stream.
468 /// @param v The weather to stream.
469 /// @return The stream.
470 friend std::ostream& operator<<(std::ostream& os, const weather& v)
471 {
472 os << v.to_string();
473 return os;
474 }
475
476 private:
477 core::seed_signature _signature;
478 friend class wtg;
479};
480
481/// @brief Weather Generator: daily weather drawn around monthly climate
482/// normals.
483///
484/// A Richardson (1981) type weather generator: rain falls by a two-state
485/// Markov chain, wet-day amounts are gamma distributed, the daily mean
486/// temperature and the daily range follow the season with residuals from a
487/// lag-1 autoregressive process, months carry their own anomalies, and cloud,
488/// humidity and wind follow the wet and dry days. Its parameters come from
489/// the place's monthly normals (CRU TS 4.10, 1991-2020) and from fits on
490/// GHCN-Daily stations (doc/data.md).
491///
492/// A day's weather depends only on the seed, the place and the date: the
493/// draws of each day come from that day's own stream, and the chains are
494/// read back to the day where their state no longer matters. So the weather
495/// of 10 March is the same whether it is asked for alone or inside a series
496/// that starts in January. Output is identical on every compiler and
497/// platform (dasmig-core).
498///
499/// Can be used as a singleton via instance() or constructed independently.
500///
501/// @par Thread safety
502/// Each instance is independent. Concurrent calls to get_weather(request)
503/// on the **same** instance require external synchronisation.
504class wtg
505{
506 public:
507 /// @brief The longest series one call returns, in days (about 100 years).
508 static constexpr int max_series_days = 36600;
509
510 /// @brief Default constructor — creates an empty generator with no
511 /// data.
512 wtg() = default;
513
514 wtg(const wtg&) = delete; ///< Not copyable.
515 wtg& operator=(const wtg&) = delete; ///< Not copyable.
516 wtg(wtg&&) noexcept = default; ///< Movable.
517 wtg& operator=(wtg&&) noexcept = default; ///< Movable.
518 ~wtg() = default; ///< Destructor.
519
520 /// @brief Access the global singleton instance.
521 ///
522 /// Loads the `lite` tier (or `full` if `lite` is missing) on first
523 /// access, from compiled-in data or an auto-probed `resources/`.
524 /// @return The singleton.
525 static wtg& instance()
526 {
527 static wtg inst{auto_probe_tag{}};
528 return inst;
529 }
530
531 // -- Generation -------------------------------------------------------
532
533 /// @brief Generate one day's weather.
534 /// Use weather_request{} for the defaults.
535 /// @param request What to generate.
536 /// @return The weather.
537 /// @throws std::runtime_error If no data has been loaded, or the request
538 /// is historical and no history is loaded.
539 /// @throws std::invalid_argument If the request is invalid.
540 [[nodiscard]] weather get_weather(const weather_request& request)
541 {
542 return get_weather(request, draw_seed_());
543 }
544
545 /// @brief Generate one day's weather, deterministically.
546 /// @param request What to generate.
547 /// @param call_seed Seed; the same request, seed and data give the same
548 /// result. Does not touch the instance engine.
549 /// @return The weather; equal to day 0 of get_weather_series() with the
550 /// same request and seed.
551 /// @throws std::runtime_error If no data has been loaded.
552 /// @throws std::invalid_argument If the request is invalid.
553 [[nodiscard]] weather get_weather(const weather_request& request, std::uint64_t call_seed) const
554 {
555 return std::move(run_(request, 1, call_seed).front());
556 }
557
558 /// @brief Generate the weather of consecutive days.
559 /// @param request The place and the first day (`date`).
560 /// @param days How many days, 1 to max_series_days.
561 /// @return The days, in order.
562 /// @throws std::runtime_error If no data has been loaded.
563 /// @throws std::invalid_argument If the request or @p days is invalid.
564 [[nodiscard]] std::vector<weather> get_weather_series(const weather_request& request, int days)
565 {
566 return get_weather_series(request, days, draw_seed_());
567 }
568
569 /// @brief Generate the weather of consecutive days, deterministically.
570 ///
571 /// Day `i` equals get_weather() for the same place and seed with `date`
572 /// moved `i` days on, so series that overlap agree on the days they
573 /// share.
574 ///
575 /// @param request The place and the first day (`date`).
576 /// @param days How many days, 1 to max_series_days.
577 /// @param call_seed Seed of the series; every day carries it.
578 /// @return The days, in order.
579 /// @throws std::runtime_error If no data has been loaded.
580 /// @throws std::invalid_argument If the request or @p days is invalid.
581 [[nodiscard]] std::vector<weather> get_weather_series(const weather_request& request, int days,
582 std::uint64_t call_seed) const
583 {
584 if (days < 1 || days > max_series_days)
585 {
586 throw std::invalid_argument(std::format("weather-generator: days must be 1 to {}", max_series_days));
587 }
588 return run_(request, days, call_seed);
589 }
590
591 /// @brief The monthly normals of the climate cell nearest a point.
592 /// @param latitude Degrees north, -90 to 90.
593 /// @param longitude Degrees east, -180 to 180.
594 /// @return The cell's normals.
595 /// @throws std::runtime_error If no data has been loaded.
596 /// @throws std::invalid_argument If the point is out of range or more
597 /// than 100 km from land.
598 [[nodiscard]] climate_normals climate_at(double latitude, double longitude) const
599 {
600 require_data_();
601 check_point_(latitude, longitude);
602 return _cells[locate_(latitude, longitude).cell].normals;
603 }
604
605 // -- Batch generation -------------------------------------------------
606
607 /// @brief Generate a batch of weather results from a seed drawn like
608 /// get_weather() draws its own.
609 ///
610 /// Item `i` is `get_weather(request, core::item_seed(batch_seed,
611 /// options.first + i))` for the drawn batch seed; see the seeded
612 /// overload.
613 ///
614 /// @param request What every item is generated from.
615 /// @param options Count, first index, threads and uniqueness.
616 /// @return The results, in index order.
617 [[nodiscard]] std::vector<weather> get_weather_batch(const weather_request& request,
618 const core::batch_options& options)
619 {
620 return get_weather_batch(request, options, draw_seed_());
621 }
622
623 /// @brief Generate a batch of weather results, deterministically.
624 ///
625 /// Item `i` is exactly `get_weather(request, core::item_seed(batch_seed,
626 /// options.first + i))`, so an item replays on its own, a range can be
627 /// generated directly, two batches that follow each other equal one, and
628 /// the output does not depend on `options.threads`. With no place set,
629 /// each item draws its own place.
630 ///
631 /// @param request What every item is generated from.
632 /// @param options Count, first index, threads and uniqueness (see
633 /// core::unique_rule; the rule must be made for `weather`).
634 /// @param batch_seed Seed of the whole batch.
635 /// @return The results, in index order.
636 /// @throws std::invalid_argument If the request or options are invalid.
637 /// @throws std::runtime_error If no data has been loaded, or a unique
638 /// batch runs out of attempts.
639 [[nodiscard]] std::vector<weather> get_weather_batch(const weather_request& request,
640 const core::batch_options& options,
641 std::uint64_t batch_seed) const
642 {
643 return core::run_batch(options, batch_seed,
644 [&](std::uint64_t item_seed) { return get_weather(request, item_seed); });
645 }
646
647 /// @brief Stream a batch of weather results to a sink without holding
648 /// the whole batch.
649 ///
650 /// Generates the same items as get_weather_batch() and calls
651 /// `sink(index, item)` in index order on the calling thread; `index`
652 /// includes `options.first`.
653 ///
654 /// @tparam Sink Callable taking `(std::uint64_t, weather&)`; for example
655 /// core::jsonl_writer or core::csv_writer.
656 /// @param request What every item is generated from.
657 /// @param options Count, first index, threads and uniqueness.
658 /// @param batch_seed Seed of the whole batch.
659 /// @param sink Receives the items.
660 template <typename Sink>
661 void for_each_weather(const weather_request& request, const core::batch_options& options,
662 std::uint64_t batch_seed, Sink&& sink) const
663 {
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));
667 }
668
669 /// @brief Stream a batch of weather results with a batch seed drawn like
670 /// get_weather() draws its own.
671 /// @tparam Sink Callable taking `(std::uint64_t, weather&)`.
672 /// @param request What every item is generated from.
673 /// @param options Count, first index, threads and uniqueness.
674 /// @param sink Receives the items.
675 template <typename Sink>
676 void for_each_weather(const weather_request& request, const core::batch_options& options, Sink&& sink)
677 {
678 for_each_weather(request, options, draw_seed_(), std::forward<Sink>(sink));
679 }
680
681 // -- Data management --------------------------------------------------
682
683 /// @brief Check whether any data has been loaded.
684 /// @return True when at least one climate cell is loaded.
685 [[nodiscard]] bool has_data() const { return !_cells.empty(); }
686
687 /// @brief Return the number of loaded climate cells.
688 /// @return The count.
689 [[nodiscard]] std::size_t count() const { return _cells.size(); }
690
691 /// @brief Load data from a resource directory, replacing all data and
692 /// layers loaded before.
693 ///
694 /// Expects the directory to contain `climate.tsv`, and reads the land
695 /// mask from `land.tsv` when it is there (without one, a point must be
696 /// within 100 km of a cell). A path that is not a directory, or a
697 /// directory without `climate.tsv`, is ignored.
698 ///
699 /// @param dir Directory such as `resources/lite`.
700 /// @throws core::tsv_error If the file is malformed.
701 void load(const std::filesystem::path& dir)
702 {
703 std::error_code ec;
704 const auto file = dir / data_file_;
705 if (!std::filesystem::is_directory(dir, ec) || !std::filesystem::is_regular_file(file, ec))
706 {
707 return;
708 }
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{};
712 }
713
714 /// @brief Load a dataset tier, replacing all data and layers loaded
715 /// before.
716 ///
717 /// `lite` is a 2-degree grid (5,163 cells), `full` a half-degree grid
718 /// (67,213 cells). Uses the compiled-in copy when the library is built
719 /// with `DASMIG_WEATHER_EMBED`, otherwise searches `resources/`,
720 /// `../resources`, `weather-generator/resources` and
721 /// `../weather-generator/resources`.
722 ///
723 /// @param tier The tier.
724 /// @return False when the tier was not found.
725 /// @throws core::tsv_error If the file is malformed.
726 [[nodiscard]] bool load(dataset tier)
727 {
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{};
735 return true;
736 }
737
738 /// @brief Add a data layer from a TSV file with the `climate.tsv`
739 /// columns.
740 ///
741 /// Rows are keyed by `key`. In the default override mode a row replaces
742 /// the loaded row with the same key and new keys are added (a station's
743 /// own normals, say, which then serve the points nearest it); a row with
744 /// `disabled` set to 1 removes its key. `elevation`, `area`, `humidity`,
745 /// `cloud` and `wind` may be left out (0 m, never drawn as a random
746 /// place, 70%, 50% and 3.5 m/s).
747 ///
748 /// @param file The layer file.
749 /// @param mode How the layer combines with the data loaded before.
750 /// @return What the layer did (rows added, replaced, removed, warnings).
751 /// @throws core::tsv_error If the file cannot be read or a row is
752 /// invalid; the data is then unchanged.
753 const core::layer_info& extend(const std::filesystem::path& file,
754 core::layer_mode mode = core::layer_mode::override)
755 {
756 return extend(core::tsv_document::load(file), mode);
757 }
758
759 /// @brief Add a data layer from TSV text (for example from a
760 /// WebAssembly host).
761 /// @param text TSV with the `climate.tsv` columns.
762 /// @param source Name used in errors.
763 /// @param mode How the layer combines with the data loaded before.
764 /// @return What the layer did.
765 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
766 const core::layer_info& extend_from_string(std::string_view text, std::string source,
767 core::layer_mode mode = core::layer_mode::override)
768 {
769 return extend(core::tsv_document::parse(text, std::move(source)), mode);
770 }
771
772 /// @brief Add a data layer.
773 /// @param doc TSV with the `climate.tsv` columns.
774 /// @param mode How the layer combines with the data loaded before.
775 /// @return What the layer did.
776 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
777 const core::layer_info& extend(core::tsv_document doc, core::layer_mode mode = core::layer_mode::override)
778 {
779 const auto& info =
780 _table.extend(std::move(doc), mode, [](const core::tsv_row& row) { (void)cell::parse(row); });
781 rebuild_();
782 return info;
783 }
784
785 /// @brief Load the months of 1991-2020 for historical requests, from the
786 /// compiled-in copy or `resources/history/history.tsv`.
787 ///
788 /// Not loaded by instance() or load(): the table is large (23 MB)
789 /// and only historical requests read it. It holds, for each 2-degree
790 /// block of land, each month's temperature, precipitation and wet days
791 /// against the 1991-2020 normals (CRU TS 4.10); a cell takes the
792 /// block its point lies in.
793 ///
794 /// @return False when the file was not found.
795 /// @throws core::tsv_error If the file is malformed.
796 [[nodiscard]] bool load_history()
797 {
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;
801 read_history_(*doc);
802 return true;
803 }
804
805 /// @brief Load the months of 1991-2020 from a file.
806 /// @param file A `history.tsv`.
807 /// @throws core::tsv_error If the file cannot be read or is malformed;
808 /// the months loaded before are then kept.
809 void load_history(const std::filesystem::path& file) { read_history_(core::tsv_document::load(file)); }
810
811 /// @brief Check whether historical requests can be served.
812 /// @return True when load_history() has loaded the months.
813 [[nodiscard]] bool has_history() const { return !_history.empty(); }
814
815 /// @brief The data layers loaded, in order.
816 /// @return The layer records.
817 [[nodiscard]] const std::vector<core::layer_info>& layers() const { return _table.layers(); }
818
819 /// @brief Hash of the loaded data, recorded in each result's
820 /// signature().
821 /// @return The hash; 0 when nothing is loaded.
822 [[nodiscard]] std::uint64_t data_hash() const { return _table.data_hash(); }
823
824 private:
825 // -- The model's constants --------------------------------------------
826 //
827 // Fitted on GHCN-Daily stations, 1991-2020, by scripts/calibrate.py
828 // unless marked tuned (adjusted from the fit with scripts/validate.py)
829 // or curated; doc/data.md has the fits.
830
831 struct model
832 {
833 // Rain persistence d = P(wet | wet) - P(wet | dry) = d0 + d1 f
834 // (Geng et al. 1986 found 0.25). Tuned: the fit gives d0 = 0.277,
835 // lowered because the month anomalies also cluster wet days.
836 static constexpr double persistence0 = 0.27;
837 static constexpr double persistence1 = 0.00647;
838
839 // Gamma shape of wet-day amounts = a0 + a1 ln(mean wet-day mm).
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;
844
845 // Dry-day minus wet-day daily mean temperature and daily range, as
846 // shares of the diurnal range: wet days are a little cooler and have
847 // a much smaller range (cooler afternoons, warmer nights). The mean's
848 // is tuned from the fit's 0.032 so the generated dry-minus-wet Tmax
849 // matches the stations once the residuals and anomalies are added;
850 // the range's is the fit's.
851 static constexpr double wet_mean = 0.016;
852 static constexpr double wet_range = 0.2204;
853
854 // Day-to-day spread (°C) of the daily mean temperature and of the
855 // daily range = c . (1, annual range, dtr, f, season, season * range,
856 // |latitude|), where season is the day's mean temperature against
857 // the year's, -1 (coldest) to 1.
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;
862
863 // Lag-1 autoregression of the standardized (mean, range) residuals:
864 // x_t = A x_{t-1} + B e_t (Richardson 1981), row-major.
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};
867
868 // Weibull shape of the daily mean wind, and Gamma(1 + 1/k).
869 static constexpr double wind_shape = 2.902;
870 static constexpr double wind_gamma = 0.89171496;
871
872 // Snow on a wet day: logit p = s0 + s1 T, T the daily mean
873 // temperature (p = 0.5 at 0.76 °C). Curated: snow above sleet_above
874 // falls as sleet.
875 static constexpr double snow0 = 0.4632;
876 static constexpr double snow1 = -0.6058;
877 static constexpr double sleet_above = 1.0;
878
879 // Thunder on a rain or sleet day: logit p = t0 + t1 Tmax + t2 ln(mm)
880 // (fitted on US stations, whose reports are consistent).
881 static constexpr std::array<double, 3> thunder{-5.0964, 0.1213, 0.3431};
882
883 // Curated (doc/data.md): wet days have
884 // cloud_wet of the clear sky left covered on top of the mean, and
885 // daily cover is Beta with concentration cloud_kappa.
886 static constexpr double cloud_wet = 0.6;
887 static constexpr double cloud_kappa = 4.0;
888 // Curated: wet days are humidity_wet points more humid than dry
889 // days; daily humidity has a spread of humidity_sd points.
890 static constexpr double humidity_wet = 10.0;
891 static constexpr double humidity_sd = 7.0;
892
893 // Month-to-month anomalies (tuned with scripts/validate.py so the
894 // year-to-year spread of monthly means matches the stations): the
895 // temperature anomaly's share of the daily spread, and the log
896 // spread of the odds of a wet day and of the wet-day amount, both
897 // moved by one precipitation anomaly.
898 static constexpr double temperature_month = 0.48;
899 static constexpr double wet_month = 0.32;
900 static constexpr double amount_month = 0.28;
901
902 static constexpr double lapse_rate = 0.0065; // °C per metre (the standard atmosphere)
903
904 // Hours (curated; doc/data.md). Temperature after Cesaraccio et
905 // al. (2001): a sine from tmin at sunrise up to tmax at the warmest
906 // hour, sunset - 4 h (or midway in a short day), a sine down to the
907 // sunset temperature, tmax - sunset_share (tmax - the next tmin),
908 // and a square-root fall to the next tmin at the next sunrise.
909 static constexpr double sunset_share = 0.39;
910 static constexpr double warmest_before_sunset = 4.0;
911 // Wet hours: 2 + 2.5 ln(1 + mm) on average (half that with
912 // thunder), gamma distributed with shape 2, in one spell or, with
913 // two_spells, two.
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; // a thunder spell is centred on 15:00 local
918 // Wind: a 25% swing peaking at 14:00 local, times log-normal noise;
919 // cloud: logit noise around the day's cover. Both lag-1 correlated.
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;
927
928 // Days read back: the temperature residual sums this many days of
929 // innovations (A^window is below 1e-7), and the rain chain looks
930 // back at most this far for the day its state stops mattering.
931 static constexpr int window = 48;
932 };
933
934 // -- Internal data structures -----------------------------------------
935
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";
939
940 // The months of history.tsv: 2-degree blocks (90 rows of 180), each with
941 // history_months months from January of history_first_year.
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;
948 // Per month: temperature anomaly (°C), precipitation (% of normal) and
949 // wet-day anomaly (days).
950 using history_month = std::array<float, 3>;
951
952 // The land cells of a 0.5-degree grid, as runs of columns per row: row
953 // i is centred at -89.75 + 0.5 i, column j at -179.75 + 0.5 j.
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>>>; // per row: [first, last] columns
959
960 static land_mask read_land_(const core::tsv_document& doc)
961 {
962 land_mask mask(land_rows);
963 for (const auto& row : doc.rows())
964 {
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"))
968 {
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)
973 {
974 throw row.error("columns", "must be runs such as 12-40 of columns 0 to 719");
975 }
976 mask[static_cast<std::size_t>(i)].emplace_back(*a, *b);
977 }
978 std::ranges::sort(mask[static_cast<std::size_t>(i)]);
979 }
980 return mask;
981 }
982
983 // The index of the 2-degree block a point lies in, row-major.
984 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): latitude, longitude as everywhere
985 static std::size_t history_block_of_(double latitude, double longitude)
986 {
987 const auto row = std::clamp(static_cast<int>(std::floor((latitude + 90.0) / 2.0)), 0, history_rows - 1);
988 const auto column =
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);
991 }
992
993 void read_history_(const core::tsv_document& doc)
994 {
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())
998 {
999 // The key is the block's centre, "latitude,longitude".
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
1004 ? std::nullopt
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))
1007 {
1008 throw row.error("key", "must be a block centre such as 49.00,3.00");
1009 }
1010 if (row.get<int>("first_year") != history_first_year)
1011 {
1012 throw row.error("first_year", std::format("must be {}", history_first_year));
1013 }
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)
1021 {
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))
1025 {
1026 throw row.error(name, std::format("must hold {} values separated by ';'", history_months));
1027 }
1028 for (std::size_t m = 0; m < items.size(); ++m)
1029 {
1030 const auto x = core::detail::parse_number<double>(items[m]);
1031 if (!x || outside_(*x, range.first, range.second))
1032 {
1033 throw row.error(name, std::format("values must be {} to {}", range.first, range.second));
1034 }
1035 months[static_cast<std::size_t>(slot) + m].at(v) = static_cast<float>(*x);
1036 }
1037 }
1038 }
1039 _history_block = std::move(block);
1040 _history = std::move(months);
1041 _history_hash = doc.content_hash();
1042 }
1043
1044 static core::table_schema schema_()
1045 {
1046 return {.key = {"key"},
1047 .required = {"latitude", "longitude", "tmean", "dtr", "precipitation", "wet_days"},
1048 .optional = {"elevation", "area", "humidity", "cloud", "wind"}};
1049 }
1050
1051 // True when x is not a number in [lo, hi].
1052 static bool outside_(double x, double lo, double hi) { return !std::isfinite(x) || x < lo || x > hi; }
1053
1054 // Twelve control points per element: linear interpolation between them
1055 // (at mid-month) gives back each month's mean, so a day's value follows
1056 // the season smoothly instead of stepping at month ends.
1057 using months = std::array<double, 12>;
1058
1059 struct cell
1060 {
1061 climate_normals normals;
1062 double area{0.0};
1063 std::array<double, 3> xyz{};
1064 // Controls for the daily parameters.
1065 months tmean{}, dtr{}, wet{}, amount{}, humidity{}, cloud{}, wind{};
1066 double annual_range{0.0};
1067 double annual_mean{0.0};
1068
1069 static months read_months_(const core::tsv_row& row, std::string_view column, double lo, double hi)
1070 {
1071 const auto items = row.list(column);
1072 if (items.size() != 12) throw row.error(column, "must hold 12 values separated by ';'");
1073 months out{};
1074 for (std::size_t i = 0; i < 12; ++i)
1075 {
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)
1079 {
1080 throw row.error(column, std::format("values must be {} to {}", lo, hi));
1081 }
1082 out.at(i) = *v;
1083 }
1084 return out;
1085 }
1086
1087 static months constant_(double v)
1088 {
1089 months m{};
1090 m.fill(v);
1091 return m;
1092 }
1093
1094 // Parse and check a row; throws core::tsv_error located at the row.
1095 static cell parse(const core::tsv_row& row)
1096 {
1097 cell c;
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()))
1108 {
1109 throw row.error("area", "must not be negative");
1110 }
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);
1118 return c;
1119 }
1120 };
1121
1122 // The parameters of one day at one cell.
1123 struct day_params
1124 {
1125 double tmean, dtr, wet, amount, humidity, cloud, wind;
1126 double anomaly; // the months' temperature anomaly, standard normal
1127 double residual; // the share of the spread left to the daily residual
1128 };
1129
1130 // One day's draws that later days read back.
1131 struct day_draws
1132 {
1133 day_params p{};
1134 double u_wet{};
1135 std::array<double, 2> e{}; // temperature innovations
1136 };
1137
1138 core::layered_table _table{schema_()};
1139 std::vector<cell> _cells; // in table row order
1140 std::vector<std::uint32_t> _tree; // k-d tree over the cells' unit vectors (implicit, median-split)
1141 core::weighted_table<std::size_t> _area; // cell index, by land area
1142 land_mask _land; // empty when the data has no land.tsv
1143
1144 std::vector<std::int32_t> _history_block; // block row * 180 + column -> first month in _history, or -1
1145 std::vector<history_month> _history;
1146 std::uint64_t _history_hash{0};
1147
1148 core::rng _engine{core::rng::from_entropy()};
1149
1150 struct auto_probe_tag
1151 {
1152 };
1153
1154 explicit wtg(auto_probe_tag /*tag*/)
1155 {
1156 if (!load(dataset::lite)) (void)load(dataset::full);
1157 }
1158
1159 // -- Helpers ----------------------------------------------------------
1160
1161 static std::span<const core::embedded_file> embedded_files_()
1162 {
1163#ifdef DASMIG_WEATHER_EMBED
1164 return embedded::weathergen::files;
1165#else
1166 return {};
1167#endif
1168 }
1169
1170 std::uint64_t draw_seed_() { return _engine(); }
1171
1172 void require_data_() const
1173 {
1174 if (_cells.empty()) throw std::runtime_error("No weather data loaded. Call load() first.");
1175 }
1176
1177 static void check_point_(double latitude, double longitude) // NOLINT(bugprone-easily-swappable-parameters)
1178 {
1179 if (outside_(latitude, -90.0, 90.0))
1180 {
1181 throw std::invalid_argument("weather-generator: latitude must be -90 to 90");
1182 }
1183 if (outside_(longitude, -180.0, 180.0))
1184 {
1185 throw std::invalid_argument("weather-generator: longitude must be -180 to 180");
1186 }
1187 }
1188
1189 // Great-circle distance in km between two unit vectors.
1190 static double km_(const std::array<double, 3>& a, const std::array<double, 3>& b)
1191 {
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;
1194 }
1195
1196 static std::array<double, 3> unit_vector_(double latitude, double longitude)
1197 {
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};
1201 }
1202
1203 static double distance2_(const std::array<double, 3>& a, const std::array<double, 3>& b)
1204 {
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);
1209 }
1210
1211 // Controls whose linear interpolation between mid-months gives back the
1212 // monthly means: (c[m-1] + 6 c[m] + c[m+1]) / 8 = v[m], solved by
1213 // iteration (each step shrinks the error threefold).
1214 static months controls_(const months& v)
1215 {
1216 months c = v;
1217 for (int it = 0; it < 40; ++it)
1218 {
1219 months next{};
1220 for (std::size_t m = 0; m < 12; ++m)
1221 {
1222 next.at(m) = ((8.0 * v.at(m)) - c.at((m + 11) % 12) - c.at((m + 1) % 12)) / 6.0;
1223 }
1224 c = next;
1225 }
1226 return c;
1227 }
1228
1229 void rebuild_()
1230 {
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())
1235 {
1236 cell c = cell::parse(row);
1237 const auto& n = c.normals;
1238 c.xyz = unit_vector_(n.latitude, n.longitude);
1239 months wet{};
1240 months amount{};
1241 months cloud{};
1242 double total = 0.0;
1243 double total_wet = 0.0;
1244 for (std::size_t m = 0; m < 12; ++m)
1245 {
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;
1251 }
1252 const double annual_amount = total_wet > 0.0 ? total / total_wet : 0.0;
1253 for (std::size_t m = 0; m < 12; ++m)
1254 {
1255 amount.at(m) = n.wet_days.at(m) >= 0.1 ? n.precipitation.at(m) / n.wet_days.at(m) : annual_amount;
1256 }
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;
1266 double sum = 0.0;
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);
1271 }
1272 _cells = std::move(cells);
1273 _area = std::move(area);
1274 build_tree_();
1275 }
1276
1277 // -- Nearest cell -----------------------------------------------------
1278
1279 void build_tree_()
1280 {
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);
1284 }
1285
1286 // Recursion depth is log2 of the cell count (17 for the full tier).
1287 // NOLINTNEXTLINE(misc-no-recursion)
1288 void build_tree_(std::size_t lo, std::size_t hi, int axis)
1289 {
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)
1296 {
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);
1300 });
1301 build_tree_(lo, mid, (axis + 1) % 3);
1302 build_tree_(mid + 1, hi, (axis + 1) % 3);
1303 }
1304
1305 // The nearest cell by chord distance; ties go to the earlier row, so the
1306 // answer does not depend on the tree's shape.
1307 [[nodiscard]] std::size_t nearest_(const std::array<double, 3>& q) const
1308 {
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);
1312 return best;
1313 }
1314
1315 // NOLINTNEXTLINE(misc-no-recursion, bugprone-easily-swappable-parameters): depth log2 of the cells
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
1318 {
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))
1324 {
1325 best_d = d;
1326 best = i;
1327 }
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)
1335 {
1336 if (left_first) search_(q, mid + 1, hi, next, best, best_d);
1337 else search_(q, lo, mid, next, best, best_d);
1338 }
1339 }
1340
1341 // -- Days -------------------------------------------------------------
1342
1343 static unsigned month_length_(int year, unsigned month)
1344 {
1345 return static_cast<unsigned>(
1346 std::chrono::year_month_day_last{std::chrono::year{year} / std::chrono::month{month} / std::chrono::last}
1347 .day());
1348 }
1349
1350 // The anomalies of one calendar month at one place: (temperature,
1351 // precipitation), standard normal, from the month's own stream.
1352 static std::array<double, 2> month_anomaly_(std::uint64_t month_seed, std::int64_t month_index)
1353 {
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)};
1357 }
1358
1359 // The month anomalies one call reads, each drawn once. Days are read in
1360 // order, so the months asked for never go back before the first.
1361 class month_anomalies
1362 {
1363 public:
1364 explicit month_anomalies(std::uint64_t seed) : _seed{seed} {}
1365
1366 std::array<double, 2> operator()(std::int64_t index)
1367 {
1368 if (_values.empty()) _first = index;
1369 while (_first + static_cast<std::int64_t>(_values.size()) <= index)
1370 {
1371 _values.push_back(month_anomaly_(_seed, _first + static_cast<std::int64_t>(_values.size())));
1372 }
1373 return _values[static_cast<std::size_t>(index - _first)];
1374 }
1375
1376 private:
1377 std::uint64_t _seed;
1378 std::int64_t _first{0};
1379 std::vector<std::array<double, 2>> _values;
1380 };
1381
1382 // The months as they were at one cell, for historical requests: per
1383 // month index (year * 12 + month - 1) the controls of the temperature
1384 // anomaly and of the log ratios of the wet-day odds and the wet-day
1385 // amount to the normals'. A control is the inverse of the
1386 // interpolation's (1, 6, 1) / 8 filter applied to the months around it,
1387 // over a fixed reach, so it does not depend on the days a call asks for.
1388 class month_history
1389 {
1390 public:
1391 month_history(const cell& c, std::span<const history_month> months)
1392 : _wet_days{c.normals.wet_days}, _months{months}
1393 {
1394 }
1395
1396 std::array<double, 3> operator()(std::int64_t index)
1397 {
1398 if (_values.empty()) _first = index;
1399 while (_first + static_cast<std::int64_t>(_values.size()) <= index)
1400 {
1401 _values.push_back(control_(_first + static_cast<std::int64_t>(_values.size())));
1402 }
1403 return _values[static_cast<std::size_t>(index - _first)];
1404 }
1405
1406 private:
1407 static constexpr int reach = 12; // r^12 is below 1e-9
1408 static constexpr double root = -0.17157287525380990; // 2 sqrt(2) - 3
1409 static constexpr double gain = std::numbers::sqrt2; // 8 / sqrt(32)
1410
1411 // The month's own values; zero outside the record or the data.
1412 [[nodiscard]] std::array<double, 3> month_(std::int64_t index) const
1413 {
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)
1424 {
1425 const double g = std::clamp(f, 0.005, 0.995);
1426 return core::math::log(g / (1.0 - g));
1427 };
1428 const double odds = log_odds(wet_now / length) - log_odds(std::min(wet, normal_length) / normal_length);
1429 // The amount per wet day: the precipitation's ratio over the
1430 // wet days'.
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)};
1434 }
1435
1436 [[nodiscard]] std::array<double, 3> control_(std::int64_t index) const
1437 {
1438 std::array<double, 3> out{};
1439 double weight = gain;
1440 for (int k = 0; k <= reach; ++k)
1441 {
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)));
1445 weight *= root;
1446 }
1447 return out;
1448 }
1449
1450 months _wet_days;
1451 std::span<const history_month> _months;
1452 std::int64_t _first{0};
1453 std::vector<std::array<double, 3>> _values;
1454 };
1455
1456 // A day's parameters, interpolated between the controls of the months
1457 // around it, with the months' anomalies interpolated the same way (or,
1458 // when @p history is set, the months as they were).
1459 static day_params params_(const cell& c, std::chrono::year_month_day ymd, month_anomalies& anomalies,
1460 month_history* history)
1461 {
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));
1465 // Months after the middle of the year's January (negative before).
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); // the month before or the month itself
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))); };
1474
1475 if (history != nullptr) return historical_params_(c, *history, index, t, lerp);
1476
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]));
1481 // Variance of the interpolated anomaly, to keep means and spreads.
1482 const double v = core::math::fenced((1.0 - t) * (1.0 - t)) + core::math::fenced(t * t);
1483 // The precipitation anomaly scales the odds of a wet day, so the
1484 // fraction stays below 1 however wet the month, and the wet-day amount
1485 // by a log-normal factor. Each correction keeps its expected value:
1486 // the odds' moves from -1/2 to +1/2 of the variance with the fraction
1487 // (a dry month's fraction scales like the odds, a wet month's dry
1488 // fraction like their inverse), and the amount's also takes its
1489 // covariance with the fraction, f (1 - f) sf zp, so the expected
1490 // total stays.
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)));
1497
1498 const double wet_odds = core::math::fenced(f * odds);
1499
1500 day_params p{};
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);
1504 p.amount =
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);
1509 p.anomaly = za;
1510 p.residual = std::sqrt(1.0 - core::math::fenced(model::temperature_month * model::temperature_month * v));
1511 return p;
1512 }
1513
1514 // A historical day: the normals moved by the months as they were, with
1515 // no drawn month anomaly. The residual keeps the share it has on a
1516 // normal day, which is the spread within a month.
1517 template <typename Lerp>
1518 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): a month index and a share of it
1519 static day_params historical_params_(const cell& c, month_history& history, std::int64_t index, double t,
1520 const Lerp& lerp)
1521 {
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)));
1528
1529 day_params p{};
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);
1537 p.anomaly = 0.0;
1538 p.residual = std::sqrt(1.0 - core::math::fenced(model::temperature_month * model::temperature_month * v));
1539 return p;
1540 }
1541
1542 static double persistence_(double f)
1543 {
1544 return std::clamp(model::persistence0 + core::math::fenced(model::persistence1 * f), 0.0, 0.9);
1545 }
1546
1547 // P(wet | dry) and P(wet | wet) that keep the wet-day fraction f.
1548 static std::pair<double, double> transitions_(double f)
1549 {
1550 const double d = persistence_(f);
1551 const double p01 = core::math::fenced(f * (1.0 - d));
1552 return {p01, p01 + d};
1553 }
1554
1555 static double logistic_(double z) { return 1.0 / (1.0 + core::math::exp(-z)); }
1556
1557 static double spread_(const std::array<double, 7>& c, const cell& cl, const day_params& p)
1558 {
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)};
1562 double s = 0.0;
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);
1565 }
1566
1567 // A^k B for k = 0 .. window - 1: the temperature residual of a day is
1568 // the sum of these times the innovations of that day and the ones
1569 // before it.
1570 static const std::array<std::array<double, 4>, model::window>& impulse_()
1571 {
1572 static const auto table = []
1573 {
1574 std::array<std::array<double, 4>, model::window> out{};
1575 std::array<double, 4> m = model::b;
1576 for (auto& k : out)
1577 {
1578 k = m;
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])};
1584 }
1585 return out;
1586 }();
1587 return table;
1588 }
1589
1590 struct sun
1591 {
1592 double daylight_minutes{};
1593 std::optional<double> rise; // minutes after 00:00 UTC of the date (may be negative or past 1440)
1594 std::optional<double> set;
1595 };
1596
1597 // Sunrise, sunset and day length on a date at a point, by NOAA's solar
1598 // calculator equations (Meeus): the sun's position at local noon, the
1599 // equation of time, and the hour angle at which the sun's centre is
1600 // 0.833 degrees below the horizon (refraction and the sun's radius).
1601 static sun sun_(std::chrono::sys_days day, double latitude, double longitude) // NOLINT(bugprone-easily-swappable-parameters)
1602 {
1603 using core::math::fenced;
1604 const double days = static_cast<double>(day.time_since_epoch().count());
1605 // Julian centuries since J2000.0 at local mean noon.
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));
1617 const double eps0 =
1618 23.0 + ((26.0 + ((21.448 - fenced(t * (46.815 + fenced(t * (0.00059 - fenced(t * 0.001813)))))) / 60.0)) /
1619 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 =
1626 4.0 * deg *
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;
1631
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));
1634 // cos(hour angle) = numerator / denominator: above 1 the sun never
1635 // rises, below -1 it never sets.
1636 if (std::abs(denominator) < 1e-12 || numerator >= denominator || numerator <= -denominator)
1637 {
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};
1640 }
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)};
1643 }
1644
1645 // x rounded to 1 / per (round_to_(x, 10) to 0.1): the division gives the
1646 // double nearest the decimal, so JSON prints -1.2, not -1.2000000000000002;
1647 // adding 0 turns -0 into 0.
1648 static double round_to_(double x, double per) { return (std::round(x * per) / per) + 0.0; }
1649
1650 static void check_request_(const weather_request& request)
1651 {
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())
1654 {
1655 throw std::invalid_argument("weather-generator: set latitude and longitude together");
1656 }
1657 if (request.latitude) check_point_(*request.latitude, *request.longitude);
1658 if (request.elevation && outside_(*request.elevation, -500.0, 9000.0))
1659 {
1660 throw std::invalid_argument("weather-generator: elevation must be -500 to 9000 metres");
1661 }
1662 if (outside_(request.warming, -10.0, 10.0))
1663 {
1664 throw std::invalid_argument("weather-generator: warming must be -10 to 10 °C");
1665 }
1666 if (request.units != unit_system::metric && request.units != unit_system::imperial)
1667 {
1668 throw std::invalid_argument("weather-generator: units must be metric or imperial");
1669 }
1670 }
1671
1672 // Where a call's weather is: the requested point and its nearest cell,
1673 // or a cell drawn by land area.
1674 struct place
1675 {
1676 std::size_t cell{0};
1677 double latitude{0.0};
1678 double longitude{0.0};
1679 double distance_km{0.0};
1680 };
1681
1682 // Distance in km from a point to the nearest land cell of the mask
1683 // (0 inside one), searched within the rows and columns that can be
1684 // within the limit; infinity when there is no mask or no land near.
1685 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
1686 [[nodiscard]] double land_km_(double latitude, double longitude, const std::array<double, 3>& q) const
1687 {
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)
1693 {
1694 for (const auto& [a, b] : _land[static_cast<std::size_t>(i)])
1695 {
1696 if (j < a) return false;
1697 if (j <= b) return true;
1698 }
1699 return false;
1700 };
1701 const int i0 = row_of(latitude);
1702 const int j0 = column_of(longitude);
1703 if (is_land(i0, j0)) return 0.0;
1704
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)
1707 {
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);
1711 // Columns that can lie within the limit at this row's widest
1712 // latitude, plus one for the cell's own width.
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)
1717 {
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);
1721 // The cell's nearest longitude to the point, across the date line.
1722 double offset = longitude - west;
1723 offset -= 360.0 * std::floor(offset / 360.0); // 0 to 360 east of the cell's west edge
1724 double nearest_lon = west; // the point lies west of the cell
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)));
1728 }
1729 }
1730 return best;
1731 }
1732
1733 // The cell a point uses and its distance to land: the nearest cell by
1734 // chord, if the point is within 100 km of the mask's land or of a cell.
1735 [[nodiscard]] place locate_(double latitude, double longitude) const
1736 {
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))
1741 {
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"}));
1745 }
1746 return {.cell = index, .latitude = latitude, .longitude = longitude, .distance_km = km};
1747 }
1748
1749 [[nodiscard]] place place_(const weather_request& request, const core::rng& call) const
1750 {
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};
1759 }
1760
1761 static std::uint64_t day_index_(std::chrono::sys_days day)
1762 {
1763 return static_cast<std::uint64_t>(static_cast<std::int64_t>(day.time_since_epoch().count()));
1764 }
1765
1766 // Wet or dry on day k: read back to the latest day whose draw settles the
1767 // state whatever the day before was, then run forward.
1768 static bool wet_on_(const std::vector<day_draws>& draws, int k)
1769 {
1770 const auto at = [&](int i) -> const day_draws& { return draws[static_cast<std::size_t>(i)]; };
1771 int s = k;
1772 int known = -1; // 1 wet, 0 dry
1773 for (; s > k - model::window && known < 0; --s)
1774 {
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;
1778 }
1779 bool wet = false;
1780 if (known < 0)
1781 {
1782 // Not settled within the window (probability below 1e-20): start
1783 // from the stationary chance.
1784 wet = at(s).u_wet < at(s).p.wet;
1785 }
1786 else
1787 {
1788 ++s; // the loop stepped past the settling day
1789 wet = known == 1;
1790 }
1791 for (int t = s + 1; t <= k; ++t)
1792 {
1793 const auto [p01, p11] = transitions_(at(t).p.wet);
1794 wet = at(t).u_wet < (wet ? p11 : p01);
1795 }
1796 return wet;
1797 }
1798
1799 struct temperatures
1800 {
1801 double mean;
1802 double tmax;
1803 double tmin;
1804 };
1805
1806 // The day's mean temperature (the season, the wet or dry day, and a
1807 // spread split between the month's anomaly and the day's residual, so
1808 // the total spread is unchanged) and its range (log-normal around its
1809 // mean, so it is never negative and keeps the mean and spread).
1810 static temperatures temperatures_(const std::vector<day_draws>& draws, int k, bool wet, const cell& c,
1811 double shift)
1812 {
1813 // Residuals of the daily mean and range: the sum over the window,
1814 // oldest first.
1815 const auto& impulse = impulse_();
1816 double rm = 0.0;
1817 double rr = 0.0;
1818 for (int j = model::window - 1; j >= 0; --j)
1819 {
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]);
1824 }
1825
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)};
1839 }
1840
1841 // Amount, form and thunder of a wet day; returns the amount before
1842 // rounding.
1843 static double precipitation_(weather& w, const day_params& p, const temperatures& t, const core::rng& day)
1844 {
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;
1854
1855 w.precipitation = round_to_(amount, 10.0);
1856 // A cm of snow per mm of water; sleet is half snow.
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);
1859
1860 if (w.type != precipitation_type::snow)
1861 {
1862 auto th = day.split("thunder");
1863 const auto& c = model::thunder;
1864 const double z =
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);
1867 }
1868 return amount;
1869 }
1870
1871 // Cloud cover, humidity and wind, which follow the wet and dry days;
1872 // returns the wind before rounding.
1873 static double sky_(weather& w, const day_params& p, bool wet, const core::rng& day)
1874 {
1875 const double f = p.wet;
1876 {
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);
1884 }
1885 {
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);
1891 }
1892 {
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);
1896 return wind;
1897 }
1898 }
1899
1900 // One simulated day: the weather in metric units, and what the hours and
1901 // the imperial units need before rounding.
1902 struct day_result
1903 {
1904 weather w;
1905 temperatures t{};
1906 double amount{0.0}; // mm (0 on a dry day)
1907 double wind{0.0}; // m/s
1908 sun s{};
1909 std::uint64_t seed{0}; // the day's stream
1910 };
1911
1912 // The weather of `days` days from request.date: checks the request,
1913 // simulates (with the day before and after for the hours), adds the
1914 // hours and converts the units.
1915 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): days and seed differ in type and meaning
1916 [[nodiscard]] std::vector<weather> run_(const weather_request& request, int days, std::uint64_t call_seed) const
1917 {
1918 require_data_();
1919 check_request_(request);
1920 if (request.historical)
1921 {
1922 if (_history.empty())
1923 {
1924 throw std::runtime_error("No weather history loaded. Call load_history() first.");
1925 }
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)
1930 {
1931 throw std::invalid_argument(std::format("weather-generator: historical days must fall in {} to {}",
1932 history_first_year, history_last_year));
1933 }
1934 }
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)
1941 {
1942 const auto at = static_cast<std::size_t>(k);
1943 auto& d = sim[at];
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));
1947 }
1948 return out;
1949 }
1950
1951 // The days from request.date + offset, each from its own stream, with
1952 // `window` days before the first read for the chains.
1953 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): offset, days and seed differ in meaning
1954 [[nodiscard]] std::vector<day_result> simulate_(const weather_request& request, int offset, int days,
1955 std::uint64_t call_seed) const
1956 {
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));
1963
1964 // The place's stream: the same seed gives unrelated weather at
1965 // different cells, and the same weather at points of one cell.
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)
1970 {
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),
1974 history_months));
1975 }
1976 const std::uint64_t data =
1977 request.historical ? core::hash_combine(data_hash(), _history_hash) : data_hash();
1978 const auto first =
1979 std::chrono::sys_days{request.date} + std::chrono::days{offset} - std::chrono::days{model::window};
1980 const int span = model::window + days;
1981
1982 std::vector<day_draws> draws(static_cast<std::size_t>(span));
1983 for (int k = 0; k < span; ++k)
1984 {
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)};
1993 }
1994
1995 std::vector<day_result> out;
1996 out.reserve(static_cast<std::size_t>(days));
1997 for (int k = model::window; k < span; ++k)
1998 {
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);
2005
2006 day_result result;
2007 result.t = t;
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);
2025 // Sunshine: the daylight left uncovered by the day's cloud
2026 // (curated; doc/data.md).
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);
2033 result.s = sun;
2034 out.push_back(std::move(result));
2035 }
2036 return out;
2037 }
2038
2039 // -- Units ------------------------------------------------------------
2040
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;
2044
2045 // The day in imperial units, from the values before rounding.
2046 static void to_imperial_(day_result& d)
2047 {
2048 weather& w = d.w;
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);
2054 // A cm of snow per mm of water, sleet half of it, in inches.
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);
2060 }
2061
2062 // -- Hours ------------------------------------------------------------
2063
2064 // Spread `tenths` over the hours by `weights`: each hour gets its share
2065 // rounded down, then the hours with the largest remainders (the earlier
2066 // first) one more, so the hours add up to `tenths` exactly; `floor` is
2067 // given to every hour with a positive weight first.
2068 static std::array<long long, 24> allocate_(long long tenths, const std::array<double, 24>& weights,
2069 long long floor)
2070 {
2071 std::array<long long, 24> out{};
2072 double total = 0.0;
2073 long long given = 0;
2074 for (std::size_t h = 0; h < 24; ++h)
2075 {
2076 if (weights.at(h) > 0.0)
2077 {
2078 total += weights.at(h);
2079 out.at(h) = floor;
2080 given += floor;
2081 }
2082 }
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)
2087 {
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;
2093 }
2094 for (long long left = tenths - given; left > 0; --left)
2095 {
2096 std::size_t best = 0;
2097 for (std::size_t h = 1; h < 24; ++h)
2098 {
2099 if (remainder.at(h) > remainder.at(best)) best = h;
2100 }
2101 ++out.at(best);
2102 remainder.at(best) = -1.0;
2103 }
2104 return out;
2105 }
2106
2107 // Saturation vapour pressure over water, hPa (Magnus; WMO 2008).
2108 static double saturation_(double celsius)
2109 {
2110 return 6.112 * core::math::exp((17.62 * celsius) / (243.12 + celsius));
2111 }
2112
2113 // A day's 24 hours from the day itself and its neighbours (for the
2114 // night before sunrise and after sunset), from the day's "hours" stream.
2115 // NOLINTNEXTLINE(readability-function-cognitive-complexity): one pass per element
2116 static std::vector<weather_hour> hours_(const day_result& before, const day_result& d,
2117 const day_result& after, bool imperial)
2118 {
2119 using core::math::fenced;
2120 const weather& w = d.w;
2121 // Local mean solar hours from the local midnight of the date.
2122 const double solar = 4.0 * w.longitude; // minutes ahead of UTC
2123 const auto local = [&](double utc_minutes, double day_shift)
2124 { return ((utc_minutes + solar) / 60.0) + day_shift; };
2125
2126 // Temperature: the sunrise, warmest hour and sunset of the day, the
2127 // sunset before and the sunrise after.
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)
2132 {
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)
2142 {
2143 const auto t = static_cast<double>(h);
2144 double v = 0.0;
2145 if (t < rise)
2146 {
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)));
2149 }
2150 else if (t <= warmest)
2151 {
2152 v = tn + fenced((tx - tn) * core::math::sin_deg(90.0 * (t - rise) / std::max(warmest - rise, 1e-6)));
2153 }
2154 else if (t <= set)
2155 {
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));
2158 }
2159 else
2160 {
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)));
2163 }
2164 temperature.at(static_cast<std::size_t>(h)) = v;
2165 }
2166 }
2167 else
2168 {
2169 // Polar day or night: a plain daily wave, coolest at 03:00.
2170 for (int h = 0; h < 24; ++h)
2171 {
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);
2174 }
2175 }
2176
2177 // The part of each hour the sun is up.
2178 std::array<double, 24> daylight{};
2179 for (int h = 0; h < 24; ++h)
2180 {
2181 double up = w.daylight_hours >= 24.0 ? 1.0 : 0.0;
2182 if (d.s.rise && d.s.set)
2183 {
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);
2187 }
2188 daylight.at(static_cast<std::size_t>(h)) = up;
2189 }
2190
2191 const core::rng stream = core::rng{d.seed}.split("hours");
2192
2193 // Precipitation: one or two spells of wet hours, the day's amount
2194 // spread over them by exponential weights.
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)
2199 {
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)))
2208 : wet_hours;
2209 const auto spell = [&](long long length, long long start)
2210 {
2211 for (long long h = start; h < start + length; ++h)
2212 {
2213 const double u = 1.0 - core::uniform01(r); // (0, 1]
2214 weights.at(static_cast<std::size_t>(h)) += -core::math::log(u) + 1e-9;
2215 }
2216 };
2217 long long start = 0;
2218 if (w.thunder)
2219 {
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);
2222 }
2223 else
2224 {
2225 start = core::uniform_int<long long>(r, 0, 24 - first);
2226 }
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);
2232 }
2233
2234 // Cloud and wind: lag-1 noise around the day's values (the wind's
2235 // mean then scaled to the day's).
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)
2246 {
2247 if (h > 0)
2248 {
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));
2251 }
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;
2260 }
2261 // Scaled so the hours' mean is the day's wind.
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;
2265
2266 // Sunshine: the day's hours shared by the clear daylight of each
2267 // hour; an hour given more than its daylight keeps its daylight and
2268 // the rest goes to the others.
2269 std::array<double, 24> sunshine{};
2270 {
2271 std::array<bool, 24> full{};
2272 double left = w.sunshine_hours;
2273 for (int pass = 0; pass < 24 && left > 1e-9; ++pass)
2274 {
2275 double clear = 0.0;
2276 double spare = 0.0;
2277 for (std::size_t h = 0; h < 24; ++h)
2278 {
2279 if (full.at(h)) continue;
2280 clear += fenced(daylight.at(h) * (1.0 - cloud.at(h)));
2281 spare += daylight.at(h);
2282 }
2283 if (spare <= 0.0) break;
2284 bool capped = false;
2285 for (std::size_t h = 0; h < 24; ++h)
2286 {
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))
2291 {
2292 full.at(h) = true;
2293 sunshine.at(h) = daylight.at(h);
2294 left -= daylight.at(h);
2295 capped = true;
2296 }
2297 }
2298 if (capped) continue;
2299 for (std::size_t h = 0; h < 24; ++h)
2300 {
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;
2304 }
2305 left = 0.0;
2306 }
2307 }
2308
2309 // Humidity: the day's vapour pressure against each hour's saturation.
2310 const double vapour = (w.humidity / 100.0) * saturation_(w.tmean);
2311
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)
2316 {
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)
2323 {
2324 o.type = w.type;
2325 o.thunder = w.thunder;
2326 }
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);
2334 if (imperial)
2335 {
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);
2340 }
2341 else
2342 {
2343 o.temperature = round_to_(temperature.at(h), 10.0);
2344 o.precipitation = mm;
2345 o.snowfall = cm;
2346 o.wind = round_to_(wind.at(h), 10.0);
2347 }
2348 }
2349 return out;
2350 }
2351
2352 // An hour's condition: the day's thresholds for dry hours, rates per
2353 // hour (mm of water) for wet ones.
2354 static weather_condition hour_condition_(const weather_hour& o, double mm)
2355 {
2356 switch (o.type)
2357 {
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;
2370 }
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;
2375 }
2376
2377 static weather_condition condition_(const weather& w)
2378 {
2379 switch (w.type)
2380 {
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;
2393 }
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;
2398 }
2399};
2400
2401} // namespace dasmig
2402
2403#endif // DASMIG_WEATHERGEN_HPP
One day's weather at one place.
double distance_km
Distance from the point to land, km (0 on land; at most 100).
std::string cell
Key of the climate cell drawn from.
std::chrono::year_month_day date
The day.
bool thunder
Thunder heard (on rain or sleet days only).
std::string to_json() const
This weather as a JSON document.
std::uint64_t seed() const
Retrieve the random seed used to generate this weather.
bool frost
tmin below 0 °C (32 °F).
unit_system units
const core::seed_signature & signature() const
The seed and the data this weather was generated from.
double tmean
Mean temperature, °C (°F): halfway between tmin and tmax.
std::optional< std::chrono::sys_time< std::chrono::minutes > > sunrise
Sunrise, UTC (unset in polar day and polar night). It can fall on the date before in UTC,...
double humidity
Mean relative humidity, %.
std::optional< std::chrono::sys_time< std::chrono::minutes > > sunset
Sunset, UTC (unset in polar day and polar night).
double tmin
Lowest temperature, °C (°F).
double daylight_hours
Time from sunrise to sunset, hours (0 in polar night, 24 in polar day).
double longitude
Degrees east: the requested point, or the drawn cell's.
std::string date_string() const
ISO 8601 date string (YYYY-MM-DD).
void to_json(core::json_writer &w) const
Write this weather as a JSON object.
weather_condition condition
The day in a word.
double snowfall
Fresh snow, cm (in): 10 cm per 10 mm of water.
double precipitation
Precipitation, mm (in) of water (0 on a dry day).
double cloud_cover
Mean cloud cover, 0 to 1.
static std::string time_string(std::chrono::sys_time< std::chrono::minutes > t)
An instant as ISO 8601 UTC to the minute (2024-01-15T07:42Z).
std::string to_string() const
Human-readable summary string.
precipitation_type type
The form it falls in.
double wind
Mean wind speed at 10 m, m/s (mph).
double latitude
Degrees north: the requested point, or the drawn cell's.
double elevation
Metres: the requested elevation, or the cell's.
friend std::ostream & operator<<(std::ostream &os, const weather &v)
Stream the summary to an output stream.
double tmax
Highest temperature, °C (°F).
std::vector< weather_hour > hours
The day's 24 hours, when the request set hourly (empty otherwise).
Weather Generator: daily weather drawn around monthly climate normals.
bool has_history() const
Check whether historical requests can be served.
bool load(dataset tier)
Load a dataset tier, replacing all data and layers loaded before.
const core::layer_info & extend(core::tsv_document doc, core::layer_mode mode=core::layer_mode::override)
Add a data layer.
bool load_history()
Load the months of 1991-2020 for historical requests, from the compiled-in copy or resources/history/...
void for_each_weather(const weather_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of weather results with a batch seed drawn like get_weather() draws its own.
void load(const std::filesystem::path &dir)
Load data from a resource directory, replacing all data and layers loaded before.
const std::vector< core::layer_info > & layers() const
The data layers loaded, in order.
const core::layer_info & extend(const std::filesystem::path &file, core::layer_mode mode=core::layer_mode::override)
Add a data layer from a TSV file with the climate.tsv columns.
const core::layer_info & extend_from_string(std::string_view text, std::string source, core::layer_mode mode=core::layer_mode::override)
Add a data layer from TSV text (for example from a WebAssembly host).
weather get_weather(const weather_request &request, std::uint64_t call_seed) const
Generate one day's weather, deterministically.
void for_each_weather(const weather_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of weather results to a sink without holding the whole batch.
bool has_data() const
Check whether any data has been loaded.
wtg & operator=(const wtg &)=delete
Not copyable.
std::size_t count() const
Return the number of loaded climate cells.
weather get_weather(const weather_request &request)
Generate one day's weather. Use weather_request{} for the defaults.
std::vector< weather > get_weather_series(const weather_request &request, int days)
Generate the weather of consecutive days.
std::uint64_t data_hash() const
Hash of the loaded data, recorded in each result's signature().
std::vector< weather > get_weather_series(const weather_request &request, int days, std::uint64_t call_seed) const
Generate the weather of consecutive days, deterministically.
std::vector< weather > get_weather_batch(const weather_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of weather results, deterministically.
void load_history(const std::filesystem::path &file)
Load the months of 1991-2020 from a file.
wtg(const wtg &)=delete
Not copyable.
wtg()=default
Default constructor — creates an empty generator with no data.
static constexpr int max_series_days
The longest series one call returns, in days (about 100 years).
static wtg & instance()
Access the global singleton instance.
wtg(wtg &&) noexcept=default
Movable.
climate_normals climate_at(double latitude, double longitude) const
The monthly normals of the climate cell nearest a point.
std::vector< weather > get_weather_batch(const weather_request &request, const core::batch_options &options)
Generate a batch of weather results from a seed drawn like get_weather() draws its own.
The monthly climate normals of a place (climate_at()).
std::string to_json() const
The normals as a JSON document.
std::array< double, 12 > wind
Mean wind speed at 10 m, m/s.
std::array< double, 12 > dtr
Mean diurnal range (Tmax - Tmin), °C.
std::array< double, 12 > wet_days
Days with at least 0.1 mm.
double longitude
The cell's point, degrees east.
std::array< double, 12 > precipitation
Precipitation, mm per month.
std::array< double, 12 > cloud
Cloud cover, %.
std::array< double, 12 > humidity
Mean relative humidity, %.
double elevation
The cell's mean elevation, metres.
double latitude
The cell's point, degrees north (a block's land centroid).
std::string cell
The climate cell's key (its block centre, or a layer's own key).
void to_json(core::json_writer &w) const
Write the normals as a JSON object.
std::array< double, 12 > tmean
Mean temperature, °C.
One hour of a day's weather (weather::hours, when the request asks for them).
std::chrono::sys_time< std::chrono::minutes > time
Start of the hour, UTC.
double daylight_hours
The part of the hour the sun is up, 0 to 1.
precipitation_type type
The form it falls in (none in a dry hour).
int hour
Hour of the local mean solar day, 0 to 23.
double sunshine_hours
Bright sunshine in the hour, 0 to 1.
void to_json(core::json_writer &w) const
Write this hour as a JSON object.
double precipitation
Precipitation in the hour, mm (in).
double snowfall
Fresh snow in the hour, cm (in).
double wind
Mean wind speed at 10 m, m/s (mph).
weather_condition condition
The hour in a word.
bool thunder
Thunder in the hour.
double humidity
Relative humidity, %.
double cloud_cover
Cloud cover, 0 to 1.
double temperature
Temperature at the start of the hour, °C (°F).
What a weather call asks for.
bool hourly
Also give each day's 24 hours in weather::hours. The days themselves are the same with or without the...
std::optional< double > longitude
Degrees east, -180 to 180. Set with latitude.
double warming
Degrees Celsius added to every temperature, -10 to 10. The normals are 1991-2020 means (centred on 20...
std::optional< double > elevation
Metres above sea level, -500 to 9000. Temperatures move by the standard lapse rate (6....
static constexpr std::chrono::year_month_day default_date
The default date, 2023-07-01 (birth-generator's, user-generator's and device-generator's reference da...
std::optional< double > latitude
Degrees north, -90 to 90. Set with longitude, or leave both unset to draw a place on land by area.
std::chrono::year_month_day date
The day (any valid date). A series starts here.
unit_system units
The units of the weather values (metric by default). The place keeps metres and km either way,...
bool historical
Draw each month around the month as it was (CRU TS, January 1991 to December 2020) instead of around ...
constexpr std::string_view label(weather_condition c) noexcept
The English label of a condition (clear, partly cloudy, ...).
weather_condition
The day's weather in one word or two.
@ clear
Dry, cloud cover below 20%.
@ overcast
Dry, cloud cover 85% or more.
@ heavy_snow
Snow of 10 mm of water (about 10 cm) or more.
@ thunderstorm
Rain or sleet with thunder.
@ partly_cloudy
Dry, cloud cover 20% to 50%.
@ heavy_rain
Rain of 10 mm or more (a heavy precipitation day).
@ light_rain
Rain below 2.5 mm.
@ light_snow
Snow below 1 mm of water (about 1 cm).
@ mostly_cloudy
Dry, cloud cover 50% to 85%.
unit_system
The units of a result's weather values.
@ metric
°C, mm of precipitation, cm of snow, m/s.
@ imperial
°F, inches of precipitation and of snow, mph.
precipitation_type
The form precipitation falls in.
@ sleet
Rain and snow mixed.
constexpr std::string_view to_string(precipitation_type t) noexcept
The key of a precipitation type (none, rain, sleet, snow).