AEME 0.4.0
Choosing which GLM-AED sediment, flux and diagnostic output to keep
GLM-AED writes far more than the standard variables: sediment temperature and heat flux, sediment-water fluxes (lake-wide and by sediment zone), sediment pools and AED diagnostics. read_glm_output() has always swept these into the diag / sediment sub-lists of the model output under their raw netCDF names; this release makes them easy to request, name and use, e.g. from aemetools sensitivity analysis and calibration, without carrying the whole payload in every ensemble member.
- New dataset
glm_extra_vars: a registry giving these variables AEME names (SED_temp,SED_hflux,SED_ztemp,SED_Fsed_oxy, … for sediment-zone variables;SEDL_Fsed_oxy, … for the matching lake-wide series), labels and units. Generated fromdata-raw/glm_extra_vars.csv. Either the AEME or the raw netCDF name works wherever a variable is looked up:get_var(),check_var_in_output(),check_aeme_vars()andplot_*(). - New
extra_varsargument onrun_aeme(),run_aeme_ensemble(),load_output(),read_model_outputs()andread_glm_output()to choose which of those variables are read:NULL(default, as before) reads all,"none"none, or a vector of names and/or the keywords"sediment"/"diag". Variables not requested are never read fromoutput.nc. In a test lake this cut the stored output from 18.9 MB to 3.5 MB per run, which adds up over an ensemble.run_aeme()andrun_aeme_ensemble()also now exposekeep_diag. - New
sed_layersargument to keep only some sediment layers ofSED_temp, andzone_summaryto add an area-weighted lake-wide series for each sediment-zone variable (<name>_lake, using the time-varying zone areazarea; zones with no area are ignored). Naming a series such as"SED_Fsed_oxy_lake"inextra_varsbuilds just that series. -
get_var()gainszoneandsed_layerto subset sediment variables. -
get_output_vars()labels these variables from the registry. - Fixed
check_var_in_output()reporting variables held in thediag/sedimentsub-lists as missing, and sediment-zone variables (*_Z) requested throughvars_simbeing returned as bare arrays rather than labelled grouped variables.
Observations recorded relative to the lake bed
-
add_obs()gainsdepth_ref(and accepts adepth_refcolumn inlake) so an observation can be recorded as a height above the lake bed ("bottom"), for example a logger 1 m above the sediment, instead of a depth below the surface ("surface", the default and the usual case). Stored observations keepdepthin metres below the surface, so everything that reads it is unaffected. A bottom-referenced observation also storesheight_above_bedanddepth_ref, and itsdepthis worked out aslevel - heightfrom the lake level observations (interpolated in time), or from the full depth of the basin with a warning if there are none. Observations that are all surface-referenced get no new columns. -
get_var(use_obs = TRUE)compares a bottom-referenced observation with the model at (the model’s own water level -height_above_bed) on that date, rather than at the depth it was placed at when added. It also no longer fails when the observations fall on a single date. -
ensemble_summary()now places observations in the reference of the summary before matching them todepths. Withdepth_ref = "bottom"a surface observation is converted to a height above the bed, and with"surface"a bottom observation to a depth below it, using the lake level observations (interpolated in time) or, where there are none, the ensemble median of the modelled level. Previously observations were matched todepthsas depths below the surface whateverdepth_refwas, so with"bottom"they were misaligned.score_ensemble()uses the same observations. -
get_obs_column_names(include_optional = TRUE)now also returnsdepth_refandheight_above_bed.
get_var(depth_ref = "bottom") follows the water level
-
Behaviour change.
get_var(depth = d, depth_ref = "bottom")now reads the profiledmetres above the lake bed, i.e.level - dmetres below the surface on each date, using the modelled water level (LKE_lvlwtr). It previously used the basin’s full depth as a fixed datum, so “1 m from the bottom” was always read at (full depth - 1) m below the surface whatever the water level; the two only agree when the level equals the full depth. With levels that move (inflow/outflow, drawdown, or an extended hypsograph) the old answer was off by the difference between the level and the full depth, which reached about 3 m and over 1 degC in an ensemble test. On a date when the level is belowdepththe value is nowNArather than the surface value. Results fromget_var(), and fromensemble_summary(depth_ref = "bottom")which calls it, will change for bottom-referenced depths; surface-referenced extraction is unchanged.write_aeme_parquet(depth_ref = "bottom")uses the same definition.
write_aeme_parquet() can summarise at fixed depths
- AEME stores GLM output on a depth grid scaled to each day’s lake level, so members whose water level differs report different depths and the summary tier, which collapses across members at each stored depth, ended up with one or two members per group.
write_aeme_parquet()gainsdepths(anddepth_ref): each member’s profile is linearly interpolated to the requested depths on every date before the members are collapsed. A depth outside a member’s water column on a date is left out for that member, son_ensis the number of members that contributed. -
depth_ref = "surface"(default) measuresdepthsin metres below the water surface;"bottom"measures them in metres above the lake bed, converted per member and date from the member’s water level (LKE_lvlwtr). - The summary tier has a new
depth_refcolumn ("surface"or"bottom"for depth-resolved variables,NAfor variables with no depth dimension).output_rawis unchanged and always holds the stored depths in metres below the surface.
Faster write_aeme_parquet() summary tier
- The ensemble summary tier written by
write_aeme_parquet()is now computed without a list-column per group. The standard depth grid is scaled to each member’s water level, so depths differ between members and a 12-member ensemble has about a million summary groups; the olddplyr::summarise()over them took over a minute (66 s of a 96 s write). The same table now takes a second or two (the whole write about 15 s). The values are identical to the previous implementation, checked on the full ensemble and on edge cases (missing values, single members, ties). New tests cover the writer, which had none.
Ensemble summary and plotting moved from aemetools
-
ensemble_summary()andplot_ensemble()now live in AEME, next torun_aeme_ensemble()andwrite_aeme_parquet().ensemble_summary()extracts each member once and returns per-datemean,sd,nand quantiles with the member frame and depth-aligned observations;plot_ensemble()draws a ribbon or per-member lines from anAemeobject or a summary. Observation timestamps (stored as UTCPOSIXct) are reduced to calendar dates to match daily output. Requires dplyr >= 1.1.0. - New
score_ensemble()verifies an ensemble against the lake observations (from anensemble_summary()or anAemeobject): coverage of the central interval, bias and RMSE of the ensemble mean, spread, spread-skill ratio and mean CRPS, per model, variable and depth and pooled over depths. Coverage of a small ensemble is biased low even when it is reliable (about(n - 1) * conf_int / (n + 1)fornmembers); the documentation says how to read it.
Parameter helpers moved from aemetools
-
zone_ratio_param(),zone_offset_param()andexpand_zone_ratios()(plusis_zone_ratio()andzone_base_name()) now live in AEME.input_model_parameters()expands zone-ratio and zone-offset rows itself, so parameter sets that use them work with every function that writes parameters, includingrun_aeme_ensemble(). -
encode_param(),decode_param()anddecode_param_full()now live in AEME. Thegroup/name[index]format is unchanged.encode_param()returnscharacter(0)for empty input instead of"/[]", and decoding a string that is not in the format givesNAinstead of a wrong group and name.
run_aeme_ensemble() runs supplied parameter sets
- New
run_aeme_ensemble()runs a model once per supplied parameter set (a list ofparamdata.frames, or one long data.frame with anensemblecolumn) and stores each member asoutput(aeme)$ens_001 ... ens_00k, the structureget_var(ens_n =)andwrite_aeme_parquet()already use. Members that fail are dropped with a warning;parallel = TRUEruns each worker in its own copy of the model directory. Sampling sets from parameter bounds and extracting a PEST++ posterior stay in the aemetools package. -
run_aeme_ensemble()also drops members that fail silently: GLM exits normally when an unphysical parameter set drains the lake, leaving a water level at or below zero and thenNA. Withdrop_incomplete = TRUE(default) such members are removed and named in the warning, along with the first error message when a member errors. - New article “Parameter uncertainty with ensembles” (Uncertainty menu) covering sampled and supplied ensembles and
write_aeme_parquet()/read_aeme_parquet().
estimate_zone_fluxes() documentation and Tier 2 normalisation
-
Tier 2 (observed-data) adjustment now preserves the lake-wide total. The documentation has always said that only inter-zone ratios are adjusted and the lake-wide area-weighted total is preserved, but the
fsed_ammandfsed_frpmultipliers were applied after the Tier 1 normalisation and not re-normalised, so the total drifted whenever observations were available. The adjusted fluxes are now re-normalised. Lakes built with observations of near-bed NH4 or FRP will seefsed_amm/fsed_frpzone values change; the area-weighted lake average equals the baseline again. - Documentation corrected: only NH4 and FRP are adjusted from observations (the O2 and NO3 adjustments are disabled), the depth scale factor is capped at 2 (not 4), the NO3 sign change is at 0.6 (not 0.5) of the maximum depth, and zones are the GLM sediment zones of a built model.
- The normalisation is now an internal function,
normalise_zone_flux(), with unit tests.
Phytoplankton default fixes and a new succession-diagnostic function
Follow-up to an investigation into whether GLM-AED can reproduce genuine multi-group phytoplankton succession (diatom/green/cyanobacteria), which surfaced two structural issues in the shipped aed_phyto_pars.csv template (inst/extdata/aed/aed_phyto_pars.csv) that affect every lake built from package defaults, not just the one investigated:
-
X_nmin/X_nmax/X_pmin/X_pmax(Droop internal-quota bounds) were byte-identical across every phytoplankton group. Under this template’s configuration (simINDynamics/simIPDynamics = 2for every group), these bounds – notK_N/K_P, which only apply in a rare edge case – are the actual nutrient-competition lever, so shipping them identical meant no group-level nutrient differentiation was possible regardless of what else was tuned. Given modest, literature-informed, stability-tested differentiation: cyanobacteria get a wider phosphorus luxury-storage range (X_pmin0.001->0.0007,X_pmax0.005->0.009, reflecting documented polyphosphate storage in bloom-forming taxa), and diatoms get a tighter, more efficient nitrogen/phosphorus economy (X_nmin0.02->0.025,X_nmax0.07->0.055,X_pmin0.001->0.0012,X_pmax0.005->0.0038, reflecting documented low-luxury nutrient uptake in diatoms relative to greens/ cyanobacteria). Every other group is unchanged. -
simSiUptakewas 0 (off) for every group, includingdiatom. Peraed_phyto_pars.csv’s own dbase andaed_bio_utils.F90’sphyto_fSi(), this switch gates whether silica limitation is computed at all –fSidefaults to1(unlimited) whenever it’s off. A diatom group structurally requires silica; shipping the default with this switch off meant silica limitation was silently inert for the one group it’s supposed to apply to. Now1fordiatom, unchanged (0) for every other group.
(A third candidate fix – aed_zooplankton being absent from a lake’s active &aed_models list, which silently zeroes out all top-down grazing – turned out to already be correct in this package’s own default inst/extdata/aed/aed.nml; the omission found during the investigation was specific to one external project’s lake configuration, not a package default.)
aed_succession_index()
New exported function computing a system-level diagnostic for whether a model run shows genuine, recurring, multi-group phytoplankton succession – computed purely from model output via get_var(), no observations required. Returns three components (evenness, dom_entropy, periodicity) plus a composite; see ?aed_succession_index for what each one catches and the two failed designs (raw autocorrelation; naive mode-matching) that led to the current match_rate/regime_shift construction for periodicity. Intended as a PEST regularisation observation (biasing a calibration away from single-group-monopoly or one-way-drift solutions) or as a standalone diagnostic.
Daily-mean output for GLM-AED and Simstrat
set_output_time_step(aeme, frequency, daily_mean = TRUE) (new argument, also time(aeme)$output_daily_mean) makes every model produce a daily-mean output stream alongside its raw frequency output, so a run can be done at a sub-daily cadence while the results compared against daily observations are true daily means rather than instantaneous snapshots – the behaviour GOTM’s native output_daily stream already provided, now available for all three hydrodynamic models.
-
GOTM-WET keeps writing its daily means itself; the
output_dailyblock ofoutput.yamlis now restricted to the targeted variables (themodel_controlssimulateset plus the internals the reader needs) instead of every variable. -
GLM-AED and Simstrat have no native time-averaging, so
run_aeme()averages their sub-dailyoutput.ncby calendar day into a companionoutput_daily.nc(same structure, one record per day). The raw sub-daily file is kept. The companion carries only the variables the reader consumes — the targetedmodel_controlsset plus each reader’s internals — so for GLM, whose own netCDF cannot be sub-selected, the companion is also where the stored output is pruned (its large 4-Dlight/umean/ wave fields are dropped): ~20 variables instead of ~60, and the averaging pass is several times faster and smaller. -
run_aeme()/get_var()read the daily means transparently: whenoutput_daily_meanisTRUEtheoutput_daily.nccompanion is read if present, otherwise the rawoutput.ncis averaged by calendar day on read. - Model integration is unchanged (
dt/Timestep sare untouched); the cost is the extra in-run output I/O and one post-run averaging pass. - Averaging arbitrary (non-daily) storage cadences is not yet supported.
Observation dates are POSIXct
The Date column of observations$lake and observations$level is now a UTC POSIXct rather than a Date. Daily observations are anchored at 12:00:00 UTC, so a daily value sits unambiguously inside its calendar day when matched against a sub-daily (POSIXct) model axis – midnight is a day boundary. Genuinely sub-daily observations keep their time-of-day.
- The conversion happens at every entry point:
yaml_to_aeme(),lake_obs_to_aeme(),add_obs(),read_aeme_from_files()andaeme_constructor()(a non-Date/POSIXctcolumn is coerced with a warning). Objects loaded from an older.rdsare converted silently bymigrate_aeme()on the firstcheck_aeme()/show()/plot()/build_aeme(), or explicitly byupgrade_aeme(). - Observation-to-model matching remains a calendar-day join throughout (
get_var(use_obs = TRUE),assess_model(),align_depth_data(), the water-balance surface-temperature and level fits,plot_*overlays), so results are unchanged for daily runs.
Timezone of input data
The model runs entirely in UTC: input timestamps are converted to UTC once, at ingest; every model gets UTC boundary conditions (GLM’s nml timezone stays 0); output is read back and stored in UTC; and only plots / summary() render in local time. You never manage timezones per model or on the way out.
-
Declare the timezone of your input data once, via
aeme_constructor(tz = ...),new_aeme(tz = ...),build_aeme(tz = ...)orset_time(tz = ...). It is stored astime$tz(a concrete Olson name) and round-trips throughtime.csv,aeme.yamland.rds. -
Default
tzis"UTC". Gridded reanalysis (ERA5, etc.) and model conventions are all UTC, so most workflows need nothing. Set a non-UTC zone only when your source data really is in local time. Legacy.rdsobjects migrate totime$tz = "UTC". - Timestamps you supply –
time$start/time$stopand the date columns of meteo, inflow, outflow and observation inputs – are read as wall-clock time intime$tzand converted to UTC at ingest (add_met(),add_inflows(),aeme_constructor(),yaml_to_aeme()). Daily (calendar) data is never shifted. - When you declare a non-UTC
time$tz, a date column that a CSV reader tagged"UTC"(readr/read.csv’s default) is reinterpreted in your declared zone –"UTC"is an unreliable label there. A column carrying a genuinely different timezone (e.g."America/New_York") is always kept as an absolute instant. -
standardise_met()now warns when sub-dailyMET_radswdpeaks more than 3 h from astronomical solar noon for the lake’s longitude – the usual sign that meteo timestamps are in local time rather than UTC. - Datetime parsing across the model-output readers,
check_time(),standardise_met()/standardise_inflow(),aeme_time_axis()and the variable-index helpers is now explicitly UTC and independent of the session timezone.show()/summary()printtime$tzand renderstart/stopin it. - GLM’s numeric
timezonenml field is a solar-geometry parameter, unrelated totime$tz, and is unchanged.
Initial conditions
- New
set_initial_conditions()/get_initial_conditions()expose the initial water depth, temperature/salinity profile, and biogeochemical water-column pools thatbuild_aeme()writes into each model. Values can be set once for every model or overridden per model viamodel_init(e.g. a GLM-AED-specific temperature profile). The specification is stored inconfiguration(aeme)$initial_conditionsand resolved per model at build time (generic defaults <- per-model overrides); it survivesload_configuration(). Scalar water-quality values fold intomodel_controls$initial_wc; depth-resolved (depth/value) profiles are applied to the built GLM-AED / Simstrat directories via the existingset_*_init()writers. - New
assess_spin_up()builds and runs an ensemble of initial-condition perturbations across a range of spin-up lengths and reports how quickly the ensemble spread at the start of the analysis period collapses, with an optional recommended spin-up. The summary gives per-depth-then-averagedspread/driftin the variable’s units plus dimensionlessspread_cv/drift_cv(normalised by the ensemble mean), andmetricselects which the recommendation targets.plot_spin_up()plots the chosen metric against spin-up length.
Sub-daily (hourly) forcing and output
-
timegains anoutput_time_stepelement (seconds; default86400, i.e. daily) separate from the integrationtime_step.set_time()gainstime_step/output_time_steparguments, and both round-trip throughtime.csv,aeme.yaml, and.rds(objects/files without the new field default to daily). - Supplying sub-daily meteo / inflow (a
POSIXctDatecolumn, or any sub-daily-spaced series) is now carried throughbuild_aeme()at its native resolution instead of being collapsed to daily, and written to GLM-AED (subdaily/nsave, full timestamps), GOTM-WET (output.yamlcadence, real time-of-day,time/dt), and Simstrat (Output.Times) accordingly. Setoutput_time_step(e.g.3600) to get sub-daily output; model output is read back asPOSIXctwhen it carries a time-of-day and asDate(unchanged) when it does not. - AEME does not temporally disaggregate forcing: sub-daily runs require forcing supplied at (at least) that cadence, and
build_aeme()aborts otherwise. DYRESM-CAEDYM and the water-balance path remain daily. - Reading output back no longer silently discards everything when the reconstructed output time axis is longer than the records a run actually wrote (e.g. an hourly
output_time_stepset on a run still written daily, or astopthat is not an exact multiple of the output step). The model readers now keep the overlapping steps and warn, andget_date_index()trims the index to the file (and warns) when given apath/lake_dir.get_var(use_obs = TRUE)against sub-daily output now matches one model step per observation day (nearest midnight) instead of joining each daily observation to all 24 hourly steps. - GLM computes its
daily_*diagnostics – surface energy fluxes, evaporation, surface area/temperature, lake level, in/out/overflow volumes – once per simulated day, so on a sub-dailyread_glm_output()they previously came backNAat ~23 of every 24 steps (and any variable derived from them, e.g.LKE_evprte,HYD_surft). Each day’s single written value is now carried across that day’s sub-daily steps; daily runs are unaffected. - The plotting functions now handle a sub-daily (
POSIXct) output axis:plot_output()/plot_model_output()/plot_glm_output()heat-map tiles are drawn at the real output step instead of collapsing to 1-second slivers;plot_output(backend = "base")no longer errors on the non-uniqueas.Date()axis; observation overlays (align_depth_data()) match daily observations to the model day rather than an exact timestamp; and axis date labels adapt to the plotted span.
New model
- Added Simstrat-AED2 (
"simstrat_aed2") as a fourth supported model, alongside DYRESM-CAEDYM, GLM-AED, and GOTM-WET, with the fullbuild_aeme()/run_aeme()/output-reading pipeline:build_simstrat(),run_simstrat_aed2(),read_simstrat_output(),check_simstrat_par(),write_simstrat_nc(), and AED2 biogeochemistry support viainitialise_aed2(). - Added Simstrat-AED (
"simstrat_aed") as a fifth supported model, coupling Simstrat to AED (v3) instead of AED2 - the same actively developed biogeochemical library GLM-AED already links, rather than the older AED2. Shares the module-activation/cross-module-dependency logic with GLM-AED via a new common engine (resolve_aed_active_modules()inR/aed_modules.R) instead of a second independent copy, so the two AED couplings behave identically for the samemodel_controls. Newrun_simstrat_aed(),initialise_simstrat_aed(),install_simstrat_aed()/list_simstrat_aed_versions()/simstrat_aed_exe_path(), andsimstrat_aed_parametersdataset;build_simstrat()gained abgc_lib = c("aed2", "aed")argument and now shares its AED config templates (inst/extdata/aed/) with GLM-AED rather than each model carrying its own copy. - Simstrat inflow handling reworked so it can carry nutrient/heat load like GLM-AED.
make_inf_simstrat()now:- converts AED/AED2 inflow concentrations to the model’s native units via
conversion_aed(the same conversionmake_inf_glm()applies) - previously written unconverted; - merges multiple inflow streams with a flow-weighted mean of every concentration-like quantity (
HYD_temp,CHM_salt, each BGC var) instead of summing the BGC concentrations, so the single combined series carries the same total load as GLM-AED’s per-stream inflows; - gained the
AEME.simstrat_inflow_loadoption ("none"(default) /"bgc"/"all") controlling whetherTinp.dat/Sinp.datand the AED inflow files are written depth-integrated (effective) or single-point (inert)."none"keeps the pre-0.4.x behaviour."bgc"makes the inflow nutrient load effective."all"also advects inflow temperature/salinity but is experimental - it currently produces an unphysical warm surface bias because Simstrat does not plunge a surface point source the way GLM does. When effective, the advected scalar is forced to0on dates with negligible inflow.
- converts AED/AED2 inflow concentrations to the model’s native units via
- Simstrat-AED benthic zones are now sized to the lake. When
use_bgcis on and thesimstrat.parAEDConfigblock runs a zoned benthic mode (BenthicMode = 2),build_simstrat()setsNZones/ZoneHeightsfrom the hypsography via the sameestimate_sed_zones()helperbuild_glm()uses for GLM-AED, instead of leaving the template’s hard-coded values. Falls back to the template values if the estimate can’t be computed.
Non-cohesive sediment (aed_noncohesive)
- The bundled AED template (
inst/extdata/aed/aed.nml) now ships an&aed_noncohesiveblock with two suspended-sediment groups (num_ss = 2), including constant settling and shear-driven resuspension defaults, and listsaed_noncohesivein&aed_modelsimmediately afteraed_sedflux. The&aed_noncohesiveblock is placed after&aed_sed_const2din the file: libaed reads the module namelists in one forward pass without rewinding betweenaed_sedflux(which also consumes&aed_sed_const2d) andaed_noncohesive, so an earlier block is never found and GLM aborts with “ERROR reading namelist aed_noncohesive”. - Module activation is wired up in
R/aed_modules.R: theNCSvariable prefix maps toaed_noncohesive, so simulating anyNCS_ss*variable now activates the module (it carries no forced cross-module dependencies).set_glm_aed_models()’s defaultaed_modelsincludes it. -
&aed_totalsnow counts both groups as TSS (TSS_vars = 'NCS_ss1','NCS_ss2').set_aed_totals()re-derivesTSS_vars/TSS_varscaleduringbuild_aeme()from the activeaed_noncohesivenum_ss(oneNCS_ss<i>per group, unit scaling), so the totals survive a build.
Observations schema: single depth column
- The lake observations data frame now uses a single required
depthcolumn (nominal sampling depth, m positive-down from the surface) in place of thedepth_from/depth_topair. Every analytical consumer already collapsed that pair to its midpoint, so this matches how observations were actually used.get_obs_column_names()returnsc("Date", "var_aeme", "depth", "value"); passinclude_optional = TRUEfor the optional columns. - Two optional columns are recognised:
depth_to(bottom of an integrated sample, for provenance - not consumed by AEME core) andsd(measurement standard deviation, in the variable’s units, for 1/sd weighting in PEST-style calibration downstream). - Backward compatible:
add_obs(),lake_obs_to_aeme(),yaml_to_aeme(), the constructor, andmigrate_aeme()/upgrade_aeme()all accept the legacydepth_from/depth_tolayout and collapse it todepth(interval midpoint) with a one-time deprecation warning. Objects loaded from older.rdsfiles are migrated automatically oncheck_aeme()/show()/plot(). -
lake_obs_to_aeme()gained optionaldepth_to_col_nameandsd_col_namearguments. - Fixed a latent bug in
input_model_parameters()that computed a half-thickness ((depth_to - depth_from) / 2) instead of a midpoint; the value was unused and the line has been removed.
Model binaries moved out of the package
Model executables are no longer bundled inside the package - they’re now downloaded on demand from GitHub release assets into a persistent local cache, verified against a published SHA256 checksum before use. This keeps the installed package small and lets binaries be updated independently of the R package version.
-
install_glm_aed(),install_gotm_wet(),install_dy_cd(),install_simstrat_aed2()— download and verify a specific (or the"latest") model executable version for the current platform. Pairedlist_*_versions()(what’s published, per platform) and*_exe_path()(locate an already-installed executable) helpers for each model.install_glm_aed()supports Windows, macOS, and Linux (including bundled dylibs on macOS); GOTM-WET, DYRESM-CAEDYM, and Simstrat-AED2 are currently Windows-only, matching the platforms binaries have actually been built for. -
install_models()— convenience wrapper installing the latest available version of every model (or a chosen subset) in one call; models with no published binary for the current platform (or no release published yet) are reported and skipped rather than blocking the others. -
run_gotm_wet()andrun_dy_cd()now resolve their executable the same wayrun_glm_aed()already did: an explicitAEME.gotm_exec/AEME.dyresm_execoption, then a requested/previously installed version, with a clear error pointing atinstall_gotm_wet()/install_dy_cd()if nothing is found.get_gotm_wet_version()andget_dy_cd_version()updated to match. -
inst/extbin/gotm_wet/andinst/extbin/dy_cd/removed from version control (still used locally if present - see.gitignore). - GLM version handling reworked to support multiple installed versions side by side (switch between them via
glm_exe_path()/AEME.glm_versionwithout re-downloading), with corrected OS detection and a GLM version passed through correctly to parallel workers inrun_aeme(parallel = TRUE). -
install_glm_aed()/list_glm_versions()now understand a trailing"+"on a version (e.g."4.0.0+"), selecting the GLM+ (AED+) build published asglm-<os>-<version>+.zip. The.github/workflows/build-glm-v4.yamlworkflow was rewritten along the lines ofbuild-simstrat.yamlto build both the regularglmandglm+binaries (the latter linking the privatelibaed-riparian/-light/-devmodules) via explicit per- librarymakesteps, instead of AED_Tools’build_glm.sh/build_env.inc/build_aedlibs.inc.
GLM v4 hydrodynamic configuration
-
build_aeme(model = "glm_aed")now ships and selects a GLM-v4 hydrodynamic namelist. Aglm4.nmltemplate was added atinst/extdata/glm_aed/, andbuild_glm()copies it in (instead of alwaysglm3.nml) when the pinned/installed GLM binary is v4 — resolved via.preferred_glm_major_version(), the same priority orderfind_glm_nml()uses. Falls back toglm3.nmlwhen the version can’t be determined or no matching template ships. - The
&sedimentblock is now merged rather than overwritten when a model is (re)built.make_stg_glm()still refreshes the zone geometry and per-zone parameters AEME derives from the bathymetry (n_zones,zone_heights,sed_temp_*,sed_reflectivity,sed_roughness, …), but preserves the expanded GLM-v4 soil-column heat-model keys aglm4.nmlcarries (sed_heat_model,n_sed_layers,sed_layer_depth,sed_vwc,sed_spinup_days,sed_deep_temp). Undersed_heat_model = 2,sed_heat_Ksoil/sed_temp_depthare left as the template’s scalars instead of being expanded to per-zone vectors. - The GLM-v4
&mass_balanceblock is populated from the AED variables that are switched on. New internalset_glm_mass_balance()fillsbalance_vars/balance_varnumstraight from the&init_profileswq_namesthatinitialise_glm()has just written, so the two lists cannot drift apart; with biogeochemistry off, or no qualifying variable, it defaults tobalance_varnum = 0and dropsbalance_vars. Only touches the nml when a&mass_balanceblock is already present (i.e. aglm4.nmltemplate). -
initialise_glm()no longer writes the aggregate totals (NIT_tn,PHS_tp,CAR_toc→ AED diagnosticsTOT_tn/TOT_tp/TOT_toc), particulate-inorganic pools (PHS_pip→PHS_frp_ads,NIT_pin),PHY_tchla, or theNCS_ss*groups into&init_profileswq_names— none are GLM-AED water-column state variables, and GLM aborts withCannot find "<var>" for initial value(and, on GLM v4, the equivalent... for mass balance output) when they appear. The exclusion list is shared via the new internalglm_non_state_vars()and mirrors whatinitialise_aed()already drops. -
build_glm()now forcessed_heat_modelback to1whenuse_bgc = FALSE: GLM v4’s dynamic soil-temperature solver (sed_heat_model = 2,zZSoilTemp) is provided by the WQ library and GLM aborts with it enabled but no active WQ module.check_glm_nml()gained a matching validation rule that flagssed_heat_model = 2without an active&wq_setup(wq_lib = 'aed'/'api').
Restricting model output for calibration
-
set_output_vars(aeme, model, vars, mass_balance = TRUE)— rewrites the output section of a model’s configuration so onlyvars(mapped to each model’s own output names viakey_naming), plus the handful of internals AEME always needs to read a result back, are written. Aimed at calibration / sensitivity analysis, where the objective uses one or two variables but every model otherwise writes its full state every step. Per model: GLM-AED drops the fixed-depthWQ_*.csvpoint outputs and, withmass_balance = FALSE, the&mass_balanceblock — the whole-lakelake.csvis kept, because GLM 4.x only writes the netCDF diagnostic scalars (lake_level, …) while that CSV is open; Simstrat switches off “write everything” and pins the variable list, cutting ~25*_out.datfiles to a handful; GOTM-WET replaces the/*all-variables output source with an explicit list; DYRESM-CAEDYM has a fixed output form and is left unchanged. The change is made in memory — callwrite_configuration()(or usebuild_aeme(), below) to write it out. -
build_aeme()gainedoutput_varsandmass_balancearguments: whenoutput_varsis supplied,build_aeme()appliesset_output_vars()to every built model and re-writes the trimmed configuration to disk, so a lake can be built restricted from the start.output_vars = NULL(the default) leaves every model writing its full output, unchanged from before.
OS-aware model selection
-
check_model()gained anos_validargument: whenTRUE, restricts the requested models to ones actually runnable on the current platform (DYRESM-CAEDYM, GOTM-WET, and Simstrat-AED2 need Windows; GLM-AED runs everywhere), falling back to GLM-AED with an informative message rather than failing outright.run_aeme()now applies this automatically. - An
Aemeobject now remembers which model(s) it was last configured for (configuration(aeme)$model, defaulting to"glm_aed"), solist_models(aeme)reflects the actual configured model set instead of always listing every model AEME supports.
New functions
-
upgrade_aeme()— upgrades anAemeobject saved by an older AEME version to the current layout, in idempotent steps, reporting what it changed. On top of the per-model backfills (time$spin_up,inflows$factor,outflows$factor,configuration) it renames the legacyoutflows$lvl/outflows$outflow_lvlelement tooutflows$elevation, adds the per-modeloutputplaceholders and an integern_members, coerces a legacyobservations$leveltibble to a plain data frame, fills scalarconfigurationbuild defaults fromconfig_defaults(), and reordersparameterscolumns toparam_colnames()order. It does not rebuild model configuration or output — rerunbuild_aeme()for those. Stampsconfiguration$aeme_upgradedwith the installed version. -
migrate_aeme()— the silent, idempotent worker behindupgrade_aeme(), now also covering theoutflows$elevationrename,outputplaceholders, andobservations$levelcoercion. Still called automatically bybuild_aeme(),check_aeme(),show(), andplot()so older saved objects keep working without needing to be rebuilt.
Bug fixes
-
GLM-AED outflow setup wrote a nonsensical
outl_elvsand coercedbsn_len_outl/bsn_wid_outltoNAfor lakes whose hypsography extends below 0 m, e.g. a lake bed below sea level. Several problems inbuild_glm(), all rooted in-1being AEME’s “not specified” sentinel for an outflowelevation(add_outflows();build_aeme()sets the water-balance outflow to-1):- the sentinel was recognised only via
heights_wdr <= 0, so for a lake bed below 0 m a real request could be swallowed, or the-1sentinel leak through as a literal elevation; - the “not specified” replacement was
init_depth - 1, a water depth written intooutl_elvs, which for a fixed outlet GLM reads as an absolute elevation on the hypsography datum - so it fell outside the hypsography andelipse_dims()’s area lookup returnedNA; -
outlet_type/flt_off_swwere inferred from the sign of the elevation (ifelse(heights_wdr < 0, 2, 1)), so the default-1sentinel silently produced a floating offtake, for which GLM expectsoutl_elvsas a depth below the surface in[0, depth]and rejects negative values (“above lake surface”).build_glm()now interprets an outflowelevationexplicitly: the-1/NAsentinel means not specified and defaults to a floating offtake (outlet_type = 2,flt_off_sw = .true.) drawing ~1 m above the bed but tracking the surface, withoutl_elvswritten as the depth-below-surface GLM requires for a floating outlet; any explicitelevationis placed as a fixed outlet at that absolute elevation, validated and clamped into[base_elev, crest_elev]. Basin length/width are looked up at the outlet’s true absolute elevation (make_wdr_glm()gained adims_elevargument for this) and clamped intorange(bathy$elev)so aset_glm_outflows()call with an out-of-range outlet still yields finite dimensions. For lakes whose hypsography sits at/above 0 m the generated&outflowblock is unchanged; sub-sea-level lakes previously failed and now build.
- the sentinel was recognised only via
set_glm_outflow_config()(new, exported) — fine-grained control of the GLM-AED&outflowblock for an existing configuration, beyond the fixed/floating splitset_glm_outflows()offers: per-outlet adaptive/target-temperature (type 3) and submerged (type 6) outlets,outlet_critthresholds,target_temp,withdrTemp_fl, bedseepage/seepage_rate, weircrest_width/crest_factor,outflow_thick_limitandsingle_layer_draw, plus anadaptivelist for the block-levelcrit_*controls. Elevations are given on the hypsography datum and converted per outlet; every value is checked against GLM’s own ranges (fromsrc/glm_init.c) before writing. It edits only the nml, leaving the flow-forcing CSVs alone.run_aeme()reported a successful run and then failed with an opaque netCDF error (open_nc_safe(): “File path must be a single character string”) when a model crashed. Three causes: GLM-AED is built with gfortran, whoseSTOP "..."(e.g. a malformedaed_noncohesivenamelist) exits0, sorun_aeme()’sstatus == 0success test passed; theverbose = TRUEpath ofrun_glm_aed()/run_gotm_wet()checked nothing at all, leaving a misleadingclitick; and a failed model was still handed toload_output(). Now eachrun_*()attaches a logical$success(exit status and the model’s completion banner, and for Simstrat the netCDF conversion),run_aeme()gates the “Model run complete!” message and output loading on it, andread_model_outputs()aborts with a clearaeme_error_missing_output(“re-run withverbose = TRUE”) when the expected output file is absent.cli_inform_safe()andcli_safe()did not forward an evaluation environment tocli, so any message containing a{}expression that referenced a local variable of the calling function failed withobject '<name>' not found—cliwas interpolating against the wrapper’s own frame. This surfaced when building anyAemeobject whose inflow tables still used pre-standard column names (e.g.NIT_din), wherestandardise_inflow()reports"Renaming {length(matched)} column{?s}". Both wrappers now take.envir = parent.frame()and pass it through.read_model_config()assumed any.parconfiguration file was Simstrat’s JSON format, but DYRESM-CAEDYM’sdyresm3p1.parshares that extension and is plain text -read_model_config(model = "dy_cd", ...)failed on any lake with a DYRESM-CAEDYM configuration. Now scoped tomodel == "simstrat_aed2"only.Fixed a lake-level inversion bug affecting every Simstrat-AED2 simulation with non-trivial inflows/outflows:
.write_simstrat_grid_file()wrote the two-point depth header used to force a non-zero trapezoidal integration in descending order, but Simstrat’sIntegrate()computesdx = x(i) - x(i-1)directly from the file’s own (unreordered) depth values. A descending header therefore silently negated every flux this writer produces - inflow, outflow, temperature, salinity, and AED2 inflow concentrations alike - which is why simulated lake level for Simstrat-AED2 tracked in the opposite direction to the other three models. Fixed by writing the depths in ascending order; verified by comparing Simstrat’s ownQvertoutput variable against the expected net inflow/outflow signal (now matching almost exactly, correlation 0.9986, vs. an exact sign-flip before the fix).calc_evap()gained a dedicatedmodel == "simstrat_aed2"branch implementing Simstrat’s own evaporation formula (a Livingstone & Imboden wind function with a Gill (1992) saturation vapour pressure, fromstrat_forcing.f90), used byestimate_lake_wlev()when fitting the water balance for Simstrat-AED2. It previously shared GLM-AED’s simpler bulk-aerodynamic formula.build_aeme()did not callmigrate_aeme(), so anAemeobject saved beforesimstrat_aed2existed (missingtime$spin_up[["simstrat_aed2"]]) would crash insidecheck_time()’scompute_spinup_dates()the first time it was built with a newer AEME version.build_aeme()now migrates the object on entry, matchingcheck_aeme()/show()/plot().-
write_simstrat_nc()mishandled AED’s sediment-zone output (<var>_zone_out.dat, Simstrat-AED only). These files also match the general*_out.datglob and were being written against the shared water-columnzdimension/grid used by regular depth-profile variables, which either put zone values at the wrong depths or failed outright whenever a zone file’s column count (one per benthic zone) didn’t happen to match the water column’s level count. Zone variables now get their ownzonenetCDF dimension, coordinate-valued by each zone’s reference depth- keeping the existing
<var>and new<var>_zonevariables distinct. No changes were needed on the reading side:read_simstrat_output()’sload_allsweep already routes any variable shaped other than(time)or(z, time)through the same generic grouped-variable path GLM-AED’s ownnzones-dimensioned output uses, so zone variables come back asaeme_grouped_varobjects automatically. Verified against a real Simstrat-AED Rotorua run (75 sediment-zone variables across 3 zones).
- keeping the existing
New data
-
simstrat_aed2_parameter_library— a comprehensive reference table of Simstrat-AED2 parameters (physical parameters from the Simstrat User Manual, plus AED2 biogeochemical parameters shared withglm_aed_parameter_library), mirroring the existingglm_aed_parameter_librarydataset.
Documentation
- Added the
simstrat-aed2article (vignettes/articles/simstrat-aed2.Rmd), covering Simstrat-AED2’s model description, AED2 module coupling, the new parameter library, model-specific features (automatic AED2 module selection, inflow modes, ice/snow, water balance fitting), and calibration (thesimstrat_aed2_parametersdataset and Simstrat’s native PEST-based workflow), mirroring the existingglm-aedarticle.
Testing
- Added
tests/testthat/helper-glm.Randsetup.Rwith shared helpers for skipping/filtering tests by platform availability (skip_if_models_unavailable(),filter_platform_models(),skip_if_no_glm()) and CI coverage extended to macOS and Ubuntu, in addition to Windows.
get_model_outfile() returns a single file by default
get_model_outfile() previously returned every file a model run produced – for GLM-AED that meant its netCDF plus any configured csv_lake/csv_point/ mass-balance CSVs – so callers that only wanted the netCDF had to guess which entry it was (nc_files[["output"]], else the first element), and a couple did so incorrectly. get_model_outfile() now returns just the primary file per model by default (the entry named "output", or the only file when there is one); pass all = TRUE to get every file as before.
AEME 0.3.1
New functions
-
get_config_value()— retrieve a configuration value from anAemeobject, falling back to package defaults when not set. -
get_wbal_param()— retrieve the fitted water-balance outflow parameters (Candh_inv) stored in anAemeobject. -
set_wbal_param()— store water-balance outflow parameters in anAemeobject for use in subsequentbuild_aeme()calls. -
plot_output_base()— new base-R plotting function for AEME model output, producing heatmap-style depth–time plots without anyggplot2dependency. Can also be invoked viaplot_output(..., backend = "base"). -
get_mean_sea_level_pressure()— utility function to convert station pressure to mean sea level pressure given air temperature and elevation. -
get_station_pressure()— inverse ofget_mean_sea_level_pressure(); converts mean sea level pressure back to station pressure.
Improvements
-
plot_output()— newbackendargument ("ggplot2"or"base") selects the plotting engine. Also improved variable/model availability checks: models or variables missing from all output are now warned and dropped gracefully rather than erroring. -
run_aeme()/run_glm_aed()— newargsparameter passes additional command-line arguments to the model executable (e.g."--xdisp"to display GLM plots using theplots.nmlsettings during a run). -
build_glm()— automatically copies the bundledplots.nmltemplate into the GLM simulation directory when one is not already present. -
build_aeme()—modelandpathare now resolved from theAemeconfiguration when not supplied as arguments. All otherbuild_aeme()arguments default throughconfig_defaults()if not specified. -
estimate_zone_fluxes()— output table now rendered usingclitablefor cleaner formatted console display; documentation expanded with method description, depth-scaling references, and a full description of the return value. -
initialise_aed()— informative message now only shown when an initialisation value differs meaningfully from the replaced default. -
initialise_glm()— added guard for requiredinit_profilesfields (wq_names,num_wq_vars,wq_init_vals) that may be absent from older NML files. -
set_glm_aed_models()— messaging improved when AED sub-models are removed. -
run_glm_aed_diagnostics()—plotargument now defaults toFALSE. -
calc_water_balance()/estimate_lake_wlev()/estimate_surface_temperature()/standardise_inflow()/build_glm()— console messaging overhauled using the newcli_safe()internal helper, which respects theAEME.informoption and supports indented output. Missing inflow state variables are now reported with their filled default values rather than a generic warning. -
clitablemoved fromSuggeststoImports;knitrmoved fromImportstoSuggests;psychrolibremoved fromImports(psychrometric calculations now handled internally viaget_mean_sea_level_pressure()/get_station_pressure()).
Bug fixes
- Fixed variable naming for DY-CD model output (
read_dy_output(),read_model_outputs()). - Removed pH from default model controls (
get_model_controls()). - Simplified and corrected variable-name look-ups in
lake_obs_to_aeme(),read_dy_output(),read_glm_output(),read_gotm_output(), andread_model_outputs()to use the updatedkey_naming$var_aemecolumn. - Removed a now-redundant internal helper
format_model_vars_vec(); its behaviour is folded intoget_model_vars(as_vector = TRUE). - Fixed initialisation guard in
initialise_FABM(),initialise_aed(), andinitialise_glm()— empty model-controls tables now return early with an informative message rather than erroring. - Fixed a CLI bug in
estimate_lake_wlev(). - Fixed a typo bug in
estimate_zone_fluxes()column-renaming step (dplyr::case_matchreplaced withdplyr::recodefor compatibility).
AEME 0.3.0
AEME 0.2.0
New functions
-
add_outflows()— add outflow data to an AEME object. -
add_output()— add model output to an AEME object. -
add_deriv_output()— compute and attach derived variables (thermocline depth, stratification, Schmidt stability, TLI components, oxygen metrics) to model output. -
check_model()— validate that a model name is supported. -
check_gotm_yaml()— validate a GOTM YAML configuration file. -
check_path()— helper to verify that a file/directory path exists. -
check_utils()— miscellaneous input-checking utilities. -
convert_do()— convert dissolved-oxygen values between units (mg/L ↔︎ % saturation). -
deriv_registry()— registry of derived-variable definitions used byadd_deriv_output(). -
estimate_lake_wlev()— estimate lake water level from inflow/outflow data and a hypsograph. -
estimate_sed_zones()— estimate sediment zones from a hypsograph. -
estimate_surface_temperature()— estimate lake surface temperature. -
estimate_zone_fluxes()— estimate sediment-zone fluxes for AED models. -
get_aed_sed_const2d_param()— retrieve 2-D sediment constant parameters from an AED configuration. -
get_aeme_path()— return the path to the AEME package installation. -
get_date_index()— return time-step indices for a given date range. -
get_deriv_vars()— list available derived variables. -
get_glm_sed_params()— retrieve GLM sediment parameters. -
get_glm_sed_zones()— retrieve GLM sediment zone definitions. -
get_hm_palette()— return the built-in heat-map colour palette. -
get_vars_sim()— get the list of variables to simulate. -
get_wbal_components()— extract water-balance component data from model output. -
glm_sed_params()— construct a GLM sediment-parameter object. -
glm_to_aeme()— convert a GLM NML file to an AEME configuration object. -
guess_aeme_vars()— auto-detect AEME variable names from a data frame. -
initialise_aed()— initialise AED2 configuration (replacesinitialiseAED()). -
is_strat()— test whether a water column is stratified. -
palettes— built-in colour palettes for AEME plots. -
param_colnames()— return expected column names for the parameter table. -
plot_est_wbal()/plot_water_balance()— plot estimated water-balance components. -
plot_glm_config()— plot GLM model configuration diagnostics. -
plot_glm_diagnostics()— plot GLM-AED diagnostic outputs. -
plot_wbal_comp()— plot water-balance component comparison across models. -
plot_wbal_summaries()— plot summarised water-balance results. -
plot_weir_calibration()— plot weir calibration results. -
plot_wlev()— plot simulated water-level time series. -
read_aed_param_csv()— read AED parameter values from a CSV file. -
read_dy_output(),read_dy_stg()— read DYRESCD/DY-CD model output and stage files. -
read_glm_output()— read GLM model output NetCDF files. -
read_gotm_flux_output(),read_gotm_hyps(),read_gotm_output()— read GOTM output files. -
read_model_config()— read a model configuration file. -
read_model_hypsograph()— read a hypsograph from a model configuration. -
read_model_nc()— low-level reader for model NetCDF files. -
read_model_outputs()— unified reader for all supported model outputs. -
read_model_wlev()— read water-level output from any supported model. -
reset_wbal_param()— reset water-balance parameters to defaults. -
resolve_dependencies()— resolve derived-variable computation dependencies. -
run_glm_aed_diagnostics()— run GLM-AED post-run diagnostics. -
set_aed_sed_const2d()— set 2-D AED sediment constant parameters. -
set_aed_totals()— set AED total-concentration parameters. -
set_glm_aed_models()— configure which AED sub-models are active. -
set_vars_sim()— set the variables to be output by a simulation. -
standardise_inflow()— standardise inflow data columns and units. -
standardise_met()— standardise meteorological data columns and units.
Improvements and changes
-
AEME S4 object overhaul — the
AemeS4 class has been substantially refactored: accessor/replacement generics have been simplified and slot structure updated for consistency. -
build_aeme()— major refactor; improved handling of precipitation, inflows, outflows, and lake-level initialisation. -
calc_fairall()— vectorised and performance-improved implementation of the Fairall bulk aerodynamic algorithm. -
calc_water_balance()— refactored water-balance calculation; improved precipitation and evaporation handling. -
calc_lake_obs_deriv()— extended to compute additional derived observation variables. -
expand_met()— improved meteorological gap-filling and unit conversion logic. -
load_output()— now dispatches to the new unifiedread_model_outputs()functions. -
check_aeme_vars(),check_glm_nml()— improved validation logic and error messages. -
check_time()— more robust time-period validation. -
print()/show()/summary()— AEME object print methods migrated to use cli for formatted console output. -
Inflow/outflow handling — new auto-detection of column names and units via
standardise_inflow()andguess_aeme_vars();add_inflows()andadd_outflows()updated accordingly. - Rain and snow units — standardised to mm/day throughout the package.
- GLM NML helpers — new tests and improved handling of sediment parameters and zone detection.
-
GOTM YAML — new
check_gotm_yaml()validates GOTM configuration before model run. -
AED initialisation —
initialiseAED()replaced byinitialise_aed(); library name spelling corrected to “Ecodynamics”. -
GLM binaries — bundled GLM-AED binaries removed from
inst/extbin/; users now provide their own executable or use theglmtoolspackage option. -
Parameter library — internal parameter library (
bc2e234) andget_aeme_params()updated to include new parameters. - pkgdown site — favicon assets and extra CSS added; GLM-AED vignette registered.
- Removed deprecated
nc_listify()internal helper. - Updated Roxygen documentation to v8 conventions throughout.
New vignettes
-
GLM-AED (
vignettes/articles/glm-aed.Rmd) — end-to-end walkthrough of running GLM coupled with AED2. -
Reservoir AEME (
vignettes/articles/reservoir-aeme.Rmd) — demonstration of AEME on a reservoir.
Bug fixes
- Fixed phytoplankton group index when building AED configurations (#
45559f4). - Fixed outflow renaming bug in
build_aeme()(#04838b0). - Fixed inflow name update logic in
build_glm()(#5c0eb58). - Fixed water-balance weir/outflow handling when only the water-balance outflow is present (#
af39204). - Fixed
guess_aeme_vars()guessing logic for edge cases (#9e9e622). - Fixed phytoplankton defaults in AED initialisation (#
c9e78d4). - Fixed precipitation unit parsing for percentage values (#
16bcf0b). - Corrected air-pressure unit conversion in meteorological processing.
