ERA5-Land hourly data to SWAT inputs
Source:vignettes/articles/era5-land-hourly-to-swat.Rmd
era5-land-hourly-to-swat.RmdThis article documents a production-style workflow for preparing
ERA5-Land hourly NetCDF data as SWAT-ready weather input files with
wcswatin. The test area is the São Lourenço basin region in
Mato Grosso, Brazil, used here as a compact region of interest for
demonstrating the full data path. The gridded case covers 2025-10-01 to
2025-12-31.
The workflow is shown with representative code, compact summaries, and precomputed results from the case run.
Case data
The case was run with six ERA5-Land hourly NetCDF files:
| File | Size |
|---|---|
hourly_10m_u_component_of_wind_2025.nc |
2.5 MB |
hourly_10m_v_component_of_wind_2025.nc |
2.5 MB |
hourly_2m_dewpoint_temperature_2025.nc |
2.0 MB |
hourly_2m_temperature_2025.nc |
2.0 MB |
hourly_surface_solar_radiation_downwards_2025.nc |
1.8 MB |
hourly_total_precipitation_2025.nc |
1.8 MB |
The processed study area has 229 grid cells and each hourly cube has
2,208 layers. The expected long table created by
cube2table() has 505,632 rows per variable.
case_root <- Sys.getenv("WCSWATIN_CASE_ROOT")
input_dir <- file.path(case_root, "data/input")
output_dir <- file.path(case_root, "data/output")Before choosing the extraction path, inspect downloaded files with
raster_info(). The number of layers and the time resolution
make it clear whether the file is hourly, daily, or a multi-variable
cube.
raster_info(file.path(input_dir, "nc_files/hourly/hourly_2m_temperature_2025.nc"))
raster_info(file.path(input_dir, "nc_files/daily/daily_2m_temperature_2025.nc"))Step 1: define the study area
study_area_records() combines one raster model layer, a
basin boundary, and a DEM to create the table used by the extraction
workflow.
study_area_tbl <- study_area_records(
raster_model = nc_file,
roi = basin_limit,
dem = dem_raster
)
data.table::fwrite(
study_area_tbl,
file.path(output_dir, "study_area.csv")
)Precomputed result:
| LON | LAT | ID | ROW | COL | ELEVATION |
|---|---|---|---|---|---|
| -55.2 | -15.5 | 36 | 2 | 8 | 698 |
| -55.1 | -15.5 | 37 | 2 | 9 | 716 |
| -55.0 | -15.5 | 38 | 2 | 10 | 660 |
Step 2: create SWAT main files
main_input_var() creates a mainFile.csv for
each target variable. The case creates main files for the original
variables and the derived variables.
variables <- c("u10", "v10", "d2m", "t2m", "ssrd", "tp", "ws10", "rh2m")
for (var in variables) {
main_tbl <- main_input_var(
study_area = file.path(output_dir, "study_area.csv"),
var_name = var
)
data.table::fwrite(
main_tbl,
file.path(output_dir, var, "mainFile.csv")
)
}Example mainFile.csv rows for u10:
| ID | NAME | LAT | LON | ELEVATION |
|---|---|---|---|---|
| 36 | u10_36 | -15.5 | -55.2 | 698 |
| 37 | u10_37 | -15.5 | -55.1 | 716 |
| 38 | u10_38 | -15.5 | -55.0 | 660 |
Step 3: extract hourly cubes
cube2table() converts each NetCDF cube to one long table
containing extracted values for the study area cells. This is where the
future integration is most useful: raster layers can be
extracted independently, while the high-level cube2table()
call stays the same. Users choose the execution strategy with
future::plan() before calling the function, so the workflow
can run sequentially, in local parallel workers, or with another
compatible future backend.
future::plan(future::multicore, workers = 10)
cube2table(
input_path = nc_file,
n_layers = 2208,
study_area = file.path(output_dir, "study_area.csv"),
future_scheduling = 1,
final_dir = file.path(output_dir, "u10/intermediate/layer_h"),
side_effect = "only"
)
future::plan(future::sequential)In the benchmarked run, extracting one variable took about 6.6 to 7.1
minutes with 8-10 local workers, compared with about 39.5 minutes
sequentially. The performance gain comes from changing the
future plan, not from rewriting the extraction code. For
example, sequential is useful for debugging,
multisession is portable across operating systems,
multicore can be faster on Unix-like systems outside GUI
sessions, and other future backends can be used when the
user’s environment supports them. See the future overview
for backend options: https://cran.r-project.org/web/packages/future/vignettes/future-1-overview.html.
Step 4: write one series per pixel
layervalues2pixel() transforms the long layer table into
SWAT-style time-series files, one file per grid cell.
layervalues2pixel(
layer_values = file.path(output_dir, "u10/intermediate/layer_h/tbls.csv"),
main_tbl = file.path(output_dir, "u10/mainFile.csv"),
col_name = "20251001",
inline_output = TRUE,
path_output = file.path(output_dir, "u10/intermediate/pixel_h"),
append = FALSE
)The case created 229 hourly files per original variable.
Step 5: aggregate hourly values to daily values
Daily aggregation depends on the variable semantics.
| Variable | Original unit | SWAT unit | Daily operation |
|---|---|---|---|
u10 |
m s^-1 | m s^-1 | mean |
v10 |
m s^-1 | m s^-1 | mean |
d2m |
K | deg C | convert to deg C, then daily min/max |
t2m |
K | deg C | convert to deg C, then daily min/max |
ssrd |
J m^-2 | MJ m^-2 | convert to MJ m^-2, then value at 00:00 |
tp |
m | mm | convert to mm, then value at 00:00 |
ws10 |
m s^-1 | m s^-1 | derived from u10 and v10
|
rh2m |
% | % | derived from d2m and t2m, then mean |
Mean aggregation:
daily_aggregation(
folder_in = file.path(output_dir, "u10/intermediate/pixel_h"),
folder_out = file.path(output_dir, "u10/pixel_d"),
pattern = ".txt$",
from = "2025-10-01 01",
to = "2025-12-31 23",
drop_first_record = FALSE,
aggregation_function = mean,
na.rm = FALSE
)Accumulated variables use the hour that stores the completed
accumulation period. For ERA5-Land daily accumulated values timestamped
at midnight, this is value_hour = 0.
daily_aggregation(
folder_in = file.path(output_dir, "tp/intermediate/pixel_h_mm"),
folder_out = file.path(output_dir, "tp/pixel_d"),
pattern = ".txt$",
from = "2025-10-01 01",
to = "2025-12-31 23",
drop_first_record = FALSE,
mode = "value_at_hour",
value_hour = 0,
na.rm = FALSE
)Daily NetCDF shortcut
If the source product is already daily, the workflow can skip the
hourly intermediate files and daily_aggregation(). In that
case, use raster_info() to confirm the variable, unit,
layer count, and date range, then run cube2table() on the
daily cube and pass the result directly to
layervalues2pixel().
daily_info <- raster_info(
file.path(input_dir, "nc_files/daily/daily_2m_temperature_2025.nc")
)
cube2table(
input_path = file.path(input_dir, "nc_files/daily/daily_2m_temperature_2025.nc"),
n_layers = daily_info$n_layers,
study_area = file.path(output_dir, "study_area.csv"),
final_dir = file.path(output_dir, "t2m/intermediate/layer_d"),
side_effect = "only"
)
layervalues2pixel(
layer_values = file.path(output_dir, "t2m/intermediate/layer_d/tbls.csv"),
main_tbl = file.path(output_dir, "t2m/mainFile.csv"),
col_name = "20251001",
inline_output = FALSE,
path_output = file.path(output_dir, "t2m/pixel_d"),
append = FALSE
)Another option is to reduce the hourly NetCDF locally before
extraction. datacube_aggregation() can apply a daily
aggregation function, or it can select the layer timestamped at a
specific hour before cube2table() runs.
datacube_aggregation(
input_path = file.path(input_dir, "nc_files/hourly/hourly_2m_temperature_2025.nc"),
output_filename = file.path(output_dir, "t2m_daily_mean.tif"),
fun = mean,
cores = 4
)For accumulated products whose daily value is stored at a specific
timestamp, use mode = "value_at_hour". ERA5-Land
accumulated values timestamped at 00:00 represent the
previous day, so the output dates can be shifted back one day. If the
first selected 00:00 layer belongs to the day before the
requested period, drop it before writing the daily cube.
datacube_aggregation(
input_path = file.path(input_dir, "nc_files/hourly/hourly_total_precipitation_2025.nc"),
output_filename = file.path(output_dir, "tp_daily_value_at_00.tif"),
mode = "value_at_hour",
value_hour = 0,
date_shift_days = -1,
drop_first_layer = TRUE
)Step 6: derive wind speed and relative humidity
windspeed_calculator() derives wind speed from daily
u10 and v10.
windspeed_calculator(
folder_uas = file.path(output_dir, "u10/pixel_d"),
folder_vas = file.path(output_dir, "v10/pixel_d"),
folder_out = file.path(output_dir, "ws10/pixel_d"),
col_name = "20251001",
file_name_output = "ws10",
pattern = ".txt$"
)rh_calculator() derives hourly relative humidity from
dewpoint and air temperature, followed by daily mean aggregation.
rh_calculator(
folder_dpt = file.path(output_dir, "d2m/intermediate/pixel_h"),
folder_tas = file.path(output_dir, "t2m/intermediate/pixel_h"),
folder_out = file.path(output_dir, "rh2m/intermediate/pixel_h"),
file_name_output = "rh2m",
m_value = 7.591386,
Tn_value = 240.7263,
pattern = ".txt$"
)Final output
The final daily SWAT-style outputs are compact enough to inspect and archive.
| Variable | Main file | Daily files | Daily output size |
|---|---|---|---|
u10 |
yes | 229 | 381.0 KB |
v10 |
yes | 229 | 375.7 KB |
d2m |
yes | 229 | 284.4 KB |
t2m |
yes | 229 | 285.5 KB |
ssrd |
yes | 229 | 200.0 KB |
tp |
yes | 229 | 360.2 KB |
ws10 |
yes | 229 | 357.1 KB |
rh2m |
yes | 229 | 349.5 KB |
The complete output directory from this run was about 193 MB because it also kept hourly intermediate tables and interpolation artifacts. Those intermediates are useful for auditing, while the final daily files are the main SWAT-ready products.