|
| 1 | +# GROUND_DATA -> Site:GroundTemperature:BuildingSurface. |
| 2 | +# EnergyPlus uses this object for building surfaces whose outside boundary |
| 3 | +# condition is Ground, so DeST's hourly user-defined ground temperatures are |
| 4 | +# reduced to the 12 monthly values required by the IDD. |
| 5 | +destep_conv_ground_temperature <- function(dest, ep) { |
| 6 | + if (!destep_has_rows(dest, "GROUND_DATA")) return(NULL) |
| 7 | + |
| 8 | + ground <- destep_ground_temperature_table(dest) |
| 9 | + monthly <- destep_monthly_ground_temperature(ground) |
| 10 | + |
| 11 | + out <- destep_add( |
| 12 | + dest, ep, |
| 13 | + "Site:GroundTemperature:BuildingSurface" := |
| 14 | + destep_ground_temperature_value(monthly) |
| 15 | + ) |
| 16 | + attr(out, "table") <- monthly |
| 17 | + |
| 18 | + out |
| 19 | +} |
| 20 | + |
| 21 | +# Select one GROUND_DATA series and return the validated hourly table used by |
| 22 | +# the converter. The selection mirrors the DeST library path when available |
| 23 | +# and keeps the unique-ID fallback explicit for models like the current fixture. |
| 24 | +destep_ground_temperature_table <- function(dest) { |
| 25 | + ground_id <- destep_select_ground_data_id(dest) |
| 26 | + if (is.null(ground_id)) return(data.table::data.table()) |
| 27 | + |
| 28 | + ground <- DBI::dbGetQuery( |
| 29 | + dest, |
| 30 | + sprintf( |
| 31 | + " |
| 32 | + SELECT |
| 33 | + ID, |
| 34 | + HOUR, |
| 35 | + T |
| 36 | + FROM GROUND_DATA |
| 37 | + WHERE ID = %s |
| 38 | + ORDER BY HOUR |
| 39 | + ", |
| 40 | + DBI::dbQuoteLiteral(dest, ground_id) |
| 41 | + ) |
| 42 | + ) |
| 43 | + data.table::setDT(ground) |
| 44 | + destep_force_numeric(ground, c("ID", "HOUR", "T")) |
| 45 | + destep_validate_ground_temperature_table(ground, ground_id) |
| 46 | + |
| 47 | + ground |
| 48 | +} |
| 49 | + |
| 50 | +# Resolve the ground-temperature data set from ENVIRONMENT/SYS_CITY first. |
| 51 | +# If that bridge is absent or points outside GROUND_DATA, a single available |
| 52 | +# GROUND_DATA.ID is safe; multiple IDs need a deliberate selection rule. |
| 53 | +destep_select_ground_data_id <- function(dest) { |
| 54 | + resolved <- destep_resolve_city_ground_data_ids(dest) |
| 55 | + if (length(resolved) == 1L) return(resolved[[1L]]) |
| 56 | + if (length(resolved) > 1L) { |
| 57 | + stop(sprintf( |
| 58 | + "Multiple GROUND_DATA IDs are referenced by ENVIRONMENT/SYS_CITY: %s", |
| 59 | + paste(resolved, collapse = ", ") |
| 60 | + ), call. = FALSE) |
| 61 | + } |
| 62 | + |
| 63 | + ids <- DBI::dbGetQuery( |
| 64 | + dest, |
| 65 | + "SELECT DISTINCT ID FROM GROUND_DATA ORDER BY ID" |
| 66 | + )$ID |
| 67 | + ids <- ids[!is.na(ids)] |
| 68 | + if (length(ids) == 0L) return(NULL) |
| 69 | + if (length(ids) == 1L) return(ids[[1L]]) |
| 70 | + |
| 71 | + stop(sprintf( |
| 72 | + paste( |
| 73 | + "Cannot choose GROUND_DATA ID;", |
| 74 | + "multiple IDs are present and ENVIRONMENT/SYS_CITY did not select one: %s" |
| 75 | + ), |
| 76 | + paste(ids, collapse = ", ") |
| 77 | + ), call. = FALSE) |
| 78 | +} |
| 79 | + |
| 80 | +# Keep the ENVIRONMENT/SYS_CITY bridge optional because ad hoc fixtures and some |
| 81 | +# DeST exports may carry GROUND_DATA without a resolvable city-library row. |
| 82 | +destep_resolve_city_ground_data_ids <- function(dest) { |
| 83 | + if (!all(c("ENVIRONMENT", "SYS_CITY", "GROUND_DATA") %in% DBI::dbListTables(dest))) { |
| 84 | + return(numeric()) |
| 85 | + } |
| 86 | + if (!destep_table_has_fields(dest, "ENVIRONMENT", "CITY_ID") || |
| 87 | + !destep_table_has_fields(dest, "SYS_CITY", c("CITY_ID", "GROUND_ID"))) { |
| 88 | + return(numeric()) |
| 89 | + } |
| 90 | + |
| 91 | + ids <- DBI::dbGetQuery( |
| 92 | + dest, |
| 93 | + " |
| 94 | + SELECT DISTINCT C.GROUND_ID AS ID |
| 95 | + FROM ENVIRONMENT E |
| 96 | + INNER JOIN SYS_CITY C |
| 97 | + ON E.CITY_ID = C.CITY_ID |
| 98 | + INNER JOIN GROUND_DATA G |
| 99 | + ON C.GROUND_ID = G.ID |
| 100 | + WHERE C.GROUND_ID IS NOT NULL |
| 101 | + ORDER BY C.GROUND_ID |
| 102 | + " |
| 103 | + )$ID |
| 104 | + |
| 105 | + ids[!is.na(ids)] |
| 106 | +} |
| 107 | + |
| 108 | +# Check a table's columns before running optional bridge SQL. This avoids |
| 109 | +# turning small unit-test fixtures into schema-completeness tests. |
| 110 | +destep_table_has_fields <- function(dest, table, fields) { |
| 111 | + all(fields %in% DBI::dbListFields(dest, table)) |
| 112 | +} |
| 113 | + |
| 114 | +# A BuildingSurface ground-temperature object has no room for gaps or duplicate |
| 115 | +# hours, so the selected DeST series must be exactly one non-leap 8760-hour year. |
| 116 | +destep_validate_ground_temperature_table <- function(ground, ground_id) { |
| 117 | + issues <- character() |
| 118 | + hour <- ground$HOUR |
| 119 | + |
| 120 | + if (nrow(ground) != 8760L) { |
| 121 | + issues <- c(issues, sprintf( |
| 122 | + "expected 8760 rows but found %i", |
| 123 | + nrow(ground) |
| 124 | + )) |
| 125 | + } |
| 126 | + if (anyNA(hour)) { |
| 127 | + issues <- c(issues, "HOUR contains missing values") |
| 128 | + } else { |
| 129 | + missing_hours <- setdiff(0:8759, hour) |
| 130 | + duplicate_hours <- unique(hour[duplicated(hour)]) |
| 131 | + unexpected_hours <- setdiff(hour, 0:8759) |
| 132 | + |
| 133 | + if (length(missing_hours)) { |
| 134 | + issues <- c(issues, sprintf( |
| 135 | + "missing HOUR value(s): %s", |
| 136 | + destep_format_integer_sample(missing_hours) |
| 137 | + )) |
| 138 | + } |
| 139 | + if (length(duplicate_hours)) { |
| 140 | + issues <- c(issues, sprintf( |
| 141 | + "duplicate HOUR value(s): %s", |
| 142 | + destep_format_integer_sample(duplicate_hours) |
| 143 | + )) |
| 144 | + } |
| 145 | + if (length(unexpected_hours)) { |
| 146 | + issues <- c(issues, sprintf( |
| 147 | + "unexpected HOUR value(s): %s", |
| 148 | + destep_format_integer_sample(unexpected_hours) |
| 149 | + )) |
| 150 | + } |
| 151 | + } |
| 152 | + if (anyNA(ground$T)) { |
| 153 | + issues <- c(issues, "T contains missing values") |
| 154 | + } |
| 155 | + |
| 156 | + if (length(issues)) { |
| 157 | + stop(sprintf( |
| 158 | + "Invalid GROUND_DATA series for ID %s: %s", |
| 159 | + ground_id, |
| 160 | + paste(issues, collapse = "; ") |
| 161 | + ), call. = FALSE) |
| 162 | + } |
| 163 | + |
| 164 | + invisible(ground) |
| 165 | +} |
| 166 | + |
| 167 | +# Show enough hour IDs for a useful error while keeping long validation messages |
| 168 | +# readable. |
| 169 | +destep_format_integer_sample <- function(x, n = 10L) { |
| 170 | + x <- sort(unique(as.integer(x))) |
| 171 | + out <- paste(utils::head(x, n), collapse = ", ") |
| 172 | + if (length(x) > n) out <- paste0(out, ", ...") |
| 173 | + out |
| 174 | +} |
| 175 | + |
| 176 | +# Aggregate the validated hourly series using the standard non-leap calendar |
| 177 | +# implied by DeST's HOUR = 0:8759 convention. |
| 178 | +destep_monthly_ground_temperature <- function(ground) { |
| 179 | + month_hours <- c(31L, 28L, 31L, 30L, 31L, 30L, 31L, 31L, 30L, 31L, 30L, 31L) * 24L |
| 180 | + month <- rep(seq_along(month_hours), month_hours) |
| 181 | + |
| 182 | + monthly <- data.table::data.table( |
| 183 | + GROUND_DATA_ID = unique(ground$ID), |
| 184 | + MONTH = seq_along(month_hours), |
| 185 | + GROUND_TEMPERATURE = as.numeric(tapply(ground$T, month, mean)) |
| 186 | + ) |
| 187 | + monthly |
| 188 | +} |
| 189 | + |
| 190 | +# Build the exact EnergyPlus field list for Site:GroundTemperature:BuildingSurface. |
| 191 | +destep_ground_temperature_value <- function(monthly) { |
| 192 | + values <- as.list(monthly$GROUND_TEMPERATURE) |
| 193 | + names(values) <- paste0( |
| 194 | + tolower(month.name), |
| 195 | + "_ground_temperature" |
| 196 | + ) |
| 197 | + values |
| 198 | +} |
0 commit comments