A short description has been given in the Synthetic meter data creation workflow.
We choose AMY (Annual Meteorological Year) as the observed input. AMY together with other separate local weather variables are commonly used as the observed inputs for calibration.
For calibration parameters, we select 7 commonly used ones for demonstration, including:
The mapping between those parameters and corresponding EnergyPlus inputs for the baseline model are listed in the table blow:
# | Calibration parameter | Class | Object | Field | Value |
---|---|---|---|---|---|
1 | Material properties | Material |
Steel Frame NonRes Wall Insulation |
Conductivity {W/m-K} |
0.049 |
2 | Density {kg/m3} |
265.0 | |||
3 | Specific Heat {J/kg-K} |
836.8 | |||
4 | WindowMaterial:SimpleGlazingSystem |
NonRes Fixed Assembly Window |
U-Factor {W/m2-K} |
3.237 | |
5 | Solar Heat Gain Coefficient |
0.39 | |||
6 | Infiltration rate | ZoneInfiltration:DesignFlowRate |
All *_Infiltration objects |
Flow per Exterior Surface Area {m3/s-m2} |
0.000302 |
7 | Occupancy density | People |
All objects for offices | Zone Floor Area per Person {m2/persion} |
18.58 |
8 | Lighting power density | Lights |
All *_Lights objects for offices |
Watts per Zone Floor Area {W/m2} |
10.76 |
9 | Electric equipment power sensity | ElectricEquipment |
All *_Equip objects for offices |
Watts per Zone Floor Area {W/m2} |
10.76 |
10 | Cooling setpoint | Schedule:Compact |
CLGSETP_SCH |
Field 6 and Field 13 |
24 |
11 | Heating setpoint | Schedule:Compact |
HTGSETP_SCH |
Field 6 , Field 16 and Field 21 |
21 |
For observed output, we choose hourly building electricity consumption. The corresponding output variable in EnergyPlus is Electricity:Facility
with Hourly
reporting frequency.
# path of the model
<- here("data-raw/idf/RefBldgMediumOfficeNew2004_Chicago.idf")
path
# read the model as the starting point for the test model
<- read_idf(path) init
In order to create a test model for calibration, we modified the values of 11 calibration parameters in the model.
For demonstration, we set the initial parameter values before calibration to a random value in predefined ranges.
# actual parameter values in the model
<- 0.049
real_conductivity <- 265.0
real_density <- 836.8
real_specific_heat <- 3.237
real_u_value <- 0.39
real_SHGC <- 0.000302
real_infiltration <- 18.58
real_people <- 10.76
real_lpd <- 10.76
real_epd <- 24
real_clg_sp <- 21
real_htg_sp
# define the upper and lower offset
<- 0.50
offset <- 1 - offset
lower <- 1 + offset
upper
# define the lower and upper limits of parameters
<- dplyr::tribble(
param ~parameter , ~real , ~lower , ~upper ,
"conductivity" , real_conductivity , real_conductivity * lower , real_conductivity * upper ,
"density" , real_density , real_density * lower , real_density * upper ,
"specific_heat" , real_specific_heat , real_specific_heat * lower , real_specific_heat * upper ,
"u_value" , real_u_value , real_u_value * lower , real_u_value * upper ,
"SHGC" , real_SHGC , 0.1 , 0.6 ,
"infiltration" , real_infiltration , 0 , 0.0005 ,
"people" , real_people , 10 , 25 ,
"lpd" , real_lpd , 2 , 20 ,
"epd" , real_epd , 2 , 20 ,
"cooling" , real_clg_sp , 22 , 26 ,
"heating" , real_htg_sp , 15 , 21.5
)
# for reproducibility
set.seed(4)
# initial parameter values for calibration
<- param %>%
param mutate(init = map2_dbl(lower, upper, runif, n = 1L)) %>%
select(parameter, real, init, everything())
The ranges and initial values of parameters are listed in the table below.
<- param %>%
tbl_param mutate(name = c(
"Insulation conductivity (W/(m K))",
"Insulation density (kg/m<sup>3</sup>)",
"Insulation specific heat (J/(kg K))",
"Window U value [W/(m<sup>2</sup> K)]",
"Window SHGC",
"Infiltration per floor area (m<sup>3</sup>/(s m<sup>2</sup>))",
"People density (m<sup>2</sup>/person)",
"Lighting power density (W/m<sup>2</sup>)",
"Equipment power density (W/m<sup>2</sup>)",
"Cooling setpoint (°C)",
"Heating setpoint (°C)")) %>%
select(name, parameter, everything())
%>%
tbl_param ::gt() %>%
gt::cols_hide(vars(real)) %>%
gt::fmt_number(vars(init, lower, upper)) %>%
gt::tab_header(title = "List of calibration parameters and their ranges") %>%
gt::cols_label(
gtname = "Calibration parameter",
parameter = "Symbol",
init = "Initial value",
lower = "Min",
upper = "Max") %>%
::fmt_markdown(vars(name)) %>%
gt::tab_style(
gtstyle = gt::cell_text(weight = "bold"),
locations = gt::cells_column_labels(everything()))
## Warning: The `.dots` argument of `group_by()` is deprecated as of dplyr 1.0.0.
List of calibration parameters and their ranges | ||||
---|---|---|---|---|
Calibration parameter | Symbol | Initial value | Min | Max |
Insulation conductivity (W/(m K)) |
conductivity | 0.05 | 0.02 | 0.07 |
Insulation density (kg/m3) |
density | 134.87 | 132.50 | 397.50 |
Insulation specific heat (J/(kg K)) |
specific_heat | 664.20 | 418.40 | 1,255.20 |
Window U value [W/(m2 K)] |
u_value | 2.52 | 1.62 | 4.86 |
Window SHGC |
SHGC | 0.51 | 0.10 | 0.60 |
Infiltration per floor area (m3/(s m2)) |
infiltration | 0.00 | 0.00 | 0.00 |
People density (m2/person) |
people | 20.87 | 10.00 | 25.00 |
Lighting power density (W/m2) |
lpd | 18.31 | 2.00 | 20.00 |
Equipment power density (W/m2) |
epd | 19.08 | 2.00 | 20.00 |
Cooling setpoint (°C) |
cooling | 22.29 | 22.00 | 26.00 |
Heating setpoint (°C) |
heating | 19.91 | 15.00 | 21.50 |
Below we made some necessary to the model, including adding the output meter and setting the begin year of RunPeriod
objects based on our synthetic meter data.
After this, we update the model based on the initial parameter values generated above and saved the model as Init.idf
in the data/idf
folder.
# remove all existing outputs
$`Output:Variable` <- NULL
init$`Output:Meter` <- NULL
init
# add hourly building electricity output meter
$add(Output_Meter = list("Electricity:Facility", "Hourly"))
init
# make sure weater file is used
$SimulationControl$Run_Simulation_for_Weather_File_Run_Periods <- "Yes"
init
# update RunPeriod to correctly indicate an AMY EPW file is used
$RunPeriod$annual$set(
initname = "Philadelphia 2014",
begin_year = 2014,
day_of_week_for_start_day = "Wednesday"
)
<- function(var) param %>% filter(parameter == !!var) %>% pull()
init_val
update_insulation(init,
conductivity = init_val("conductivity"),
density = init_val("density"),
specific_heat = init_val("specific_heat")
)update_window(init, u_value = init_val("u_value"), SHGC = init_val("SHGC"))
update_infiltration(init, flow_per_area = init_val("infiltration"))
update_people(init, people = init_val("people"))
update_lights(init, lpd = init_val("lpd"))
update_equip(init, epd = init_val("epd"))
update_setpoint(init, cooling = init_val("cooling"), heating = init_val("heating"))
# save
$save(here("data/idf/Init.idf"), overwrite = TRUE) init
# file path of TMY3 EPW file
<- here("data-raw/epw/TMY3/USA_PA_Philadelphia.Intl.AP.724080_TMY3.epw")
path_epw_tmy3
# run annual simulation
$run(path_epw_tmy3, here("data/sim/Init"), echo = FALSE) init
<- init %>% extract_electricity() meter_init
# select one week in July to examine the discrepancy
<- meter_init %>% weekly_compare(7, 1)
plot_init
ggsave(here("figures/Init.png"), plot_init, height = 6, width = 10, dpi = 300)
::include_graphics(here("figures/Init.png")) knitr
<- meter_init %>% cal_stats() stats_init
Current calibration creteria in ASHRAE Guideline 14 for NMBE and CVRMSE is 10% and 30% respectively.
The initial NMBE and CV(RMSE) is 51.86% and 67.67%, respectively.
# read the initial test model
<- read_idf(here("data/idf/Init.idf"))
amy
# path of AMY EPW
<- here("data-raw/epw/AMY/PA_PHILADELPHIA_720304_14-13.epw")
path_epw_amy
# run annual simulation
$run(path_epw_amy, here("data/sim/AMY"), echo = FALSE) amy
<- amy %>% extract_electricity() meter_amy
# select one week in July to examine the discrepancy
<- meter_amy %>% weekly_compare(7, 1)
plot_init
ggsave(here("figures/AMY.png"), plot_init, height = 6, width = 10, dpi = 300)
::include_graphics(here("figures/AMY.png")) knitr
<- meter_amy %>% cal_stats() stats_amy
Ater using the AMY weather file, the NMBE and CV(RMSE) is 50.80% and 62.12%, respectively.
# define setup values
<- 4L # number of individuals per generation
MU <- 4L # number of offspring
LAMBDA <- 20L # maximum of generation
MAX_GEN <- 0.3 # creteria for CVRMSE
MAX_CVRMSE <- 0.1 # creteria for NMBE
MAX_NMBE <- 0.1 # mutation probability
P_MUTATE <- 0.7 # crossover probability
P_RECOMB
# define an ecr control object to store information on the objective function
<- ecr::initECRControl(calib_fitness, n.objectives = 2, minimize = TRUE)
control # initial a logger to store population and fitness
<- ecr::initLogger(control, log.pop = TRUE)
log # initial archive of Pareto front
<- ecr::initParetoArchive(control)
pareto
# set evolutionary operators
# use no priors and set all parameters to follow uniform distribution
<- ecr::registerECROperator(control, "mutate", ecr::mutUniform,
control lower = param$lower, upper = param$upper)
<- ecr::registerECROperator(control, "recombine", ecr::recCrossover)
control <- ecr::registerECROperator(control, "selectForMating", ecr::selSimple)
control <- ecr::registerECROperator(control, "selectForSurvival", ecr::selNondom)
control
# initialize population of MU random values
<- ecr::initPopulation(
pop ::genReal, n.dim = nrow(param),
MU, ecrlower = param$lower, upper = param$upper)
# evaluate fitness of initial population
<- evaluate_fitness(control, pop, 1, workers = 4)
fit
# update log
::updateLogger(log, pop, fit, MU)
ecr
# run evolutionary loop
repeat {
# generate offspring
<- ecr::generateOffspring(control, pop, fit, LAMBDA, p.recomb = P_RECOMB, p.mut = P_MUTATE)
offspring
# evaluate fitness of the offspring
<- evaluate_fitness(control, offspring, log$env$n.gens + 1L, workers = 4)
fit_offspring
# do logging
::updateLogger(log, offspring, fit_offspring, MU)
ecr
# prepare next generation
<- ecr::replaceMuPlusLambda(control, pop, offspring, fit, fit_offspring)
res
# extract population and fitness
<- res$population
pop <- res$fitness
fit
# update pareto archieve
::updateParetoArchive(pareto, pop, fit)
ecr
# check whether terminator conditions are met
<- ecr:::doTerminate(log, list(
stop_obj # stop if CVRMSE <= 30% and NMBE <= 10%
stopOnMeetCreteria(cvrmse = MAX_CVRMSE, nmbe = MAX_NMBE),
# stop if max generation matched
::stopOnIters(MAX_GEN)
ecr
))
# stop if creteria are met
if (length(stop_obj) > 0L) break
}
# extract optimization results
<- ecr:::makeECRResult(control, log, pop, fit, stop_obj) result
After the optimization completed, we extracted all the population together with the fitness values, and saved them into a table in data/population.csv
. The is_pareto
column indicates whether current individual is a Pareto front or not.
# get unique Pareto front
<- get_front(pareto)
front
# get all population together with fitness
<- get_population(log)
population # rename columns using parameter names
names(population)[seq_len(nrow(param)) + 2L] <- param$parameter
# save results
write_csv(population, here("data/population.csv"))
The figure blow shows the distribution of the Pareto front. Since we use synthetic meta data and models that only vary several parameters, it only takes 3 generations for the NSGA-II algorithm to converge.
# plot Pareto front
<- population %>%
plot_pareto ggplot(aes(nmbe, cvrmse)) +
geom_rect(aes(xmin = 0, xmax = MAX_NMBE, ymin = 0, ymax = MAX_CVRMSE),
color = "grey70", size = 1, linetype = 2, fill = "grey90", alpha = 0.1) +
geom_point() +
geom_point(data = front, color = "red") +
scale_x_continuous("NMBE / %", labels = scales::label_percent(),
breaks = seq(0, 1, 0.1), expand = c(0, 0), limits = c(0, max(population$nmbe) * 1.05)) +
scale_y_continuous("CV(RMSE) / %", labels = scales::label_percent(),
breaks = seq(0, 1, 0.1), expand = c(0, 0), limits = c(0, max(population$cvrmse) * 1.05)) +
theme_bw()
ggsave(here::here("figures/pareto.png"), plot_pareto, height = 6, width = 6, dpi = 300)
::include_graphics(here("figures/pareto.png")) knitr
%>%
population filter(is_pareto) %>%
distinct(!!!syms(c(param$parameter, "nmbe", "cvrmse"))) %>%
mutate(index = seq_len(n())) %>%
select(index, everything()) %>%
::gt() %>%
gt::fmt_number(all_of(param$parameter)) %>%
gt::fmt_percent(vars(nmbe, cvrmse)) %>%
gt::cols_label(
gtindex = "#",
conductivity = gt::html("Insulation conductivity (W/(m K))"),
density = gt::html("Insulation density (kg/m<sup>3</sup>)"),
specific_heat = gt::html("Insulation specific heat (J/(kg K))"),
u_value = gt::html("Window U value [W/(m<sup>2</sup> K)]"),
SHGC = gt::html("Window SHGC"),
infiltration = gt::html("Infiltration per floor area (m<sup>3</sup>/(s m<sup>2</sup>))"),
people = gt::html("People density (m<sup>2</sup>/person)"),
lpd = gt::html("Lighting power density (W/m<sup>2</sup>)"),
epd = gt::html("Equipment power density (W/m<sup>2</sup>)"),
cooling = gt::html("Cooling setpoint (°C)"),
heating = gt::html("Heating setpoint (°C)"),
nmbe = "NMBE",
cvrmse = "CV(RMSE)") %>%
::tab_style(
gtstyle = gt::cell_text(weight = "bold"),
locations = gt::cells_column_labels(everything()))
# | Insulation conductivity (W/(m K)) | Insulation density (kg/m3) | Insulation specific heat (J/(kg K)) | Window U value [W/(m2 K)] | Window SHGC | Infiltration per floor area (m3/(s m2)) | People density (m2/person) | Lighting power density (W/m2) | Equipment power density (W/m2) | Cooling setpoint (°C) | Heating setpoint (°C) | NMBE | CV(RMSE) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0.07 | 204.81 | 904.41 | 4.72 | 0.23 | 0.00 | 14.09 | 10.05 | 8.53 | 22.63 | 19.03 | 8.12% | 11.84% |
2 | 0.03 | 204.81 | 904.41 | 4.72 | 0.54 | 0.00 | 11.66 | 15.46 | 10.41 | 25.50 | 15.30 | 0.39% | 29.71% |
%>%
population filter(is_pareto) %>%
distinct(!!!syms(c(param$parameter, "nmbe", "cvrmse")), .keep_all = TRUE) %>%
select(index_gen, index_ind, nmbe, cvrmse) %>%
mutate(path_plot = here(sprintf("figures/Gen%i/Gen%i_Ind%i.png", index_gen, index_gen, index_ind))) %>%
pull(path_plot) %>%
::include_graphics() knitr
sessionInfo()
## R version 4.1.0 (2021-05-18)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 19042)
##
## Matrix products: default
##
## locale:
## [1] LC_COLLATE=English_United States.1252
## [2] LC_CTYPE=English_United States.1252
## [3] LC_MONETARY=English_United States.1252
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.1252
## system code page: 936
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] here_1.0.1 eplusr_0.14.2 lubridate_1.7.10 forcats_0.5.1
## [5] stringr_1.4.0 dplyr_1.0.5 purrr_0.3.4 readr_1.4.0
## [9] tidyr_1.1.3 tibble_3.1.0 ggplot2_3.3.3 tidyverse_1.3.0
##
## loaded via a namespace (and not attached):
## [1] fs_1.5.0 bit64_4.0.5 RColorBrewer_1.1-2 progress_1.2.2
## [5] httr_1.4.2 rprojroot_2.0.2 ecr_2.1.0 tools_4.1.0
## [9] backports_1.2.1 utf8_1.2.1 R6_2.5.0 lazyeval_0.2.2
## [13] DBI_1.1.1 colorspace_2.0-0 withr_2.4.1 tidyselect_1.1.0
## [17] prettyunits_1.1.1 processx_3.5.0 mco_1.15.6 bit_4.0.4
## [21] compiler_4.1.0 parallelMap_1.5.0 cli_2.3.1 rvest_1.0.0
## [25] gt_0.2.2 xml2_1.3.2 plotly_4.9.4.1 labeling_0.4.2
## [29] sass_0.3.1 scales_1.1.1 checkmate_2.0.0 plot3D_1.3
## [33] callr_3.5.1 commonmark_1.7 digest_0.6.27 rmarkdown_2.7
## [37] smoof_1.6.0.2 pkgconfig_2.0.3 htmltools_0.5.1.1 parallelly_1.24.0
## [41] dbplyr_2.1.0 fastmap_1.1.0 highr_0.8 htmlwidgets_1.5.3
## [45] rlang_0.4.10 readxl_1.3.1 rstudioapi_0.13 RSQLite_2.2.4
## [49] BBmisc_1.11 generics_0.1.0 farver_2.1.0 jsonlite_1.7.2
## [53] magrittr_2.0.1 Rcpp_1.0.6 munsell_0.5.0 fansi_0.4.2
## [57] lifecycle_1.0.0 stringi_1.5.3 yaml_2.2.1 RJSONIO_1.3-1.4
## [61] plyr_1.8.6 misc3d_0.9-0 grid_4.1.0 blob_1.2.1
## [65] listenv_0.8.0 parallel_4.1.0 crayon_1.4.1 haven_2.3.1
## [69] hms_1.0.0 knitr_1.31 ps_1.6.0 pillar_1.5.1
## [73] tcltk_4.1.0 future.apply_1.7.0 codetools_0.2-18 reshape2_1.4.4
## [77] fastmatch_1.1-0 reprex_1.0.0 glue_1.4.2 evaluate_0.14
## [81] ParamHelpers_1.14 data.table_1.14.0 modelr_0.1.8 vctrs_0.3.6
## [85] cellranger_1.1.0 gtable_0.3.0 future_1.21.0 assertthat_0.2.1
## [89] cachem_1.0.4 xfun_0.21 formattable_0.2.1 broom_0.7.5
## [93] viridisLite_0.3.0 memoise_2.0.0 units_0.7-1 globals_0.14.0
## [97] ellipsis_0.3.1