|
Weather Generator 1.1.0
Daily weather for any place and date from climate normals for C++23
|
weather-generator draws daily weather for a place on land and a date: the low and the high, rain, sleet or snow and how much, thunder, cloud, humidity, wind, sunrise, sunset and sunshine, around the place's 1991-2020 climate normals. This guide covers everything beyond the README. For the full API reference, run doxygen Doxyfile from the repository root and open doc/api/html/index.html; for the data and the model, see Data and Method.
Copy dasmig/weathergen.hpp, dasmig/core.hpp and the dasmig/core/ directory into your include path, keeping them together, and either the resources/ folder or dasmig/weathergen_data.hpp (see Compiled-in Data). Build with -std=c++23 (GCC 14+, Clang 18+) or /std:c++latest (MSVC 19.38+).
wtg::instance() loads the lite tier on first use (or full if there is no lite), from the compiled-in copy or from the first of resources/, ../resources, weather-generator/resources and ../weather-generator/resources that has it. Independent instances start empty:
| Tier | Grid | Cells | File | Loads in |
|---|---|---|---|---|
lite | 2 degrees | 5,163 | 2.0 MB | about 60 ms |
full | 0.5 degrees | 67,213 | 26.0 MB | about 0.8 s |
Both cover every land cell of CRU TS except Antarctica, and share land.tsv, the half-degree land mask behind the sea rule below. full follows coasts, mountains and rain shadows more closely: against stations it was not fitted on, its monthly Tmax errors are about a fifth smaller than lite's (see Data and Method). Each load replaces the data and every layer loaded before.
| Field | Default | Meaning |
|---|---|---|
latitude, longitude | unset: a place drawn by land area | degrees, -90 to 90 and -180 to 180; set both or neither |
elevation | the cell's mean elevation | metres, -500 to 9000; temperatures move 6.5 °C per km from the cell's elevation |
date | 2023-07-01 | any valid date; the first day of a series |
warming | 0 | °C added to every temperature, -10 to 10 |
units | unit_system::metric | metric or imperial (°F, inches, mph); see Imperial Units |
hourly | false | also give each day's 24 hours; see Hours |
historical | false | draw each month around the month as it was, 1991-2020; see Historical Months |
A point uses the nearest climate cell (by distance on the sphere; ties go to the earlier row). The point's own latitude, longitude and elevation are returned; the climate is the cell's. Without a point, a cell is drawn with probability proportional to its land area, and the result carries the cell's point.
The data covers land. A point at sea gets the weather of the nearest cell if it is within 100 km of land (a bay, a strait, a ship near the coast), and distance_km says how far out it is; further out the call throws std::invalid_argument:
Land here is the half-degree mask, so a point on a small island or a thin peninsula can read a few km out to sea, and one in a wide estuary 0 km.
The normals are 1991-2020 means, centred on 2005. Land has warmed by about half a degree since; set warming to bring temperatures to a later climate. It moves temperatures only, so the share of snow moves with it, but rain amounts and wet days stay as they were.
The library never reads the clock. A request is used for one call only, so nothing a call asks for changes the next.
| Field | Meaning |
|---|---|
date | the day (date_string() gives YYYY-MM-DD) |
cell | the climate cell's key, its block centre ("49.00,3.00") |
latitude, longitude, elevation | the requested point and elevation, or the cell's |
distance_km | how far the point is from land, km, to 0.1: 0 on land, at most 100 |
tmin, tmax | the day's lowest and highest temperature, °C, to 0.1 |
tmean | halfway between them, °C, to 0.1 |
frost | tmin below 0 °C |
precipitation | mm of water, to 0.1; 0 on a dry day, at least 0.1 on a wet day |
snowfall | fresh snow, cm, to 0.1: 1 cm per mm of water for snow, half that for sleet |
type | none, rain, sleet or snow |
thunder | thunder on a rain or sleet day |
cloud_cover | mean cloud cover, 0 to 1, to 0.01 |
humidity | mean relative humidity, %, whole |
wind | mean wind speed at 10 m, m/s, to 0.1 |
daylight_hours | from sunrise to sunset, hours, to 0.01: 24 in polar day, 0 in polar night |
sunshine_hours | the daylight not covered by cloud, hours, to 0.1 |
sunrise, sunset | std::optional<std::chrono::sys_time<std::chrono::minutes>>, UTC to the minute; empty in polar day and night. weather::time_string(t) gives YYYY-MM-DDTHH:MMZ |
condition | the day in a word, below |
units | unit_system::metric or imperial, as requested |
hours | the 24 hours when the request set hourly, empty otherwise |
Sunrise and sunset come from NOAA's solar calculator equations at the point, for the sun's upper edge at the horizon with standard refraction (the tests check them against a plain double-precision implementation of the same equations, to half a minute). They are in UTC, so in the far east or west a sunrise can fall on the day before (Sydney's 1 January sunrise is at 18:48 UTC on 31 December). The library has no time zones: add the place's offset for local time.
condition | When |
|---|---|
clear, partly_cloudy, mostly_cloudy, overcast | dry, with cloud cover below 0.2, below 0.5, below 0.85, and above |
light_rain, rain, heavy_rain | rain below 2.5 mm, below 10 mm, and 10 mm or more |
sleet | sleet |
light_snow, snow, heavy_snow | snow below 1 mm of water, below 10 mm, and 10 mm or more |
thunderstorm | rain or sleet with thunder |
to_string(condition) gives the key (partly_cloudy) and label(condition) the English words (partly cloudy); to_string(type) gives none, rain, sleet or snow. The result converts to a one-line summary (std::string, operator<<):
get_weather_series(request, days, seed) returns days consecutive days from request.date (1 to wtg::max_series_days, 36,600, about a century). A series is what to use for a stretch of days: wet days cluster into spells, warm and cold spells last several days, and some months run wetter or colder than their normals, as in the records.
A day depends only on the seed, the cell and the date, not on where the series started:
So a game can ask for today alone each day and still get weather that runs on from yesterday's, and two series that overlap agree on the days they share. Points in the same cell with the same seed get the same weather (apart from the elevation's shift); different cells with the same seed get unrelated weather.
Each call reads 48 days before its first day, so one day alone costs about as much as ten days of a series: a single day takes about 15 µs, a year as one series about 0.5 ms (see make bench).
units = dasmig::unit_system::imperial gives the weather values in °F, inches and mph: tmin, tmean and tmax to 0.1 °F, precipitation to 0.01 in, snowfall to 0.1 in (10 in of snow per inch of water) and wind to 0.1 mph. The values are converted before rounding, so they are the metric day's, not its rounded numbers converted again. Everything else is the same day: frost is still a tmin below 32 °F, the condition thresholds are the metric ones, and the place keeps elevation in metres and distance_km in km. The request's elevation and warming are always metres and °C.
The JSON of an imperial day has "units":"imperial" after the date. A metric day has no units key, so metric JSON is exactly 1.0's.
hourly = true adds each day's 24 hours to weather::hours. The day itself does not change: a day with hours is the day without them plus the hours, and a day's hours, like the day, do not depend on where its series starts. They come from a stream of their own.
| Field | Meaning |
|---|---|
hour, time | the hour of the place's mean solar day, 0 to 23, and its start in UTC |
temperature | at the start of the hour, °C (°F), to 0.1 |
precipitation, snowfall, type, thunder | in the hour; dry hours have type none |
cloud_cover, humidity, wind | in the hour |
daylight_hours, sunshine_hours | the part of the hour the sun is up, and the bright sunshine in it, 0 to 1, to 0.01 |
condition | the hour in a word: the day's cloud thresholds, and per hour light rain below 0.5 mm, rain below 4 mm, heavy rain from 4 mm (snow below 0.5 mm of water, below 2 mm, and more) |
The hours are those of local mean solar time: hour 0 starts at the place's mean midnight, 4 * longitude minutes before 00:00 UTC, so the coolest hour is near sunrise and the warmest in the afternoon at any longitude. Add the place's time zone yourself for clock time; it differs from mean solar time by up to an hour or two, and by daylight saving.
tmin at sunrise to tmax at the warmest hour (four hours before sunset, or midway through a short day), falls to the sunset temperature, then falls as a square root to the next day's tmin at the next sunrise (Cesaraccio et al. 2001). The hours before sunrise finish the previous night, so one day runs into the next with no jump at midnight. In polar day and night there is no sunrise: the hours follow a plain daily wave between tmin and tmax, coolest at 03:00.The JSON of a day with hours has an hours array after sunset. A long hourly series is large: a year is 8,760 hours.
historical = true draws each month around the month as it really was, from January 1991 to December 2020, instead of around the 1991-2020 normals. A cold winter is cold, a drought summer dry and a wet spring wet, while the days within each month are still drawn: the seed decides the weather of each day, and history decides the month's character.
history.tsv holds, for each 2-degree block of land, each month's temperature anomaly (°C), its precipitation as a percentage of the normal and its wet-day anomaly (days), against the 1991-2020 normals of CRU TS 4.10. A cell takes the anomalies of the block its point lies in, so a full-tier cell gets the block's anomaly on its own normals. load() does not read it and it is not compiled in; call load_history() (or load_history(path)) before the first historical request.std::invalid_argument; without the history loaded it throws std::runtime_error. The 48 days read before the first day may fall before 1991 and use the normals.data combines the climate's hash with the history's, so a historical result replays only against the same history.climate_at(latitude, longitude) returns the normals of the cell a point uses, the means its weather is drawn around:
Each element has twelve values, January to December: tmean (°C), dtr (diurnal range, Tmax - Tmin, °C), precipitation (mm per month), wet_days (days with 0.1 mm or more), humidity (%), cloud (% cover) and wind (m/s at 10 m). A point more than 100 km from land throws, as for the weather.
snowfall is the day's fresh snow; nothing tracks snow on the ground or melt.frost is only a tmin below 0, not ground frost.warming if set, unless historical draws 1991-2020 as it was. There are no El Niño years outside that range, no droughts longer than the model's month anomalies carry, and no extremes beyond the fitted distributions.lite), and the wettest days (the 95th percentile) about 0.5 mm lighter. See Data and Method for every measured bias.get_weather(request, seed) and get_weather_series(request, days, seed) give the same result for the same request, seed and data, on every platform, without advancing the instance engine.get_weather(request) and get_weather_series(request, days) draw a fresh seed from the instance engine, seeded from system entropy. There are no generator-level seed() or unseed() controls; for deterministic sequences, keep a dasmig::core::rng in the caller and pass its next value as the seed.w.seed() is the seed a result was generated from: get_weather(request, w.seed()) replays it with the same request and data. Every day of a series carries the series' seed, and get_weather with that seed and the day's date gives the day back.The shipped climate table is a base layer. Files with the climate.tsv columns can be loaded on top of it to add cells, change them or remove them, without editing the shipped file. Rows are keyed by key.
| Column | Required | Meaning |
|---|---|---|
key | yes | the cell's name; the shipped cells use their block centre, "49.00,3.00" |
latitude, longitude | yes | the cell's point; requests use the nearest |
elevation | no (0) | mean elevation, m |
area | no (0) | land area weight for drawing a random place; 0 is never drawn |
tmean, dtr, precipitation, wet_days | yes | twelve values each, separated by ; |
humidity, cloud, wind | no (70, 50, 3.5) | twelve values each: %, % cover, m/s |
area decides whether it can be drawn as a random place. The land mask stays the shipped one, so a layer cell at sea is reached only by points within 100 km of land, and a point within 100 km of its own position always counts as near enough.humidity, cloud and wind only for a temperate place: their defaults (70%, 50% cover, 3.5 m/s) would make a desert half cloudy.dasmig::core::tsv_error with the file, line and column, and the data stays as it was.layers() lists what is loaded: source, mode, rows added, replaced and removed, and warnings such as unknown columns.load(...) replaces the base and drops every layer.Every result records the seed and the data it came from:
to_json() gives the result as JSON, the form the WebAssembly layer and the game consume. Seeds are strings because JavaScript numbers cannot hold every 64-bit value. to_json(writer) writes into an existing dasmig::core::json_writer, for composing larger documents.
sunrise and sunset are null in polar day and night.
Define DASMIG_WEATHER_EMBED before including the header to compile the lite tier into the program. load(dataset::lite) and instance() then use it and need no files. The compiled-in data has the same data_hash() as the file, so seeds replay the same either way. full is never compiled in.
dasmig/weathergen_data.hpp is generated from resources/lite by make embed; CI checks it is up to date.
Each wtg instance is independent. Concurrent calls to the unseeded get_weather(request) and get_weather_series(request, days) on the same instance require external synchronisation (e.g. a std::mutex); the seeded calls and climate_at are const and may run concurrently while the data is not being loaded or extended.
get_weather_batch returns a std::vector of single days, and for_each_weather streams them to a sink in index order without holding the batch. Every item uses the same request with its own seed: with a place set, a batch is many possible versions of one day there; without one, each item draws its own place.
i is exactly get_weather(request, core::item_seed(batch_seed, first + i)). A range can be generated directly (first = 1000 starts at item 1000), two batches that follow each other equal one, and the output is the same for any threads (0 means the hardware count). Without a batch_seed one is drawn like the single getter draws its own.core::batch_options has count, first, threads and unique. The request describes one item; the options describe the batch..unique = core::unique_rule::by<weather>(key) regenerates an item whose key already appeared in the batch (one day per cell, say), with a deterministic retry seed, and throws std::runtime_error naming the item when the attempts run out.core::jsonl_writer writes one to_json() per line, core::csv_writer flattens each result's JSON into dotted columns (place.cell, tmax, ...). Any callable (std::uint64_t index, weather&) works.get_weather_series per place; it is const and safe to run on several threads.| Exception | When |
|---|---|
std::runtime_error | Any generation call or climate_at before data is loaded; a historical request before load_history(); a unique batch that runs out of attempts |
std::invalid_argument | A point more than 100 km from land (weather or climate_at); latitude or longitude out of range or set alone; elevation outside -500 to 9000 m; warming outside -10 to 10; an invalid date or units; days outside 1 to 36,600; a historical day outside 1991-2020; no place requested and no cell with an area |
dasmig::core::tsv_error | A malformed climate.tsv, layer or history.tsv (message gives file, line and column); the data is unchanged |