star-meteo

Time varying meteorological data
git clone git://git.meso-star.fr/star-meteo.git
Log | Files | Refs | README | LICENSE

commit d777910d641333340057146bb9510a89477130a0
parent 28e6331b01a81db47093bca66868711b3ce5c6ee
Author: Vincent Forest <vincent.forest@meso-star.com>
Date:   Mon, 11 Aug 2025 14:29:55 +0200

Update the format and adapt the loading procedure

Add a header to the meteorological data that defines the geographical
location at which the data is provided and the albedo of the ground. The
number of time intervals is also indicated in order to pre-allocate the
required memory.

Add air humidity and relative humidity to the data per time interval.

Diffstat:
Mdoc/smeteo.5 | 48+++++++++++++++++++++++++++++++++++++++++-------
Msrc/smeteo.c | 4++++
Msrc/smeteo.h | 28+++++++++++++++++++---------
Msrc/smeteo_c.h | 7+++++++
Msrc/smeteo_load.c | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/test_smeteo_load.c | 26++++++++++++++++++++++----
6 files changed, 233 insertions(+), 34 deletions(-)

diff --git a/doc/smeteo.5 b/doc/smeteo.5 @@ -12,7 +12,7 @@ .\" .\" You should have received a copy of the GNU Lesser General Public License .\" along with this program. If not, see <http://www.gnu.org/licenses/>. -.Dd May 16, 2025 +.Dd August 11, 2025 .\"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" .Dt SMETEO 5 .Os @@ -25,11 +25,21 @@ A .Nm file stores meteorological data in plain text. -Each line contains a set of meteorological data for a given date. -Lines should be sorted in ascending date order. +The data is provided for a continuous period, discretized into fixed +time intervals. +The albedo of the ground, the geographical position (longitude, +latitude), and the number of intervals are indicated at the beginning of +the file. .Pp -Empty lines are ignored, as are comments, which are strings beginning with the -sharp character +After the header, the following lines contain a set of meteorological +data averaged over a time interval. +For the same file, the duration of the intervals is constant. +The date indicated for each interval corresponds to the middle of the +time interval. +The lines must be sorted in ascending chronological order. +.Pp +Empty lines are ignored, as are comments, which are strings beginning +with the sharp character .Pq Li # . .Pp The grammar of the file format is presented below in a Backus-Naur form. @@ -43,14 +53,32 @@ in descriptions is a comment and does not form part of the description. They are used to provide additional information. .Pp The file format is as follows: -.Bl -column (data-set) (::=) () -.It Ao Va smeteo Ac Ta ::= Ta Ao Va data-set Ac +.Bl -column (#intervals) (::=) () +.It Ao Va smeteo Ac Ta ::= Ta Ao Va header Ac +.It Ta Ta Ao Va data-set Ac .It Ta Ta ... +.It Ta Ta +.It Ao Va header Ac Ta ::= Ta Ao Va albedo Ac +.It Ta Ta Ao Va longitude Ac +.It Ta Ta Ao Va latitude Ac +.It Ta Ta Ao Va #intervals Ac +.It Ta Ta +.It Ao Va albedo Ac Ta ::= Ta Va real No # in [0, 1] +.It Ao Va longitude Ac Ta ::= Ta Va real No # in [0, 180] [deg] +.It Ao Va latitude Ac Ta ::= Ta Va real No # in [0, 90] [deg] +.It Ao Va #intervals Ac Ta ::= Ta Va integer +.It Ta Ta .It Ao Va data-set Ac Ta ::= Ta .Aq Va date .Aq Va Tsrf .Aq Va Tatm +.Aq Va Ahum +.Aq Va Rhum +\e +.It Ta Ta .Aq Va SWdn +.Aq Va SWdn_direct +.Aq Va SWdn_diffuse .Aq Va SWup \e .It Ta Ta @@ -90,7 +118,13 @@ The file format is as follows: .It \ Ta Ta .It Ao Va Tsrf Ac Ta ::= Ta Va real No # Surface temperature >0 [K] .It Ao Va Tatm Ac Ta ::= Ta Va real No # Atmosphere temperature >0 [K] +.It Ao Va Ahum Ac Ta ::= Ta Va real No # Air humidity [g(water)/kg(air)] +.It Ao Va Rhum Ac Ta ::= Ta Va real No # Relative humidity in [0..100] .It Ao Va SWdn Ac Ta ::= Ta Va real No # ShortWave downward flux [W.m^2] +.It Ao Va SWdn_direct Ac Ta ::= Ta Va real +# Direct ShortWave downward flux [W.m^2] +.It Ao Va SWdn_diffuse Ac Ta ::= Ta Va real +# Diffuse ShortWave downward flux [W.m^2] .It Ao Va SWup Ac Ta ::= Ta Va real No # ShortWave upward flux [W.m^2] .It Ao Va Trad Ac Ta ::= Ta Va real No # Radiative temperature >0 [K] .It Ao Va h Ac Ta ::= Ta Va real No # Convective coefficent >0 [W/K/m^2] diff --git a/src/smeteo.c b/src/smeteo.c @@ -92,6 +92,10 @@ smeteo_get_desc(const struct smeteo* smeteo, struct smeteo_desc* desc) *desc = SMETEO_DESC_NULL; + desc->albedo = smeteo->albedo; + desc->longitude = smeteo->longitude; + desc->latitude = smeteo->latitude; + desc->nentries = darray_entry_size_get(&smeteo->entries); if(str_len(&smeteo->filename)) desc->filename = str_cget(&smeteo->filename); if(desc->nentries) desc->entries = darray_entry_cdata_get(&smeteo->entries); diff --git a/src/smeteo.h b/src/smeteo.h @@ -34,30 +34,40 @@ #define SMETEO(Func) smeteo_ ## Func #endif +/* Meteo data for a fixed time interval across the entire dataset*/ struct smeteo_entry { - struct tm time; + struct tm time; /* Time at the middle of the time interval */ + double Tsrf; /* Surface temperature [K] */ double Tatm; /* Atmosphere temperature [K] */ - double SWdn; /* Shortwave downward flux [W/m^2 of horizontal surface] */ - double SWup; /* Shortwave downward flux [W/m^2 of horizontal surface] */ + double Ahum; /* Air humidity [g(water)/kg(air)] */ + double Rhum; /* Relative humidity in [0-100] */ + double SWdn_direct; /* Direct shortwave downward flux [W/m^2] */ + double SWdn_diffuse; /* Diffuse shortwave downward flux [W/m^2] */ + double SWup; /* Shortwave upward flux [W/m^2] */ double Trad; /* Radiative temperature [K] */ double H; /* Convection coefficient [W/K/m^2] */ + double LE; /* Latent flux >0 from ground to atmosphere [W/m^2 ] */ - /* Latent flux >0 from ground to atmosphere [W/m^2 of horizontal surace ] */ - double LE; - - /* Time as a fraction of a day since 10:30pm on 31 December 1849 */ + /* Time as a fraction of a day since 00:00 on 1 january 1859 */ double day_1850; }; -#define SMETEO_ENTRY_NULL__ {{0},0,0,0,0,0,0,0,0} +#define SMETEO_ENTRY_NULL__ {0} static const struct smeteo_entry SMETEO_ENTRY_NULL = SMETEO_ENTRY_NULL__; struct smeteo_desc { const char* filename; + + double longitude; /* Longitude of geographical position [deg] */ + double latitude; /* Latitude of geographical position [deg] */ + + double albedo; /* Ground albedo, for shortwave radiation */ + + /* List of meteo data over a period divided into fixed time intervals */ const struct smeteo_entry* entries; size_t nentries; }; -#define SMETEO_DESC_NULL__ {NULL,NULL,0} +#define SMETEO_DESC_NULL__ {0} static const struct smeteo_desc SMETEO_DESC_NULL = SMETEO_DESC_NULL__; struct smeteo_create_args { diff --git a/src/smeteo_c.h b/src/smeteo_c.h @@ -39,6 +39,13 @@ struct smeteo { struct str filename; + + /* File header */ + double longitude; /* Longitude of geographical position [deg] */ + double latitude; /* Latitude of geographical position [deg] */ + double albedo; /* Ground albedo, for shortwave radiation */ + + /* Meteo data */ struct darray_entry entries; int verbose; /* Verbosity level */ diff --git a/src/smeteo_load.c b/src/smeteo_load.c @@ -25,6 +25,11 @@ #include <string.h> #include <time.h> +/* Error message when reading a line */ +#define ERROR_READ_LINE(SMeteo, TxtRdr, Res) \ + ERROR((SMeteo), "%s: error reading line -- %s\n", \ + txtrdr_get_name(TxtRdr), res_to_cstr(Res)) + /******************************************************************************* * Helper functions ******************************************************************************/ @@ -90,17 +95,18 @@ parse_double struct txtrdr* txtrdr, const char* name, const char* str, - double range[2], /* Inclusive boundaries */ + const double min, /* Inclusive boundary */ + const double max, /* Inclusive boundary */ double* out_dbl) { double dbl = 0; res_T res = RES_OK; - ASSERT(smeteo && txtrdr && name && out_dbl); + ASSERT(smeteo && txtrdr && name && min <= max && out_dbl); if(!str) { res = RES_BAD_ARG; goto error; } res = cstr_to_double(str, &dbl); - if(res == RES_OK && (dbl < range[0] || range[1] < dbl)) res = RES_BAD_ARG; + if(res == RES_OK && (dbl < min || max < dbl)) res = RES_BAD_ARG; if(res != RES_OK) goto error; *out_dbl = dbl; @@ -122,6 +128,16 @@ parse_line(struct smeteo* smeteo, struct txtrdr* txtrdr) { struct smeteo_entry entry = SMETEO_ENTRY_NULL; + /* Shortwave downward flux parsed but not stored internally because it is + * equal to the sum of the direct and diffuse components, both of which are + * analyzed and stored */ + double SWdn = 0; + + /* Shortwave downward shortwave flux calculated from the sum of its + * components, and the epsilon used to compare it with the one read */ + double SWdn_sum = 0; + double SWdn_eps = 0; + char* line = NULL; char* date = NULL; char* hour = NULL; @@ -138,20 +154,40 @@ parse_line(struct smeteo* smeteo, struct txtrdr* txtrdr) if(res != RES_OK) goto error; #define PARSE_DOUBLE(Name, Var, Min, Max) { \ - double range[2] = {Min, Max}; /* Inclusive boundaries */ \ - char* Var = strtok_r(NULL, " \t", &tk_ctx); \ - res = parse_double(smeteo, txtrdr, Name, Var, range, &entry.Var); \ + char* str = strtok_r(NULL, " \t", &tk_ctx); \ + res = parse_double(smeteo, txtrdr, Name, str, Min, Max, &Var); \ if(res != RES_OK) goto error; \ } (void)0 - PARSE_DOUBLE("surface temperature [K]", Tsrf, 0, INF); - PARSE_DOUBLE("atmosphere temperature [K]", Tatm, 0, INF); + + #define PARSE_ATTRIB(Name, Var, Min, Max) \ + PARSE_DOUBLE(Name, entry.Var, Min, Max) + + PARSE_ATTRIB("surface temperature [K]", Tsrf, 0, INF); + PARSE_ATTRIB("atmosphere temperature [K]", Tatm, 0, INF); + PARSE_ATTRIB("air humidity [g(water)/kg(air)]", Ahum, 0, INF); + PARSE_ATTRIB("relative humidity", Rhum, 0, 100); PARSE_DOUBLE("shortwave downward flux [W/m^2]", SWdn, 0, INF); - PARSE_DOUBLE("shortwave upward flux [W/m^2]", SWup, 0, INF); - PARSE_DOUBLE("radiative temperature [K]", Trad, 0, INF); - PARSE_DOUBLE("convection coefficient [W/K/m^2]", H, -INF, INF); - PARSE_DOUBLE("latent flux [W/m^2]", LE, 0, INF); - PARSE_DOUBLE("number of days since 1850", day_1850, 0, INF); + PARSE_ATTRIB("direct shortwave downward flux [W/m^2]", SWdn_direct, 0, INF); + PARSE_ATTRIB("diffuse shortwave downward flux [W/m^2]", SWdn_diffuse, 0, INF); + PARSE_ATTRIB("shortwave upward flux [W/m^2]", SWup, 0, INF); + PARSE_ATTRIB("radiative temperature [K]", Trad, 0, INF); + PARSE_ATTRIB("convection coefficient [W/K/m^2]", H, -INF, INF); + PARSE_ATTRIB("latent flux [W/m^2]", LE, 0, INF); + PARSE_ATTRIB("number of days since 1850", day_1850, 0, INF); + #undef PARSE_DOUBLE + #undef PARSE_ATTRIB + + SWdn_sum = entry.SWdn_direct + entry.SWdn_diffuse; + SWdn_eps = SWdn*1e-6; + if(!eq_eps(SWdn_sum, SWdn, SWdn*1e-6)) { + WARN(smeteo, + "%s:%zu: suspicious downward flux in short waves. It does not appear to be" + "(approximately) equal to the sum of its components (direct + diffuse):" + "%g != %g +/- %g [W/m^2]\n", + txtrdr_get_name(txtrdr), txtrdr_get_line_num(txtrdr), + SWdn, SWdn_sum, SWdn_eps); + } res = darray_entry_push_back(&smeteo->entries, &entry); if(res != RES_OK) { @@ -167,6 +203,94 @@ error: } static res_T +parse_header_var + (struct smeteo* smeteo, + struct txtrdr* txtrdr, + const char* name, + const double min, /* Inclusive boundary */ + const double max, /* Inclusive boundary */ + double* var) +{ + char* line = NULL; + char* tk = NULL; + char* tk_ctx = NULL; + + res_T res = RES_OK; + ASSERT(smeteo && txtrdr && name && min <= max); + + if((res = txtrdr_read_line(txtrdr)) != RES_OK) { + ERROR_READ_LINE(smeteo, txtrdr, res); + goto error; + } + + if((line = txtrdr_get_line(txtrdr)) == NULL) { + ERROR(smeteo, "%s:%zu: missing %s\n", + txtrdr_get_name(txtrdr), txtrdr_get_line_num(txtrdr), name); + res = RES_BAD_ARG; + goto error; + } + + tk = strtok_r(line, " \t", &tk_ctx); + res = parse_double(smeteo, txtrdr, name, tk, min, max, var); + if(res != RES_OK) goto error; + +exit: + return res; +error: + goto exit; +} + +static res_T +parse_header(struct smeteo* smeteo, struct txtrdr* txtrdr) +{ + double ndate = 0; + res_T res = RES_OK; + ASSERT(smeteo && txtrdr); + + #define PARSE_VAR(Name, Min, Max, Var) { \ + res = parse_header_var(smeteo, txtrdr, Name, Min, Max, Var); \ + if(res != RES_OK) goto error; \ + } (void)0 + + PARSE_VAR("albedo", 0, 1, &smeteo->albedo); + PARSE_VAR("longitude", 0, 180, &smeteo->longitude); + PARSE_VAR("latitude", 0, 90, &smeteo->latitude); + + /* To simplify, parse the number of intervals in double precision and ensure + * that it can encode an integer. The representation of doubles allows all + * integers up to 2^48 to be represented. This is therefore the upper limit of + * the analyzed value, since even if a larger integer could be analyzed, there + * would be no guarantee that it would actually correspond to the integer + * entered */ + PARSE_VAR("date count", 0, (double)(1llu<<48), &ndate); + + #undef PARSE_VAR + + /* Ensure that the number of dates is an integer + * (for simplicity, it has been analyzed as a double) */ + if(ndate != (size_t)ndate) { + ERROR(smeteo, "%s:%zu: invalid date count `%g'\n", + txtrdr_get_name(txtrdr), txtrdr_get_line_num(txtrdr), ndate); + res = RES_OK; + goto error; + } + + /* Pre-allocate the list of time intervals */ + res = darray_entry_reserve(&smeteo->entries, (size_t)ndate); + if(res != RES_OK) { + ERROR(smeteo, + "%s: error allocating the data list by time interval -- %s\n", + txtrdr_get_name(txtrdr), res_to_cstr(res)); + goto error; + } + +exit: + return res; +error: + goto exit; +} + +static res_T load_stream(struct smeteo* smeteo, FILE* fp, const char* name) { struct txtrdr* txtrdr = NULL; @@ -183,9 +307,11 @@ load_stream(struct smeteo* smeteo, FILE* fp, const char* name) goto error; } + if((res = parse_header(smeteo, txtrdr)) != RES_OK) goto error; + for(;;) { if((res = txtrdr_read_line(txtrdr)) != RES_OK) { - ERROR(smeteo, "%s: error reading line -- %s\n", name, res_to_cstr(res)); + ERROR_READ_LINE(smeteo, txtrdr, res); goto error; } if(!txtrdr_get_cline(txtrdr)) break; /* No more line */ diff --git a/src/test_smeteo_load.c b/src/test_smeteo_load.c @@ -15,6 +15,7 @@ #include "smeteo.h" +#include <rsys/math.h> #include <rsys/mem_allocator.h> #include <string.h> @@ -27,13 +28,19 @@ check_api(struct smeteo* smeteo) { struct smeteo_desc desc = SMETEO_DESC_NULL; const char* filename = "test.txt"; + const double albedo = 0.314; + const double longitude = 10.42; /* [deg] */ + const double latitude = 16.01; /* [deg] */ FILE* fp = NULL; CHK((fp = fopen(filename, "w+")) != NULL); - #define CHECK_EMPTY_FILE(Name) { \ + #define CHECK_NO_DATA(Name) { \ CHK(smeteo_get_desc(smeteo, &desc) == RES_OK); \ CHK(!strcmp(desc.filename, Name)); \ + CHK(eq_eps(desc.albedo, albedo, 1e-6)); \ + CHK(eq_eps(desc.longitude, longitude, 1e-6)); \ + CHK(eq_eps(desc.latitude, latitude, 1e-6)); \ CHK(desc.entries == NULL); \ CHK(desc.nentries == 0); \ } (void)0 @@ -41,14 +48,25 @@ check_api(struct smeteo* smeteo) CHK(smeteo_load(smeteo, NULL) == RES_BAD_ARG); CHK(smeteo_load(NULL, filename) == RES_BAD_ARG); CHK(smeteo_load(smeteo, "none.txt") == RES_IO_ERR); - CHK(smeteo_load(smeteo, filename) == RES_OK); /* Empty file should be OK */ - CHECK_EMPTY_FILE(filename); + /* An empty file is not valid: at least the header is required */ + CHK(smeteo_load(smeteo, filename) == RES_BAD_ARG); + + CHK(fprintf(fp, "%g # Albedo\n", albedo) > 0); + CHK(fprintf(fp, "%g # Longitude [deg]\n", longitude) > 0); + CHK(fprintf(fp, "%g # Latitude [deg]\n", latitude) > 0); + CHK(fprintf(fp, "0 # Date count\n") > 0); + CHK(fflush(fp) == 0); + + CHK(smeteo_load(smeteo, filename) == RES_OK); + CHECK_NO_DATA(filename); + + rewind(fp); CHK(smeteo_load_stream(NULL, fp, "foo") == RES_BAD_ARG); CHK(smeteo_load_stream(smeteo, NULL, "foo") == RES_BAD_ARG); CHK(smeteo_load_stream(smeteo, fp, NULL) == RES_BAD_ARG); CHK(smeteo_load_stream(smeteo, fp, "foo") == RES_OK); - CHECK_EMPTY_FILE("foo"); + CHECK_NO_DATA("foo"); #undef CHECK_EMPTY_FILE