diff --git a/.gitignore b/.gitignore index cb671c5..e1a28cc 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ *.tgz *.o +rQSWATPlus.Rcheck/ +rQSWATPlus_*.tar.gz diff --git a/rQSWATPlus/.Rbuildignore b/rQSWATPlus/.Rbuildignore new file mode 100644 index 0000000..5f3d199 --- /dev/null +++ b/rQSWATPlus/.Rbuildignore @@ -0,0 +1,5 @@ +^.*\.Rproj$ +^\.Rproj\.user$ +^LICENSE\.md$ +^README\.Rmd$ +^\.github$ diff --git a/rQSWATPlus/DESCRIPTION b/rQSWATPlus/DESCRIPTION new file mode 100644 index 0000000..7004b8b --- /dev/null +++ b/rQSWATPlus/DESCRIPTION @@ -0,0 +1,40 @@ +Package: rQSWATPlus +Title: R Interface to QSWATPlus for SWAT+ Model Setup +Version: 0.1.0 +Authors@R: + person("QSWATPlus Contributors", role = c("aut", "cre"), + email = "qswatplus@example.com") +Author: QSWATPlus Contributors [aut, cre] +Maintainer: QSWATPlus Contributors +Description: Provides R functions to replicate the QSWATPlus QGIS plugin + workflow for setting up SWAT+ (Soil and Water Assessment Tool) models. + Includes watershed delineation using TauDEM (via the 'traudem' package), + Hydrologic Response Unit (HRU) creation from landuse, soil, and slope + data, and generation of SWAT+ project databases. The package supports + the complete workflow from DEM processing to database creation. +License: GPL (>= 3) + file LICENSE +URL: https://github.com/limnotrack/QSWATPlus +BugReports: https://github.com/limnotrack/QSWATPlus/issues +Depends: + R (>= 4.1.0) +Imports: + terra, + sf, + RSQLite, + DBI, + cli, + rlang +Suggests: + testthat (>= 3.0.0), + traudem, + tmap, + ggplot2, + knitr, + rmarkdown, + withr +Config/testthat/edition: 3 +Encoding: UTF-8 +LazyData: false +Roxygen: list(markdown = TRUE) +RoxygenNote: 7.3.3 +VignetteBuilder: knitr diff --git a/rQSWATPlus/LICENSE b/rQSWATPlus/LICENSE new file mode 100644 index 0000000..6e64875 --- /dev/null +++ b/rQSWATPlus/LICENSE @@ -0,0 +1,2 @@ +YEAR: 2024 +COPYRIGHT HOLDER: QSWATPlus Contributors diff --git a/rQSWATPlus/NAMESPACE b/rQSWATPlus/NAMESPACE new file mode 100644 index 0000000..ecdc557 --- /dev/null +++ b/rQSWATPlus/NAMESPACE @@ -0,0 +1,47 @@ +# Generated by roxygen2: do not edit by hand + +S3method(print,qswat_db_check) +S3method(print,qswat_project) +export(qswat_check_database) +export(qswat_create_hrus) +export(qswat_create_slope_classes) +export(qswat_create_streams) +export(qswat_delineate) +export(qswat_plot_dem) +export(qswat_plot_hru_summary) +export(qswat_plot_hrus) +export(qswat_plot_landuse) +export(qswat_plot_landuse_summary) +export(qswat_plot_lsus) +export(qswat_plot_overview) +export(qswat_plot_soil) +export(qswat_plot_soil_summary) +export(qswat_plot_streams) +export(qswat_plot_watershed) +export(qswat_read_gis) +export(qswat_read_gwflow_config) +export(qswat_read_landuse_lookup) +export(qswat_read_soil_lookup) +export(qswat_read_usersoil) +export(qswat_run) +export(qswat_setup) +export(qswat_setup_gwflow) +export(qswat_write_database) +import(terra) +importFrom(DBI,dbConnect) +importFrom(DBI,dbDisconnect) +importFrom(DBI,dbExecute) +importFrom(DBI,dbGetQuery) +importFrom(DBI,dbListTables) +importFrom(DBI,dbWriteTable) +importFrom(RSQLite,SQLite) +importFrom(rlang,.data) +importFrom(sf,st_as_sf) +importFrom(sf,st_bbox) +importFrom(sf,st_coordinates) +importFrom(sf,st_crs) +importFrom(sf,st_geometry_type) +importFrom(sf,st_read) +importFrom(sf,st_transform) +importFrom(sf,st_write) +importFrom(utils,read.csv) diff --git a/rQSWATPlus/R/database.R b/rQSWATPlus/R/database.R new file mode 100644 index 0000000..e57d121 --- /dev/null +++ b/rQSWATPlus/R/database.R @@ -0,0 +1,2354 @@ +#' Write SWAT+ Project Database +#' +#' Writes the HRU, subbasin, and routing data to a SWAT+ project +#' SQLite database. This database is used as input for the SWAT+ +#' Editor. +#' +#' @param project A `qswat_project` object with HRU data from +#' [qswat_create_hrus()]. +#' @param db_file Character or NULL. Path for the output SQLite +#' database. If NULL, creates the database in the project directory +#' as `project.sqlite`. +#' @param overwrite Logical. If TRUE, overwrite existing database. +#' Default is FALSE. +#' @param usersoil Character or NULL. Controls which soil physical +#' properties dataset is used to populate the `global_usersoil` table +#' in the project database. Mirrors the GUI option. When `NULL` +#' (default), the value stored in `project$usersoil` (set via +#' [qswat_setup()]) is used; an explicit value here overrides it. +#' Accepted values: +#' \describe{ +#' \item{`NULL`}{Use `project$usersoil`, or leave `global_usersoil` +#' empty if that is also NULL.} +#' \item{`"FAO_usersoil"`}{Copy FAO global soil data (13 soil types) +#' from the bundled `QSWATPlusProjHAWQS.sqlite` database.} +#' \item{`"global_usersoil"`}{Copy the full global soil dataset +#' (thousands of soil types) from the bundled +#' `QSWATPlusProjHAWQS.sqlite` database.} +#' \item{file path}{Path to a CSV file containing soil parameters in +#' the `global_usersoil` table format. The file must have at minimum +#' a column named `SNAM` (soil name). See [qswat_read_usersoil()].} +#' } +#' Populating `global_usersoil` allows the SWAT+ Editor to resolve +#' soil physical properties when generating SWAT+ input files. +#' +#' @return The `project` object, updated with the path to the created +#' database. +#' +#' @details +#' Creates a SQLite database with the following tables: +#' \describe{ +#' \item{gis_subbasins}{Subbasin-level data including area, elevation, +#' and slope statistics} +#' \item{gis_hrus}{HRU data including subbasin, land use, soil, +#' slope class, area, and elevation} +#' \item{gis_routing}{Routing topology connecting subbasins} +#' \item{gis_channels}{Channel/stream network data} +#' \item{gis_lsus}{Landscape unit data} +#' \item{gis_aquifers}{Aquifer data for each subbasin} +#' \item{gis_deep_aquifers}{Deep aquifer data} +#' \item{gis_water}{Water body data} +#' \item{gis_points}{Point source data} +#' \item{global_usersoil}{Soil physical properties (populated when +#' `usersoil` is specified)} +#' \item{global_soils}{Soil name lookup (populated when `usersoil` +#' is specified)} +#' } +#' +#' The database format is compatible with the SWAT+ Editor for +#' further model parameterization. +#' +#' @examples +#' \dontrun{ +#' # Default: no soil physical properties +#' db_path <- qswat_write_database(project) +#' +#' # Set usersoil in qswat_setup() so it applies automatically +#' project <- qswat_setup(..., usersoil = "FAO_usersoil") +#' db_path <- qswat_write_database(project) # uses FAO_usersoil +#' +#' # Override the project-level setting at write time +#' db_path <- qswat_write_database(project, usersoil = "global_usersoil") +#' +#' # Use a custom CSV file +#' db_path <- qswat_write_database(project, usersoil = "my_soils.csv") +#' } +#' +#' @export +qswat_write_database <- function(project, + db_file = NULL, + overwrite = FALSE, + usersoil = NULL) { + + if (!inherits(project, "qswat_project")) { + stop("'project' must be a qswat_project object.", call. = FALSE) + } + if (is.null(project$hru_data)) { + stop("No HRU data found. Run qswat_create_hrus() first.", call. = FALSE) + } + + if (is.null(db_file)) { + db_file <- file.path(project$project_dir, "project.sqlite") + } + + if (file.exists(db_file) && !overwrite) { + stop("Database already exists: ", db_file, + ". Use overwrite = TRUE to replace.", call. = FALSE) + } + + if (file.exists(db_file) && overwrite) { + file.remove(db_file) + } + + message("Writing SWAT+ project database...") + + # Copy reference database to project folder + ref_db_path <- .copy_reference_database(project$project_dir) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + # Create tables + .create_db_tables(con) + + # Write project configuration + .write_project_config(con, project, db_file, ref_db_path) + + # Compute subbasin centroids in WGS84 once; shared by all spatial tables + centroids <- .compute_subbasin_centroids_wgs84(project) + + # Write subbasin data + .write_subbasin_table(con, project$basin_data, centroids) + + # Write HRU data + .write_hru_table(con, project$hru_data, project$slope_classes, centroids) + + # Write routing topology + .write_routing_table(con, project) + + # Write channel data + .write_channel_table(con, project) + + # Write LSU data + .write_lsu_table(con, project, centroids) + + # Write aquifer data (only when SWAT+ aquifers are enabled) + if (!isFALSE(project$use_aquifers)) { + .write_aquifer_table(con, project$basin_data, centroids) + } + + # Write water body data + .write_water_table(con) + + # Write point source data + .write_point_table(con) + + # Write intermediate data tables + .write_basinsdata_table(con, project$basin_data) + .write_hrusdata_table(con, project$hru_data) + .write_lsusdata_table(con, project$hru_data, project$basin_data) + + # Ensure all required tables exist with sensible defaults + ensure_write_tables(con) + + # Populate soil physical properties: + # explicit argument takes precedence, otherwise use project-level setting + effective_usersoil <- if (!is.null(usersoil)) usersoil else project$usersoil + if (!is.null(effective_usersoil)) { + .populate_usersoil(con, effective_usersoil) + } + + # Populate soils_sol and soils_sol_layer from global_usersoil + # (usersoil -> global_usersoil -> soils_sol, matching Python plugin flow) + .write_soils_sol_table(con, project$hru_data) + + message("Database written to: ", db_file) + project$db_file <- db_file + # invisible(db_file) + return(project) +} + + +#' Create Database Tables +#' +#' Creates all tables required by the SWAT+ Editor, matching the +#' schema from the QSWATPlusProj.sqlite template database. +#' @noRd +.create_db_tables <- function(con) { + + # ---- project_config: critical for SWAT+ Editor ---- + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS project_config ( + id INTEGER PRIMARY KEY NOT NULL DEFAULT (1), + project_name TEXT, + project_directory TEXT, + editor_version TEXT, + gis_type TEXT, + gis_version TEXT, + project_db TEXT, + reference_db TEXT, + wgn_db TEXT, + wgn_table_name TEXT, + weather_data_dir TEXT, + weather_data_format TEXT, + input_files_dir TEXT, + input_files_last_written DATETIME, + swat_last_run DATETIME, + delineation_done BOOLEAN DEFAULT (0) NOT NULL, + hrus_done BOOLEAN DEFAULT (0) NOT NULL, + soil_table TEXT, + soil_layer_table TEXT, + output_last_imported DATETIME, + imported_gis BOOLEAN DEFAULT (0) NOT NULL, + is_lte BOOLEAN NOT NULL DEFAULT (0), + use_gwflow BOOLEAN NOT NULL DEFAULT (0) + ) + ") + + # ---- GIS tables ---- + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_subbasins ( + id INTEGER PRIMARY KEY UNIQUE NOT NULL, + area REAL, + slo1 REAL, + len1 REAL, + sll REAL, + lat REAL, + lon REAL, + elev REAL, + elevmin REAL, + elevmax REAL, + waterid INTEGER + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_hrus ( + id INTEGER PRIMARY KEY UNIQUE NOT NULL, + lsu INTEGER, + arsub REAL, + arlsu REAL, + landuse TEXT, + arland REAL, + soil TEXT, + arso REAL, + slp TEXT, + arslp REAL, + slope REAL, + lat REAL, + lon REAL, + elev REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_routing ( + sourceid INTEGER, + sourcecat TEXT, + hyd_typ TEXT, + sinkid INTEGER, + sinkcat TEXT, + percent REAL + ) + ") + DBI::dbExecute(con, " + CREATE UNIQUE INDEX IF NOT EXISTS source + ON gis_routing (sourceid, sourcecat) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_channels ( + id INTEGER PRIMARY KEY UNIQUE NOT NULL, + subbasin INTEGER, + areac REAL, + strahler INTEGER, + len2 REAL, + slo2 REAL, + wid2 REAL, + dep2 REAL, + elevmin REAL, + elevmax REAL, + midlat REAL, + midlon REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_lsus ( + id INTEGER PRIMARY KEY UNIQUE NOT NULL, + category INTEGER, + channel INTEGER, + subbasin INTEGER, + area REAL, + slope REAL, + len1 REAL, + csl REAL, + wid1 REAL, + dep1 REAL, + lat REAL, + lon REAL, + elev REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_aquifers ( + id INTEGER PRIMARY KEY, + subbasin INTEGER, + deep_aquifer INTEGER, + area REAL, + lat REAL, + lon REAL, + elev REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_deep_aquifers ( + id INTEGER PRIMARY KEY, + area REAL, + lat REAL, + lon REAL, + elev REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_water ( + id INTEGER PRIMARY KEY UNIQUE NOT NULL, + wtype TEXT, + lsu INTEGER, + area REAL, + lat REAL, + lon REAL, + elev REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_points ( + id INTEGER PRIMARY KEY UNIQUE NOT NULL, + subbasin INTEGER, + ptype TEXT, + xpr REAL, + ypr REAL, + lat REAL, + lon REAL, + elev REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_elevationbands ( + subbasin INTEGER PRIMARY KEY UNIQUE NOT NULL, + elevb1 REAL, elevb2 REAL, elevb3 REAL, + elevb4 REAL, elevb5 REAL, elevb6 REAL, + elevb7 REAL, elevb8 REAL, elevb9 REAL, + elevb10 REAL, + elevb_fr1 REAL, elevb_fr2 REAL, elevb_fr3 REAL, + elevb_fr4 REAL, elevb_fr5 REAL, elevb_fr6 REAL, + elevb_frR7 REAL, elevb_fr8 REAL, elevb_fr9 REAL, + elevb_fr10 REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_landexempt (landuse TEXT) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gis_splithrus ( + landuse TEXT, + sublanduse TEXT, + percent REAL + ) + ") + + # ---- Configuration tables ---- + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS config_delin ( + DEM TEXT, + burn TEXT, + existingWshed BOOLEAN DEFAULT (false), + useSQLite BOOLEAN DEFAULT (true), + fromArc INTEGER, + fromGRASS BOOLEAN DEFAULT (false), + gridSize INTEGER, + isHAWQS BOOLEAN DEFAULT (false), + isHUC INTEGER DEFAULT (false), + net STRING, + outlets TEXT, + snapThreshold INTEGER DEFAULT (300), + thresholdCh INTEGER, + thresholdSt INTEGER, + useGridModel BOOLEAN DEFAULT (false), + useOutlets BOOLEAN DEFAULT (false), + verticalUnits TEXT DEFAULT meters, + wshed TEXT, + subbasins TEXT, + gridDrainage BOOLEAN DEFAULT (false), + streamDrainage BOOLEAN DEFAULT (false), + drainageTable TEXT, + lakes TEXT, + lakesDone BOOLEAN DEFAULT (false), + gridLakesAdded BOOLEAN DEFAULT (false), + lakePointsAdded BOOLEAN DEFAULT (false), + delinNet TEXT, + channels TEXT, + snapOutlets TEXT, + subsNoLakes TEXT + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS config_hru ( + areaVal INTEGER DEFAULT (0), + elevBandsThreshold INTEGER DEFAULT (0), + isArea BOOLEAN DEFAULT (false), + isDominantHRU BOOLEAN DEFAULT (false), + isMultiple BOOLEAN DEFAULT (true), + isTarget BOOLEAN DEFAULT (false), + landuseVal INTEGER, + numElevBands INTEGER DEFAULT (0), + slopeBands TEXT, + slopeBandsFile TEXT, + slopeVal INTEGER, + soilVal INTEGER, + targetVal INTEGER, + useArea BOOLEAN DEFAULT (false), + useGWFlow BOOLEAN DEFAULT (false) + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS config_landuse ( + file TEXT, + plant TEXT, + tabl TEXT, + urban TEXT, + water INTEGER + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS config_lsu ( + channelMergeByPercent BOOLEAN DEFAULT (true), + channelMergeVal INTEGER, + floodplainFile TEXT, + thresholdResFlood INTEGER, + thresholdResNoFlood INTEGER, + useLandscapes BOOLEAN DEFAULT (false), + useLeftRight BOOLEAN DEFAULT (false) + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS config_observed (observedFile TEXT) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS config_params ( + burninDepth REAL, + channelWidthMultiplier REAL, + channelWidthExponent REAL, + channelDepthMultiplier REAL, + channelDepthExponent REAL, + reachSlopeMultiplier REAL, + tributarySlopeMultiplier REAL, + meanSlopeMultiplier REAL, + mainLengthMultiplier REAL, + tributaryLengthMultiplier REAL, + upslopeHRUDrain REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS config_soil ( + \"database\" TEXT, + databaseTable TEXT, + file TEXT, + tabl TEXT, + useSSURGO BOOLEAN DEFAULT (false), + useSTATSGO BOOLEAN DEFAULT (false) + ) + ") + + # ---- Intermediate data tables ---- + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS BASINSDATA ( + basin INTEGER, + drainArea REAL, + outletCol INTEGER, + outletRow INTEGER, + outletElevation REAL, + startCol INTEGER, + startRow INTEGER, + startToOutletDistance REAL, + startToOutletDrop REAL, + farCol INTEGER, + farRow INTEGER, + farthest INTEGER, + farElevation REAL, + farDistance REAL, + maxElevation REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS HRUSDATA ( + hru INTEGER, + lsu INTEGER, + basin INTEGER, + crop INTEGER, + soil INTEGER, + slope INTEGER, + cellCount INTEGER, + area REAL, + totalElevation REAL, + totalSlope REAL, + totalLatitude REAL, + totalLongitude REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS LSUSDATA ( + lsu INTEGER, + basin INTEGER, + cellCount INTEGER, + area REAL, + totalElevation REAL, + totalSlope REAL, + totalLatitude REAL, + totalLongitude REAL, + cropSoilSlopeArea REAL, + hru INTEGER + ) + ") + DBI::dbExecute(con, " + CREATE UNIQUE INDEX IF NOT EXISTS basin_lu_index ON LSUSDATA (lsu, basin) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS WATERDATA ( + id INTEGER PRIMARY KEY, + subbasin INTEGER, + area REAL, + lat REAL, + lon REAL, + elev REAL + ) + ") + + # ---- Lookup / reference tables ---- + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS global_landuses ( + LANDUSE_ID INTEGER PRIMARY KEY, + SWAT_CODE TEXT + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS global_soils ( + SOIL_ID INTEGER PRIMARY KEY, + SNAM TEXT + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS global_usersoil ( + OBJECTID INTEGER, MUID TEXT, SEQN TEXT, SNAM TEXT, S5ID TEXT, + CMPPCT TEXT, NLAYERS REAL, HYDGRP TEXT, SOL_ZMX REAL, + ANION_EXCL REAL, SOL_CRK REAL, TEXTURE TEXT, + SOL_Z1 REAL, SOL_BD1 REAL, SOL_AWC1 REAL, SOL_K1 REAL, + SOL_CBN1 REAL, CLAY1 REAL, SILT1 REAL, SAND1 REAL, + ROCK1 REAL, SOL_ALB1 REAL, USLE_K1 REAL, SOL_EC1 REAL, + SOL_Z2 REAL, SOL_BD2 REAL, SOL_AWC2 REAL, SOL_K2 REAL, + SOL_CBN2 REAL, CLAY2 REAL, SILT2 REAL, SAND2 REAL, + ROCK2 REAL, SOL_ALB2 REAL, USLE_K2 REAL, SOL_EC2 REAL, + SOL_Z3 REAL, SOL_BD3 REAL, SOL_AWC3 REAL, SOL_K3 REAL, + SOL_CBN3 REAL, CLAY3 REAL, SILT3 REAL, SAND3 REAL, + ROCK3 REAL, SOL_ALB3 REAL, USLE_K3 REAL, SOL_EC3 REAL, + SOL_Z4 REAL, SOL_BD4 REAL, SOL_AWC4 REAL, SOL_K4 REAL, + SOL_CBN4 REAL, CLAY4 REAL, SILT4 REAL, SAND4 REAL, + ROCK4 REAL, SOL_ALB4 REAL, USLE_K4 REAL, SOL_EC4 REAL, + SOL_Z5 REAL, SOL_BD5 REAL, SOL_AWC5 REAL, SOL_K5 REAL, + SOL_CBN5 REAL, CLAY5 REAL, SILT5 REAL, SAND5 REAL, + ROCK5 REAL, SOL_ALB5 REAL, USLE_K5 REAL, SOL_EC5 REAL, + SOL_Z6 REAL, SOL_BD6 REAL, SOL_AWC6 REAL, SOL_K6 REAL, + SOL_CBN6 REAL, CLAY6 REAL, SILT6 REAL, SAND6 REAL, + ROCK6 REAL, SOL_ALB6 REAL, USLE_K6 REAL, SOL_EC6 REAL, + SOL_Z7 REAL, SOL_BD7 REAL, SOL_AWC7 REAL, SOL_K7 REAL, + SOL_CBN7 REAL, CLAY7 REAL, SILT7 REAL, SAND7 REAL, + ROCK7 REAL, SOL_ALB7 REAL, USLE_K7 REAL, SOL_EC7 REAL, + SOL_Z8 REAL, SOL_BD8 REAL, SOL_AWC8 REAL, SOL_K8 REAL, + SOL_CBN8 REAL, CLAY8 REAL, SILT8 REAL, SAND8 REAL, + ROCK8 REAL, SOL_ALB8 REAL, USLE_K8 REAL, SOL_EC8 REAL, + SOL_Z9 REAL, SOL_BD9 REAL, SOL_AWC9 REAL, SOL_K9 REAL, + SOL_CBN9 REAL, CLAY9 REAL, SILT9 REAL, SAND9 REAL, + ROCK9 REAL, SOL_ALB9 REAL, USLE_K9 REAL, SOL_EC9 REAL, + SOL_Z10 REAL, SOL_BD10 REAL, SOL_AWC10 REAL, SOL_K10 REAL, + SOL_CBN10 REAL, CLAY10 REAL, SILT10 REAL, SAND10 REAL, + ROCK10 REAL, SOL_ALB10 REAL, USLE_K10 REAL, SOL_EC10 REAL, + SOL_CAL1 REAL, SOL_CAL2 REAL, SOL_CAL3 REAL, SOL_CAL4 REAL, + SOL_CAL5 REAL, SOL_CAL6 REAL, SOL_CAL7 REAL, SOL_CAL8 REAL, + SOL_CAL9 REAL, SOL_CAL10 REAL, + SOL_PH1 REAL, SOL_PH2 REAL, SOL_PH3 REAL, SOL_PH4 REAL, + SOL_PH5 REAL, SOL_PH6 REAL, SOL_PH7 REAL, SOL_PH8 REAL, + SOL_PH9 REAL, SOL_PH10 REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS plant ( + id INTEGER NOT NULL PRIMARY KEY, name VARCHAR(255) NOT NULL, + plnt_typ VARCHAR(255) NOT NULL, gro_trig VARCHAR(255) NOT NULL, + nfix_co REAL NOT NULL, days_mat REAL NOT NULL, + bm_e REAL NOT NULL, harv_idx REAL NOT NULL, + lai_pot REAL NOT NULL, frac_hu1 REAL NOT NULL, + lai_max1 REAL NOT NULL, frac_hu2 REAL NOT NULL, + lai_max2 REAL NOT NULL, hu_lai_decl REAL NOT NULL, + dlai_rate REAL NOT NULL, can_ht_max REAL NOT NULL, + rt_dp_max REAL NOT NULL, tmp_opt REAL NOT NULL, + tmp_base REAL NOT NULL, frac_n_yld REAL NOT NULL, + frac_p_yld REAL NOT NULL, frac_n_em REAL NOT NULL, + frac_n_50 REAL NOT NULL, frac_n_mat REAL NOT NULL, + frac_p_em REAL NOT NULL, frac_p_50 REAL NOT NULL, + frac_p_mat REAL NOT NULL, harv_idx_ws REAL NOT NULL, + usle_c_min REAL NOT NULL, stcon_max REAL NOT NULL, + vpd REAL NOT NULL, frac_stcon REAL NOT NULL, + ru_vpd REAL NOT NULL, co2_hi REAL NOT NULL, + bm_e_hi REAL NOT NULL, plnt_decomp REAL NOT NULL, + lai_min REAL NOT NULL, bm_tree_acc REAL NOT NULL, + yrs_mat REAL NOT NULL, bm_tree_max REAL NOT NULL, + ext_co REAL NOT NULL, leaf_tov_mn REAL NOT NULL, + leaf_tov_mx REAL NOT NULL, bm_dieoff REAL NOT NULL, + rt_st_beg REAL NOT NULL, rt_st_end REAL NOT NULL, + plnt_pop1 REAL NOT NULL, frac_lai1 REAL NOT NULL, + plnt_pop2 REAL NOT NULL, frac_lai2 REAL NOT NULL, + frac_sw_gro REAL NOT NULL, aeration REAL NOT NULL, + rsd_pctcov REAL NOT NULL, rsd_covfac REAL NOT NULL, + description TEXT + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS urban ( + id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL, + frac_imp REAL NOT NULL, frac_dc_imp REAL NOT NULL, + curb_den REAL NOT NULL, urb_wash REAL NOT NULL, + dirt_max REAL NOT NULL, t_halfmax REAL NOT NULL, + conc_totn REAL NOT NULL, conc_totp REAL NOT NULL, + conc_no3n REAL NOT NULL, urb_cn REAL NOT NULL, + description TEXT + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS WGEN_User ( + id INTEGER NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + lat REAL NOT NULL, lon REAL NOT NULL, + elev REAL NOT NULL, rain_yrs INTEGER NOT NULL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS WGEN_User_mon ( + id INTEGER NOT NULL PRIMARY KEY, + weather_wgn_cli_id INTEGER NOT NULL, + month INTEGER NOT NULL, + tmp_max_ave REAL NOT NULL, tmp_min_ave REAL NOT NULL, + tmp_max_sd REAL NOT NULL, tmp_min_sd REAL NOT NULL, + pcp_ave REAL NOT NULL, pcp_sd REAL NOT NULL, + pcp_skew REAL NOT NULL, wet_dry REAL NOT NULL, + wet_wet REAL NOT NULL, pcp_days REAL NOT NULL, + pcp_hhr REAL NOT NULL, slr_ave REAL NOT NULL, + dew_ave REAL NOT NULL, wnd_ave REAL NOT NULL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS weather_sta_cli ( + id INTEGER NOT NULL PRIMARY KEY, + name VARCHAR (255) NOT NULL, + wgn_id INTEGER, + pcp VARCHAR (255), + tmp VARCHAR (255), + slr VARCHAR (255), + hmd VARCHAR (255), + wnd VARCHAR (255), + pet VARCHAR (255), + atmo_dep VARCHAR (255), + lat REAL, + lon REAL + ) + ") +} + + + +#' Compute Per-Subbasin Centroids in WGS84 +#' +#' Groups raster cells by subbasin ID, computes the mean XY centre in the +#' raster's native CRS, then reprojects to WGS84 (EPSG:4326). +#' Returns NULL silently when no watershed raster is available. +#' @noRd +.compute_subbasin_centroids_wgs84 <- function(project) { + rast_obj <- NULL + if (!is.null(project$watershed_rast) && + inherits(project$watershed_rast, "SpatRaster")) { + rast_obj <- project$watershed_rast + } else if (!is.null(project$watershed_file) && + file.exists(project$watershed_file)) { + rast_obj <- tryCatch(terra::rast(project$watershed_file), + error = function(e) NULL) + } + if (is.null(rast_obj)) return(NULL) + + tryCatch({ + vals <- terra::values(rast_obj, mat = FALSE) + valid <- !is.na(vals) & vals > 0 + if (sum(valid) == 0L) return(NULL) + + cell_ids <- which(valid) + coords <- terra::xyFromCell(rast_obj, cell_ids) + sub_ids <- as.integer(vals[cell_ids]) + + df <- data.frame(subbasin = sub_ids, + x = coords[, 1], + y = coords[, 2]) + + # Mean XY per subbasin in projected CRS + xmean <- tapply(df$x, df$subbasin, mean) + ymean <- tapply(df$y, df$subbasin, mean) + subs <- as.integer(names(xmean)) + + pts <- sf::st_as_sf( + data.frame(subbasin = subs, x = as.numeric(xmean), y = as.numeric(ymean)), + coords = c("x", "y"), + crs = sf::st_crs(terra::crs(rast_obj)) + ) + pts_wgs84 <- sf::st_transform(pts, crs = 4326) + xy_wgs84 <- sf::st_coordinates(pts_wgs84) + + data.frame(subbasin = subs, + lat = as.numeric(xy_wgs84[, "Y"]), + lon = as.numeric(xy_wgs84[, "X"])) + }, error = function(e) { + message("Note: Could not compute subbasin centroids for lat/lon: ", + conditionMessage(e)) + NULL + }) +} + + +#' Look up lat/lon for a vector of subbasin IDs from the centroid table. +#' Returns 0 for any subbasin not found in the table or when \code{centroids} +#' is NULL. +#' @noRd +.lookup_latlon <- function(subbasin_ids, centroids) { + if (is.null(centroids)) { + return(list(lat = rep(0, length(subbasin_ids)), + lon = rep(0, length(subbasin_ids)))) + } + idx <- match(subbasin_ids, centroids$subbasin) + lat <- ifelse(is.na(idx), 0, centroids$lat[idx]) + lon <- ifelse(is.na(idx), 0, centroids$lon[idx]) + list(lat = lat, lon = lon) +} + + +#' Write Subbasin Table +#' @noRd +.write_subbasin_table <- function(con, basin_data, centroids = NULL) { + if (is.null(basin_data) || nrow(basin_data) == 0) return(invisible()) + + ll <- .lookup_latlon(basin_data$subbasin, centroids) + + df <- data.frame( + id = basin_data$subbasin, + area = basin_data$area_ha, + slo1 = basin_data$mean_slope, + len1 = sqrt(basin_data$area_ha * 10000), + sll = basin_data$mean_slope, + lat = ll$lat, + lon = ll$lon, + elev = basin_data$mean_elevation, + elevmin = basin_data$min_elevation, + elevmax = basin_data$max_elevation, + waterid = 0L, + stringsAsFactors = FALSE + ) + + DBI::dbWriteTable(con, "gis_subbasins", df, append = TRUE, row.names = FALSE) +} + + +#' Write HRU Table +#' @noRd +.write_hru_table <- function(con, hru_data, slope_classes, centroids = NULL) { + if (is.null(hru_data) || nrow(hru_data) == 0) return(invisible()) + + # Compute percentages + sub_areas <- tapply(hru_data$area_ha, hru_data$subbasin, sum) + hru_data$sub_area <- sub_areas[as.character(hru_data$subbasin)] + + # arsub = HRU area / subbasin area + hru_data$arsub <- hru_data$area_ha / hru_data$sub_area * 100 + + # arlsu = HRU area / LSU area (using subbasin as LSU) + hru_data$arlsu <- hru_data$area_ha / hru_data$sub_area * 100 + + # arland = landuse area / subbasin area + hru_data$arland <- 0 + hru_data$arso <- 0 + hru_data$arslp <- 0 + + for (sub in unique(hru_data$subbasin)) { + idx <- hru_data$subbasin == sub + sub_area <- sum(hru_data$area_ha[idx]) + for (lu in unique(hru_data$landuse[idx])) { + lu_idx <- idx & hru_data$landuse == lu + lu_area <- sum(hru_data$area_ha[lu_idx]) + hru_data$arland[lu_idx] <- lu_area / sub_area * 100 + for (soil in unique(hru_data$soil[lu_idx])) { + soil_idx <- lu_idx & hru_data$soil == soil + soil_area <- sum(hru_data$area_ha[soil_idx]) + hru_data$arso[soil_idx] <- soil_area / lu_area * 100 + for (slp in unique(hru_data$slope_class[soil_idx])) { + slp_idx <- soil_idx & hru_data$slope_class == slp + slp_area <- sum(hru_data$area_ha[slp_idx]) + hru_data$arslp[slp_idx] <- slp_area / soil_area * 100 + } + } + } + } + + # Build slope class label + slp_label <- as.character(hru_data$slope_class) + if (!is.null(slope_classes) && nrow(slope_classes) > 0) { + slp_label <- slope_classes$label[ + match(hru_data$slope_class, slope_classes$class_id)] + slp_label[is.na(slp_label)] <- as.character( + hru_data$slope_class[is.na(slp_label)]) + } + + df <- data.frame( + id = hru_data$hru_id, + lsu = hru_data$subbasin, + arsub = round(hru_data$arsub, 4), + arlsu = round(hru_data$arlsu, 4), + landuse = hru_data$landuse, + arland = round(hru_data$arland, 4), + soil = hru_data$soil, + arso = round(hru_data$arso, 4), + slp = slp_label, + arslp = round(hru_data$arslp, 4), + slope = hru_data$mean_slope, + lat = .lookup_latlon(hru_data$subbasin, centroids)$lat, + lon = .lookup_latlon(hru_data$subbasin, centroids)$lon, + elev = hru_data$mean_elevation, + stringsAsFactors = FALSE + ) + + DBI::dbWriteTable(con, "gis_hrus", df, append = TRUE, row.names = FALSE) +} + + +#' Resolve the first valid downstream WSNO (> 0), following the chain past any +#' WSNO == 0 stream links. Returns NA_integer_ if the chain reaches an outlet +#' without finding a valid subbasin. +#' @noRd +.resolve_downstream_wsno <- function(topo, link_no) { + visited <- character(0) + while (!is.na(link_no) && link_no >= 0) { + key <- as.character(link_no) + if (key %in% visited) break # guard against topology loops + visited <- c(visited, key) + + row <- topo[topo$LINKNO == link_no, , drop = FALSE] + if (nrow(row) == 0) break + + wsno <- row$WSNO[1] + if (!is.na(wsno) && wsno > 0) return(wsno) + + # WSNO == 0: follow further downstream + ds_link <- row$DSLINKNO[1] + if (is.na(ds_link) || ds_link < 0) break # reached the outlet + link_no <- ds_link + } + NA_integer_ +} + + +#' Write Routing Table +#' @noRd +.write_routing_table <- function(con, project) { + if (is.null(project$stream_topology)) return(invisible()) + + topo <- project$stream_topology + routes <- list() + + for (i in seq_len(nrow(topo))) { + link <- topo$LINKNO[i] + ds_link <- topo$DSLINKNO[i] + wsno <- topo$WSNO[i] + + # Skip stream links that have no valid subbasin (WSNO == 0 or NA). + # These are typically the most-downstream outlet reach generated by + # TauDEM when an outlet point is provided; they have no cells in the + # watershed raster and therefore no entry in gis_subbasins. + if (is.na(wsno) || wsno == 0) next + + if (!is.na(ds_link) && ds_link >= 0) { + # Follow the chain past any WSNO == 0 intermediate links. + ds_wsno <- .resolve_downstream_wsno(topo, ds_link) + + if (!is.na(ds_wsno)) { + routes[[length(routes) + 1]] <- data.frame( + sourceid = wsno, + sourcecat = "sub", + hyd_typ = "tot", + sinkid = ds_wsno, + sinkcat = "sub", + percent = 100, + stringsAsFactors = FALSE + ) + } else { + # Downstream chain ends at an outlet (WSNO == 0 or NA at outlet) + routes[[length(routes) + 1]] <- data.frame( + sourceid = wsno, + sourcecat = "sub", + hyd_typ = "tot", + sinkid = 0L, + sinkcat = "outlet", + percent = 100, + stringsAsFactors = FALSE + ) + } + } else { + # Direct outlet + routes[[length(routes) + 1]] <- data.frame( + sourceid = wsno, + sourcecat = "sub", + hyd_typ = "tot", + sinkid = 0L, + sinkcat = "outlet", + percent = 100, + stringsAsFactors = FALSE + ) + } + } + + if (length(routes) > 0) { + routing_df <- do.call(rbind, routes) + DBI::dbWriteTable(con, "gis_routing", routing_df, + append = TRUE, row.names = FALSE) + } +} + + +#' Write Channel Table +#' @noRd +.write_channel_table <- function(con, project) { + if (is.null(project$stream_topology)) return(invisible()) + + topo <- project$stream_topology + # Only include channels whose WSNO maps to a real subbasin (WSNO > 0). + # WSNO == 0 is assigned by TauDEM to the outlet reach when an outlet point + # is provided; it has no corresponding subbasin in gis_subbasins. + valid <- !is.na(topo$WSNO) & topo$WSNO > 0 + + # Estimate drainage area per channel from basin data if available + areac_val <- rep(0, sum(valid)) + if (!is.null(project$basin_data)) { + bd <- project$basin_data + areac_val <- bd$area_ha[match(topo$WSNO[valid], bd$subbasin)] + areac_val[is.na(areac_val)] <- 0 + } + + # Use Strahler stream order from topology if available + strahler_val <- rep(1L, sum(valid)) + if ("strmOrder" %in% names(topo)) { + strahler_val <- topo$strmOrder[valid] + strahler_val[is.na(strahler_val)] <- 1L + } + + # Calculate midpoint lat/lon from stream geometries if available. + # Matches the Python approach: midpoint is 50% along each reach, then + # projected to WGS84 lat/lon. + midlat_val <- rep(0, sum(valid)) + midlon_val <- rep(0, sum(valid)) + if (!is.null(project$streams_sf) && inherits(project$streams_sf, "sf")) { + tryCatch({ + streams_valid <- project$streams_sf[valid, ] + # Sample the midpoint (50%) along each linestring. + # st_line_sample may return sfc_MULTIPOINT (one point per feature); + # cast to POINT to guarantee a single row per feature from st_coordinates. + geom_valid <- sf::st_geometry(streams_valid) + mids <- sf::st_line_sample(geom_valid, sample = 0.5) + mids <- sf::st_cast(mids, "POINT") + # Preserve CRS for the transform + mids <- sf::st_set_crs(mids, sf::st_crs(streams_valid)) + # Reproject to WGS84 for lat/lon output + mids_wgs84 <- sf::st_transform(mids, crs = 4326) + coords <- sf::st_coordinates(mids_wgs84) + if (nrow(coords) == sum(valid)) { + midlon_val <- as.numeric(coords[, "X"]) + midlat_val <- as.numeric(coords[, "Y"]) + } + }, error = function(e) { + message("Note: Could not compute midlat/midlon from stream geometries: ", + conditionMessage(e)) + }) + } + + df <- data.frame( + id = seq_len(sum(valid)), + subbasin = topo$WSNO[valid], + areac = areac_val, + strahler = strahler_val, + len2 = if (!all(is.na(topo$Length))) topo$Length[valid] else 0, + slo2 = 0.01, + wid2 = 1.0, + dep2 = 0.5, + elevmin = 0, + elevmax = 0, + midlat = midlat_val, + midlon = midlon_val, + stringsAsFactors = FALSE + ) + + # Fill elevmin/elevmax from basin data if available + if (!is.null(project$basin_data)) { + bd <- project$basin_data + idx <- match(df$subbasin, bd$subbasin) + df$elevmin[!is.na(idx)] <- bd$min_elevation[idx[!is.na(idx)]] + df$elevmax[!is.na(idx)] <- bd$max_elevation[idx[!is.na(idx)]] + } + + DBI::dbWriteTable(con, "gis_channels", df, append = TRUE, row.names = FALSE) +} + + +#' Write LSU Table +#' @noRd +.write_lsu_table <- function(con, project, centroids = NULL) { + if (is.null(project$basin_data)) return(invisible()) + + bd <- project$basin_data + ll <- .lookup_latlon(bd$subbasin, centroids) + + df <- data.frame( + id = seq_len(nrow(bd)), + category = 0L, + channel = bd$subbasin, + subbasin = bd$subbasin, + area = bd$area_ha, + slope = bd$mean_slope, + len1 = sqrt(bd$area_ha * 10000), + csl = bd$mean_slope, + wid1 = 1.0, + dep1 = 0.5, + lat = ll$lat, + lon = ll$lon, + elev = bd$mean_elevation, + stringsAsFactors = FALSE + ) + + DBI::dbWriteTable(con, "gis_lsus", df, append = TRUE, row.names = FALSE) +} + + +#' Write Aquifer Tables +#' @noRd +.write_aquifer_table <- function(con, basin_data, centroids = NULL) { + if (is.null(basin_data)) return(invisible()) + + ll <- .lookup_latlon(basin_data$subbasin, centroids) + + # One shallow aquifer per subbasin + aq_df <- data.frame( + id = seq_len(nrow(basin_data)), + subbasin = basin_data$subbasin, + deep_aquifer = 1L, + area = basin_data$area_ha, + lat = ll$lat, + lon = ll$lon, + elev = basin_data$mean_elevation, + stringsAsFactors = FALSE + ) + DBI::dbWriteTable(con, "gis_aquifers", aq_df, + append = TRUE, row.names = FALSE) + + # One deep aquifer for whole watershed: area-weighted centroid + if (!is.null(centroids) && nrow(centroids) > 0) { + idx <- match(basin_data$subbasin, centroids$subbasin) + valid_i <- which(!is.na(idx)) + w_valid <- basin_data$area_ha[valid_i] + w_total <- sum(w_valid) + if (w_total > 0) { + lat_deep <- sum(centroids$lat[idx[valid_i]] * w_valid) / w_total + lon_deep <- sum(centroids$lon[idx[valid_i]] * w_valid) / w_total + } else { + lat_deep <- 0 + lon_deep <- 0 + } + } else { + lat_deep <- 0 + lon_deep <- 0 + } + deep_df <- data.frame( + id = 1L, + area = sum(basin_data$area_ha), + lat = lat_deep, + lon = lon_deep, + elev = mean(basin_data$mean_elevation), + stringsAsFactors = FALSE + ) + DBI::dbWriteTable(con, "gis_deep_aquifers", deep_df, + append = TRUE, row.names = FALSE) +} + + +#' Write Water Body Table (empty placeholder) +#' @noRd +.write_water_table <- function(con) { + # Empty table - populated by user or SWAT+ Editor + invisible() +} + + +#' Write Point Source Table (empty placeholder) +#' @noRd +.write_point_table <- function(con) { + # Empty table - populated by user or SWAT+ Editor + invisible() +} + + +#' Populate soils_sol and soils_sol_layer from global_usersoil +#' +#' Mirrors the Python \code{writeSoilsTable()} / \code{writeUsedSoilRow()} +#' logic: reads only the soil names actually used by HRUs from the +#' \code{global_usersoil} table (populated earlier by +#' \code{.populate_usersoil()}), then writes summary rows to +#' \code{soils_sol} and per-layer rows to \code{soils_sol_layer}. +#' +#' The data flow matches the Python plugin: +#' usersoil -> global_usersoil -> soils_sol / soils_sol_layer +#' +#' @param con DBI connection to the project database. +#' @param hru_data Data frame of HRU data (must contain a \code{soil} column). +#' @return Invisible \code{NULL}. +#' @noRd +.write_soils_sol_table <- function(con, hru_data) { + if (is.null(hru_data)) return(invisible(NULL)) + + # Check global_usersoil exists and has data + tables <- DBI::dbListTables(con) + if (!"global_usersoil" %in% tables) return(invisible(NULL)) + n_us <- .safe_table_count(con, "global_usersoil") + if (n_us == 0L) return(invisible(NULL)) + + # Get unique soil names used in HRUs + used_soils <- unique(hru_data$soil) + if (length(used_soils) == 0L) return(invisible(NULL)) + + # Read matching rows from global_usersoil + placeholders <- paste(rep("?", length(used_soils)), collapse = ", ") + sql <- paste0("SELECT * FROM global_usersoil WHERE SNAM IN (", + placeholders, ")") + us_data <- DBI::dbGetQuery(con, sql, params = as.list(used_soils)) + + if (nrow(us_data) == 0L) { + message(" Warning: No matching soils found in global_usersoil ", + "for HRU soil names.") + return(invisible(NULL)) + } + + # Clear existing data + DBI::dbExecute(con, "DELETE FROM soils_sol") + DBI::dbExecute(con, "DELETE FROM soils_sol_layer") + + # Build soils_sol and soils_sol_layer from the wide-format usersoil data + # global_usersoil has 152 columns: + # OBJECTID, MUID, SEQN, SNAM, S5ID, CMPPCT, NLAYERS, HYDGRP, SOL_ZMX, + # ANION_EXCL, SOL_CRK, TEXTURE, + # SOL_Z1..SOL_EC1 (12 cols x 10 layers), + # SOL_CAL1..SOL_CAL10, SOL_PH1..SOL_PH10 + sol_rows <- list() + layer_rows <- list() + lid <- 0L + + for (i in seq_len(nrow(us_data))) { + row <- us_data[i, ] + sid <- i + + # soils_sol row + sol_rows[[i]] <- data.frame( + id = sid, + name = as.character(row$SNAM), + hyd_grp = as.character(row$HYDGRP), + dp_tot = as.numeric(row$SOL_ZMX), + anion_excl = as.numeric(row$ANION_EXCL), + perc_crk = as.numeric(row$SOL_CRK), + texture = if ("TEXTURE" %in% names(row)) as.character(row$TEXTURE) else NA_character_, + description = NA_character_, + stringsAsFactors = FALSE + ) + + # Number of layers + nlayers <- as.integer(row$NLAYERS) + if (is.na(nlayers) || nlayers < 1L) nlayers <- 1L + if (nlayers > 10L) nlayers <- 10L + + # Layer property column prefixes (wide format) + layer_props <- c("SOL_Z", "SOL_BD", "SOL_AWC", "SOL_K", + "SOL_CBN", "CLAY", "SILT", "SAND", + "ROCK", "SOL_ALB", "USLE_K", "SOL_EC") + + row_names <- names(row) + + for (j in seq_len(nlayers)) { + lid <- lid + 1L + + col_names <- paste0(layer_props, j) + cal_col <- paste0("SOL_CAL", j) + ph_col <- paste0("SOL_PH", j) + + # Core layer props are NOT NULL in the schema; default to 0 + get_val <- function(cn) { + if (cn %in% row_names) as.numeric(row[[cn]]) else 0 + } + + layer_rows[[lid]] <- data.frame( + id = lid, + soil_id = sid, + layer_num = j, + dp = get_val(col_names[1]), + bd = get_val(col_names[2]), + awc = get_val(col_names[3]), + soil_k = get_val(col_names[4]), + carbon = get_val(col_names[5]), + clay = get_val(col_names[6]), + silt = get_val(col_names[7]), + sand = get_val(col_names[8]), + rock = get_val(col_names[9]), + alb = get_val(col_names[10]), + usle_k = get_val(col_names[11]), + ec = get_val(col_names[12]), + caco3 = if (cal_col %in% row_names) as.numeric(row[[cal_col]]) else NA_real_, + ph = if (ph_col %in% row_names) as.numeric(row[[ph_col]]) else NA_real_, + stringsAsFactors = FALSE + ) + } + } + + sol_df <- do.call(rbind, sol_rows) + layer_df <- do.call(rbind, layer_rows) + + DBI::dbWriteTable(con, "soils_sol", sol_df, append = TRUE, row.names = FALSE) + DBI::dbWriteTable(con, "soils_sol_layer", layer_df, append = TRUE, + row.names = FALSE) + + message(" Wrote ", nrow(sol_df), " soils to soils_sol, ", + nrow(layer_df), " layers to soils_sol_layer") + invisible(NULL) +} + + +#' Populate global_usersoil and global_soils from a usersoil source +#' +#' Loads soil physical properties into the \code{global_usersoil} table and +#' corresponding soil names into \code{global_soils}. The source can be +#' one of the built-in datasets bundled with the package, or a user-supplied +#' CSV file. +#' +#' @param con DBI connection to the (already created) project database. +#' @param usersoil One of \code{"FAO_usersoil"}, \code{"global_usersoil"}, +#' or a file path to a CSV. Validated before use. +#' @return Invisible \code{NULL}. +#' @noRd +.populate_usersoil <- function(con, usersoil) { + if (is.null(usersoil)) return(invisible(NULL)) + + if (!is.character(usersoil) || length(usersoil) != 1L) { + stop( + "'usersoil' must be a single string: ", + "'FAO_usersoil', 'global_usersoil', or a CSV file path.", + call. = FALSE + ) + } + + builtin_opts <- c("FAO_usersoil", "global_usersoil") + + if (usersoil %in% builtin_opts) { + # ---------------------------------------------------------------- + # Load from the bundled QSWATPlusProjHAWQS database + # ---------------------------------------------------------------- + proj_hawqs_db <- system.file("extdata", "QSWATPlusProjHAWQS.sqlite", + package = "rQSWATPlus") + if (!nzchar(proj_hawqs_db) || !file.exists(proj_hawqs_db)) { + warning( + "QSWATPlusProjHAWQS.sqlite not found; cannot populate usersoil.", + call. = FALSE + ) + return(invisible(NULL)) + } + + # Companion global_soils / FAO_soils table + soils_tbl <- if (usersoil == "FAO_usersoil") "FAO_soils" else "global_soils" + + DBI::dbExecute(con, paste0( + "ATTACH DATABASE '", proj_hawqs_db, "' AS proj_hawqs_us")) + on.exit(tryCatch( + DBI::dbExecute(con, "DETACH DATABASE proj_hawqs_us"), + error = function(e) NULL + ), add = TRUE) + + # Clear and repopulate global_usersoil + DBI::dbExecute(con, "DELETE FROM main.global_usersoil") + DBI::dbExecute(con, paste0( + "INSERT INTO main.global_usersoil ", + "SELECT * FROM proj_hawqs_us.[", usersoil, "]" + )) + n_us <- .safe_table_count(con, "global_usersoil") + message(" Loaded ", n_us, " rows into global_usersoil from '", usersoil, "'") + + # Populate global_soils + DBI::dbExecute(con, "DELETE FROM main.global_soils") + DBI::dbExecute(con, paste0( + "INSERT INTO main.global_soils ", + "SELECT * FROM proj_hawqs_us.[", soils_tbl, "]" + )) + n_gs <- .safe_table_count(con, "global_soils") + message(" Loaded ", n_gs, " rows into global_soils from '", soils_tbl, "'") + + DBI::dbExecute(con, "DETACH DATABASE proj_hawqs_us") + + } else { + # ---------------------------------------------------------------- + # Load from a user-supplied CSV file + # ---------------------------------------------------------------- + if (!file.exists(usersoil)) { + stop("usersoil file not found: ", usersoil, call. = FALSE) + } + + df <- utils::read.csv(usersoil, stringsAsFactors = FALSE, + check.names = FALSE) + names(df) <- toupper(names(df)) # normalise to uppercase + + if (!"SNAM" %in% names(df)) { + stop( + "usersoil CSV '", basename(usersoil), + "' must contain an 'SNAM' column with soil names.", + call. = FALSE + ) + } + + # Keep only columns that exist in the global_usersoil table + expected_cols <- DBI::dbListFields(con, "global_usersoil") + valid_cols <- intersect(names(df), expected_cols) + df <- df[, valid_cols, drop = FALSE] + + # Remove rows with missing soil names + df <- df[!is.na(df$SNAM) & nzchar(df$SNAM), , drop = FALSE] + if (nrow(df) == 0L) { + warning("No valid rows in usersoil CSV; global_usersoil not populated.", + call. = FALSE) + return(invisible(NULL)) + } + + DBI::dbExecute(con, "DELETE FROM main.global_usersoil") + DBI::dbWriteTable(con, "global_usersoil", df, + append = TRUE, row.names = FALSE) + n_us <- .safe_table_count(con, "global_usersoil") + message(" Loaded ", n_us, " rows into global_usersoil from CSV: ", + basename(usersoil)) + + # Populate global_soils from unique SNAM values in the CSV + snames <- unique(df$SNAM) + soil_df <- data.frame( + SOIL_ID = seq_along(snames), + SNAM = snames, + stringsAsFactors = FALSE + ) + DBI::dbExecute(con, "DELETE FROM main.global_soils") + DBI::dbWriteTable(con, "global_soils", soil_df, + append = TRUE, row.names = FALSE) + message(" Loaded ", nrow(soil_df), " rows into global_soils from CSV") + } + + invisible(NULL) +} + + + +#' +#' Copies the bundled reference databases to the project folder: +#' \itemize{ +#' \item \file{QSWATPlusProj.sqlite} \rarr \file{swatplus_datasets.sqlite} +#' (standard project template) +#' \item \file{QSWATPlusRefHAWQS.sqlite} \rarr +#' \file{swatplus_datasets_ref.sqlite} (HAWQS reference/parameter data) +#' \item \file{QSWATPlusProjHAWQS.sqlite} \rarr +#' \file{swatplus_datasets_proj_hawqs.sqlite} (HAWQS project template) +#' } +#' Returns the normalised path to the main copy (or \code{NA} if the bundled +#' source cannot be found). +#' @noRd +.copy_reference_database <- function(project_dir) { + src <- system.file("extdata", "QSWATPlusProj.sqlite", + package = "rQSWATPlus") + if (!nzchar(src) || !file.exists(src)) { + message("Reference database not found; skipping copy.") + return(NA_character_) + } + + dest <- file.path(project_dir, "swatplus_datasets.sqlite") + if (!file.exists(dest)) { + file.copy(src, dest) + message("Reference database copied to: ", dest) + } + + # Copy HAWQS reference database (rich parameter data) + ref_hawqs_src <- system.file("extdata", "QSWATPlusRefHAWQS.sqlite", + package = "rQSWATPlus") + if (nzchar(ref_hawqs_src) && file.exists(ref_hawqs_src)) { + ref_hawqs_dest <- file.path(project_dir, "swatplus_datasets_ref.sqlite") + if (!file.exists(ref_hawqs_dest)) { + file.copy(ref_hawqs_src, ref_hawqs_dest) + } + } + + # Copy HAWQS project template database + proj_hawqs_src <- system.file("extdata", "QSWATPlusProjHAWQS.sqlite", + package = "rQSWATPlus") + if (nzchar(proj_hawqs_src) && file.exists(proj_hawqs_src)) { + proj_hawqs_dest <- file.path(project_dir, + "swatplus_datasets_proj_hawqs.sqlite") + if (!file.exists(proj_hawqs_dest)) { + file.copy(proj_hawqs_src, proj_hawqs_dest) + } + } + + normalizePath(dest, mustWork = FALSE) +} + + +#' Write project_config Table +#' +#' Populates the project_config table with project metadata so that +#' SWAT+ Editor can recognize and open the database. +#' @noRd +.write_project_config <- function(con, project, db_file, ref_db_path = NA_character_) { + project_name <- basename(project$project_dir) + project_dir <- normalizePath(project$project_dir, mustWork = FALSE) + + df <- data.frame( + id = 1L, + project_name = project_name, + project_directory = project_dir, + editor_version = NA_character_, + gis_type = "qgis", + gis_version = NA_character_, + project_db = normalizePath(db_file, mustWork = FALSE), + reference_db = ref_db_path, + wgn_db = NA_character_, + wgn_table_name = "wgn_cfsr_world", + weather_data_dir = NA_character_, + weather_data_format = NA_character_, + input_files_dir = file.path(project_dir, "Scenarios", + "Default", "TxtInOut"), + input_files_last_written = NA_character_, + swat_last_run = NA_character_, + delineation_done = 1L, + hrus_done = 1L, + soil_table = "soils_sol", + soil_layer_table = "soils_sol_layer", + output_last_imported = NA_character_, + imported_gis = 1L, + is_lte = 0L, + use_gwflow = if (isTRUE(project$use_gwflow)) 1L else 0L, + stringsAsFactors = FALSE + ) + + DBI::dbWriteTable(con, "project_config", df, + append = TRUE, row.names = FALSE) +} + + +#' Write BASINSDATA Table +#' @noRd +.write_basinsdata_table <- function(con, basin_data) { + if (is.null(basin_data) || nrow(basin_data) == 0) return(invisible()) + + df <- data.frame( + basin = basin_data$subbasin, + drainArea = basin_data$area_ha, + outletCol = 0L, + outletRow = 0L, + outletElevation = basin_data$min_elevation, + startCol = 0L, + startRow = 0L, + startToOutletDistance = sqrt(basin_data$area_ha * 10000), + startToOutletDrop = basin_data$max_elevation - basin_data$min_elevation, + farCol = 0L, + farRow = 0L, + farthest = 0L, + farElevation = basin_data$max_elevation, + farDistance = sqrt(basin_data$area_ha * 10000), + maxElevation = basin_data$max_elevation, + stringsAsFactors = FALSE + ) + + DBI::dbWriteTable(con, "BASINSDATA", df, append = TRUE, row.names = FALSE) +} + + +#' Write HRUSDATA Table +#' @noRd +.write_hrusdata_table <- function(con, hru_data) { + if (is.null(hru_data) || nrow(hru_data) == 0) return(invisible()) + + df <- data.frame( + hru = hru_data$hru_id, + lsu = hru_data$subbasin, + basin = hru_data$subbasin, + crop = as.integer(factor(hru_data$landuse)), + soil = as.integer(factor(hru_data$soil)), + slope = hru_data$slope_class, + cellCount = hru_data$cell_count, + area = hru_data$area_ha, + totalElevation = hru_data$mean_elevation * hru_data$cell_count, + totalSlope = hru_data$mean_slope * hru_data$cell_count, + totalLatitude = 0, + totalLongitude = 0, + stringsAsFactors = FALSE + ) + + DBI::dbWriteTable(con, "HRUSDATA", df, append = TRUE, row.names = FALSE) +} + + +#' Write LSUSDATA Table +#' @noRd +.write_lsusdata_table <- function(con, hru_data, basin_data) { + if (is.null(basin_data) || nrow(basin_data) == 0) return(invisible()) + + n_hrus_per_sub <- tapply(hru_data$hru_id, hru_data$subbasin, length) + + df <- data.frame( + lsu = basin_data$subbasin, + basin = basin_data$subbasin, + cellCount = 0L, + area = basin_data$area_ha, + totalElevation = basin_data$mean_elevation * + round(basin_data$area_ha * 10000 / 900), + totalSlope = basin_data$mean_slope * + round(basin_data$area_ha * 10000 / 900), + totalLatitude = 0, + totalLongitude = 0, + cropSoilSlopeArea = basin_data$area_ha, + hru = as.integer(n_hrus_per_sub[as.character(basin_data$subbasin)]), + stringsAsFactors = FALSE + ) + + DBI::dbWriteTable(con, "LSUSDATA", df, append = TRUE, row.names = FALSE) +} + + +#' Safe row count for a table +#' +#' Returns the number of rows in \code{tbl_name}, or \code{-1L} if the table +#' does not exist or an error occurs. +#' @noRd +.safe_table_count <- function(con, tbl_name) { + tryCatch( + DBI::dbGetQuery(con, + paste0("SELECT COUNT(*) AS n FROM main.", tbl_name))$n, + error = function(e) -1L + ) +} + + +#' Populate reference/parameter tables from the SWAT+ datasets databases +#' +#' Uses SQLite ATTACH to copy reference data (plants, fertilizers, operations, +#' structural BMPs, land use, calibration parameters, soils, weather generator, +#' etc.) from the bundled reference databases into the project database. +#' +#' Data are sourced from two databases (in priority order): +#' \enumerate{ +#' \item \file{QSWATPlusRefHAWQS.sqlite} -- rich parameter/reference data +#' (plants_plt, fertilizer_frt, management schedules, soil, wgn, …) +#' \item \file{QSWATPlusProj.sqlite} -- standard project template (fallback) +#' } +#' +#' Additionally, HAWQS-specific tables (\code{plant_HAWQS}, \code{urban_HAWQS}, +#' CDL landuse field tables, \code{statsgo_ssurgo_lkey*}) are copied from +#' \file{QSWATPlusProjHAWQS.sqlite}. +#' +#' Only empty or missing tables are populated; tables with existing data are +#' left untouched. +#' +#' @param con DBI connection to the project database. +#' @return Invisible \code{NULL}. +#' @keywords internal +populate_from_datasets <- function(con) { + + # ------------------------------------------------------------------ + # Locate bundled databases + # ------------------------------------------------------------------ + ref_hawqs_db <- "" + proj_hawqs_db <- "" + proj_db <- "" + if (requireNamespace("rQSWATPlus", quietly = TRUE)) { + ref_hawqs_db <- system.file("extdata", "QSWATPlusRefHAWQS.sqlite", + package = "rQSWATPlus") + proj_hawqs_db <- system.file("extdata", "QSWATPlusProjHAWQS.sqlite", + package = "rQSWATPlus") + proj_db <- system.file("extdata", "QSWATPlusProj.sqlite", + package = "rQSWATPlus") + } + + has_ref_hawqs <- nzchar(ref_hawqs_db) && file.exists(ref_hawqs_db) + has_proj_hawqs <- nzchar(proj_hawqs_db) && file.exists(proj_hawqs_db) + has_proj <- nzchar(proj_db) && file.exists(proj_db) + + if (!has_ref_hawqs && !has_proj) return(invisible(NULL)) + + # ------------------------------------------------------------------ + # 1. HAWQS Reference database (primary source for parameter data) + # ------------------------------------------------------------------ + if (has_ref_hawqs) { + con2 <- DBI::dbConnect(RSQLite::SQLite(), ref_hawqs_db) + ref_tables_avail <- DBI::dbListTables(con2) + DBI::dbDisconnect(con2) + + DBI::dbExecute(con, paste0( + "ATTACH DATABASE '", ref_hawqs_db, "' AS ref_hawqs")) + on.exit(tryCatch(DBI::dbExecute(con, "DETACH DATABASE ref_hawqs"), + error = function(e) NULL), add = TRUE) + + # Reference tables to copy (ordered for FK dependencies) + ref_tables <- c( + # Parameter database + "plants_plt", "fertilizer_frt", "tillage_til", "pesticide_pst", + "pathogens_pth", "urban_urb", "septic_sep", "snow_sno", + # LUM lookup tables + "cntable_lum", "ovn_table_lum", "cons_prac_lum", + # Operations (graze_ops depends on fertilizer_frt) + "harv_ops", "fire_ops", "irr_ops", "sweep_ops", "chem_app_ops", + "graze_ops", + # Structural BMPs + "bmpuser_str", "filterstrip_str", "grassedww_str", + "septic_str", "tiledrain_str", + # Calibration + "cal_parms_cal", + # Soils LTE + "soils_lte_sol", + # Land use (depends on cntable, cons_prac, ovn_table, etc.) + "landuse_lum", + # Management schedules (HAWQS-specific rich data) + "management_sch", "management_sch_auto", "management_sch_op", + # Basin parameters + "codes_bsn", "parameters_bsn", + # Initial conditions + "plant_ini", "plant_ini_item", + # Print settings + "print_prt", "print_prt_object", + # File CIO + "file_cio", "file_cio_classification", + # Soil and weather generator data from HAWQS ref + "soil", "soil_layer", "wgn", "wgn_mon", + # HAWQS-specific reference tables + "urban", "NLCD_CDL_color_scheme", "tropical_bounds", "version" + ) + + for (tbl in ref_tables) { + if (!(tbl %in% ref_tables_avail)) next + n <- .safe_table_count(con, tbl) + if (n <= 0L) { + tryCatch( + DBI::dbExecute(con, paste0("DROP TABLE IF EXISTS main.", tbl)), + error = function(e) NULL + ) + DBI::dbExecute(con, paste0( + "CREATE TABLE main.", tbl, + " AS SELECT * FROM ref_hawqs.", tbl + )) + } + } + + # Decision tables from HAWQS ref + dtl_tables_available <- all(c("d_table_dtl", "d_table_dtl_cond", + "d_table_dtl_cond_alt", "d_table_dtl_act", "d_table_dtl_act_out") %in% + ref_tables_avail) + if (dtl_tables_available && .safe_table_count(con, "d_table_dtl") <= 0L) { + .copy_decision_tables(con, "ref_hawqs") + } + + DBI::dbExecute(con, "DETACH DATABASE ref_hawqs") + } + + # ------------------------------------------------------------------ + # 2. Standard project template (fallback for anything not in HAWQS ref) + # ------------------------------------------------------------------ + if (has_proj) { + con2 <- DBI::dbConnect(RSQLite::SQLite(), proj_db) + ds_tables <- DBI::dbListTables(con2) + DBI::dbDisconnect(con2) + + DBI::dbExecute(con, paste0( + "ATTACH DATABASE '", proj_db, "' AS datasets")) + on.exit(tryCatch(DBI::dbExecute(con, "DETACH DATABASE datasets"), + error = function(e) NULL), add = TRUE) + + # Fallback reference tables (same list as before) + fallback_tables <- c( + "plants_plt", "fertilizer_frt", "tillage_til", "pesticide_pst", + "pathogens_pth", "urban_urb", "septic_sep", "snow_sno", + "cntable_lum", "ovn_table_lum", "cons_prac_lum", + "harv_ops", "fire_ops", "irr_ops", "sweep_ops", "chem_app_ops", + "graze_ops", + "bmpuser_str", "filterstrip_str", "grassedww_str", + "septic_str", "tiledrain_str", + "cal_parms_cal", "soils_lte_sol", "landuse_lum" + ) + + for (tbl in fallback_tables) { + if (!(tbl %in% ds_tables)) next + n <- .safe_table_count(con, tbl) + if (n <= 0L) { + tryCatch( + DBI::dbExecute(con, paste0("DROP TABLE IF EXISTS main.", tbl)), + error = function(e) NULL + ) + DBI::dbExecute(con, paste0( + "CREATE TABLE main.", tbl, + " AS SELECT * FROM datasets.", tbl + )) + } + } + + # Decision tables fallback + dtl_fb_available <- all(c("d_table_dtl", "d_table_dtl_cond", + "d_table_dtl_cond_alt", "d_table_dtl_act", "d_table_dtl_act_out") %in% + ds_tables) + if (dtl_fb_available && .safe_table_count(con, "d_table_dtl") <= 0L) { + .copy_decision_tables(con, "datasets") + } + + DBI::dbExecute(con, "DETACH DATABASE datasets") + } + + # ------------------------------------------------------------------ + # 3. HAWQS project template (HAWQS-specific tables) + # ------------------------------------------------------------------ + if (has_proj_hawqs) { + con2 <- DBI::dbConnect(RSQLite::SQLite(), proj_hawqs_db) + hawqs_proj_tables <- DBI::dbListTables(con2) + DBI::dbDisconnect(con2) + + DBI::dbExecute(con, paste0( + "ATTACH DATABASE '", proj_hawqs_db, "' AS proj_hawqs")) + on.exit(tryCatch(DBI::dbExecute(con, "DETACH DATABASE proj_hawqs"), + error = function(e) NULL), add = TRUE) + + # HAWQS-specific tables to copy + hawqs_tables <- c( + "plant_HAWQS", "urban_HAWQS", + "statsgo_ssurgo_lkey", "statsgo_ssurgo_lkey1" + ) + # CDL landuse field tables + cdl_tables <- paste0("landuse_fields_CDL_", + sprintf("%02d", seq_len(18))) + hawqs_tables <- c(hawqs_tables, cdl_tables) + + for (tbl in hawqs_tables) { + if (!(tbl %in% hawqs_proj_tables)) next + n <- .safe_table_count(con, tbl) + if (n <= 0L) { + tryCatch( + DBI::dbExecute(con, + paste0("DROP TABLE IF EXISTS main.[", tbl, "]")), + error = function(e) NULL + ) + DBI::dbExecute(con, paste0( + "CREATE TABLE main.[", tbl, + "] AS SELECT * FROM proj_hawqs.[", tbl, "]" + )) + } + } + + DBI::dbExecute(con, "DETACH DATABASE proj_hawqs") + } + + invisible(NULL) +} + + +#' Copy decision tables from an ATTACHed database +#' +#' @param con DBI connection (with database already ATTACHed). +#' @param alias Character alias of the ATTACHed database +#' (e.g. \code{"ref_hawqs"} or \code{"datasets"}). +#' @noRd +.copy_decision_tables <- function(con, alias) { + tryCatch( + DBI::dbExecute(con, "DROP TABLE IF EXISTS main.d_table_dtl"), + error = function(e) NULL + ) + DBI::dbExecute(con, paste0(" + CREATE TABLE main.d_table_dtl AS + SELECT * FROM ", alias, ".d_table_dtl + WHERE file_name IN ('lum.dtl', 'res_rel.dtl') + AND (file_name != 'res_rel.dtl' + OR name IN ('corps_med_res1','corps_med_res', + 'wetland','drawdown_days','flood_season'))")) + + for (sub_tbl in c("d_table_dtl_cond", "d_table_dtl_cond_alt", + "d_table_dtl_act", "d_table_dtl_act_out")) { + tryCatch( + DBI::dbExecute(con, paste0("DROP TABLE IF EXISTS main.", sub_tbl)), + error = function(e) NULL + ) + } + + DBI::dbExecute(con, paste0(" + CREATE TABLE main.d_table_dtl_cond AS + SELECT c.* FROM ", alias, ".d_table_dtl_cond c + INNER JOIN main.d_table_dtl d ON c.d_table_id = d.id")) + + DBI::dbExecute(con, paste0(" + CREATE TABLE main.d_table_dtl_cond_alt AS + SELECT ca.* FROM ", alias, ".d_table_dtl_cond_alt ca + INNER JOIN main.d_table_dtl_cond c ON ca.cond_id = c.id")) + + DBI::dbExecute(con, paste0(" + CREATE TABLE main.d_table_dtl_act AS + SELECT a.* FROM ", alias, ".d_table_dtl_act a + INNER JOIN main.d_table_dtl d ON a.d_table_id = d.id")) + + DBI::dbExecute(con, paste0(" + CREATE TABLE main.d_table_dtl_act_out AS + SELECT ao.* FROM ", alias, ".d_table_dtl_act_out ao + INNER JOIN main.d_table_dtl_act a ON ao.act_id = a.id")) +} + +#' Ensure all required SWAT+ tables exist before writing files +#' +#' Creates any missing tables that \code{\link{write_config_files}} needs and +#' populates mandatory tables with sensible defaults (mirroring the Python +#' SWAT+ Editor \code{setup.py} initialisation). Tables that already exist +#' are left untouched. +#' +#' @param con DBI connection to the project database. +#' @return Invisible \code{NULL}. +#' @keywords internal +ensure_write_tables <- function(con) { + + # ---- Populate reference tables from the bundled datasets database ---- + populate_from_datasets(con) + + # ---- helper: create a table only if it does not exist ---- + create_if_missing <- function(sql) { + DBI::dbExecute(con, sql) + } + + # ---- helper: insert a default row when a table is empty ---- + insert_if_empty <- function(tbl, sql) { + n <- tryCatch( + DBI::dbGetQuery(con, + paste0("SELECT COUNT(*) AS n FROM ", tbl))$n, + error = function(e) 0L) + if (n == 0L) DBI::dbExecute(con, sql) + } + + # ================================================================== + # 1. Simulation tables + # ================================================================== + create_if_missing(" + CREATE TABLE IF NOT EXISTS print_prt ( + id INTEGER PRIMARY KEY, + nyskip INTEGER, day_start INTEGER, yrc_start INTEGER, + day_end INTEGER, yrc_end INTEGER, interval INTEGER, + csvout INTEGER, dbout INTEGER, cdfout INTEGER, + crop_yld TEXT DEFAULT 'b', mgtout INTEGER, hydcon INTEGER, + fdcout INTEGER + )") + insert_if_empty("print_prt", + "INSERT INTO print_prt + (nyskip, day_start, yrc_start, day_end, yrc_end, interval, + csvout, dbout, cdfout, crop_yld, mgtout, hydcon, fdcout) + VALUES (1, 0, 0, 0, 0, 1, 0, 0, 0, 'b', 0, 0, 0)") + + create_if_missing(" + CREATE TABLE IF NOT EXISTS print_prt_object ( + id INTEGER PRIMARY KEY, + print_prt_id INTEGER, + name TEXT, daily INTEGER, monthly INTEGER, + yearly INTEGER, avann INTEGER, + FOREIGN KEY (print_prt_id) REFERENCES print_prt(id) + )") + + # Default print objects (52 rows) -------------------------------- + prt_id <- tryCatch( + DBI::dbGetQuery(con, "SELECT id FROM print_prt ORDER BY id LIMIT 1")$id, + error = function(e) 1L) + + if (.safe_table_count(con, "print_prt_object") == 0L) { + # Names where yearly=1, avann=1 + active <- c( + "basin_wb", "basin_nb", "basin_ls", "basin_pw", "basin_aqu", + "basin_res", "basin_cha", "basin_sd_cha", "basin_psc", + "region_wb", "region_nb", "region_ls", "region_pw", "region_aqu", + "region_res", "region_sd_cha", "region_psc", "water_allo", + "lsunit_wb", "lsunit_nb", "lsunit_ls", "lsunit_pw", + "hru_wb", "hru_nb", "hru_ls", "hru_pw", + "hru-lte_wb", "hru-lte_nb", "hru-lte_ls", "hru-lte_pw", + "channel", "channel_sd", "aquifer", "reservoir", "recall", + "hyd", "ru", "pest") + # Names where all flags = 0 + inactive <- c( + "basin_salt", "hru_salt", "ru_salt", "aqu_salt", + "channel_salt", "res_salt", "wetland_salt", + "basin_cs", "hru_cs", "ru_cs", "aqu_cs", + "channel_cs", "res_cs", "wetland_cs") + + rows <- data.frame( + print_prt_id = prt_id, + name = c(active, inactive), + daily = 0L, + monthly = 0L, + yearly = c(rep(1L, length(active)), rep(0L, length(inactive))), + avann = c(rep(1L, length(active)), rep(0L, length(inactive))), + stringsAsFactors = FALSE) + DBI::dbWriteTable(con, "print_prt_object", rows, append = TRUE) + } + + # ================================================================== + # 2. Basin tables + # ================================================================== + create_if_missing(" + CREATE TABLE IF NOT EXISTS codes_bsn ( + id INTEGER PRIMARY KEY, + pet_file TEXT, wq_file TEXT, + pet INTEGER, event INTEGER, crack INTEGER, swift_out INTEGER, + sed_det INTEGER, rte_cha INTEGER, deg_cha INTEGER, wq_cha INTEGER, + nostress INTEGER, cn INTEGER, c_fact INTEGER, carbon INTEGER, + lapse INTEGER, uhyd INTEGER, sed_cha INTEGER, tiledrain INTEGER, + wtable INTEGER, soil_p INTEGER, gampt INTEGER, + atmo_dep TEXT, stor_max INTEGER, i_fpwet INTEGER, + gwflow INTEGER DEFAULT 0 + )") + insert_if_empty("codes_bsn", + "INSERT INTO codes_bsn + (pet_file, wq_file, + pet, event, crack, swift_out, sed_det, rte_cha, deg_cha, wq_cha, + nostress, cn, c_fact, carbon, lapse, uhyd, sed_cha, tiledrain, + wtable, soil_p, gampt, atmo_dep, stor_max, i_fpwet, gwflow) + VALUES (NULL, NULL, + 1, 0, 0, 1, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 'a', 0, 1, 0)") + + create_if_missing(" + CREATE TABLE IF NOT EXISTS parameters_bsn ( + id INTEGER PRIMARY KEY, + lai_noevap REAL, sw_init REAL, surq_lag REAL, + adj_pkrt REAL, adj_pkrt_sed REAL, lin_sed REAL, exp_sed REAL, + orgn_min REAL, n_uptake REAL, p_uptake REAL, + n_perc REAL, p_perc REAL, p_soil REAL, p_avail REAL, + rsd_decomp REAL, pest_perc REAL, + msk_co1 REAL, msk_co2 REAL, msk_x REAL, + nperco_lchtile REAL, evap_adj REAL, scoef REAL, + denit_exp REAL, denit_frac REAL, man_bact REAL, + adj_uhyd REAL, cn_froz REAL, dorm_hr REAL, + plaps REAL, tlaps REAL, n_fix_max REAL, + rsd_decay REAL, rsd_cover REAL, urb_init_abst REAL, + petco_pmpt REAL, uhyd_alpha REAL, + splash REAL, rill REAL, surq_exp REAL, cov_mgt REAL, + cha_d50 REAL, co2 REAL, day_lag_max REAL, + igen INTEGER + )") + insert_if_empty("parameters_bsn", + "INSERT INTO parameters_bsn + (lai_noevap, sw_init, surq_lag, adj_pkrt, adj_pkrt_sed, + lin_sed, exp_sed, orgn_min, n_uptake, p_uptake, + n_perc, p_perc, p_soil, p_avail, rsd_decomp, pest_perc, + msk_co1, msk_co2, msk_x, nperco_lchtile, evap_adj, scoef, + denit_exp, denit_frac, man_bact, adj_uhyd, cn_froz, dorm_hr, + plaps, tlaps, n_fix_max, rsd_decay, rsd_cover, urb_init_abst, + petco_pmpt, uhyd_alpha, splash, rill, surq_exp, cov_mgt, + cha_d50, co2, day_lag_max, igen) + VALUES (3.0, 0.0, 4.0, 1.0, 484.0, + 0.0, 0.0, 0.0, 20.0, 20.0, + 0.1, 10.0, 175.0, 0.4, 0.05, 0.5, + 0.75, 0.25, 0.2, 0.5, 0.6, 1.0, + 1.4, 1.3, 0.15, 0.0, 0.0, 0.0, + 0.0, 0.0, 20.0, 0.01, 0.3, 1.0, + 1.0, 5.0, 1.0, 0.7, 1.2, 0.03, + 50.0, 400.0, 0.0, 0)") + + # ================================================================== + # 3. Climate tables (weather_sta_cli already in create_project_db; + # just make sure it exists) + # ================================================================== + create_if_missing(" + CREATE TABLE IF NOT EXISTS weather_sta_cli ( + id INTEGER PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + lat REAL, lon REAL, elev REAL, + pcp TEXT, tmp TEXT, slr TEXT, hmd TEXT, wnd TEXT, pet TEXT, + atmo_dep TEXT, wgn_id INTEGER + )") + + # ================================================================== + # 17. Structural tables + # ================================================================== + create_if_missing("CREATE TABLE IF NOT EXISTS tiledrain_str ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS septic_str ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS filterstrip_str ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS grassedww_str ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS bmpuser_str ( + id INTEGER PRIMARY KEY, name TEXT)") + + # ================================================================== + # 18. HRU parameter database tables + # ================================================================== + create_if_missing("CREATE TABLE IF NOT EXISTS plants_plt ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS fertilizer_frt ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS tillage_til ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS pesticide_pst ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS pathogens_pth ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS urban_urb ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS septic_sep ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS snow_sno ( + id INTEGER PRIMARY KEY, name TEXT)") + + # ================================================================== + # 19. Operations tables + # ================================================================== + create_if_missing("CREATE TABLE IF NOT EXISTS harv_ops ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS graze_ops ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS irr_ops ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS chem_app_ops ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS fire_ops ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS sweep_ops ( + id INTEGER PRIMARY KEY, name TEXT)") + + # ================================================================== + # 20. Land use management tables + # ================================================================== + create_if_missing("CREATE TABLE IF NOT EXISTS landuse_lum ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS management_sch ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS cntable_lum ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS cons_prac_lum ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS ovn_table_lum ( + id INTEGER PRIMARY KEY, name TEXT)") + + # ================================================================== + # 21. Calibration / change tables + # ================================================================== + create_if_missing("CREATE TABLE IF NOT EXISTS cal_parms_cal ( + id INTEGER PRIMARY KEY, name TEXT)") + + # ================================================================== + # 22. Initial condition tables + # ================================================================== + create_if_missing("CREATE TABLE IF NOT EXISTS plant_ini ( + id INTEGER PRIMARY KEY, name TEXT)") + + # ================================================================== + # 23. Soils tables + # ================================================================== + create_if_missing("CREATE TABLE IF NOT EXISTS soils_sol ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + hyd_grp TEXT NOT NULL, + dp_tot REAL NOT NULL, + anion_excl REAL NOT NULL, + perc_crk REAL NOT NULL, + texture TEXT, + description TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS soils_lte_sol ( + id INTEGER PRIMARY KEY, name TEXT)") + + # ================================================================== + # 24. Decision table tables + # ================================================================== + create_if_missing(" + CREATE TABLE IF NOT EXISTS d_table_dtl ( + id INTEGER PRIMARY KEY, name TEXT, file_name TEXT, + description TEXT + )") + create_if_missing(" + CREATE TABLE IF NOT EXISTS d_table_dtl_cond ( + id INTEGER PRIMARY KEY, d_table_dtl_id INTEGER, + var TEXT, obj TEXT, obj_num INTEGER, + lim_var TEXT, lim_op TEXT, lim_const REAL, + alt TEXT + )") + create_if_missing(" + CREATE TABLE IF NOT EXISTS d_table_dtl_act ( + id INTEGER PRIMARY KEY, d_table_dtl_id INTEGER, + act_typ TEXT, obj TEXT, obj_num INTEGER, + name TEXT, option TEXT, const1 REAL, + const2 REAL, fp TEXT + )") + + # ================================================================== + # 26. file_cio_classification + file_cio + # ================================================================== + create_if_missing(" + CREATE TABLE IF NOT EXISTS file_cio_classification ( + id INTEGER PRIMARY KEY, name TEXT + )") + create_if_missing(" + CREATE TABLE IF NOT EXISTS file_cio ( + id INTEGER PRIMARY KEY, + classification_id INTEGER, + order_in_class INTEGER, + file_name TEXT, + FOREIGN KEY (classification_id) + REFERENCES file_cio_classification(id) + )") + + if (.safe_table_count(con, "file_cio_classification") == 0L) { + cls_names <- c( + "simulation", "basin", "climate", "connect", "channel", + "reservoir", "routing_unit", "hru", "exco", "recall", + "dr", "aquifer", "herd", "water_rights", "link", + "hydrology", "structural", "hru_parm_db", "ops", "lum", + "chg", "init", "soils", "decision_table", "regions", + "pcp_path", "tmp_path", "slr_path", "hmd_path", "wnd_path", + "out_path") + + cls_df <- data.frame(name = cls_names, stringsAsFactors = FALSE) + DBI::dbWriteTable(con, "file_cio_classification", cls_df, append = TRUE) + + # Build file_cio records (classification_id, order_in_class, file_name) + # ------ classification_id values map to cls_names insertion order ------ + cls_lookup <- DBI::dbGetQuery(con, + "SELECT id, name FROM file_cio_classification ORDER BY id") + cid <- function(name) cls_lookup$id[cls_lookup$name == name] + + file_rows <- data.frame( + classification_id = integer(0), + order_in_class = integer(0), + file_name = character(0), + stringsAsFactors = FALSE) + + add_files <- function(section, files) { + for (i in seq_along(files)) { + file_rows[nrow(file_rows) + 1L, ] <<- list(cid(section), i, files[i]) + } + } + + add_files("simulation", c("time.sim", "print.prt", "object.prt", + "object.cnt", "constituents.cs")) + add_files("basin", c("codes.bsn", "parameters.bsn")) + add_files("climate", c("weather-sta.cli", "weather-wgn.cli", + "pet.cli", "pcp.cli", "tmp.cli", + "slr.cli", "hmd.cli", "wnd.cli", "atmodep.cli")) + add_files("connect", c("hru.con", "hru-lte.con", "rout_unit.con", + "gwflow.con", "aquifer.con", "aquifer2d.con", + "channel.con", "reservoir.con", "recall.con", + "exco.con", "delratio.con", "outlet.con", + "chandeg.con")) + add_files("channel", c("initial.cha", "channel.cha", "hydrology.cha", + "sediment.cha", "nutrients.cha", + "channel-lte.cha", "hyd-sed-lte.cha", + "temperature.cha")) + add_files("reservoir", c("initial.res", "reservoir.res", "hydrology.res", + "sediment.res", "nutrients.res", "weir.res", + "wetland.wet", "hydrology.wet")) + add_files("routing_unit", c("rout_unit.def", "rout_unit.ele", + "rout_unit.rtu", "rout_unit.dr")) + add_files("hru", c("hru-data.hru", "hru-lte.hru")) + add_files("exco", c("exco.exc", "exco_om.exc", "exco_pest.exc", + "exco_path.exc", "exco_hmet.exc", "exco_salt.exc")) + add_files("recall", "recall.rec") + add_files("dr", c("delratio.del", "dr_om.del", "dr_pest.del", + "dr_path.del", "dr_hmet.del", "dr_salt.del")) + add_files("aquifer", c("initial.aqu", "aquifer.aqu")) + add_files("herd", c("animal.hrd", "herd.hrd", "ranch.hrd")) + add_files("water_rights", c("water_allocation.wro", "element.wro", + "define.wro")) + add_files("link", c("chan-surf.lin", "chan-aqu.lin")) + add_files("hydrology", c("hydrology.hyd", "topography.hyd", "field.fld")) + add_files("structural", c("tiledrain.str", "septic.str", + "filterstrip.str", "grassedww.str", + "bmpuser.str")) + add_files("hru_parm_db", c("plants.plt", "fertilizer.frt", "tillage.til", + "pesticide.pst", "pathogens.pth", + "metals.mtl", "salts.slt", "urban.urb", + "septic.sep", "snow.sno")) + add_files("ops", c("harv.ops", "graze.ops", "irr.ops", + "chem_app.ops", "fire.ops", "sweep.ops")) + add_files("lum", c("landuse.lum", "management.sch", "cntable.lum", + "cons_prac.lum", "ovn_table.lum")) + add_files("chg", c("cal_parms.cal", "calibration.cal", "codes.sft", + "wb_parms.sft", "water_balance.sft", + "ch_sed_budget.sft", "ch_sed_parms.sft", + "plant_parms.sft", "plant_gro.sft")) + add_files("init", c("plant.ini", "soil_plant.ini", "om_water.ini", + "pest_hru.ini", "pest_water.ini", + "path_hru.ini", "path_water.ini", + "hmet_hru.ini", "hmet_water.ini", + "salt_hru.ini", "salt_water.ini")) + add_files("soils", c("soils.sol", "nutrients.sol", "soils_lte.sol")) + add_files("decision_table", c("lum.dtl", "res_rel.dtl", + "scen_lu.dtl", "flo_con.dtl")) + add_files("regions", c( + "ls_unit.ele", "ls_unit.def", "ls_reg.ele", "ls_reg.def", + "ls_cal.reg", + "ch_catunit.ele", "ch_catunit.def", "ch_reg.def", + "aqu_catunit.ele", "aqu_catunit.def", "aqu_reg.def", + "res_catunit.ele", "res_catunit.def", "res_reg.def", + "rec_catunit.ele", "rec_catunit.def", "rec_reg.def")) + + DBI::dbWriteTable(con, "file_cio", file_rows, append = TRUE) + } + + # ================================================================== + # 28. Missing _item / sub-tables required by Python QSWAT+ + # ================================================================== + + # -- plant_ini_item table -- + create_if_missing(" + CREATE TABLE IF NOT EXISTS plant_ini_item ( + id INTEGER PRIMARY KEY, name TEXT + )") + + # -- Decision table sub-tables -- + create_if_missing(" + CREATE TABLE IF NOT EXISTS d_table_dtl_cond_alt ( + id INTEGER PRIMARY KEY, cond_id INTEGER, name TEXT + )") + create_if_missing(" + CREATE TABLE IF NOT EXISTS d_table_dtl_act_out ( + id INTEGER PRIMARY KEY, act_id INTEGER, name TEXT + )") + + # -- Management sub-tables -- + create_if_missing("CREATE TABLE IF NOT EXISTS management_sch_auto ( + id INTEGER PRIMARY KEY, name TEXT)") + create_if_missing("CREATE TABLE IF NOT EXISTS management_sch_op ( + id INTEGER PRIMARY KEY, name TEXT)") + + # -- Soils layer table -- + create_if_missing("CREATE TABLE IF NOT EXISTS soils_sol_layer ( + id INTEGER NOT NULL PRIMARY KEY, + soil_id INTEGER NOT NULL, + layer_num INTEGER NOT NULL, + dp REAL NOT NULL, + bd REAL NOT NULL, + awc REAL NOT NULL, + soil_k REAL NOT NULL, + carbon REAL NOT NULL, + clay REAL NOT NULL, + silt REAL NOT NULL, + sand REAL NOT NULL, + rock REAL NOT NULL, + alb REAL NOT NULL, + usle_k REAL NOT NULL, + ec REAL NOT NULL, + caco3 REAL, + ph REAL, + FOREIGN KEY (soil_id) REFERENCES soils_sol (id) ON DELETE CASCADE)") + + invisible(NULL) +} + + +#' Read GIS Tables from a SWAT+ Project Database +#' +#' Reads all \code{gis_*} tables from an existing SWAT+ project SQLite +#' database and returns them as a named list of data frames. +#' +#' @param db_file Character. Path to a SWAT+ project SQLite database (the +#' file written by [qswat_write_database()]). +#' +#' @return A named list. Each element is a data frame corresponding to one +#' \code{gis_*} table found in the database. If no GIS tables are present +#' an empty list is returned. +#' +#' @details +#' The following tables are returned when they contain data (empty tables are +#' still included as zero-row data frames): +#' \itemize{ +#' \item \code{gis_subbasins} +#' \item \code{gis_channels} +#' \item \code{gis_lsus} +#' \item \code{gis_hrus} +#' \item \code{gis_aquifers} +#' \item \code{gis_deep_aquifers} +#' \item \code{gis_water} +#' \item \code{gis_points} +#' \item \code{gis_routing} +#' \item \code{gis_elevationbands} +#' \item \code{gis_landexempt} +#' \item \code{gis_splithrus} +#' } +#' +#' @examples +#' \dontrun{ +#' gis <- qswat_read_gis("my_project.sqlite") +#' head(gis$gis_subbasins) +#' head(gis$gis_channels) +#' } +#' +#' @export +qswat_read_gis <- function(db_file) { + if (!is.character(db_file) || length(db_file) != 1L) { + stop("'db_file' must be a single character string.", call. = FALSE) + } + if (!file.exists(db_file)) { + stop("Database file not found: ", db_file, call. = FALSE) + } + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + all_tables <- DBI::dbListTables(con) + gis_tables <- sort(all_tables[grepl("^gis_", all_tables)]) + + if (length(gis_tables) == 0L) { + message("No gis_* tables found in: ", db_file) + return(list()) + } + + result <- lapply(gis_tables, function(tbl) { + DBI::dbReadTable(con, tbl) + }) + names(result) <- gis_tables + + message("Read ", length(gis_tables), " GIS table(s): ", + paste(gis_tables, collapse = ", ")) + result +} diff --git a/rQSWATPlus/R/delineation.R b/rQSWATPlus/R/delineation.R new file mode 100644 index 0000000..c61658e --- /dev/null +++ b/rQSWATPlus/R/delineation.R @@ -0,0 +1,327 @@ +#' Run Watershed Delineation Using TauDEM +#' +#' Performs watershed delineation on a DEM using TauDEM tools via the +#' `traudem` package. This replicates the delineation workflow from +#' the QSWATPlus QGIS plugin. +#' +#' @param project A `qswat_project` object created by [qswat_setup()]. +#' @param threshold Numeric. Flow accumulation threshold for stream +#' definition (number of cells). If NULL, a default threshold of +#' 1% of total cells is used. +#' @param channel_threshold Numeric or NULL. Threshold for channel +#' definition. Defaults to `threshold / 10` if NULL. +#' @param n_processes Integer. Number of MPI processes for TauDEM. +#' Default is 1 (no parallelization). +#' @param quiet Logical. If TRUE, suppress TauDEM output messages. +#' Default is FALSE. +#' +#' @return An updated `qswat_project` object with delineation results. +#' +#' @details +#' The delineation workflow follows these steps: +#' \enumerate{ +#' \item **Pit removal**: Fill pits/depressions in the DEM +#' \item **D8 flow direction**: Calculate flow directions using D8 +#' algorithm +#' \item **D-infinity flow direction**: Calculate slope and flow +#' angles +#' \item **D8 contributing area**: Calculate flow accumulation +#' \item **Stream definition**: Apply threshold to identify streams +#' \item **Move outlets**: Snap outlet points to nearest stream +#' (if outlets provided) +#' \item **Stream network**: Generate stream network topology and +#' watershed boundaries +#' } +#' +#' Requires TauDEM to be installed and accessible. Use +#' `traudem::taudem_sitrep()` to verify your TauDEM installation. +#' +#' @examples +#' \dontrun{ +#' # After qswat_setup() +#' project <- qswat_delineate(project, threshold = 100) +#' } +#' +#' @export +qswat_delineate <- function(project, + threshold = NULL, + channel_threshold = NULL, + n_processes = 1L, + quiet = FALSE) { + + if (!inherits(project, "qswat_project")) { + stop("'project' must be a qswat_project object.", call. = FALSE) + } + + if (!requireNamespace("traudem", quietly = TRUE)) { + stop( + "Package 'traudem' is required for watershed delineation.\n", + "Install it with: install.packages('traudem')\n", + "Then install TauDEM: see vignette('taudem-installation', package = 'traudem')", + call. = FALSE + ) + } + + dem_file <- project$dem_file + raster_dir <- file.path(project$project_dir, "Watershed", "Rasters") + + # Default threshold: 1% of total cells + + if (is.null(threshold)) { + threshold <- max(1, floor(project$nrow * project$ncol * 0.01)) + message("Using default stream threshold: ", threshold, " cells") + } + if (is.null(channel_threshold)) { + channel_threshold <- max(1, floor(threshold / 10)) + message("Using default channel threshold: ", channel_threshold, " cells") + } + + project$stream_threshold <- threshold + project$channel_threshold <- channel_threshold + + # Set n_processes for traudem + if (n_processes > 1L) { + old_np <- Sys.getenv("TAUDEM_NPROC") + Sys.setenv(TAUDEM_NPROC = n_processes) + on.exit(Sys.setenv(TAUDEM_NPROC = old_np), add = TRUE) + } + + # --- Step 1: Pit Removal --- + if (!quiet) message("Step 1/7: Removing pits from DEM...") + fel_file <- traudem::taudem_pitremove( + input_elevation = dem_file, + output_elevation = file.path(raster_dir, "fel.tif"), + quiet = quiet + ) + project$fel_file <- fel_file + + # --- Step 2: D8 Flow Direction --- + if (!quiet) message("Step 2/7: Computing D8 flow directions...") + d8_result <- traudem::taudem_d8flowdir( + input_elevation = fel_file, + output_d8flowdir = file.path(raster_dir, "p.tif"), + output_d8slope = file.path(raster_dir, "sd8.tif"), + quiet = quiet + ) + project$p_file <- d8_result[["output_d8flowdir_grid"]] + project$sd8_file <- d8_result[["output_d8slopes_grid"]] + + # --- Step 3: D-infinity Flow Direction --- + if (!quiet) message("Step 3/7: Computing D-infinity flow directions...") + dinf_result <- traudem::taudem_exec( + program = "DinfFlowDir", + args = c("-fel", fel_file, + "-slp", file.path(raster_dir, "slp.tif"), + "-ang", file.path(raster_dir, "ang.tif")), + quiet = quiet + ) + project$slp_file <- file.path(raster_dir, "slp.tif") + project$ang_file <- file.path(raster_dir, "ang.tif") + + # --- Step 4: D8 Contributing Area --- + if (!quiet) message("Step 4/7: Computing D8 contributing area...") + ad8_args <- list( + input_d8flowdir = project$p_file, + output_contributing_area = file.path(raster_dir, "ad8.tif"), + quiet = quiet + ) + if (!is.null(project$outlet_file)) { + ad8_args$outlet_file = project$outlet_file + } + ad8_file <- do.call(traudem::taudem_aread8, ad8_args) + project$ad8_file <- ad8_file + + # --- Step 5: Stream Definition (Threshold) --- + if (!quiet) message("Step 5/7: Defining streams with threshold = ", threshold, "...") + src_stream_file <- traudem::taudem_threshold( + input_area = project$ad8_file, + output_stream_raster = file.path(raster_dir, "src_stream.tif"), + threshold = threshold, + quiet = quiet + ) + project$src_stream_file <- src_stream_file + + # Channel threshold (finer network) + if (!quiet) message(" Defining channels with threshold = ", channel_threshold, "...") + src_channel_file <- traudem::taudem_threshold( + input_area = project$ad8_file, + output_stream_raster = file.path(raster_dir, "src_channel.tif"), + threshold = channel_threshold, + quiet = quiet + ) + project$src_channel_file <- src_channel_file + + # --- Step 6: Move Outlets to Streams (if outlets provided) --- + if (!is.null(project$outlet_file)) { + if (!quiet) message("Step 6/7: Snapping outlets to streams...") + moved_outlet <- traudem::taudem_moveoutletstostream( + input_d8flowdir = project$p_file, + input_stream_raster = project$src_stream_file, + outlet_file = project$outlet_file, + output_moved_outlet = file.path(project$project_dir, "Watershed", + "Shapes", "moved_outlet.shp"), + quiet = quiet + ) + project$outlet_file <- moved_outlet + } else { + if (!quiet) message("Step 6/7: No outlets provided, skipping outlet snapping.") + } + + # --- Step 7: Stream Network --- + if (!quiet) message("Step 7/7: Generating stream network...") + shape_dir <- file.path(project$project_dir, "Watershed", "Shapes") + + stream_args <- c( + "-fel", project$fel_file, + "-p", project$p_file, + "-ad8", project$ad8_file, + "-src", project$src_stream_file, + "-ord", file.path(raster_dir, "ord.tif"), + "-tree", file.path(project$project_dir, "Watershed", "Text", "tree.dat"), + "-coord", file.path(project$project_dir, "Watershed", "Text", "coord.dat"), + "-net", file.path(shape_dir, "stream.shp"), + "-w", file.path(raster_dir, "w.tif") + ) + if (!is.null(project$outlet_file)) { + stream_args <- c(stream_args, "-o", project$outlet_file) + } + + traudem::taudem_exec( + program = "StreamNet", + args = stream_args, + quiet = quiet + ) + + project$ord_file <- file.path(raster_dir, "ord.tif") + project$stream_file <- file.path(shape_dir, "stream.shp") + project$watershed_file <- file.path(raster_dir, "w.tif") + + # Check if files were created + if (!file.exists(project$stream_file)) { + cli::cli_abort("Stream shapefile not created: + {.file {project$stream_file}}") + } + if (!file.exists(project$watershed_file)) { + cli::cli_abort("Watershed raster not created: + {.file {project$watershed_file}}") + } + + + # Also create channel network with finer threshold + if (!quiet) message(" Generating channel network...") + channel_args <- c( + "-fel", project$fel_file, + "-p", project$p_file, + "-ad8", project$ad8_file, + "-src", project$src_channel_file, + "-ord", file.path(raster_dir, "ord_channel.tif"), + "-tree", file.path(project$project_dir, "Watershed", "Text", + "tree_channel.dat"), + "-coord", file.path(project$project_dir, "Watershed", "Text", + "coord_channel.dat"), + "-net", file.path(shape_dir, "channel.shp"), + "-w", file.path(raster_dir, "w_channel.tif") + ) + if (!is.null(project$outlet_file)) { + channel_args <- c(channel_args, "-o", project$outlet_file) + } + + traudem::taudem_exec( + program = "StreamNet", + args = channel_args, + quiet = quiet + ) + + project$channel_file <- file.path(shape_dir, "channel.shp") + project$channel_watershed_file <- file.path(raster_dir, "w_channel.tif") + + if (!quiet) message("Delineation complete.") + return(project) +} + + +#' Create Stream/Channel Network from Delineation +#' +#' Reads the stream and channel network shapefiles generated during +#' delineation and extracts topology information (connectivity, +#' stream order, lengths, slopes). +#' +#' @param project A `qswat_project` object that has been through +#' [qswat_delineate()]. +#' +#' @return An updated `qswat_project` object with stream topology +#' information. +#' +#' @details +#' Extracts the following topology data from TauDEM's StreamNet output: +#' \itemize{ +#' \item Stream links and their downstream connections +#' \item Subbasin (watershed) identifiers +#' \item Stream orders (Strahler) +#' \item Channel lengths and slopes +#' \item Drainage areas +#' } +#' +#' @examples +#' \dontrun{ +#' # After qswat_delineate() +#' project <- qswat_create_streams(project) +#' } +#' +#' @export +qswat_create_streams <- function(project) { + + if (!inherits(project, "qswat_project")) { + stop("'project' must be a qswat_project object.", call. = FALSE) + } + if (is.null(project$stream_file)) { + stop("Delineation must be run first. Use qswat_delineate().", call. = FALSE) + } + if (!file.exists(project$stream_file)) { + stop("Stream file not found: ", project$stream_file, call. = FALSE) + } + + # Read stream network shapefile + streams <- sf::st_read(project$stream_file, quiet = TRUE) + + # Extract topology from TauDEM StreamNet output + # Standard TauDEM field names + topology <- data.frame( + LINKNO = streams$LINKNO, + DSLINKNO = streams$DSLINKNO, + USLINKNO1 = if ("USLINKNO1" %in% names(streams)) streams$USLINKNO1 else NA, + USLINKNO2 = if ("USLINKNO2" %in% names(streams)) streams$USLINKNO2 else NA, + strmOrder = if ("strmOrder" %in% names(streams)) streams$strmOrder else NA, + Length = if ("Length" %in% names(streams)) streams$Length else NA, + WSAreaKm2 = if ("WSAreaKm2" %in% names(streams)) streams$WSAreaKm2 else + if ("DSContArea" %in% names(streams)) streams$DSContArea else NA, + WSNO = if ("WSNO" %in% names(streams)) streams$WSNO else NA, + stringsAsFactors = FALSE + ) + + project$stream_topology <- topology + project$streams_sf <- streams + + # Read channel network if available + if (!is.null(project$channel_file) && file.exists(project$channel_file)) { + channels <- sf::st_read(project$channel_file, quiet = TRUE) + channel_topology <- data.frame( + LINKNO = channels$LINKNO, + DSLINKNO = channels$DSLINKNO, + WSNO = if ("WSNO" %in% names(channels)) channels$WSNO else NA, + strmOrder = if ("strmOrder" %in% names(channels)) channels$strmOrder else NA, + Length = if ("Length" %in% names(channels)) channels$Length else NA, + stringsAsFactors = FALSE + ) + project$channel_topology <- channel_topology + project$channels_sf <- channels + } + + # Read watershed raster for subbasin mapping + if (!is.null(project$watershed_file) && file.exists(project$watershed_file)) { + project$watershed_rast <- terra::rast(project$watershed_file) + } + + message("Stream topology extracted: ", nrow(topology), " stream links") + return(project) +} diff --git a/rQSWATPlus/R/gwflow.R b/rQSWATPlus/R/gwflow.R new file mode 100644 index 0000000..f1252e1 --- /dev/null +++ b/rQSWATPlus/R/gwflow.R @@ -0,0 +1,896 @@ +#' Read gwflow Configuration File +#' +#' Reads a SWAT+ gwflow initialisation (`gwflow.ini`) file and returns its +#' settings as a named list. The ini file uses the `[DEFAULT]` section +#' format produced by Python's `configparser` module. +#' +#' @param ini_file Character. Path to the `gwflow.ini` file. If `NULL` +#' the bundled template shipped with the package is used. +#' +#' @return A named list of gwflow configuration parameters with their +#' default values filled in where a key is absent from the file. +#' +#' @examples +#' ini <- system.file("extdata", "gwflow.ini", package = "rQSWATPlus") +#' cfg <- qswat_read_gwflow_config(ini) +#' cfg$cell_size +#' +#' @export +qswat_read_gwflow_config <- function(ini_file = NULL) { + + if (is.null(ini_file)) { + ini_file <- system.file("extdata", "gwflow.ini", package = "rQSWATPlus") + } + + if (!file.exists(ini_file)) { + stop("gwflow.ini not found: ", ini_file, call. = FALSE) + } + + lines <- readLines(ini_file, warn = FALSE) + + # Remove comment lines (starting with #) and blank lines, strip inline comments + lines <- lines[!grepl("^\\s*#", lines)] + lines <- lines[nzchar(trimws(lines))] + lines <- sub("\\s*#.*$", "", lines) # strip trailing comments + + cfg <- list() + for (line in lines) { + line <- trimws(line) + if (grepl("^\\[", line)) next # section header + if (!grepl("=", line)) next + parts <- strsplit(line, "=", fixed = TRUE)[[1]] + if (length(parts) < 2) next + key <- trimws(parts[1]) + value <- trimws(paste(parts[-1], collapse = "=")) + cfg[[key]] <- value + } + + # Helper to coerce with a default + get_int <- function(k, d) { v <- cfg[[k]]; if (is.null(v)) d else as.integer(v) } + get_dbl <- function(k, d) { v <- cfg[[k]]; if (is.null(v)) d else as.numeric(v) } + + list( + cell_size = get_int("cell_size", 200L), + boundary_conditions = get_int("Boundary_conditions", 1L), + hruorlsu_recharge = get_int("HRUorLSU_recharge", 2L), + gw_soiltransfer = get_int("GW_soiltransfer", 1L), + saturation_excess = get_int("Saturation_excess", 1L), + ext_pumping = get_int("ext_pumping", 0L), + reservoir_exchange = get_int("reservoir_exchange", 1L), + wetland_exchange = get_int("wetland_exchange", 1L), + floodplain_exchange = get_int("floodplain_exchange", 1L), + canal_seepage = get_int("canal_seepage", 0L), + solute_transport = get_int("solute_transport", 1L), + transport_steps = get_int("transport_steps", 1L), + recharge_delay = get_dbl("recharge_delay", 0.0), + exdp = get_dbl("EXDP", 1.00), + wt_depth = get_dbl("WT_depth", 5.0), + river_depth = get_dbl("river_depth", 5.0), + tile_depth = get_dbl("tile_depth", 1.22), + tile_area = get_dbl("tile_area", 50.0), + tile_k = get_dbl("tile_k", 5.0), + tile_groups = get_int("tile_groups", 0L), + tile_groups_number = get_int("tile_groups_number", 1L), + resbed_thickness = get_dbl("resbed_thickness", 2.0), + resbed_k = get_dbl("resbed_k", 9.99e-6), + wet_thickness = get_dbl("wet_thickness", 0.25), + daily_output = get_int("daily_output", 1L), + annual_output = get_int("annual_output", 1L), + aa_output = get_int("aa_output", 1L), + row_det = get_int("row_det", 0L), + col_det = get_int("col_det", 0L), + timestep_balance = get_dbl("timestep_balance", 1.0), + init_sy = get_dbl("init_sy", 0.2), + init_n = get_dbl("init_n", 0.25), + streambed_k = get_dbl("streambed_k", 0.005), + streambed_thick = get_dbl("streambed_thick", 0.5), + init_no3 = get_dbl("init_NO3", 3.0), + init_p = get_dbl("init_P", 0.05), + denit_constant = get_dbl("denit_constant", -0.0001), + disp_coef = get_dbl("disp_coef", 5.0), + nit_sorp = get_dbl("nit_sorp", 1.0), + pho_sorp = get_dbl("pho_sorp", 1.0) + ) +} + + +#' Set Up SWAT+ Groundwater Flow (gwflow) Tables +#' +#' Creates the gwflow tables in the project SQLite database and populates +#' them from the supplied configuration and available spatial data. +#' This is the R equivalent of the gwflow setup performed by +#' `GWFlow.createTables()` and the GIS routines in the QGIS plugin's +#' `gwflow.py`. +#' +#' The function mirrors the SWAT+ gwflow database schema defined in the +#' Python plugin: `gwflow_base`, `gwflow_zone`, `gwflow_grid`, +#' `gwflow_out_days`, `gwflow_obs_locs`, `gwflow_solutes`, +#' `gwflow_init_conc`, `gwflow_hrucell`, `gwflow_fpcell`, +#' `gwflow_rivcell`, `gwflow_lsucell`, `gwflow_rescell`. +#' +#' When the project object contains spatial data (`lsu_sf`, `hru_sf`, +#' `channels_sf` or `streams_sf`) the function also runs the GIS +#' processing steps to populate the grid and cell connection tables. +#' This happens automatically when called from [qswat_run()] with +#' `use_gwflow = TRUE`. +#' +#' @param project A `qswat_project` object returned by +#' [qswat_write_database()]. +#' @param gwflow_config A named list of gwflow settings as returned by +#' [qswat_read_gwflow_config()]. If `NULL` the bundled `gwflow.ini` +#' defaults are used. +#' @param overwrite Logical. If `TRUE`, drop and recreate existing gwflow +#' tables. Default `FALSE`. +#' @param aquifer_thickness Numeric (constant thickness in metres, +#' default `20`) **or** a file path to a raster (`.tif`) or polygon +#' shapefile (`.shp`) containing aquifer thickness values. When a +#' shapefile is provided it must have a numeric attribute named +#' `"thickness"` or `"Avg_Thick"`. When a raster is provided the +#' mean cell value (in metres) is extracted for each grid cell. +#' @param conductivity_file Optional path to a polygon shapefile that +#' defines aquifer hydraulic-conductivity zones. The shapefile must +#' contain a numeric column `"aquifer_k"` (K in m/day) or +#' `"logK_Ferr_"` (log10 intrinsic permeability × 100 as stored in +#' GLHYMPS). If `NULL` a single default zone is created using +#' `default_aquifer_k`. +#' @param default_aquifer_k Numeric. Default hydraulic conductivity +#' (m/day) for the single zone created when `conductivity_file` is +#' `NULL`. Default `1.0`. +#' +#' @return The `project` object with `project$use_gwflow` set to `TRUE` +#' (invisibly). +#' +#' @details +#' The following tables are created and, when spatial data are +#' available, populated: +#' \describe{ +#' \item{gwflow_base}{Single-row table with global gwflow parameters +#' (always populated; `row_count`/`col_count` updated after grid +#' creation).} +#' \item{gwflow_zone}{Aquifer hydraulic parameter zones (one default +#' zone, or one row per zone from `conductivity_file`).} +#' \item{gwflow_grid}{Active grid cells with elevation, aquifer +#' thickness, initial head, and zone assignment.} +#' \item{gwflow_out_days}{Optional output day pairs (not populated +#' automatically; left empty for user entry).} +#' \item{gwflow_obs_locs}{Observation cell IDs (not populated +#' automatically; left empty for user entry).} +#' \item{gwflow_solutes}{Chemical solute definitions and initial +#' concentrations (always populated with 10 default solutes).} +#' \item{gwflow_init_conc}{Per-cell initial solute concentrations +#' (not populated automatically).} +#' \item{gwflow_hrucell}{HRU-to-cell area mapping (populated when +#' HRU recharge mode is selected and `hru_sf` is available).} +#' \item{gwflow_fpcell}{Floodplain-cell connections (empty in +#' rQSWATPlus because LSUs are 1:1 with subbasins, with no +#' floodplain landscape units).} +#' \item{gwflow_rivcell}{River-cell connections (channel-cell +#' intersection lengths).} +#' \item{gwflow_lsucell}{LSU-cell area mapping.} +#' \item{gwflow_rescell}{Reservoir-cell connections (populated via +#' the `gis_water`–`gwflow_lsucell` join when water bodies are +#' present).} +#' } +#' +#' @examples +#' \dontrun{ +#' cfg <- qswat_read_gwflow_config() +#' project <- qswat_setup_gwflow(project, gwflow_config = cfg) +#' } +#' +#' @export +qswat_setup_gwflow <- function(project, + gwflow_config = NULL, + overwrite = FALSE, + aquifer_thickness = 20.0, + conductivity_file = NULL, + default_aquifer_k = 1.0) { + + if (!inherits(project, "qswat_project")) { + stop("'project' must be a qswat_project object.", call. = FALSE) + } + if (is.null(project$db_file) || !file.exists(project$db_file)) { + stop("No database found. Run qswat_write_database() first.", call. = FALSE) + } + + if (is.null(gwflow_config)) { + gwflow_config <- qswat_read_gwflow_config() + } + + con <- DBI::dbConnect(RSQLite::SQLite(), project$db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + .create_gwflow_tables(con, overwrite = overwrite) + .write_gwflow_base(con, gwflow_config) + .write_gwflow_solutes(con, gwflow_config) + + # Populate spatial tables (grid, zones, cell connections) when spatial + # data from qswat_create_hrus() / qswat_create_streams() is present. + if (!is.null(project$lsu_sf) && inherits(project$lsu_sf, "sf") && + nrow(project$lsu_sf) > 0) { + .populate_gwflow_tables( + con = con, + project = project, + cfg = gwflow_config, + aquifer_thickness = aquifer_thickness, + conductivity_file = conductivity_file, + default_aquifer_k = default_aquifer_k + ) + } else { + message("Note: No spatial data in project object; gwflow_zone, ", + "gwflow_grid, gwflow_rivcell, gwflow_lsucell, and ", + "gwflow_hrucell remain empty.") + message(" Run qswat_setup_gwflow() after qswat_run() to populate them.") + } + + # Update project_config to flag gwflow as enabled + DBI::dbExecute(con, "UPDATE project_config SET use_gwflow = 1") + + message("gwflow tables initialised in: ", project$db_file) + project$use_gwflow <- TRUE + invisible(project) +} + + +# ---- internal helpers -------------------------------------------------------- + +#' Create gwflow Database Tables +#' @noRd +.create_gwflow_tables <- function(con, overwrite = FALSE) { + + # Drop existing tables if overwrite requested (order respects FK constraints) + if (overwrite) { + drop_order <- c("gwflow_hrucell", "gwflow_fpcell", "gwflow_rivcell", + "gwflow_lsucell", "gwflow_rescell", "gwflow_init_conc", + "gwflow_obs_locs", "gwflow_out_days", "gwflow_solutes", + "gwflow_grid", "gwflow_zone", "gwflow_base") + for (tbl in drop_order) { + DBI::dbExecute(con, paste("DROP TABLE IF EXISTS", tbl)) + } + } + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_base ( + cell_size INTEGER, + row_count INTEGER, + col_count INTEGER, + boundary_conditions INTEGER, + recharge INTEGER, + soil_transfer INTEGER, + saturation_excess INTEGER, + external_pumping INTEGER, + tile_drainage INTEGER, + reservoir_exchange INTEGER, + wetland_exchange INTEGER, + floodplain_exchange INTEGER, + canal_seepage INTEGER, + solute_transport INTEGER, + transport_steps INTEGER, + disp_coef REAL, + recharge_delay REAL, + et_extinction_depth REAL, + water_table_depth REAL, + river_depth REAL, + tile_depth REAL, + tile_area REAL, + tile_k REAL, + tile_groups INTEGER, + resbed_thickness REAL, + resbed_k REAL, + wet_thickness REAL, + daily_output INTEGER, + annual_output INTEGER, + aa_output INTEGER, + daily_output_row INTEGER, + daily_output_col INTEGER, + timestep_balance REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_zone ( + zone_id INTEGER PRIMARY KEY, + aquifer_k REAL, + specific_yield REAL, + streambed_k REAL, + streambed_thickness REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_grid ( + cell_id INTEGER PRIMARY KEY, + status INTEGER, + zone INTEGER REFERENCES gwflow_zone (zone_id), + elevation REAL, + aquifer_thickness REAL, + extinction_depth REAL, + initial_head REAL, + tile INTEGER + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_out_days ( + year INTEGER, + jday INTEGER + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_obs_locs ( + cell_id INTEGER + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_solutes ( + solute_name TEXT, + sorption REAL, + rate_const REAL, + canal_irr REAL, + init_data TEXT, + init_conc REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_init_conc ( + cell_id INTEGER REFERENCES gwflow_grid (cell_id), + init_no3 REAL DEFAULT 0, + init_p REAL DEFAULT 0, + init_so4 REAL DEFAULT 0, + init_ca REAL DEFAULT 0, + init_mg REAL DEFAULT 0, + init_na REAL DEFAULT 0, + init_k REAL DEFAULT 0, + init_cl REAL DEFAULT 0, + init_co3 REAL DEFAULT 0, + init_hco3 REAL DEFAULT 0 + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_hrucell ( + cell_id INTEGER REFERENCES gwflow_grid (cell_id), + hru INTEGER REFERENCES gis_hrus (id), + area_m2 REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_fpcell ( + cell_id INTEGER REFERENCES gwflow_grid (cell_id), + channel INTEGER REFERENCES gis_channels (id), + area_m2 REAL, + conductivity REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_rivcell ( + cell_id INTEGER REFERENCES gwflow_grid (cell_id), + channel INTEGER REFERENCES gis_channels (id), + length_m REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_lsucell ( + cell_id INTEGER REFERENCES gwflow_grid (cell_id), + lsu INTEGER REFERENCES gis_lsus (id), + area_m2 REAL + ) + ") + + DBI::dbExecute(con, " + CREATE TABLE IF NOT EXISTS gwflow_rescell ( + cell_id INTEGER REFERENCES gwflow_grid (cell_id), + res INTEGER REFERENCES gis_water (id), + res_stage REAL + ) + ") +} + + +#' Write gwflow_base Table Row +#' @noRd +.write_gwflow_base <- function(con, cfg) { + df <- data.frame( + cell_size = cfg$cell_size, + row_count = 0L, + col_count = 0L, + boundary_conditions = cfg$boundary_conditions, + recharge = cfg$hruorlsu_recharge, + soil_transfer = cfg$gw_soiltransfer, + saturation_excess = cfg$saturation_excess, + external_pumping = cfg$ext_pumping, + tile_drainage = 0L, + reservoir_exchange = cfg$reservoir_exchange, + wetland_exchange = cfg$wetland_exchange, + floodplain_exchange = cfg$floodplain_exchange, + canal_seepage = cfg$canal_seepage, + solute_transport = cfg$solute_transport, + transport_steps = cfg$transport_steps, + disp_coef = cfg$disp_coef, + recharge_delay = cfg$recharge_delay, + et_extinction_depth = cfg$exdp, + water_table_depth = cfg$wt_depth, + river_depth = cfg$river_depth, + tile_depth = cfg$tile_depth, + tile_area = cfg$tile_area, + tile_k = cfg$tile_k, + tile_groups = cfg$tile_groups, + resbed_thickness = cfg$resbed_thickness, + resbed_k = cfg$resbed_k, + wet_thickness = cfg$wet_thickness, + daily_output = cfg$daily_output, + annual_output = cfg$annual_output, + aa_output = cfg$aa_output, + daily_output_row = cfg$row_det, + daily_output_col = cfg$col_det, + timestep_balance = cfg$timestep_balance, + stringsAsFactors = FALSE + ) + DBI::dbWriteTable(con, "gwflow_base", df, append = TRUE, row.names = FALSE) +} + + +#' Write gwflow_solutes Default Rows +#' +#' Inserts the ten default solutes matching the Python gwflow.py defaults. +#' @noRd +.write_gwflow_solutes <- function(con, cfg) { + solutes <- data.frame( + solute_name = c("no3-n", "p", "so4", "ca", "mg", + "na", "k", "cl", "co3", "hco3"), + sorption = c(cfg$nit_sorp, cfg$pho_sorp, 1, 1, 1, 1, 1, 1, 1, 1), + rate_const = c(cfg$denit_constant, 0, 0, 0, 0, 0, 0, 0, 0, 0), + canal_irr = c(3, 0.05, 0, 0, 0, 0, 0, 0, 0, 0), + init_data = rep("single", 10), + init_conc = c(cfg$init_no3, cfg$init_p, 100, 50, 30, 40, 1, 25, 1, 80), + stringsAsFactors = FALSE + ) + DBI::dbWriteTable(con, "gwflow_solutes", solutes, + append = TRUE, row.names = FALSE) +} + + +#' Populate gwflow Spatial Tables from Project GIS Data +#' +#' Runs the GIS processing steps that mirror the Python gwflow.py routines: +#' fishnet grid creation, active-cell detection, elevation extraction, +#' conductivity zone assignment, and spatial intersections with channels, +#' LSUs, and (optionally) HRUs and water bodies. +#' +#' @param con DBI connection to the project SQLite database. +#' @param project `qswat_project` object with `lsu_sf`, `hru_sf`, +#' `channels_sf`/`streams_sf`, and `dem_file` populated. +#' @param cfg gwflow configuration list from [qswat_read_gwflow_config()]. +#' @param aquifer_thickness Numeric constant (metres) or file path. +#' @param conductivity_file Path to conductivity shapefile, or `NULL`. +#' @param default_aquifer_k Default K in m/day when no file is given. +#' @noRd +.populate_gwflow_tables <- function(con, project, cfg, + aquifer_thickness = 20.0, + conductivity_file = NULL, + default_aquifer_k = 1.0) { + + watershed_sf <- project$lsu_sf + crs_obj <- sf::st_crs(watershed_sf) + cell_size <- cfg$cell_size + + # ------------------------------------------------------------------ + # 1. Build fishnet grid over the watershed bounding box + # ------------------------------------------------------------------ + message(" [gwflow] Creating fishnet grid (cell_size = ", cell_size, " m)...") + bbox_obj <- sf::st_bbox(watershed_sf) + + grid_geom <- sf::st_make_grid( + watershed_sf, + cellsize = cell_size, + what = "polygons" + ) + grid_sf <- sf::st_sf( + cell_id = seq_along(grid_geom), + geometry = grid_geom, + crs = crs_obj + ) + + # Compute grid dimensions for gwflow_base + n_cols <- ceiling((bbox_obj["xmax"] - bbox_obj["xmin"]) / cell_size) + n_rows <- ceiling((bbox_obj["ymax"] - bbox_obj["ymin"]) / cell_size) + + # ------------------------------------------------------------------ + # 2. Identify active cells (those that intersect the watershed) + # ------------------------------------------------------------------ + message(" [gwflow] Identifying active cells...") + watershed_union <- sf::st_union(watershed_sf) + hits <- which(lengths(sf::st_intersects(grid_sf, watershed_union)) > 0) + if (length(hits) == 0) { + message(" [gwflow] Warning: no grid cells intersect the watershed.") + return(invisible(NULL)) + } + grid_active <- grid_sf[hits, , drop = FALSE] + + # ------------------------------------------------------------------ + # 3. Assign elevation from DEM + # ------------------------------------------------------------------ + message(" [gwflow] Extracting DEM elevation per cell...") + cell_elev <- rep(0.0, nrow(grid_active)) + if (!is.null(project$dem_file) && file.exists(project$dem_file)) { + dem_rast <- terra::rast(project$dem_file) + # Project grid to DEM CRS for extraction + grid_v_dem <- terra::vect( + sf::st_transform(grid_active, crs = terra::crs(dem_rast)) + ) + ex <- terra::extract(dem_rast, grid_v_dem, fun = "mean", na.rm = TRUE) + if (ncol(ex) >= 2) { + vals <- ex[, 2] + vals[is.na(vals)] <- 0.0 + cell_elev <- as.numeric(vals) + } + } + + # ------------------------------------------------------------------ + # 4. Assign aquifer thickness + # ------------------------------------------------------------------ + message(" [gwflow] Assigning aquifer thickness...") + # Resolve the default/fallback thickness once (avoids repeated numeric coercion) + default_thick <- if (is.numeric(aquifer_thickness)) { + aquifer_thickness[1] + } else { + 20.0 # numeric default when a file path is given + } + cell_thick <- rep(default_thick, nrow(grid_active)) + + if (is.character(aquifer_thickness) && file.exists(aquifer_thickness)) { + ext <- tolower(tools::file_ext(aquifer_thickness)) + if (ext %in% c("tif", "tiff", "img")) { + # Raster source + thick_rast <- terra::rast(aquifer_thickness) + grid_v_aq <- terra::vect( + sf::st_transform(grid_active, crs = terra::crs(thick_rast)) + ) + ex_t <- terra::extract(thick_rast, grid_v_aq, fun = "mean", na.rm = TRUE) + if (ncol(ex_t) >= 2) { + vals <- ex_t[, 2] + vals[is.na(vals) | vals <= 0] <- default_thick + cell_thick <- as.numeric(vals) + } + } else if (ext %in% c("shp", "gpkg", "geojson")) { + # Vector source: spatial join and average + thick_sf <- sf::st_read(aquifer_thickness, quiet = TRUE) + thick_sf <- sf::st_transform(thick_sf, crs = crs_obj) + # Detect the thickness column + t_col <- intersect(c("thickness", "Avg_Thick"), + names(thick_sf))[1] + if (!is.na(t_col)) { + grid_j <- sf::st_join( + grid_active[, "cell_id"], + thick_sf[, c(t_col, "geometry")], + join = sf::st_intersects + ) + t_agg <- stats::aggregate( + grid_j[[t_col]], + by = list(cell_id = grid_j$cell_id), + FUN = function(x) mean(x[x > 0], na.rm = TRUE) + ) + idx <- match(grid_active$cell_id, t_agg$cell_id) + vals <- t_agg$x[idx] + vals[is.na(vals) | vals <= 0] <- default_thick + cell_thick <- as.numeric(vals) + } + } + } + + # ------------------------------------------------------------------ + # 5. Assign conductivity zones + # ------------------------------------------------------------------ + message(" [gwflow] Assigning conductivity zones...") + cell_zone <- rep(1L, nrow(grid_active)) + + if (!is.null(conductivity_file) && file.exists(conductivity_file)) { + k_sf <- sf::st_read(conductivity_file, quiet = TRUE) + k_sf <- sf::st_transform(k_sf, crs = crs_obj) + + # Detect the K column + if ("aquifer_k" %in% names(k_sf)) { + k_col <- "aquifer_k" + } else if ("logK_Ferr_" %in% names(k_sf)) { + # GLHYMPS convention: logK_Ferr_ is log10(K_intrinsic) × 100 (m²). + # Convert to hydraulic conductivity K [m/day]: + # K_intrinsic [m²] → K_saturated [m/s] via: K_sat = K_intr * ρg/μ + # ρ = 1000 kg/m³ (water density) + # g = 9.81 m/s² (gravitational acceleration) + # μ = 0.001 Pa·s (dynamic viscosity at ~20 °C) + # Then convert [m/s] → [m/day]: × 86400 s/day + k_sf$aquifer_k <- (10^(k_sf$logK_Ferr_ / 100)) * 1000 * 9.81 / 0.001 * 86400 + k_col <- "aquifer_k" + } else { + message(" [gwflow] Warning: conductivity shapefile has no 'aquifer_k' ", + "or 'logK_Ferr_' column; using single default zone.") + k_col <- NULL + } + + if (!is.null(k_col)) { + # Assign sequential zone IDs to conductivity polygons + k_sf$zone_id <- seq_len(nrow(k_sf)) + + # Spatial join: assign zone to each active cell (take first match) + grid_j <- sf::st_join( + grid_active[, "cell_id"], + k_sf[, c("zone_id", k_col, "geometry")], + join = sf::st_intersects + ) + # If a cell hits multiple zones take the one with the smallest zone_id + z_agg <- stats::aggregate( + grid_j$zone_id, + by = list(cell_id = grid_j$cell_id), + FUN = function(x) min(x, na.rm = TRUE) + ) + idx <- match(grid_active$cell_id, z_agg$cell_id) + zone_vals <- z_agg$x[idx] + zone_vals[is.na(zone_vals)] <- 1L + cell_zone <- as.integer(zone_vals) + + # Build zone table + k_agg <- stats::aggregate( + k_sf[[k_col]], + by = list(zone_id = k_sf$zone_id), + FUN = function(x) mean(x, na.rm = TRUE) + ) + zone_df <- data.frame( + zone_id = k_agg$zone_id, + aquifer_k = k_agg$x, + specific_yield = cfg$init_sy, + streambed_k = cfg$streambed_k, + streambed_thickness = cfg$streambed_thick, + stringsAsFactors = FALSE + ) + } else { + # No usable K column → fall through to single default zone below + conductivity_file <- NULL + } + } + + if (is.null(conductivity_file)) { + # Single default zone + zone_df <- data.frame( + zone_id = 1L, + aquifer_k = default_aquifer_k, + specific_yield = cfg$init_sy, + streambed_k = cfg$streambed_k, + streambed_thickness = cfg$streambed_thick, + stringsAsFactors = FALSE + ) + } + + # ------------------------------------------------------------------ + # 6. Detect boundary cells + # ------------------------------------------------------------------ + message(" [gwflow] Detecting boundary cells...") + boundary_line <- sf::st_cast( + sf::st_boundary(watershed_union), "MULTILINESTRING" + ) + is_boundary <- as.integer( + lengths(sf::st_intersects(grid_active, boundary_line)) > 0 + ) + + # status = 1 (active interior) or 2 (active + boundary) + cell_status <- 1L + is_boundary + + # ------------------------------------------------------------------ + # 7. Write gwflow_zone + # ------------------------------------------------------------------ + message(" [gwflow] Writing gwflow_zone (", nrow(zone_df), " zone(s))...") + DBI::dbWriteTable(con, "gwflow_zone", zone_df, + append = TRUE, row.names = FALSE) + + # ------------------------------------------------------------------ + # 8. Write gwflow_grid (active cells only, mirroring Python status logic) + # ------------------------------------------------------------------ + message(" [gwflow] Writing gwflow_grid (", nrow(grid_active), + " active cells)...") + grid_df <- data.frame( + cell_id = grid_active$cell_id, + status = cell_status, + zone = cell_zone, + elevation = cell_elev, + aquifer_thickness = cell_thick, + extinction_depth = cfg$exdp, + initial_head = cell_elev - cfg$wt_depth, + tile = 0L, + stringsAsFactors = FALSE + ) + DBI::dbWriteTable(con, "gwflow_grid", grid_df, + append = TRUE, row.names = FALSE) + + # Update gwflow_base row/col counts + DBI::dbExecute( + con, + sprintf("UPDATE gwflow_base SET row_count = %d, col_count = %d", + as.integer(n_rows), as.integer(n_cols)) + ) + + # ------------------------------------------------------------------ + # 9. gwflow_rivcell: channel–cell intersections + # ------------------------------------------------------------------ + channels_sf <- project$channels_sf + if (is.null(channels_sf) && !is.null(project$streams_sf)) { + channels_sf <- project$streams_sf + } + + if (!is.null(channels_sf) && inherits(channels_sf, "sf") && + nrow(channels_sf) > 0) { + message(" [gwflow] Computing channel–cell intersections...") + + tryCatch({ + # Ensure matching CRS + channels_sf <- sf::st_transform(channels_sf, crs = crs_obj) + + # Read gis_channels id / subbasin mapping from DB + gis_ch <- DBI::dbGetQuery(con, "SELECT id, subbasin FROM gis_channels") + + # Intersect: keep only grid cells that a channel runs through + # Build a minimal sf with just WSNO (= subbasin ID) and geometry + wsno_vals <- if ("WSNO" %in% names(channels_sf)) channels_sf$WSNO + else if ("LINKNO" %in% names(channels_sf)) channels_sf$LINKNO + else seq_len(nrow(channels_sf)) + + channels_work <- sf::st_sf( + WSNO = wsno_vals, + geometry = sf::st_geometry(channels_sf), + crs = crs_obj + ) + + riv_inter <- sf::st_intersection( + channels_work, + grid_active[, c("cell_id", "geometry")] + ) + + if (nrow(riv_inter) > 0) { + # Only keep line/multiline geometries (the channel segments) + keep <- sf::st_geometry_type(riv_inter) %in% + c("LINESTRING", "MULTILINESTRING") + riv_inter <- riv_inter[keep, , drop = FALSE] + } + + if (nrow(riv_inter) > 0) { + riv_inter$length_m <- as.numeric(sf::st_length(riv_inter)) + + # Map WSNO → gis_channels.id + ch_id <- gis_ch$id[match(riv_inter$WSNO, gis_ch$subbasin)] + + riv_df <- data.frame( + cell_id = riv_inter$cell_id, + channel = ch_id, + length_m = riv_inter$length_m, + stringsAsFactors = FALSE + ) + riv_df <- riv_df[!is.na(riv_df$channel) & riv_df$length_m > 0, ] + + if (nrow(riv_df) > 0) { + message(" [gwflow] Writing gwflow_rivcell (", + nrow(riv_df), " row(s))...") + DBI::dbWriteTable(con, "gwflow_rivcell", riv_df, + append = TRUE, row.names = FALSE) + } + } + }, error = function(e) { + message(" [gwflow] Warning: could not compute channel–cell ", + "intersections: ", conditionMessage(e)) + }) + } + + # ------------------------------------------------------------------ + # 10. gwflow_lsucell: LSU–cell area intersections + # ------------------------------------------------------------------ + message(" [gwflow] Computing LSU–cell intersections...") + tryCatch({ + gis_lsus <- DBI::dbGetQuery(con, "SELECT id, subbasin FROM gis_lsus") + + lsu_inter <- sf::st_intersection( + watershed_sf[, c("subbasin", "geometry")], + grid_active[, c("cell_id", "geometry")] + ) + + if (nrow(lsu_inter) > 0) { + lsu_inter$area_m2 <- as.numeric(sf::st_area(lsu_inter)) + lsu_id <- gis_lsus$id[match(lsu_inter$subbasin, gis_lsus$subbasin)] + + lsu_df <- data.frame( + cell_id = lsu_inter$cell_id, + lsu = lsu_id, + area_m2 = lsu_inter$area_m2, + stringsAsFactors = FALSE + ) + lsu_df <- lsu_df[!is.na(lsu_df$lsu) & lsu_df$area_m2 > 0, ] + + if (nrow(lsu_df) > 0) { + message(" [gwflow] Writing gwflow_lsucell (", + nrow(lsu_df), " row(s))...") + DBI::dbWriteTable(con, "gwflow_lsucell", lsu_df, + append = TRUE, row.names = FALSE) + } + } + }, error = function(e) { + message(" [gwflow] Warning: could not compute LSU–cell intersections: ", + conditionMessage(e)) + }) + + # ------------------------------------------------------------------ + # 11. gwflow_hrucell: HRU–cell area intersections (HRU recharge mode) + # ------------------------------------------------------------------ + hru_recharge <- cfg$hruorlsu_recharge == 1L || cfg$hruorlsu_recharge == 3L + + if (hru_recharge && + !is.null(project$hru_sf) && inherits(project$hru_sf, "sf") && + nrow(project$hru_sf) > 0) { + message(" [gwflow] Computing HRU–cell intersections...") + tryCatch({ + hru_inter <- sf::st_intersection( + project$hru_sf[, c("hru_id", "geometry")], + grid_active[, c("cell_id", "geometry")] + ) + + if (nrow(hru_inter) > 0) { + hru_inter$area_m2 <- as.numeric(sf::st_area(hru_inter)) + + hru_df <- data.frame( + cell_id = hru_inter$cell_id, + hru = hru_inter$hru_id, + area_m2 = hru_inter$area_m2, + stringsAsFactors = FALSE + ) + hru_df <- hru_df[!is.na(hru_df$hru) & hru_df$area_m2 > 0, ] + + if (nrow(hru_df) > 0) { + message(" [gwflow] Writing gwflow_hrucell (", + nrow(hru_df), " row(s))...") + DBI::dbWriteTable(con, "gwflow_hrucell", hru_df, + append = TRUE, row.names = FALSE) + } + } + }, error = function(e) { + message(" [gwflow] Warning: could not compute HRU–cell intersections: ", + conditionMessage(e)) + }) + } + + # ------------------------------------------------------------------ + # 12. gwflow_rescell: reservoir–cell connections via gis_water join + # (mirrors the Python SQL: SELECT gwflow_lsucell.cell_id, + # gis_water.id, gis_water.elev FROM gis_water + # INNER JOIN gwflow_lsucell USING (lsu)) + # ------------------------------------------------------------------ + tryCatch({ + gis_water_n <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gis_water" + )$n + lsucell_n <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gwflow_lsucell" + )$n + + if (gis_water_n > 0 && lsucell_n > 0) { + res_rows <- DBI::dbGetQuery(con, " + SELECT gwflow_lsucell.cell_id AS cell_id, + gis_water.id AS res, + gis_water.elev AS res_stage + FROM gis_water + INNER JOIN gwflow_lsucell ON gis_water.lsu = gwflow_lsucell.lsu + ") + if (nrow(res_rows) > 0) { + message(" [gwflow] Writing gwflow_rescell (", + nrow(res_rows), " row(s))...") + DBI::dbWriteTable(con, "gwflow_rescell", res_rows, + append = TRUE, row.names = FALSE) + } + } + }, error = function(e) { + message(" [gwflow] Note: could not populate gwflow_rescell: ", + conditionMessage(e)) + }) + + invisible(NULL) +} diff --git a/rQSWATPlus/R/hru_creation.R b/rQSWATPlus/R/hru_creation.R new file mode 100644 index 0000000..d970faa --- /dev/null +++ b/rQSWATPlus/R/hru_creation.R @@ -0,0 +1,679 @@ +#' Create Hydrologic Response Units (HRUs) +#' +#' Creates HRUs by overlaying land use, soil, and slope rasters on the +#' delineated watershed. Each unique combination of subbasin, land use, +#' soil type, and slope class forms an HRU. +#' +#' @param project A `qswat_project` object that has been through +#' [qswat_delineate()] and [qswat_create_streams()]. +#' @param landuse_lookup A data frame from [qswat_read_landuse_lookup()] +#' mapping raster values to SWAT+ land use codes. +#' @param soil_lookup A data frame from [qswat_read_soil_lookup()] +#' mapping raster values to soil names. +#' @param slope_classes A data frame from [qswat_create_slope_classes()] +#' defining slope percentage classes. Default creates a single class. +#' @param hru_method Character. Method for selecting HRUs. One of: +#' \describe{ +#' \item{`"dominant_hru"`}{Single HRU per subbasin: the combination +#' of land use, soil, and slope class with the largest area. This +#' corresponds to the "Dominant HRU" option in the QSWATPlus GUI.} +#' \item{`"dominant_luse"`}{Single HRU per subbasin formed from the +#' dominant land use, the dominant soil within that land use, and +#' the dominant slope class within that soil. This corresponds to +#' the "Dominant landuse, soil and slope" option.} +#' \item{`"filter_threshold"`}{Multiple HRUs retained after applying +#' hierarchical percentage or area thresholds (default). This +#' corresponds to the "Filter by landuse, soil, slope" option.} +#' \item{`"filter_area"`}{Multiple HRUs retained by applying a +#' single minimum-area threshold. This corresponds to the "Filter +#' by area" option.} +#' \item{`"target"`}{Multiple HRUs reduced to approximately +#' `target_hrus` per subbasin by removing the smallest HRUs. This +#' corresponds to the "Target number of HRUs" option.} +#' } +#' Default is `"filter_threshold"`. +#' @param threshold_type Character. Whether threshold values are +#' percentages (`"percent"`, the default) or absolute areas in +#' hectares (`"area"`). Only used when `hru_method` is +#' `"filter_threshold"`. +#' @param landuse_threshold Numeric. When `hru_method = "filter_threshold"` +#' and `threshold_type = "percent"`: minimum percentage of subbasin +#' area that a land use must occupy to be kept. When +#' `threshold_type = "area"`: minimum area in hectares. Default 0 +#' (keep all). +#' @param soil_threshold Numeric. When `hru_method = "filter_threshold"` +#' and `threshold_type = "percent"`: minimum percentage of land-use +#' area that a soil must occupy. When `threshold_type = "area"`: +#' minimum area in hectares. Default 0 (keep all). +#' @param slope_threshold Numeric. When `hru_method = "filter_threshold"` +#' and `threshold_type = "percent"`: minimum percentage of soil area +#' that a slope class must occupy. When `threshold_type = "area"`: +#' minimum area in hectares. Default 0 (keep all). +#' @param area_threshold Numeric. Minimum area in hectares used when +#' `hru_method = "filter_area"`. Default 0 (keep all). +#' @param target_hrus Integer or NULL. Target number of HRUs per +#' subbasin used when `hru_method = "target"`. NULL means no target +#' filtering. +#' @param use_gwflow Logical. If `TRUE`, gwflow groundwater modelling +#' will be used (equivalent to checking "Use gwflow" in the +#' QSWATPlus GUI). The project database `use_gwflow` flag will be set +#' to 1. Call [qswat_setup_gwflow()] after [qswat_write_database()] +#' to initialise the gwflow tables. Default `FALSE`. +#' @param use_aquifers Logical. If `TRUE` (the default), SWAT+ aquifer +#' objects are created for each subbasin (equivalent to "Use SWAT+ +#' aquifers" in the GUI). Set to `FALSE` to omit aquifer tables. +#' +#' @return An updated `qswat_project` object with the following new fields: +#' \describe{ +#' \item{`hru_data`}{Data frame of HRU attributes (one row per HRU).} +#' \item{`basin_data`}{Data frame of subbasin-level statistics.} +#' \item{`hru_sf`}{`sf` polygon object: one (possibly multi-part) polygon +#' per retained HRU, with all HRU attributes joined. Suitable for +#' mapping and spatial analysis.} +#' \item{`lsu_sf`}{`sf` polygon object: one polygon per LSU / subbasin +#' (in rQSWATPlus, LSUs and subbasins are 1:1). Suitable for overlaying +#' subbasin boundaries.} +#' \item{`slope_classes`}{Slope-class definition used.} +#' \item{`use_gwflow`}{Logical groundwater flag.} +#' \item{`use_aquifers`}{Logical aquifer flag.} +#' } +#' +#' @details +#' The HRU creation process follows the QSWATPlus approach: +#' \enumerate{ +#' \item Read the watershed/subbasin raster from delineation +#' \item For each cell in the watershed, extract the corresponding +#' land use, soil, and slope values +#' \item Classify slope into the specified slope bands +#' \item Group cells by subbasin-landuse-soil-slope combinations +#' \item Apply the selected HRU method to filter/reduce HRUs +#' \item Compute statistics (area, mean elevation, mean slope) for +#' each retained HRU +#' } +#' +#' The `"filter_threshold"` method applies thresholds hierarchically: +#' land use threshold within each subbasin, then soil threshold within +#' each remaining land use, then slope threshold within each remaining +#' soil. Areas from eliminated HRUs are redistributed proportionally to +#' the surviving HRUs. +#' +#' @examples +#' \dontrun{ +#' lu_lookup <- qswat_read_landuse_lookup("landuse_lookup.csv") +#' soil_lookup <- qswat_read_soil_lookup("soil_lookup.csv") +#' slope_classes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) +#' +#' # Multiple HRUs with percentage thresholds (default) +#' project <- qswat_create_hrus(project, lu_lookup, soil_lookup, +#' slope_classes, +#' hru_method = "filter_threshold", +#' landuse_threshold = 20, +#' soil_threshold = 10, +#' slope_threshold = 10) +#' +#' # Single dominant HRU per subbasin +#' project <- qswat_create_hrus(project, lu_lookup, soil_lookup, +#' hru_method = "dominant_hru") +#' +#' # Target number of HRUs with gwflow enabled +#' project <- qswat_create_hrus(project, lu_lookup, soil_lookup, +#' hru_method = "target", +#' target_hrus = 5L, +#' use_gwflow = TRUE) +#' } +#' +#' @export +qswat_create_hrus <- function(project, + landuse_lookup, + soil_lookup, + slope_classes = qswat_create_slope_classes(), + hru_method = c("filter_threshold", "dominant_hru", + "dominant_luse", "filter_area", + "target"), + threshold_type = c("percent", "area"), + landuse_threshold = 15, + soil_threshold = 10, + slope_threshold = 10, + area_threshold = 0, + target_hrus = NULL, + use_gwflow = FALSE, + use_aquifers = TRUE) { + hru_method <- match.arg(hru_method) + threshold_type <- match.arg(threshold_type) + + if (!inherits(project, "qswat_project")) { + stop("'project' must be a qswat_project object.", call. = FALSE) + } + if (missing(landuse_lookup)) { + if (file.exists(project$landuse_lookup)) { + landuse_lookup <- qswat_read_landuse_lookup(project$landuse_lookup) + } else { + stop("Land use lookup not provided and not found in project.", + call. = FALSE) + } + } + + if (missing(soil_lookup)) { + if (file.exists(project$soil_lookup)) { + soil_lookup <- qswat_read_soil_lookup(project$soil_lookup) + } else { + stop("Soil lookup not provided and not found in project.", + call. = FALSE) + } + } + + # Validate hru_method-specific requirements + if (hru_method == "target" && + (is.null(target_hrus) || !is.numeric(target_hrus) || target_hrus < 1)) { + stop("'target_hrus' must be a positive integer when hru_method = 'target'.", + call. = FALSE) + } + if (hru_method == "filter_area" && + (!is.numeric(area_threshold) || area_threshold < 0)) { + stop("'area_threshold' must be a non-negative numeric value when hru_method = 'filter_area'.", + call. = FALSE) + } + + # Load rasters + wshed_rast <- .load_watershed_raster(project) + landuse_rast <- terra::rast(project$landuse_file) + soil_rast <- terra::rast(project$soil_file) + + # Calculate slope from DEM or use sd8 from delineation + slope_rast <- .get_slope_raster(project) + + # Get DEM for elevation stats + + dem_rast <- terra::rast(project$dem_file) + + message("Computing HRUs from raster overlays...") + + # Align all rasters to watershed grid + landuse_aligned <- terra::project(landuse_rast, wshed_rast, method = "near") + soil_aligned <- terra::project(soil_rast, wshed_rast, method = "near") + slope_aligned <- terra::project(slope_rast, wshed_rast, method = "near") + dem_aligned <- terra::project(dem_rast, wshed_rast, method = "bilinear") + + # Read all rasters into memory + wshed_vals <- terra::values(wshed_rast, mat = FALSE) + landuse_vals <- terra::values(landuse_aligned, mat = FALSE) + soil_vals <- terra::values(soil_aligned, mat = FALSE) + slope_vals <- terra::values(slope_aligned, mat = FALSE) + dem_vals <- terra::values(dem_aligned, mat = FALSE) + + # Get valid cells (non-NA in watershed) + valid <- !is.na(wshed_vals) & wshed_vals > 0 + if (sum(valid) == 0) { + stop("No valid watershed cells found.", call. = FALSE) + } + + # Calculate cell area + cell_area <- .compute_cell_area(wshed_rast, project$units) + + # Build cell data frame (cell_idx tracks each row's position in wshed_vals) + cell_data <- data.frame( + cell_idx = which(valid), + subbasin = wshed_vals[valid], + landuse_val = landuse_vals[valid], + soil_val = soil_vals[valid], + slope_pct = slope_vals[valid], + elevation = dem_vals[valid], + stringsAsFactors = FALSE + ) + + # Map raster values to names using lookups + cell_data$landuse <- .map_lookup(cell_data$landuse_val, landuse_lookup$value, + landuse_lookup$landuse) + cell_data$soil <- .map_lookup(cell_data$soil_val, soil_lookup$value, + soil_lookup$soil) + + # Classify slope into bands + cell_data$slope_class <- .classify_slope(cell_data$slope_pct, slope_classes) + + # Remove cells with unmapped values + unmapped_lu <- is.na(cell_data$landuse) + unmapped_soil <- is.na(cell_data$soil) + unmapped_slope <- is.na(cell_data$slope_class) + + if (sum(unmapped_lu) > 0) { + pct <- round(100 * sum(unmapped_lu) / nrow(cell_data), 1) + message(" ", sum(unmapped_lu), " cells (", pct, + "%) had unmapped land use values - excluded") + } + if (sum(unmapped_soil) > 0) { + pct <- round(100 * sum(unmapped_soil) / nrow(cell_data), 1) + message(" ", sum(unmapped_soil), " cells (", pct, + "%) had unmapped soil values - excluded") + } + + valid_cells <- !unmapped_lu & !unmapped_soil & !unmapped_slope + cell_data <- cell_data[valid_cells, ] + + if (nrow(cell_data) == 0) { + stop("No valid cells after lookup mapping. Check lookup tables.", + call. = FALSE) + } + + # Aggregate cells into HRUs + hru_data <- stats::aggregate( + cbind(cell_count = 1, elevation = elevation, + slope_pct = slope_pct) ~ subbasin + landuse + soil + slope_class, + data = cell_data, + FUN = function(x) { + if (length(x) == 1 && is.numeric(x)) return(x) + return(sum(x)) + } + ) + + # Re-aggregate properly + hru_data <- .aggregate_hrus(cell_data, cell_area) + + # Apply HRU method to filter/reduce HRUs + hru_data <- switch(hru_method, + dominant_hru = .apply_dominant_hru(hru_data), + dominant_luse = .apply_dominant_luse(hru_data), + filter_threshold = { + if (landuse_threshold > 0 || soil_threshold > 0 || slope_threshold > 0) { + .apply_thresholds(hru_data, landuse_threshold, + soil_threshold, slope_threshold, + by_area = (threshold_type == "area")) + } else { + hru_data + } + }, + filter_area = { + if (area_threshold > 0) { + .apply_area_filter(hru_data, area_threshold) + } else { + hru_data + } + }, + target = .apply_target_filter(hru_data, target_hrus) + ) + + # Apply target HRU filtering (legacy path kept for backward compat when + # hru_method is not "target" but target_hrus is still supplied) + # NOTE: prefer hru_method = "target" for intentional target-based selection. + if (hru_method != "target" && !is.null(target_hrus) && target_hrus > 0) { + hru_data <- .apply_target_filter(hru_data, target_hrus) + } + + # Assign HRU IDs + hru_data$hru_id <- seq_len(nrow(hru_data)) + + # Compute basin-level statistics + basin_data <- .compute_basin_stats(hru_data, cell_data, cell_area) + + # Build spatial HRU and LSU polygon objects + message("Building spatial HRU and LSU polygons...") + project$hru_sf <- .build_hru_sf(wshed_rast, cell_data, hru_data) + project$lsu_sf <- .build_lsu_sf(wshed_rast) + + project$hru_data <- hru_data + project$basin_data <- basin_data + project$slope_classes <- slope_classes + project$use_gwflow <- isTRUE(use_gwflow) + project$use_aquifers <- isTRUE(use_aquifers) + + message("Created ", nrow(hru_data), " HRUs across ", + length(unique(hru_data$subbasin)), " subbasins") + + return(project) +} + + +#' Load Watershed Raster +#' @noRd +.load_watershed_raster <- function(project) { + if (!is.null(project$watershed_file) && file.exists(project$watershed_file)) { + return(terra::rast(project$watershed_file)) + } + stop("Watershed raster not found. Run qswat_delineate() first.", + call. = FALSE) +} + + +#' Get or Compute Slope Raster +#' @noRd +.get_slope_raster <- function(project) { + # Use D8 slope from TauDEM if available + if (!is.null(project$sd8_file) && file.exists(project$sd8_file)) { + slp <- terra::rast(project$sd8_file) + # Convert from radians to percent if needed + # TauDEM sd8 outputs slope as drop/distance (tangent), convert to percent + return(slp * 100) + } + # Compute from DEM + dem <- terra::rast(project$dem_file) + slp <- terra::terrain(dem, v = "slope", unit = "degrees") + # Convert degrees to percent + return(tan(slp * pi / 180) * 100) +} + + +#' Compute Cell Area +#' @noRd +.compute_cell_area <- function(rast, units = "meters") { + res <- terra::res(rast) + area <- res[1] * res[2] # cell area in map units^2 + + # Convert to hectares + if (units == "meters") { + area_ha <- area / 10000 + } else if (units == "degrees") { + # Approximate using center latitude + ext <- terra::ext(rast) + center_lat <- (ext$ymin + ext$ymax) / 2 + m_per_deg_lat <- 111320 + m_per_deg_lon <- 111320 * cos(center_lat * pi / 180) + area_m2 <- res[1] * m_per_deg_lon * res[2] * m_per_deg_lat + area_ha <- area_m2 / 10000 + } else { + # Assume feet + area_ha <- area * 0.3048^2 / 10000 + } + + return(area_ha) +} + + +#' Map Raster Values to Names via Lookup +#' @noRd +.map_lookup <- function(values, lookup_values, lookup_names) { + idx <- match(values, lookup_values) + return(lookup_names[idx]) +} + + +#' Classify Slope into Bands +#' @noRd +.classify_slope <- function(slope_pct, slope_classes) { + classes <- rep(NA_integer_, length(slope_pct)) + for (i in seq_len(nrow(slope_classes))) { + in_class <- slope_pct >= slope_classes$min_slope[i] & + slope_pct < slope_classes$max_slope[i] + in_class[is.na(in_class)] <- FALSE + classes[in_class] <- slope_classes$class_id[i] + } + return(classes) +} + + +#' Aggregate Cell Data into HRUs +#' @noRd +.aggregate_hrus <- function(cell_data, cell_area) { + # Group by subbasin, landuse, soil, slope_class + groups <- paste(cell_data$subbasin, cell_data$landuse, + cell_data$soil, cell_data$slope_class, sep = "|") + + unique_groups <- unique(groups) + n_hrus <- length(unique_groups) + + hru_list <- lapply(unique_groups, function(g) { + idx <- groups == g + parts <- strsplit(g, "\\|")[[1]] + data.frame( + subbasin = as.integer(parts[1]), + landuse = parts[2], + soil = parts[3], + slope_class = as.integer(parts[4]), + cell_count = sum(idx), + area_ha = sum(idx) * cell_area, + mean_elevation = mean(cell_data$elevation[idx], na.rm = TRUE), + mean_slope = mean(cell_data$slope_pct[idx], na.rm = TRUE), + stringsAsFactors = FALSE + ) + }) + + do.call(rbind, hru_list) +} + + +#' Apply Dominant Area Thresholds +#' @noRd +.apply_thresholds <- function(hru_data, lu_thresh, soil_thresh, slope_thresh, + by_area = FALSE) { + subbasins <- unique(hru_data$subbasin) + result_list <- list() + + for (sub in subbasins) { + sub_data <- hru_data[hru_data$subbasin == sub, ] + sub_area <- sum(sub_data$area_ha) + + if (sub_area == 0) next + + # Apply landuse threshold + if (lu_thresh > 0) { + lu_areas <- tapply(sub_data$area_ha, sub_data$landuse, sum) + if (by_area) { + keep_lu <- names(lu_areas[lu_areas >= lu_thresh]) + } else { + lu_pct <- lu_areas / sub_area * 100 + keep_lu <- names(lu_pct[lu_pct >= lu_thresh]) + } + sub_data <- sub_data[sub_data$landuse %in% keep_lu, ] + } + + # Apply soil threshold (within each landuse) + if (soil_thresh > 0 && nrow(sub_data) > 0) { + keep_rows <- logical(nrow(sub_data)) + for (lu in unique(sub_data$landuse)) { + lu_idx <- sub_data$landuse == lu + lu_area <- sum(sub_data$area_ha[lu_idx]) + soil_areas <- tapply(sub_data$area_ha[lu_idx], + sub_data$soil[lu_idx], sum) + if (by_area) { + keep_soil <- names(soil_areas[soil_areas >= soil_thresh]) + } else { + soil_pct <- soil_areas / lu_area * 100 + keep_soil <- names(soil_pct[soil_pct >= soil_thresh]) + } + keep_rows[lu_idx] <- sub_data$soil[lu_idx] %in% keep_soil + } + sub_data <- sub_data[keep_rows, ] + } + + # Apply slope threshold (within each landuse-soil combo) + if (slope_thresh > 0 && nrow(sub_data) > 0) { + keep_rows <- logical(nrow(sub_data)) + combos <- paste(sub_data$landuse, sub_data$soil, sep = "|") + for (combo in unique(combos)) { + c_idx <- combos == combo + c_area <- sum(sub_data$area_ha[c_idx]) + slope_areas <- tapply(sub_data$area_ha[c_idx], + sub_data$slope_class[c_idx], sum) + if (by_area) { + keep_slope <- names(slope_areas[slope_areas >= slope_thresh]) + } else { + slope_pct <- slope_areas / c_area * 100 + keep_slope <- names(slope_pct[slope_pct >= slope_thresh]) + } + keep_rows[c_idx] <- as.character(sub_data$slope_class[c_idx]) %in% + keep_slope + } + sub_data <- sub_data[keep_rows, ] + } + + if (nrow(sub_data) > 0) { + result_list[[length(result_list) + 1]] <- sub_data + } + } + + if (length(result_list) == 0) { + stop("All HRUs were eliminated by thresholds. Use lower threshold values.", + call. = FALSE) + } + + do.call(rbind, result_list) +} + + +#' Apply Minimum-Area Filter (filter_area method) +#' @noRd +.apply_area_filter <- function(hru_data, area_threshold) { + kept <- hru_data[hru_data$area_ha >= area_threshold, ] + if (nrow(kept) == 0) { + stop(sprintf( + "All HRUs were eliminated by area_threshold = %g ha. Use a smaller value.", + area_threshold), call. = FALSE) + } + kept +} + + +#' Select Single Dominant HRU Per Subbasin +#' +#' Keeps only the HRU with the largest area in each subbasin. +#' @noRd +.apply_dominant_hru <- function(hru_data) { + subbasins <- unique(hru_data$subbasin) + result_list <- lapply(subbasins, function(sub) { + sub_data <- hru_data[hru_data$subbasin == sub, ] + sub_data[which.max(sub_data$area_ha), , drop = FALSE] + }) + do.call(rbind, result_list) +} + + +#' Select Single HRU from Dominant Landuse/Soil/Slope Per Subbasin +#' +#' For each subbasin selects the dominant land use, then within that +#' land use the dominant soil, and within that soil the dominant slope +#' class. The resulting three-way combination forms a single HRU. +#' @noRd +.apply_dominant_luse <- function(hru_data) { + subbasins <- unique(hru_data$subbasin) + result_list <- lapply(subbasins, function(sub) { + sub_data <- hru_data[hru_data$subbasin == sub, ] + + # Dominant landuse by area + lu_areas <- tapply(sub_data$area_ha, sub_data$landuse, sum) + dom_lu <- names(which.max(lu_areas)) + lu_data <- sub_data[sub_data$landuse == dom_lu, ] + + # Dominant soil within dominant landuse + soil_areas <- tapply(lu_data$area_ha, lu_data$soil, sum) + dom_soil <- names(which.max(soil_areas)) + soil_data <- lu_data[lu_data$soil == dom_soil, ] + + # Dominant slope class within dominant soil + soil_data[which.max(soil_data$area_ha), , drop = FALSE] + }) + do.call(rbind, result_list) +} + + +#' Apply Target Number of HRUs Filter +#' @noRd +.apply_target_filter <- function(hru_data, target_hrus) { + subbasins <- unique(hru_data$subbasin) + result_list <- list() + + for (sub in subbasins) { + sub_data <- hru_data[hru_data$subbasin == sub, ] + + if (nrow(sub_data) <= target_hrus) { + result_list[[length(result_list) + 1]] <- sub_data + next + } + + # Keep the largest HRUs + sub_data <- sub_data[order(-sub_data$area_ha), ] + result_list[[length(result_list) + 1]] <- sub_data[seq_len(target_hrus), ] + } + + do.call(rbind, result_list) +} + + +#' Compute Basin-Level Statistics +#' @noRd +.compute_basin_stats <- function(hru_data, cell_data, cell_area) { + subbasins <- unique(hru_data$subbasin) + + basin_list <- lapply(subbasins, function(sub) { + sub_cells <- cell_data[cell_data$subbasin == sub, ] + sub_hrus <- hru_data[hru_data$subbasin == sub, ] + + data.frame( + subbasin = sub, + area_ha = nrow(sub_cells) * cell_area, + mean_elevation = mean(sub_cells$elevation, na.rm = TRUE), + min_elevation = min(sub_cells$elevation, na.rm = TRUE), + max_elevation = max(sub_cells$elevation, na.rm = TRUE), + mean_slope = mean(sub_cells$slope_pct, na.rm = TRUE), + n_hrus = nrow(sub_hrus), + n_landuses = length(unique(sub_hrus$landuse)), + n_soils = length(unique(sub_hrus$soil)), + stringsAsFactors = FALSE + ) + }) + + do.call(rbind, basin_list) +} + + +#' Build sf polygon object for HRUs +#' +#' Creates a raster in which every valid watershed cell is labelled with its +#' HRU ID, dissolves contiguous cells sharing the same ID into polygons, and +#' joins the HRU attribute table. Cells whose exact (subbasin, landuse, soil, +#' slope_class) combination was eliminated by the chosen HRU method are +#' reassigned to the largest-area HRU in their subbasin so that the entire +#' watershed remains covered. +#' @noRd +.build_hru_sf <- function(wshed_rast, cell_data, hru_data) { + # Build key -> hru_id lookup from the retained HRUs + hru_key <- paste(hru_data$subbasin, hru_data$landuse, + hru_data$soil, hru_data$slope_class, sep = "|") + hru_id_map <- stats::setNames(hru_data$hru_id, hru_key) + + # Map every valid+mapped cell to its HRU ID + cell_key <- paste(cell_data$subbasin, cell_data$landuse, + cell_data$soil, cell_data$slope_class, sep = "|") + cell_hru_id <- hru_id_map[cell_key] + + # For cells whose combination was eliminated (NA after lookup), assign the + # dominant (largest-area) HRU of the same subbasin + na_cells <- is.na(cell_hru_id) + if (any(na_cells)) { + dom_by_sub <- vapply(unique(hru_data$subbasin), function(s) { + sh <- hru_data[hru_data$subbasin == s, , drop = FALSE] + sh$hru_id[which.max(sh$area_ha)] + }, integer(1L)) + dom_map <- stats::setNames(dom_by_sub, + as.character(unique(hru_data$subbasin))) + cell_hru_id[na_cells] <- + dom_map[as.character(cell_data$subbasin[na_cells])] + } + + # Fill a raster with HRU IDs. + # cell_data$cell_idx holds the 1-based linear cell index in wshed_rast for + # each row; it was stored as which(valid) when cell_data was first built, + # so it is always a valid index into hru_vals. + hru_vals <- rep(NA_integer_, terra::ncell(wshed_rast)) + hru_vals[cell_data$cell_idx] <- cell_hru_id + hru_rast <- terra::rast(wshed_rast) + names(hru_rast) <- "hru_id" + terra::values(hru_rast) <- hru_vals + + # Vectorise: dissolve contiguous cells sharing the same HRU ID + polys <- terra::as.polygons(hru_rast, dissolve = TRUE) + polys_sf <- sf::st_as_sf(polys) + + # Join HRU attributes + merge(polys_sf, hru_data, by = "hru_id", all.x = TRUE) +} + + +#' Build sf polygon object for LSUs (subbasins) +#' +#' Vectorises the watershed raster so that each unique subbasin ID becomes one +#' (possibly multi-part) polygon. In rQSWATPlus, LSUs and subbasins are in a +#' 1:1 relationship. +#' @noRd +.build_lsu_sf <- function(wshed_rast) { + polys <- terra::as.polygons(wshed_rast, dissolve = TRUE) + names(polys)[1] <- "subbasin" + sf::st_as_sf(polys) +} diff --git a/rQSWATPlus/R/lookup.R b/rQSWATPlus/R/lookup.R new file mode 100644 index 0000000..e80245d --- /dev/null +++ b/rQSWATPlus/R/lookup.R @@ -0,0 +1,267 @@ +#' Read Land Use Lookup Table +#' +#' Reads a CSV file that maps land use raster values to SWAT+ land use +#' codes. The lookup table is used during HRU creation to assign SWAT+ +#' land use categories. +#' +#' @param lookup_file Character. Path to the CSV file. The file should +#' have at least two columns: the raster value and the SWAT+ land use +#' code. +#' @param value_col Character or integer. Name or index of the column +#' containing raster values. Default is 1 (first column). +#' @param name_col Character or integer. Name or index of the column +#' containing SWAT+ land use codes. Default is 2 (second column). +#' +#' @return A data frame with columns `value` (integer) and `landuse` +#' (character). +#' +#' @details +#' The lookup CSV should map each unique value in the land use raster to +#' a SWAT+ land use code (e.g., AGRL, FRSD, PAST, URBN, WATR). The +#' format is flexible - the function will detect columns based on +#' position or name. +#' +#' Common SWAT+ land use codes include: +#' \itemize{ +#' \item AGRL - Agricultural land (generic) +#' \item AGRR - Agricultural row crops +#' \item FRSD - Forest (deciduous) +#' \item FRSE - Forest (evergreen) +#' \item FRST - Forest (mixed) +#' \item PAST - Pasture +#' \item RNGE - Range (grass) +#' \item URBN - Urban (generic) +#' \item URHD - Urban high density +#' \item URMD - Urban medium density +#' \item WATR - Water +#' \item WETF - Wetland (forested) +#' \item WETL - Wetland +#' } +#' +#' @examples +#' lu_file <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") +#' lu_lookup <- qswat_read_landuse_lookup(lu_file) +#' head(lu_lookup) +#' +#' @export +qswat_read_landuse_lookup <- function(lookup_file, + value_col = 1, + name_col = 2) { + if (!file.exists(lookup_file)) { + stop("Lookup file not found: ", lookup_file, call. = FALSE) + } + + df <- utils::read.csv(lookup_file, stringsAsFactors = FALSE) + + if (ncol(df) < 2) { + stop("Lookup file must have at least 2 columns.", call. = FALSE) + } + + result <- data.frame( + value = as.integer(df[[value_col]]), + landuse = trimws(as.character(df[[name_col]])), + stringsAsFactors = FALSE + ) + + # Remove rows with NA values + result <- result[!is.na(result$value) & result$landuse != "", ] + + if (nrow(result) == 0) { + stop("No valid entries found in lookup file.", call. = FALSE) + } + + return(result) +} + + +#' Read Soil Lookup Table +#' +#' Reads a CSV file that maps soil raster values to SWAT+ soil names. +#' The lookup table is used during HRU creation to assign soil types. +#' +#' @param lookup_file Character. Path to the CSV file. +#' @param value_col Character or integer. Name or index of the column +#' containing raster values. Default is 1 (first column). +#' @param name_col Character or integer. Name or index of the column +#' containing soil names. Default is 2 (second column). +#' +#' @return A data frame with columns `value` (integer) and `soil` +#' (character). +#' +#' @details +#' The lookup CSV maps each unique value in the soil raster to a SWAT+ +#' soil name. Soil names should correspond to entries in the SWAT+ +#' soils database (e.g., STATSGO or SSURGO soil names). +#' +#' @examples +#' soil_file <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") +#' soil_lookup <- qswat_read_soil_lookup(soil_file) +#' head(soil_lookup) +#' +#' @export +qswat_read_soil_lookup <- function(lookup_file, + value_col = 1, + name_col = 2) { + if (!file.exists(lookup_file)) { + stop("Lookup file not found: ", lookup_file, call. = FALSE) + } + + df <- utils::read.csv(lookup_file, stringsAsFactors = FALSE) + + if (ncol(df) < 2) { + stop("Lookup file must have at least 2 columns.", call. = FALSE) + } + + result <- data.frame( + value = as.integer(df[[value_col]]), + soil = trimws(as.character(df[[name_col]])), + stringsAsFactors = FALSE + ) + + result <- result[!is.na(result$value) & result$soil != "", ] + + if (nrow(result) == 0) { + stop("No valid entries found in lookup file.", call. = FALSE) + } + + return(result) +} + + +#' Read User Soil Data from CSV +#' +#' Reads a CSV file containing soil physical parameters in the SWAT+ +#' `global_usersoil` table format. The resulting data frame can be passed +#' directly to [qswat_write_database()] via the `usersoil` argument to +#' populate soil physical properties in the project database. +#' +#' @param csv_file Character. Path to the CSV file containing soil parameters. +#' Must have at minimum a column named `SNAM` (soil name). Additional +#' columns should match the `global_usersoil` table schema: +#' `NLAYERS`, `HYDGRP`, `SOL_ZMX`, `ANION_EXCL`, `SOL_CRK`, `TEXTURE`, +#' and for each layer \emph{n} = 1--10: `SOL_Zn`, `SOL_BDn`, `SOL_AWCn`, +#' `SOL_Kn`, `SOL_CBNn`, `CLAYn`, `SILTn`, `SANDn`, `ROCKn`, +#' `SOL_ALBn`, `USLE_Kn`, `SOL_ECn`. +#' +#' @return A data frame with column names normalised to uppercase, suitable +#' for passing to [qswat_write_database()] as the `usersoil` argument. +#' Rows with missing `SNAM` values are removed. +#' +#' @details +#' Column names are converted to uppercase on reading so the data frame is +#' compatible with the SWAT+ project database schema regardless of the case +#' used in the CSV file. +#' +#' Instead of passing the data frame directly to \code{qswat_write_database}, +#' you can also pass the file path as a string; [qswat_write_database()] will +#' call this function internally. +#' +#' @examples +#' csv_file <- tempfile(fileext = ".csv") +#' write.csv(data.frame( +#' SNAM = c("MySoil1", "MySoil2"), +#' NLAYERS = c(2L, 3L), +#' HYDGRP = c("B", "C"), +#' SOL_ZMX = c(1000, 1500), +#' ANION_EXCL = c(0.5, 0.5), +#' SOL_CRK = c(0.5, 0.5), +#' SOL_Z1 = c(300, 200), +#' SOL_BD1 = c(1.4, 1.5) +#' ), csv_file, row.names = FALSE) +#' +#' soil_data <- qswat_read_usersoil(csv_file) +#' head(soil_data) +#' +#' @seealso [qswat_write_database()] +#' @export +qswat_read_usersoil <- function(csv_file) { + if (!file.exists(csv_file)) { + stop("CSV file not found: ", csv_file, call. = FALSE) + } + + df <- utils::read.csv(csv_file, stringsAsFactors = FALSE, check.names = FALSE) + + if (nrow(df) == 0L) { + stop("No data found in usersoil CSV file.", call. = FALSE) + } + + names(df) <- toupper(names(df)) + + if (!"SNAM" %in% names(df)) { + stop( + "usersoil CSV must have an 'SNAM' column with soil names.", + call. = FALSE + ) + } + + df <- df[!is.na(df$SNAM) & nzchar(df$SNAM), , drop = FALSE] + + if (nrow(df) == 0L) { + stop( + "No valid soil entries found (all SNAM values are missing or empty).", + call. = FALSE + ) + } + + return(df) +} + + + +#' Create Slope Classification Bands +#' +#' Defines slope percentage classes for HRU creation. Slope bands +#' are used to further subdivide HRUs by terrain steepness. +#' +#' @param breaks Numeric vector. Slope percentage breakpoints defining +#' the class boundaries. For example, `c(0, 5, 15, 9999)` creates +#' three classes: 0-5%, 5-15%, and >15%. +#' @param labels Character vector or NULL. Labels for each slope class. +#' If NULL, labels are auto-generated from the breaks. +#' +#' @return A data frame with columns `min_slope`, `max_slope`, `label`, +#' and `class_id`. +#' +#' @examples +#' # Three slope classes +#' slopes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) +#' slopes +#' +#' # Single slope class (no subdivision) +#' slopes <- qswat_create_slope_classes(c(0, 9999)) +#' slopes +#' +#' @export +qswat_create_slope_classes <- function(breaks = c(0, 9999), labels = NULL) { + if (length(breaks) < 2) { + stop("At least two breakpoints are required.", call. = FALSE) + } + if (any(diff(breaks) <= 0)) { + stop("Breaks must be strictly increasing.", call. = FALSE) + } + + n_classes <- length(breaks) - 1 + + if (is.null(labels)) { + labels <- vapply(seq_len(n_classes), function(i) { + if (breaks[i + 1] >= 9999) { + paste0(breaks[i], "+") + } else { + paste0(breaks[i], "-", breaks[i + 1]) + } + }, character(1)) + } + + if (length(labels) != n_classes) { + stop("Number of labels must equal number of classes (", n_classes, ").", + call. = FALSE) + } + + data.frame( + min_slope = breaks[-length(breaks)], + max_slope = breaks[-1], + label = labels, + class_id = seq_len(n_classes), + stringsAsFactors = FALSE + ) +} diff --git a/rQSWATPlus/R/rQSWATPlus-package.R b/rQSWATPlus/R/rQSWATPlus-package.R new file mode 100644 index 0000000..1928bbb --- /dev/null +++ b/rQSWATPlus/R/rQSWATPlus-package.R @@ -0,0 +1,56 @@ +#' rQSWATPlus: R Interface to QSWATPlus for SWAT+ Model Setup +#' +#' @description +#' The rQSWATPlus package provides R functions to replicate the QSWATPlus +#' QGIS plugin workflow for setting up SWAT+ (Soil and Water Assessment Tool) +#' hydrological models. +#' +#' @details +#' The package implements a complete workflow for SWAT+ model setup: +#' +#' \enumerate{ +#' \item **Project setup** ([qswat_setup()]): Initialize project structure +#' and validate inputs +#' \item **Watershed delineation** ([qswat_delineate()]): DEM processing +#' and watershed delineation using TauDEM via the traudem package +#' \item **Stream network** ([qswat_create_streams()]): Extract stream +#' topology from delineation results +#' \item **HRU creation** ([qswat_create_hrus()]): Create Hydrologic +#' Response Units from land use, soil, and slope overlays +#' \item **Database output** ([qswat_write_database()]): Write SWAT+ +#' project database for use with SWAT+ Editor +#' } +#' +#' A convenience function [qswat_run()] executes all steps in sequence. +#' +#' The package includes example data from the Ravn watershed in Denmark +#' for testing and demonstration. +#' +#' @section TauDEM Dependency: +#' Watershed delineation requires TauDEM to be installed. Use +#' `traudem::taudem_sitrep()` to check your installation. See +#' `vignette("taudem-installation", package = "traudem")` for +#' installation instructions. +#' +#' @seealso +#' \itemize{ +#' \item `vignette("introduction", package = "rQSWATPlus")` for a +#' getting started guide +#' \item `vignette("data-requirements", package = "rQSWATPlus")` for +#' input data format specifications +#' \item `vignette("workflow", package = "rQSWATPlus")` for a complete +#' workflow example +#' } +#' +#' @import terra +#' @importFrom sf st_read st_write st_crs st_transform st_geometry_type +#' st_coordinates st_as_sf st_bbox +#' @importFrom DBI dbConnect dbDisconnect dbWriteTable dbExecute dbGetQuery +#' dbListTables +#' @importFrom RSQLite SQLite +#' @importFrom utils read.csv +#' @importFrom rlang .data +#' @name rQSWATPlus-package +#' @aliases rQSWATPlus +#' @keywords internal +"_PACKAGE" diff --git a/rQSWATPlus/R/setup_project.R b/rQSWATPlus/R/setup_project.R new file mode 100644 index 0000000..63bf957 --- /dev/null +++ b/rQSWATPlus/R/setup_project.R @@ -0,0 +1,248 @@ +#' Set Up a QSWATPlus Project +#' +#' Initializes a QSWATPlus project directory structure and validates +#' input files. This is typically the first step in the SWAT+ model +#' setup workflow. +#' +#' @param project_dir Character. Path to the project directory. Will be +#' created if it does not exist. +#' @param outlet_file Character or NULL. Optional path to outlet point +#' shapefile for watershed delineation. +#' @param dem_file Character. Path to the DEM (Digital Elevation Model) +#' raster file (GeoTIFF or other GDAL-supported format). +#' @param landuse_file Character. Path to the land use raster file. +#' @param landuse_lookup Character. Path to the land use lookup CSV file, +#' mapping raster values to SWAT+ land use codes. +#' @param soil_file Character. Path to the soil raster file. +#' @param soil_lookup Character. Path to the soil lookup CSV file, +#' mapping raster values to SWAT+ soil names. +#' @param overwrite Logical. If TRUE, overwrite existing project files. +#' Default is FALSE. +#' @param usersoil Character or NULL. Soil physical properties dataset to +#' use when writing the project database with [qswat_write_database()]. +#' Stored in the project object and used automatically unless overridden +#' in [qswat_write_database()]. Accepted values: +#' \describe{ +#' \item{`NULL`}{(default) Leave `global_usersoil` empty.} +#' \item{`"FAO_usersoil"`}{Use FAO global soil data from the bundled +#' `QSWATPlusProjHAWQS.sqlite` database.} +#' \item{`"global_usersoil"`}{Use the full global soil dataset from +#' the bundled `QSWATPlusProjHAWQS.sqlite` database.} +#' \item{file path}{Path to a CSV file in `global_usersoil` format. +#' See [qswat_read_usersoil()].} +#' } +#' @param ... Additional arguments to include in the returned project +#' object. This allows users to add custom metadata or file paths as needed. +#' +#' @return A list of class `"qswat_project"` containing project +#' configuration and file paths. +#' +#' @details +#' The function performs the following steps: +#' \enumerate{ +#' \item Creates the project directory structure +#' \item Validates that all input files exist and are readable +#' \item Checks that raster files have compatible coordinate systems +#' \item Copies input files to the project directory +#' \item Returns a project object for use in subsequent functions +#' } +#' +#' All raster inputs should ideally share the same coordinate reference +#' system. The DEM should be in a projected CRS (not geographic) for +#' accurate area and slope calculations. +#' +#' @examples +#' \dontrun{ +#' dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") +#' landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") +#' soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") +#' lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") +#' soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") +#' +#' # Setup with FAO soil data applied at database-write time +#' project <- qswat_setup( +#' project_dir = tempdir(), +#' dem_file = dem, +#' landuse_file = landuse, +#' landuse_lookup = lu_lookup, +#' soil_file = soil, +#' soil_lookup = soil_lookup, +#' usersoil = "FAO_usersoil" +#' ) +#' } +#' +#' @export +qswat_setup <- function(project_dir, + overwrite = FALSE, + dem_file = NULL, + landuse_file = NULL, + landuse_lookup = NULL, + soil_file = NULL, + soil_lookup = NULL, + outlet_file = NULL, + usersoil = NULL, + ...) { + + # 1. Validate inputs only if they are provided + input_files <- list( + dem = dem_file, + landuse = landuse_file, + soil = soil_file, + lu_lookup = landuse_lookup, + s_lookup = soil_lookup, + outlet = outlet_file + ) + + for (f in Filter(Negate(is.null), input_files)) { + if (!file.exists(f)) { + stop("File not found: ", f, call. = FALSE) + } + } + + # 2. Create project directory structure + dirs <- file.path(project_dir, c("Source", "Watershed", + "Watershed/Rasters", + "Watershed/Shapes", + "Watershed/Text")) + for (d in c(project_dir, dirs)) { + if (!dir.exists(d)) { + dir.create(d, recursive = TRUE) + } + } + + # 3. Process DEM metadata if provided + dem_crs <- NULL + units <- NULL + res <- NULL + ext <- NULL + dims <- c(nrow = NULL, ncol = NULL) + + if (!is.null(dem_file)) { + dem <- terra::rast(dem_file) + dem_crs <- terra::crs(dem) + if (dem_crs == "") warning("DEM has no CRS defined.", call. = FALSE) + + units <- .detect_units(terra::crs(dem, describe = TRUE)) + res <- terra::res(dem) + ext <- as.vector(terra::ext(dem)) + dims <- c(nrow = terra::nrow(dem), ncol = terra::ncol(dem)) + } + + # 4. Helper for copying files to "Source" + copy_to_source <- function(file_path) { + if (is.null(file_path)) return(NULL) + dest <- file.path(project_dir, "Source", basename(file_path)) + if (overwrite || !file.exists(dest)) { + file.copy(file_path, dest, overwrite = overwrite) + } + return(normalizePath(dest, mustWork = FALSE)) + } + + # Copy standard files + dem_proj <- copy_to_source(dem_file) + landuse_proj <- copy_to_source(landuse_file) + soil_proj <- copy_to_source(soil_file) + lu_lkp_proj <- copy_to_source(landuse_lookup) + s_lkp_proj <- copy_to_source(soil_lookup) + + # Special handling for Shapefile (Outlet) components + outlet_proj <- NULL + if (!is.null(outlet_file)) { + out_base <- tools::file_path_sans_ext(basename(outlet_file)) + out_dir <- dirname(outlet_file) + for (ext_sh in c("shp", "shx", "dbf", "prj", "cpg", "qpj")) { + src <- file.path(out_dir, paste0(out_base, ".", ext_sh)) + if (file.exists(src)) { + file.copy(src, file.path(project_dir, "Source", paste0(out_base, ".", ext_sh)), + overwrite = overwrite) + } + } + outlet_proj <- normalizePath(file.path(project_dir, "Source", paste0(out_base, ".shp")), mustWork = FALSE) + } + + # 5. Build the project object + project <- list( + project_dir = normalizePath(project_dir), + dem_file = dem_proj, + landuse_file = landuse_proj, + landuse_lookup = lu_lkp_proj, + soil_file = soil_proj, + soil_lookup = s_lkp_proj, + outlet_file = outlet_proj, + usersoil = usersoil, + crs = dem_crs, + units = units, + cell_size = res, + extent = ext, + nrow = dims["nrow"], + ncol = dims["ncol"], + # Placeholders for future steps + fel_file = NULL, p_file = NULL, sd8_file = NULL, slp_file = NULL, + ang_file = NULL, ad8_file = NULL, sca_file = NULL, src_stream_file = NULL, + src_channel_file = NULL, ord_file = NULL, tree_file = NULL, coord_file = NULL, + stream_file = NULL, watershed_file = NULL, hru_data = NULL, basin_data = NULL, + stream_threshold = NULL, channel_threshold = NULL, db_file = NULL + ) + + # Merge ellipsis arguments + extra_args <- list(...) + if (length(extra_args) > 0) { + project <- utils::modifyList(project, extra_args) + } + + class(project) <- "qswat_project" + return(project) +} + + +#' Print QSWATPlus Project Summary +#' +#' @param x A `qswat_project` object. +#' @param ... Additional arguments (ignored). +#' +#' @return Invisibly returns `x`. +#' @export +print.qswat_project <- function(x, ...) { + cat("QSWATPlus Project\n") + cat(" Directory:", x$project_dir, "\n") + if (!is.null(x$db_file)) { + cat(" Sqlite database:", x$db_file, "\n") + } + cat(" DEM:", basename(x$dem_file), "\n") + cat(" Dimensions:", x$nrow, "x", x$ncol, "\n") + + if (!is.null(x$cell_size)) { + cat(" Cell size:", paste(round(x$cell_size, 2), collapse = " x "), "\n") + cat(" Units:", x$units, "\n") + } + + if (!is.null(x$stream_threshold)) { + cat(" Stream threshold:", x$stream_threshold, "\n") + } + if (!is.null(x$hru_data)) { + cat(" HRUs:", nrow(x$hru_data), "\n") + } + + invisible(x) +} + + +#' Detect Horizontal Units from CRS +#' @noRd +.detect_units <- function(crs_info) { + if (is.null(crs_info) || nrow(crs_info) == 0) { + return("unknown") + } + # Check if projected + if (!is.na(crs_info$code) && crs_info$code != "" && + !is.na(crs_info$authority) && crs_info$authority == "EPSG") { + # Projected CRS typically use meters + return("meters") + } + # Check WKT for unit info + if (!is.na(crs_info$name) && grepl("longlat|geographic|geodetic", + crs_info$name, ignore.case = TRUE)) { + return("degrees") + } + return("meters") +} diff --git a/rQSWATPlus/R/validate_database.R b/rQSWATPlus/R/validate_database.R new file mode 100644 index 0000000..e0b407e --- /dev/null +++ b/rQSWATPlus/R/validate_database.R @@ -0,0 +1,638 @@ +#' Check Database Compatibility with SWAT+ Editor +#' +#' Validates that a SQLite database produced by [qswat_write_database()] +#' is compatible with the SWAT+ Editor. +#' +#' @param db_file Character. Path to the SQLite database file. +#' @param verbose Logical. If `TRUE`, print detailed check results. +#' Default `TRUE`. +#' +#' @return A list of class `"qswat_db_check"` containing: +#' \describe{ +#' \item{passed}{Logical. `TRUE` if all checks passed.} +#' \item{checks}{A data frame with columns `check`, `status`, +#' and `message` describing each validation.} +#' \item{warnings}{Character vector of non-fatal warnings.} +#' \item{errors}{Character vector of fatal errors.} +#' } +#' +#' @details +#' The function performs the following checks against the SWAT+ Editor +#' requirements: +#' \enumerate{ +#' \item **Required tables** exist (gis_subbasins, gis_channels, +#' gis_lsus, gis_hrus, gis_routing) +#' \item **Required columns** are present in each table with correct +#' types +#' \item **Non-empty data** in mandatory tables +#' \item **Referential integrity**: HRU subbasins reference existing +#' subbasins, channel subbasins reference existing subbasins, +#' routing source/sink IDs exist in their respective tables +#' \item **Routing validity**: every routing chain terminates at an +#' outlet, routing percentages sum to 100 +#' \item **Completeness**: every subbasin has at least one HRU, +#' every subbasin has at least one channel +#' \item **Data quality**: areas are positive, no NULL values in +#' required fields +#' \item **Reference data**: SWAT+ parameter tables (plants_plt, +#' fertilizer_frt, etc.) from QSWATPlusRefHAWQS.sqlite are +#' present and non-empty +#' \item **HAWQS tables**: HAWQS-specific tables (plant_HAWQS, +#' urban_HAWQS, CDL landuse field tables, statsgo_ssurgo_lkey) +#' from QSWATPlusProjHAWQS.sqlite are present +#' \item **gwflow tables**: when `use_gwflow = 1` in project_config, +#' all twelve gwflow tables exist and the core tables +#' (gwflow_base, gwflow_zone, gwflow_grid, gwflow_solutes, +#' gwflow_rivcell) are populated; recharge-specific cell mapping +#' tables (gwflow_hrucell for HRU recharge, gwflow_lsucell for +#' LSU recharge) are non-empty according to the recharge mode +#' stored in gwflow_base +#' } +#' +#' @examples +#' \dontrun{ +#' result <- qswat_check_database("project.sqlite") +#' if (!result$passed) { +#' cat("Errors found:\n") +#' cat(result$errors, sep = "\n") +#' } +#' } +#' +#' @export +qswat_check_database <- function(db_file, verbose = TRUE) { + if (!file.exists(db_file)) { + stop("Database file not found: ", db_file, call. = FALSE) + } + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + checks <- list() + warnings_list <- character() + errors_list <- character() + + # Helper to record a check + record <- function(name, pass, msg) { + checks[[length(checks) + 1]] <<- data.frame( + check = name, + status = if (pass) "PASS" else "FAIL", + message = msg, + stringsAsFactors = FALSE + ) + if (!pass) errors_list[[length(errors_list) + 1]] <<- msg + } + + warn <- function(msg) { + warnings_list[[length(warnings_list) + 1]] <<- msg + } + + # ---- 1. Required tables ---- + existing_tables <- DBI::dbListTables(con) + + required_tables <- c("gis_subbasins", "gis_channels", "gis_lsus", + "gis_hrus", "gis_routing", "project_config") + + for (tbl in required_tables) { + ok <- tbl %in% existing_tables + record( + paste0("table_exists:", tbl), + ok, + if (ok) paste0("Table '", tbl, "' exists") + else paste0("Required table '", tbl, "' is missing") + ) + } + + # Optional GIS / config tables (warnings only) + optional_tables <- c( + # GIS tables + "gis_aquifers", "gis_deep_aquifers", "gis_water", + "gis_points", "gis_elevationbands", + "gis_landexempt", "gis_splithrus", + # Intermediate data + "BASINSDATA", "HRUSDATA", "LSUSDATA", "WATERDATA", + # Config tables + "config_delin", "config_hru", "config_landuse", + "config_lsu", "config_observed", "config_params", + "config_soil", + # Reference parameter tables (from QSWATPlusRefHAWQS.sqlite) + "plants_plt", "fertilizer_frt", "tillage_til", "pesticide_pst", + "pathogens_pth", "urban_urb", "septic_sep", "snow_sno", + "cntable_lum", "ovn_table_lum", "cons_prac_lum", + "harv_ops", "fire_ops", "irr_ops", "sweep_ops", "chem_app_ops", + "graze_ops", + "bmpuser_str", "filterstrip_str", "grassedww_str", + "septic_str", "tiledrain_str", + "cal_parms_cal", "soils_lte_sol", "landuse_lum", + "management_sch", "management_sch_auto", "management_sch_op", + "codes_bsn", "parameters_bsn", + "plant_ini", "plant_ini_item", + "d_table_dtl", "d_table_dtl_cond", "d_table_dtl_cond_alt", + "d_table_dtl_act", "d_table_dtl_act_out", + "file_cio", "file_cio_classification", + "soil", "soil_layer", "wgn", "wgn_mon", + "NLCD_CDL_color_scheme", "tropical_bounds", "version", + # HAWQS-specific tables (from QSWATPlusProjHAWQS.sqlite) + "plant_HAWQS", "urban_HAWQS", + "statsgo_ssurgo_lkey", "statsgo_ssurgo_lkey1" + ) + # CDL landuse field tables (from QSWATPlusProjHAWQS.sqlite) + optional_tables <- c(optional_tables, + paste0("landuse_fields_CDL_", + sprintf("%02d", seq_len(18)))) + + for (tbl in optional_tables) { + if (!(tbl %in% existing_tables)) { + warn(paste0("Optional table '", tbl, "' is missing")) + } + } + + # Short-circuit if core tables are missing + if (!all(required_tables %in% existing_tables)) { + result <- .build_check_result(checks, warnings_list, errors_list) + if (verbose) .print_check_result(result) + return(invisible(result)) + } + + # ---- 2. Required columns ---- + expected_cols <- list( + gis_subbasins = c("id", "area", "slo1", "len1", "sll", "lat", "lon", + "elev", "elevmin", "elevmax", "waterid"), + gis_hrus = c("id", "lsu", "landuse", "soil", "slope", + "lat", "lon", "elev"), + gis_channels = c("id", "subbasin", "strahler", "midlat", "midlon"), + gis_lsus = c("id", "channel", "subbasin", "area", "slope", "len1", + "lat", "lon", "elev"), + gis_routing = c("sourceid", "sourcecat", "hyd_typ", "sinkid", + "sinkcat", "percent"), + project_config = c("id", "project_name", "delineation_done", + "hrus_done", "use_gwflow") + ) + + for (tbl in names(expected_cols)) { + cols <- DBI::dbListFields(con, tbl) + missing <- setdiff(expected_cols[[tbl]], cols) + ok <- length(missing) == 0 + record( + paste0("columns:", tbl), + ok, + if (ok) paste0("Table '", tbl, "' has all required columns") + else paste0("Table '", tbl, "' missing columns: ", + paste(missing, collapse = ", ")) + ) + } + + # ---- 3. Non-empty data ---- + for (tbl in required_tables) { + n <- DBI::dbGetQuery(con, paste("SELECT COUNT(*) AS n FROM", tbl))$n + ok <- n > 0 + record( + paste0("non_empty:", tbl), + ok, + if (ok) paste0("Table '", tbl, "' has ", n, " rows") + else paste0("Table '", tbl, "' is empty") + ) + } + + # ---- 4. Referential integrity ---- + subbasins <- DBI::dbGetQuery(con, "SELECT id FROM gis_subbasins")$id + hrus <- DBI::dbGetQuery(con, "SELECT * FROM gis_hrus") + channels <- DBI::dbGetQuery(con, "SELECT id, subbasin FROM gis_channels") + lsus <- DBI::dbGetQuery(con, "SELECT id, channel FROM gis_lsus") + routing <- DBI::dbGetQuery(con, "SELECT * FROM gis_routing") + + # HRU → LSU (lsu column references gis_lsus.id) + # In the rQSWATPlus schema, HRU.lsu maps to the subbasin ID which also + # serves as the LSU ID. We allow references to either gis_lsus.id or + # gis_subbasins.id since they share the same ID space. + if (nrow(hrus) > 0 && "lsu" %in% names(hrus) && nrow(lsus) > 0) { + orphan_hru <- hrus$lsu[!hrus$lsu %in% lsus$id] + # Also allow referencing subbasins directly + orphan_hru2 <- orphan_hru[!orphan_hru %in% subbasins] + ok <- length(orphan_hru2) == 0 + record( + "ref_integrity:hru_lsu", + ok, + if (ok) "All HRUs reference valid LSUs or subbasins" + else paste0(length(orphan_hru2), " HRU(s) reference non-existent ", + "LSU(s): ", + paste(unique(orphan_hru2), collapse = ", ")) + ) + } + + # Channel → subbasin + if (nrow(channels) > 0 && "subbasin" %in% names(channels)) { + orphan_ch <- channels$subbasin[!channels$subbasin %in% subbasins] + ok <- length(orphan_ch) == 0 + record( + "ref_integrity:channel_subbasin", + ok, + if (ok) "All channels reference valid subbasins" + else paste0(length(orphan_ch), " channel(s) reference non-existent ", + "subbasin(s): ", + paste(unique(orphan_ch), collapse = ", ")) + ) + } + + # LSU → channel + if (nrow(lsus) > 0 && "channel" %in% names(lsus)) { + ch_ids <- channels$id + orphan_lsu <- lsus$channel[!lsus$channel %in% ch_ids] + # LSU.channel may point to subbasin rather than channel id + # Allow either channel IDs or subbasin IDs + orphan_lsu2 <- orphan_lsu[!orphan_lsu %in% subbasins] + ok <- length(orphan_lsu2) == 0 + record( + "ref_integrity:lsu_channel", + ok, + if (ok) "All LSUs reference valid channels or subbasins" + else paste0(length(orphan_lsu2), " LSU(s) reference non-existent ", + "channel/subbasin: ", + paste(unique(orphan_lsu2), collapse = ", ")) + ) + } + + # ---- 5. Routing validity ---- + if (nrow(routing) > 0) { + # 5a. At least one outlet + outlet_rows <- routing$sinkcat %in% c("outlet", "X", "x") + has_outlet <- any(outlet_rows) + record( + "routing:has_outlet", + has_outlet, + if (has_outlet) + paste0("Routing has ", sum(outlet_rows), " outlet route(s)") + else "No outlet found in routing table" + ) + + # 5b. Routing percentages sum to ~100 per source + if ("percent" %in% names(routing)) { + pct_tolerance <- 1 # allow 1% rounding error + pct_sums <- stats::aggregate( + percent ~ sourceid + sourcecat, + data = routing, FUN = sum + ) + bad_pct <- pct_sums[abs(pct_sums$percent - 100) > pct_tolerance, ] + ok <- nrow(bad_pct) == 0 + record( + "routing:percent_sum", + ok, + if (ok) + "All routing percentages sum to 100%" + else paste0(nrow(bad_pct), " source(s) have routing percentages ", + "that do not sum to 100%") + ) + } + + # 5c. No routing loops (simple cycle detection) + loop_found <- .detect_routing_loop(routing) + record( + "routing:no_loops", + !loop_found, + if (!loop_found) "No routing loops detected" + else "Circular routing loop detected in routing table" + ) + } + + # ---- 6. Completeness ---- + # Every subbasin should have >= 1 HRU (via LSU) + if (nrow(hrus) > 0 && length(subbasins) > 0 && "lsu" %in% names(hrus)) { + # HRU.lsu → LSU.id → LSU.channel → subbasin + hru_subs <- unique(hrus$lsu) + # LSUs link to subbasins via channel field + if (nrow(lsus) > 0) { + lsu_subs <- unique(lsus$channel[lsus$id %in% hru_subs]) + hru_subs <- unique(c(hru_subs, lsu_subs)) + } + missing_hru <- subbasins[!subbasins %in% hru_subs] + ok <- length(missing_hru) == 0 + record( + "completeness:subbasin_hrus", + ok, + if (ok) "Every subbasin has at least one HRU" + else paste0(length(missing_hru), " subbasin(s) have no HRUs: ", + paste(missing_hru, collapse = ", ")) + ) + } + + # Every subbasin should have >= 1 channel + if (nrow(channels) > 0 && length(subbasins) > 0 && + "subbasin" %in% names(channels)) { + subs_with_ch <- unique(channels$subbasin) + missing_ch <- subbasins[!subbasins %in% subs_with_ch] + ok <- length(missing_ch) == 0 + record( + "completeness:subbasin_channels", + ok, + if (ok) "Every subbasin has at least one channel" + else paste0(length(missing_ch), " subbasin(s) have no channels: ", + paste(missing_ch, collapse = ", ")) + ) + } + + # ---- 7. Data quality ---- + # Positive areas + if (nrow(hrus) > 0 && "area" %in% names(hrus)) { + neg_area <- sum(hrus$area <= 0, na.rm = TRUE) + ok <- neg_area == 0 + record( + "quality:hru_areas", + ok, + if (ok) "All HRU areas are positive" + else paste0(neg_area, " HRU(s) have zero or negative area") + ) + } + + if (length(subbasins) > 0) { + sub_data <- DBI::dbGetQuery(con, "SELECT * FROM gis_subbasins") + neg_sub <- sum(sub_data$area <= 0, na.rm = TRUE) + ok <- neg_sub == 0 + record( + "quality:subbasin_areas", + ok, + if (ok) "All subbasin areas are positive" + else paste0(neg_sub, " subbasin(s) have zero or negative area") + ) + } + + # No NULL IDs in key tables + null_ids <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gis_hrus WHERE id IS NULL" + )$n + ok <- null_ids == 0 + record( + "quality:no_null_ids", + ok, + if (ok) "No NULL IDs in gis_hrus" + else paste0(null_ids, " HRU row(s) have NULL id") + ) + + # ---- 8. Reference data (from QSWATPlusRefHAWQS.sqlite) ---- + # These tables should have data when they exist; warn if empty. + ref_data_tables <- c( + "plants_plt", "fertilizer_frt", "tillage_til", "pesticide_pst", + "urban_urb", "cal_parms_cal", "landuse_lum", + "cntable_lum", "ovn_table_lum", "cons_prac_lum", + "harv_ops", "graze_ops", "soils_lte_sol", + "d_table_dtl", "file_cio_classification" + ) + for (tbl in ref_data_tables) { + if (!(tbl %in% existing_tables)) next + n <- tryCatch( + DBI::dbGetQuery(con, paste("SELECT COUNT(*) AS n FROM", tbl))$n, + error = function(e) NA_integer_ + ) + if (!is.na(n) && n == 0L) { + warn(paste0("Reference table '", tbl, "' exists but has no data")) + } + } + + # ---- 9. HAWQS-specific tables (from QSWATPlusProjHAWQS.sqlite) ---- + hawqs_tables <- c( + "plant_HAWQS", "urban_HAWQS", + "statsgo_ssurgo_lkey", "statsgo_ssurgo_lkey1" + ) + hawqs_tables <- c(hawqs_tables, + paste0("landuse_fields_CDL_", + sprintf("%02d", seq_len(18)))) + missing_hawqs <- hawqs_tables[!hawqs_tables %in% existing_tables] + if (length(missing_hawqs) > 0) { + warn(paste0(length(missing_hawqs), " HAWQS-specific table(s) are ", + "missing (install QSWATPlusProjHAWQS.sqlite to enable): ", + paste(missing_hawqs, collapse = ", "))) + } + + # ---- 10. gwflow tables (if gwflow is enabled) ---- + use_gwflow <- tryCatch({ + pc <- DBI::dbGetQuery( + con, "SELECT use_gwflow FROM project_config LIMIT 1" + ) + if (nrow(pc) > 0 && !is.na(pc$use_gwflow)) as.integer(pc$use_gwflow) + else 0L + }, error = function(e) 0L) + + if (use_gwflow == 1L) { + gwflow_all_tables <- c( + "gwflow_base", "gwflow_zone", "gwflow_grid", + "gwflow_out_days", "gwflow_obs_locs", "gwflow_solutes", + "gwflow_init_conc", "gwflow_hrucell", "gwflow_fpcell", + "gwflow_rivcell", "gwflow_lsucell", "gwflow_rescell" + ) + missing_gw <- gwflow_all_tables[!gwflow_all_tables %in% existing_tables] + ok <- length(missing_gw) == 0 + record( + "gwflow:tables_exist", + ok, + if (ok) "All gwflow tables exist" + else paste0("gwflow is enabled but ", length(missing_gw), + " table(s) are missing: ", + paste(missing_gw, collapse = ", ")) + ) + + if (ok) { + # gwflow_base: exactly 1 row + n_base <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gwflow_base" + )$n + ok_base <- n_base == 1L + record( + "gwflow:base_populated", + ok_base, + if (ok_base) "gwflow_base has 1 configuration row" + else paste0("gwflow_base must have exactly 1 row (found ", n_base, ")") + ) + + # gwflow_zone: at least 1 row + n_zone <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gwflow_zone" + )$n + ok_zone <- n_zone > 0 + record( + "gwflow:zone_populated", + ok_zone, + if (ok_zone) paste0("gwflow_zone has ", n_zone, " zone(s)") + else "gwflow_zone is empty (no aquifer zones defined)" + ) + + # gwflow_grid: at least 1 row + n_grid <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gwflow_grid" + )$n + ok_grid <- n_grid > 0 + record( + "gwflow:grid_populated", + ok_grid, + if (ok_grid) paste0("gwflow_grid has ", n_grid, " cell(s)") + else "gwflow_grid is empty (no grid cells defined)" + ) + + # gwflow_solutes: at least 1 row (Python writes 10 default solutes) + n_sol <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gwflow_solutes" + )$n + ok_sol <- n_sol > 0 + record( + "gwflow:solutes_populated", + ok_sol, + if (ok_sol) paste0("gwflow_solutes has ", n_sol, " solute(s)") + else "gwflow_solutes is empty (no solutes defined)" + ) + + # gwflow_rivcell: at least 1 row + n_riv <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gwflow_rivcell" + )$n + ok_riv <- n_riv > 0 + record( + "gwflow:rivcell_populated", + ok_riv, + if (ok_riv) paste0("gwflow_rivcell has ", n_riv, " river cell(s)") + else "gwflow_rivcell is empty (no river-cell connections defined)" + ) + + # Recharge-specific checks (mirrors gwflow.py HRUorLSU_recharge logic): + # recharge = 1 → HRU-cell only → gwflow_hrucell must be populated + # recharge = 2 → LSU-cell only → gwflow_lsucell must be populated + # recharge = 3 → both HRU + LSU → both must be populated + # Only evaluate when gwflow_base has exactly 1 row (ok_base is TRUE); + # otherwise the table is already flagged as empty above. + if (ok_base) { + recharge <- as.integer( + DBI::dbGetQuery(con, + "SELECT recharge FROM gwflow_base LIMIT 1" + )$recharge + ) + + hru_recharge <- recharge == 1L || recharge == 3L + lsu_recharge <- recharge == 2L || recharge == 3L + + if (hru_recharge) { + n_hru <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gwflow_hrucell" + )$n + ok_hru <- n_hru > 0 + record( + "gwflow:hrucell_populated", + ok_hru, + if (ok_hru) paste0("gwflow_hrucell has ", n_hru, + " HRU-cell mapping(s)") + else paste0("gwflow_hrucell is empty but recharge mode (", recharge, + ") requires HRU-cell connections") + ) + } + + if (lsu_recharge) { + n_lsu <- DBI::dbGetQuery( + con, "SELECT COUNT(*) AS n FROM gwflow_lsucell" + )$n + ok_lsu <- n_lsu > 0 + record( + "gwflow:lsucell_populated", + ok_lsu, + if (ok_lsu) paste0("gwflow_lsucell has ", n_lsu, + " LSU-cell mapping(s)") + else paste0("gwflow_lsucell is empty but recharge mode (", recharge, + ") requires LSU-cell connections") + ) + } + } + } + } + + result <- .build_check_result(checks, warnings_list, errors_list) + if (verbose) .print_check_result(result) + return(invisible(result)) +} + + +#' Detect Routing Loops +#' +#' Simple cycle detection: follow each source through sinks and check +#' if we revisit a node before reaching an outlet. +#' @noRd +.detect_routing_loop <- function(routing) { + # Build adjacency: source → sinks + outlet_cats <- c("outlet", "X", "x") + sources <- unique(paste(routing$sourceid, routing$sourcecat, sep = ":")) + + for (src in sources) { + visited <- character() + current <- src + steps <- 0L + # A path can visit at most N unique nodes before repeating (cycle) + max_steps <- nrow(routing) + 1L + + while (steps < max_steps) { + if (current %in% visited) return(TRUE) # loop + visited <- c(visited, current) + parts <- strsplit(current, ":")[[1]] + sid <- as.integer(parts[1]) + scat <- parts[2] + # Find sink for this source + row <- routing[routing$sourceid == sid & routing$sourcecat == scat, ] + if (nrow(row) == 0) break # no further routing + sink_cat <- row$sinkcat[1] + if (sink_cat %in% outlet_cats) break # reached outlet + current <- paste(row$sinkid[1], sink_cat, sep = ":") + steps <- steps + 1L + } + } + return(FALSE) +} + + +#' Build Check Result Object +#' @noRd +.build_check_result <- function(checks, warnings_list, errors_list) { + checks_df <- do.call(rbind, checks) + result <- list( + passed = length(errors_list) == 0, + checks = checks_df, + warnings = unlist(warnings_list), + errors = unlist(errors_list) + ) + class(result) <- "qswat_db_check" + result +} + + +#' Print Check Results +#' @noRd +.print_check_result <- function(x) { + n_pass <- sum(x$checks$status == "PASS") + n_fail <- sum(x$checks$status == "FAIL") + n_warn <- length(x$warnings) + + cat("SWAT+ Editor Database Compatibility Check\n") + cat("==========================================\n") + + for (i in seq_len(nrow(x$checks))) { + icon <- if (x$checks$status[i] == "PASS") "\u2714" else "\u2718" + cat(sprintf(" %s %s\n", icon, x$checks$message[i])) + } + + if (n_warn > 0) { + cat("\nWarnings:\n") + for (w in x$warnings) cat(sprintf(" ! %s\n", w)) + } + + cat(sprintf("\nResult: %d passed, %d failed, %d warnings\n", + n_pass, n_fail, n_warn)) + + if (x$passed) { + cat("Database is compatible with SWAT+ Editor.\n") + } else { + cat("Database has compatibility issues. Fix errors above.\n") + } +} + + +#' Print Method for Database Check Results +#' +#' @param x A `qswat_db_check` object. +#' @param ... Additional arguments (ignored). +#' @return Invisibly returns `x`. +#' @export +print.qswat_db_check <- function(x, ...) { + .print_check_result(x) + invisible(x) +} diff --git a/rQSWATPlus/R/visualization.R b/rQSWATPlus/R/visualization.R new file mode 100644 index 0000000..cb150b6 --- /dev/null +++ b/rQSWATPlus/R/visualization.R @@ -0,0 +1,716 @@ +#' Plot DEM Elevation Map +#' +#' Creates a spatial map of the Digital Elevation Model using `tmap`. +#' +#' @param project A `qswat_project` object. +#' @param title Character. Map title. Default `"DEM Elevation"`. +#' @param palette Character. Colour palette name passed to tmap. +#' Default `"terrain"`. +#' @param ... Additional arguments passed to `tmap::tm_raster()`. +#' +#' @return A `tmap` object that can be printed or further customised. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_setup(...) +#' qswat_plot_dem(project) +#' } +#' +#' @export +qswat_plot_dem <- function(project, title = "DEM Elevation", + palette = "terrain", ...) { + .check_tmap() + dem <- terra::rast(project$dem_file) + tmap::tm_shape(dem) + + tmap::tm_raster( + col.scale = tmap::tm_scale_continuous(values = palette), + col.legend = tmap::tm_legend(title = "Elevation (m)", + position = tmap::tm_pos_out("right", + "center")) + ) + + tmap::tm_title(text = title) +} + + +#' Plot Land Use Map +#' +#' Creates a spatial map of the land use raster with categorical +#' labels from the lookup table. +#' +#' @param project A `qswat_project` object. +#' @param landuse_lookup Data frame from [qswat_read_landuse_lookup()], +#' or `NULL` to read from the project. +#' @param title Character. Map title. +#' @param ... Additional arguments passed to `tmap::tm_raster()`. +#' +#' @return A `tmap` object. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_setup(...) +#' qswat_plot_landuse(project) +#' } +#' +#' @export +qswat_plot_landuse <- function(project, landuse_lookup = NULL, + title = "Land Use", ...) { + .check_tmap() + lu_rast <- terra::rast(project$landuse_file) + + if (is.null(landuse_lookup)) { + landuse_lookup <- qswat_read_landuse_lookup(project$landuse_lookup) + } + + # Create a categorical raster with labels + lu_vals <- terra::values(lu_rast, mat = FALSE) + labels <- .map_lookup(lu_vals, landuse_lookup$value, landuse_lookup$landuse) + label_rast <- terra::rast(lu_rast) + terra::values(label_rast) <- as.integer(factor(labels)) + levels(label_rast) <- data.frame( + id = seq_along(levels(factor(labels))), + landuse = levels(factor(labels)) + ) + + tmap::tm_shape(label_rast) + + tmap::tm_raster(col.scale = tmap::tm_scale_categorical(), + col.legend = tmap::tm_legend(title = "Land Use", + position = tmap::tm_pos_out("right", + "center"))) +} + + +#' Plot Soil Map +#' +#' Creates a spatial map of the soil raster with categorical labels +#' from the lookup table. +#' +#' @param project A `qswat_project` object. +#' @param soil_lookup Data frame from [qswat_read_soil_lookup()], +#' or `NULL` to read from the project. +#' @param title Character. Map title. +#' @param ... Additional arguments passed to `tmap::tm_raster()`. +#' +#' @return A `tmap` object. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_setup(...) +#' qswat_plot_soil(project) +#' } +#' +#' @export +qswat_plot_soil <- function(project, soil_lookup = NULL, + title = "Soil Types", ...) { + .check_tmap() + soil_rast <- terra::rast(project$soil_file) + + if (is.null(soil_lookup)) { + soil_lookup <- qswat_read_soil_lookup(project$soil_lookup) + } + + soil_vals <- terra::values(soil_rast, mat = FALSE) + labels <- .map_lookup(soil_vals, soil_lookup$value, soil_lookup$soil) + label_rast <- terra::rast(soil_rast) + terra::values(label_rast) <- as.integer(factor(labels)) + levels(label_rast) <- data.frame( + id = seq_along(levels(factor(labels))), + soil = levels(factor(labels)) + ) + + tmap::tm_shape(label_rast) + + tmap::tm_raster(col.scale = tmap::tm_scale_categorical(), + col.legend = tmap::tm_legend(title = "Soil", + position = tmap::tm_pos_out("right", + "center"))) +} + + +#' Plot Stream Network +#' +#' Creates a spatial map of the stream network overlaid on the DEM +#' or watershed raster. +#' +#' @param project A `qswat_project` object that has been through +#' [qswat_delineate()] and [qswat_create_streams()]. +#' @param show_dem Logical. If `TRUE`, display the DEM as background. +#' Default `TRUE`. +#' @param title Character. Map title. +#' @param ... Additional arguments passed to `tmap::tm_lines()`. +#' +#' @return A `tmap` object. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_delineate(project, threshold = 500) +#' project <- qswat_create_streams(project) +#' qswat_plot_streams(project) +#' } +#' +#' @export +qswat_plot_streams <- function(project, show_dem = TRUE, + title = "Stream Network", ...) { + .check_tmap() + + if (is.null(project$streams_sf)) { + stop("Stream data not available. Run qswat_create_streams() first.", + call. = FALSE) + } + + m <- if (show_dem) { + dem <- terra::rast(project$dem_file) + tmap::tm_shape(dem) + + # tmap::tm_raster(palette = "Greys", title = "Elevation (m)", + # alpha = 0.6) + tmap::tm_raster( + col.scale = tmap::tm_scale_continuous(values = "brewer.greys"), + col_alpha = 0.6, + col.legend = tmap::tm_legend(title = "Elevation (m)", + position = tmap::tm_pos_out("right", + "center"))) + } else { + NULL + } + + m <- m + + tmap::tm_shape(project$streams_sf) + + tmap::tm_lines(col = "blue", lwd = 2) + + tmap::tm_title(text = title) + m +} + + +#' Plot Watershed / Subbasins +#' +#' Creates a spatial map of the delineated watershed showing +#' subbasin boundaries. +#' +#' @param project A `qswat_project` object that has been through +#' [qswat_delineate()]. +#' @param title Character. Map title. +#' @param ... Additional arguments passed to `tmap::tm_raster()`. +#' +#' @return A `tmap` object. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_delineate(project, threshold = 500) +#' qswat_plot_watershed(project) +#' } +#' +#' @export +qswat_plot_watershed <- function(project, + title = "Watershed Subbasins", ...) { + .check_tmap() + + if (is.null(project$watershed_file) || + !file.exists(project$watershed_file)) { + stop("Watershed raster not available. Run qswat_delineate() first.", + call. = FALSE) + } + + wshed <- terra::rast(project$watershed_file) + tmap::tm_shape(wshed) + + tmap::tm_raster( + col.scale = tmap::tm_scale_categorical(values = "brewer.set3"), + col.legend = tmap::tm_legend(title = "Subbasin", + position = tmap::tm_pos_out("right", + "center")) + ) + + tmap::tm_title(text = title) +} + + +#' Summary Bar Chart of Land Use Distribution +#' +#' Creates a bar chart showing the area or percentage of each land use +#' type across the watershed, using `ggplot2`. +#' +#' @param project A `qswat_project` object with HRU data from +#' [qswat_create_hrus()]. +#' @param type Character. One of `"area"` (hectares) or `"percent"`. +#' Default `"percent"`. +#' @param title Character. Plot title. +#' +#' @return A `ggplot` object. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +#' qswat_plot_landuse_summary(project) +#' } +#' +#' @export +qswat_plot_landuse_summary <- function(project, type = "percent", + title = "Land Use Distribution") { + .check_ggplot2() + .check_hru_data(project) + + hru <- project$hru_data + lu_area <- stats::aggregate(area_ha ~ landuse, data = hru, FUN = sum) + total <- sum(lu_area$area_ha) + lu_area$pct <- lu_area$area_ha / total * 100 + lu_area <- lu_area[order(-lu_area$area_ha), ] + lu_area$landuse <- factor(lu_area$landuse, + levels = lu_area$landuse) + + yvar <- if (type == "percent") "pct" else "area_ha" + ylab <- if (type == "percent") "Watershed Area (%)" else "Area (ha)" + + ggplot2::ggplot(lu_area, ggplot2::aes(x = .data$landuse, + y = .data[[yvar]])) + + ggplot2::geom_col(fill = "forestgreen", colour = "grey30") + + ggplot2::labs(title = title, x = "Land Use", y = ylab) + + ggplot2::theme_minimal() + + ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, + hjust = 1)) +} + + +#' Summary Bar Chart of Soil Distribution +#' +#' Creates a bar chart showing the area or percentage of each soil +#' type across the watershed, using `ggplot2`. +#' +#' @param project A `qswat_project` object with HRU data from +#' [qswat_create_hrus()]. +#' @param type Character. One of `"area"` (hectares) or `"percent"`. +#' Default `"percent"`. +#' @param title Character. Plot title. +#' +#' @return A `ggplot` object. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +#' qswat_plot_soil_summary(project) +#' } +#' +#' @export +qswat_plot_soil_summary <- function(project, type = "percent", + title = "Soil Distribution") { + .check_ggplot2() + .check_hru_data(project) + + hru <- project$hru_data + soil_area <- stats::aggregate(area_ha ~ soil, data = hru, FUN = sum) + total <- sum(soil_area$area_ha) + soil_area$pct <- soil_area$area_ha / total * 100 + soil_area <- soil_area[order(-soil_area$area_ha), ] + soil_area$soil <- factor(soil_area$soil, levels = soil_area$soil) + + yvar <- if (type == "percent") "pct" else "area_ha" + ylab <- if (type == "percent") "Watershed Area (%)" else "Area (ha)" + + ggplot2::ggplot(soil_area, ggplot2::aes(x = .data$soil, + y = .data[[yvar]])) + + ggplot2::geom_col(fill = "sienna", colour = "grey30") + + ggplot2::labs(title = title, x = "Soil", y = ylab) + + ggplot2::theme_minimal() + + ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, + hjust = 1)) +} + + +#' Summary Plot of HRU Breakdown +#' +#' Creates a multi-panel summary of HRU composition showing the +#' distribution of land use, soil, and slope classes, and optionally +#' a per-subbasin breakdown. +#' +#' @param project A `qswat_project` object with HRU data from +#' [qswat_create_hrus()]. +#' @param by_subbasin Logical. If `TRUE`, show a stacked bar chart of +#' HRU composition per subbasin. Default `FALSE`. +#' @param title Character. Overall plot title. +#' +#' @return A `ggplot` object. When `by_subbasin = FALSE` a faceted +#' plot of land use, soil, and slope breakdowns is returned. +#' When `by_subbasin = TRUE` a stacked bar chart per subbasin is +#' returned. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +#' qswat_plot_hru_summary(project) +#' qswat_plot_hru_summary(project, by_subbasin = TRUE) +#' } +#' +#' @export +qswat_plot_hru_summary <- function(project, by_subbasin = FALSE, + title = "HRU Composition") { + .check_ggplot2() + .check_hru_data(project) + + hru <- project$hru_data + + if (by_subbasin) { + # Stacked bar chart: area by landuse per subbasin + hru$subbasin_f <- factor(hru$subbasin) + ggplot2::ggplot(hru, ggplot2::aes(x = .data$subbasin_f, + y = .data$area_ha, + fill = .data$landuse)) + + ggplot2::geom_col() + + ggplot2::labs(title = title, x = "Subbasin", y = "Area (ha)", + fill = "Land Use") + + ggplot2::theme_minimal() + } else { + # Faceted summary: landuse, soil, slope + lu <- stats::aggregate(area_ha ~ landuse, data = hru, FUN = sum) + lu$category <- "Land Use" + names(lu) <- c("class", "area_ha", "category") + + sl <- stats::aggregate(area_ha ~ soil, data = hru, FUN = sum) + sl$category <- "Soil" + names(sl) <- c("class", "area_ha", "category") + + slope_classes <- project$slope_classes + slp <- stats::aggregate(area_ha ~ slope_class, data = hru, FUN = sum) + if (!is.null(slope_classes) && nrow(slope_classes) > 0) { + slp$slope_class <- slope_classes$label[ + match(slp$slope_class, slope_classes$class_id)] + } + slp$category <- "Slope Class" + names(slp) <- c("class", "area_ha", "category") + + combined <- rbind(lu, sl, slp) + combined$category <- factor(combined$category, + levels = c("Land Use", "Soil", + "Slope Class")) + + ggplot2::ggplot(combined, ggplot2::aes(x = .data$class, + y = .data$area_ha)) + + ggplot2::geom_col(fill = "steelblue", colour = "grey30") + + ggplot2::facet_wrap(~ category, scales = "free_x") + + ggplot2::labs(title = title, x = NULL, y = "Area (ha)") + + ggplot2::theme_minimal() + + ggplot2::theme(axis.text.x = ggplot2::element_text(angle = 45, + hjust = 1)) + } +} + + +#' Plot HRU Map +#' +#' Creates a spatial map of HRU polygons, coloured by a chosen attribute. +#' Requires [qswat_create_hrus()] to have been run. +#' +#' @param project A `qswat_project` object. +#' @param color_by Character. Column in `project$hru_sf` used to colour the +#' polygons. Typical choices: `"landuse"`, `"soil"`, `"slope_class"`, +#' `"hru_id"`. Default `"landuse"`. +#' @param title Character. Map title. +#' +#' @return A `tmap` object. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +#' qswat_plot_hrus(project) +#' qswat_plot_hrus(project, color_by = "soil") +#' } +#' +#' @export +qswat_plot_hrus <- function(project, color_by = "landuse", + title = "HRUs") { + .check_tmap() + + if (is.null(project$hru_sf)) { + stop("HRU spatial data not available. Run qswat_create_hrus() first.", + call. = FALSE) + } + if (!color_by %in% names(project$hru_sf)) { + stop("Column '", color_by, "' not found in project$hru_sf.", + call. = FALSE) + } + + tmap::tm_shape(project$hru_sf) + + tmap::tm_polygons( + fill = color_by, + fill.scale = tmap::tm_scale_categorical(), + col = "grey30", + lwd = 0.5, + fill.legend = tmap::tm_legend( + title = color_by, + position = tmap::tm_pos_out("right", "center") + ) + ) + + tmap::tm_title(text = title) +} + + +#' Plot LSU (Landscape Unit / Subbasin) Map +#' +#' Creates a spatial map of Landscape Units (LSUs). In rQSWATPlus, LSUs and +#' subbasins are in a 1:1 relationship; each polygon represents one subbasin / +#' LSU. Requires [qswat_create_hrus()] to have been run (or at least +#' [qswat_delineate()]). +#' +#' @param project A `qswat_project` object. +#' @param title Character. Map title. +#' +#' @return A `tmap` object. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +#' qswat_plot_lsus(project) +#' } +#' +#' @export +qswat_plot_lsus <- function(project, title = "Landscape Units (LSUs)") { + .check_tmap() + + lsu_sf <- project$lsu_sf + if (is.null(lsu_sf)) { + if (is.null(project$watershed_file) || + !file.exists(project$watershed_file)) { + stop("LSU data not available. Run qswat_delineate() first.", + call. = FALSE) + } + wshed <- terra::rast(project$watershed_file) + lsu_sf <- .build_lsu_sf(wshed) + } + + tmap::tm_shape(lsu_sf) + + tmap::tm_polygons( + fill = "subbasin", + fill.scale = tmap::tm_scale_categorical(values = "brewer.set3"), + col = "black", + lwd = 1.5, + fill.legend = tmap::tm_legend( + title = "LSU / Subbasin", + position = tmap::tm_pos_out("right", "center") + ) + ) + + tmap::tm_title(text = title) +} + + +#' Plot Watershed Overview +#' +#' Creates a combined overlay map that can include any subset of: DEM, +#' land use, soil, LSU/subbasin polygons, HRU polygons, stream network, and +#' outlet points. +#' +#' @param project A `qswat_project` object. +#' @param layers Character vector of layers to include. Any combination of: +#' \describe{ +#' \item{`"dem"`}{DEM raster shown as a greyscale background.} +#' \item{`"landuse"`}{Land-use raster (semi-transparent categorical fill).} +#' \item{`"soil"`}{Soil raster (semi-transparent categorical fill).} +#' \item{`"lsu"`}{LSU / subbasin polygons coloured by subbasin ID.} +#' \item{`"hru"`}{HRU polygons coloured by land use.} +#' \item{`"streams"`}{Stream-network lines in blue.} +#' \item{`"outlets"`}{Outlet points in red.} +#' } +#' Default: `c("dem", "lsu", "hru", "streams", "outlets")`. +#' @param landuse_lookup Data frame from [qswat_read_landuse_lookup()], or +#' `NULL` to read from the project. Only used when `"landuse"` is in +#' `layers`. +#' @param soil_lookup Data frame from [qswat_read_soil_lookup()], or `NULL` +#' to read from the project. Only used when `"soil"` is in `layers`. +#' @param title Character. Map title. +#' +#' @return A `tmap` object. +#' +#' @examples +#' \dontrun{ +#' project <- qswat_run(...) +#' +#' # Default layers +#' qswat_plot_overview(project) +#' +#' # All available layers +#' qswat_plot_overview(project, +#' layers = c("dem", "landuse", "soil", "lsu", "hru", "streams", "outlets")) +#' } +#' +#' @export +qswat_plot_overview <- function(project, + layers = c("dem", "lsu", "hru", + "streams", "outlets"), + landuse_lookup = NULL, + soil_lookup = NULL, + title = "Watershed Overview") { + .check_tmap() + + m <- NULL + + # 1. DEM greyscale background ---------------------------------------- + if ("dem" %in% layers && + !is.null(project$dem_file) && file.exists(project$dem_file)) { + dem <- terra::rast(project$dem_file) + m <- tmap::tm_shape(dem) + + tmap::tm_raster( + col.scale = tmap::tm_scale_continuous(values = "brewer.greys"), + col_alpha = 0.7, + col.legend = tmap::tm_legend( + title = "Elevation (m)", + position = tmap::tm_pos_out("right", "center") + ) + ) + } + + # 2. Land-use raster (semi-transparent) -------------------------------- + if ("landuse" %in% layers && + !is.null(project$landuse_file) && file.exists(project$landuse_file)) { + if (is.null(landuse_lookup)) { + landuse_lookup <- qswat_read_landuse_lookup(project$landuse_lookup) + } + lu_rast <- terra::rast(project$landuse_file) + lu_vals <- terra::values(lu_rast, mat = FALSE) + lu_labels <- .map_lookup(lu_vals, landuse_lookup$value, + landuse_lookup$landuse) + lu_cat <- terra::rast(lu_rast) + terra::values(lu_cat) <- as.integer(factor(lu_labels)) + levels(lu_cat) <- data.frame( + id = seq_along(levels(factor(lu_labels))), + landuse = levels(factor(lu_labels)) + ) + m <- m + + tmap::tm_shape(lu_cat) + + tmap::tm_raster( + # col.scale / col.legend are the tmap v4 API for tm_raster (rasters + # use col; polygons use fill). + col.scale = tmap::tm_scale_categorical(values = "brewer.pastel1"), + col_alpha = 0.5, + col.legend = tmap::tm_legend( + title = "Land Use", + position = tmap::tm_pos_out("right", "center") + ) + ) + } + + # 3. Soil raster (semi-transparent) ------------------------------------ + if ("soil" %in% layers && + !is.null(project$soil_file) && file.exists(project$soil_file)) { + if (is.null(soil_lookup)) { + soil_lookup <- qswat_read_soil_lookup(project$soil_lookup) + } + soil_rast <- terra::rast(project$soil_file) + soil_vals <- terra::values(soil_rast, mat = FALSE) + soil_labels <- .map_lookup(soil_vals, soil_lookup$value, soil_lookup$soil) + soil_cat <- terra::rast(soil_rast) + terra::values(soil_cat) <- as.integer(factor(soil_labels)) + levels(soil_cat) <- data.frame( + id = seq_along(levels(factor(soil_labels))), + soil = levels(factor(soil_labels)) + ) + m <- m + + tmap::tm_shape(soil_cat) + + tmap::tm_raster( + # Use a different palette from landuse so both can be shown together. + col.scale = tmap::tm_scale_categorical(values = "brewer.pastel2"), + col_alpha = 0.5, + col.legend = tmap::tm_legend( + title = "Soil", + position = tmap::tm_pos_out("right", "center") + ) + ) + } + + # 4. LSU / subbasin polygons ------------------------------------------- + if ("lsu" %in% layers) { + lsu_sf <- project$lsu_sf + if (is.null(lsu_sf) && + !is.null(project$watershed_file) && + file.exists(project$watershed_file)) { + wshed <- terra::rast(project$watershed_file) + lsu_sf <- .build_lsu_sf(wshed) + } + if (!is.null(lsu_sf)) { + m <- m + + tmap::tm_shape(lsu_sf) + + tmap::tm_polygons( + fill = "subbasin", + fill.scale = tmap::tm_scale_categorical(values = "brewer.set3"), + fill_alpha = 0.3, + col = "black", + lwd = 1.5, + fill.legend = tmap::tm_legend( + title = "LSU / Subbasin", + position = tmap::tm_pos_out("right", "center") + ) + ) + } + } + + # 5. HRU polygons coloured by land use --------------------------------- + if ("hru" %in% layers && !is.null(project$hru_sf)) { + m <- m + + tmap::tm_shape(project$hru_sf) + + tmap::tm_polygons( + fill = "landuse", + fill.scale = tmap::tm_scale_categorical(), + fill_alpha = 0.6, + col = "grey40", + lwd = 0.4, + fill.legend = tmap::tm_legend( + title = "HRU Land Use", + position = tmap::tm_pos_out("right", "center") + ) + ) + } + + # 6. Stream network ---------------------------------------------------- + if ("streams" %in% layers && !is.null(project$streams_sf)) { + m <- m + + tmap::tm_shape(project$streams_sf) + + tmap::tm_lines(col = "blue", lwd = 1.5) + } + + # 7. Outlet points ----------------------------------------------------- + if ("outlets" %in% layers && + !is.null(project$outlet_file) && + file.exists(project$outlet_file)) { + outlets_sf <- sf::st_read(project$outlet_file, quiet = TRUE) + m <- m + + tmap::tm_shape(outlets_sf) + + tmap::tm_dots(fill = "red", size = 0.3) + } + + if (is.null(m)) { + stop("No valid layers could be drawn. ", + "Check that the requested layers exist in the project.", + call. = FALSE) + } + + m + tmap::tm_title(text = title) +} + + +#' Check tmap availability +#' @noRd +.check_tmap <- function() { + if (!requireNamespace("tmap", quietly = TRUE)) { + stop( + "Package 'tmap' is required for spatial map plots.\n", + "Install it with: install.packages('tmap')", + call. = FALSE + ) + } +} + +#' Check ggplot2 availability +#' @noRd +.check_ggplot2 <- function() { + if (!requireNamespace("ggplot2", quietly = TRUE)) { + stop( + "Package 'ggplot2' is required for summary plots.\n", + "Install it with: install.packages('ggplot2')", + call. = FALSE + ) + } +} + +#' Check HRU data exists in project +#' @noRd +.check_hru_data <- function(project) { + if (!inherits(project, "qswat_project")) { + stop("'project' must be a qswat_project object.", call. = FALSE) + } + if (is.null(project$hru_data) || nrow(project$hru_data) == 0) { + stop("No HRU data available. Run qswat_create_hrus() first.", + call. = FALSE) + } +} diff --git a/rQSWATPlus/R/workflow.R b/rQSWATPlus/R/workflow.R new file mode 100644 index 0000000..46d2d3a --- /dev/null +++ b/rQSWATPlus/R/workflow.R @@ -0,0 +1,187 @@ +#' Run Complete QSWATPlus Workflow +#' +#' Convenience function that runs the full QSWATPlus workflow in a single +#' call: project setup, watershed delineation, stream network creation, +#' HRU generation, and database output. +#' +#' @param project_dir Character. Path to the project directory. +#' @param dem_file Character. Path to the DEM raster file. +#' @param landuse_file Character. Path to the land use raster file. +#' @param soil_file Character. Path to the soil raster file. +#' @param landuse_lookup Character. Path to the land use lookup CSV. +#' @param soil_lookup Character. Path to the soil lookup CSV. +#' @param outlet_file Character or NULL. Optional outlet shapefile. +#' @param threshold Numeric or NULL. Stream threshold (cells). +#' If NULL, 1 percent of total cells is used. +#' @param channel_threshold Numeric or NULL. Channel threshold. +#' @param slope_breaks Numeric vector. Slope class boundaries in +#' percent. Default `c(0, 9999)` for a single class. +#' @param hru_method Character. HRU selection method. See +#' [qswat_create_hrus()] for the full description of each option. +#' Default `"filter_threshold"`. +#' @param threshold_type Character. `"percent"` (default) or `"area"`. +#' Controls whether `landuse_threshold`, `soil_threshold`, and +#' `slope_threshold` are percentages or absolute hectares. Only used +#' when `hru_method = "filter_threshold"`. +#' @param landuse_threshold Numeric. Min land use percent (or area ha). +#' Default 0. +#' @param soil_threshold Numeric. Min soil percent (or area ha). +#' Default 0. +#' @param slope_threshold Numeric. Min slope percent (or area ha). +#' Default 0. +#' @param area_threshold Numeric. Minimum HRU area in hectares used +#' when `hru_method = "filter_area"`. Default 0. +#' @param target_hrus Integer or NULL. Target number of HRUs per +#' subbasin when `hru_method = "target"`. Default NULL. +#' @param use_gwflow Logical. Enable gwflow groundwater modelling. +#' When `TRUE`, [qswat_setup_gwflow()] is called automatically after +#' [qswat_write_database()] to initialise the gwflow tables in the +#' project database. Default FALSE. +#' @param gwflow_config A named list of gwflow settings as returned by +#' [qswat_read_gwflow_config()]. Only used when `use_gwflow = TRUE`. +#' If `NULL` the bundled `gwflow.ini` defaults are used. +#' @param aquifer_thickness Numeric constant (aquifer thickness in metres, +#' default `20`) or a file path to a raster or polygon shapefile +#' containing aquifer thickness values. Passed to [qswat_setup_gwflow()] +#' when `use_gwflow = TRUE`. +#' @param conductivity_file Optional path to a polygon shapefile defining +#' aquifer hydraulic-conductivity zones. Passed to [qswat_setup_gwflow()] +#' when `use_gwflow = TRUE`. If `NULL` a single default zone is used. +#' @param default_aquifer_k Default hydraulic conductivity (m/day) for the +#' single zone created when `conductivity_file = NULL`. Default `1.0`. +#' @param use_aquifers Logical. Create SWAT+ aquifer objects. Default +#' TRUE. +#' @param n_processes Integer. Number of MPI processes. Default 1. +#' @param quiet Logical. Suppress output. Default FALSE. +#' @param db_file Character or NULL. Output database path. +#' @param usersoil Character or NULL. Soil physical properties dataset to +#' use when writing the project database. Passed to [qswat_setup()] and +#' stored in the project object. Accepted values are the same as for +#' [qswat_setup()]: `"FAO_usersoil"`, `"global_usersoil"`, a CSV file +#' path, or `NULL` (default, leaves `global_usersoil` empty). +#' +#' @return A `qswat_project` object with all results. +#' +#' @examples +#' \dontrun{ +#' dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") +#' landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") +#' soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") +#' lu_lookup <- system.file("extdata", "ravn_landuse.csv", +#' package = "rQSWATPlus") +#' soil_lookup <- system.file("extdata", "ravn_soil.csv", +#' package = "rQSWATPlus") +#' outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") +#' +#' project <- qswat_run( +#' project_dir = tempdir(), +#' dem_file = dem, +#' landuse_file = landuse, +#' soil_file = soil, +#' landuse_lookup = lu_lookup, +#' soil_lookup = soil_lookup, +#' outlet_file = outlet, +#' threshold = 100, +#' slope_breaks = c(0, 5, 15, 9999) +#' ) +#' } +#' +#' @export +qswat_run <- function(project_dir, + dem_file, + landuse_file, + soil_file, + landuse_lookup, + soil_lookup, + outlet_file = NULL, + threshold = NULL, + channel_threshold = NULL, + slope_breaks = c(0, 9999), + hru_method = "filter_threshold", + threshold_type = "percent", + landuse_threshold = 0, + soil_threshold = 0, + slope_threshold = 0, + area_threshold = 0, + target_hrus = NULL, + use_gwflow = FALSE, + gwflow_config = NULL, + aquifer_thickness = 20.0, + conductivity_file = NULL, + default_aquifer_k = 1.0, + use_aquifers = TRUE, + n_processes = 1L, + quiet = FALSE, + db_file = NULL, + usersoil = NULL) { + + # Step 1: Setup + if (!quiet) message("=== Step 1/5: Setting up project ===") + project <- qswat_setup( + project_dir = project_dir, + dem_file = dem_file, + landuse_file = landuse_file, + soil_file = soil_file, + landuse_lookup = landuse_lookup, + soil_lookup = soil_lookup, + outlet_file = outlet_file, + usersoil = usersoil, + overwrite = TRUE + ) + + # Step 2: Delineation + if (!quiet) message("\n=== Step 2/5: Running watershed delineation ===") + project <- qswat_delineate( + project = project, + threshold = threshold, + channel_threshold = channel_threshold, + n_processes = n_processes, + quiet = quiet + ) + + # Step 3: Stream network + if (!quiet) message("\n=== Step 3/5: Creating stream network ===") + project <- qswat_create_streams(project) + + # Step 4: HRU creation + if (!quiet) message("\n=== Step 4/5: Creating HRUs ===") + lu_lookup <- qswat_read_landuse_lookup(project$landuse_lookup) + soil_lkp <- qswat_read_soil_lookup(project$soil_lookup) + slopes <- qswat_create_slope_classes(slope_breaks) + + project <- qswat_create_hrus( + project = project, + landuse_lookup = lu_lookup, + soil_lookup = soil_lkp, + slope_classes = slopes, + hru_method = hru_method, + threshold_type = threshold_type, + landuse_threshold = landuse_threshold, + soil_threshold = soil_threshold, + slope_threshold = slope_threshold, + area_threshold = area_threshold, + target_hrus = target_hrus, + use_gwflow = use_gwflow, + use_aquifers = use_aquifers + ) + + # Step 5: Write database + if (!quiet) message("\n=== Step 5/5: Writing database ===") + project <- qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + # Optional Step 6: Set up gwflow tables + if (isTRUE(use_gwflow)) { + if (!quiet) message("\n=== Step 6/6: Setting up gwflow tables ===") + project <- qswat_setup_gwflow( + project, + gwflow_config = gwflow_config, + overwrite = TRUE, + aquifer_thickness = aquifer_thickness, + conductivity_file = conductivity_file, + default_aquifer_k = default_aquifer_k + ) + } + + if (!quiet) message("\n=== QSWATPlus workflow complete! ===") + return(project) +} diff --git a/rQSWATPlus/README.md b/rQSWATPlus/README.md new file mode 100644 index 0000000..238cfdf --- /dev/null +++ b/rQSWATPlus/README.md @@ -0,0 +1,114 @@ +# rQSWATPlus + +R interface to the [QSWATPlus](https://github.com/limnotrack/QSWATPlus) workflow for setting up [SWAT+](https://swat.tamu.edu/software/plus/) hydrological models. + +## Overview + +`rQSWATPlus` provides R functions to replicate the QSWATPlus QGIS plugin workflow for SWAT+ model setup. It handles: + +- **Watershed delineation** from DEMs using TauDEM (via [`traudem`](https://github.com/lucarraro/traudem/)) +- **HRU creation** from land use, soil, and slope overlays +- **Stream network** topology extraction +- **Database output** (SQLite) compatible with the SWAT+ Editor + +## Installation + +```r +# Install from GitHub +# install.packages("devtools") +devtools::install_github("limnotrack/QSWATPlus", subdir = "rQSWATPlus") +``` + +### Prerequisites + +TauDEM must be installed on your system. See [`traudem` installation guide](https://lucarraro.github.io/traudem/articles/taudem-installation.html). + +```r +# Check TauDEM installation +traudem::taudem_sitrep() +``` + +## Quick Start + +```r +library(rQSWATPlus) + +# Complete workflow in one call +project <- qswat_run( + project_dir = "my_project", + dem_file = "dem.tif", + landuse_file = "landuse.tif", + soil_file = "soil.tif", + landuse_lookup = "landuse_lookup.csv", + soil_lookup = "soil_lookup.csv", + outlet_file = "outlets.shp", + threshold = 500, + slope_breaks = c(0, 5, 15, 9999) +) +``` + +Or step by step: + +```r +# 1. Setup +project <- qswat_setup(project_dir, dem_file, landuse_file, + soil_file, landuse_lookup, soil_lookup) + +# 2. Delineate watershed +project <- qswat_delineate(project, threshold = 500) + +# 3. Extract stream network +project <- qswat_create_streams(project) + +# 4. Create HRUs +lu <- qswat_read_landuse_lookup("landuse_lookup.csv") +soil <- qswat_read_soil_lookup("soil_lookup.csv") +slopes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) +project <- qswat_create_hrus(project, lu, soil, slopes) + +# 5. Write database +qswat_write_database(project) +``` + +## Example Data + +The package includes example data from the Ravn watershed (Denmark): + +```r +dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") +landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") +soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") +lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") +soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") +outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") +``` + +## Input Data Requirements + +| Input | Format | Description | +|-------|--------|-------------| +| DEM | GeoTIFF | Digital elevation model (projected CRS) | +| Land use | GeoTIFF | Integer land use classification raster | +| Soil | GeoTIFF | Integer soil map unit raster | +| Land use lookup | CSV | Maps raster values → SWAT+ land use codes | +| Soil lookup | CSV | Maps raster values → soil names | +| Outlets | Shapefile | Optional watershed outlet points | + +See `vignette("data-requirements", package = "rQSWATPlus")` for detailed specifications. + +## Vignettes + +- `vignette("introduction")` - Getting started guide +- `vignette("data-requirements")` - Input data format specifications +- `vignette("workflow")` - Complete worked example with the Ravn dataset + +## Dependencies + +- [terra](https://rspatial.github.io/terra/) - Raster data handling +- [sf](https://r-spatial.github.io/sf/) - Vector/shapefile processing +- [RSQLite](https://rsqlite.r-dbi.org/) - SQLite database output +- [traudem](https://github.com/lucarraro/traudem/) - TauDEM interface + +## License + +GPL (>= 3) diff --git a/rQSWATPlus/inst/extdata/QSWATPlusProj.sqlite b/rQSWATPlus/inst/extdata/QSWATPlusProj.sqlite new file mode 100644 index 0000000..e601cc5 Binary files /dev/null and b/rQSWATPlus/inst/extdata/QSWATPlusProj.sqlite differ diff --git a/rQSWATPlus/inst/extdata/QSWATPlusProjHAWQS.sqlite b/rQSWATPlus/inst/extdata/QSWATPlusProjHAWQS.sqlite new file mode 100644 index 0000000..96e9ff7 Binary files /dev/null and b/rQSWATPlus/inst/extdata/QSWATPlusProjHAWQS.sqlite differ diff --git a/rQSWATPlus/inst/extdata/QSWATPlusRefHAWQS.sqlite b/rQSWATPlus/inst/extdata/QSWATPlusRefHAWQS.sqlite new file mode 100644 index 0000000..baf0abf Binary files /dev/null and b/rQSWATPlus/inst/extdata/QSWATPlusRefHAWQS.sqlite differ diff --git a/rQSWATPlus/inst/extdata/gwflow.ini b/rQSWATPlus/inst/extdata/gwflow.ini new file mode 100644 index 0000000..0d2464f --- /dev/null +++ b/rQSWATPlus/inst/extdata/gwflow.ini @@ -0,0 +1,80 @@ +# Initial settings for gwflow +[DEFAULT] +# Grid cell size (m) +cell_size = 200 +# Boundary conditions (1 = constant head; 2 = no flow) +Boundary_conditions = 1 +# Recharge connection type (HRU-cell = 1, LSU-cell = 2, both = 3) +HRUorLSU_recharge = 2 +# Groundwater --> soil transfer is simulated (0 = no; 1 = yes) +GW_soiltransfer = 1 +# Saturation excess flow is simulated (0 = no; 1 = yes) +Saturation_excess = 1 +# External groundwater pumping (0 = off, 1 = on) +ext_pumping = 0 +# Groundwater-reservoir exchange (0=off; 1=on) +reservoir_exchange = 1 +# Groundwater-wetland exchange (0=off; 1=on) +wetland_exchange = 1 +# Groundwater-floodplain exchange (0=off; 1=on) +floodplain_exchange = 1 +# Canal seepage to groundwater (0=off; 1=on) +canal_seepage = 0 +# Groundwater solute transport (0=off; 1=on) +solute_transport = 1 +# Recharge delay (days) between soil profile and water table +recharge_delay = 0.0 +# Groundwater ET extinction depth (m) +EXDP = 1.00 +# Water table depth (m) at start of simulation +WT_depth = 5 +# Vertical distance (m) of streambed below the DEM value +river_depth = 5.0 +# Depth (m) of tiles below ground surface +tile_depth = 1.22 +# Area (m2) of groundwater inflow (circumference*length) * flow length +tile_area = 50 +# Hydraulic conductivity (m/day) of the drain perimeter +tile_k = 5.0 +# Tile cell groups (flag: 0 = no; 1 = yes) +tile_groups = 0 +# Reservoir bed thickness (m) +resbed_thickness = 2.0 +# Reservoir bed conductivity +resbed_k = 9.99E-06 +# Wetland bottom material thickness (m) +wet_thickness = 0.25 +# Write flags (daily, annual, average annual) (0=off; 1=on) +daily_output = 1 +annual_output = 1 +aa_output = 1 +# Cell row for detailed sources/sink output (0 means not used) +row_det = 0 +# Cell column for detailed sources/sink output (0 means not used) +col_det = 0 +# Time step (days) to solve groundwater balance equation +timestep_balance = 1 +# Initial specific yield 'Sy' +init_sy = 0.2 +# Initial porosity 'n' +init_n = 0.25 +# Streambed hydraulic conductivity (m/day) +streambed_k = 0.005 +# Streambed thickness (m) +streambed_thick = 0.5 +# Initial NO3 concentration +init_NO3 = 3.0 +# Initial P concentration +init_P = 0.05 +# Number of tile cell groups +tile_groups_number = 1 +# First-order rate constant for denitrification (1/day) +denit_constant = -0.0001 +# Dispersion coefficient (m2/day) +disp_coef = 5.00 +# Sorption retardation coefficient for Nitrate +nit_sorp = 1.00 +# Sorption retardation coefficient for Phosphorus +pho_sorp = 1.00 +# Number of transport time steps per flow time step +transport_steps = 1 diff --git a/rQSWATPlus/inst/extdata/ravn_dem.tif b/rQSWATPlus/inst/extdata/ravn_dem.tif new file mode 100644 index 0000000..3add851 Binary files /dev/null and b/rQSWATPlus/inst/extdata/ravn_dem.tif differ diff --git a/rQSWATPlus/inst/extdata/ravn_landuse.csv b/rQSWATPlus/inst/extdata/ravn_landuse.csv new file mode 100644 index 0000000..b9c02bb --- /dev/null +++ b/rQSWATPlus/inst/extdata/ravn_landuse.csv @@ -0,0 +1,54 @@ +"Value","Landuse" +0000,PAST +1100,UTRN +1110,URHD +1120,URMD +1121,URHD +1122,URLD +1123,URLD +1210,UIDU +1221,UTRN +1222,UTRN +1223,UTRN +1224,UTRN +1226,UTRN +1228,UTRN +1229,UTRN +1240,URLD +1242,UTRN +1310,RNGB +1340,UIDU +1341,URLD +1420,URLD +1421,URLD +1422,URLD +2112,AGRL +2116,ORCD +2117,AGRL +2118,PAST +2119,PAST +2115,PAST +2222,ONIO +2300,PAST +2310,URLD +2430,PAST +3100,FRST +3110,FRSD +3120,FRSE +3130,FRST +3210,PAST +3220,RNGB +3250,RNGB +3310,RNGB +3330,RNGB +4110,WETN +4112,WETN +4120,WETN +4130,WETN +4210,WETN +5120,WATR +5121,WATR +5123,WATR +5126,WATR +5230,WATR +6000,PAST diff --git a/rQSWATPlus/inst/extdata/ravn_landuse.tif b/rQSWATPlus/inst/extdata/ravn_landuse.tif new file mode 100644 index 0000000..afa90b2 Binary files /dev/null and b/rQSWATPlus/inst/extdata/ravn_landuse.tif differ diff --git a/rQSWATPlus/inst/extdata/ravn_outlet.dbf b/rQSWATPlus/inst/extdata/ravn_outlet.dbf new file mode 100644 index 0000000..d9d321d Binary files /dev/null and b/rQSWATPlus/inst/extdata/ravn_outlet.dbf differ diff --git a/rQSWATPlus/inst/extdata/ravn_outlet.prj b/rQSWATPlus/inst/extdata/ravn_outlet.prj new file mode 100644 index 0000000..3359577 --- /dev/null +++ b/rQSWATPlus/inst/extdata/ravn_outlet.prj @@ -0,0 +1 @@ +PROJCS["ETRS_1989_ETRS-TM32",GEOGCS["GCS_ETRS_1989",DATUM["D_ETRS_1989",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",9.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]] \ No newline at end of file diff --git a/rQSWATPlus/inst/extdata/ravn_outlet.shp b/rQSWATPlus/inst/extdata/ravn_outlet.shp new file mode 100644 index 0000000..458fe96 Binary files /dev/null and b/rQSWATPlus/inst/extdata/ravn_outlet.shp differ diff --git a/rQSWATPlus/inst/extdata/ravn_outlet.shx b/rQSWATPlus/inst/extdata/ravn_outlet.shx new file mode 100644 index 0000000..1815901 Binary files /dev/null and b/rQSWATPlus/inst/extdata/ravn_outlet.shx differ diff --git a/rQSWATPlus/inst/extdata/ravn_soil.csv b/rQSWATPlus/inst/extdata/ravn_soil.csv new file mode 100644 index 0000000..5289732 --- /dev/null +++ b/rQSWATPlus/inst/extdata/ravn_soil.csv @@ -0,0 +1,49 @@ +VALUE,NAME +1011,DK1011 +1018,DK1018 +1021,DK1021 +1024,DK1024 +1026,DK1026 +1046,DK1046 +1047,DK1047 +1048,DK1048 +1067,DK1067 +2011,DK2011 +2014,DK2014 +2021,DK2021 +2024,DK2024 +2028,DK2028 +2044,DK2044 +2048,DK2048 +2211,DK2211 +2411,DK2411 +3011,DK3011 +3012,DK3012 +3017,DK3017 +3018,DK3018 +3028,DK3028 +3037,DK3037 +3038,DK3038 +3047,DK3047 +3048,DK3048 +3110,DK3110 +4016,DK4016 +4018,DK4018 +4027,DK4027 +4037,DK4037 +4038,DK4038 +4046,DK4046 +4047,DK4047 +4048,DK4048 +4067,DK4067 +5016,DK5016 +5018,DK5018 +5037,DK5037 +5038,DK5038 +5046,DK5046 +5047,DK5047 +5048,DK5048 +5067,DK5067 +5077,DK5077 +998,DK998 +999,DK999 diff --git a/rQSWATPlus/inst/extdata/ravn_soil.tif b/rQSWATPlus/inst/extdata/ravn_soil.tif new file mode 100644 index 0000000..e21d6d5 Binary files /dev/null and b/rQSWATPlus/inst/extdata/ravn_soil.tif differ diff --git a/rQSWATPlus/inst/extdata/ravn_usersoil.csv b/rQSWATPlus/inst/extdata/ravn_usersoil.csv new file mode 100644 index 0000000..9cec85e --- /dev/null +++ b/rQSWATPlus/inst/extdata/ravn_usersoil.csv @@ -0,0 +1,299 @@ +OBJECTID,MUID,SEQN,SNAM,S5ID,CMPPCT,NLAYERS,HYDGRP,SOL_ZMX,ANION_EXCL,SOL_CRK,TEXTURE,SOL_Z1,SOL_BD1,SOL_AWC1,SOL_K1,SOL_CBN1,CLAY1,SILT1,SAND1,ROCK1,SOL_ALB1,USLE_K1,SOL_EC1,SOL_Z2,SOL_BD2,SOL_AWC2,SOL_K2,SOL_CBN2,CLAY2,SILT2,SAND2,ROCK2,SOL_ALB2,USLE_K2,SOL_EC2,SOL_Z3,SOL_BD3,SOL_AWC3,SOL_K3,SOL_CBN3,CLAY3,SILT3,SAND3,ROCK3,SOL_ALB3,USLE_K3,SOL_EC3,SOL_Z4,SOL_BD4,SOL_AWC4,SOL_K4,SOL_CBN4,CLAY4,SILT4,SAND4,ROCK4,SOL_ALB4,USLE_K4,SOL_EC4,SOL_Z5,SOL_BD5,SOL_AWC5,SOL_K5,SOL_CBN5,CLAY5,SILT5,SAND5,ROCK5,SOL_ALB5,USLE_K5,SOL_EC5,SOL_Z6,SOL_BD6,SOL_AWC6,SOL_K6,SOL_CBN6,CLAY6,SILT6,SAND6,ROCK6,SOL_ALB6,USLE_K6,SOL_EC6,SOL_Z7,SOL_BD7,SOL_AWC7,SOL_K7,SOL_CBN7,CLAY7,SILT7,SAND7,ROCK7,SOL_ALB7,USLE_K7,SOL_EC7,SOL_Z8,SOL_BD8,SOL_AWC8,SOL_K8,SOL_CBN8,CLAY8,SILT8,SAND8,ROCK8,SOL_ALB8,USLE_K8,SOL_EC8,SOL_Z9,SOL_BD9,SOL_AWC9,SOL_K9,SOL_CBN9,CLAY9,SILT9,SAND9,ROCK9,SOL_ALB9,USLE_K9,SOL_EC9,SOL_Z10,SOL_BD10,SOL_AWC10,SOL_K10,SOL_CBN10,CLAY10,SILT10,SAND10,ROCK10,SOL_ALB10,USLE_K10,SOL_EC10,SOL_CAL1,SOL_CAL2,SOL_CAL3,SOL_CAL4,SOL_CAL5,SOL_CAL6,SOL_CAL7,SOL_CAL8,SOL_CAL9,SOL_CAL10,SOL_PH1,SOL_PH2,SOL_PH3,SOL_PH4,SOL_PH5,SOL_PH6,SOL_PH7,SOL_PH8,SOL_PH9,SOL_PH10 +7,VT030,1,MISSISQUOI,VT0040,12,4,A,1651,0.5,0.5,LS-LS-GR--COS-GR--COS,127,1.1,0.15,550,2.03,3.5,16.09,80.41,10.48,0.01,0.17,0,304.8,1.42,0.11,500,0.68,3.5,16.09,80.41,11.56,0.06,0.17,0,889,1.52,0.03,650,0.23,2.5,6.43,91.07,15.69,0.15,0.17,0,1651,1.52,0.02,500,0.08,2.5,6.43,91.07,30,0.2,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +8,VT030,9,WAREHAM,MA0025,4,4,C,1524,0.5,0.5,LFS-S-COS-COS,152.4,1.1,0.14,750,2.03,2,16.87,81.13,5.6,0.01,0.17,0,406.4,1.4,0.05,1050,0.58,1.5,1.53,96.97,7.02,0.08,0.17,0,914.4,1.5,0.04,900,0.29,1.5,6.49,92.01,7.48,0.13,0.17,0,1524,1.5,0.03,750,0.29,1,6.53,92.47,10,0.13,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +9,VT030,18,WEIDER,VT0111,3,3,B,1651,0.5,0.5,SIL-VFSL-S,152.4,1.33,0.18,15,1.45,10,57.09,32.91,3.83,0.01,0.32,0,635,1.38,0.13,85,0.48,10,25.63,64.37,3.97,0.09,0.43,0,1651,1.45,0.04,650,0.16,1.5,1.53,96.97,10,0.17,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +1,VT028,10,FARMINGTON,NY0094,3,3,C,482.6000061,0.5,0.5,L-L-UWB,203.2,1.25,0.16,33,2.33,18.5,38.54,42.96,10.29,0.01,0.32,0,457.2,1.35,0.13,23,0.78,18.5,38.54,42.96,17.44,0.05,0.32,0,482.6,2.5,0.01,600,0.26,5,25,70,98,0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +2,VT028,19,ST. ALBANS,VT0003,2,3,B,1651,0.5,0.5,SL-SY--SL-SY--LCOS,177.8,1.13,0.17,270,3.2,6.5,30.37,63.13,11.75,0.01,0.24,0,482.6,1.23,0.11,260,1.07,5,30.69,64.31,31.01,0.03,0.24,0,1651,1.45,0.06,450,0.36,2.5,17.25,80.25,30,0.12,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +3,VT029,2,BINGHAMVILLE,VT0041,16,3,D,1651,0.5,0.5,SIL-SI-SIL,279.4,1.35,0.22,14,2.91,7.5,70.69,21.81,1.29,0.01,0.49,0,685.8,1.35,0.23,3.9,0.97,7.5,86.52,5.98,1.29,0.04,0.64,0,1651,1.35,0.21,1.8,0.32,12.5,73.09,14.41,1.29,0.12,0.64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +10,VT023,11,PALATINE,NY0006,3,4,B,736.5999756,0.5,0.5,SIL-CN--SIL-CNV-SIL-UWB,279.4,1.25,0.19,26,3.49,11.5,56.14,32.36,7.51,0.01,0.32,0,457.2,1.35,0.14,15,0.29,11.5,56.14,32.36,20.93,0.13,0.28,0,711.2,1.83,0.09,6.8,0.29,11.5,56.14,32.36,51.86,0.13,0.2,0,736.6,2.5,0.01,500,0.1,5,25,70,98,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +11,VT023,13,SWANTON,ME0017,3,3,C,1651,0.5,0.5,FSL-FSL-SIC,177.8,1.15,0.19,210,3.49,8.5,26.89,64.61,1.1,0.01,0.28,0,558.8,1.3,0.15,150,1.02,8.5,26.89,64.61,1.24,0.03,0.32,0,1651,1.55,0.18,0.47,0.15,45,47.63,7.37,0,0.17,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +12,VT023,16,HINESBURG,VT0008,1,3,C,1651,0.5,0.5,FSL-S-VFSL,228.6,1.35,0.19,450,2.62,3,35.21,61.79,5.25,0.01,0.24,0,660.4,1.4,0.07,600,0.87,3,1.51,95.49,5.44,0.04,0.24,0,1651,1.5,0.13,70,0.29,9.5,25.77,64.73,2.89,0.13,0.43,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +13,VT023,19,LIVINGSTON,VT0018,1,4,D,1574.800049,0.5,0.5,C-C-C-C,177.8,1.35,0.19,1.7,5.81,62.5,26.24,11.26,0,0.01,0.49,0,914.4,1.38,0.13,0.14,0.58,75,17.58,7.42,0,0.08,0.49,0,1168.4,1.38,0.12,0.1,0.19,75,17.58,7.42,0,0.16,0.49,0,1574.8,1.4,0.11,0.07,0.06,75,17.58,7.42,0,0.2,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +14,VT025,5,STETSON,ME0021,4,4,B,1651,0.5,0.5,FSL-GR--FSL-GRV-LS-GRX-CO,177.8,1.15,0.16,200,2.33,7.5,27.18,65.32,10.9,0.01,0.17,0,533.4,1.15,0.13,250,1.45,5,34.48,60.52,25.24,0.01,0.1,0,685.8,1.3,0.09,400,0.73,1.5,16.52,81.98,27.62,0.06,0.1,0,1651,1.52,0.01,350,0.15,1,6.53,92.47,65,0.17,0.05,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +15,VT025,6,SCARBORO,MA0080,4,4,D,1651,0.5,0.5,MK--LS-FS-S-GR--COS,228.6,0.85,0.08,450,0,4,15.98,80.02,20,0.23,0.17,0,406.4,1.25,0.04,850,0,3,0.62,96.38,3.68,0.23,0.17,0,838.2,1.45,0.04,750,0,1,1.54,97.46,8.81,0.23,0.1,0,1651,1.45,0.02,700,0,1,6.53,92.47,20.85,0.23,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +19,VT043,4,MONSON,ME0102,9,4,C,406.3999939,0.5,0.5,STV-SIL-SIL-CN--SIL-UWB,25.4,0.85,0.11,10,0.58,10,57.09,32.91,40,0.08,0.24,0,177.8,1.45,0.15,6.9,1.74,14,55.62,30.38,18.76,0.01,0.28,0,381,1.55,0.14,5.7,0.73,14,55.62,30.38,20.91,0.06,0.28,0,406.4,2.5,0.01,500,0.24,5,25,70,98,0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +20,VT043,6,ABRAM,ME0115,6,2,D,127,0.5,0.5,SIL-UWB,101.6,1,0.5,65,1.74,3.5,59.25,37.25,13.19,0.01,0.32,0,127,2.5,0.5,400,0.58,5,25,70,98,0.08,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +21,VT044,6,DIXFIELD,ME0105,6,3,C,1651,0.5,0.5,STV-FSL-FSL-GR--FSL,76.2,1.05,0.09,160,0.58,6.5,27.48,66.03,40,0.08,0.17,0,609.6,1.3,0.13,180,1.31,6.5,27.48,66.03,19.11,0.02,0.24,0,1651,1.8,0.1,110,0.15,6.5,27.48,66.03,26.24,0.17,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +4,VT027,1,LIMERICK,VT0017,42,3,C,1651,0.5,0.5,SIL-VFSL-VFSL,127,1.3,0.21,12,2.03,7,71.07,21.93,0,0.01,0.49,0,711.2,1.3,0.17,90,0.68,6,40.57,53.43,0,0.06,0.49,0,1651,1.35,0.16,130,0.23,4.5,41.22,54.28,0,0.15,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +5,VT027,8,DUANE,NY0034,1,3,B,1320.800049,0.5,0.5,LFS-GRV-S-GRV-S,101.6,1.25,0.16,800,3.2,3,16.7,80.3,7.51,0.01,0.17,0,965.2,1.4,0.04,450,1.07,2.5,1.52,95.98,45.77,0.03,0.17,0,1320.8,1.55,0.02,400,0.36,1.5,1.53,96.97,54.01,0.12,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +6,VT028,2,MASSENA,NY0044,10,3,C,2032,0.5,0.5,STX-L-GR--FSL-GR--FSL,177.8,1.25,0.06,20,3.2,15,40.67,44.33,65,0.01,0.2,0,584.2,1.35,0.1,88,0.29,12.5,20.24,67.26,19.41,0.13,0.2,0,2032,1.83,0.08,52,0.1,12.5,20.24,67.26,30.91,0.19,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +16,VT025,8,WINDSOR,CT0014,3,3,A,1651,0.5,0.5,LS-LS-S,50.8,1.1,0.15,650,1.74,2,16.41,81.59,4.41,0.01,0.17,0,508,1.42,0.11,650,0.58,1.5,16.52,81.98,5.64,0.08,0.17,0,1651,1.52,0.04,700,0.19,1,1.54,97.46,7.6,0.16,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +17,VT025,11,ADAMS,NY0025,3,4,A,1778,0.5,0.5,LS-LS-S-FS,101.6,1.15,0.15,750,2.03,2.5,16.3,81.2,1.1,0.01,0.17,0,152.4,1.15,0.11,200,1.16,8.5,8.93,82.57,5.84,0.02,0.17,0,660.4,1.27,0.07,750,1.16,2.5,1.52,95.98,1.22,0.02,0.17,0,1778,1.35,0.04,700,0.15,2.5,0.62,96.88,8.5,0.17,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +18,VT025,17,HARTLAND,VT0011,3,4,B,1651,0.5,0.5,VFSL-SI-VFSL-FSL,152.4,1.3,0.18,270,2.91,6.5,29.39,64.11,1.24,0.01,0.49,0,482.6,1.35,0.23,6.6,0.97,6.5,87.46,6.04,1.29,0.04,0.64,0,812.8,1.4,0.14,56,0.32,9.5,28.45,62.05,1.34,0.12,0.64,0,1651,1.4,0.12,59,0.11,9.5,26.59,63.91,1.34,0.19,0.64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +22,VT017,13,COVINGTON,VT0012,1,3,D,1651,0.5,0.5,SICL-C-C,203.2,1.35,0.2,7.9,5.81,35,47.76,17.24,1.29,0.01,0.49,0,609.6,1.35,0.15,0.23,1.94,75,17.58,7.42,1.29,0.01,0.49,0,1651,1.35,0.13,0.12,0.65,75,17.58,7.42,1.29,0.07,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +23,VT017,16,PANTON,VT0019,1,3,D,2032,0.5,0.5,SIC-C-C,203.2,1.35,0.19,0.94,5.81,59.5,40,0.5,0,0.01,0.49,0,1168.4,1.38,0.15,0.21,1.94,75,17.58,7.42,0,0.01,0.49,0,2032,1.38,0.13,0.12,0.65,75,17.58,7.42,0,0.07,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +24,VT021,6,ENOSBURG,VT0007,5,3,C,1651,0.5,0.5,LFS-S-VFSL,203.2,1.7,0.14,700,1.74,3,16.7,80.3,1.94,0.01,0.24,0,812.8,1.7,0.06,600,0.58,3,1.51,95.49,1.94,0.08,0.24,0,1651,1.6,0.13,54,0.19,10.5,25.49,64.01,0,0.16,0.43,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +25,VT033,7,KENDAIA,NY0103,4,3,C,1524,0.5,0.5,STV-SIL-L-GR--L,203.2,1.25,0.12,2.7,0,18.5,54.4,27.1,40,0.23,0.24,0,609.6,1.35,0.13,6.9,0,22.5,37.7,39.8,17.44,0.23,0.24,0,1524,1.83,0.09,3.8,0,22.5,37.7,39.8,42.18,0.23,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +26,VT033,12,ELMWOOD,ME0004,1,3,C,1651,0.5,0.5,FSL-SL-C,228.6,1.15,0.19,240,2.91,7.5,27.18,65.32,1.1,0.01,0.28,0,584.2,1.3,0.13,130,0.73,8.5,23.76,67.74,1.24,0.06,0.32,0,1651,1.52,0.17,1.5,0.15,45,28.9,26.1,0,0.17,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +27,VT033,15,LYONS,NY0105,1,3,D,1524,0.5,0.5,STV-SIL-GR--L-GR--L,228.6,1.25,0.12,2,0,22.5,52.72,24.78,40,0.23,0.28,0,914.4,1.35,0.13,5.1,0,23,37.45,39.55,17.44,0.23,0.37,0,1524,1.83,0.1,4.2,0,23,37.45,39.55,35.58,0.23,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +28,VT030,19,WINOOSKI,MA0023,2,2,B,1524,0.5,0.5,SIL-LVFS,203.2,1.25,0.21,5.1,2.03,11.5,67.63,20.87,1.2,0.01,0.49,0,1524,1.35,0.11,200,0.68,6,16.36,77.64,1.29,0.06,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +29,VT032,9,KARS,NY0228,4,3,A,1524,0.5,0.5,FSL-GRV-SL-GRV-S,279.4,1.25,0.14,77,2.03,11.5,26.01,62.49,8.88,0.01,0.24,0,558.8,1.4,0.07,45,0.68,11.5,23.11,65.39,46.03,0.06,0.17,0,1524,1.55,0.02,350,0.23,2.5,1.52,95.98,58.59,0.15,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +30,VT032,13,BENSON,VT0002,3,3,D,558.7999878,0.5,0.5,L-CNV-L-UWB,127,1.4,0.16,19,2.33,20,37.83,42.17,12.77,0.01,0.28,0,457.2,1.55,0.09,8.1,0.78,20,37.83,42.17,49.5,0.05,0.17,0,558.8,2.5,0.01,500,0.26,5,25,70,98,0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +31,VT033,17,NELLIS,NY0211,1,4,B,1828.800049,0.5,0.5,SIL-GR--FSL-GR--FSL-SL,177.8,1.45,0.17,20,2.33,11.5,56.14,32.36,10.16,0.01,0.32,0,431.8,1.55,0.1,86,0.29,11.5,26.01,62.49,21.39,0.13,0.24,0,584.2,1.55,0.1,76,0.29,11.5,26.01,62.49,21.39,0.13,0.24,0,1828.8,1.77,0.08,58,0.29,10.5,23.33,66.17,30.04,0.13,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +32,VT033,20,MELROSE,ME0034,1,3,C,1651,0.5,0.5,FSL-FSL-C,177.8,1.15,0.19,240,2.91,7.5,27.18,65.32,1.1,0.01,0.28,0,584.2,1.3,0.15,170,1.02,7.5,27.18,65.32,1.24,0.03,0.32,0,1651,1.55,0.17,1.5,0.15,45,28.9,26.1,0,0.17,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +33,VT033,21,WHATELY,ME0035,1,3,D,1651,0.5,0.5,FSL-FSL-C,177.8,1.13,0.2,280,3.78,7.5,27.18,65.32,0,0.01,0.28,0,508,1.3,0.15,190,0.73,7.5,27.18,65.32,0,0.06,0.28,0,1651,1.55,0.17,1.6,0.15,45,28.9,26.1,0,0.17,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +34,VT034,12,LYME,NH0042,4,3,C,1524,0.5,0.5,STV-FSL-SL-FSL,177.8,1.13,0.08,150,0,6.5,27.48,66.03,40,0.23,0.24,0,762,1.48,0.1,120,0,6.5,30.37,63.13,16.36,0.23,0.32,0,1524,1.58,0.11,180,0,4.5,34.67,60.83,19.01,0.23,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +35,VT035,17,MARLOW,NH0010,2,3,C,1651,0.5,0.5,STV-L-FSL-FSL,152.4,1.15,0.09,46,0,7.5,45.18,47.32,40,0.23,0.2,0,787.4,1.45,0.1,120,0,6.5,27.48,66.03,19.45,0.23,0.32,0,1651,1.88,0.1,110,0,6.5,27.48,66.03,25.79,0.23,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +36,VT035,18,WORDEN,VT0070,2,4,C,1651,0.5,0.5,L-GR--SL-GR--SL-FSL,50.8,0.85,0.19,110,3.49,7.5,45.18,47.32,4.29,0.01,0.49,0,457.2,0.95,0.15,220,1.16,6.5,30.37,63.13,6.9,0.02,0.64,0,1270,1.9,0.12,130,0.39,6.5,30.37,63.13,14.6,0.11,0.37,0,1651,1.8,0.11,120,0.13,6.5,27.48,66.03,13.94,0.18,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +37,VT036,2,WILMINGTON,VT0082,7,4,D,1651,0.5,0.5,STV-FSL-FSL-FSL-FSL,50.8,0.85,0.12,210,3.49,6.5,27.48,66.03,40,0.01,0.43,0,482.6,0.95,0.14,230,1.16,6.5,27.48,66.03,14.54,0.02,0.64,0,1346.2,1.9,0.13,150,0.39,6.5,27.48,66.03,10,0.11,0.37,0,1651,1.8,0.11,120,0.13,6.5,27.48,66.03,10,0.18,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +38,VT036,20,MUNDAL,VT0079,1,3,C,2286,0.5,0.5,STV-FSL-FSL-FSL,76.2,0.85,0.08,150,0,6.5,27.48,66.03,40,0.23,0.43,0,635,0.95,0.12,180,0,6.5,27.48,66.03,7.79,0.23,0.64,0,2286,1.9,0.11,130,0,6.5,27.48,66.03,10,0.23,0.37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +39,VT037,14,MONADNOCK,NH0035,2,3,B,1651,0.5,0.5,STV-FSL-FSL-LS,127,1,0.09,220,0,4.5,34.67,60.83,40,0.23,0.24,0,584.2,1.05,0.13,250,0,4.5,34.67,60.83,11.12,0.23,0.28,0,1651,1.45,0.06,350,0,3,16.2,80.8,32.12,0.23,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +40,VT004,19,KILLINGTON,VT0097,1,3,D,457.2000122,0.5,0.5,STV-L-GRV-FSL-UWB,25.4,0.8,0.11,63,3.49,7.5,45.18,47.32,45.89,0.01,0.32,0,431.8,0.8,0.09,160,1.16,5.5,34.3,60.2,47.53,0.02,0.28,0,457.2,2.5,0.01,500,0.39,5,25,70,98,0.11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +41,VT006,6,ENCHANTED,ME0024,5,4,B,1193.800049,0.5,0.5,STV-SIL-CN--FSL-CBX-COS-U,76.2,1.05,0.11,34,0.58,5.5,58.02,36.48,40,0.08,0.15,0,635,1.05,0.12,180,1.45,5.5,34.3,60.2,25.49,0.01,0.1,0,1168.4,1.15,0.02,190,0.29,5.5,3.84,90.66,50.97,0.13,0.1,0,1193.8,2.5,0.01,400,0.1,5,25,70,98,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +42,VT006,8,HOGBACK,VT0077,5,3,C,406.3999939,0.5,0.5,STV-FSL-FSL-UWB,50.8,0.8,0.12,160,3.49,7.5,27.18,65.32,40,0.01,0.43,0,381,0.8,0.14,180,1.16,7.5,27.18,65.32,9.48,0.02,0.64,0,406.4,2.5,0.01,350,0.39,5,25,70,98,0.11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +43,VT037,17,PITTSFIELD,MA0015,2,3,B,1651,0.5,0.5,STV-FSL-FSL-FSL,228.6,1.5,0.09,140,0,6,34.12,59.88,40,0.23,0.2,0,812.8,1.65,0.1,61,0,9,26.74,64.26,19.49,0.23,0.32,0,1651,1.7,0.1,120,0,5.5,34.3,60.2,26.57,0.23,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +44,VT038,14,RAWSONVILLE,VT0081,3,4,C,736.5999756,0.5,0.5,BYV-FSL-GR--FSL-LFS-UWB,25.4,0.85,0.12,210,3.49,6.5,27.48,66.03,40,0.01,0.43,0,482.6,0.85,0.15,240,1.16,6.5,27.48,66.03,8.13,0.02,0.64,0,711.2,0.95,0.07,200,0.39,6.5,6.68,86.82,12.94,0.11,0.28,0,736.6,2.5,0.01,350,0.13,5,25,70,98,0.18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +45,VT040,2,LONDONDERRY,VT0043,9,3,C,152.3999939,0.5,0.5,SIL-FSL-UWB,50.8,1.2,0.19,78,2.91,4.5,58.63,36.87,6.08,0.01,0.43,0,127,1.2,0.15,300,0.97,4.5,34.67,60.83,6.08,0.04,0.43,0,152.4,2.5,0.01,450,0.32,5,25,70,98,0.12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +46,VT040,7,STRATTON,VT0054,6,4,C,431.7999878,0.5,0.5,SIL-FSL-GR--FSL-UWB,101.6,1.15,0.19,89,3.49,4,58.94,37.06,10.48,0.01,0.49,0,177.8,0.85,0.15,240,1.16,6.5,27.48,66.03,10,0.02,0.64,0,406.4,0.9,0.09,170,0.39,5.5,34.3,60.2,39.33,0.11,0.49,0,431.8,2.5,0.01,500,0.13,5,25,70,98,0.18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +47,VT040,9,GLEBE,VT0092,5,3,C,660.4000244,0.5,0.5,STV-VFSL-VFSL-UWB,203.2,0.9,0.15,550,8.14,4,35.71,60.29,40,0.01,0.43,0,635,0.8,0.16,300,2.71,6.5,26.63,66.87,12.01,0.01,0.64,0,660.4,2.5,0.01,210,0.9,5,25,70,98,0.04,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +48,VT040,14,HOUGHTONVILLE,VT0074,4,3,C,1651,0.5,0.5,BYV-FSL-FSL-FSL,101.6,0.85,0.12,210,3.49,6.5,27.48,66.03,40,0.01,0.43,0,584.2,0.95,0.15,240,1.16,6.5,27.48,66.03,9,0.02,0.64,0,1651,1.65,0.11,140,0.39,6.5,27.48,66.03,23.42,0.11,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +49,VT040,18,SISK,ME0126,3,3,C,1651,0.5,0.5,BYV-FSL-GR--SL-GR--FSL,25.4,1,0.09,160,0.58,6.5,27.48,66.03,40,0.08,0.28,0,508,1.35,0.14,120,3.05,9,23.65,67.35,20.03,0.01,0.32,0,1651,1.75,0.1,58,0.15,9,26.74,64.26,24.51,0.17,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +50,VT042,15,SUNAPEE,NH0040,4,3,B,1524,0.5,0.5,STV-FSL-FSL-GR--LS,76.2,1,0.08,97,0,8,27.03,64.97,40,0.23,0.2,0,609.6,1.05,0.11,150,0,7,27.33,65.67,11.12,0.23,0.2,0,1524,1.35,0.06,190,0,6,9.17,84.83,13.86,0.23,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +51,VT043,3,ELLIOTTSVILLE,ME0086,11,4,B,660.4000244,0.5,0.5,STV-SIL-SIL-SIL-UWB,25.4,0.85,0.12,12,1.45,10,57.09,32.91,40,0.01,0.24,0,406.4,1.3,0.15,6.7,1.31,14,55.62,30.38,19.42,0.02,0.28,0,635,1.55,0.14,5.1,0.15,14,55.62,30.38,20.91,0.17,0.28,0,660.4,2.5,0.01,450,0.05,5,25,70,98,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +52,VT044,9,LYMAN,MA0079,4,3,C,457.2000122,0.5,0.5,STV-L-CN--FSL-UWB,152.4,0.98,0.09,46,0,7.5,45.18,47.32,40,0.23,0.2,0,431.8,1.15,0.11,160,0,6,34.12,59.88,17.28,0.23,0.32,0,457.2,2.5,0.01,600,0,5,25,70,98,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +53,VT045,5,RUMNEY,ME0014,7,3,C,1651,0.5,0.5,FSL-FSL-GRV-COS,228.6,1.25,0.19,450,3.49,5,34.48,60.52,6.31,0.01,0.24,0,762,1.3,0.14,290,0.58,5,34.48,60.52,6.55,0.08,0.37,0,1651,1.4,0.02,700,0.29,1.5,6.49,92.01,40,0.13,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +54,VT045,10,COLTON,NY0029,4,4,A,1828.800049,0.5,0.5,GR--LS-GRV-S-GRV-S-CBV-S,203.2,1.25,0.12,650,2.33,3,16.2,80.8,30,0.01,0.15,0,228.6,1.3,0.04,700,0.73,3.5,1.5,95,38.92,0.06,0.24,0,558.8,1.4,0.03,850,0.15,2.5,1.52,95.98,40.69,0.17,0.15,0,1828.8,1.55,0.01,400,0.05,1.5,1.53,96.97,65.69,0.21,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +55,VT021,7,SCANTIC,ME0044,5,3,D,1651,0.5,0.5,SIL-SICL-SIC,279.4,1.13,0.22,6.6,3.49,26.5,50,23.5,1.09,0.01,0.32,0,736.6,1.45,0.22,0.65,1.02,37.5,55.03,7.47,1.38,0.03,0.49,0,1651,1.6,0.16,0.33,0.15,45,47.63,7.37,1.52,0.17,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +56,VT021,16,LORDSTOWN,NY0096,3,4,C,787.4000244,0.5,0.5,L-CN--SIL-CNV-FSL-UWB,127,1.25,0.16,37,2.33,13,41.62,45.38,10.11,0.01,0.28,0,660.4,1.35,0.14,5.5,0.78,13,56.26,30.74,27.1,0.05,0.28,0,762,1.35,0.08,33,0.26,11.5,26.01,62.49,40.8,0.14,0.28,0,787.4,2.5,0.01,400,0.09,5,25,70,98,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +57,VT023,1,VERGENNES,VT0001,24,4,C,1651,0.5,0.5,C-C-C-C,152.4,1.4,0.18,0.9,2.33,58.5,25.65,15.85,0,0.01,0.49,0,406.4,1.25,0.14,0.13,0.78,75,17.58,7.42,0,0.05,0.49,0,736.6,1.25,0.12,0.1,0.26,75,17.58,7.42,0,0.14,0.49,0,1651,1.35,0.11,0.08,0.09,75,17.58,7.42,0,0.2,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +76,VT007,17,SACO,CT0013,3,4,D,1524,0.5,0.5,SIL-VFSL-VFSL-GRV-COS,304.8,1.2,0.23,11,3.78,9.5,69.16,21.34,0,0.01,0.49,0,812.8,1.35,0.16,97,1.26,8.5,28.76,62.74,0,0.02,0.64,0,1219.2,1.35,0.14,77,0.42,8.5,28.76,62.74,0,0.1,0.64,0,1524,1.45,0.02,300,0.14,4.5,3.88,91.62,40,0.18,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +77,VT007,19,RIPPOWAM,CT0065,3,3,C,1651,0.5,0.5,FSL-FSL-GRV-COS,127,1.23,0.2,450,3.2,4,34.85,61.15,4.89,0.01,0.2,0,685.8,1.33,0.16,300,1.07,3.5,35.03,61.47,5.26,0.03,0.2,0,1651,1.38,0.03,600,0.36,1,6.53,92.47,40,0.12,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +78,VT008,2,BOMOSEEN,VT0095,10,3,C,1651,0.5,0.5,SIL-CNV-FSL-CNV-FSL,203.2,1.15,0.18,20,2.33,10,57.09,32.91,6.82,0.01,0.32,0,685.8,1.45,0.11,61,0.78,10,26.45,63.55,23.58,0.05,0.37,0,1651,1.85,0.09,48,0.26,10,26.45,63.55,35.71,0.14,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +79,VT008,20,PAXTON,CT0061,3,3,C,1651,0.5,0.5,STV-FSL-FSL-FSL,50.8,1.13,0.08,110,0,7.5,27.18,65.32,40,0.23,0.2,0,660.4,1.48,0.1,90,0,7.5,27.18,65.32,19.72,0.23,0.32,0,1651,1.85,0.1,86,0,7.5,27.18,65.32,23.56,0.23,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +80,VT009,8,AMENIA,NY0214,5,3,B,1524,0.5,0.5,STV-L-GR--FSL-GR--FSL,203.2,1.25,0.09,21,0,11.5,43.23,45.27,40,0.23,0.24,0,711.2,1.45,0.1,36,0,11.5,26.01,62.49,20.29,0.23,0.24,0,1524,1.83,0.08,30,0,11.5,26.01,62.49,33.45,0.23,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +81,VT010,10,HUBBARDTON,VT0049,4,3,D,152.3999939,0.5,0.5,FLV-SIL-FLV-FSL-UWB,76.2,1.25,0.1,7.8,0,10.5,56.77,32.73,45.68,0.23,0.24,0,127,1.25,0.07,41,0,10.5,26.3,63.2,45.68,0.23,0.2,0,152.4,2.5,0.01,700,0,5,25,70,98,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +82,VT010,17,PITTSTOWN,MA0008,2,3,C,1524,0.5,0.5,STV-SIL-VFSL-CN--VFSL,254,1.15,0.11,19,0,7,58.99,34.01,40,0.23,0.2,0,736.6,1.45,0.1,110,0,7,26.49,66.51,19.45,0.23,0.37,0,1524,1.85,0.09,100,0,7,26.49,66.51,27.54,0.23,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +83,VT014,19,CLAVERACK,NY0195,2,3,C,1828.800049,0.5,0.5,FSL-FS-SICL,203.2,1.35,0.18,350,2.33,6.5,27.48,66.03,1.29,0.01,0.28,0,812.8,1.35,0.04,900,0.29,2,0.63,97.37,1.29,0.13,0.17,0,1828.8,1.27,0.2,0.53,0.29,39.5,53.27,7.23,1.22,0.13,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +84,VT015,13,GEORGIA,VT0015,4,3,C,1651,0.5,0.5,L-GRV-FSL-SIL,203.2,1.15,0.17,51,3.2,12.5,41.86,45.64,5.65,0.01,0.32,0,660.4,1.45,0.09,39,1.07,11.5,26.01,62.49,40,0.03,0.32,0,1651,1.7,0.16,4.9,0.36,14,55.62,30.38,10,0.12,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +58,VT046,13,SALMON,NY0220,3,4,B,1778,0.5,0.5,VFSL-VFSL-VFSL-SIL,203.2,1.2,0.17,100,2.33,10,28.29,61.71,3.54,0.01,0.49,0,584.2,1.3,0.15,73,0.78,10,28.29,61.71,3.83,0.05,0.64,0,914.4,1.55,0.13,48,0.26,10,28.29,61.71,4.53,0.14,0.64,0,1778,1.55,0.2,0.46,0.09,26.5,64.45,9.05,4.53,0.2,0.64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +59,VT046,18,DUXBURY,VT0056,3,4,A,1651,0.5,0.5,FSL-FSL-FSL-GRV-COS,127,1.35,0.17,450,2.03,4,34.85,61.15,5.36,0.01,0.32,0,406.4,1,0.16,350,2.03,5,34.48,60.52,6.24,0.01,0.49,0,635,1.2,0.14,400,0.87,2.5,35.39,62.11,11.62,0.04,0.28,0,1651,1.55,0.02,600,0.29,1,6.53,92.47,37.96,0.13,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +60,VT049,5,MUNSON,VT0010,6,3,D,1651,0.5,0.5,SIL-VFSL-SIC,203.2,1.3,0.22,21,3.78,6.5,71.46,22.04,1.24,0.01,0.49,0,381,1.35,0.16,100,1.26,9.5,28.45,62.05,1.29,0.02,0.49,0,1651,1.4,0.18,0.41,0.42,47.5,46.98,5.52,1.34,0.1,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +61,VT002,3,POOTATUCK,CT0064,13,3,B,1651,0.5,0.5,FSL-FSL-GRV-COS,101.6,1.23,0.18,400,2.33,4,34.85,61.15,4.89,0.01,0.2,0,736.6,1.33,0.15,280,0.78,3.5,35.03,61.47,5.26,0.05,0.2,0,1651,1.38,0.03,600,0.26,1,6.53,92.47,40,0.14,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +62,VT002,4,GROTON,CT0046,6,4,A,1828.800049,0.5,0.5,GR--SL-GR--SL-GRV-LS-GRX-,203.2,1.15,0.13,280,1.45,5,30.69,64.31,26.5,0.01,0.17,0,457.2,1.38,0.09,270,0.48,4,30.91,65.1,38.81,0.09,0.17,0,762,1.45,0.06,270,0.16,3.5,16.09,80.41,40.08,0.17,0.17,0,1828.8,1.55,0.01,350,0.05,1,6.53,92.47,65,0.21,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +63,VT002,6,ADRIAN,MI0028,5,2,A,1524,0.5,0.5,MUCK-S,863.6,0.3,0.35,180,9.88,10,45,45,0,0.01,0.1,0,1524,1.58,0.13,450,3.29,6,1.88,92.11,12.94,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +64,VT002,12,BELGRADE,MA0076,3,3,B,1524,0.5,0.5,SIL-VFSL-SIL,228.6,1.05,0.21,6.8,1.74,9.5,69.16,21.34,1.01,0.01,0.49,0,1066.8,1.25,0.14,58,0.58,9.5,28.45,62.05,1.2,0.08,0.64,0,1524,1.3,0.17,2.7,0.19,11,68.02,20.98,12.47,0.16,0.64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +65,VT002,18,FREDON,NJ0038,1,3,C,1524,0.5,0.5,FSL-GR--SIL-GRV-S,177.8,1.3,0.14,110,2.33,13.5,20.01,66.49,5.66,0.01,0.32,0,660.4,1.3,0.15,8.4,0.44,13.5,55.94,30.56,16.17,0.1,0.32,0,1524,1.5,0.03,250,0.15,6,1.88,92.11,40,0.17,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +66,VT002,21,HERO,CT0047,1,3,B,1524,0.5,0.5,L-GR--FSL-GRV-COS,228.6,1.25,0.16,53,2.03,9,44.45,46.55,8.88,0.01,0.32,0,685.8,1.42,0.11,68,0.68,9,26.74,64.26,19.55,0.06,0.24,0,1524,1.55,0.02,500,0.23,2.5,6.43,91.07,39.5,0.15,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +67,VT003,6,GALWAY,NY0218,4,4,B,838.2000122,0.5,0.5,STV-SIL-GR--FSL-GRV-SL-UW,254,1.25,0.12,8.4,2.33,12.5,56.59,30.91,40,0.01,0.24,0,609.6,1.35,0.12,67,0.78,11.5,26.01,62.49,17.44,0.05,0.24,0,812.8,1.35,0.07,40,0.26,10.5,23.33,66.17,34.88,0.14,0.24,0,838.2,2.5,0.01,400,0.09,5,25,70,98,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +68,VT004,4,MACOMBER,NY0321,8,3,C,609.5999756,0.5,0.5,STV-SIL-CN--L-UWB,76.2,1.25,0.13,4.3,2.33,18.5,54.4,27.1,40,0.01,0.24,0,584.2,1.35,0.08,9.2,0.78,18.5,38.54,42.96,50,0.05,0.24,0,609.6,2.5,0.01,800,0.26,5,25,70,98,0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +69,VT004,13,BERKSHIRE,MA0030,2,3,B,1651,0.5,0.5,STV-FSL-FSL-FSL,203.2,1.13,0.11,180,2.03,6.5,27.48,66.03,40,0.01,0.2,0,558.8,1.23,0.12,180,0.68,6.5,27.48,66.03,20.81,0.06,0.32,0,1651,1.45,0.11,130,0.23,5.5,34.3,60.2,23.73,0.15,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +70,VT007,4,TIOGA,NY0075,6,3,B,1828.800049,0.5,0.5,FSL-GR--FSL-GRV-LS,203.2,1.27,0.16,140,2.33,11.5,26.01,62.49,1.22,0.01,0.37,0,914.4,1.3,0.11,62,0.29,11.5,26.01,62.49,14.05,0.13,0.28,0,1828.8,1.4,0.06,100,0.29,9,8.88,82.12,24.66,0.13,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +71,VT007,7,PINNEBOG,MI0282,5,3,A,1524,0.5,0.5,MUCK-MPT-MUCK,863.6,0.3,0.35,300,9.88,10,45,45,0,0.01,0.1,0,1270,0.3,0.35,240,9.88,10,45,45,0,0.01,0.1,0,1524,0.3,0.35,300,9.88,10,45,45,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +72,VT007,8,CASTILE,NY0199,5,3,B,1524,0.5,0.5,GR--L-GRV-SL-GRV-S,330.2,1.25,0.15,48,4.07,12,42.98,45.02,23.22,0.01,0.24,0,863.6,1.4,0.09,66,1.36,9.5,23.54,66.96,35.88,0.02,0.17,0,1524,1.55,0.04,190,0.45,6,1.88,92.11,42.74,0.1,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +73,VT007,10,QUONSET,MA0058,5,3,A,1524,0.5,0.5,CN--SL-CN--LS-CNV-COS,50.8,1.25,0.13,300,2.21,4.5,30.8,64.7,31.03,0.01,0.17,0,508,1.45,0.09,550,0.74,2.5,16.3,81.2,30.01,0.06,0.17,0,1524,1.45,0.02,500,0.25,1,6.53,92.47,51.37,0.14,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +74,VT007,11,TISBURY,CT0053,4,3,B,1524,0.5,0.5,SIL-VFSL-GRV-S,203.2,1.15,0.21,13,2.33,7.5,70.69,21.81,2.23,0.01,0.49,0,660.4,1.45,0.14,100,0.78,7.5,29.08,63.42,2.8,0.05,0.64,0,1524,1.52,0.03,550,0.26,1.5,1.53,96.97,36.99,0.14,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +75,VT007,15,KINGSBURY,NY0170,4,3,D,1524,0.5,0.5,SICL-C-C,203.2,1.17,0.21,1.5,3.49,38.5,54.15,7.35,0,0.01,0.49,0,711.2,1.5,0.13,0.13,0.29,75,17.58,7.42,0,0.13,0.28,0,1524,1.45,0.12,0.23,0.29,62.5,26.24,11.26,0,0.13,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +85,VT015,15,WALPOLE,CT0015,4,3,C,1651,0.5,0.5,FSL-SL-GRV-COS,177.8,1.13,0.19,400,2.91,4,34.85,61.15,6.81,0.01,0.2,0,609.6,1.42,0.14,350,0.97,4,30.91,65.1,13.17,0.04,0.24,0,1651,1.52,0.03,600,0.32,1,6.53,92.47,40,0.12,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +86,VT016,2,HINCKLEY,MA0100,9,3,A,1651,0.5,0.5,GR--LFS-GR--LS-CB--COS,177.8,1.1,0.11,400,2.62,6,6.72,87.28,22.12,0.01,0.17,0,381,1.3,0.09,500,0.87,3,16.2,80.8,31.38,0.04,0.17,0,1651,1.4,0.02,500,0.29,1.5,6.49,92.01,50.63,0.13,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +87,VT016,3,SUDBURY,MA0027,8,3,B,660.4000244,0.5,0.5,FSL-SL-GR--COS,330.2,1.25,0.18,400,2.33,4,34.85,61.15,8.88,0.01,0.24,0,482.6,1.3,0.13,300,0.78,4.5,30.8,64.7,12.15,0.05,0.24,0,660.4,1.35,0.04,700,0.26,2,6.46,91.54,12.56,0.14,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +88,VT016,4,LINWOOD,MI0001,6,3,A,1524,0.5,0.5,MUCK-MUCK-SIL,228.6,0.3,0.35,230,9.88,10,45,45,1.14,0.01,0.1,0,863.6,0.3,0.35,160,9.88,10,45,45,1.14,0.01,0.1,0,1524,1.75,0.18,6.7,3.29,20,53.4,26.6,11.82,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +89,VT016,6,UDIPSAMMENTS,DE0211,4,1,D,25.39999962,0.5,0.5,,25.4,0,0,450,0,0,0,0,0,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +90,VT016,9,MIDDLEBURY,NY0045,4,3,B,1524,0.5,0.5,L-GR--FSL-S,203.2,1.27,0.17,49,2.91,11.5,43.23,45.27,6.43,0.01,0.28,0,1092.2,1.3,0.13,50,0.97,11.5,26.01,62.49,7.97,0.04,0.28,0,1524,1.4,0.04,260,0.32,5.5,1.47,93.03,21.53,0.12,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +91,VT016,19,SHEEPSCOT,ME0089,3,4,B,1651,0.5,0.5,FSL-GR--FSL-GRV-S-GRX-COS,76.2,1.15,0.17,450,2.33,4,34.85,61.15,9.55,0.01,0.17,0,304.8,1.35,0.12,450,0.44,3,35.21,61.79,24.86,0.1,0.1,0,635,1.58,0.02,550,0.44,1.5,1.53,96.97,61.09,0.1,0.1,0,1651,1.58,0.01,450,0.15,1.5,6.49,92.01,62.06,0.17,0.05,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +92,VT017,8,WALLKILL,NY0053,3,4,C,1828.800049,0.5,0.5,SIL-GR--L-SP-SP,203.2,1.27,0.21,11,4.65,18.5,54.4,27.1,2.47,0.01,0.37,0,609.6,1.3,0.16,15,1.55,21,37.35,41.65,7.97,0.01,0.37,0,812.8,0.5,0.4,280,0.52,10,45,45,0,0.09,0,0,1828.8,0.5,0.4,650,0.17,10,45,45,0,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +93,VT017,11,BIRDSALL,MA0033,1,3,D,1524,0.5,0.5,SIL-VFSL-VFSL,228.6,1.05,0.22,9.4,2.91,9.5,69.16,21.34,0,0.01,0.49,0,431.8,1.35,0.15,85,0.97,9.5,28.45,62.05,1.29,0.04,0.64,0,1524,1.35,0.14,56,0.32,9.5,28.45,62.05,1.29,0.12,0.64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +94,VT049,6,RAYNHAM,VT0005,6,3,C,1828.800049,0.5,0.5,SIL-VFSL-VFSL,152.4,1.35,0.22,14,3.78,9.5,69.16,21.34,1.29,0.01,0.49,0,558.8,1.35,0.16,140,1.26,9.5,28.45,62.05,1.29,0.02,0.64,0,1828.8,1.4,0.17,100,2.33,9.5,28.45,62.05,1.34,0.01,0.64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +95,VT049,21,DEERFIELD,MA0013,1,3,B,1651,0.5,0.5,LFS-S-S,228.6,1.1,0.12,500,1.45,4.5,16.44,79.06,4.41,0.01,0.17,0,482.6,1.33,0.06,650,0.48,4,1.49,94.51,5.26,0.09,0.17,0,1651,1.45,0.04,600,0.16,2.5,1.52,95.98,10.4,0.17,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +96,VT050,5,ELDRIDGE,VT0009,6,3,C,1651,0.5,0.5,LFS-FS-FS,228.6,1.5,0.14,650,1.74,3,16.7,80.3,2.86,0.01,0.24,0,685.8,1.7,0.06,600,0.58,3,0.62,96.38,3.22,0.08,0.24,0,1651,1.65,0.06,88,0.19,9.5,0.75,89.75,3.17,0.16,0.43,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +97,VT050,7,BUXTON,ME0043,5,4,C,1651,0.5,0.5,SIL-SICL-SICL-SIC,203.2,1.05,0.22,6.5,3.2,22.5,52.72,24.78,1.01,0.01,0.32,0,406.4,1.33,0.19,3.2,1.02,32.5,47.82,19.68,1.27,0.03,0.49,0,889,1.6,0.18,1.8,0.29,32.5,47.82,19.68,1.52,0.13,0.49,0,1651,1.6,0.15,0.27,0.15,45,47.63,7.37,1.52,0.17,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +98,VT050,17,SUNNY,VT0110,2,3,C,1651,0.5,0.5,SIL-VFSL-GRV-S,203.2,1.33,0.19,20,2.33,10,57.09,32.91,2.56,0.01,0.32,0,863.6,1.38,0.14,70,0.78,10,25.63,64.37,2.66,0.05,0.43,0,1651,1.45,0.03,550,0.26,1.5,1.53,96.97,40,0.14,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +99,VT051,15,LUPTON,MI0090,3,2,A,1651,0.5,0.5,MUCK-MUCK,254,0.3,0.35,58,9.88,10,45,45,0,0.01,0.1,0,1651,0.3,0.35,160,9.88,10,45,45,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +100,VT051,18,DUMMERSTON,VT0086,3,3,B,1651,0.5,0.5,STV-SIL-CN--FSL-CN--SL,76.2,1.1,0.12,34,1.74,6,57.71,36.29,40,0.01,0.28,0,762,1.3,0.12,130,0.58,6,34.12,59.88,17.8,0.08,0.28,0,1651,1.5,0.09,120,0.19,6,30.48,63.52,27.29,0.16,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +101,VT052,12,WESTBURY,NY0055,5,4,C,1828.800049,0.5,0.5,STV-FSL-GR--SL-GR--SL-GR-,203.2,1.05,0.11,200,2.91,7.5,27.18,65.32,40,0.01,0.24,0,508,1.55,0.1,200,1.16,7.5,23.97,68.53,27.95,0.02,0.24,0,1041.4,1.85,0.07,120,0.29,7.5,23.97,68.53,38.06,0.13,0.24,0,1828.8,1.85,0.07,100,0.29,7.5,23.97,68.53,38.06,0.13,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +102,VT053,8,FULLAM,VT0087,5,3,C,1651,0.5,0.5,SIL-CN--FSL-CN--FSL,152.4,1.1,0.17,43,1.74,6,57.71,36.29,6.67,0.01,0.32,0,558.8,1.3,0.13,170,0.58,6,34.12,59.88,13.22,0.08,0.37,0,1651,1.9,0.1,100,0.19,6,34.12,59.88,32.23,0.16,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +103,VT053,18,VERSHIRE,VT0035,4,4,B,787.4000244,0.5,0.5,STV-VFSL-VFSL-VFSL-UWB,76.2,1.1,0.09,86,1.45,11,25.35,63.65,40,0.01,0.32,0,508,1.4,0.12,100,1.45,11,25.35,63.65,16.91,0.01,0.24,0,762,1.55,0.12,69,1.45,11,25.35,63.65,18.39,0.01,0.24,0,787.4,2.5,0.01,300,0.48,5,25,70,98,0.09,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +104,VT055,6,TUNBRIDGE,VT0075,6,4,C,736.5999756,0.5,0.5,STV-FSL-GR--FSL-GR--FSL-U,76.2,1,0.11,220,2.91,7,27.33,65.67,40,0.01,0.2,0,355.6,1.3,0.15,290,2.33,6,34.12,59.88,16.75,0.01,0.2,0,711.2,1.35,0.13,180,0.87,5,34.48,60.52,17.28,0.04,0.2,0,736.6,2.5,0.01,550,0.29,5,25,70,98,0.13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +105,VT056,14,SHELBURNE,MA0085,3,3,C,1651,0.5,0.5,STV-FSL-FSL-FSL,152.4,1.17,0.09,190,0,5,34.48,60.52,40,0.23,0.2,0,482.6,1.45,0.1,180,0,5,34.48,60.52,30.07,0.23,0.32,0,1651,1.83,0.12,170,0,5,34.48,60.52,10,0.23,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +106,VT056,20,STOWE,VT0022,1,3,C,1651,0.5,0.5,FSL-FSL-FSL,228.6,0.85,0.19,280,4.36,7.5,27.18,65.32,8.94,0.01,0.24,0,736.6,1,0.14,130,1.45,7.5,27.18,65.32,13.92,0.01,0.32,0,1651,1.7,0.1,78,0.48,8.5,26.89,64.61,26.73,0.09,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +107,VT059,4,NINIGRET,CT0018,6,3,B,1524,0.5,0.5,FSL-FSL-GRV-COS,203.2,1.13,0.16,180,2.03,7.5,27.18,65.32,6.15,0.01,0.32,0,660.4,1.48,0.13,110,0.68,7.5,27.18,65.32,7.91,0.06,0.37,0,1524,1.58,0.02,550,0.23,1,6.53,92.47,40,0.15,0.15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +108,VT059,8,WOODSTOCK,VT0021,5,4,D,482.6000061,0.5,0.5,FSL-FSL-LFS-UWB,152.4,0.7,0.2,350,4.07,6.5,27.48,66.03,6.65,0.01,0.24,0,406.4,1.15,0.15,240,1.36,6.5,27.48,66.03,11.73,0.02,0.2,0,457.2,1.5,0.09,650,0.45,3,16.7,80.3,16.59,0.1,0.2,0,482.6,2.5,0.01,850,0.15,5,25,70,98,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +109,VT059,10,COLRAIN,VT0033,5,3,B,1651,0.5,0.5,FSL-GR--LFS-GR--LFS,152.4,1.13,0.19,350,2.91,5,34.48,60.52,4.41,0.01,0.24,0,838.2,1.23,0.11,300,0.97,5,16.35,78.65,7.27,0.04,0.24,0,1651,1.45,0.09,270,0.32,5,16.35,78.65,14.73,0.12,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +110,VT059,12,AGAWAM,MA0001,4,5,B,1651,0.5,0.5,FSL-FSL-FSL-GR--S-FS,279.4,1.15,0.16,200,1.74,7,27.33,65.67,3.4,0.01,0.28,0,406.4,1.3,0.13,210,0.58,5.5,34.3,60.2,10.92,0.08,0.37,0,660.4,1.35,0.13,240,0.19,3.5,35.03,61.47,11.3,0.16,0.28,0,1397,1.35,0.03,650,0.06,1.5,1.53,96.97,15.77,0.2,0.17,0,1651,1.4,0.03,700,0.02,0.5,0.64,98.86,10,0.22,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +111,VT060,4,CABOT,VT0014,8,3,D,1651,0.5,0.5,STV-SIL-GR--SL-GRV-SL,203.2,0.9,0.13,24,4.65,8.5,58.04,33.46,40,0.01,0.28,0,355.6,1.5,0.13,260,1.55,5.5,30.58,63.92,22.58,0.01,0.28,0,1651,1.8,0.1,120,0.52,6.5,30.37,63.13,27.65,0.09,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +112,VT060,8,POMFRET,VT0030,4,4,A,1498.599976,0.5,0.5,LFS-LS-S-WB,76.2,1.05,0.15,700,2.91,4,16.53,79.47,5.24,0.01,0.17,0,762,1.05,0.12,350,0.97,4,15.98,80.02,8.63,0.04,0.17,0,1473.2,1.2,0.04,550,0.32,3,1.51,95.49,9.64,0.12,0.17,0,1498.6,1.7,0.05,650,0.11,20,40,40,90,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +113,VT063,6,GLOVER,VT0072,5,4,D,406.3999939,0.5,0.5,STV-SIL-FSL-CN--FSL-UWB,101.6,0.7,0.12,17,2.91,11,56.46,32.54,40,0.01,0.32,0,330.2,0.85,0.14,110,2.33,11,26.15,62.85,13.08,0.01,0.2,0,381,1.4,0.11,93,2.03,11,26.15,62.85,26.05,0.01,0.2,0,406.4,2.5,0.01,350,0.68,5,25,70,98,0.06,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +114,VT065,4,HADLEY,MA0022,8,3,B,1828.800049,0.5,0.5,VFSL-VFS-S,279.4,1.35,0.16,190,2.03,7,29.23,63.77,1.29,0.01,0.49,0,1727.2,1.35,0.09,280,0.68,6,6,88,1.29,0.06,0.49,0,1828.8,1.35,0.05,400,0.23,4.5,1.49,94.01,1.29,0.15,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +115,VT065,6,MERRIMAC,MA0026,7,4,A,1524,0.5,0.5,FSL-SL-GR--LS-S,381,1.15,0.16,270,1.74,5,34.48,60.52,9.79,0.01,0.24,0,558.8,1.3,0.14,400,0.58,2.5,31.23,66.28,10.92,0.08,0.24,0,660.4,1.3,0.09,450,0.19,2,16.41,81.59,15.69,0.16,0.17,0,1524,1.4,0.02,450,0.06,1.5,1.53,96.97,46.03,0.2,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +116,VT065,8,WAITSFIELD,VT0044,6,3,B,1651,0.5,0.5,SIL-VFSL-GRV-COS,228.6,1.33,0.18,20,1.45,8.5,58.04,33.46,3.83,0.01,0.32,0,508,1.33,0.13,120,0.48,8.5,26.06,65.44,3.83,0.09,0.43,0,1651,1.4,0.02,550,0.16,1.5,6.49,92.01,40,0.17,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +117,VT065,12,LAMOINE,ME0108,2,4,D,1651,0.5,0.5,SIL-SICL-SICL-SIC,177.8,1.05,0.22,6.5,3.2,22.5,52.72,24.78,1.01,0.01,0.32,0,431.8,1.33,0.19,3.2,1.02,32.5,47.82,19.68,1.27,0.03,0.49,0,533.4,1.6,0.18,2.3,0.29,32.5,47.82,19.68,1.52,0.13,0.49,0,1651,1.6,0.15,0.27,0.15,45,47.63,7.37,1.52,0.17,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +118,VT065,13,GRANGE,NH0054,1,3,C,1524,0.5,0.5,SIL-VFSL-FS,152.4,1.1,0.2,65,2.62,5.5,58.02,36.48,1.05,0.01,0.43,0,685.8,1.45,0.17,180,0.87,5.5,35.15,59.35,1.38,0.04,0.43,0,1524,1.5,0.05,600,0.29,3,0.62,96.38,10,0.13,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +119,VT066,4,BUCKLAND,VT0025,6,3,C,1778,0.5,0.5,STV-L-FSL-GR--FSL,228.6,0.85,0.11,85,3.2,7.5,45.18,47.32,40,0.01,0.28,0,635,1,0.13,230,0.87,7.5,27.18,65.32,12.69,0.04,0.37,0,1778,1.7,0.1,78,0.58,10.5,26.3,63.2,21.56,0.08,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +120,VT067,1,PEACHAM,VT0020,25,3,D,1651,0.5,0.5,MUCK-GR--SL-GR--SL,177.8,0.3,0.35,350,9.88,10,45,45,0,0.01,0.1,0,254,1.3,0.17,250,3.29,6.5,30.37,63.13,14.54,0.01,0.1,0,1651,1.9,0.12,140,1.1,6.5,30.37,63.13,19.91,0.03,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +121,VT068,13,SEARSPORT,ME0077,3,4,D,1651,0.5,0.5,MUCK-MK--S-COS-S,203.2,0.3,0.35,350,9.88,10,45,45,0,0.01,0.1,0,330.2,1.25,0.21,1150,6.69,3,1.51,95.49,6.31,0.01,0.1,0,1016,1.45,0.04,850,0.15,1,6.53,92.47,7.25,0.17,0.1,0,1651,1.45,0.03,600,0.15,1,1.54,97.46,19,0.17,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +122,VT068,15,ROUNDABOUT,ME0097,3,3,C,1651,0.5,0.5,SIL-VFSL-VFSL,177.8,1.05,0.22,9.4,3.78,10.5,68.4,21.1,2.04,0.01,0.43,0,762,1.45,0.16,59,1.16,10.5,28.13,61.37,2.8,0.02,0.64,0,1651,1.55,0.14,46,0.15,10.5,28.13,61.37,1.48,0.17,0.64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +123,VT068,18,MOOSILAUKE,NH0043,3,3,C,1651,0.5,0.5,L-LS-S,203.2,1,0.18,100,3.2,7.5,45.18,47.32,6.1,0.01,0.24,0,508,1.42,0.13,550,1.07,4,15.98,80.02,10,0.03,0.24,0,1651,1.52,0.04,650,0.36,1,1.54,97.46,22.04,0.12,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +124,VT071,9,MEDOMAK,ME0088,3,3,D,1651,0.5,0.5,SIL-LVFS-LVFS,304.8,1.05,0.21,31,3.49,6,66.26,27.74,2.04,0.01,0.32,0,685.8,1.23,0.11,240,0.73,6,16.36,77.64,2.38,0.06,0.49,0,1651,1.4,0.11,210,0.58,6,16.36,77.64,2.7,0.08,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +125,VT072,7,NICHOLVILLE,NY0099,4,4,C,1778,0.5,0.5,SIL-VFSL-LVFS-SL,254,1.35,0.21,7.6,2.33,10,68.78,21.22,3.97,0.01,0.49,0,457.2,1.35,0.15,73,0.78,10,28.29,61.71,3.97,0.05,0.64,0,762,1.55,0.09,74,0.26,10,10,80,4.53,0.14,0.64,0,1778,1.55,0.1,54,0.09,10,23.44,66.56,4.53,0.2,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +126,VT072,10,WONSQUEAK,ME0121,4,3,D,1651,0.5,0.5,MUCK-MUCK-SIL,203.2,0.3,0.35,29,9.88,10,45,45,0,0.01,0.1,0,812.8,0.3,0.35,230,9.88,10,45,45,0,0.01,0.1,0,1651,1.6,0.17,3.7,0.58,17.5,53.35,29.15,9.42,0.08,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +127,VT075,13,BUCKSPORT,ME0122,2,3,D,1651,0.5,0.5,MUCK-MUCK-MUCK,304.8,0.3,0.35,130,9.88,10,45,45,0,0.01,0.1,0,635,0.3,0.35,260,9.88,10,45,45,0,0.01,0.1,0,1651,0.3,0.35,140,9.88,10,45,45,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +128,VT078,5,STOCKBRIDGE,CT0011,8,4,C,1651,0.5,0.5,L-SIL-SIL-GRV-FSL,254,1.13,0.16,43,2.33,11.5,43.23,45.27,10.48,0.01,0.28,0,711.2,1.52,0.16,8.3,0.78,11.5,56.14,32.36,17.09,0.05,0.37,0,1066.8,1.73,0.14,6.3,0.26,11.5,56.14,32.36,22.73,0.14,0.24,0,1651,1.73,0.08,35,0.09,10.5,26.3,63.2,32.93,0.2,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +129,VT078,19,UDORTHENTS,DE0214,2,1,D,25.39999962,0.5,0.5,,25.4,0,0,240,0,0,0,0,0,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +130,VT079,2,MONARDA,ME0012,16,3,D,1651,0.5,0.5,STV-SIL-GR--SIL-GR--SIL,76.2,1.15,0.12,7.2,2.33,14,55.62,30.38,40,0.01,0.2,0,431.8,1.5,0.16,7.4,1.16,14,55.62,30.38,19.99,0.02,0.28,0,1651,1.83,0.14,4,0.15,14,55.62,30.38,23.31,0.17,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +131,VT079,3,PLAISTED,ME0008,9,3,C,1651,0.5,0.5,STV-SIL-GR--SIL-GR--SIL,25.4,1.1,0.11,34,0.58,5.5,58.02,36.48,40,0.08,0.2,0,660.4,1.1,0.16,32,1.89,5.5,58.02,36.48,14.09,0.01,0.24,0,1651,1.75,0.14,27,0.29,5.5,58.02,36.48,20.69,0.13,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +132,VT079,10,HOWLAND,ME0005,3,3,C,1651,0.5,0.5,SIL-SIL-GR--SIL,152.4,1.05,0.19,67,3.2,5.5,58.02,36.48,8.85,0.01,0.24,0,609.6,1.15,0.16,48,1.02,5.5,58.02,36.48,14.63,0.03,0.24,0,1651,1.75,0.14,33,0.29,5.5,58.02,36.48,20.69,0.13,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +133,VT079,13,WINNECOOK,ME0073,3,4,C,889,0.5,0.5,SIL-CN--SIL-CNV-SIL-UWB,228.6,1.05,0.18,34,2.91,7.5,58.67,33.83,9,0.01,0.28,0,711.2,1.25,0.11,17,1.02,6.5,59.31,34.19,43.23,0.03,0.28,0,863.6,1.25,0.08,12,0.15,6.5,59.31,34.19,54.81,0.17,0.28,0,889,2.5,0.01,400,0.05,5,25,70,98,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +134,VT079,14,THORNDIKE,ME0095,3,3,C,457.2000122,0.5,0.5,STV-SIL-CN--SIL-UWB,50.8,1.05,0.11,18,0.58,7.5,58.67,33.83,40,0.08,0.17,0,431.8,1.15,0.1,14,1.31,7.5,58.67,33.83,47.52,0.02,0.17,0,457.2,2.5,0.01,60,0.44,5,25,70,98,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +135,VT080,13,CANAAN,NH0057,3,3,C,457.2000122,0.5,0.5,STV-SL-GRV-FSL-UWB,76.2,1.1,0.08,280,0,4,30.91,65.1,40,0.23,0.2,0,431.8,1.45,0.08,230,0,3,35.21,61.79,47.17,0.23,0.17,0,457.2,2.5,0.01,450,0,5,25,70,98,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +136,VT080,17,CHOCORUA,NH0022,2,3,D,1651,0.5,0.5,PEAT-HM-GR--S,203.2,0.3,0.25,230,9.88,10,45,45,0.63,0.01,0.01,0,863.6,0.3,0.35,130,3.29,10,45,45,0.83,0.01,0.01,0,1651,1.35,0.07,550,1.1,3,1.51,95.49,11.3,0.03,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +137,VT081,3,MACHIAS,ME0033,10,3,B,1651,0.5,0.5,FSL-GR--SL-GRV-S,152.4,1,0.16,160,2.33,8.5,26.89,64.61,10.48,0.01,0.17,0,508,1.05,0.1,120,0.73,8.5,23.76,67.74,22.65,0.06,0.1,0,1651,1.55,0.02,450,0.15,2.5,1.52,95.98,45.37,0.17,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +138,VT081,4,MASARDIS,ME0069,8,4,A,1651,0.5,0.5,FSL-GR--SL-GRX-COS-GRX-CO,25.4,1,0.12,150,0.58,8.5,26.89,64.61,8.41,0.08,0.17,0,279.4,1.05,0.11,150,1.45,8.5,23.76,67.74,23.23,0.01,0.1,0,431.8,1.2,0.03,450,0.73,4.5,3.88,91.62,53.46,0.06,0.1,0,1651,1.55,0.01,450,0.15,2.5,6.43,91.07,59.74,0.17,0.05,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +139,VT081,8,METALLAK,NH0055,5,3,B,1524,0.5,0.5,VFSL-VFSL-FS,279.4,1.23,0.18,350,2.62,6.5,26.63,66.87,1.17,0.01,0.43,0,584.2,1.35,0.17,240,0.87,5.5,35.15,59.35,1.29,0.04,0.28,0,1524,1.5,0.04,700,0.29,1,0.63,98.37,10,0.13,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +140,VT081,9,MADAWASKA,ME0018,5,3,B,1651,0.5,0.5,VFSL-FSL-FS,50.8,1.1,0.18,250,3.2,8,26.2,65.8,3.26,0.01,0.28,0,711.2,1.25,0.16,170,1.31,7,27.33,65.67,3.68,0.02,0.28,0,1651,1.45,0.05,700,0.15,2.5,0.62,96.88,4.25,0.17,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +141,VT081,12,GROVETON,NH0047,5,3,B,1651,0.5,0.5,VFSL-FSL-FS,76.2,1.15,0.19,300,2.33,5.5,35.15,59.35,1.1,0.01,0.32,0,711.2,1.35,0.15,180,0.78,5.5,34.3,60.2,2.61,0.05,0.32,0,1651,1.4,0.05,600,0.26,3,0.62,96.38,2.7,0.14,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +142,VT082,1,ONDAWA,ME0010,15,3,B,1651,0.5,0.5,FSL-FSL-GRV-COS,228.6,1.27,0.2,400,3.49,5,34.48,60.52,1.22,0.01,0.24,0,762,1.3,0.18,250,1.74,5,34.48,60.52,1.24,0.01,0.37,0,1651,1.4,0.04,550,1.02,1.5,6.49,92.01,40,0.03,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +143,VT082,2,CORNISH,ME0079,11,3,C,1651,0.5,0.5,VFSL-VFSL-LVFS,304.8,1.15,0.18,170,2.91,9.5,28.45,62.05,0,0.01,0.32,0,889,1.2,0.14,110,0.73,8.5,28.76,62.74,0,0.06,0.49,0,1651,1.3,0.1,270,0.29,6,16.36,77.64,0,0.13,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +144,VT082,6,CHARLES,ME0082,3,2,C,1651,0.5,0.5,SIL-VFSL,152.4,1.13,0.22,15,4.36,10,68.78,21.22,0,0.01,0.32,0,1651,1.25,0.16,96,1.45,10,28.29,61.71,0,0.01,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +145,VT082,7,NAUMBURG,NY0047,3,3,C,1778,0.5,0.5,LS-S-COS,482.6,1.35,0.19,750,2.91,3,16.2,80.8,2.61,0.01,0.17,0,1041.4,1.35,0.07,600,0.97,3,1.51,95.49,2.61,0.04,0.17,0,1778,1.55,0.04,650,0.32,3,6.39,90.61,2.99,0.12,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +146,VT082,8,LOVEWELL,ME0081,2,3,B,1651,0.5,0.5,VFSL-VFSL-LVFS,355.6,1.15,0.18,230,2.91,8,28.92,63.08,1.1,0.01,0.32,0,558.8,1.17,0.14,150,0.73,8.5,28.76,62.74,1.12,0.06,0.49,0,1651,1.3,0.09,270,0.29,6,16.36,77.64,1.24,0.13,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +147,VT082,10,SUNDAY,ME0083,2,2,A,1651,0.5,0.5,LFS-FS,101.6,1.4,0.13,700,1.16,2.5,16.78,80.72,1.34,0.02,0.15,0,1651,1.4,0.04,750,0.15,1,0.63,98.37,4.11,0.17,0.15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +148,VT082,13,VASSALBORO,ME0048,3,2,D,1651,0.5,0.5,MPT-FB,431.8,0.3,0.35,280,9.88,10,45,45,0,0.01,0.1,0,1651,0.1,0.25,190,9.88,10,45,45,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +149,VT082,14,HERMON,ME0002,3,4,A,1651,0.5,0.5,STV-SL-FSL-GRV-COS-GRV-CO,25.4,1.02,0.09,280,0.58,4,30.91,65.1,40,0.08,0.1,0,177.8,1.02,0.17,300,2.91,4,34.85,61.15,10,0.01,0.1,0,762,1.08,0.04,300,1.02,4.5,3.88,91.62,42.5,0.03,0.1,0,1651,1.4,0.02,400,0.15,2.5,6.43,91.07,47.22,0.17,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +150,VT082,17,FRYEBURG,ME0117,2,4,B,1651,0.5,0.5,VFSL-VFSL-LVFS-SIL,279.4,1.23,0.17,250,2.33,7.5,29.08,63.42,1.17,0.01,0.32,0,558.8,1.13,0.15,200,1.45,7.5,29.08,63.42,1.08,0.01,0.49,0,1270,1.2,0.11,190,1.02,7.5,15,77.5,1.15,0.03,0.49,0,1651,1.4,0.17,28,0.58,1.5,69.43,29.07,7.02,0.08,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +151,VT082,19,CROGHAN,NY0031,2,3,B,1828.800049,0.5,0.5,LFS-S-FS,177.8,1.3,0.17,950,3.2,2.5,16.78,80.72,1.24,0.01,0.17,0,914.4,1.35,0.07,850,1.07,2.5,1.52,95.98,6.78,0.03,0.17,0,1828.8,1.35,0.04,850,0.36,2.5,0.62,96.88,6.78,0.12,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +152,VT082,21,COLONEL,ME0107,2,3,C,1651,0.5,0.5,STV-FSL-FSL-GR--FSL,25.4,1.05,0.09,160,0.58,6.5,27.48,66.03,40,0.08,0.17,0,431.8,1.3,0.13,180,1.31,6.5,27.48,66.03,19.11,0.02,0.24,0,1651,1.8,0.1,120,0.15,6.5,27.48,66.03,24.65,0.17,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +153,VT083,9,GREENWOOD,MI0143,5,2,A,1524,0.5,0.5,MPT-MPT,152.4,0.3,0.35,28,9.88,10,45,45,0,0.01,0.1,0,1524,0.3,0.35,220,9.88,10,45,45,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +154,VT083,10,PODUNK,ME0050,5,3,B,1651,0.5,0.5,FSL-FSL-GRV-COS,254,1.27,0.2,240,3.49,8,27.03,64.97,0,0.01,0.24,0,762,1.3,0.16,190,1.02,6.5,27.48,66.03,0,0.03,0.37,0,1651,1.4,0.03,500,0.58,3,6.39,90.61,40,0.08,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +155,VT083,13,AU GRES,MI0109,4,3,B,1524,0.5,0.5,LS-LS-S,330.2,1.42,0.11,110,1.74,12.5,4.23,83.27,7.13,0.01,0.15,0,762,1.6,0.08,160,0.47,8,8.97,83.03,7.94,0.09,0.1,0,1524,1.6,0.04,600,0.15,4,1.49,94.51,7.94,0.17,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +156,VT084,7,HAVEN,NY0002,4,3,B,1524,0.5,0.5,VFSL-GR--SL-LFS,482.6,1.25,0.16,96,2.33,11.5,25.2,63.3,6.31,0.01,0.32,0,711.2,1.4,0.11,62,0.78,10,23.44,66.56,14.97,0.05,0.24,0,1524,1.55,0.1,500,0.26,1.5,16.96,81.54,10,0.14,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +157,VT084,8,UNADILLA,NH0046,4,3,B,1524,0.5,0.5,VFSL-VFSL-VFSL,76.2,1.35,0.18,110,2.62,10,28.29,61.71,1.29,0.01,0.49,0,609.6,1.35,0.15,84,0.87,9.5,28.45,62.05,1.29,0.04,0.64,0,1524,1.55,0.14,56,0.29,9.5,28.45,62.05,1.48,0.13,0.64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +158,VT084,11,SCIO,NY0221,4,3,B,1828.800049,0.5,0.5,SIL-VFSL-GRV-S,228.6,1.35,0.22,15,2.91,8.5,69.93,21.57,1.29,0.01,0.49,0,1016,1.35,0.14,110,0.97,8.5,28.76,62.74,1.29,0.04,0.24,0,1828.8,1.55,0.03,600,0.32,2.5,1.52,95.98,40,0.12,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +159,VT086,3,LANESBORO,MA0095,8,3,C,1524,0.5,0.5,STV-SIL-CN--SIL-CN--SIL,50.8,1.1,0.11,19,0,7,58.99,34.01,40,0.23,0.2,0,736.6,1.38,0.13,16,0,7,58.99,34.01,20.72,0.23,0.37,0,1524,1.83,0.12,16,0,6.5,59.31,34.19,27.81,0.23,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +160,VT086,8,TACONIC,NY0322,4,3,C,381,0.5,0.5,STV-L-CN--L-UWB,127,1.25,0.11,23,2.33,18.5,38.54,42.96,40,0.01,0.24,0,355.6,1.35,0.08,14,0.29,18.5,38.54,42.96,47.53,0.13,0.24,0,381,2.5,0.01,500,0.1,5,25,70,98,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +161,VT086,12,BRAYTON,ME0101,3,3,C,1651,0.5,0.5,STV-SIL-FSL-FSL,101.6,1.15,0.12,29,3.49,7,58.99,34.01,40,0.01,0.2,0,584.2,1.52,0.12,180,0.73,7,27.33,65.67,23.46,0.06,0.32,0,1651,1.85,0.1,110,0.15,7,27.33,65.67,27.1,0.17,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +162,VT086,17,FLUVAQUENTS,DE0202,2,1,D,25.39999962,0.5,0.5,,25.4,0,0,89,0,0,0,0,0,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +163,VT087,1,SAPRISTS,NY8006,28,1,A,1778,0.5,0.5,MUCK,1778,0.3,0.35,38,9.88,10,45,45,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +164,VT087,3,AQUENTS,NY8007,12,2,D,1778,0.5,0.5,L-SIL,228.6,1.23,0.19,81,6.4,17.5,39.47,43.03,4.79,0.01,0.37,0,1778,1.4,0.17,4.6,1.45,26.5,50,23.5,15.91,0.01,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +165,VT087,4,WAYLAND,NY0078,12,3,C,1828.800049,0.5,0.5,SIL-SICL-SIL,177.8,1.23,0.23,2.5,2.62,25,65.77,9.23,1.17,0.01,0.43,0,965.2,1.35,0.21,1.1,1.16,27.5,65.34,7.16,1.29,0.02,0.43,0,1828.8,1.4,0.19,1.2,0.87,20,68.57,11.43,13.3,0.04,0.43,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +166,VT087,7,SODUS,NY0013,2,3,C,1828.800049,0.5,0.5,L-VFSL-GR--VFSL,177.8,1.25,0.16,44,2.62,11.5,43.23,45.27,11.74,0.01,0.28,0,508,1.35,0.12,66,0.87,11.5,25.2,63.3,19.16,0.04,0.2,0,1828.8,1.85,0.09,33,0.29,11.5,25.2,63.3,33.12,0.13,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +167,VT087,8,IRA,NY0039,2,4,C,1524,0.5,0.5,FSL-GR--FSL-GR--FSL-GR--F,203.2,1.25,0.15,91,2.33,11.5,26.01,62.49,10.55,0.01,0.28,0,508,1.35,0.12,65,0.78,11.5,26.01,62.49,19.16,0.05,0.2,0,1016,1.85,0.1,37,0.26,11.5,26.01,62.49,24.51,0.14,0.2,0,1524,1.8,0.07,24,0.09,11.5,26.01,62.49,46.43,0.2,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +168,VT087,10,COLONIE,NY0086,2,3,A,2032,0.5,0.5,LVFS-FS-FS,203.2,1.35,0.11,850,0.87,1.5,16.92,81.58,1.29,0.04,0.17,0,1600.2,1.35,0.04,850,0.29,1.5,0.63,97.87,1.29,0.13,0.17,0,2032,1.55,0.04,900,0.1,1.5,0.63,97.87,1.48,0.19,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +169,VT087,11,BEACHES,DC0002,2,2,D,1524,0.5,0.5,S-COS,152.4,1.6,0.03,1000,0.03,0.5,1.55,97.95,7.94,0.22,0.05,2,1524,1.6,0.03,800,0.03,0.5,6.56,92.94,7.94,0.22,0.05,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +170,VT087,14,SUN,NY0051,1,3,D,1828.800049,0.5,0.5,L-GR--FSL-GR--FSL,228.6,1.25,0.18,97,5.23,11.5,43.23,45.27,7.51,0.01,0.28,0,914.4,1.35,0.1,69,0.29,11.5,26.01,62.49,19.16,0.13,0.2,0,1828.8,1.65,0.08,55,0.29,11.5,26.01,62.49,35,0.13,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +171,VT087,16,WILLIAMSON,NY0056,3,4,C,1524,0.5,0.5,SIL-VFSL-VFSL-VFSL,203.2,1.25,0.22,7,2.62,11,68.02,20.98,1.2,0.01,0.49,0,508,1.25,0.15,61,0.87,11,27.98,61.02,1.2,0.04,0.64,0,1219.2,1.45,0.14,40,0.29,11,27.98,61.02,1.38,0.13,0.64,0,1524,1.45,0.13,34,0.1,11,27.98,61.02,1.38,0.19,0.64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +172,VT088,14,HAMLIN,NY0074,2,4,B,2159,0.5,0.5,SIL-VFSL-VFSL-VFSL,228.6,1.27,0.23,3.6,2.33,13,72.68,14.32,1.22,0.01,0.49,0,508,1.3,0.15,55,0.78,11.5,27.82,60.68,1.24,0.05,0.49,0,914.4,1.3,0.14,36,0.26,11.5,27.82,60.68,1.24,0.14,0.49,0,2159,1.4,0.13,110,0.09,6.5,29.39,64.11,1.34,0.2,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +173,VT088,16,OAKVILLE,MI0038,2,3,A,1524,0.5,0.5,LFS-FS-S,177.8,1.42,0.09,260,0.73,8,6.57,85.43,2.75,0.06,0.17,0,863.6,1.48,0.05,500,0.24,5,0.61,94.39,1.41,0.14,0.15,0,1524,1.52,0.04,450,0.08,5,1.48,93.52,2.94,0.2,0.15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +174,VT089,6,MANLIUS,NY0021,7,4,C,787.4000244,0.5,0.5,CN--SIL-CNV-SIL-CNV-SIL-U,152.4,1.25,0.14,15,1.74,12,55.82,32.18,29.39,0.01,0.28,0,457.2,1.35,0.08,7.7,0.29,12,55.82,32.18,53.21,0.13,0.2,0,762,1.83,0.08,5.8,0.1,12,55.82,32.18,55,0.19,0.2,0,787.4,2.5,0.01,140,0.03,5,25,70,98,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +175,VT089,15,HOOSIC,NY0090,2,3,A,1524,0.5,0.5,SL-GR--SL-GRV-S,152.4,1.25,0.16,400,2.33,5.5,30.58,63.92,11.74,0.01,0.24,0,558.8,1.4,0.09,260,0.78,5.5,30.58,63.92,38.04,0.05,0.17,0,1524,1.55,0.02,450,0.26,2.5,1.52,95.98,54.5,0.14,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +176,VT089,16,SCRIBA,NY0049,2,3,C,1828.800049,0.5,0.5,L-GR--FSL-GR--FSL,330.2,1.25,0.16,62,2.91,9.5,44.2,46.3,11.74,0.01,0.28,0,1219.2,1.85,0.09,54,0.97,9.5,26.59,63.91,42.33,0.04,0.2,0,1828.8,1.8,0.08,46,0.32,9.5,26.59,63.91,44.12,0.12,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +177,VT090,1,NASSAU,NY0093,22,3,C,457.2000122,0.5,0.5,SIL-CNV-SIL-UWB,76.2,1.25,0.17,70,2.33,5.5,58.02,36.48,11.74,0.01,0.32,0,431.8,1.35,0.09,37,0.29,5.5,58.02,36.48,50.84,0.13,0.2,0,457.2,2.5,0.01,550,0.1,5,25,70,98,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +178,VT090,11,UDIFLUVENTS,DE0209,2,1,D,25.39999962,0.5,0.5,,25.4,0,0,500,0,0,0,0,0,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +179,VT091,4,URBAN LAND,DC0035,8,1,D,152.3999939,0.5,0.5,VAR,152.4,1.5,0.1,500,0,15,30,55,20,0.23,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +180,VT091,6,CHARLTON,CT0002,6,3,B,1651,0.5,0.5,L-FSL-GR--SL,203.2,1.13,0.16,72,2.03,7.5,45.18,47.32,9.36,0.01,0.24,0,660.4,1.52,0.12,150,0.68,5.5,34.3,60.2,20.26,0.06,0.24,0,1651,1.58,0.1,180,0.23,4.5,30.8,64.7,28.8,0.15,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +181,VT091,10,BLASDELL,NY0088,4,3,A,1524,0.5,0.5,CN--SIL-CNV-SIL-CNV-SIL,203.2,1.25,0.15,13,2.62,12,55.82,32.18,25.61,0.01,0.28,0,914.4,1.4,0.1,5,0.87,12,55.82,32.18,49.87,0.04,0.17,0,1524,1.55,0.08,4.1,0.29,12,55.82,32.18,55,0.13,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +182,VT091,12,KNICKERBOCKER,NY0304,3,5,A,1828.800049,0.5,0.5,FSL-FSL-LFS-S-GR--S,304.8,1.25,0.17,230,2.91,8.5,26.89,64.61,6.31,0.01,0.2,0,508,1.4,0.12,160,0.29,8.5,26.89,64.61,7.02,0.13,0.2,0,889,1.55,0.09,400,0.29,5,16.35,78.65,7.71,0.13,0.17,0,1346.2,1.55,0.04,400,0.29,5,1.48,93.52,7.71,0.13,0.17,0,1828.8,1.55,0.04,400,0.29,5,1.48,93.52,15.91,0.13,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +183,VT091,13,OCCUM,NY0528,3,3,B,1346.199951,0.5,0.5,SIL-SL-GR--S,203.2,1.27,0.19,16,2.33,11.5,56.14,32.36,6.43,0.01,0.32,0,762,1.27,0.12,52,0.78,11.5,23.11,65.39,6.43,0.05,0.32,0,1346.2,1.5,0.04,450,0.26,3.5,1.5,95,26.62,0.14,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +184,VT091,18,PALMS,MI0023,2,3,A,1524,0.5,0.5,MUCK-MUCK-GR--SL,355.6,0.3,0.35,57,9.88,10,45,45,0,0.01,0.1,0,889,0.3,0.35,37,9.88,10,45,45,0,0.01,0.1,0,1524,1.6,0.13,50,3.29,19.5,14.91,65.59,13.11,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +185,VT093,13,CARLISLE,MI0020,2,1,A,1676.400024,0.5,0.5,MUCK,1676.4,0.3,0.35,17,9.88,10,45,45,1.18,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +186,VT093,15,HUDSON,NY0038,2,4,C,1828.800049,0.5,0.5,SICL-SIC-SIC-SIC,203.2,1.13,0.19,6.3,2.62,30,52.15,17.85,1.08,0.01,0.49,0,406.4,1.27,0.18,0.33,0.29,47.5,46.98,5.52,2.47,0.13,0.28,0,711.2,1.27,0.17,0.36,0.29,42.5,49.8,7.7,2.47,0.13,0.28,0,1828.8,1.27,0.15,0.22,0.29,47.5,46.98,5.52,2.47,0.13,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +187,VT093,17,TEEL,NY0076,1,3,B,1828.800049,0.5,0.5,SIL-VFSL-FSL,254,1.27,0.22,5.1,2.33,13,72.68,14.32,1.22,0.01,0.49,0,965.2,1.3,0.14,56,0.29,11.5,27.82,60.68,1.24,0.13,0.49,0,1828.8,1.4,0.12,190,0.29,6.5,27.48,66.03,9.84,0.13,0.49,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +188,VT094,1,KEARSARGE,NH0031,8,3,B,457.2000122,0.5,0.5,STV-SIL-SIL-UWB,101.6,1.1,0.11,5.4,0,13,56.26,30.74,40,0.23,0.28,0,431.8,1.35,0.15,8.8,0,11,56.46,32.54,12.32,0.23,0.37,0,457.2,2.5,0.01,600,0,5,25,70,98,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +189,VT094,4,CARDIGAN,NH0033,8,4,B,787.4000244,0.5,0.5,STV-SIL-SIL-SIL-UWB,101.6,1.1,0.11,5.4,0,13,56.26,30.74,40,0.23,0.28,0,558.8,1.3,0.15,6,0,13,56.26,30.74,11.92,0.23,0.37,0,762,1.6,0.14,7.9,0,10,57.09,32.91,19.59,0.23,0.37,0,787.4,2.5,0.01,140,0,5,25,70,98,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +190,VT094,8,DUTCHESS,VT0052,5,3,B,1651,0.5,0.5,SIL-SIL-CNV-FSL,101.6,1.1,0.19,54,3.2,7,58.99,34.01,6.55,0.01,0.32,0,660.4,1.4,0.14,28,0.29,7,58.99,34.01,19.91,0.13,0.37,0,1651,1.5,0.09,140,0.29,7,27.33,65.67,33.31,0.13,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +191,VT094,9,STISSING,MA0050,5,3,C,1524,0.5,0.5,STV-SIL-CN--SIL-CN--SIL,228.6,1.05,0.11,19,0,7,58.99,34.01,40,0.23,0.2,0,406.4,1.35,0.14,21,0,7,58.99,34.01,18.36,0.23,0.37,0,1524,1.8,0.12,16,0,6.5,59.31,34.19,31.06,0.23,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +192,VT094,10,WARWICK,MA0059,5,3,A,1651,0.5,0.5,FSL-SY--FSL-LS,203.2,1.15,0.15,220,1.74,5.5,34.3,60.2,14.63,0.01,0.24,0,762,1.3,0.1,120,0.58,5.5,34.3,60.2,34.19,0.08,0.24,0,1651,1.4,0.09,500,0.19,1.5,16.52,81.98,10,0.16,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +193,VT094,17,BERNARDSTON,MA0010,3,3,C,1651,0.5,0.5,STV-SIL-CN--SIL-CN--SIL,152.4,1.1,0.12,24,2.03,7,58.99,34.01,40,0.01,0.2,0,508,1.38,0.14,22,0.68,7,58.99,34.01,20.72,0.06,0.37,0,1651,1.83,0.12,17,0.23,6.5,59.31,34.19,27.81,0.15,0.28,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +194,VT095,2,SKERRY,NH0004,9,3,C,1651,0.5,0.5,STV-FSL-GR--FSL-GR--LS,101.6,0.95,0.09,250,0,4,34.85,61.15,40,0.23,0.2,0,635,1.45,0.12,240,0,4.5,34.67,60.83,19.18,0.23,0.28,0,1651,1.67,0.07,350,0,3,16.2,80.8,30,0.23,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +195,VT095,3,SUCCESS,NH0052,9,4,A,1651,0.5,0.5,STV-SL-LS-S-S,177.8,0.8,0.16,700,9.88,3.5,31.01,65.49,40,0.01,0.17,0,482.6,1.1,0.17,350,3.29,6.5,9.12,84.38,10,0.01,0.2,0,1066.8,1.58,0.04,290,1.1,4,1.49,94.51,53.08,0.03,0.1,0,1651,1.35,0.03,350,0.37,3,1.51,95.49,46.65,0.11,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +196,VT095,10,PERU,NH0014,5,3,C,1651,0.5,0.5,STV-FSL-FSL-FSL,152.4,0.9,0.08,150,0,6.5,27.48,66.03,40,0.23,0.2,0,558.8,1.45,0.11,160,0,6.5,27.48,66.03,19.45,0.23,0.32,0,1651,1.83,0.1,120,0,6.5,27.48,66.03,23.31,0.23,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +197,VT095,14,WAUMBEK,NH0016,3,3,B,1651,0.5,0.5,STV-FSL-GRV-LFS-CB--COS,254,0.95,0.09,300,0,3,35.21,61.79,40,0.23,0.17,0,660.4,1.1,0.07,400,0,3,16.7,80.3,23.11,0.23,0.17,0,1651,1.67,0.02,450,0,1.5,6.49,92.01,57.89,0.23,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +198,VT096,8,PILLSBURY,NH0038,5,3,C,1651,0.5,0.5,STV-L-FSL-FSL,127,1.15,0.09,46,0,7.5,45.18,47.32,40,0.23,0.24,0,558.8,1.4,0.11,160,0,6,34.12,59.88,18.91,0.23,0.32,0,1651,1.9,0.1,110,0,6,34.12,59.88,24.04,0.23,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +199,VT096,9,ROCK OUTCROP,DC0015,5,1,D,1524,0.5,0.5,UWB,1524,2.5,0.01,180,0,5,25,70,98,0.23,0.01,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +200,VT096,11,BECKET,NH0002,5,3,C,1651,0.5,0.5,STV-FSL-FSL-SL,50.8,0.95,0.09,250,0,4,34.85,61.15,40,0.23,0.17,0,787.4,1.45,0.11,180,0,4.5,34.67,60.83,19.18,0.23,0.28,0,1651,1.67,0.12,300,0,3,31.12,65.88,10,0.23,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +201,VTW,7,WATER,DC0038,3,1,D,25.39999962,0.5,0.5,,25.4,1.72,0,260,0,0,0,0,0,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +202,VTPIT,10,PITS,NY0029,4,4,A,1828.800049,0.5,0.5,GR--LS-GRV-S-GRV-S-CBV-S,203.2,1.25,0.12,650,2.33,3,16.2,80.8,30,0.01,0.15,0,228.6,1.3,0.04,700,0.73,3.5,1.5,95,38.92,0.06,0.24,0,558.8,1.4,0.03,850,0.15,2.5,1.52,95.98,40.69,0.17,0.15,0,1828.8,1.55,0.01,400,0.05,1.5,1.53,96.97,65.69,0.21,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +203,xxx,0,DK998,xxx,0,3,A,800,0.5,0.5,FK3,300,1.32,0.2,28.49,2.52,7.73,16.58,75.69,0,0.01,0.18,0,700,1.42,0.18,13.01,2.08,8.71,14.83,76.45,0,0.01,0.18,0,1200,1.6,0.16,12.54,0.64,7.73,16.58,75.69,0,0.01,0.18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +204,xxx,0,DK999,xxx,0,3,A,800,0.5,0.5,FK4,300,0.7,0.24,23.43,12.6,8.71,14.83,76.45,0,0.01,0.21,0,700,0.9,0.22,13.87,7.93,9.4,12.81,77.79,0,0.01,0.21,0,1200,1.6,0.16,11.94,0.64,9.59,16.05,74.36,0,0.01,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +205,xxx,0,DK1011,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.16,36.55,1.65,3.3,9.49,87.21,0,0.01,0.11,0,700,1.45,0.13,30.93,0.46,1.11,8.59,90.31,0,0.01,0.11,0,1200,1.43,0.11,24.26,0.06,1.6,5.62,92.78,0,0.01,0.11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +206,xxx,0,DK1018,xxx,0,3,A,500,0.5,0.5,FK2,300,1.44,0.17,39.07,1.65,3.3,9.49,87.21,0,0.01,0.13,0,700,1.46,0.17,20.79,0.8,5.17,14.37,80.46,0,0.01,0.13,0,1200,1.64,0.15,12.14,0.21,8.43,13.09,78.48,0,0.01,0.13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +207,xxx,0,DK1021,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.46,0.18,26.3,1.57,2.88,15.67,81.45,0,0.01,0.19,0,700,1.45,0.13,30.93,0.46,1.11,8.59,90.31,0,0.01,0.19,0,1200,1.43,0.11,24.26,0.06,1.6,5.62,92.78,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +208,xxx,0,DK1024,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.39,0.19,33.01,1.57,2.88,15.67,81.45,0,0.01,0.2,0,700,1.45,0.15,26,0.63,2.43,11.32,86.25,0,0.01,0.2,0,1200,1.66,0.15,11.45,0.19,6.02,15.66,78.32,0,0.01,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +209,xxx,0,DK1026,xxx,0,3,A,1200,0.5,0.5,FK2,300,1.46,0.18,26.74,1.57,2.88,15.67,81.45,0,0.01,0.2,0,700,1.52,0.17,13.6,1.05,8.05,18.16,73.79,0,0.01,0.2,0,1200,1.77,0.14,5.68,0.17,14.04,18.2,67.75,0,0.01,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +210,xxx,0,DK1046,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.41,0.2,21.96,2.02,8.19,19.86,71.95,0,0.01,0.22,0,700,1.52,0.17,13.6,1.05,8.05,18.16,73.79,0,0.01,0.22,0,1200,1.77,0.14,5.68,0.17,14.04,18.2,67.75,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +211,xxx,0,DK1047,xxx,0,3,A,1500,0.5,0.5,FK3,300,1.41,0.2,21.96,2.02,8.19,19.86,71.95,0,0.01,0.23,0,700,1.66,0.16,7.57,0.93,11.79,20.75,67.46,0,0.01,0.23,0,1200,1.67,0.15,6.75,0.18,18.76,18.61,62.64,0,0.01,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +212,xxx,0,DK1048,xxx,0,3,A,800,0.5,0.5,FK3,300,1.38,0.2,24.32,2.02,8.19,19.86,71.95,0,0.01,0.22,0,700,1.46,0.17,20.79,0.8,5.17,14.37,80.46,0,0.01,0.22,0,1200,1.64,0.15,12.14,0.21,8.43,13.09,78.48,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +213,xxx,0,DK1067,xxx,0,3,C,1500,0.5,0.5,FK4,300,1.54,0.18,12.5,1.98,12.04,22.63,65.34,0,0.01,0.23,0,700,1.66,0.16,7.62,0.93,11.79,20.75,67.46,0,0.01,0.23,0,1200,1.67,0.15,6.77,0.18,18.76,18.61,62.64,0,0.01,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +214,xxx,0,DK2011,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.16,40.4,1.55,2.67,8.63,88.69,0,0.01,0.14,0,700,1.45,0.13,31.69,0.21,1.71,7.74,90.55,0,0.01,0.14,0,1200,1.43,0.12,29.94,0.09,1.9,6.26,91.83,0,0.01,0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +215,xxx,0,DK2014,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.44,0.16,43.14,1.55,2.67,8.63,88.69,0,0.01,0.19,0,700,1.45,0.17,22.55,0.58,4.34,17,78.65,0,0.01,0.19,0,1200,1.6,0.16,7.4,0.5,20.91,27.1,51.99,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +216,xxx,0,DK2021,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.19,20.43,1.86,3.72,18.68,77.6,0,0.01,0.3,0,700,1.45,0.13,31.69,0.21,1.71,7.74,90.55,0,0.01,0.3,0,1200,1.43,0.12,29.94,0.09,1.9,6.26,91.83,0,0.01,0.3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +217,xxx,0,DK2024,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.39,0.2,25.82,1.86,3.72,18.68,77.6,0,0.01,0.3,0,700,1.45,0.17,22.55,0.58,4.34,17,78.65,0,0.01,0.3,0,1200,1.6,0.16,7.4,0.5,20.91,27.1,51.99,0,0.01,0.3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +218,xxx,0,DK2028,xxx,0,3,A,800,0.5,0.5,FK2,300,1.39,0.2,25.82,1.86,3.72,18.68,77.6,0,0.01,0.31,0,700,1.46,0.18,18.21,0.87,6.09,20.69,73.23,0,0.01,0.31,0,1200,1.66,0.16,9.64,0.15,9.53,17.54,72.94,0,0.01,0.31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +219,xxx,0,DK2044,xxx,0,3,A,1000,0.5,0.5,FK3,300,1.38,0.2,22.47,2.03,6.63,21.5,71.87,0,0.01,0.32,0,700,1.45,0.17,22.55,0.58,4.34,17,78.65,0,0.01,0.32,0,1200,1.6,0.16,7.4,0.5,20.91,27.1,51.99,0,0.01,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +220,xxx,0,DK2048,xxx,0,3,A,800,0.5,0.5,FK3,300,1.38,0.2,22.47,2.03,6.63,21.5,71.87,0,0.01,0.32,0,700,1.46,0.18,18.21,0.87,6.09,20.69,73.23,0,0.01,0.32,0,1200,1.66,0.16,9.64,0.15,9.53,17.54,72.94,0,0.01,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +221,xxx,0,DK2211,xxx,0,3,A,800,0.5,0.5,FK2,300,1.4,0.2,24.99,1.86,3.72,18.68,77.6,0,0.01,0.31,0,700,1.47,0.18,18.49,0.85,5.17,19.03,75.79,0,0.01,0.31,0,1200,1.65,0.15,11.6,0.17,5.52,17.42,77.06,0,0.01,0.31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +222,xxx,0,DK2411,xxx,0,3,A,800,0.5,0.5,FK3,300,1.42,0.2,19.62,2.03,6.63,21.5,71.87,0,0.01,0.32,0,700,1.47,0.18,18.49,0.85,5.17,19.03,75.79,0,0.01,0.32,0,1200,1.65,0.15,11.6,0.17,5.52,17.42,77.06,0,0.01,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +223,xxx,0,DK3011,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.16,40.4,1.55,2.67,8.63,88.69,0,0.01,0.1,0,700,1.45,0.13,31.69,0.21,1.71,7.74,90.55,0,0.01,0.1,0,1200,1.43,0.12,29.94,0.09,1.9,6.26,91.83,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +224,xxx,0,DK3012,xxx,0,3,A,500,0.5,0.5,FK2,300,1.32,0.19,53.01,2.04,3.63,8.65,87.72,0,0.01,0.11,0,700,1.57,0.14,16.07,1.05,3.97,8.66,87.37,0,0.01,0.11,0,1200,1.53,0.1,29.51,0.23,2.61,5.17,92.22,0,0.01,0.11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +225,xxx,0,DK3017,xxx,0,3,A,1200,0.5,0.5,FK2,300,1.46,0.17,33.86,2.04,3.63,8.65,87.72,0,0.01,0.12,0,700,1.52,0.16,17.22,0.63,7.58,13.87,78.55,0,0.01,0.12,0,1200,1.76,0.14,6.4,0.13,12.44,13.34,74.22,0,0.01,0.12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +226,xxx,0,DK3018,xxx,0,3,A,500,0.5,0.5,FK2,300,1.44,0.17,35.65,2.04,3.63,8.65,87.72,0,0.01,0.11,0,700,1.45,0.15,24.36,0.75,5.26,10.05,84.68,0,0.01,0.11,0,1200,1.57,0.12,18.37,0.12,4.51,7.8,87.69,0,0.01,0.11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +227,xxx,0,DK3028,xxx,0,3,A,500,0.5,0.5,FK2,300,1.39,0.19,28.1,2.05,3.32,15.18,81.5,0,0.01,0.18,0,700,1.45,0.15,24.36,0.75,5.26,10.05,84.68,0,0.01,0.18,0,1200,1.57,0.12,18.37,0.12,4.51,7.8,87.69,0,0.01,0.18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +228,xxx,0,DK3037,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.48,0.18,24.15,2.08,6.12,12.64,81.24,0,0.01,0.17,0,700,1.52,0.16,17.22,0.63,7.58,13.87,78.55,0,0.01,0.17,0,1200,1.76,0.14,6.4,0.13,12.44,13.34,74.22,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +229,xxx,0,DK3038,xxx,0,3,A,500,0.5,0.5,FK3,300,1.4,0.19,31.85,2.08,6.12,12.64,81.24,0,0.01,0.14,0,700,1.45,0.15,24.36,0.75,5.26,10.05,84.68,0,0.01,0.14,0,1200,1.57,0.12,18.37,0.12,4.51,7.8,87.69,0,0.01,0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +230,xxx,0,DK3047,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.41,0.19,22.68,2.36,6.57,16.04,77.39,0,0.01,0.17,0,700,1.52,0.16,17.22,0.63,7.58,13.87,78.55,0,0.01,0.17,0,1200,1.76,0.14,6.4,0.13,12.44,13.34,74.22,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +231,xxx,0,DK3048,xxx,0,3,A,500,0.5,0.5,FK3,300,1.38,0.2,25.17,2.36,6.57,16.04,77.39,0,0.01,0.17,0,700,1.45,0.15,24.36,0.75,5.26,10.05,84.68,0,0.01,0.17,0,1200,1.57,0.12,18.37,0.12,4.51,7.8,87.69,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +232,xxx,0,DK3110,xxx,0,3,A,500,0.5,0.5,FK2,300,1.44,0.17,35.65,2.04,3.63,8.65,87.72,0,0.01,0.1,0,700,1.52,0.12,25.79,0.61,3.24,6.4,90.36,0,0.01,0.1,0,1200,1.57,0.09,30.5,0.15,2.31,3.49,94.2,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +233,xxx,0,DK4016,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.18,29.44,1.69,4.01,13.01,82.97,0,0.01,0.19,0,700,1.52,0.16,17.6,0.71,5.17,14.84,79.99,0,0.01,0.19,0,1200,1.68,0.14,10.26,0.12,7.01,11.32,81.66,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +234,xxx,0,DK4018,xxx,0,3,A,500,0.5,0.5,FK2,300,1.44,0.18,30.96,1.69,4.01,13.01,82.97,0,0.01,0.17,0,700,1.46,0.16,20.7,0.83,5.18,14.04,80.78,0,0.01,0.17,0,1200,1.64,0.14,10.99,0.12,8.02,13.33,78.66,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +235,xxx,0,DK4027,xxx,0,3,A,1200,0.5,0.5,FK2,300,1.46,0.19,23.32,1.82,3.92,16.55,79.53,0,0.01,0.21,0,700,1.66,0.16,9.12,0.58,11.32,17.81,70.86,0,0.01,0.21,0,1200,1.67,0.15,6.53,0.14,19.06,16.32,64.63,0,0.01,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +236,xxx,0,DK4037,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.48,0.18,27.77,1.59,6.79,13.79,79.42,0,0.01,0.19,0,700,1.66,0.16,9.12,0.58,11.32,17.81,70.86,0,0.01,0.19,0,1200,1.67,0.15,6.53,0.14,19.06,16.32,64.63,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +237,xxx,0,DK4038,xxx,0,3,A,500,0.5,0.5,FK3,300,1.4,0.19,36.24,1.59,6.79,13.79,79.42,0,0.01,0.17,0,700,1.46,0.16,20.7,0.83,5.18,14.04,80.78,0,0.01,0.17,0,1200,1.64,0.14,10.99,0.12,8.02,13.33,78.66,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +238,xxx,0,DK4046,xxx,0,3,A,500,0.5,0.5,FK3,300,1.41,0.19,24.51,1.83,7.12,19.01,73.87,0,0.01,0.22,0,700,1.52,0.16,17.6,0.71,5.17,14.84,79.99,0,0.01,0.22,0,1200,1.68,0.19,24.51,0,7,11.32,81.68,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +239,xxx,0,DK4047,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.41,0.19,24.51,1.83,7.12,19.01,73.87,0,0.01,0.21,0,700,1.66,0.16,9.12,0.58,11.32,17.81,70.86,0,0.01,0.21,0,1200,1.67,0.15,6.53,0.14,19.06,16.32,64.63,0,0.01,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +240,xxx,0,DK4048,xxx,0,3,A,500,0.5,0.5,FK3,300,1.38,0.2,27.08,1.83,7.12,19.01,73.87,0,0.01,0.22,0,700,1.46,0.16,20.7,0.83,5.18,14.04,80.78,0,0.01,0.22,0,1200,1.64,0.14,10.99,0.12,8.02,13.33,78.66,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +241,xxx,0,DK4067,xxx,0,3,B,1500,0.5,0.5,FK4,300,1.54,0.18,15.86,1.42,12.5,22.63,64.87,0,0.01,0.22,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.22,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +242,xxx,0,DK5016,xxx,0,3,A,800,0.5,0.5,FK2,300,1.46,0.16,41,1.19,3.88,10.78,85.34,0,0.01,0.19,0,700,1.52,0.17,15.84,0.61,8.89,16.92,74.19,0,0.01,0.19,0,1200,1.73,0.15,7.66,0.14,9.22,16.17,74.62,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +243,xxx,0,DK5018,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.44,0.17,43.01,1.19,3.88,10.78,85.34,0,0.01,0.17,0,700,1.46,0.17,20.14,0.57,7.98,16.16,75.86,0,0.01,0.17,0,1200,1.66,0.15,10.86,0.16,8.22,13.37,78.4,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +244,xxx,0,DK5037,xxx,0,3,A,1500,0.5,0.5,FK3,300,1.48,0.18,26.98,1.45,7.99,15.51,76.5,0,0.01,0.22,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.22,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +245,xxx,0,DK5038,xxx,0,3,A,1000,0.5,0.5,FK3,300,1.4,0.19,35.14,1.45,7.99,15.51,76.5,0,0.01,0.2,0,700,1.46,0.17,20.14,0.57,7.98,16.16,75.86,0,0.01,0.2,0,1200,1.66,0.15,10.86,0.16,8.22,13.37,78.4,0,0.01,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +246,xxx,0,DK5046,xxx,0,3,A,800,0.5,0.5,FK3,300,1.41,0.19,27.73,1.4,8.71,20.06,71.23,0,0.01,0.22,0,700,1.52,0.17,15.84,0.61,8.89,16.92,74.19,0,0.01,0.22,0,1200,1.73,0.15,7.66,0.14,9.22,16.17,74.62,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +247,xxx,0,DK5047,xxx,0,3,A,1500,0.5,0.5,FK3,300,1.41,0.19,27.73,1.4,8.71,20.06,71.23,0,0.01,0.23,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.23,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +248,xxx,0,DK5048,xxx,0,3,A,1000,0.5,0.5,FK3,300,1.38,0.2,30.54,1.4,8.71,20.06,71.23,0,0.01,0.22,0,700,1.46,0.17,20.14,0.57,7.98,16.16,75.86,0,0.01,0.22,0,1200,1.66,0.15,10.86,0.16,8.22,13.37,78.4,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +249,xxx,0,DK5067,xxx,0,3,B,1500,0.5,0.5,FK4,300,1.54,0.18,15.86,1.42,12.5,22.63,64.87,0,0.01,0.24,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.24,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +250,xxx,0,DK5077,xxx,0,3,B,1500,0.5,0.5,FK4,300,1.58,0.18,14.76,1.42,12.5,22.63,64.87,0,0.01,0.23,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.23,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +301,xxx,0,DK10110,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.16,36.55,1.65,3.3,9.49,87.21,0,0.01,0.11,0,700,1.45,0.13,30.93,0.46,1.11,8.59,90.31,0,0.01,0.11,0,1200,1.43,0.11,24.26,0.06,1.6,5.62,92.78,0,0.01,0.11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +302,xxx,0,DK10180,xxx,0,3,A,500,0.5,0.5,FK2,300,1.44,0.17,39.07,1.65,3.3,9.49,87.21,0,0.01,0.13,0,700,1.46,0.17,20.79,0.8,5.17,14.37,80.46,0,0.01,0.13,0,1200,1.64,0.15,12.14,0.21,8.43,13.09,78.48,0,0.01,0.13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +303,xxx,0,DK10210,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.46,0.18,26.3,1.57,2.88,15.67,81.45,0,0.01,0.19,0,700,1.45,0.13,30.93,0.46,1.11,8.59,90.31,0,0.01,0.19,0,1200,1.43,0.11,24.26,0.06,1.6,5.62,92.78,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +304,xxx,0,DK10240,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.39,0.19,33.01,1.57,2.88,15.67,81.45,0,0.01,0.2,0,700,1.45,0.15,26,0.63,2.43,11.32,86.25,0,0.01,0.2,0,1200,1.66,0.15,11.45,0.19,6.02,15.66,78.32,0,0.01,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +305,xxx,0,DK10260,xxx,0,3,A,1200,0.5,0.5,FK2,300,1.46,0.18,26.74,1.57,2.88,15.67,81.45,0,0.01,0.2,0,700,1.52,0.17,13.6,1.05,8.05,18.16,73.79,0,0.01,0.2,0,1200,1.77,0.14,5.68,0.17,14.04,18.2,67.75,0,0.01,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +306,xxx,0,DK10460,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.41,0.2,21.96,2.02,8.19,19.86,71.95,0,0.01,0.22,0,700,1.52,0.17,13.6,1.05,8.05,18.16,73.79,0,0.01,0.22,0,1200,1.77,0.14,5.68,0.17,14.04,18.2,67.75,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +307,xxx,0,DK10470,xxx,0,3,A,1500,0.5,0.5,FK3,300,1.41,0.2,21.96,2.02,8.19,19.86,71.95,0,0.01,0.23,0,700,1.66,0.16,7.57,0.93,11.79,20.75,67.46,0,0.01,0.23,0,1200,1.67,0.15,6.75,0.18,18.76,18.61,62.64,0,0.01,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +308,xxx,0,DK10480,xxx,0,3,A,800,0.5,0.5,FK3,300,1.38,0.2,24.32,2.02,8.19,19.86,71.95,0,0.01,0.22,0,700,1.46,0.17,20.79,0.8,5.17,14.37,80.46,0,0.01,0.22,0,1200,1.64,0.15,12.14,0.21,8.43,13.09,78.48,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +309,xxx,0,DK10670,xxx,0,3,C,1500,0.5,0.5,FK4,300,1.54,0.18,12.5,1.98,12.04,22.63,65.34,0,0.01,0.23,0,700,1.66,0.16,7.62,0.93,11.79,20.75,67.46,0,0.01,0.23,0,1200,1.67,0.15,6.77,0.18,18.76,18.61,62.64,0,0.01,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +310,xxx,0,DK20110,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.16,40.4,1.55,2.67,8.63,88.69,0,0.01,0.14,0,700,1.45,0.13,31.69,0.21,1.71,7.74,90.55,0,0.01,0.14,0,1200,1.43,0.12,29.94,0.09,1.9,6.26,91.83,0,0.01,0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +311,xxx,0,DK20140,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.44,0.16,43.14,1.55,2.67,8.63,88.69,0,0.01,0.19,0,700,1.45,0.17,22.55,0.58,4.34,17,78.65,0,0.01,0.19,0,1200,1.6,0.16,7.4,0.5,20.91,27.1,51.99,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +312,xxx,0,DK20210,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.19,20.43,1.86,3.72,18.68,77.6,0,0.01,0.3,0,700,1.45,0.13,31.69,0.21,1.71,7.74,90.55,0,0.01,0.3,0,1200,1.43,0.12,29.94,0.09,1.9,6.26,91.83,0,0.01,0.3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +313,xxx,0,DK20240,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.39,0.2,25.82,1.86,3.72,18.68,77.6,0,0.01,0.3,0,700,1.45,0.17,22.55,0.58,4.34,17,78.65,0,0.01,0.3,0,1200,1.6,0.16,7.4,0.5,20.91,27.1,51.99,0,0.01,0.3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +314,xxx,0,DK20280,xxx,0,3,A,800,0.5,0.5,FK2,300,1.39,0.2,25.82,1.86,3.72,18.68,77.6,0,0.01,0.31,0,700,1.46,0.18,18.21,0.87,6.09,20.69,73.23,0,0.01,0.31,0,1200,1.66,0.16,9.64,0.15,9.53,17.54,72.94,0,0.01,0.31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +315,xxx,0,DK20440,xxx,0,3,A,1000,0.5,0.5,FK3,300,1.38,0.2,22.47,2.03,6.63,21.5,71.87,0,0.01,0.32,0,700,1.45,0.17,22.55,0.58,4.34,17,78.65,0,0.01,0.32,0,1200,1.6,0.16,7.4,0.5,20.91,27.1,51.99,0,0.01,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +316,xxx,0,DK20480,xxx,0,3,A,800,0.5,0.5,FK3,300,1.38,0.2,22.47,2.03,6.63,21.5,71.87,0,0.01,0.32,0,700,1.46,0.18,18.21,0.87,6.09,20.69,73.23,0,0.01,0.32,0,1200,1.66,0.16,9.64,0.15,9.53,17.54,72.94,0,0.01,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +317,xxx,0,DK22110,xxx,0,3,A,800,0.5,0.5,FK2,300,1.4,0.2,24.99,1.86,3.72,18.68,77.6,0,0.01,0.31,0,700,1.47,0.18,18.49,0.85,5.17,19.03,75.79,0,0.01,0.31,0,1200,1.65,0.15,11.6,0.17,5.52,17.42,77.06,0,0.01,0.31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +318,xxx,0,DK24110,xxx,0,3,A,800,0.5,0.5,FK3,300,1.42,0.2,19.62,2.03,6.63,21.5,71.87,0,0.01,0.32,0,700,1.47,0.18,18.49,0.85,5.17,19.03,75.79,0,0.01,0.32,0,1200,1.65,0.15,11.6,0.17,5.52,17.42,77.06,0,0.01,0.32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +319,xxx,0,DK30110,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.16,40.4,1.55,2.67,8.63,88.69,0,0.01,0.1,0,700,1.45,0.13,31.69,0.21,1.71,7.74,90.55,0,0.01,0.1,0,1200,1.43,0.12,29.94,0.09,1.9,6.26,91.83,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +320,xxx,0,DK30120,xxx,0,3,A,500,0.5,0.5,FK2,300,1.32,0.19,53.01,2.04,3.63,8.65,87.72,0,0.01,0.11,0,700,1.57,0.14,16.07,1.05,3.97,8.66,87.37,0,0.01,0.11,0,1200,1.53,0.1,29.51,0.23,2.61,5.17,92.22,0,0.01,0.11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +321,xxx,0,DK30170,xxx,0,3,A,1200,0.5,0.5,FK2,300,1.46,0.17,33.86,2.04,3.63,8.65,87.72,0,0.01,0.12,0,700,1.52,0.16,17.22,0.63,7.58,13.87,78.55,0,0.01,0.12,0,1200,1.76,0.14,6.4,0.13,12.44,13.34,74.22,0,0.01,0.12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +322,xxx,0,DK30180,xxx,0,3,A,500,0.5,0.5,FK2,300,1.44,0.17,35.65,2.04,3.63,8.65,87.72,0,0.01,0.11,0,700,1.45,0.15,24.36,0.75,5.26,10.05,84.68,0,0.01,0.11,0,1200,1.57,0.12,18.37,0.12,4.51,7.8,87.69,0,0.01,0.11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +323,xxx,0,DK30280,xxx,0,3,A,500,0.5,0.5,FK2,300,1.39,0.19,28.1,2.05,3.32,15.18,81.5,0,0.01,0.18,0,700,1.45,0.15,24.36,0.75,5.26,10.05,84.68,0,0.01,0.18,0,1200,1.57,0.12,18.37,0.12,4.51,7.8,87.69,0,0.01,0.18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +324,xxx,0,DK30370,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.48,0.18,24.15,2.08,6.12,12.64,81.24,0,0.01,0.17,0,700,1.52,0.16,17.22,0.63,7.58,13.87,78.55,0,0.01,0.17,0,1200,1.76,0.14,6.4,0.13,12.44,13.34,74.22,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +325,xxx,0,DK30380,xxx,0,3,A,500,0.5,0.5,FK3,300,1.4,0.19,31.85,2.08,6.12,12.64,81.24,0,0.01,0.14,0,700,1.45,0.15,24.36,0.75,5.26,10.05,84.68,0,0.01,0.14,0,1200,1.57,0.12,18.37,0.12,4.51,7.8,87.69,0,0.01,0.14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +326,xxx,0,DK30470,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.41,0.19,22.68,2.36,6.57,16.04,77.39,0,0.01,0.17,0,700,1.52,0.16,17.22,0.63,7.58,13.87,78.55,0,0.01,0.17,0,1200,1.76,0.14,6.4,0.13,12.44,13.34,74.22,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +327,xxx,0,DK30480,xxx,0,3,A,500,0.5,0.5,FK3,300,1.38,0.2,25.17,2.36,6.57,16.04,77.39,0,0.01,0.17,0,700,1.45,0.15,24.36,0.75,5.26,10.05,84.68,0,0.01,0.17,0,1200,1.57,0.12,18.37,0.12,4.51,7.8,87.69,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +328,xxx,0,DK31100,xxx,0,3,A,500,0.5,0.5,FK2,300,1.44,0.17,35.65,2.04,3.63,8.65,87.72,0,0.01,0.1,0,700,1.52,0.12,25.79,0.61,3.24,6.4,90.36,0,0.01,0.1,0,1200,1.57,0.09,30.5,0.15,2.31,3.49,94.2,0,0.01,0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +329,xxx,0,DK40160,xxx,0,3,A,500,0.5,0.5,FK2,300,1.46,0.18,29.44,1.69,4.01,13.01,82.97,0,0.01,0.19,0,700,1.52,0.16,17.6,0.71,5.17,14.84,79.99,0,0.01,0.19,0,1200,1.68,0.14,10.26,0.12,7.01,11.32,81.66,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +330,xxx,0,DK40180,xxx,0,3,A,500,0.5,0.5,FK2,300,1.44,0.18,30.96,1.69,4.01,13.01,82.97,0,0.01,0.17,0,700,1.46,0.16,20.7,0.83,5.18,14.04,80.78,0,0.01,0.17,0,1200,1.64,0.14,10.99,0.12,8.02,13.33,78.66,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +331,xxx,0,DK40270,xxx,0,3,A,1200,0.5,0.5,FK2,300,1.46,0.19,23.32,1.82,3.92,16.55,79.53,0,0.01,0.21,0,700,1.66,0.16,9.12,0.58,11.32,17.81,70.86,0,0.01,0.21,0,1200,1.67,0.15,6.53,0.14,19.06,16.32,64.63,0,0.01,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +332,xxx,0,DK40370,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.48,0.18,27.77,1.59,6.79,13.79,79.42,0,0.01,0.19,0,700,1.66,0.16,9.12,0.58,11.32,17.81,70.86,0,0.01,0.19,0,1200,1.67,0.15,6.53,0.14,19.06,16.32,64.63,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +333,xxx,0,DK40380,xxx,0,3,A,500,0.5,0.5,FK3,300,1.4,0.19,36.24,1.59,6.79,13.79,79.42,0,0.01,0.17,0,700,1.46,0.16,20.7,0.83,5.18,14.04,80.78,0,0.01,0.17,0,1200,1.64,0.14,10.99,0.12,8.02,13.33,78.66,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +334,xxx,0,DK40460,xxx,0,3,A,500,0.5,0.5,FK3,300,1.41,0.19,24.51,1.83,7.12,19.01,73.87,0,0.01,0.22,0,700,1.52,0.16,17.6,0.71,5.17,14.84,79.99,0,0.01,0.22,0,1200,1.68,0.19,24.51,0,7,11.32,81.68,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +335,xxx,0,DK40470,xxx,0,3,A,1200,0.5,0.5,FK3,300,1.41,0.19,24.51,1.83,7.12,19.01,73.87,0,0.01,0.21,0,700,1.66,0.16,9.12,0.58,11.32,17.81,70.86,0,0.01,0.21,0,1200,1.67,0.15,6.53,0.14,19.06,16.32,64.63,0,0.01,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +336,xxx,0,DK40480,xxx,0,3,A,500,0.5,0.5,FK3,300,1.38,0.2,27.08,1.83,7.12,19.01,73.87,0,0.01,0.22,0,700,1.46,0.16,20.7,0.83,5.18,14.04,80.78,0,0.01,0.22,0,1200,1.64,0.14,10.99,0.12,8.02,13.33,78.66,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +337,xxx,0,DK40670,xxx,0,3,B,1500,0.5,0.5,FK4,300,1.54,0.18,15.86,1.42,12.5,22.63,64.87,0,0.01,0.22,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.22,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +338,xxx,0,DK50160,xxx,0,3,A,800,0.5,0.5,FK2,300,1.46,0.16,41,1.19,3.88,10.78,85.34,0,0.01,0.19,0,700,1.52,0.17,15.84,0.61,8.89,16.92,74.19,0,0.01,0.19,0,1200,1.73,0.15,7.66,0.14,9.22,16.17,74.62,0,0.01,0.19,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +339,xxx,0,DK50180,xxx,0,3,A,1000,0.5,0.5,FK2,300,1.44,0.17,43.01,1.19,3.88,10.78,85.34,0,0.01,0.17,0,700,1.46,0.17,20.14,0.57,7.98,16.16,75.86,0,0.01,0.17,0,1200,1.66,0.15,10.86,0.16,8.22,13.37,78.4,0,0.01,0.17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +340,xxx,0,DK50370,xxx,0,3,A,1500,0.5,0.5,FK3,300,1.48,0.18,26.98,1.45,7.99,15.51,76.5,0,0.01,0.22,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.22,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +341,xxx,0,DK50380,xxx,0,3,A,1000,0.5,0.5,FK3,300,1.4,0.19,35.14,1.45,7.99,15.51,76.5,0,0.01,0.2,0,700,1.46,0.17,20.14,0.57,7.98,16.16,75.86,0,0.01,0.2,0,1200,1.66,0.15,10.86,0.16,8.22,13.37,78.4,0,0.01,0.2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +342,xxx,0,DK50460,xxx,0,3,A,800,0.5,0.5,FK3,300,1.41,0.19,27.73,1.4,8.71,20.06,71.23,0,0.01,0.22,0,700,1.52,0.17,15.84,0.61,8.89,16.92,74.19,0,0.01,0.22,0,1200,1.73,0.15,7.66,0.14,9.22,16.17,74.62,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +343,xxx,0,DK50470,xxx,0,3,A,1500,0.5,0.5,FK3,300,1.41,0.19,27.73,1.4,8.71,20.06,71.23,0,0.01,0.23,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.23,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +344,xxx,0,DK50480,xxx,0,3,A,1000,0.5,0.5,FK3,300,1.38,0.2,30.54,1.4,8.71,20.06,71.23,0,0.01,0.22,0,700,1.46,0.17,20.14,0.57,7.98,16.16,75.86,0,0.01,0.22,0,1200,1.66,0.15,10.86,0.16,8.22,13.37,78.4,0,0.01,0.22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +345,xxx,0,DK50670,xxx,0,3,B,1500,0.5,0.5,FK4,300,1.54,0.18,15.86,1.42,12.5,22.63,64.87,0,0.01,0.24,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.24,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.24,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +346,xxx,0,DK50770,xxx,0,3,B,1500,0.5,0.5,FK4,300,1.58,0.18,14.76,1.42,12.5,22.63,64.87,0,0.01,0.23,0,700,1.7,0.15,6.43,0.59,15.86,21.14,63,0,0.01,0.23,0,1200,1.67,0.15,6.38,0.15,18.96,20.21,60.83,0,0.01,0.23,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +299,xxx,0,DK9980,xxx,0,3,A,800,0.5,0.5,FK3,300,1.32,0.2,28.49,2.52,7.73,16.58,75.69,0,0.01,0.18,0,700,1.42,0.18,13.01,2.08,8.71,14.83,76.45,0,0.01,0.18,0,1200,1.6,0.16,12.54,0.64,7.73,16.58,75.69,0,0.01,0.18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +300,xxx,0,DK9990,xxx,0,3,A,800,0.5,0.5,FK4,300,0.7,0.24,23.43,12.6,8.71,14.83,76.45,0,0.01,0.21,0,700,0.9,0.22,13.87,7.93,9.4,12.81,77.79,0,0.01,0.21,0,1200,1.6,0.16,11.94,0.64,9.59,16.05,74.36,0,0.01,0.21,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 diff --git a/rQSWATPlus/man/ensure_write_tables.Rd b/rQSWATPlus/man/ensure_write_tables.Rd new file mode 100644 index 0000000..dac4d0c --- /dev/null +++ b/rQSWATPlus/man/ensure_write_tables.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/database.R +\name{ensure_write_tables} +\alias{ensure_write_tables} +\title{Ensure all required SWAT+ tables exist before writing files} +\usage{ +ensure_write_tables(con) +} +\arguments{ +\item{con}{DBI connection to the project database.} +} +\value{ +Invisible \code{NULL}. +} +\description{ +Creates any missing tables that \code{\link{write_config_files}} needs and +populates mandatory tables with sensible defaults (mirroring the Python +SWAT+ Editor \code{setup.py} initialisation). Tables that already exist +are left untouched. +} +\keyword{internal} diff --git a/rQSWATPlus/man/populate_from_datasets.Rd b/rQSWATPlus/man/populate_from_datasets.Rd new file mode 100644 index 0000000..5a47f6c --- /dev/null +++ b/rQSWATPlus/man/populate_from_datasets.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/database.R +\name{populate_from_datasets} +\alias{populate_from_datasets} +\title{Populate reference/parameter tables from the SWAT+ datasets databases} +\usage{ +populate_from_datasets(con) +} +\arguments{ +\item{con}{DBI connection to the project database.} +} +\value{ +Invisible \code{NULL}. +} +\description{ +Uses SQLite ATTACH to copy reference data (plants, fertilizers, operations, +structural BMPs, land use, calibration parameters, soils, weather generator, +etc.) from the bundled reference databases into the project database. +} +\details{ +Data are sourced from two databases (in priority order): +\enumerate{ +\item \file{QSWATPlusRefHAWQS.sqlite} -- rich parameter/reference data +(plants_plt, fertilizer_frt, management schedules, soil, wgn, …) +\item \file{QSWATPlusProj.sqlite} -- standard project template (fallback) +} + +Additionally, HAWQS-specific tables (\code{plant_HAWQS}, \code{urban_HAWQS}, +CDL landuse field tables, \code{statsgo_ssurgo_lkey*}) are copied from +\file{QSWATPlusProjHAWQS.sqlite}. + +Only empty or missing tables are populated; tables with existing data are +left untouched. +} +\keyword{internal} diff --git a/rQSWATPlus/man/print.qswat_db_check.Rd b/rQSWATPlus/man/print.qswat_db_check.Rd new file mode 100644 index 0000000..a42b426 --- /dev/null +++ b/rQSWATPlus/man/print.qswat_db_check.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/validate_database.R +\name{print.qswat_db_check} +\alias{print.qswat_db_check} +\title{Print Method for Database Check Results} +\usage{ +\method{print}{qswat_db_check}(x, ...) +} +\arguments{ +\item{x}{A \code{qswat_db_check} object.} + +\item{...}{Additional arguments (ignored).} +} +\value{ +Invisibly returns \code{x}. +} +\description{ +Print Method for Database Check Results +} diff --git a/rQSWATPlus/man/print.qswat_project.Rd b/rQSWATPlus/man/print.qswat_project.Rd new file mode 100644 index 0000000..a12d5c1 --- /dev/null +++ b/rQSWATPlus/man/print.qswat_project.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/setup_project.R +\name{print.qswat_project} +\alias{print.qswat_project} +\title{Print QSWATPlus Project Summary} +\usage{ +\method{print}{qswat_project}(x, ...) +} +\arguments{ +\item{x}{A \code{qswat_project} object.} + +\item{...}{Additional arguments (ignored).} +} +\value{ +Invisibly returns \code{x}. +} +\description{ +Print QSWATPlus Project Summary +} diff --git a/rQSWATPlus/man/qswat_check_database.Rd b/rQSWATPlus/man/qswat_check_database.Rd new file mode 100644 index 0000000..de39c04 --- /dev/null +++ b/rQSWATPlus/man/qswat_check_database.Rd @@ -0,0 +1,71 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/validate_database.R +\name{qswat_check_database} +\alias{qswat_check_database} +\title{Check Database Compatibility with SWAT+ Editor} +\usage{ +qswat_check_database(db_file, verbose = TRUE) +} +\arguments{ +\item{db_file}{Character. Path to the SQLite database file.} + +\item{verbose}{Logical. If \code{TRUE}, print detailed check results. +Default \code{TRUE}.} +} +\value{ +A list of class \code{"qswat_db_check"} containing: +\describe{ +\item{passed}{Logical. \code{TRUE} if all checks passed.} +\item{checks}{A data frame with columns \code{check}, \code{status}, +and \code{message} describing each validation.} +\item{warnings}{Character vector of non-fatal warnings.} +\item{errors}{Character vector of fatal errors.} +} +} +\description{ +Validates that a SQLite database produced by \code{\link[=qswat_write_database]{qswat_write_database()}} +is compatible with the SWAT+ Editor. +} +\details{ +The function performs the following checks against the SWAT+ Editor +requirements: +\enumerate{ +\item \strong{Required tables} exist (gis_subbasins, gis_channels, +gis_lsus, gis_hrus, gis_routing) +\item \strong{Required columns} are present in each table with correct +types +\item \strong{Non-empty data} in mandatory tables +\item \strong{Referential integrity}: HRU subbasins reference existing +subbasins, channel subbasins reference existing subbasins, +routing source/sink IDs exist in their respective tables +\item \strong{Routing validity}: every routing chain terminates at an +outlet, routing percentages sum to 100 +\item \strong{Completeness}: every subbasin has at least one HRU, +every subbasin has at least one channel +\item \strong{Data quality}: areas are positive, no NULL values in +required fields +\item \strong{Reference data}: SWAT+ parameter tables (plants_plt, +fertilizer_frt, etc.) from QSWATPlusRefHAWQS.sqlite are +present and non-empty +\item \strong{HAWQS tables}: HAWQS-specific tables (plant_HAWQS, +urban_HAWQS, CDL landuse field tables, statsgo_ssurgo_lkey) +from QSWATPlusProjHAWQS.sqlite are present +\item \strong{gwflow tables}: when \code{use_gwflow = 1} in project_config, +all twelve gwflow tables exist and the core tables +(gwflow_base, gwflow_zone, gwflow_grid, gwflow_solutes, +gwflow_rivcell) are populated; recharge-specific cell mapping +tables (gwflow_hrucell for HRU recharge, gwflow_lsucell for +LSU recharge) are non-empty according to the recharge mode +stored in gwflow_base +} +} +\examples{ +\dontrun{ +result <- qswat_check_database("project.sqlite") +if (!result$passed) { + cat("Errors found:\n") + cat(result$errors, sep = "\n") +} +} + +} diff --git a/rQSWATPlus/man/qswat_create_hrus.Rd b/rQSWATPlus/man/qswat_create_hrus.Rd new file mode 100644 index 0000000..8ecedda --- /dev/null +++ b/rQSWATPlus/man/qswat_create_hrus.Rd @@ -0,0 +1,161 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/hru_creation.R +\name{qswat_create_hrus} +\alias{qswat_create_hrus} +\title{Create Hydrologic Response Units (HRUs)} +\usage{ +qswat_create_hrus( + project, + landuse_lookup, + soil_lookup, + slope_classes = qswat_create_slope_classes(), + hru_method = c("filter_threshold", "dominant_hru", "dominant_luse", "filter_area", + "target"), + threshold_type = c("percent", "area"), + landuse_threshold = 15, + soil_threshold = 10, + slope_threshold = 10, + area_threshold = 0, + target_hrus = NULL, + use_gwflow = FALSE, + use_aquifers = TRUE +) +} +\arguments{ +\item{project}{A \code{qswat_project} object that has been through +\code{\link[=qswat_delineate]{qswat_delineate()}} and \code{\link[=qswat_create_streams]{qswat_create_streams()}}.} + +\item{landuse_lookup}{A data frame from \code{\link[=qswat_read_landuse_lookup]{qswat_read_landuse_lookup()}} +mapping raster values to SWAT+ land use codes.} + +\item{soil_lookup}{A data frame from \code{\link[=qswat_read_soil_lookup]{qswat_read_soil_lookup()}} +mapping raster values to soil names.} + +\item{slope_classes}{A data frame from \code{\link[=qswat_create_slope_classes]{qswat_create_slope_classes()}} +defining slope percentage classes. Default creates a single class.} + +\item{hru_method}{Character. Method for selecting HRUs. One of: +\describe{ +\item{\code{"dominant_hru"}}{Single HRU per subbasin: the combination +of land use, soil, and slope class with the largest area. This +corresponds to the "Dominant HRU" option in the QSWATPlus GUI.} +\item{\code{"dominant_luse"}}{Single HRU per subbasin formed from the +dominant land use, the dominant soil within that land use, and +the dominant slope class within that soil. This corresponds to +the "Dominant landuse, soil and slope" option.} +\item{\code{"filter_threshold"}}{Multiple HRUs retained after applying +hierarchical percentage or area thresholds (default). This +corresponds to the "Filter by landuse, soil, slope" option.} +\item{\code{"filter_area"}}{Multiple HRUs retained by applying a +single minimum-area threshold. This corresponds to the "Filter +by area" option.} +\item{\code{"target"}}{Multiple HRUs reduced to approximately +\code{target_hrus} per subbasin by removing the smallest HRUs. This +corresponds to the "Target number of HRUs" option.} +} +Default is \code{"filter_threshold"}.} + +\item{threshold_type}{Character. Whether threshold values are +percentages (\code{"percent"}, the default) or absolute areas in +hectares (\code{"area"}). Only used when \code{hru_method} is +\code{"filter_threshold"}.} + +\item{landuse_threshold}{Numeric. When \code{hru_method = "filter_threshold"} +and \code{threshold_type = "percent"}: minimum percentage of subbasin +area that a land use must occupy to be kept. When +\code{threshold_type = "area"}: minimum area in hectares. Default 0 +(keep all).} + +\item{soil_threshold}{Numeric. When \code{hru_method = "filter_threshold"} +and \code{threshold_type = "percent"}: minimum percentage of land-use +area that a soil must occupy. When \code{threshold_type = "area"}: +minimum area in hectares. Default 0 (keep all).} + +\item{slope_threshold}{Numeric. When \code{hru_method = "filter_threshold"} +and \code{threshold_type = "percent"}: minimum percentage of soil area +that a slope class must occupy. When \code{threshold_type = "area"}: +minimum area in hectares. Default 0 (keep all).} + +\item{area_threshold}{Numeric. Minimum area in hectares used when +\code{hru_method = "filter_area"}. Default 0 (keep all).} + +\item{target_hrus}{Integer or NULL. Target number of HRUs per +subbasin used when \code{hru_method = "target"}. NULL means no target +filtering.} + +\item{use_gwflow}{Logical. If \code{TRUE}, gwflow groundwater modelling +will be used (equivalent to checking "Use gwflow" in the +QSWATPlus GUI). The project database \code{use_gwflow} flag will be set +to 1. Call \code{\link[=qswat_setup_gwflow]{qswat_setup_gwflow()}} after \code{\link[=qswat_write_database]{qswat_write_database()}} +to initialise the gwflow tables. Default \code{FALSE}.} + +\item{use_aquifers}{Logical. If \code{TRUE} (the default), SWAT+ aquifer +objects are created for each subbasin (equivalent to "Use SWAT+ +aquifers" in the GUI). Set to \code{FALSE} to omit aquifer tables.} +} +\value{ +An updated \code{qswat_project} object with the following new fields: +\describe{ +\item{\code{hru_data}}{Data frame of HRU attributes (one row per HRU).} +\item{\code{basin_data}}{Data frame of subbasin-level statistics.} +\item{\code{hru_sf}}{\code{sf} polygon object: one (possibly multi-part) polygon +per retained HRU, with all HRU attributes joined. Suitable for +mapping and spatial analysis.} +\item{\code{lsu_sf}}{\code{sf} polygon object: one polygon per LSU / subbasin +(in rQSWATPlus, LSUs and subbasins are 1:1). Suitable for overlaying +subbasin boundaries.} +\item{\code{slope_classes}}{Slope-class definition used.} +\item{\code{use_gwflow}}{Logical groundwater flag.} +\item{\code{use_aquifers}}{Logical aquifer flag.} +} +} +\description{ +Creates HRUs by overlaying land use, soil, and slope rasters on the +delineated watershed. Each unique combination of subbasin, land use, +soil type, and slope class forms an HRU. +} +\details{ +The HRU creation process follows the QSWATPlus approach: +\enumerate{ +\item Read the watershed/subbasin raster from delineation +\item For each cell in the watershed, extract the corresponding +land use, soil, and slope values +\item Classify slope into the specified slope bands +\item Group cells by subbasin-landuse-soil-slope combinations +\item Apply the selected HRU method to filter/reduce HRUs +\item Compute statistics (area, mean elevation, mean slope) for +each retained HRU +} + +The \code{"filter_threshold"} method applies thresholds hierarchically: +land use threshold within each subbasin, then soil threshold within +each remaining land use, then slope threshold within each remaining +soil. Areas from eliminated HRUs are redistributed proportionally to +the surviving HRUs. +} +\examples{ +\dontrun{ +lu_lookup <- qswat_read_landuse_lookup("landuse_lookup.csv") +soil_lookup <- qswat_read_soil_lookup("soil_lookup.csv") +slope_classes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) + +# Multiple HRUs with percentage thresholds (default) +project <- qswat_create_hrus(project, lu_lookup, soil_lookup, + slope_classes, + hru_method = "filter_threshold", + landuse_threshold = 20, + soil_threshold = 10, + slope_threshold = 10) + +# Single dominant HRU per subbasin +project <- qswat_create_hrus(project, lu_lookup, soil_lookup, + hru_method = "dominant_hru") + +# Target number of HRUs with gwflow enabled +project <- qswat_create_hrus(project, lu_lookup, soil_lookup, + hru_method = "target", + target_hrus = 5L, + use_gwflow = TRUE) +} + +} diff --git a/rQSWATPlus/man/qswat_create_slope_classes.Rd b/rQSWATPlus/man/qswat_create_slope_classes.Rd new file mode 100644 index 0000000..3a360bf --- /dev/null +++ b/rQSWATPlus/man/qswat_create_slope_classes.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lookup.R +\name{qswat_create_slope_classes} +\alias{qswat_create_slope_classes} +\title{Create Slope Classification Bands} +\usage{ +qswat_create_slope_classes(breaks = c(0, 9999), labels = NULL) +} +\arguments{ +\item{breaks}{Numeric vector. Slope percentage breakpoints defining +the class boundaries. For example, \code{c(0, 5, 15, 9999)} creates +three classes: 0-5\%, 5-15\%, and >15\%.} + +\item{labels}{Character vector or NULL. Labels for each slope class. +If NULL, labels are auto-generated from the breaks.} +} +\value{ +A data frame with columns \code{min_slope}, \code{max_slope}, \code{label}, +and \code{class_id}. +} +\description{ +Defines slope percentage classes for HRU creation. Slope bands +are used to further subdivide HRUs by terrain steepness. +} +\examples{ +# Three slope classes +slopes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) +slopes + +# Single slope class (no subdivision) +slopes <- qswat_create_slope_classes(c(0, 9999)) +slopes + +} diff --git a/rQSWATPlus/man/qswat_create_streams.Rd b/rQSWATPlus/man/qswat_create_streams.Rd new file mode 100644 index 0000000..969b585 --- /dev/null +++ b/rQSWATPlus/man/qswat_create_streams.Rd @@ -0,0 +1,38 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/delineation.R +\name{qswat_create_streams} +\alias{qswat_create_streams} +\title{Create Stream/Channel Network from Delineation} +\usage{ +qswat_create_streams(project) +} +\arguments{ +\item{project}{A \code{qswat_project} object that has been through +\code{\link[=qswat_delineate]{qswat_delineate()}}.} +} +\value{ +An updated \code{qswat_project} object with stream topology +information. +} +\description{ +Reads the stream and channel network shapefiles generated during +delineation and extracts topology information (connectivity, +stream order, lengths, slopes). +} +\details{ +Extracts the following topology data from TauDEM's StreamNet output: +\itemize{ +\item Stream links and their downstream connections +\item Subbasin (watershed) identifiers +\item Stream orders (Strahler) +\item Channel lengths and slopes +\item Drainage areas +} +} +\examples{ +\dontrun{ +# After qswat_delineate() +project <- qswat_create_streams(project) +} + +} diff --git a/rQSWATPlus/man/qswat_delineate.Rd b/rQSWATPlus/man/qswat_delineate.Rd new file mode 100644 index 0000000..b994c04 --- /dev/null +++ b/rQSWATPlus/man/qswat_delineate.Rd @@ -0,0 +1,64 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/delineation.R +\name{qswat_delineate} +\alias{qswat_delineate} +\title{Run Watershed Delineation Using TauDEM} +\usage{ +qswat_delineate( + project, + threshold = NULL, + channel_threshold = NULL, + n_processes = 1L, + quiet = FALSE +) +} +\arguments{ +\item{project}{A \code{qswat_project} object created by \code{\link[=qswat_setup]{qswat_setup()}}.} + +\item{threshold}{Numeric. Flow accumulation threshold for stream +definition (number of cells). If NULL, a default threshold of +1\% of total cells is used.} + +\item{channel_threshold}{Numeric or NULL. Threshold for channel +definition. Defaults to \code{threshold / 10} if NULL.} + +\item{n_processes}{Integer. Number of MPI processes for TauDEM. +Default is 1 (no parallelization).} + +\item{quiet}{Logical. If TRUE, suppress TauDEM output messages. +Default is FALSE.} +} +\value{ +An updated \code{qswat_project} object with delineation results. +} +\description{ +Performs watershed delineation on a DEM using TauDEM tools via the +\code{traudem} package. This replicates the delineation workflow from +the QSWATPlus QGIS plugin. +} +\details{ +The delineation workflow follows these steps: +\enumerate{ +\item \strong{Pit removal}: Fill pits/depressions in the DEM +\item \strong{D8 flow direction}: Calculate flow directions using D8 +algorithm +\item \strong{D-infinity flow direction}: Calculate slope and flow +angles +\item \strong{D8 contributing area}: Calculate flow accumulation +\item \strong{Stream definition}: Apply threshold to identify streams +\item \strong{Move outlets}: Snap outlet points to nearest stream +(if outlets provided) +\item \strong{Stream network}: Generate stream network topology and +watershed boundaries +} + +Requires TauDEM to be installed and accessible. Use +\code{traudem::taudem_sitrep()} to verify your TauDEM installation. +} +\examples{ +\dontrun{ +# After qswat_setup() +project <- qswat_delineate(project, threshold = 100) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_dem.Rd b/rQSWATPlus/man/qswat_plot_dem.Rd new file mode 100644 index 0000000..9984d7b --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_dem.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_dem} +\alias{qswat_plot_dem} +\title{Plot DEM Elevation Map} +\usage{ +qswat_plot_dem(project, title = "DEM Elevation", palette = "terrain", ...) +} +\arguments{ +\item{project}{A \code{qswat_project} object.} + +\item{title}{Character. Map title. Default \code{"DEM Elevation"}.} + +\item{palette}{Character. Colour palette name passed to tmap. +Default \code{"terrain"}.} + +\item{...}{Additional arguments passed to \code{tmap::tm_raster()}.} +} +\value{ +A \code{tmap} object that can be printed or further customised. +} +\description{ +Creates a spatial map of the Digital Elevation Model using \code{tmap}. +} +\examples{ +\dontrun{ +project <- qswat_setup(...) +qswat_plot_dem(project) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_hru_summary.Rd b/rQSWATPlus/man/qswat_plot_hru_summary.Rd new file mode 100644 index 0000000..c962fb3 --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_hru_summary.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_hru_summary} +\alias{qswat_plot_hru_summary} +\title{Summary Plot of HRU Breakdown} +\usage{ +qswat_plot_hru_summary(project, by_subbasin = FALSE, title = "HRU Composition") +} +\arguments{ +\item{project}{A \code{qswat_project} object with HRU data from +\code{\link[=qswat_create_hrus]{qswat_create_hrus()}}.} + +\item{by_subbasin}{Logical. If \code{TRUE}, show a stacked bar chart of +HRU composition per subbasin. Default \code{FALSE}.} + +\item{title}{Character. Overall plot title.} +} +\value{ +A \code{ggplot} object. When \code{by_subbasin = FALSE} a faceted +plot of land use, soil, and slope breakdowns is returned. +When \code{by_subbasin = TRUE} a stacked bar chart per subbasin is +returned. +} +\description{ +Creates a multi-panel summary of HRU composition showing the +distribution of land use, soil, and slope classes, and optionally +a per-subbasin breakdown. +} +\examples{ +\dontrun{ +project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +qswat_plot_hru_summary(project) +qswat_plot_hru_summary(project, by_subbasin = TRUE) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_hrus.Rd b/rQSWATPlus/man/qswat_plot_hrus.Rd new file mode 100644 index 0000000..d551af1 --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_hrus.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_hrus} +\alias{qswat_plot_hrus} +\title{Plot HRU Map} +\usage{ +qswat_plot_hrus(project, color_by = "landuse", title = "HRUs") +} +\arguments{ +\item{project}{A \code{qswat_project} object.} + +\item{color_by}{Character. Column in \code{project$hru_sf} used to colour the +polygons. Typical choices: \code{"landuse"}, \code{"soil"}, \code{"slope_class"}, +\code{"hru_id"}. Default \code{"landuse"}.} + +\item{title}{Character. Map title.} +} +\value{ +A \code{tmap} object. +} +\description{ +Creates a spatial map of HRU polygons, coloured by a chosen attribute. +Requires \code{\link[=qswat_create_hrus]{qswat_create_hrus()}} to have been run. +} +\examples{ +\dontrun{ +project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +qswat_plot_hrus(project) +qswat_plot_hrus(project, color_by = "soil") +} + +} diff --git a/rQSWATPlus/man/qswat_plot_landuse.Rd b/rQSWATPlus/man/qswat_plot_landuse.Rd new file mode 100644 index 0000000..3b21c02 --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_landuse.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_landuse} +\alias{qswat_plot_landuse} +\title{Plot Land Use Map} +\usage{ +qswat_plot_landuse(project, landuse_lookup = NULL, title = "Land Use", ...) +} +\arguments{ +\item{project}{A \code{qswat_project} object.} + +\item{landuse_lookup}{Data frame from \code{\link[=qswat_read_landuse_lookup]{qswat_read_landuse_lookup()}}, +or \code{NULL} to read from the project.} + +\item{title}{Character. Map title.} + +\item{...}{Additional arguments passed to \code{tmap::tm_raster()}.} +} +\value{ +A \code{tmap} object. +} +\description{ +Creates a spatial map of the land use raster with categorical +labels from the lookup table. +} +\examples{ +\dontrun{ +project <- qswat_setup(...) +qswat_plot_landuse(project) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_landuse_summary.Rd b/rQSWATPlus/man/qswat_plot_landuse_summary.Rd new file mode 100644 index 0000000..ff5a7dc --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_landuse_summary.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_landuse_summary} +\alias{qswat_plot_landuse_summary} +\title{Summary Bar Chart of Land Use Distribution} +\usage{ +qswat_plot_landuse_summary( + project, + type = "percent", + title = "Land Use Distribution" +) +} +\arguments{ +\item{project}{A \code{qswat_project} object with HRU data from +\code{\link[=qswat_create_hrus]{qswat_create_hrus()}}.} + +\item{type}{Character. One of \code{"area"} (hectares) or \code{"percent"}. +Default \code{"percent"}.} + +\item{title}{Character. Plot title.} +} +\value{ +A \code{ggplot} object. +} +\description{ +Creates a bar chart showing the area or percentage of each land use +type across the watershed, using \code{ggplot2}. +} +\examples{ +\dontrun{ +project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +qswat_plot_landuse_summary(project) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_lsus.Rd b/rQSWATPlus/man/qswat_plot_lsus.Rd new file mode 100644 index 0000000..df3ab86 --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_lsus.Rd @@ -0,0 +1,29 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_lsus} +\alias{qswat_plot_lsus} +\title{Plot LSU (Landscape Unit / Subbasin) Map} +\usage{ +qswat_plot_lsus(project, title = "Landscape Units (LSUs)") +} +\arguments{ +\item{project}{A \code{qswat_project} object.} + +\item{title}{Character. Map title.} +} +\value{ +A \code{tmap} object. +} +\description{ +Creates a spatial map of Landscape Units (LSUs). In rQSWATPlus, LSUs and +subbasins are in a 1:1 relationship; each polygon represents one subbasin / +LSU. Requires \code{\link[=qswat_create_hrus]{qswat_create_hrus()}} to have been run (or at least +\code{\link[=qswat_delineate]{qswat_delineate()}}). +} +\examples{ +\dontrun{ +project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +qswat_plot_lsus(project) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_overview.Rd b/rQSWATPlus/man/qswat_plot_overview.Rd new file mode 100644 index 0000000..f8b09f7 --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_overview.Rd @@ -0,0 +1,59 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_overview} +\alias{qswat_plot_overview} +\title{Plot Watershed Overview} +\usage{ +qswat_plot_overview( + project, + layers = c("dem", "lsu", "hru", "streams", "outlets"), + landuse_lookup = NULL, + soil_lookup = NULL, + title = "Watershed Overview" +) +} +\arguments{ +\item{project}{A \code{qswat_project} object.} + +\item{layers}{Character vector of layers to include. Any combination of: +\describe{ +\item{\code{"dem"}}{DEM raster shown as a greyscale background.} +\item{\code{"landuse"}}{Land-use raster (semi-transparent categorical fill).} +\item{\code{"soil"}}{Soil raster (semi-transparent categorical fill).} +\item{\code{"lsu"}}{LSU / subbasin polygons coloured by subbasin ID.} +\item{\code{"hru"}}{HRU polygons coloured by land use.} +\item{\code{"streams"}}{Stream-network lines in blue.} +\item{\code{"outlets"}}{Outlet points in red.} +} +Default: \code{c("dem", "lsu", "hru", "streams", "outlets")}.} + +\item{landuse_lookup}{Data frame from \code{\link[=qswat_read_landuse_lookup]{qswat_read_landuse_lookup()}}, or +\code{NULL} to read from the project. Only used when \code{"landuse"} is in +\code{layers}.} + +\item{soil_lookup}{Data frame from \code{\link[=qswat_read_soil_lookup]{qswat_read_soil_lookup()}}, or \code{NULL} +to read from the project. Only used when \code{"soil"} is in \code{layers}.} + +\item{title}{Character. Map title.} +} +\value{ +A \code{tmap} object. +} +\description{ +Creates a combined overlay map that can include any subset of: DEM, +land use, soil, LSU/subbasin polygons, HRU polygons, stream network, and +outlet points. +} +\examples{ +\dontrun{ +project <- qswat_run(...) + +# Default layers +qswat_plot_overview(project) + +# All available layers +qswat_plot_overview(project, + layers = c("dem", "landuse", "soil", "lsu", "hru", "streams", "outlets")) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_soil.Rd b/rQSWATPlus/man/qswat_plot_soil.Rd new file mode 100644 index 0000000..38d16db --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_soil.Rd @@ -0,0 +1,32 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_soil} +\alias{qswat_plot_soil} +\title{Plot Soil Map} +\usage{ +qswat_plot_soil(project, soil_lookup = NULL, title = "Soil Types", ...) +} +\arguments{ +\item{project}{A \code{qswat_project} object.} + +\item{soil_lookup}{Data frame from \code{\link[=qswat_read_soil_lookup]{qswat_read_soil_lookup()}}, +or \code{NULL} to read from the project.} + +\item{title}{Character. Map title.} + +\item{...}{Additional arguments passed to \code{tmap::tm_raster()}.} +} +\value{ +A \code{tmap} object. +} +\description{ +Creates a spatial map of the soil raster with categorical labels +from the lookup table. +} +\examples{ +\dontrun{ +project <- qswat_setup(...) +qswat_plot_soil(project) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_soil_summary.Rd b/rQSWATPlus/man/qswat_plot_soil_summary.Rd new file mode 100644 index 0000000..a8d152c --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_soil_summary.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_soil_summary} +\alias{qswat_plot_soil_summary} +\title{Summary Bar Chart of Soil Distribution} +\usage{ +qswat_plot_soil_summary(project, type = "percent", title = "Soil Distribution") +} +\arguments{ +\item{project}{A \code{qswat_project} object with HRU data from +\code{\link[=qswat_create_hrus]{qswat_create_hrus()}}.} + +\item{type}{Character. One of \code{"area"} (hectares) or \code{"percent"}. +Default \code{"percent"}.} + +\item{title}{Character. Plot title.} +} +\value{ +A \code{ggplot} object. +} +\description{ +Creates a bar chart showing the area or percentage of each soil +type across the watershed, using \code{ggplot2}. +} +\examples{ +\dontrun{ +project <- qswat_create_hrus(project, lu_lookup, soil_lookup) +qswat_plot_soil_summary(project) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_streams.Rd b/rQSWATPlus/man/qswat_plot_streams.Rd new file mode 100644 index 0000000..1f16c67 --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_streams.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_streams} +\alias{qswat_plot_streams} +\title{Plot Stream Network} +\usage{ +qswat_plot_streams(project, show_dem = TRUE, title = "Stream Network", ...) +} +\arguments{ +\item{project}{A \code{qswat_project} object that has been through +\code{\link[=qswat_delineate]{qswat_delineate()}} and \code{\link[=qswat_create_streams]{qswat_create_streams()}}.} + +\item{show_dem}{Logical. If \code{TRUE}, display the DEM as background. +Default \code{TRUE}.} + +\item{title}{Character. Map title.} + +\item{...}{Additional arguments passed to \code{tmap::tm_lines()}.} +} +\value{ +A \code{tmap} object. +} +\description{ +Creates a spatial map of the stream network overlaid on the DEM +or watershed raster. +} +\examples{ +\dontrun{ +project <- qswat_delineate(project, threshold = 500) +project <- qswat_create_streams(project) +qswat_plot_streams(project) +} + +} diff --git a/rQSWATPlus/man/qswat_plot_watershed.Rd b/rQSWATPlus/man/qswat_plot_watershed.Rd new file mode 100644 index 0000000..5a58546 --- /dev/null +++ b/rQSWATPlus/man/qswat_plot_watershed.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/visualization.R +\name{qswat_plot_watershed} +\alias{qswat_plot_watershed} +\title{Plot Watershed / Subbasins} +\usage{ +qswat_plot_watershed(project, title = "Watershed Subbasins", ...) +} +\arguments{ +\item{project}{A \code{qswat_project} object that has been through +\code{\link[=qswat_delineate]{qswat_delineate()}}.} + +\item{title}{Character. Map title.} + +\item{...}{Additional arguments passed to \code{tmap::tm_raster()}.} +} +\value{ +A \code{tmap} object. +} +\description{ +Creates a spatial map of the delineated watershed showing +subbasin boundaries. +} +\examples{ +\dontrun{ +project <- qswat_delineate(project, threshold = 500) +qswat_plot_watershed(project) +} + +} diff --git a/rQSWATPlus/man/qswat_read_gis.Rd b/rQSWATPlus/man/qswat_read_gis.Rd new file mode 100644 index 0000000..26dc343 --- /dev/null +++ b/rQSWATPlus/man/qswat_read_gis.Rd @@ -0,0 +1,47 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/database.R +\name{qswat_read_gis} +\alias{qswat_read_gis} +\title{Read GIS Tables from a SWAT+ Project Database} +\usage{ +qswat_read_gis(db_file) +} +\arguments{ +\item{db_file}{Character. Path to a SWAT+ project SQLite database (the +file written by \code{\link[=qswat_write_database]{qswat_write_database()}}).} +} +\value{ +A named list. Each element is a data frame corresponding to one +\code{gis_*} table found in the database. If no GIS tables are present +an empty list is returned. +} +\description{ +Reads all \code{gis_*} tables from an existing SWAT+ project SQLite +database and returns them as a named list of data frames. +} +\details{ +The following tables are returned when they contain data (empty tables are +still included as zero-row data frames): +\itemize{ +\item \code{gis_subbasins} +\item \code{gis_channels} +\item \code{gis_lsus} +\item \code{gis_hrus} +\item \code{gis_aquifers} +\item \code{gis_deep_aquifers} +\item \code{gis_water} +\item \code{gis_points} +\item \code{gis_routing} +\item \code{gis_elevationbands} +\item \code{gis_landexempt} +\item \code{gis_splithrus} +} +} +\examples{ +\dontrun{ +gis <- qswat_read_gis("my_project.sqlite") +head(gis$gis_subbasins) +head(gis$gis_channels) +} + +} diff --git a/rQSWATPlus/man/qswat_read_gwflow_config.Rd b/rQSWATPlus/man/qswat_read_gwflow_config.Rd new file mode 100644 index 0000000..3e37a23 --- /dev/null +++ b/rQSWATPlus/man/qswat_read_gwflow_config.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/gwflow.R +\name{qswat_read_gwflow_config} +\alias{qswat_read_gwflow_config} +\title{Read gwflow Configuration File} +\usage{ +qswat_read_gwflow_config(ini_file = NULL) +} +\arguments{ +\item{ini_file}{Character. Path to the \code{gwflow.ini} file. If \code{NULL} +the bundled template shipped with the package is used.} +} +\value{ +A named list of gwflow configuration parameters with their +default values filled in where a key is absent from the file. +} +\description{ +Reads a SWAT+ gwflow initialisation (\code{gwflow.ini}) file and returns its +settings as a named list. The ini file uses the \verb{[DEFAULT]} section +format produced by Python's \code{configparser} module. +} +\examples{ +ini <- system.file("extdata", "gwflow.ini", package = "rQSWATPlus") +cfg <- qswat_read_gwflow_config(ini) +cfg$cell_size + +} diff --git a/rQSWATPlus/man/qswat_read_landuse_lookup.Rd b/rQSWATPlus/man/qswat_read_landuse_lookup.Rd new file mode 100644 index 0000000..e3772a3 --- /dev/null +++ b/rQSWATPlus/man/qswat_read_landuse_lookup.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lookup.R +\name{qswat_read_landuse_lookup} +\alias{qswat_read_landuse_lookup} +\title{Read Land Use Lookup Table} +\usage{ +qswat_read_landuse_lookup(lookup_file, value_col = 1, name_col = 2) +} +\arguments{ +\item{lookup_file}{Character. Path to the CSV file. The file should +have at least two columns: the raster value and the SWAT+ land use +code.} + +\item{value_col}{Character or integer. Name or index of the column +containing raster values. Default is 1 (first column).} + +\item{name_col}{Character or integer. Name or index of the column +containing SWAT+ land use codes. Default is 2 (second column).} +} +\value{ +A data frame with columns \code{value} (integer) and \code{landuse} +(character). +} +\description{ +Reads a CSV file that maps land use raster values to SWAT+ land use +codes. The lookup table is used during HRU creation to assign SWAT+ +land use categories. +} +\details{ +The lookup CSV should map each unique value in the land use raster to +a SWAT+ land use code (e.g., AGRL, FRSD, PAST, URBN, WATR). The +format is flexible - the function will detect columns based on +position or name. + +Common SWAT+ land use codes include: +\itemize{ +\item AGRL - Agricultural land (generic) +\item AGRR - Agricultural row crops +\item FRSD - Forest (deciduous) +\item FRSE - Forest (evergreen) +\item FRST - Forest (mixed) +\item PAST - Pasture +\item RNGE - Range (grass) +\item URBN - Urban (generic) +\item URHD - Urban high density +\item URMD - Urban medium density +\item WATR - Water +\item WETF - Wetland (forested) +\item WETL - Wetland +} +} +\examples{ +lu_file <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") +lu_lookup <- qswat_read_landuse_lookup(lu_file) +head(lu_lookup) + +} diff --git a/rQSWATPlus/man/qswat_read_soil_lookup.Rd b/rQSWATPlus/man/qswat_read_soil_lookup.Rd new file mode 100644 index 0000000..f2902c6 --- /dev/null +++ b/rQSWATPlus/man/qswat_read_soil_lookup.Rd @@ -0,0 +1,36 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lookup.R +\name{qswat_read_soil_lookup} +\alias{qswat_read_soil_lookup} +\title{Read Soil Lookup Table} +\usage{ +qswat_read_soil_lookup(lookup_file, value_col = 1, name_col = 2) +} +\arguments{ +\item{lookup_file}{Character. Path to the CSV file.} + +\item{value_col}{Character or integer. Name or index of the column +containing raster values. Default is 1 (first column).} + +\item{name_col}{Character or integer. Name or index of the column +containing soil names. Default is 2 (second column).} +} +\value{ +A data frame with columns \code{value} (integer) and \code{soil} +(character). +} +\description{ +Reads a CSV file that maps soil raster values to SWAT+ soil names. +The lookup table is used during HRU creation to assign soil types. +} +\details{ +The lookup CSV maps each unique value in the soil raster to a SWAT+ +soil name. Soil names should correspond to entries in the SWAT+ +soils database (e.g., STATSGO or SSURGO soil names). +} +\examples{ +soil_file <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") +soil_lookup <- qswat_read_soil_lookup(soil_file) +head(soil_lookup) + +} diff --git a/rQSWATPlus/man/qswat_read_usersoil.Rd b/rQSWATPlus/man/qswat_read_usersoil.Rd new file mode 100644 index 0000000..0c93031 --- /dev/null +++ b/rQSWATPlus/man/qswat_read_usersoil.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/lookup.R +\name{qswat_read_usersoil} +\alias{qswat_read_usersoil} +\title{Read User Soil Data from CSV} +\usage{ +qswat_read_usersoil(csv_file) +} +\arguments{ +\item{csv_file}{Character. Path to the CSV file containing soil parameters. +Must have at minimum a column named \code{SNAM} (soil name). Additional +columns should match the \code{global_usersoil} table schema: +\code{NLAYERS}, \code{HYDGRP}, \code{SOL_ZMX}, \code{ANION_EXCL}, \code{SOL_CRK}, \code{TEXTURE}, +and for each layer \emph{n} = 1--10: \code{SOL_Zn}, \code{SOL_BDn}, \code{SOL_AWCn}, +\code{SOL_Kn}, \code{SOL_CBNn}, \code{CLAYn}, \code{SILTn}, \code{SANDn}, \code{ROCKn}, +\code{SOL_ALBn}, \code{USLE_Kn}, \code{SOL_ECn}.} +} +\value{ +A data frame with column names normalised to uppercase, suitable +for passing to \code{\link[=qswat_write_database]{qswat_write_database()}} as the \code{usersoil} argument. +Rows with missing \code{SNAM} values are removed. +} +\description{ +Reads a CSV file containing soil physical parameters in the SWAT+ +\code{global_usersoil} table format. The resulting data frame can be passed +directly to \code{\link[=qswat_write_database]{qswat_write_database()}} via the \code{usersoil} argument to +populate soil physical properties in the project database. +} +\details{ +Column names are converted to uppercase on reading so the data frame is +compatible with the SWAT+ project database schema regardless of the case +used in the CSV file. + +Instead of passing the data frame directly to \code{qswat_write_database}, +you can also pass the file path as a string; \code{\link[=qswat_write_database]{qswat_write_database()}} will +call this function internally. +} +\examples{ +csv_file <- tempfile(fileext = ".csv") +write.csv(data.frame( + SNAM = c("MySoil1", "MySoil2"), + NLAYERS = c(2L, 3L), + HYDGRP = c("B", "C"), + SOL_ZMX = c(1000, 1500), + ANION_EXCL = c(0.5, 0.5), + SOL_CRK = c(0.5, 0.5), + SOL_Z1 = c(300, 200), + SOL_BD1 = c(1.4, 1.5) +), csv_file, row.names = FALSE) + +soil_data <- qswat_read_usersoil(csv_file) +head(soil_data) + +} +\seealso{ +\code{\link[=qswat_write_database]{qswat_write_database()}} +} diff --git a/rQSWATPlus/man/qswat_run.Rd b/rQSWATPlus/man/qswat_run.Rd new file mode 100644 index 0000000..f836849 --- /dev/null +++ b/rQSWATPlus/man/qswat_run.Rd @@ -0,0 +1,152 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/workflow.R +\name{qswat_run} +\alias{qswat_run} +\title{Run Complete QSWATPlus Workflow} +\usage{ +qswat_run( + project_dir, + dem_file, + landuse_file, + soil_file, + landuse_lookup, + soil_lookup, + outlet_file = NULL, + threshold = NULL, + channel_threshold = NULL, + slope_breaks = c(0, 9999), + hru_method = "filter_threshold", + threshold_type = "percent", + landuse_threshold = 0, + soil_threshold = 0, + slope_threshold = 0, + area_threshold = 0, + target_hrus = NULL, + use_gwflow = FALSE, + gwflow_config = NULL, + aquifer_thickness = 20, + conductivity_file = NULL, + default_aquifer_k = 1, + use_aquifers = TRUE, + n_processes = 1L, + quiet = FALSE, + db_file = NULL, + usersoil = NULL +) +} +\arguments{ +\item{project_dir}{Character. Path to the project directory.} + +\item{dem_file}{Character. Path to the DEM raster file.} + +\item{landuse_file}{Character. Path to the land use raster file.} + +\item{soil_file}{Character. Path to the soil raster file.} + +\item{landuse_lookup}{Character. Path to the land use lookup CSV.} + +\item{soil_lookup}{Character. Path to the soil lookup CSV.} + +\item{outlet_file}{Character or NULL. Optional outlet shapefile.} + +\item{threshold}{Numeric or NULL. Stream threshold (cells). +If NULL, 1 percent of total cells is used.} + +\item{channel_threshold}{Numeric or NULL. Channel threshold.} + +\item{slope_breaks}{Numeric vector. Slope class boundaries in +percent. Default \code{c(0, 9999)} for a single class.} + +\item{hru_method}{Character. HRU selection method. See +\code{\link[=qswat_create_hrus]{qswat_create_hrus()}} for the full description of each option. +Default \code{"filter_threshold"}.} + +\item{threshold_type}{Character. \code{"percent"} (default) or \code{"area"}. +Controls whether \code{landuse_threshold}, \code{soil_threshold}, and +\code{slope_threshold} are percentages or absolute hectares. Only used +when \code{hru_method = "filter_threshold"}.} + +\item{landuse_threshold}{Numeric. Min land use percent (or area ha). +Default 0.} + +\item{soil_threshold}{Numeric. Min soil percent (or area ha). +Default 0.} + +\item{slope_threshold}{Numeric. Min slope percent (or area ha). +Default 0.} + +\item{area_threshold}{Numeric. Minimum HRU area in hectares used +when \code{hru_method = "filter_area"}. Default 0.} + +\item{target_hrus}{Integer or NULL. Target number of HRUs per +subbasin when \code{hru_method = "target"}. Default NULL.} + +\item{use_gwflow}{Logical. Enable gwflow groundwater modelling. +When \code{TRUE}, \code{\link[=qswat_setup_gwflow]{qswat_setup_gwflow()}} is called automatically after +\code{\link[=qswat_write_database]{qswat_write_database()}} to initialise the gwflow tables in the +project database. Default FALSE.} + +\item{gwflow_config}{A named list of gwflow settings as returned by +\code{\link[=qswat_read_gwflow_config]{qswat_read_gwflow_config()}}. Only used when \code{use_gwflow = TRUE}. +If \code{NULL} the bundled \code{gwflow.ini} defaults are used.} + +\item{aquifer_thickness}{Numeric constant (aquifer thickness in metres, +default \code{20}) or a file path to a raster or polygon shapefile +containing aquifer thickness values. Passed to \code{\link[=qswat_setup_gwflow]{qswat_setup_gwflow()}} +when \code{use_gwflow = TRUE}.} + +\item{conductivity_file}{Optional path to a polygon shapefile defining +aquifer hydraulic-conductivity zones. Passed to \code{\link[=qswat_setup_gwflow]{qswat_setup_gwflow()}} +when \code{use_gwflow = TRUE}. If \code{NULL} a single default zone is used.} + +\item{default_aquifer_k}{Default hydraulic conductivity (m/day) for the +single zone created when \code{conductivity_file = NULL}. Default \code{1.0}.} + +\item{use_aquifers}{Logical. Create SWAT+ aquifer objects. Default +TRUE.} + +\item{n_processes}{Integer. Number of MPI processes. Default 1.} + +\item{quiet}{Logical. Suppress output. Default FALSE.} + +\item{db_file}{Character or NULL. Output database path.} + +\item{usersoil}{Character or NULL. Soil physical properties dataset to +use when writing the project database. Passed to \code{\link[=qswat_setup]{qswat_setup()}} and +stored in the project object. Accepted values are the same as for +\code{\link[=qswat_setup]{qswat_setup()}}: \code{"FAO_usersoil"}, \code{"global_usersoil"}, a CSV file +path, or \code{NULL} (default, leaves \code{global_usersoil} empty).} +} +\value{ +A \code{qswat_project} object with all results. +} +\description{ +Convenience function that runs the full QSWATPlus workflow in a single +call: project setup, watershed delineation, stream network creation, +HRU generation, and database output. +} +\examples{ +\dontrun{ +dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") +landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") +soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") +lu_lookup <- system.file("extdata", "ravn_landuse.csv", + package = "rQSWATPlus") +soil_lookup <- system.file("extdata", "ravn_soil.csv", + package = "rQSWATPlus") +outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") + +project <- qswat_run( + project_dir = tempdir(), + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup, + outlet_file = outlet, + threshold = 100, + slope_breaks = c(0, 5, 15, 9999) +) +} + +} diff --git a/rQSWATPlus/man/qswat_setup.Rd b/rQSWATPlus/man/qswat_setup.Rd new file mode 100644 index 0000000..9a2808c --- /dev/null +++ b/rQSWATPlus/man/qswat_setup.Rd @@ -0,0 +1,103 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/setup_project.R +\name{qswat_setup} +\alias{qswat_setup} +\title{Set Up a QSWATPlus Project} +\usage{ +qswat_setup( + project_dir, + overwrite = FALSE, + dem_file = NULL, + landuse_file = NULL, + landuse_lookup = NULL, + soil_file = NULL, + soil_lookup = NULL, + outlet_file = NULL, + usersoil = NULL, + ... +) +} +\arguments{ +\item{project_dir}{Character. Path to the project directory. Will be +created if it does not exist.} + +\item{overwrite}{Logical. If TRUE, overwrite existing project files. +Default is FALSE.} + +\item{dem_file}{Character. Path to the DEM (Digital Elevation Model) +raster file (GeoTIFF or other GDAL-supported format).} + +\item{landuse_file}{Character. Path to the land use raster file.} + +\item{landuse_lookup}{Character. Path to the land use lookup CSV file, +mapping raster values to SWAT+ land use codes.} + +\item{soil_file}{Character. Path to the soil raster file.} + +\item{soil_lookup}{Character. Path to the soil lookup CSV file, +mapping raster values to SWAT+ soil names.} + +\item{outlet_file}{Character or NULL. Optional path to outlet point +shapefile for watershed delineation.} + +\item{usersoil}{Character or NULL. Soil physical properties dataset to +use when writing the project database with \code{\link[=qswat_write_database]{qswat_write_database()}}. +Stored in the project object and used automatically unless overridden +in \code{\link[=qswat_write_database]{qswat_write_database()}}. Accepted values: +\describe{ +\item{\code{NULL}}{(default) Leave \code{global_usersoil} empty.} +\item{\code{"FAO_usersoil"}}{Use FAO global soil data from the bundled +\code{QSWATPlusProjHAWQS.sqlite} database.} +\item{\code{"global_usersoil"}}{Use the full global soil dataset from +the bundled \code{QSWATPlusProjHAWQS.sqlite} database.} +\item{file path}{Path to a CSV file in \code{global_usersoil} format. +See \code{\link[=qswat_read_usersoil]{qswat_read_usersoil()}}.} +}} + +\item{...}{Additional arguments to include in the returned project +object. This allows users to add custom metadata or file paths as needed.} +} +\value{ +A list of class \code{"qswat_project"} containing project +configuration and file paths. +} +\description{ +Initializes a QSWATPlus project directory structure and validates +input files. This is typically the first step in the SWAT+ model +setup workflow. +} +\details{ +The function performs the following steps: +\enumerate{ +\item Creates the project directory structure +\item Validates that all input files exist and are readable +\item Checks that raster files have compatible coordinate systems +\item Copies input files to the project directory +\item Returns a project object for use in subsequent functions +} + +All raster inputs should ideally share the same coordinate reference +system. The DEM should be in a projected CRS (not geographic) for +accurate area and slope calculations. +} +\examples{ +\dontrun{ +dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") +landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") +soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") +lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") +soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + +# Setup with FAO soil data applied at database-write time +project <- qswat_setup( + project_dir = tempdir(), + dem_file = dem, + landuse_file = landuse, + landuse_lookup = lu_lookup, + soil_file = soil, + soil_lookup = soil_lookup, + usersoil = "FAO_usersoil" +) +} + +} diff --git a/rQSWATPlus/man/qswat_setup_gwflow.Rd b/rQSWATPlus/man/qswat_setup_gwflow.Rd new file mode 100644 index 0000000..102a1d2 --- /dev/null +++ b/rQSWATPlus/man/qswat_setup_gwflow.Rd @@ -0,0 +1,106 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/gwflow.R +\name{qswat_setup_gwflow} +\alias{qswat_setup_gwflow} +\title{Set Up SWAT+ Groundwater Flow (gwflow) Tables} +\usage{ +qswat_setup_gwflow( + project, + gwflow_config = NULL, + overwrite = FALSE, + aquifer_thickness = 20, + conductivity_file = NULL, + default_aquifer_k = 1 +) +} +\arguments{ +\item{project}{A \code{qswat_project} object returned by +\code{\link[=qswat_write_database]{qswat_write_database()}}.} + +\item{gwflow_config}{A named list of gwflow settings as returned by +\code{\link[=qswat_read_gwflow_config]{qswat_read_gwflow_config()}}. If \code{NULL} the bundled \code{gwflow.ini} +defaults are used.} + +\item{overwrite}{Logical. If \code{TRUE}, drop and recreate existing gwflow +tables. Default \code{FALSE}.} + +\item{aquifer_thickness}{Numeric (constant thickness in metres, +default \code{20}) \strong{or} a file path to a raster (\code{.tif}) or polygon +shapefile (\code{.shp}) containing aquifer thickness values. When a +shapefile is provided it must have a numeric attribute named +\code{"thickness"} or \code{"Avg_Thick"}. When a raster is provided the +mean cell value (in metres) is extracted for each grid cell.} + +\item{conductivity_file}{Optional path to a polygon shapefile that +defines aquifer hydraulic-conductivity zones. The shapefile must +contain a numeric column \code{"aquifer_k"} (K in m/day) or +\code{"logK_Ferr_"} (log10 intrinsic permeability × 100 as stored in +GLHYMPS). If \code{NULL} a single default zone is created using +\code{default_aquifer_k}.} + +\item{default_aquifer_k}{Numeric. Default hydraulic conductivity +(m/day) for the single zone created when \code{conductivity_file} is +\code{NULL}. Default \code{1.0}.} +} +\value{ +The \code{project} object with \code{project$use_gwflow} set to \code{TRUE} +(invisibly). +} +\description{ +Creates the gwflow tables in the project SQLite database and populates +them from the supplied configuration and available spatial data. +This is the R equivalent of the gwflow setup performed by +\code{GWFlow.createTables()} and the GIS routines in the QGIS plugin's +\code{gwflow.py}. +} +\details{ +The function mirrors the SWAT+ gwflow database schema defined in the +Python plugin: \code{gwflow_base}, \code{gwflow_zone}, \code{gwflow_grid}, +\code{gwflow_out_days}, \code{gwflow_obs_locs}, \code{gwflow_solutes}, +\code{gwflow_init_conc}, \code{gwflow_hrucell}, \code{gwflow_fpcell}, +\code{gwflow_rivcell}, \code{gwflow_lsucell}, \code{gwflow_rescell}. + +When the project object contains spatial data (\code{lsu_sf}, \code{hru_sf}, +\code{channels_sf} or \code{streams_sf}) the function also runs the GIS +processing steps to populate the grid and cell connection tables. +This happens automatically when called from \code{\link[=qswat_run]{qswat_run()}} with +\code{use_gwflow = TRUE}. + +The following tables are created and, when spatial data are +available, populated: +\describe{ +\item{gwflow_base}{Single-row table with global gwflow parameters +(always populated; \code{row_count}/\code{col_count} updated after grid +creation).} +\item{gwflow_zone}{Aquifer hydraulic parameter zones (one default +zone, or one row per zone from \code{conductivity_file}).} +\item{gwflow_grid}{Active grid cells with elevation, aquifer +thickness, initial head, and zone assignment.} +\item{gwflow_out_days}{Optional output day pairs (not populated +automatically; left empty for user entry).} +\item{gwflow_obs_locs}{Observation cell IDs (not populated +automatically; left empty for user entry).} +\item{gwflow_solutes}{Chemical solute definitions and initial +concentrations (always populated with 10 default solutes).} +\item{gwflow_init_conc}{Per-cell initial solute concentrations +(not populated automatically).} +\item{gwflow_hrucell}{HRU-to-cell area mapping (populated when +HRU recharge mode is selected and \code{hru_sf} is available).} +\item{gwflow_fpcell}{Floodplain-cell connections (empty in +rQSWATPlus because LSUs are 1:1 with subbasins, with no +floodplain landscape units).} +\item{gwflow_rivcell}{River-cell connections (channel-cell +intersection lengths).} +\item{gwflow_lsucell}{LSU-cell area mapping.} +\item{gwflow_rescell}{Reservoir-cell connections (populated via +the \code{gis_water}–\code{gwflow_lsucell} join when water bodies are +present).} +} +} +\examples{ +\dontrun{ +cfg <- qswat_read_gwflow_config() +project <- qswat_setup_gwflow(project, gwflow_config = cfg) +} + +} diff --git a/rQSWATPlus/man/qswat_write_database.Rd b/rQSWATPlus/man/qswat_write_database.Rd new file mode 100644 index 0000000..eef303a --- /dev/null +++ b/rQSWATPlus/man/qswat_write_database.Rd @@ -0,0 +1,94 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/database.R +\name{qswat_write_database} +\alias{qswat_write_database} +\title{Write SWAT+ Project Database} +\usage{ +qswat_write_database( + project, + db_file = NULL, + overwrite = FALSE, + usersoil = NULL +) +} +\arguments{ +\item{project}{A \code{qswat_project} object with HRU data from +\code{\link[=qswat_create_hrus]{qswat_create_hrus()}}.} + +\item{db_file}{Character or NULL. Path for the output SQLite +database. If NULL, creates the database in the project directory +as \code{project.sqlite}.} + +\item{overwrite}{Logical. If TRUE, overwrite existing database. +Default is FALSE.} + +\item{usersoil}{Character or NULL. Controls which soil physical +properties dataset is used to populate the \code{global_usersoil} table +in the project database. Mirrors the GUI option. When \code{NULL} +(default), the value stored in \code{project$usersoil} (set via +\code{\link[=qswat_setup]{qswat_setup()}}) is used; an explicit value here overrides it. +Accepted values: +\describe{ +\item{\code{NULL}}{Use \code{project$usersoil}, or leave \code{global_usersoil} +empty if that is also NULL.} +\item{\code{"FAO_usersoil"}}{Copy FAO global soil data (13 soil types) +from the bundled \code{QSWATPlusProjHAWQS.sqlite} database.} +\item{\code{"global_usersoil"}}{Copy the full global soil dataset +(thousands of soil types) from the bundled +\code{QSWATPlusProjHAWQS.sqlite} database.} +\item{file path}{Path to a CSV file containing soil parameters in +the \code{global_usersoil} table format. The file must have at minimum +a column named \code{SNAM} (soil name). See \code{\link[=qswat_read_usersoil]{qswat_read_usersoil()}}.} +} +Populating \code{global_usersoil} allows the SWAT+ Editor to resolve +soil physical properties when generating SWAT+ input files.} +} +\value{ +The \code{project} object, updated with the path to the created +database. +} +\description{ +Writes the HRU, subbasin, and routing data to a SWAT+ project +SQLite database. This database is used as input for the SWAT+ +Editor. +} +\details{ +Creates a SQLite database with the following tables: +\describe{ +\item{gis_subbasins}{Subbasin-level data including area, elevation, +and slope statistics} +\item{gis_hrus}{HRU data including subbasin, land use, soil, +slope class, area, and elevation} +\item{gis_routing}{Routing topology connecting subbasins} +\item{gis_channels}{Channel/stream network data} +\item{gis_lsus}{Landscape unit data} +\item{gis_aquifers}{Aquifer data for each subbasin} +\item{gis_deep_aquifers}{Deep aquifer data} +\item{gis_water}{Water body data} +\item{gis_points}{Point source data} +\item{global_usersoil}{Soil physical properties (populated when +\code{usersoil} is specified)} +\item{global_soils}{Soil name lookup (populated when \code{usersoil} +is specified)} +} + +The database format is compatible with the SWAT+ Editor for +further model parameterization. +} +\examples{ +\dontrun{ +# Default: no soil physical properties +db_path <- qswat_write_database(project) + +# Set usersoil in qswat_setup() so it applies automatically +project <- qswat_setup(..., usersoil = "FAO_usersoil") +db_path <- qswat_write_database(project) # uses FAO_usersoil + +# Override the project-level setting at write time +db_path <- qswat_write_database(project, usersoil = "global_usersoil") + +# Use a custom CSV file +db_path <- qswat_write_database(project, usersoil = "my_soils.csv") +} + +} diff --git a/rQSWATPlus/man/rQSWATPlus-package.Rd b/rQSWATPlus/man/rQSWATPlus-package.Rd new file mode 100644 index 0000000..65ba299 --- /dev/null +++ b/rQSWATPlus/man/rQSWATPlus-package.Rd @@ -0,0 +1,56 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/rQSWATPlus-package.R +\docType{package} +\name{rQSWATPlus-package} +\alias{rQSWATPlus-package} +\alias{rQSWATPlus} +\title{rQSWATPlus: R Interface to QSWATPlus for SWAT+ Model Setup} +\description{ +The rQSWATPlus package provides R functions to replicate the QSWATPlus +QGIS plugin workflow for setting up SWAT+ (Soil and Water Assessment Tool) +hydrological models. +} +\details{ +The package implements a complete workflow for SWAT+ model setup: + +\enumerate{ +\item \strong{Project setup} (\code{\link[=qswat_setup]{qswat_setup()}}): Initialize project structure +and validate inputs +\item \strong{Watershed delineation} (\code{\link[=qswat_delineate]{qswat_delineate()}}): DEM processing +and watershed delineation using TauDEM via the traudem package +\item \strong{Stream network} (\code{\link[=qswat_create_streams]{qswat_create_streams()}}): Extract stream +topology from delineation results +\item \strong{HRU creation} (\code{\link[=qswat_create_hrus]{qswat_create_hrus()}}): Create Hydrologic +Response Units from land use, soil, and slope overlays +\item \strong{Database output} (\code{\link[=qswat_write_database]{qswat_write_database()}}): Write SWAT+ +project database for use with SWAT+ Editor +} + +A convenience function \code{\link[=qswat_run]{qswat_run()}} executes all steps in sequence. + +The package includes example data from the Ravn watershed in Denmark +for testing and demonstration. +} +\section{TauDEM Dependency}{ + +Watershed delineation requires TauDEM to be installed. Use +\code{traudem::taudem_sitrep()} to check your installation. See +\code{vignette("taudem-installation", package = "traudem")} for +installation instructions. +} + +\seealso{ +\itemize{ +\item \code{vignette("introduction", package = "rQSWATPlus")} for a +getting started guide +\item \code{vignette("data-requirements", package = "rQSWATPlus")} for +input data format specifications +\item \code{vignette("workflow", package = "rQSWATPlus")} for a complete +workflow example +} +} +\author{ +\strong{Maintainer}: QSWATPlus Contributors \email{qswatplus@example.com} + +} +\keyword{internal} diff --git a/rQSWATPlus/tests/testthat.R b/rQSWATPlus/tests/testthat.R new file mode 100644 index 0000000..0154235 --- /dev/null +++ b/rQSWATPlus/tests/testthat.R @@ -0,0 +1,5 @@ +# This file is part of the standard testthat setup +library(testthat) +library(rQSWATPlus) + +test_check("rQSWATPlus") diff --git a/rQSWATPlus/tests/testthat/test-database.R b/rQSWATPlus/tests/testthat/test-database.R new file mode 100644 index 0000000..8f8a516 --- /dev/null +++ b/rQSWATPlus/tests/testthat/test-database.R @@ -0,0 +1,930 @@ +test_that("qswat_write_database validates input", { + expect_error( + qswat_write_database(list()), + "qswat_project" + ) + project <- structure(list(hru_data = NULL), class = "qswat_project") + expect_error( + qswat_write_database(project), + "No HRU data" + ) +}) + +test_that("database table creation works", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + rQSWATPlus:::.create_db_tables(con) + + tables <- DBI::dbListTables(con) + + # GIS tables + expect_true("gis_subbasins" %in% tables) + expect_true("gis_hrus" %in% tables) + expect_true("gis_routing" %in% tables) + expect_true("gis_channels" %in% tables) + expect_true("gis_lsus" %in% tables) + expect_true("gis_aquifers" %in% tables) + expect_true("gis_deep_aquifers" %in% tables) + expect_true("gis_water" %in% tables) + expect_true("gis_points" %in% tables) + expect_true("gis_elevationbands" %in% tables) + expect_true("gis_landexempt" %in% tables) + expect_true("gis_splithrus" %in% tables) + + # project_config + expect_true("project_config" %in% tables) + + # Configuration tables + expect_true("config_delin" %in% tables) + expect_true("config_hru" %in% tables) + expect_true("config_landuse" %in% tables) + expect_true("config_lsu" %in% tables) + expect_true("config_observed" %in% tables) + expect_true("config_params" %in% tables) + expect_true("config_soil" %in% tables) + + # Intermediate data tables + expect_true("BASINSDATA" %in% tables) + expect_true("HRUSDATA" %in% tables) + expect_true("LSUSDATA" %in% tables) + expect_true("WATERDATA" %in% tables) + + # Reference / lookup tables + expect_true("global_landuses" %in% tables) + expect_true("global_soils" %in% tables) + expect_true("global_usersoil" %in% tables) + expect_true("plant" %in% tables) + expect_true("urban" %in% tables) + expect_true("WGEN_User" %in% tables) + expect_true("WGEN_User_mon" %in% tables) + + # Weather tables + expect_true("weather_sta_cli" %in% tables) +}) + +test_that("subbasin table write works", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + rQSWATPlus:::.create_db_tables(con) + + basin_data <- data.frame( + subbasin = c(1L, 2L), + area_ha = c(100.0, 200.0), + mean_elevation = c(500.0, 600.0), + min_elevation = c(400.0, 500.0), + max_elevation = c(600.0, 700.0), + mean_slope = c(5.0, 10.0), + n_hrus = c(3L, 4L), + n_landuses = c(2L, 3L), + n_soils = c(2L, 2L) + ) + + rQSWATPlus:::.write_subbasin_table(con, basin_data) + + result <- DBI::dbGetQuery(con, "SELECT * FROM gis_subbasins") + expect_equal(nrow(result), 2) + expect_equal(result$id, c(1, 2)) + expect_equal(result$area, c(100, 200)) + expect_equal(result$waterid, c(0L, 0L)) +}) + +# Full database integration test +test_that("full database write works with mock data", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + # Create mock project + project <- structure(list( + project_dir = tempdir(), + hru_data = data.frame( + hru_id = 1:4, + subbasin = c(1L, 1L, 2L, 2L), + landuse = c("AGRL", "FRSD", "PAST", "AGRL"), + soil = c("TX047", "TX047", "TX236", "TX236"), + slope_class = c(1L, 1L, 2L, 2L), + cell_count = c(100L, 50L, 200L, 150L), + area_ha = c(10.0, 5.0, 20.0, 15.0), + mean_elevation = c(500, 510, 600, 610), + mean_slope = c(3.0, 4.0, 8.0, 9.0), + stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), + area_ha = c(15.0, 35.0), + mean_elevation = c(505, 605), + min_elevation = c(490, 580), + max_elevation = c(520, 640), + mean_slope = c(3.5, 8.5), + n_hrus = c(2L, 2L), + n_landuses = c(2L, 2L), + n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(c(0, 5, 15, 9999)), + stream_topology = data.frame( + LINKNO = c(1L, 2L), + DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), + strmOrder = c(2L, 1L), + Length = c(1000, 500), + stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + result <- qswat_write_database(project, db_file = db_file, overwrite = TRUE) + expect_true(file.exists(db_file)) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + subs <- DBI::dbGetQuery(con, "SELECT * FROM gis_subbasins") + expect_equal(nrow(subs), 2) + expect_true("waterid" %in% names(subs)) + expect_equal(subs$waterid, c(0L, 0L)) + + hrus <- DBI::dbGetQuery(con, "SELECT * FROM gis_hrus") + expect_equal(nrow(hrus), 4) + + routing <- DBI::dbGetQuery(con, "SELECT * FROM gis_routing") + expect_true(nrow(routing) > 0) + expect_true("hyd_typ" %in% names(routing)) + expect_true(all(routing$hyd_typ == "tot")) + + channels <- DBI::dbGetQuery(con, "SELECT * FROM gis_channels") + expect_true(nrow(channels) > 0) + expect_true("strahler" %in% names(channels)) + expect_true("midlat" %in% names(channels)) + expect_true("midlon" %in% names(channels)) + expect_equal(channels$strahler, c(2L, 1L)) + + # Check LSU table has new fields + lsus <- DBI::dbGetQuery(con, "SELECT * FROM gis_lsus") + expect_true(nrow(lsus) > 0) + expect_true("subbasin" %in% names(lsus)) + expect_true("len1" %in% names(lsus)) + expect_equal(lsus$subbasin, c(1L, 2L)) + + # Check project_config + pc <- DBI::dbGetQuery(con, "SELECT * FROM project_config") + expect_equal(nrow(pc), 1) + expect_equal(pc$delineation_done, 1) + expect_equal(pc$hrus_done, 1) + expect_true("use_gwflow" %in% names(pc)) + expect_equal(pc$use_gwflow, 0L) + + # Check BASINSDATA + bd <- DBI::dbGetQuery(con, "SELECT * FROM BASINSDATA") + expect_equal(nrow(bd), 2) + + # Check HRUSDATA + hd <- DBI::dbGetQuery(con, "SELECT * FROM HRUSDATA") + expect_equal(nrow(hd), 4) + + # Check LSUSDATA + ld <- DBI::dbGetQuery(con, "SELECT * FROM LSUSDATA") + expect_equal(nrow(ld), 2) +}) + +test_that("reference database is copied to project folder", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + project_dir <- tempfile("refdb_") + dir.create(project_dir) + on.exit(unlink(project_dir, recursive = TRUE), add = TRUE) + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + project <- structure(list( + project_dir = project_dir, + hru_data = data.frame( + hru_id = 1:2, subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), min_elevation = c(490, 580), + max_elevation = c(510, 620), mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), n_landuses = c(1L, 1L), n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = c(1L, 2L), DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), strmOrder = c(2L, 1L), + Length = c(1000, 500), stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + result <- qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + # Reference database should have been copied + ref_db <- file.path(project_dir, "swatplus_datasets.sqlite") + expect_true(file.exists(ref_db)) + + # project_config.reference_db should point to the copy + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + pc <- DBI::dbGetQuery(con, "SELECT reference_db FROM project_config") + expect_equal(pc$reference_db, normalizePath(ref_db, mustWork = FALSE)) +}) + +test_that("second write does not re-copy reference database", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + project_dir <- tempfile("refdb2_") + dir.create(project_dir) + on.exit(unlink(project_dir, recursive = TRUE), add = TRUE) + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + project <- structure(list( + project_dir = project_dir, + hru_data = data.frame( + hru_id = 1L, subbasin = 1L, landuse = "AGRL", soil = "TX047", + slope_class = 1L, cell_count = 100L, area_ha = 10.0, + mean_elevation = 500, mean_slope = 3.0, stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = 1L, area_ha = 10.0, mean_elevation = 500, + min_elevation = 490, max_elevation = 510, mean_slope = 3.0, + n_hrus = 1L, n_landuses = 1L, n_soils = 1L, + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = 1L, DSLINKNO = -1L, WSNO = 1L, strmOrder = 1L, + Length = 500, stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + ref_db <- file.path(project_dir, "swatplus_datasets.sqlite") + mtime1 <- file.info(ref_db)$mtime + + # Write again - should NOT overwrite the existing copy + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + mtime2 <- file.info(ref_db)$mtime + expect_equal(mtime1, mtime2) +}) + +# Regression test: TauDEM sometimes assigns WSNO=0 to the outlet reach when +# an outlet point is used. That channel has no corresponding subbasin and +# must not end up in gis_channels (it would otherwise cause a referential +# integrity failure in qswat_check_database). Upstream routing must be +# redirected directly to "outlet". +test_that("WSNO=0 outlet stream is excluded from channels and routing", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + project_dir <- tempfile("test_wsno0_outlet_") + dir.create(project_dir) + on.exit(unlink(project_dir, recursive = TRUE), add = TRUE) + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + # Topology: two real subbasins (WSNO 1, 2) draining into an outlet reach + # (WSNO=0) which flows to the watershed outlet. + # Link 2 (WSNO=2) → Link 1 (WSNO=1) → Link 3 (WSNO=0, outlet reach) → out + project <- structure(list( + project_dir = project_dir, + hru_data = data.frame( + hru_id = c(1L, 2L), + subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), + soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), + cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), + stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), + area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), + min_elevation = c(490, 580), + max_elevation = c(510, 620), + mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), + n_landuses = c(1L, 1L), + n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = c(1L, 2L, 3L), + DSLINKNO = c(3L, 1L, -1L), # 1→3(WSNO=0), 2→1, 3 is outlet + WSNO = c(1L, 2L, 0L), # WSNO=0 for the outlet reach + strmOrder = c(2L, 1L, 3L), + Length = c(1000, 500, 200), + stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + # gis_channels must not contain a row with subbasin = 0 + channels <- DBI::dbGetQuery(con, "SELECT * FROM gis_channels") + expect_true( + all(channels$subbasin > 0), + label = "No channel has subbasin = 0" + ) + # Only the 2 real subbasins should appear as channels + expect_equal(nrow(channels), 2L) + + # gis_routing must not have a source row for subbasin 0 + routing <- DBI::dbGetQuery(con, "SELECT * FROM gis_routing") + expect_true( + all(routing$sourceid > 0), + label = "No routing source has id = 0" + ) + + # Subbasin 1 was previously routing to subbasin 0 (the outlet reach) - it + # should now route directly to the outlet + sub1_route <- routing[routing$sourceid == 1L, ] + expect_equal(nrow(sub1_route), 1L) + expect_equal(sub1_route$sinkcat, "outlet") + + # The overall database must pass qswat_check_database + result <- qswat_check_database(db_file, verbose = FALSE) + expect_true( + result$passed, + label = paste( + "Database with WSNO=0 topology passes SWAT+ Editor check.", + if (!result$passed) + paste("Errors:", paste(result$errors, collapse = "; ")) else "" + ) + ) +}) + + +# ---- midlat/midlon tests ---- + +test_that("midlat and midlon are computed from streams_sf geometry", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + skip_if_not_installed("sf") + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + # Build two simple linestrings in UTM-like projected coords (EPSG:32632) + # Line 1: from (500000, 5000000) to (501000, 5001000) -> midpoint ~(500500, 5000500) + # Line 2: from (502000, 5002000) to (502500, 5002500) -> midpoint ~(502250, 5002250) + line1 <- sf::st_linestring(matrix(c(500000, 5000000, 501000, 5001000), ncol = 2, byrow = TRUE)) + line2 <- sf::st_linestring(matrix(c(502000, 5002000, 502500, 5002500), ncol = 2, byrow = TRUE)) + streams_sf <- sf::st_sf( + LINKNO = c(1L, 2L), + DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), + strmOrder = c(2L, 1L), + Length = c(1000, 500), + geometry = sf::st_sfc(line1, line2, crs = 32632) + ) + + project <- structure(list( + dem_file = dem, + project_dir = tempdir(), + hru_data = data.frame( + hru_id = 1:2, subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), min_elevation = c(490, 580), + max_elevation = c(510, 620), mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), n_landuses = c(1L, 1L), n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = c(1L, 2L), DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), strmOrder = c(2L, 1L), + Length = c(1000, 500), stringsAsFactors = FALSE + ), + streams_sf = streams_sf + ), class = "qswat_project") + + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + channels <- DBI::dbGetQuery(con, "SELECT midlat, midlon FROM gis_channels") + expect_equal(nrow(channels), 2L) + + # midlat/midlon must be non-zero (projected midpoints converted to WGS84) + expect_true(all(channels$midlat != 0), label = "midlat is non-zero") + expect_true(all(channels$midlon != 0), label = "midlon is non-zero") + + # Both midlatitudes should be positive (northern hemisphere, UTM zone 32N ~45 N) + expect_true(all(channels$midlat > 0), label = "midlat is positive (N hemisphere)") + + # midlon values should be within a plausible range for UTM zone 32 (~6-18 E) + expect_true(all(channels$midlon > 0 & channels$midlon < 30), + label = "midlon in plausible range for UTM zone 32") +}) + + + +# ---- lat/lon from watershed raster tests ---- + +test_that("lat/lon in gis tables are non-zero when watershed raster is supplied", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + skip_if_not_installed("sf") + skip_if_not_installed("terra") + + db_file <- tempfile(fileext = ".sqlite") + rast_file <- tempfile(fileext = ".tif") + on.exit({ unlink(db_file); unlink(rast_file) }, add = TRUE) + + # Two-cell watershed raster in UTM zone 32N (EPSG:32632) + r <- terra::rast( + nrows = 10, ncols = 10, + xmin = 500000, xmax = 503000, + ymin = 5000000, ymax = 5003000, + crs = "EPSG:32632" + ) + terra::values(r) <- NA_real_ + terra::values(r)[terra::cellFromRowCol(r, 2, 2)] <- 1 # subbasin 1 + terra::values(r)[terra::cellFromRowCol(r, 8, 8)] <- 2 # subbasin 2 + terra::writeRaster(r, rast_file, overwrite = TRUE) + + project <- structure(list( + project_dir = tempdir(), + watershed_file = rast_file, + hru_data = data.frame( + hru_id = 1:2, subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), min_elevation = c(490, 580), + max_elevation = c(510, 620), mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), n_landuses = c(1L, 1L), n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = c(1L, 2L), DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), strmOrder = c(2L, 1L), + Length = c(1000, 500), stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + for (tbl_name in c("gis_subbasins", "gis_hrus", "gis_lsus", + "gis_aquifers", "gis_deep_aquifers")) { + tbl <- DBI::dbGetQuery(con, paste0("SELECT lat, lon FROM ", tbl_name)) + expect_true(all(tbl$lat != 0), + label = paste(tbl_name, "lat is non-zero")) + expect_true(all(tbl$lon != 0), + label = paste(tbl_name, "lon is non-zero")) + expect_true(all(tbl$lat > 0), + label = paste(tbl_name, "lat is positive (N hemisphere)")) + expect_true(all(tbl$lon > 0 & tbl$lon < 30), + label = paste(tbl_name, "lon in plausible UTM 32N range")) + } +}) + + +# ---- qswat_read_gis tests ---- + +test_that("qswat_read_gis returns a list of gis_* tables", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + project <- structure(list( + project_dir = tempdir(), + hru_data = data.frame( + hru_id = 1:2, subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), min_elevation = c(490, 580), + max_elevation = c(510, 620), mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), n_landuses = c(1L, 1L), n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = c(1L, 2L), DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), strmOrder = c(2L, 1L), + Length = c(1000, 500), stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + gis <- qswat_read_gis(db_file) + + expect_type(gis, "list") + expect_true(length(gis) > 0, label = "at least one gis_* table returned") + expect_true(all(grepl("^gis_", names(gis))), + label = "all list names start with gis_") + + expect_true("gis_subbasins" %in% names(gis)) + expect_true("gis_channels" %in% names(gis)) + expect_true("gis_hrus" %in% names(gis)) + expect_true("gis_lsus" %in% names(gis)) + expect_true("gis_aquifers" %in% names(gis)) + + expect_true(all(vapply(gis, is.data.frame, logical(1))), + label = "each gis table is a data.frame") + + expect_equal(nrow(gis$gis_subbasins), 2L) + expect_equal(nrow(gis$gis_hrus), 2L) +}) + +test_that("qswat_read_gis errors on missing file", { + expect_error(qswat_read_gis("/nonexistent/path.sqlite"), "not found") +}) + +test_that("qswat_read_gis errors on non-character input", { + expect_error(qswat_read_gis(42), "single character") +}) + +# ---- usersoil tests ---- + +# Shared minimal project fixture used by all usersoil tests +.make_usersoil_project <- function() { + project_dir <- tempfile("usersoil_") + dir.create(project_dir) + structure(list( + project_dir = project_dir, + hru_data = data.frame( + hru_id = 1:2, + subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), + soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), + cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), + stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), + area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), + min_elevation = c(490, 580), + max_elevation = c(510, 620), + mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), + n_landuses = c(1L, 1L), + n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = c(1L, 2L), DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), strmOrder = c(2L, 1L), + Length = c(1000, 500), stringsAsFactors = FALSE + ) + ), class = "qswat_project") +} + +test_that("usersoil = 'FAO_usersoil' populates global_usersoil from FAO data", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + proj_hawqs <- system.file("extdata", "QSWATPlusProjHAWQS.sqlite", + package = "rQSWATPlus") + skip_if(proj_hawqs == "", message = "QSWATPlusProjHAWQS.sqlite not available") + + project <- .make_usersoil_project() + db_file <- tempfile(fileext = ".sqlite") + on.exit({ + unlink(db_file) + unlink(project$project_dir, recursive = TRUE) + }, add = TRUE) + + result <- qswat_write_database(project, db_file = db_file, + overwrite = TRUE, + usersoil = "FAO_usersoil") + expect_true(file.exists(db_file)) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + us <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_usersoil") + expect_gt(us$n, 0L, label = "global_usersoil has rows after FAO_usersoil load") + + # FAO dataset has 13 soil types + expect_equal(us$n, 13L) + + gs <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_soils") + expect_gt(gs$n, 0L, label = "global_soils populated alongside global_usersoil") + + # SNAM values should be present + snams <- DBI::dbGetQuery(con, "SELECT SNAM FROM global_usersoil")$SNAM + expect_true(all(nzchar(snams))) +}) + +test_that("usersoil = 'global_usersoil' populates global_usersoil with full dataset", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + proj_hawqs <- system.file("extdata", "QSWATPlusProjHAWQS.sqlite", + package = "rQSWATPlus") + skip_if(proj_hawqs == "", message = "QSWATPlusProjHAWQS.sqlite not available") + + project <- .make_usersoil_project() + db_file <- tempfile(fileext = ".sqlite") + on.exit({ + unlink(db_file) + unlink(project$project_dir, recursive = TRUE) + }, add = TRUE) + + result <- qswat_write_database(project, db_file = db_file, + overwrite = TRUE, + usersoil = "global_usersoil") + expect_true(file.exists(db_file)) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + us <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_usersoil") + expect_gt(us$n, 0L) + + # global dataset is much larger than FAO + fao_n <- 13L + expect_gt(us$n, fao_n, label = "global_usersoil > FAO count") + + gs <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_soils") + expect_gt(gs$n, 0L) +}) + +test_that("usersoil = CSV path populates global_usersoil from user file", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + # Write a minimal usersoil CSV with 3 soil types + csv_file <- tempfile(fileext = ".csv") + usersoil_df <- data.frame( + SNAM = c("MySoilA", "MySoilB", "MySoilC"), + NLAYERS = c(2L, 3L, 1L), + HYDGRP = c("B", "C", "A"), + SOL_ZMX = c(1000, 1500, 500), + ANION_EXCL = c(0.5, 0.5, 0.5), + SOL_CRK = c(0.5, 0.5, 0.5), + SOL_Z1 = c(300, 200, 500), + SOL_BD1 = c(1.4, 1.5, 1.3), + SOL_AWC1 = c(0.15, 0.12, 0.18), + SOL_K1 = c(8.0, 5.0, 12.0), + SOL_CBN1 = c(1.5, 2.0, 1.0), + CLAY1 = c(25, 35, 15), + SILT1 = c(30, 25, 20), + SAND1 = c(45, 40, 65), + stringsAsFactors = FALSE + ) + utils::write.csv(usersoil_df, csv_file, row.names = FALSE) + on.exit(unlink(csv_file), add = TRUE) + + project <- .make_usersoil_project() + db_file <- tempfile(fileext = ".sqlite") + on.exit({ + unlink(db_file) + unlink(project$project_dir, recursive = TRUE) + }, add = TRUE) + + result <- qswat_write_database(project, db_file = db_file, + overwrite = TRUE, + usersoil = csv_file) + expect_true(file.exists(db_file)) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + us <- DBI::dbGetQuery(con, "SELECT * FROM global_usersoil") + expect_equal(nrow(us), 3L) + expect_true("SNAM" %in% names(us)) + expect_setequal(us$SNAM, c("MySoilA", "MySoilB", "MySoilC")) + expect_equal(us$NLAYERS, c(2, 3, 1)) + + gs <- DBI::dbGetQuery(con, "SELECT * FROM global_soils") + expect_equal(nrow(gs), 3L) + expect_setequal(gs$SNAM, c("MySoilA", "MySoilB", "MySoilC")) +}) + +test_that("usersoil = NULL leaves global_usersoil empty (default behaviour)", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + project <- .make_usersoil_project() + db_file <- tempfile(fileext = ".sqlite") + on.exit({ + unlink(db_file) + unlink(project$project_dir, recursive = TRUE) + }, add = TRUE) + + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + us <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_usersoil") + expect_equal(us$n, 0L, label = "global_usersoil empty when usersoil = NULL") +}) + +test_that("usersoil CSV with missing SNAM column raises an error", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + bad_csv <- tempfile(fileext = ".csv") + utils::write.csv(data.frame(SoilName = "X", SOL_Z1 = 300), bad_csv, + row.names = FALSE) + on.exit(unlink(bad_csv), add = TRUE) + + project <- .make_usersoil_project() + db_file <- tempfile(fileext = ".sqlite") + on.exit({ + unlink(db_file) + unlink(project$project_dir, recursive = TRUE) + }, add = TRUE) + + expect_error( + qswat_write_database(project, db_file = db_file, overwrite = TRUE, + usersoil = bad_csv), + "SNAM" + ) +}) + +test_that("usersoil = non-existent file path raises an error", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + project <- .make_usersoil_project() + db_file <- tempfile(fileext = ".sqlite") + on.exit({ + unlink(db_file) + unlink(project$project_dir, recursive = TRUE) + }, add = TRUE) + + expect_error( + qswat_write_database(project, db_file = db_file, overwrite = TRUE, + usersoil = "/nonexistent/path/soils.csv"), + "not found" + ) +}) + +test_that("project$usersoil is used when no explicit usersoil arg given", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + proj_hawqs <- system.file("extdata", "QSWATPlusProjHAWQS.sqlite", + package = "rQSWATPlus") + skip_if(proj_hawqs == "", message = "QSWATPlusProjHAWQS.sqlite not available") + + # Build a project object that has usersoil = "FAO_usersoil" stored in it + project <- .make_usersoil_project() + project$usersoil <- "FAO_usersoil" + + db_file <- tempfile(fileext = ".sqlite") + on.exit({ + unlink(db_file) + unlink(project$project_dir, recursive = TRUE) + }, add = TRUE) + + # No explicit usersoil argument - should use project$usersoil + result <- qswat_write_database(project, db_file = db_file, overwrite = TRUE) + expect_true(file.exists(db_file)) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + us <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_usersoil") + expect_equal(us$n, 13L, + label = "project$usersoil='FAO_usersoil' loads 13 FAO rows") +}) + +test_that("explicit usersoil arg overrides project$usersoil", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + proj_hawqs <- system.file("extdata", "QSWATPlusProjHAWQS.sqlite", + package = "rQSWATPlus") + skip_if(proj_hawqs == "", message = "QSWATPlusProjHAWQS.sqlite not available") + + # Project says FAO, but we override with global_usersoil at write time + project <- .make_usersoil_project() + project$usersoil <- "FAO_usersoil" + + db_file <- tempfile(fileext = ".sqlite") + on.exit({ + unlink(db_file) + unlink(project$project_dir, recursive = TRUE) + }, add = TRUE) + + # Explicit "global_usersoil" overrides the project-level "FAO_usersoil" + qswat_write_database(project, db_file = db_file, overwrite = TRUE, + usersoil = "global_usersoil") + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + us <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_usersoil") + expect_gt(us$n, 13L, + label = "explicit usersoil='global_usersoil' overrides FAO (>13 rows)") +}) + +test_that("qswat_setup with usersoil flows through qswat_write_database", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + proj_hawqs <- system.file("extdata", "QSWATPlusProjHAWQS.sqlite", + package = "rQSWATPlus") + skip_if(proj_hawqs == "", message = "QSWATPlusProjHAWQS.sqlite not available") + + # Simulate what qswat_setup() produces (without DEM loading) + proj_dir <- tempfile("setup_flow_") + dir.create(proj_dir) + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_setup(project_dir = proj_dir, usersoil = "FAO_usersoil") + expect_equal(project$usersoil, "FAO_usersoil") + + # Attach mock HRU/basin data so qswat_write_database() is happy + project$hru_data <- data.frame( + hru_id = 1:2, subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), stringsAsFactors = FALSE + ) + project$basin_data <- data.frame( + subbasin = c(1L, 2L), area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), min_elevation = c(490, 580), + max_elevation = c(510, 620), mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), n_landuses = c(1L, 1L), n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ) + project$slope_classes <- qswat_create_slope_classes() + project$stream_topology <- data.frame( + LINKNO = c(1L, 2L), DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), strmOrder = c(2L, 1L), + Length = c(1000, 500), stringsAsFactors = FALSE + ) + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + result <- qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + us <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_usersoil") + expect_equal(us$n, 13L, + label = "usersoil set in qswat_setup flows to database write") +}) + diff --git a/rQSWATPlus/tests/testthat/test-delineation.R b/rQSWATPlus/tests/testthat/test-delineation.R new file mode 100644 index 0000000..64a9de2 --- /dev/null +++ b/rQSWATPlus/tests/testthat/test-delineation.R @@ -0,0 +1,110 @@ +test_that("qswat_delineate requires qswat_project", { + expect_error( + qswat_delineate(list()), + "qswat_project" + ) +}) + +test_that("qswat_create_streams requires delineation", { + project <- structure(list(stream_file = NULL), class = "qswat_project") + expect_error( + qswat_create_streams(project), + "Delineation must be run first" + ) +}) + +# Integration test that requires TauDEM - skipped if not available +test_that("full delineation workflow runs with TauDEM", { + skip_if_not_installed("traudem") + + # Check TauDEM availability + taudem_ok <- tryCatch({ + traudem::taudem_sitrep() + TRUE + }, error = function(e) FALSE) + skip_if(!taudem_ok, "TauDEM not available") + + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + + skip_if(dem == "", message = "Example data not available") + + proj_dir <- file.path(tempdir(), "test_delineation") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_setup( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup + ) + + project <- qswat_delineate(project, threshold = 500, quiet = FALSE) + + expect_false(is.null(project$fel_file)) + expect_true(file.exists(project$fel_file)) + expect_false(is.null(project$p_file)) + expect_true(file.exists(project$p_file)) + expect_false(is.null(project$ad8_file)) + expect_true(file.exists(project$ad8_file)) + expect_false(is.null(project$stream_file)) + expect_true(file.exists(project$stream_file)) + expect_false(is.null(project$watershed_file)) + expect_true(file.exists(project$watershed_file)) + expect_equal(project$stream_threshold, 500) +}) + +# Integration test that requires TauDEM - skipped if not available +test_that("full delineation workflow runs with TauDEM with outlet", { + skip_if_not_installed("traudem") + + # Check TauDEM availability + taudem_ok <- tryCatch({ + traudem::taudem_sitrep() + TRUE + }, error = function(e) FALSE) + skip_if(!taudem_ok, "TauDEM not available") + + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") + + skip_if(dem == "", message = "Example data not available") + + proj_dir <- file.path(tempdir(), "test_delineation") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_setup( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup, + outlet_file = outlet + ) + + project <- qswat_delineate(project, threshold = 500, quiet = FALSE) + + expect_false(is.null(project$fel_file)) + expect_true(file.exists(project$fel_file)) + expect_false(is.null(project$p_file)) + expect_true(file.exists(project$p_file)) + expect_false(is.null(project$ad8_file)) + expect_true(file.exists(project$ad8_file)) + expect_false(is.null(project$stream_file)) + expect_true(file.exists(project$stream_file)) + expect_false(is.null(project$watershed_file)) + expect_true(file.exists(project$watershed_file)) + expect_equal(project$stream_threshold, 500) + expect_false(is.null(project$outlet_file)) + expect_true(file.exists(project$outlet_file)) +}) diff --git a/rQSWATPlus/tests/testthat/test-gwflow.R b/rQSWATPlus/tests/testthat/test-gwflow.R new file mode 100644 index 0000000..b5b2495 --- /dev/null +++ b/rQSWATPlus/tests/testthat/test-gwflow.R @@ -0,0 +1,251 @@ +test_that("qswat_read_gwflow_config reads bundled ini file", { + ini <- system.file("extdata", "gwflow.ini", package = "rQSWATPlus") + cfg <- qswat_read_gwflow_config(ini) + + expect_type(cfg, "list") + expect_equal(cfg$cell_size, 200L) + expect_equal(cfg$boundary_conditions, 1L) + expect_equal(cfg$wt_depth, 5.0) + expect_equal(cfg$init_no3, 3.0) + expect_equal(cfg$init_p, 0.05) + expect_equal(cfg$nit_sorp, 1.0) + expect_equal(cfg$denit_constant, -0.0001) + expect_equal(cfg$solute_transport, 1L) + expect_equal(cfg$ext_pumping, 0L) + expect_equal(cfg$daily_output, 1L) + expect_equal(cfg$annual_output, 1L) +}) + +test_that("qswat_read_gwflow_config uses defaults when NULL path", { + cfg <- qswat_read_gwflow_config() + expect_type(cfg, "list") + expect_equal(cfg$cell_size, 200L) +}) + +test_that("qswat_read_gwflow_config errors on missing file", { + expect_error( + qswat_read_gwflow_config("/nonexistent/gwflow.ini"), + "not found" + ) +}) + +test_that("qswat_setup_gwflow creates gwflow tables", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + project_dir <- tempfile("gw_") + dir.create(project_dir) + on.exit(unlink(project_dir, recursive = TRUE), add = TRUE) + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + project <- structure(list( + project_dir = project_dir, + hru_data = data.frame( + hru_id = 1:2, subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), min_elevation = c(490, 580), + max_elevation = c(510, 620), mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), n_landuses = c(1L, 1L), n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = c(1L, 2L), DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), strmOrder = c(2L, 1L), + Length = c(1000, 500), stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + project <- qswat_write_database(project, db_file = db_file, overwrite = TRUE) + cfg <- qswat_read_gwflow_config() + result <- qswat_setup_gwflow(project, gwflow_config = cfg, overwrite = TRUE) + + expect_true(result$use_gwflow) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + tbls <- DBI::dbListTables(con) + gwflow_tables <- c("gwflow_base", "gwflow_zone", "gwflow_grid", + "gwflow_out_days", "gwflow_obs_locs", "gwflow_solutes", + "gwflow_init_conc", "gwflow_hrucell", "gwflow_fpcell", + "gwflow_rivcell", "gwflow_lsucell", "gwflow_rescell") + for (tbl in gwflow_tables) { + expect_true(tbl %in% tbls, label = paste0("table '", tbl, "' exists")) + } +}) + +test_that("gwflow_base is populated with correct values", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + project_dir <- tempfile("gwbase_") + dir.create(project_dir) + on.exit(unlink(project_dir, recursive = TRUE), add = TRUE) + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + project <- structure(list( + project_dir = project_dir, + hru_data = data.frame( + hru_id = 1L, subbasin = 1L, landuse = "AGRL", soil = "TX047", + slope_class = 1L, cell_count = 100L, area_ha = 10.0, + mean_elevation = 500, mean_slope = 3.0, stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = 1L, area_ha = 10.0, mean_elevation = 500, + min_elevation = 490, max_elevation = 510, mean_slope = 3.0, + n_hrus = 1L, n_landuses = 1L, n_soils = 1L, + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = 1L, DSLINKNO = -1L, WSNO = 1L, strmOrder = 1L, + Length = 500, stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + project <- qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + cfg <- qswat_read_gwflow_config() + qswat_setup_gwflow(project, gwflow_config = cfg, overwrite = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + base <- DBI::dbGetQuery(con, "SELECT * FROM gwflow_base") + expect_equal(nrow(base), 1) + expect_equal(base$cell_size, cfg$cell_size) + expect_equal(base$boundary_conditions, cfg$boundary_conditions) + expect_equal(base$water_table_depth, cfg$wt_depth) + expect_equal(base$et_extinction_depth, cfg$exdp) + expect_equal(base$recharge, cfg$hruorlsu_recharge) + expect_equal(base$solute_transport, cfg$solute_transport) +}) + +test_that("gwflow_solutes contains 10 default solutes", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + project_dir <- tempfile("gwsol_") + dir.create(project_dir) + on.exit(unlink(project_dir, recursive = TRUE), add = TRUE) + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + project <- structure(list( + project_dir = project_dir, + hru_data = data.frame( + hru_id = 1L, subbasin = 1L, landuse = "AGRL", soil = "TX047", + slope_class = 1L, cell_count = 100L, area_ha = 10.0, + mean_elevation = 500, mean_slope = 3.0, stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = 1L, area_ha = 10.0, mean_elevation = 500, + min_elevation = 490, max_elevation = 510, mean_slope = 3.0, + n_hrus = 1L, n_landuses = 1L, n_soils = 1L, + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = 1L, DSLINKNO = -1L, WSNO = 1L, strmOrder = 1L, + Length = 500, stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + project <- qswat_write_database(project, db_file = db_file, overwrite = TRUE) + cfg <- qswat_read_gwflow_config() + qswat_setup_gwflow(project, gwflow_config = cfg, overwrite = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + sol <- DBI::dbGetQuery(con, "SELECT * FROM gwflow_solutes") + expect_equal(nrow(sol), 10) + expect_true("no3-n" %in% sol$solute_name) + expect_true("p" %in% sol$solute_name) + + # Check NO3 and P initial concentrations come from config + no3_row <- sol[sol$solute_name == "no3-n", ] + expect_equal(no3_row$init_conc, cfg$init_no3) + p_row <- sol[sol$solute_name == "p", ] + expect_equal(p_row$init_conc, cfg$init_p) +}) + +test_that("qswat_setup_gwflow updates use_gwflow in project_config", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + project_dir <- tempfile("gwflag_") + dir.create(project_dir) + on.exit(unlink(project_dir, recursive = TRUE), add = TRUE) + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + project <- structure(list( + project_dir = project_dir, + hru_data = data.frame( + hru_id = 1L, subbasin = 1L, landuse = "AGRL", soil = "TX047", + slope_class = 1L, cell_count = 100L, area_ha = 10.0, + mean_elevation = 500, mean_slope = 3.0, stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = 1L, area_ha = 10.0, mean_elevation = 500, + min_elevation = 490, max_elevation = 510, mean_slope = 3.0, + n_hrus = 1L, n_landuses = 1L, n_soils = 1L, + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = 1L, DSLINKNO = -1L, WSNO = 1L, strmOrder = 1L, + Length = 500, stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + project <- qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + pc_before <- DBI::dbGetQuery(con, "SELECT use_gwflow FROM project_config") + DBI::dbDisconnect(con) + expect_equal(pc_before$use_gwflow, 0L) + + qswat_setup_gwflow(project, overwrite = TRUE) + + con2 <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con2), add = TRUE) + pc_after <- DBI::dbGetQuery(con2, "SELECT use_gwflow FROM project_config") + expect_equal(pc_after$use_gwflow, 1L) + + tabs <- DBI::dbListTables(con2) + gwflow_tabs <- tabs[grepl("gwflow", tabs)] + for (tbl in gwflow_tabs) { + # expect_true(tbl %in% tabs, label = paste0("table '", tbl, "' exists")) + df <- DBI::dbGetQuery(con2, paste0("SELECT * FROM ", tbl)) + # expect_true(nrow(df) > 0, label = paste0("table '", tbl, "' is queryable")) + } + + gwflow_grid <- DBI::dbGetQuery(con2, "SELECT * FROM gwflow_grid") +}) + +test_that("qswat_setup_gwflow validates project object", { + expect_error( + qswat_setup_gwflow(list()), + "qswat_project" + ) + project <- structure(list(db_file = NULL), class = "qswat_project") + expect_error( + qswat_setup_gwflow(project), + "No database found" + ) +}) diff --git a/rQSWATPlus/tests/testthat/test-hru.R b/rQSWATPlus/tests/testthat/test-hru.R new file mode 100644 index 0000000..d71c61e --- /dev/null +++ b/rQSWATPlus/tests/testthat/test-hru.R @@ -0,0 +1,98 @@ +test_that("internal slope classification works", { + slopes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) + # Test classification + classes <- rQSWATPlus:::.classify_slope(c(0, 2.5, 5, 10, 15, 50, NA), slopes) + expect_equal(classes[1], 1L) # 0% -> class 1 + expect_equal(classes[2], 1L) # 2.5% -> class 1 + expect_equal(classes[3], 2L) # 5% -> class 2 + expect_equal(classes[4], 2L) # 10% -> class 2 + expect_equal(classes[5], 3L) # 15% -> class 3 + expect_equal(classes[6], 3L) # 50% -> class 3 + expect_true(is.na(classes[7])) # NA -> NA +}) + +test_that("internal lookup mapping works", { + values <- c(1, 2, 3, 4, 5, 99) + lookup_vals <- c(1, 2, 3, 4, 5) + lookup_names <- c("AGRL", "FRSD", "PAST", "WATR", "URBN") + + result <- rQSWATPlus:::.map_lookup(values, lookup_vals, lookup_names) + expect_equal(result[1], "AGRL") + expect_equal(result[5], "URBN") + expect_true(is.na(result[6])) # 99 not in lookup +}) + +test_that("internal cell area calculation works", { + skip_if_not_installed("terra") + + # Create a small test raster + r <- terra::rast(nrows = 10, ncols = 10, + xmin = 0, xmax = 1000, ymin = 0, ymax = 1000, + crs = "EPSG:32632") # UTM zone 32N (meters) + area <- rQSWATPlus:::.compute_cell_area(r, "meters") + # Each cell is 100m x 100m = 10000 m2 = 1 ha + expect_equal(area, 1.0) +}) + +test_that("qswat_create_hrus validates input", { + expect_error( + qswat_create_hrus(list(), data.frame(), data.frame()), + "qswat_project" + ) +}) + +# Integration test for HRU creation (requires TauDEM for delineation) +test_that("HRU creation works with delineated project", { + skip_if_not_installed("traudem") + taudem_ok <- tryCatch({ + traudem::taudem_sitrep() + TRUE + }, error = function(e) FALSE) + skip_if(!taudem_ok, "TauDEM not available") + + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup_file <- system.file("extdata", "ravn_landuse.csv", + package = "rQSWATPlus") + soil_lookup_file <- system.file("extdata", "ravn_soil.csv", + package = "rQSWATPlus") + usersoil_file <- system.file("extdata", "ravn_usersoil.csv", + package = "rQSWATPlus") + + skip_if(dem == "", message = "Example data not available") + + proj_dir <- file.path(tempdir(), "test_hru") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_setup( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup_file, + soil_lookup = soil_lookup_file, + usersoil = usersoil_file + ) |> + qswat_delineate(threshold = 500, quiet = TRUE) |> + qswat_create_streams() |> + qswat_create_hrus() |> + qswat_write_database(overwrite = TRUE) + + expect_false(is.null(project$hru_data)) + expect_true(nrow(project$hru_data) > 0) + expect_true("subbasin" %in% names(project$hru_data)) + expect_true("landuse" %in% names(project$hru_data)) + expect_true("soil" %in% names(project$hru_data)) + expect_true("area_ha" %in% names(project$hru_data)) + + expect_false(is.null(project$basin_data)) + expect_true(nrow(project$basin_data) > 0) + + # Verify usersoil was stored and written to the database + expect_equal(project$usersoil, usersoil_file) + con <- DBI::dbConnect(RSQLite::SQLite(), project$db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + us <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_usersoil") + expect_gt(us$n, 0L, label = "global_usersoil populated from ravn_usersoil.csv") +}) diff --git a/rQSWATPlus/tests/testthat/test-lookup.R b/rQSWATPlus/tests/testthat/test-lookup.R new file mode 100644 index 0000000..b2bba64 --- /dev/null +++ b/rQSWATPlus/tests/testthat/test-lookup.R @@ -0,0 +1,117 @@ +test_that("qswat_read_landuse_lookup reads CSV correctly", { + lu_file <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + skip_if(lu_file == "", message = "Example data not available") + + lu <- qswat_read_landuse_lookup(lu_file) + + expect_s3_class(lu, "data.frame") + expect_true("value" %in% names(lu)) + expect_true("landuse" %in% names(lu)) + expect_true(nrow(lu) > 0) + expect_true(is.integer(lu$value)) + expect_true(is.character(lu$landuse)) + # Check some expected values + expect_true("PAST" %in% lu$landuse) +}) + +test_that("qswat_read_soil_lookup reads CSV correctly", { + soil_file <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + skip_if(soil_file == "", message = "Example data not available") + + soil <- qswat_read_soil_lookup(soil_file) + + expect_s3_class(soil, "data.frame") + expect_true("value" %in% names(soil)) + expect_true("soil" %in% names(soil)) + expect_true(nrow(soil) > 0) + expect_true(is.integer(soil$value)) + expect_true(is.character(soil$soil)) +}) + +test_that("qswat_read_landuse_lookup validates missing file", { + expect_error( + qswat_read_landuse_lookup("nonexistent.csv"), + "not found" + ) +}) + +test_that("qswat_read_soil_lookup validates missing file", { + expect_error( + qswat_read_soil_lookup("nonexistent.csv"), + "not found" + ) +}) + +test_that("qswat_create_slope_classes creates valid classes", { + # Single class + sc <- qswat_create_slope_classes(c(0, 9999)) + expect_equal(nrow(sc), 1) + expect_equal(sc$min_slope, 0) + expect_equal(sc$class_id, 1) + + # Multiple classes + sc <- qswat_create_slope_classes(c(0, 5, 15, 9999)) + expect_equal(nrow(sc), 3) + expect_equal(sc$class_id, c(1, 2, 3)) + expect_equal(sc$min_slope, c(0, 5, 15)) + expect_equal(sc$max_slope, c(5, 15, 9999)) + + # Custom labels + sc <- qswat_create_slope_classes(c(0, 10, 9999), + labels = c("gentle", "steep")) + expect_equal(sc$label, c("gentle", "steep")) +}) + +test_that("qswat_create_slope_classes validates input", { + expect_error(qswat_create_slope_classes(c(5)), "At least two") + expect_error(qswat_create_slope_classes(c(10, 5)), "strictly increasing") + expect_error(qswat_create_slope_classes(c(0, 9999), labels = c("a", "b")), + "Number of labels") +}) + +test_that("qswat_read_usersoil reads valid CSV and normalises column names", { + csv_file <- tempfile(fileext = ".csv") + on.exit(unlink(csv_file), add = TRUE) + + utils::write.csv(data.frame( + snam = c("MySoil1", "MySoil2"), + nlayers = c(2L, 3L), + hydgrp = c("B", "C"), + sol_zmx = c(1000, 1500) + ), csv_file, row.names = FALSE) + + result <- qswat_read_usersoil(csv_file) + + expect_s3_class(result, "data.frame") + expect_equal(nrow(result), 2L) + # Column names should be uppercase + expect_true("SNAM" %in% names(result)) + expect_true("NLAYERS" %in% names(result)) + expect_true("HYDGRP" %in% names(result)) + expect_equal(result$SNAM, c("MySoil1", "MySoil2")) +}) + +test_that("qswat_read_usersoil errors on missing file", { + expect_error(qswat_read_usersoil("nonexistent_soils.csv"), "not found") +}) + +test_that("qswat_read_usersoil errors when SNAM column is absent", { + csv_file <- tempfile(fileext = ".csv") + on.exit(unlink(csv_file), add = TRUE) + utils::write.csv(data.frame(SoilName = "X", SOL_Z1 = 300), csv_file, + row.names = FALSE) + expect_error(qswat_read_usersoil(csv_file), "SNAM") +}) + +test_that("qswat_read_usersoil drops rows with empty SNAM", { + csv_file <- tempfile(fileext = ".csv") + on.exit(unlink(csv_file), add = TRUE) + utils::write.csv(data.frame( + SNAM = c("SoilA", "", NA, "SoilB"), + NLAYERS = c(2, 1, 1, 3) + ), csv_file, row.names = FALSE) + + result <- qswat_read_usersoil(csv_file) + expect_equal(nrow(result), 2L) + expect_equal(result$SNAM, c("SoilA", "SoilB")) +}) diff --git a/rQSWATPlus/tests/testthat/test-setup.R b/rQSWATPlus/tests/testthat/test-setup.R new file mode 100644 index 0000000..42836ab --- /dev/null +++ b/rQSWATPlus/tests/testthat/test-setup.R @@ -0,0 +1,130 @@ +test_that("qswat_setup creates project structure", { + # Get example data paths + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + usersoil <- system.file("extdata", "ravn_usersoil.csv", package = "rQSWATPlus") + + skip_if(dem == "", message = "Example data not available") + + proj_dir <- file.path(tempdir(), "test_setup") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_setup( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup, + usersoil = usersoil + ) + + expect_s3_class(project, "qswat_project") + expect_true(dir.exists(proj_dir)) + expect_true(dir.exists(file.path(proj_dir, "Source"))) + expect_true(dir.exists(file.path(proj_dir, "Watershed"))) + expect_true(dir.exists(file.path(proj_dir, "Watershed", "Rasters"))) + expect_true(dir.exists(file.path(proj_dir, "Watershed", "Shapes"))) + expect_true(dir.exists(file.path(proj_dir, "Watershed", "Text"))) + expect_true(file.exists(project$dem_file)) + expect_true(file.exists(project$landuse_file)) + expect_true(file.exists(project$soil_file)) + expect_true(project$nrow > 0) + expect_true(project$ncol > 0) +}) + +test_that("qswat_setup validates missing files", { + expect_error( + qswat_setup( + project_dir = tempdir(), + dem_file = "nonexistent.tif", + landuse_file = "nonexistent.tif", + soil_file = "nonexistent.tif", + landuse_lookup = "nonexistent.csv", + soil_lookup = "nonexistent.csv" + ), + "File not found" + ) +}) + +test_that("qswat_setup with outlet file", { + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") + + skip_if(dem == "", message = "Example data not available") + skip_if(outlet == "", message = "Outlet data not available") + + proj_dir <- file.path(tempdir(), "test_setup_outlet") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_setup( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup, + outlet_file = outlet + ) + + expect_s3_class(project, "qswat_project") + expect_false(is.null(project$outlet_file)) + expect_true(file.exists(project$outlet_file)) +}) + +test_that("print.qswat_project works", { + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + + skip_if(dem == "", message = "Example data not available") + + proj_dir <- file.path(tempdir(), "test_print") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_setup( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup + ) + + output <- capture.output(print(project)) + expect_true(any(grepl("QSWATPlus Project", output))) + expect_true(any(grepl("DEM:", output))) +}) + +test_that("qswat_setup stores usersoil in project object", { + proj_dir <- file.path(tempdir(), "test_setup_usersoil") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + # usersoil = NULL (default) + p_null <- qswat_setup(project_dir = proj_dir) + expect_null(p_null$usersoil) + + # usersoil = "FAO_usersoil" + p_fao <- qswat_setup(project_dir = proj_dir, overwrite = TRUE, + usersoil = "FAO_usersoil") + expect_equal(p_fao$usersoil, "FAO_usersoil") + + # usersoil = "global_usersoil" + p_global <- qswat_setup(project_dir = proj_dir, overwrite = TRUE, + usersoil = "global_usersoil") + expect_equal(p_global$usersoil, "global_usersoil") + + # usersoil = arbitrary CSV path string (stored as-is, validated later) + p_csv <- qswat_setup(project_dir = proj_dir, overwrite = TRUE, + usersoil = "/path/to/my_soils.csv") + expect_equal(p_csv$usersoil, "/path/to/my_soils.csv") +}) diff --git a/rQSWATPlus/tests/testthat/test-validate-database.R b/rQSWATPlus/tests/testthat/test-validate-database.R new file mode 100644 index 0000000..a2d24de --- /dev/null +++ b/rQSWATPlus/tests/testthat/test-validate-database.R @@ -0,0 +1,546 @@ +test_that("qswat_check_database validates missing file", { + expect_error( + qswat_check_database("/nonexistent/path.sqlite"), + "not found" + ) +}) + +test_that("qswat_check_database detects missing tables", { + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + # Only create gis_subbasins - missing others + DBI::dbExecute(con, "CREATE TABLE gis_subbasins (id INTEGER PRIMARY KEY)") + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + expect_false(result$passed) + expect_s3_class(result, "qswat_db_check") + expect_true(any(grepl("missing", result$errors))) +}) + +test_that("qswat_check_database detects empty tables", { + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + # Create all required tables but leave them empty + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + rQSWATPlus:::.create_db_tables(con) + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + expect_false(result$passed) + expect_true(any(grepl("empty", result$errors))) +}) + +test_that("qswat_check_database passes on valid database", { + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + # Build a valid project and write the database + project <- structure(list( + project_dir = tempdir(), + hru_data = data.frame( + hru_id = 1:4, + subbasin = c(1L, 1L, 2L, 2L), + landuse = c("AGRL", "FRSD", "PAST", "AGRL"), + soil = c("TX047", "TX047", "TX236", "TX236"), + slope_class = c(1L, 1L, 2L, 2L), + cell_count = c(100L, 50L, 200L, 150L), + area_ha = c(10.0, 5.0, 20.0, 15.0), + mean_elevation = c(500, 510, 600, 610), + mean_slope = c(3.0, 4.0, 8.0, 9.0), + stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), + area_ha = c(15.0, 35.0), + mean_elevation = c(505, 605), + min_elevation = c(490, 580), + max_elevation = c(520, 640), + mean_slope = c(3.5, 8.5), + n_hrus = c(2L, 2L), + n_landuses = c(2L, 2L), + n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(c(0, 5, 15, 9999)), + stream_topology = data.frame( + LINKNO = c(1L, 2L), + DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), + strmOrder = c(2L, 1L), + Length = c(1000, 500), + stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + + result <- qswat_check_database(db_file, verbose = FALSE) + expect_true(result$passed) + expect_equal(sum(result$checks$status == "FAIL"), 0) +}) + +test_that("qswat_check_database detects referential integrity issues", { + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + rQSWATPlus:::.create_db_tables(con) + + # Write a subbasin + DBI::dbExecute(con, "INSERT INTO gis_subbasins VALUES (1,100,5,100,5,0,0,500,400,600,0)") + # Write an HRU referencing non-existent subbasin 99 + DBI::dbExecute(con, "INSERT INTO gis_hrus VALUES (1,99,'AGRL','TX047',1,10,3,0,0,500,50,50,50,100)") + # Write a channel for subbasin 1 + DBI::dbExecute(con, "INSERT INTO gis_channels VALUES (1,1,1,1,1000,0.01,1.0,0.5,0,0,0,0)") + # Write an LSU + DBI::dbExecute(con, "INSERT INTO gis_lsus VALUES (1,0,1,1,100,5,100,5,1.0,0.5,0,0,500)") + # Write routing with outlet + DBI::dbExecute(con, "INSERT INTO gis_routing VALUES (1,'sub','tot',0,'outlet',100)") + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + expect_false(result$passed) + expect_true(any(grepl("non-existent LSU", result$errors))) +}) + +test_that("qswat_check_database detects routing loops", { + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + rQSWATPlus:::.create_db_tables(con) + + # Write subbasins + DBI::dbExecute(con, "INSERT INTO gis_subbasins VALUES (1,100,5,100,5,0,0,500,400,600,0)") + DBI::dbExecute(con, "INSERT INTO gis_subbasins VALUES (2,100,5,100,5,0,0,500,400,600,0)") + # HRUs + DBI::dbExecute(con, "INSERT INTO gis_hrus VALUES (1,1,'AGRL','TX047',1,10,3,0,0,500,50,50,50,100)") + DBI::dbExecute(con, "INSERT INTO gis_hrus VALUES (2,2,'AGRL','TX047',1,10,3,0,0,500,50,50,50,100)") + # Channels + DBI::dbExecute(con, "INSERT INTO gis_channels VALUES (1,1,1,1,1000,0.01,1.0,0.5,0,0,0,0)") + DBI::dbExecute(con, "INSERT INTO gis_channels VALUES (2,2,1,1,1000,0.01,1.0,0.5,0,0,0,0)") + # LSUs + DBI::dbExecute(con, "INSERT INTO gis_lsus VALUES (1,0,1,1,100,5,100,5,1.0,0.5,0,0,500)") + DBI::dbExecute(con, "INSERT INTO gis_lsus VALUES (2,0,2,2,100,5,100,5,1.0,0.5,0,0,500)") + # Create routing loop: 1→2→1 + DBI::dbExecute(con, "INSERT INTO gis_routing VALUES (1,'sub','tot',2,'sub',100)") + DBI::dbExecute(con, "INSERT INTO gis_routing VALUES (2,'sub','tot',1,'sub',100)") + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + expect_false(result$passed) + expect_true(any(grepl("[Ll]oop", result$errors))) +}) + +test_that("qswat_check_database detects missing outlet", { + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + rQSWATPlus:::.create_db_tables(con) + + DBI::dbExecute(con, "INSERT INTO gis_subbasins VALUES (1,100,5,100,5,0,0,500,400,600,0)") + DBI::dbExecute(con, "INSERT INTO gis_hrus VALUES (1,1,'AGRL','TX047',1,10,3,0,0,500,50,50,50,100)") + DBI::dbExecute(con, "INSERT INTO gis_channels VALUES (1,1,1,1,1000,0.01,1.0,0.5,0,0,0,0)") + DBI::dbExecute(con, "INSERT INTO gis_lsus VALUES (1,0,1,1,100,5,100,5,1.0,0.5,0,0,500)") + # Routing that doesn't go to outlet - just sub→sub + DBI::dbExecute(con, "INSERT INTO gis_routing VALUES (1,'sub','tot',2,'sub',100)") + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + expect_false(result$passed) + expect_true(any(grepl("[Oo]utlet", result$errors))) +}) + +test_that("qswat_check_database detects bad routing percentages", { + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + rQSWATPlus:::.create_db_tables(con) + + DBI::dbExecute(con, "INSERT INTO gis_subbasins VALUES (1,100,5,100,5,0,0,500,400,600,0)") + DBI::dbExecute(con, "INSERT INTO gis_hrus VALUES (1,1,'AGRL','TX047',1,10,3,0,0,500,50,50,50,100)") + DBI::dbExecute(con, "INSERT INTO gis_channels VALUES (1,1,1,1,1000,0.01,1.0,0.5,0,0,0,0)") + DBI::dbExecute(con, "INSERT INTO gis_lsus VALUES (1,0,1,1,100,5,100,5,1.0,0.5,0,0,500)") + # Routing with bad percentage (50 instead of 100) + DBI::dbExecute(con, "INSERT INTO gis_routing VALUES (1,'sub','tot',0,'outlet',50)") + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + expect_false(result$passed) + expect_true(any(grepl("100%", result$errors))) +}) + +test_that("print.qswat_db_check works", { + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + # Valid database + project <- structure(list( + project_dir = tempdir(), + hru_data = data.frame( + hru_id = 1:2, + subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), + soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), + cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), + stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), + area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), + min_elevation = c(490, 580), + max_elevation = c(510, 620), + mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), + n_landuses = c(1L, 1L), + n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = c(1L, 2L), + DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), + strmOrder = c(2L, 1L), + Length = c(1000, 500), + stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + result <- qswat_check_database(db_file, verbose = FALSE) + + output <- capture.output(print(result)) + expect_true(any(grepl("Compatibility Check", output))) + expect_true(any(grepl("compatible", output))) +}) + +test_that("qswat_check_database warns about missing HAWQS and reference tables", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + # Build a valid project database (no HAWQS extras) + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + project <- structure(list( + project_dir = tempdir(), + hru_data = data.frame( + hru_id = 1:2, subbasin = c(1L, 2L), + landuse = c("AGRL", "FRSD"), soil = c("TX047", "TX236"), + slope_class = c(1L, 1L), cell_count = c(100L, 200L), + area_ha = c(10.0, 20.0), mean_elevation = c(500, 600), + mean_slope = c(3.0, 8.0), stringsAsFactors = FALSE + ), + basin_data = data.frame( + subbasin = c(1L, 2L), area_ha = c(10.0, 20.0), + mean_elevation = c(500, 600), min_elevation = c(490, 580), + max_elevation = c(510, 620), mean_slope = c(3.0, 8.0), + n_hrus = c(1L, 1L), n_landuses = c(1L, 1L), n_soils = c(1L, 1L), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(), + stream_topology = data.frame( + LINKNO = c(1L, 2L), DSLINKNO = c(-1L, 1L), + WSNO = c(1L, 2L), strmOrder = c(2L, 1L), + Length = c(1000, 500), stringsAsFactors = FALSE + ) + ), class = "qswat_project") + + qswat_write_database(project, db_file = db_file, overwrite = TRUE) + result <- qswat_check_database(db_file, verbose = FALSE) + + # Core checks should pass (no fatal errors) + expect_true(result$passed, + info = paste("Errors:", paste(result$errors, collapse = "; "))) + + # With the bundled HAWQS databases present, HAWQS tables should now + # be populated by populate_from_datasets(), so they should NOT appear + # as missing in warnings. + hawqs_missing_warns <- result$warnings[ + grepl("HAWQS-specific", result$warnings)] + + # If the bundled databases are installed (package installed), no + # HAWQS warning; otherwise one consolidated warning is acceptable. + expect_true(length(hawqs_missing_warns) <= 1, + info = paste("Unexpected HAWQS warnings:", + paste(hawqs_missing_warns, collapse = "; "))) +}) + +test_that("qswat_check_database warns when reference table exists but is empty", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + rQSWATPlus:::.create_db_tables(con) + + # Add minimal data to pass required-table checks + DBI::dbExecute(con, "INSERT INTO gis_subbasins VALUES (1,100,5,100,5,0,0,500,400,600,0)") + DBI::dbExecute(con, "INSERT INTO gis_hrus VALUES (1,1,'AGRL','TX047',1,10,3,0,0,500,50,50,50,100)") + DBI::dbExecute(con, "INSERT INTO gis_channels VALUES (1,1,1,1,1000,0.01,1.0,0.5,0,0,0,0)") + DBI::dbExecute(con, "INSERT INTO gis_lsus VALUES (1,0,1,1,100,5,100,5,1.0,0.5,0,0,500)") + DBI::dbExecute(con, "INSERT INTO gis_routing VALUES (1,'sub','tot',0,'outlet',100)") + DBI::dbExecute(con, "INSERT INTO project_config (id, project_name, delineation_done, hrus_done, use_gwflow) VALUES (1,'test',1,1,0)") + + # Add an empty plants_plt table (exists but has no data) + tryCatch(DBI::dbExecute(con, "DROP TABLE IF EXISTS plants_plt"), error = function(e) NULL) + DBI::dbExecute(con, "CREATE TABLE plants_plt (id INTEGER PRIMARY KEY, name TEXT)") + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + + # Should warn about the empty reference table + ref_warns <- result$warnings[grepl("plants_plt.*no data", result$warnings)] + expect_true(length(ref_warns) >= 1, + info = paste("Expected warning about empty plants_plt.", + "Got warnings:", paste(result$warnings, collapse = "; "))) +}) + +# ---- gwflow validation tests ---- + +# Helper to build a minimal valid database with optional gwflow setup +.make_minimal_db <- function(db_file, use_gwflow = 0L) { + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + rQSWATPlus:::.create_db_tables(con) + DBI::dbExecute(con, "INSERT INTO gis_subbasins VALUES (1,100,5,100,5,0,0,500,400,600,0)") + DBI::dbExecute(con, "INSERT INTO gis_hrus VALUES (1,1,'AGRL','TX047',1,10,3,0,0,500,50,50,50,100)") + DBI::dbExecute(con, "INSERT INTO gis_channels VALUES (1,1,1,1,1000,0.01,1.0,0.5,0,0,0,0)") + DBI::dbExecute(con, "INSERT INTO gis_lsus VALUES (1,0,1,1,100,5,100,5,1.0,0.5,0,0,500)") + DBI::dbExecute(con, "INSERT INTO gis_routing VALUES (1,'sub','tot',0,'outlet',100)") + DBI::dbExecute(con, + paste0("INSERT INTO project_config (id, project_name, delineation_done, ", + "hrus_done, use_gwflow) VALUES (1,'test',1,1,", use_gwflow, ")")) + con +} + +test_that("qswat_check_database skips gwflow checks when use_gwflow=0", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- .make_minimal_db(db_file, use_gwflow = 0L) + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + + # No gwflow checks should be present + gwflow_checks <- result$checks[grepl("^gwflow:", result$checks$check), ] + expect_equal(nrow(gwflow_checks), 0, + label = "no gwflow check rows when use_gwflow=0") + expect_true(result$passed, + info = paste("Errors:", paste(result$errors, collapse = "; "))) +}) + +test_that("qswat_check_database fails when gwflow enabled but tables missing", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + # Enable gwflow but do NOT create the gwflow tables + con <- .make_minimal_db(db_file, use_gwflow = 1L) + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + + expect_false(result$passed) + expect_true(any(grepl("gwflow is enabled but", result$errors)), + info = paste("Expected missing-tables error. Got:", + paste(result$errors, collapse = "; "))) +}) + +test_that("qswat_check_database fails when gwflow core tables are empty", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + # Create all gwflow tables but leave them empty + con <- .make_minimal_db(db_file, use_gwflow = 1L) + rQSWATPlus:::.create_gwflow_tables(con) + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + + expect_false(result$passed) + gwflow_fails <- result$errors[grepl("^gwflow", result$checks$check[ + result$checks$status == "FAIL"])] + # At minimum gwflow_base, gwflow_zone, gwflow_grid, gwflow_solutes, + # gwflow_rivcell should all fail the non-empty check + fail_checks <- result$checks$check[result$checks$status == "FAIL"] + expect_true(any(grepl("gwflow:", fail_checks)), + info = paste("Expected gwflow check failures. Failing checks:", + paste(fail_checks, collapse = ", "))) +}) + +test_that("qswat_check_database passes gwflow checks with fully populated tables (LSU recharge)", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- .make_minimal_db(db_file, use_gwflow = 1L) + rQSWATPlus:::.create_gwflow_tables(con) + + # Populate gwflow_zone (1 zone) + DBI::dbExecute(con, + "INSERT INTO gwflow_zone VALUES (1, 10.0, 0.2, 0.005, 0.5)") + + # Populate gwflow_grid (1 active cell) + DBI::dbExecute(con, + "INSERT INTO gwflow_grid VALUES (1, 1, 1, 500.0, 20.0, 1.0, 495.0, 0)") + + # Populate gwflow_base (recharge=2 → LSU recharge). + # Column order: cell_size, row_count, col_count, boundary_conditions, + # recharge, soil_transfer, saturation_excess, external_pumping, + # tile_drainage, reservoir_exchange, wetland_exchange, floodplain_exchange, + # canal_seepage, solute_transport, transport_steps, disp_coef, + # recharge_delay, et_extinction_depth, water_table_depth, river_depth, + # tile_depth, tile_area, tile_k, tile_groups, resbed_thickness, resbed_k, + # wet_thickness, daily_output, annual_output, aa_output, + # daily_output_row, daily_output_col, timestep_balance + DBI::dbExecute(con, paste0( + "INSERT INTO gwflow_base VALUES (", + "200,1,1,1,2,1,1,0,0,1,1,1,0,1,1,5.0,0,1.0,5.0,5.0,1.22,50.0,5.0,", + "0,2.0,9.99e-6,0.25,1,1,1,0,0,1.0)")) + + # Populate gwflow_solutes (10 default solutes) + solutes <- list( + c("no3-n", 1, -0.0001, 3, "single", 3.0), + c("p", 1, 0, 0.05, "single", 0.05), + c("so4", 1, 0, 0, "single", 100), + c("ca", 1, 0, 0, "single", 50), + c("mg", 1, 0, 0, "single", 30), + c("na", 1, 0, 0, "single", 40), + c("k", 1, 0, 0, "single", 1), + c("cl", 1, 0, 0, "single", 25), + c("co3", 1, 0, 0, "single", 1), + c("hco3", 1, 0, 0, "single", 80) + ) + for (s in solutes) { + DBI::dbExecute(con, sprintf( + "INSERT INTO gwflow_solutes VALUES ('%s',%s,%s,%s,'%s',%s)", + s[[1]], s[[2]], s[[3]], s[[4]], s[[5]], s[[6]])) + } + + # Populate gwflow_rivcell (1 river-cell connection) + DBI::dbExecute(con, "INSERT INTO gwflow_rivcell VALUES (1, 1, 150.0)") + + # Populate gwflow_lsucell (LSU recharge mode) + DBI::dbExecute(con, "INSERT INTO gwflow_lsucell VALUES (1, 1, 40000.0)") + + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + + gwflow_fail_checks <- result$checks$check[ + grepl("^gwflow:", result$checks$check) & + result$checks$status == "FAIL"] + expect_equal(length(gwflow_fail_checks), 0L, + label = paste("gwflow checks should all pass. Failed:", + paste(gwflow_fail_checks, collapse = ", "))) +}) + +test_that("qswat_check_database fails when gwflow_lsucell empty in LSU recharge mode", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- .make_minimal_db(db_file, use_gwflow = 1L) + rQSWATPlus:::.create_gwflow_tables(con) + + DBI::dbExecute(con, + "INSERT INTO gwflow_zone VALUES (1, 10.0, 0.2, 0.005, 0.5)") + DBI::dbExecute(con, + "INSERT INTO gwflow_grid VALUES (1, 1, 1, 500.0, 20.0, 1.0, 495.0, 0)") + # recharge=2 → LSU recharge (column 5 of gwflow_base) + DBI::dbExecute(con, paste0( + "INSERT INTO gwflow_base VALUES (", + "200,1,1,1,2,1,1,0,0,1,1,1,0,1,1,5.0,0,1.0,5.0,5.0,1.22,50.0,5.0,", + "0,2.0,9.99e-6,0.25,1,1,1,0,0,1.0)")) + for (s in list( + c("no3-n","single"), c("p","single"), c("so4","single"), + c("ca","single"), c("mg","single"), c("na","single"), + c("k","single"), c("cl","single"), c("co3","single"), + c("hco3","single") + )) { + DBI::dbExecute(con, sprintf( + "INSERT INTO gwflow_solutes VALUES ('%s',1,0,0,'%s',1)", + s[[1]], s[[2]])) + } + DBI::dbExecute(con, "INSERT INTO gwflow_rivcell VALUES (1, 1, 150.0)") + # gwflow_lsucell intentionally left empty + + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + + expect_false(result$passed) + expect_true(any(grepl("gwflow_lsucell is empty", result$errors)), + info = paste("Expected lsucell error. Got:", + paste(result$errors, collapse = "; "))) +}) + +test_that("qswat_check_database fails when gwflow_hrucell empty in HRU recharge mode", { + skip_if_not_installed("RSQLite") + skip_if_not_installed("DBI") + + db_file <- tempfile(fileext = ".sqlite") + on.exit(unlink(db_file), add = TRUE) + + con <- .make_minimal_db(db_file, use_gwflow = 1L) + rQSWATPlus:::.create_gwflow_tables(con) + + DBI::dbExecute(con, + "INSERT INTO gwflow_zone VALUES (1, 10.0, 0.2, 0.005, 0.5)") + DBI::dbExecute(con, + "INSERT INTO gwflow_grid VALUES (1, 1, 1, 500.0, 20.0, 1.0, 495.0, 0)") + # recharge=1 → HRU recharge (column 5 of gwflow_base) + DBI::dbExecute(con, paste0( + "INSERT INTO gwflow_base VALUES (", + "200,1,1,1,1,1,1,0,0,1,1,1,0,1,1,5.0,0,1.0,5.0,5.0,1.22,50.0,5.0,", + "0,2.0,9.99e-6,0.25,1,1,1,0,0,1.0)")) + for (s in list( + c("no3-n","single"), c("p","single"), c("so4","single"), + c("ca","single"), c("mg","single"), c("na","single"), + c("k","single"), c("cl","single"), c("co3","single"), + c("hco3","single") + )) { + DBI::dbExecute(con, sprintf( + "INSERT INTO gwflow_solutes VALUES ('%s',1,0,0,'%s',1)", + s[[1]], s[[2]])) + } + DBI::dbExecute(con, "INSERT INTO gwflow_rivcell VALUES (1, 1, 150.0)") + # gwflow_hrucell intentionally left empty (gwflow_lsucell also empty - not + # required for recharge=1) + + DBI::dbDisconnect(con) + + result <- qswat_check_database(db_file, verbose = FALSE) + + expect_false(result$passed) + expect_true(any(grepl("gwflow_hrucell is empty", result$errors)), + info = paste("Expected hrucell error. Got:", + paste(result$errors, collapse = "; "))) +}) diff --git a/rQSWATPlus/tests/testthat/test-visualization.R b/rQSWATPlus/tests/testthat/test-visualization.R new file mode 100644 index 0000000..4afaae8 --- /dev/null +++ b/rQSWATPlus/tests/testthat/test-visualization.R @@ -0,0 +1,154 @@ +test_that("qswat_plot_landuse_summary works with mock data", { + skip_if_not_installed("ggplot2") + + project <- structure(list( + hru_data = data.frame( + hru_id = 1:6, + subbasin = c(1L, 1L, 1L, 2L, 2L, 2L), + landuse = c("AGRL", "FRSD", "PAST", "AGRL", "PAST", "WATR"), + soil = c("TX047", "TX047", "TX236", "TX047", "TX236", "TX236"), + slope_class = c(1L, 1L, 1L, 2L, 2L, 2L), + cell_count = c(100L, 80L, 50L, 120L, 90L, 30L), + area_ha = c(10.0, 8.0, 5.0, 12.0, 9.0, 3.0), + mean_elevation = c(500, 510, 520, 600, 610, 590), + mean_slope = c(3.0, 4.0, 5.0, 8.0, 9.0, 7.0), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(c(0, 5, 15, 9999)) + ), class = "qswat_project") + + # Test percent plot + p <- qswat_plot_landuse_summary(project, type = "percent") + expect_s3_class(p, "ggplot") + + # Test area plot + p2 <- qswat_plot_landuse_summary(project, type = "area") + expect_s3_class(p2, "ggplot") +}) + +test_that("qswat_plot_soil_summary works with mock data", { + skip_if_not_installed("ggplot2") + + project <- structure(list( + hru_data = data.frame( + hru_id = 1:4, + subbasin = c(1L, 1L, 2L, 2L), + landuse = c("AGRL", "FRSD", "PAST", "AGRL"), + soil = c("TX047", "TX236", "TX047", "TX619"), + slope_class = c(1L, 1L, 2L, 2L), + cell_count = c(100L, 50L, 200L, 150L), + area_ha = c(10.0, 5.0, 20.0, 15.0), + mean_elevation = c(500, 510, 600, 610), + mean_slope = c(3.0, 4.0, 8.0, 9.0), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(c(0, 5, 15, 9999)) + ), class = "qswat_project") + + p <- qswat_plot_soil_summary(project) + expect_s3_class(p, "ggplot") +}) + +test_that("qswat_plot_hru_summary works with mock data", { + skip_if_not_installed("ggplot2") + + project <- structure(list( + hru_data = data.frame( + hru_id = 1:6, + subbasin = c(1L, 1L, 1L, 2L, 2L, 2L), + landuse = c("AGRL", "FRSD", "PAST", "AGRL", "PAST", "WATR"), + soil = c("TX047", "TX047", "TX236", "TX047", "TX236", "TX236"), + slope_class = c(1L, 1L, 2L, 1L, 2L, 2L), + cell_count = c(100L, 80L, 50L, 120L, 90L, 30L), + area_ha = c(10.0, 8.0, 5.0, 12.0, 9.0, 3.0), + mean_elevation = c(500, 510, 520, 600, 610, 590), + mean_slope = c(3.0, 4.0, 5.0, 8.0, 9.0, 7.0), + stringsAsFactors = FALSE + ), + slope_classes = qswat_create_slope_classes(c(0, 5, 15, 9999)) + ), class = "qswat_project") + + # Faceted summary + p <- qswat_plot_hru_summary(project) + expect_s3_class(p, "ggplot") + + # By-subbasin stacked bar + p2 <- qswat_plot_hru_summary(project, by_subbasin = TRUE) + expect_s3_class(p2, "ggplot") +}) + +test_that("summary plots validate inputs", { + skip_if_not_installed("ggplot2") + + expect_error(qswat_plot_landuse_summary(list()), "qswat_project") + expect_error(qswat_plot_soil_summary(list()), "qswat_project") + expect_error(qswat_plot_hru_summary(list()), "qswat_project") + + empty_proj <- structure( + list(hru_data = NULL), + class = "qswat_project" + ) + expect_error(qswat_plot_landuse_summary(empty_proj), "HRU data") + expect_error(qswat_plot_soil_summary(empty_proj), "HRU data") + expect_error(qswat_plot_hru_summary(empty_proj), "HRU data") +}) + +test_that("tmap plot functions require tmap", { + + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") + + skip_if(dem == "", message = "Example data not available") + skip_if(outlet == "", message = "Outlet data not available") + + proj_dir <- file.path(tempdir(), "test_setup_outlet") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_setup( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup, + outlet_file = outlet + ) + project <- qswat_delineate(project, threshold = 500, quiet = TRUE) + project <- qswat_create_streams(project) + + + if (!requireNamespace("tmap", quietly = TRUE)) { + expect_error(qswat_plot_dem(project), "tmap") + expect_error(qswat_plot_landuse(project), "tmap") + expect_error(qswat_plot_soil(project), "tmap") + } else { + # If tmap IS installed, just check they return tmap objects + p <- qswat_plot_dem(project) + expect_true(inherits(p, "tmap")) + p2 <- qswat_plot_landuse(project) + expect_true(inherits(p2, "tmap")) + p3 <- qswat_plot_soil(project) + expect_true(inherits(p3, "tmap")) + p4 <- qswat_plot_streams(project) + expect_true(inherits(p4, "tmap")) + p5 <- qswat_plot_watershed(project) + expect_true(inherits(p5, "tmap")) + } +}) + +test_that("stream/watershed plots validate state", { + project <- structure(list( + streams_sf = NULL, + watershed_file = NULL, + dem_file = system.file("extdata", "ravn_dem.tif", + package = "rQSWATPlus") + ), class = "qswat_project") + + skip_if_not_installed("tmap") + expect_error(qswat_plot_streams(project), "qswat_create_streams") + expect_error(qswat_plot_watershed(project), "qswat_delineate") +}) diff --git a/rQSWATPlus/tests/testthat/test-workflow.R b/rQSWATPlus/tests/testthat/test-workflow.R new file mode 100644 index 0000000..0fb23b9 --- /dev/null +++ b/rQSWATPlus/tests/testthat/test-workflow.R @@ -0,0 +1,278 @@ +# Helper for TauDEM skip guards used across workflow integration tests +skip_if_no_taudem <- function() { + skip_if_not_installed("traudem") + taudem_ok <- tryCatch({ + traudem::taudem_sitrep() + TRUE + }, error = function(e) FALSE) + skip_if(!taudem_ok, "TauDEM not available") +} + +test_that("stepped workflow works", { + skip_if_no_taudem() + + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") + usersoil <- system.file("extdata", "ravn_usersoil.csv", package = "rQSWATPlus") + + skip_if(dem == "", "Example data not available") + + project_dir <- file.path(tempdir(), "ravn_swatplus") + on.exit(unlink(project_dir, recursive = TRUE), add = TRUE) + + # Initialize the project with usersoil stored in the project object + project <- qswat_setup( + project_dir = project_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup, + outlet_file = outlet, + usersoil = usersoil + ) + + project <- qswat_delineate( + project, + threshold = 500, + channel_threshold = 50, + quiet = TRUE + ) + project <- qswat_create_streams(project) + + # Read lookup tables + lu <- qswat_read_landuse_lookup(lu_lookup) + soil_lkp <- qswat_read_soil_lookup(soil_lookup) + + # Define slope classes: 0-5%, 5-15%, and >15% + slopes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) + + # Create HRUs with area thresholds to filter small units + project <- qswat_create_hrus( + project, + landuse_lookup = lu, + soil_lookup = soil_lkp, + slope_classes = slopes, + landuse_threshold = 5, + soil_threshold = 5, + slope_threshold = 0 + ) + + # usersoil from project$usersoil is used automatically + project <- qswat_write_database(project, overwrite = TRUE) + expect_true(file.exists(project$db_file)) + + # Verify usersoil rows were written + con <- DBI::dbConnect(RSQLite::SQLite(), project$db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + us <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM global_usersoil") + expect_gt(us$n, 0L, label = "global_usersoil populated in stepped workflow") +}) + +test_that("qswat_run works", { + skip_if_no_taudem() + + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") + usersoil <- system.file("extdata", "ravn_usersoil.csv", package = "rQSWATPlus") + + skip_if(dem == "", "Example data not available") + + proj_dir <- file.path(tempdir(), "ravn_quick") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_run( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + landuse_lookup = lu_lookup, + soil_file = soil, + soil_lookup = soil_lookup, + usersoil = usersoil, + outlet_file = outlet, + threshold = 500, + slope_breaks = c(0, 5, 15, 9999), + landuse_threshold = 5, + soil_threshold = 5, + db_file = "swat.db" + ) + + result <- qswat_check_database(db_file = project$db_file, verbose = TRUE) + + expect_true(file.exists(project$db_file)) +}) + +test_that("qswat_run with use_gwflow=TRUE initialises gwflow tables", { + skip_if_no_taudem() + + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") + usersoil <- system.file("extdata", "ravn_usersoil.csv", package = "rQSWATPlus") + + skip_if(dem == "", "Example data not available") + + proj_dir <- file.path(tempdir(), "ravn_gwflow") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + project <- qswat_run( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + landuse_lookup = lu_lookup, + soil_file = soil, + soil_lookup = soil_lookup, + usersoil = usersoil, + outlet_file = outlet, + threshold = 500, + slope_breaks = c(0, 5, 15, 9999), + landuse_threshold = 5, + soil_threshold = 5, + use_gwflow = TRUE, + quiet = TRUE + ) + + expect_true(isTRUE(project$use_gwflow)) + + con <- DBI::dbConnect(RSQLite::SQLite(), project$db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + pc <- DBI::dbGetQuery(con, "SELECT use_gwflow FROM project_config") + expect_equal(pc$use_gwflow, 1L, label = "project_config.use_gwflow = 1") + + tbls <- DBI::dbListTables(con) + gwflow_tables <- c("gwflow_base", "gwflow_zone", "gwflow_grid", + "gwflow_out_days", "gwflow_obs_locs", "gwflow_solutes", + "gwflow_init_conc", "gwflow_hrucell", "gwflow_fpcell", + "gwflow_rivcell", "gwflow_lsucell", "gwflow_rescell") + for (tbl in gwflow_tables) { + expect_true(tbl %in% tbls, label = paste0("table '", tbl, "' exists")) + } + + # Verify that spatial tables are populated (not empty) + base_row <- DBI::dbGetQuery(con, "SELECT row_count, col_count FROM gwflow_base") + expect_gt(base_row$row_count, 0L, + label = "gwflow_base.row_count updated after grid creation") + expect_gt(base_row$col_count, 0L, + label = "gwflow_base.col_count updated after grid creation") + + n_zone <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM gwflow_zone")$n + expect_gt(n_zone, 0L, label = "gwflow_zone is populated") + + n_grid <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM gwflow_grid")$n + expect_gt(n_grid, 0L, label = "gwflow_grid is populated") + + n_riv <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM gwflow_rivcell")$n + expect_gt(n_riv, 0L, label = "gwflow_rivcell is populated") + + n_lsu <- DBI::dbGetQuery(con, "SELECT COUNT(*) AS n FROM gwflow_lsucell")$n + expect_gt(n_lsu, 0L, label = "gwflow_lsucell is populated") +}) + + +# Full end-to-end integration test: build project and verify SWAT+ Editor readiness +test_that("example dataset produces a SWAT+ Editor-ready database", { + skip_if_no_taudem() + + dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") + landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") + soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") + lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") + soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") + outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") + usersoil <- system.file("extdata", "ravn_usersoil.csv", package = "rQSWATPlus") + + skip_if(dem == "", "Example data not available") + + proj_dir <- file.path(tempdir(), "ravn_editor_ready") + on.exit(unlink(proj_dir, recursive = TRUE), add = TRUE) + + # ---- Run the full workflow ------------------------------------------------ + project <- qswat_run( + project_dir = proj_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + soil_lookup = soil_lookup, + landuse_lookup = lu_lookup, + usersoil = usersoil, + outlet_file = outlet, + threshold = 500, + slope_breaks = c(0, 5, 15, 9999), + landuse_threshold = 5, + soil_threshold = 5, + quiet = TRUE + ) + + db_file <- project$db_file + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + DBI::dbListTables(con) + DBI::dbDisconnect(con) + expect_true(file.exists(db_file)) + + # ---- Reference database was copied to the project folder ----------------- + ref_db <- file.path(proj_dir, "swatplus_datasets.sqlite") + expect_true( + file.exists(ref_db), + label = "swatplus_datasets.sqlite exists in project folder" + ) + + # ---- project_config is correctly populated -------------------------------- + con <- DBI::dbConnect(RSQLite::SQLite(), db_file) + on.exit(DBI::dbDisconnect(con), add = TRUE) + + pc <- DBI::dbGetQuery(con, "SELECT * FROM project_config") + expect_equal(nrow(pc), 1L, label = "project_config has exactly one row") + expect_equal( + pc$reference_db, + normalizePath(ref_db, mustWork = FALSE), + label = "reference_db points to the copied dataset" + ) + expect_equal(pc$delineation_done, 1L) + expect_equal(pc$hrus_done, 1L) + expect_equal(pc$imported_gis, 1L) + expect_equal(pc$use_gwflow, 0L, label = "gwflow disabled by default") + + # ---- Core GIS tables are populated ---------------------------------------- + subs <- DBI::dbGetQuery(con, "SELECT * FROM gis_subbasins") + expect_gt(nrow(subs), 0L, label = "gis_subbasins has rows") + + hrus <- DBI::dbGetQuery(con, "SELECT * FROM gis_hrus") + expect_gt(nrow(hrus), 0L, label = "gis_hrus has rows") + + chans <- DBI::dbGetQuery(con, "SELECT * FROM gis_channels") + expect_gt(nrow(chans), 0L, label = "gis_channels has rows") + + lsus <- DBI::dbGetQuery(con, "SELECT * FROM gis_lsus") + expect_gt(nrow(lsus), 0L, label = "gis_lsus has rows") + + routing <- DBI::dbGetQuery(con, "SELECT * FROM gis_routing") + expect_gt(nrow(routing), 0L, label = "gis_routing has rows") + + # ---- SWAT+ Editor compatibility check passes ------------------------------ + result <- qswat_check_database(db_file, verbose = TRUE) + expect_true( + result$passed, + label = paste( + "Database passes SWAT+ Editor compatibility check.", + if (!result$passed) paste("Errors:", paste(result$errors, collapse = "; ")) else "" + ) + ) + expect_equal( + sum(result$checks$status == "FAIL"), 0L, + label = "No failed checks" + ) +}) + + diff --git a/rQSWATPlus/vignettes/data-requirements.Rmd b/rQSWATPlus/vignettes/data-requirements.Rmd new file mode 100644 index 0000000..d8789ad --- /dev/null +++ b/rQSWATPlus/vignettes/data-requirements.Rmd @@ -0,0 +1,276 @@ +--- +title: "Data Requirements for rQSWATPlus" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Data Requirements for rQSWATPlus} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = FALSE +) +``` + +## Overview + +Setting up a SWAT+ model with `rQSWATPlus` requires several spatial datasets +and lookup tables. This vignette describes each input, its required format, and +best practices for data preparation. + +## Required Raster Data + +### Digital Elevation Model (DEM) + +The DEM is the foundation for watershed delineation. It provides elevation +values used to calculate flow directions, slopes, and drainage patterns. + +**Format Requirements:** +- **File format**: GeoTIFF (`.tif`) recommended; any GDAL-supported raster format +- **Data type**: Numeric (integer or float) +- **CRS**: Projected coordinate system (e.g., UTM) strongly recommended +- **Units**: Elevation in meters preferred +- **Resolution**: Depends on watershed size; 30m (SRTM), 10m (NED), or + finer for small watersheds +- **Coverage**: Must cover the entire study watershed plus some buffer + +```{r} +library(terra) +dem <- rast("path/to/dem.tif") +dem +#> class : SpatRaster +#> dimensions : 299, 209, 1 (nrow, ncol, nlyr) +#> resolution : 30, 30 (x, y) +#> extent : 567000, 573270, 6205000, 6213970 (xmin, xmax, ymin, ymax) +#> coord. ref. : ETRS89 / UTM zone 32N (EPSG:25832) +#> source : dem.tif +#> name : dem +``` + +**Sources:** +- [SRTM](https://earthexplorer.usgs.gov/) - 30m global +- [Copernicus DEM](https://spacedata.copernicus.eu/) - 30m global +- [USGS NED](https://apps.nationalmap.gov/) - 10m US +- National elevation services + +**Tips:** +- Ensure the DEM covers the area beyond your watershed boundary (TauDEM needs + context for flow direction calculation) +- Fill any artifacts or voids before use (though TauDEM's pit removal handles + small depressions) +- Projected CRS is required for accurate area/slope calculations +- Use consistent units (meters) for both horizontal and vertical + +### Land Use Raster + +The land use raster classifies land cover types across your study area. + +**Format Requirements:** +- **File format**: GeoTIFF recommended +- **Data type**: Integer (categorical values) +- **CRS**: Same as DEM (or will be reprojected) +- **Coverage**: Must cover the watershed +- **Values**: Integer codes that map to SWAT+ land use types via lookup table + +```{r} +landuse <- rast("path/to/landuse.tif") +freq(landuse) +#> layer value count +#> 1 1 0 15234 +#> 2 1 1100 567 +#> 3 1 1120 123 +#> ... +``` + +**Sources:** +- [CORINE Land Cover](https://land.copernicus.eu/) - Europe +- [NLCD](https://www.mrlc.gov/) - US +- [ESA WorldCover](https://esa-worldcover.org/) - 10m global +- National land use databases + +### Soil Raster + +The soil raster identifies soil types/map units across the study area. + +**Format Requirements:** +- **File format**: GeoTIFF recommended +- **Data type**: Integer (soil map unit IDs) +- **CRS**: Same as DEM (or will be reprojected) +- **Values**: Integer codes mapping to soil names via lookup table + +```{r} +soil <- rast("path/to/soil.tif") +freq(soil) +#> layer value count +#> 1 1 1011 2345 +#> 2 1 1018 678 +#> ... +``` + +**Sources:** +- [STATSGO/SSURGO](https://websoilsurvey.nrcs.usda.gov/) - US +- [SoilGrids](https://soilgrids.org/) - Global +- [European Soil Database](https://esdac.jrc.ec.europa.eu/) - Europe +- National soil surveys + +## Optional Vector Data + +### Outlet Points (Shapefile) + +Outlet points define where rivers exit the watershed. If provided, the +watershed is delineated upstream of these points. + +**Format Requirements:** +- **File format**: Shapefile (`.shp` with `.shx`, `.dbf`, `.prj`) +- **Geometry**: Point +- **CRS**: Same as DEM +- **Attributes**: Optional; IDs are auto-assigned + +If no outlets are provided, TauDEM will delineate the entire DEM extent. + +## Lookup Tables (CSV) + +### Land Use Lookup + +Maps integer raster values to SWAT+ land use codes. + +**Format**: CSV with at least two columns + +``` +"Value","Landuse" +0000,PAST +1100,UTRN +1110,URHD +1120,URMD +1210,UIDU +2110,AGRL +... +``` + +The first column contains the raster integer value, the second contains the +SWAT+ land use code. + +**Common SWAT+ Land Use Codes:** + +| Code | Description | +|------|-------------| +| AGRL | Agricultural land (generic) | +| AGRR | Agricultural row crops | +| CORN | Corn | +| SOYB | Soybeans | +| WHEA | Wheat | +| FRSD | Deciduous forest | +| FRSE | Evergreen forest | +| FRST | Mixed forest | +| PAST | Pasture | +| RNGE | Range/grassland | +| URBN | Urban (generic) | +| URHD | Urban high density | +| URMD | Urban medium density | +| UTRN | Urban transportation | +| WATR | Water | +| WETF | Forested wetland | +| WETL | Wetland | +| BARR | Barren | +| SWRN | Southwestern range | + +### Soil Lookup + +Maps integer raster values to soil names from the SWAT+ soils database. + +**Format**: CSV with at least two columns + +``` +VALUE,NAME +1011,DK1011 +1018,DK1018 +1021,DK1021 +... +``` + +Soil names should correspond to entries in your SWAT+ soil parameter database. + +## Slope Class Definition + +Slope classes subdivide HRUs by terrain steepness. They are defined using +percentage breakpoints: + +```{r} +library(rQSWATPlus) + +# Single slope class (no subdivision by slope) +slopes <- qswat_create_slope_classes(c(0, 9999)) + +# Three slope classes: 0-5%, 5-15%, >15% +slopes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) + +# Five slope classes +slopes <- qswat_create_slope_classes(c(0, 2, 5, 10, 25, 9999)) +``` + +## Data Preparation Checklist + +Before running `rQSWATPlus`, verify: + +- [ ] **DEM**: GeoTIFF, projected CRS, covers watershed + buffer +- [ ] **Land use**: Integer raster, covers watershed +- [ ] **Soil**: Integer raster, covers watershed +- [ ] **Land use lookup**: CSV mapping all raster values to SWAT+ codes +- [ ] **Soil lookup**: CSV mapping all raster values to soil names +- [ ] **Outlets** (optional): Point shapefile in same CRS as DEM +- [ ] **CRS consistency**: All spatial data should share the same CRS + (reprojection is handled but may introduce artifacts) +- [ ] **Coverage**: All rasters cover the watershed extent +- [ ] **No data**: NoData values are properly set in all rasters + +## Example: Inspecting the Included Ravn Dataset + +```{r, eval = TRUE} +library(rQSWATPlus) + +# Paths to included example data +dem_path <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") +lu_path <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") +soil_path <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") +lu_csv <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") +soil_csv <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") +``` + +```{r, eval = FALSE} +library(terra) + +# Inspect DEM +dem <- rast(dem_path) +dem +plot(dem, main = "Ravn DEM") + +# Check land use values and their lookup +lu <- rast(lu_path) +lu_lookup <- read.csv(lu_csv) +head(lu_lookup) + +# Check soil values and their lookup +soil <- rast(soil_path) +soil_lookup <- read.csv(soil_csv) +head(soil_lookup) +``` + +## Coordinate Reference System Notes + +- **Projected CRS** (UTM, State Plane, etc.) is required for accurate + calculations +- If your DEM is in geographic CRS (lat/lon), reproject it first: + +```{r} +# Reproject DEM to UTM +dem_geo <- rast("dem_wgs84.tif") +dem_proj <- project(dem_geo, "EPSG:32632") # UTM zone 32N +writeRaster(dem_proj, "dem_utm.tif") +``` + +- All rasters should ideally share the same CRS to avoid reprojection artifacts +- The package will attempt to align rasters internally, but pre-aligned data + gives best results diff --git a/rQSWATPlus/vignettes/introduction.Rmd b/rQSWATPlus/vignettes/introduction.Rmd new file mode 100644 index 0000000..c7adf92 --- /dev/null +++ b/rQSWATPlus/vignettes/introduction.Rmd @@ -0,0 +1,167 @@ +--- +title: "Introduction to rQSWATPlus" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Introduction to rQSWATPlus} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = FALSE +) +``` + +## Overview + +The `rQSWATPlus` package provides R functions to set up SWAT+ (Soil and Water +Assessment Tool) hydrological models. It replicates the workflow of the +[QSWATPlus](https://github.com/limnotrack/QSWATPlus) QGIS plugin, bringing +SWAT+ model setup capabilities to R users. + +SWAT+ is a watershed-scale model used for simulating hydrology, water quality, +and land management impacts. Setting up a SWAT+ model requires several spatial +processing steps that this package automates. + +## Prerequisites + +### TauDEM Installation + +Watershed delineation in `rQSWATPlus` relies on +[TauDEM](https://hydrology.usu.edu/taudem/taudem5/) (Terrain Analysis Using +Digital Elevation Models) via the [`traudem`](https://github.com/lucarraro/traudem/) +R package. + +You need to install TauDEM and its dependencies (GDAL and MPI) on your system. +See the traudem installation vignette for platform-specific instructions: + +```{r} +# Install traudem from CRAN +install.packages("traudem") + +# Or the development version +# devtools::install_github("lucarraro/traudem") + +# Check your TauDEM installation +traudem::taudem_sitrep() +``` + +### R Package Dependencies + +```{r} +# Install rQSWATPlus +# devtools::install_github("limnotrack/QSWATPlus", subdir = "rQSWATPlus") + +# Key dependencies (installed automatically): +# - terra: Raster data handling +# - sf: Vector/shapefile processing +# - RSQLite: Database output +# - traudem: TauDEM interface +``` + +## Quick Start + +The basic workflow has five steps: + +```{r} +library(rQSWATPlus) + +# 1. Setup project +project <- qswat_setup( + project_dir = "my_swatplus_project", + dem_file = "path/to/dem.tif", + landuse_file = "path/to/landuse.tif", + soil_file = "path/to/soil.tif", + landuse_lookup = "path/to/landuse_lookup.csv", + soil_lookup = "path/to/soil_lookup.csv", + outlet_file = "path/to/outlets.shp" # optional +) + +# 2. Delineate watershed +project <- qswat_delineate(project, threshold = 500) + +# 3. Create stream network +project <- qswat_create_streams(project) + +# 4. Create HRUs +lu_lookup <- qswat_read_landuse_lookup("path/to/landuse_lookup.csv") +soil_lookup <- qswat_read_soil_lookup("path/to/soil_lookup.csv") +slopes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) + +project <- qswat_create_hrus( + project, lu_lookup, soil_lookup, slopes +) + +# 5. Write database +qswat_write_database(project) +``` + +Or use the convenience function for the entire workflow: + +```{r} +project <- qswat_run( + project_dir = "my_swatplus_project", + dem_file = "path/to/dem.tif", + landuse_file = "path/to/landuse.tif", + soil_file = "path/to/soil.tif", + landuse_lookup = "path/to/landuse_lookup.csv", + soil_lookup = "path/to/soil_lookup.csv", + threshold = 500, + slope_breaks = c(0, 5, 15, 9999) +) +``` + +## Workflow Steps Explained + +### 1. Project Setup + +`qswat_setup()` creates a project directory structure and validates your input +files. It checks that rasters are readable and copies source files into the +project directory. + +### 2. Watershed Delineation + +`qswat_delineate()` runs the full TauDEM delineation workflow: + +1. **Pit removal** (`pitremove`): Fill depressions in the DEM +2. **D8 flow direction** (`d8flowdir`): Calculate flow directions +3. **D-infinity flow direction** (`dinfflowdir`): Calculate slopes and angles +4. **Contributing area** (`aread8`): Compute flow accumulation +5. **Stream threshold** (`threshold`): Define stream/channel networks +6. **Move outlets** (`moveoutletstostreams`): Snap outlets to streams +7. **Stream network** (`streamnet`): Generate topology and watersheds + +The `threshold` parameter controls the minimum drainage area (in cells) to +define a stream. A lower threshold creates more streams and smaller subbasins. + +### 3. Stream Network + +`qswat_create_streams()` extracts the stream/channel topology from the TauDEM +output, including stream links, downstream connections, stream orders, and +drainage areas. + +### 4. HRU Creation + +`qswat_create_hrus()` overlays land use, soil, and slope data on the delineated +watershed. Each unique combination of subbasin × land use × soil × slope class +creates a Hydrologic Response Unit (HRU). + +Area thresholds can be used to filter out small HRUs: +- `landuse_threshold`: Minimum % of subbasin area for a land use +- `soil_threshold`: Minimum % of land use area for a soil type +- `slope_threshold`: Minimum % of soil area for a slope class + +### 5. Database Output + +`qswat_write_database()` creates a SQLite database compatible with the SWAT+ +Editor for further parameterization and model execution. + +## Next Steps + +- See `vignette("data-requirements")` for detailed input data format + specifications +- See `vignette("workflow")` for a complete worked example using the included + Ravn watershed dataset diff --git a/rQSWATPlus/vignettes/workflow.Rmd b/rQSWATPlus/vignettes/workflow.Rmd new file mode 100644 index 0000000..8c99e28 --- /dev/null +++ b/rQSWATPlus/vignettes/workflow.Rmd @@ -0,0 +1,286 @@ +--- +title: "Complete Workflow Example" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{Complete Workflow Example} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>", + eval = TRUE +) +``` + +## Overview + +This vignette demonstrates a complete SWAT+ model setup using `rQSWATPlus` +with the included Ravn watershed dataset from Denmark. The workflow covers +every step from project initialization to database creation. + +## The Ravn Watershed Dataset + +The Ravn watershed is a small catchment in Denmark included as example data. +It includes: + +- **DEM**: 30m resolution SRTM elevation data +- **Land use**: Categorical raster with 55+ land cover classes +- **Soil**: Categorical raster with 49 soil types +- **Outlet**: Point shapefile marking the watershed outlet +- **Lookup tables**: CSV files mapping raster values to SWAT+ codes + +## Step 1: Load Package and Check Prerequisites + +```{r setup} +library(rQSWATPlus) + +# Verify TauDEM is available +traudem::taudem_sitrep() +``` + +## Step 2: Locate Example Data + +```{r data-paths} +# Get paths to example data included in the package +dem <- system.file("extdata", "ravn_dem.tif", package = "rQSWATPlus") +landuse <- system.file("extdata", "ravn_landuse.tif", package = "rQSWATPlus") +soil <- system.file("extdata", "ravn_soil.tif", package = "rQSWATPlus") +lu_lookup <- system.file("extdata", "ravn_landuse.csv", package = "rQSWATPlus") +soil_lookup <- system.file("extdata", "ravn_soil.csv", package = "rQSWATPlus") +outlet <- system.file("extdata", "ravn_outlet.shp", package = "rQSWATPlus") +``` + +## Step 3: Inspect the Input Data + +It is good practice to inspect your input data before running the workflow. + +```{r inspect} +library(terra) + +# Inspect the DEM +dem_rast <- rast(dem) +dem_rast + +# Check dimensions and resolution +cat("DEM dimensions:", nrow(dem_rast), "x", ncol(dem_rast), "\n") +cat("Resolution:", res(dem_rast), "\n") +cat("CRS:", crs(dem_rast, describe = TRUE)$name, "\n") + +# Inspect land use raster +lu_rast <- rast(landuse) +cat("\nLand use unique values:", length(unique(values(lu_rast, na.rm = TRUE))), "\n") + +# Read and inspect lookup tables +lu_table <- read.csv(lu_lookup) +cat("\nLand use lookup (first 10 rows):\n") +print(head(lu_table, 10)) + +soil_table <- read.csv(soil_lookup) +cat("\nSoil lookup (first 10 rows):\n") +print(head(soil_table, 10)) +``` + +## Step 4: Set Up the Project + +```{r setup-project} +# Create a project directory (use a temporary directory for this example) +project_dir <- file.path(tempdir(), "ravn_swatplus") + +# Initialize the project +project <- qswat_setup( + project_dir = project_dir, + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup, + outlet_file = outlet +) + +# Print project summary +print(project) +``` + +## Step 5: Run Watershed Delineation + +The delineation step processes the DEM using TauDEM to identify streams, +channels, subbasins, and watershed boundaries. + +The `threshold` parameter controls stream definition - it specifies the minimum +number of cells that must drain to a point for it to be classified as a stream. +A lower threshold creates more streams and smaller subbasins. + +```{r delineate} +# Run delineation with a stream threshold of 500 cells +# This means a cell needs at least 500 upstream cells to be a stream +project <- qswat_delineate( + project, + threshold = 500, # Stream threshold (cells) + channel_threshold = 50, # Channel threshold (finer network) + quiet = TRUE # Show TauDEM progress messages +) +``` + +### Understanding the Delineation Steps + +The delineation runs these TauDEM operations in sequence: + +1. **Pit Removal** (`pitremove`): Fills depressions in the DEM that would + trap water and prevent proper flow routing + +2. **D8 Flow Direction** (`d8flowdir`): Assigns each cell one of 8 flow + directions pointing to the steepest downslope neighbor + +3. **D-infinity Flow Direction** (`dinfflowdir`): Calculates continuous flow + angles and slopes (used for landscape analysis) + +4. **Contributing Area** (`aread8`): Computes the number of upstream cells + draining through each cell + +5. **Threshold** (`threshold`): Marks cells as streams where contributing + area exceeds the threshold value + +6. **Move Outlets** (`moveoutletstostreams`): Snaps user-provided outlet + points to the nearest stream cell + +7. **Stream Network** (`streamnet`): Generates the complete stream network + topology, watershed boundaries, and subbasin delineation + +## Step 6: Create Stream Network + +Extract stream topology from the delineation results. + +```{r streams} +project <- qswat_create_streams(project) + +# View stream topology +head(project$stream_topology) +``` + +## Step 7: Create HRUs + +HRUs (Hydrologic Response Units) are the fundamental computational units in +SWAT+. Each HRU is a unique combination of subbasin, land use, soil type, +and slope class. + +```{r hrus} +# Read lookup tables +lu <- qswat_read_landuse_lookup(lu_lookup) +soil_lkp <- qswat_read_soil_lookup(soil_lookup) + +# Define slope classes: 0-5%, 5-15%, and >15% +slopes <- qswat_create_slope_classes(c(0, 5, 15, 9999)) +print(slopes) + +# Create HRUs with area thresholds to filter small units +project <- qswat_create_hrus( + project, + landuse_lookup = lu, + soil_lookup = soil_lkp, + slope_classes = slopes, + landuse_threshold = 5, # Land use must be >5% of subbasin + soil_threshold = 5, # Soil must be >5% of land use area + slope_threshold = 0 # No slope filtering +) + +# View HRU summary +cat("Total HRUs:", nrow(project$hru_data), "\n") +cat("Subbasins:", length(unique(project$hru_data$subbasin)), "\n") + +# View first few HRUs +head(project$hru_data) + +# View basin statistics +print(project$basin_data) +``` + +### Understanding HRU Thresholds + +The threshold filtering uses a "dominant" approach: + +1. **Land use threshold** (e.g., 5%): Within each subbasin, only land uses + covering more than 5% of the subbasin area are kept. + +2. **Soil threshold** (e.g., 5%): Within each remaining land use, only soils + covering more than 5% of that land use's area are kept. + +3. **Slope threshold** (e.g., 10%): Within each remaining soil type, only + slope classes covering more than 10% of that soil's area are kept. + +Higher thresholds reduce the number of HRUs but may oversimplify the landscape. + +## Step 8: Write the SWAT+ Database + +```{r database} +# Write the project database +project <- qswat_write_database(project, overwrite = TRUE) + +# Verify the database +library(DBI) +library(RSQLite) +con <- dbConnect(SQLite(), project$db_file) + +# List tables +cat("Database tables:\n") +print(dbListTables(con)) + +# Check table contents +cat("\nSubbasins:\n") +print(dbGetQuery(con, "SELECT * FROM gis_subbasins")) + +cat("\nHRUs (first 10):\n") +print(dbGetQuery(con, "SELECT * FROM gis_hrus LIMIT 10")) + +cat("\nRouting:\n") +print(dbGetQuery(con, "SELECT * FROM gis_routing")) + +dbDisconnect(con) +``` + +## Alternative: Run Everything at Once + +For convenience, `qswat_run()` executes the entire workflow: + +```{r run-all} +project <- qswat_run( + project_dir = file.path(tempdir(), "ravn_quick"), + dem_file = dem, + landuse_file = landuse, + soil_file = soil, + landuse_lookup = lu_lookup, + soil_lookup = soil_lookup, + outlet_file = outlet, + threshold = 500, + slope_breaks = c(0, 5, 15, 9999), + landuse_threshold = 5, + soil_threshold = 5 +) +``` + +## Next Steps After rQSWATPlus + +After creating the project database with `rQSWATPlus`, the next steps in the +SWAT+ modeling workflow are: + +1. **SWAT+ Editor**: Open the SQLite database in the SWAT+ Editor to: + - Add weather/climate data + - Configure management operations + - Set up point sources + - Configure reservoirs and ponds + - Write SWAT+ input files + +2. **SWAT+ Simulation**: Run the SWAT+ executable with the generated input files + +3. **Output Analysis**: Analyze SWAT+ outputs for hydrology, water quality, + sediment transport, etc. + +## Cleanup + +```{r cleanup} +# Remove temporary project directories +unlink(file.path(tempdir(), "ravn_swatplus"), recursive = TRUE) +unlink(file.path(tempdir(), "ravn_quick"), recursive = TRUE) +```