Skip to content

Commit 5e08ff0

Browse files
authored
fix: run converted models with EnergyPlus (#22)
* fix: add annual EnergyPlus run period * fix: order compact schedule day types * fix: simplify EnergyPlus surface polygons * fix: convert material thickness to meters * fix: limit thermostats to conditioned zones * docs: record EnergyPlus simulation fixes * test: support eplusr RunPeriod table shapes
1 parent 7ea97e7 commit 5e08ff0

11 files changed

Lines changed: 141 additions & 12 deletions

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# destep 0.0.0.9000
22

3+
- Fixed EnergyPlus simulation initialization for converted DeST models by adding
4+
an annual run period and normalizing schedule day types, surface polygons,
5+
material thicknesses, and zone thermostat coverage (#22).
36
- Added `GROUND_DATA` conversion to
47
`Site:GroundTemperature:BuildingSurface` using monthly averages of the
58
selected hourly ground-temperature series (#20).

R/conv-const.R

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,9 @@ destep_conv_const <- function(dest, ep) {
340340
name = .(dt_mat$MATERIAL_NAME),
341341
# NOTE: here we use "MediumSmooth" for roughness"
342342
roughness = "MediumSmooth",
343-
thickness = .(dt_mat$LENGTH),
343+
# DeST construction lengths are stored in millimetres, while
344+
# EnergyPlus Material thickness is expressed in metres.
345+
thickness = .(dt_mat$LENGTH / 1000),
344346
conductivity = .(dt_mat$MATERIAL_CONDUCTIVITY),
345347
density = .(dt_mat$MATERIAL_DENSITY),
346348
specific_heat = .(dt_mat$MATERIAL_SPECIFIC_HEAT),

R/conv-schedule.R

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,16 @@ destep_conv_schedule_week <- function(dest, ep, schedule, type_limits, days, pre
365365
}
366366
}
367367

368+
# EnergyPlus resolves day types in field order. AllOtherDays
369+
# therefore has to be the final group, after every explicit type.
370+
has_all_other_days <- vapply(compacted, function(daytypes) {
371+
any(daytypes == ENUM_SCH_DAYTYPE[["AllOtherDays"]])
372+
}, logical(1L))
373+
compacted <- c(
374+
compacted[!has_all_other_days],
375+
compacted[has_all_other_days]
376+
)
377+
368378
len_daytype <- collapse::vlengths(compacted, use.names = FALSE)
369379
list(
370380
rleid = rep(rleid, sum(len_daytype)),

R/conv-surface.R

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ destep_conv_surface <- function(dest, ep) {
8585
)
8686
assert_unique_name(surface$NAME[surface$POINT_NO == 0L], "surface")
8787
data.table::setDT(surface)
88+
data.table::setorderv(surface, c("ID", "POINT_NO"))
8889

8990
# find the adjacent surfaces
9091
surface[BOUNDARY == "Surface", by = "PLANE", BOUNDARY_OBJECT := rev(NAME)]
@@ -122,6 +123,11 @@ destep_conv_surface <- function(dest, ep) {
122123
)]
123124
}
124125

126+
# Use one minimal vertex sequence for each polygon. EnergyPlus simplifies
127+
# adjacent polygons internally; leaving redundant collinear points can make
128+
# the two otherwise identical sides end up with different vertex counts.
129+
surface <- surface[, destep_simplify_surface_polygon(.SD), by = "ID"]
130+
125131
# TODO: how does DeST handle the case when the surface is both a floor and a ceiling?
126132
# TODO: how does EnergyPlus handle "empty floor slab"?
127133

@@ -180,3 +186,43 @@ destep_conv_surface <- function(dest, ep) {
180186

181187
out
182188
}
189+
190+
# Remove redundant vertices from one ordered DeST surface polygon while
191+
# retaining turns and at least the three vertices needed for a valid face.
192+
destep_simplify_surface_polygon <- function(surface, tolerance = 1e-8) {
193+
surface <- data.table::copy(surface)
194+
195+
repeat {
196+
n_vertex <- nrow(surface)
197+
if (n_vertex <= 3L) break
198+
199+
previous <- c(n_vertex, seq_len(n_vertex - 1L))
200+
following <- c(seq.int(2L, n_vertex), 1L)
201+
coordinates <- as.matrix(surface[, .(POINT_X, POINT_Y, POINT_Z)])
202+
incoming <- coordinates - coordinates[previous, , drop = FALSE]
203+
outgoing <- coordinates[following, , drop = FALSE] - coordinates
204+
205+
incoming_length <- sqrt(rowSums(incoming ^ 2))
206+
outgoing_length <- sqrt(rowSums(outgoing ^ 2))
207+
cross_product <- cbind(
208+
incoming[, 2L] * outgoing[, 3L] - incoming[, 3L] * outgoing[, 2L],
209+
incoming[, 3L] * outgoing[, 1L] - incoming[, 1L] * outgoing[, 3L],
210+
incoming[, 1L] * outgoing[, 2L] - incoming[, 2L] * outgoing[, 1L]
211+
)
212+
# A vertex is redundant only when the path continues straight through
213+
# it. A collinear reversal is retained because it changes the polygon.
214+
redundant <- incoming_length <= tolerance |
215+
outgoing_length <= tolerance |
216+
(
217+
sqrt(rowSums(cross_product ^ 2)) <=
218+
tolerance * pmax(1, incoming_length * outgoing_length) &
219+
rowSums(incoming * outgoing) > 0
220+
)
221+
222+
if (!any(redundant) || n_vertex - sum(redundant) < 3L) break
223+
surface <- surface[!redundant]
224+
}
225+
226+
data.table::set(surface, NULL, "POINT_NO", seq_len(nrow(surface)) - 1L)
227+
surface
228+
}

R/conv-thermostat.R

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ destep_conv_thermostat <- function(dest, ep) {
1515
skip_reason[is.na(thermostat$ROOM_GROUP_ID)] <- "ROOM.OF_ROOM_GROUP does not reference ROOM_GROUP"
1616
missing_setpoint <- is.na(skip_reason) & !destep_has_thermostat_setpoints(thermostat)
1717
skip_reason[missing_setpoint] <- "ROOM_GROUP setpoint schedule is zero or missing"
18+
# A ZoneControl:Thermostat is valid only for a Zone with equipment. Keep
19+
# this predicate aligned with IdealLoads so unconditioned DeST rooms do not
20+
# become EnergyPlus controlled zones without EquipmentConnections.
21+
unsupported_zone <- is.na(skip_reason) & (
22+
is.na(thermostat$IS_AC_ROOM) |
23+
thermostat$IS_AC_ROOM == 0L |
24+
is.na(thermostat$AC_SCHEDULE_ID) |
25+
thermostat$AC_SCHEDULE_ID == 0L
26+
)
27+
skip_reason[unsupported_zone] <- "ROOM_GROUP does not describe a supported ideal loads zone"
1828
data.table::set(thermostat, NULL, "SKIP_REASON", skip_reason)
1929
data.table::set(thermostat, NULL, "CAN_CONVERT", is.na(skip_reason))
2030
data.table::set(
@@ -28,15 +38,15 @@ destep_conv_thermostat <- function(dest, ep) {
2838

2939
if (any(!thermostat$CAN_CONVERT)) {
3040
warn(sprintf(
31-
"Skipped %i ROOM row(s) that do not have complete ROOM_GROUP thermostat setpoints.",
41+
"Skipped %i ROOM row(s) that do not describe supported controlled zones.",
3242
sum(!thermostat$CAN_CONVERT)
3343
))
3444
}
3545

36-
thermostat <- thermostat[thermostat$CAN_CONVERT]
37-
if (nrow(thermostat) == 0L) return(NULL)
46+
converted <- thermostat[thermostat$CAN_CONVERT]
47+
if (nrow(converted) == 0L) return(NULL)
3848

39-
setpoint <- unique(thermostat[, c(
49+
setpoint <- unique(converted[, c(
4050
"SET_T_MIN_SCHEDULE", "HEATING_SCHEDULE_NAME",
4151
"SET_T_MAX_SCHEDULE", "COOLING_SCHEDULE_NAME",
4252
"ENERGYPLUS_SETPOINT_NAME"
@@ -45,7 +55,7 @@ destep_conv_thermostat <- function(dest, ep) {
4555
out <- destep_combine_outputs(list(
4656
control_type = destep_thermostat_control_type_schedule(dest, ep),
4757
setpoint = destep_thermostat_setpoint_objects(dest, ep, setpoint),
48-
control = destep_thermostat_control_objects(dest, ep, thermostat)
58+
control = destep_thermostat_control_objects(dest, ep, converted)
4959
), table = thermostat)
5060

5161
out

R/conv.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,21 @@ to_eplus <- function(dest, ver = "latest", copy = TRUE, verbose = FALSE) {
150150
daylighting_reference_point_coordinate_system = "Relative"
151151
))
152152

153+
# DeST stores model inputs but no EnergyPlus simulation period. Add a
154+
# calendar-year period so each converted model can run against an EPW file.
155+
ep$add("RunPeriod" := list(
156+
name = "Annual",
157+
begin_month = 1L,
158+
begin_day_of_month = 1L,
159+
end_month = 12L,
160+
end_day_of_month = 31L,
161+
use_weather_file_holidays_and_special_days = "Yes",
162+
use_weather_file_daylight_saving_period = "Yes",
163+
apply_weekend_holiday_rule = "No",
164+
use_weather_file_rain_indicators = "Yes",
165+
use_weather_file_snow_indicators = "Yes"
166+
))
167+
153168
# update object names and make sure all names are unique
154169
destep_update_name(tmpdb)
155170

tests/testthat/test-conv-const.R

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,11 @@ test_that("can convert 'Construction' and 'Material'", {
1818
"Construction"
1919
)
2020
)
21+
material_thickness <- const$value[
22+
class_name == "Material" & field_name == "Thickness",
23+
value_num
24+
]
25+
expect_equal(max(material_thickness, na.rm = TRUE), 0.2)
26+
expect_true(any(material_thickness == 0.02))
2127
expect_s3_class(attr(const, "table"), "data.table")
2228
})

tests/testthat/test-conv-schedule.R

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,15 @@ test_that("real model schedule week and year references are resolvable", {
7070
]),
7171
0L
7272
)
73+
74+
# AllOtherDays matches every still-unassigned day type, so EnergyPlus
75+
# requires it to follow the explicit Schedule:Week:Compact assignments.
76+
for (week_name in unique(week$name)) {
77+
daytypes <- week[
78+
name == week_name & grepl("^DayType List", field) & !is.na(value),
79+
value
80+
]
81+
all_other <- which(daytypes == "For: AllOtherDays")
82+
if (length(all_other)) expect_equal(all_other, length(daytypes))
83+
}
7384
})

tests/testthat/test-conv-surface.R

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,25 @@ test_that("can convert 'BuildingSurface:Detailed'", {
1111
expect_equal(unique(surface$object$class_name), "BuildingSurface:Detailed")
1212
expect_s3_class(attr(surface, "table"), "data.table")
1313
})
14+
15+
test_that("surface polygon simplification removes only redundant vertices", {
16+
polygon <- data.table::data.table(
17+
POINT_NO = 0:5,
18+
POINT_X = c(0, 1, 2, 2, 1, 0),
19+
POINT_Y = c(0, 0, 0, 1, 1, 1),
20+
POINT_Z = 0
21+
)
22+
23+
simplified <- destep_simplify_surface_polygon(polygon)
24+
25+
expect_equal(nrow(simplified), 4L)
26+
expect_equal(simplified$POINT_NO, 0:3)
27+
expect_equal(
28+
simplified[, .(POINT_X, POINT_Y, POINT_Z)],
29+
data.table::data.table(
30+
POINT_X = c(0, 2, 2, 0),
31+
POINT_Y = c(0, 0, 1, 1),
32+
POINT_Z = 0
33+
)
34+
)
35+
})

tests/testthat/test-conv-thermostat.R

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ test_that("can convert ROOM_GROUP thermostat setpoints with shared dual setpoint
3939

4040
expect_equal(sum(thermostat$object$class_name == "Schedule:Constant"), 1L)
4141
expect_equal(sum(thermostat$object$class_name == "ThermostatSetpoint:DualSetpoint"), 2L)
42-
expect_equal(sum(thermostat$object$class_name == "ZoneControl:Thermostat"), 3L)
42+
expect_equal(sum(thermostat$object$class_name == "ZoneControl:Thermostat"), 2L)
4343

44-
expect_true(all(tab$CAN_CONVERT))
44+
expect_equal(tab$CAN_CONVERT, c(TRUE, FALSE, TRUE))
4545
expect_equal(
4646
tab$ENERGYPLUS_SETPOINT_NAME,
4747
c(
@@ -150,14 +150,14 @@ test_that("can convert ROOM_GROUP thermostat setpoints from a real DeST model",
150150
thermostat <- destep_conv_thermostat(dest, ep)
151151
tab <- attr(thermostat, "table")
152152

153-
expect_equal(sum(tab$CAN_CONVERT), 36L)
154-
expect_equal(sum(tab$CAN_CONVERT & tab$IS_AC_ROOM == 0L), 9L)
153+
expect_equal(sum(tab$CAN_CONVERT), 27L)
154+
expect_equal(sum(!tab$CAN_CONVERT & tab$IS_AC_ROOM == 0L), 9L)
155155
expect_equal(
156156
unique(tab$ENERGYPLUS_SETPOINT_NAME),
157157
"DeST Dual Setpoint H10 C7"
158158
)
159159
expect_equal(sum(thermostat$object$class_name == "ThermostatSetpoint:DualSetpoint"), 1L)
160-
expect_equal(sum(thermostat$object$class_name == "ZoneControl:Thermostat"), 36L)
160+
expect_equal(sum(thermostat$object$class_name == "ZoneControl:Thermostat"), 27L)
161161
})
162162

163163
test_that("to_eplus() includes resolvable thermostat references", {
@@ -177,7 +177,7 @@ test_that("to_eplus() includes resolvable thermostat references", {
177177
constant_names <- constant$value[constant$field == "Name"]
178178
year_names <- year$value[year$field == "Name"]
179179

180-
expect_equal(length(control_names), 36L)
180+
expect_equal(length(control_names), 27L)
181181
expect_equal(length(setpoint_names), 1L)
182182
expect_true("DeST Dual Setpoint Control Type" %in% constant_names)
183183
expect_true(all(

0 commit comments

Comments
 (0)