Hongyuan Jia|贾洪愿

A Blog of Hongyuan Jia

Update EnergyPlus using eplusr transition

Hongyuan Jia / 2019-08-26


Motivation

I often encounter a situation that I have to update my old EnergyPlus models to a newer version. EnergyPlus provides a preprocessor call IDFVersionUpdater to do this. IDFVersionUpdater is written in Fortran and the program itself envolves along with EnergyPlus. Once a new version of EnergyPlus is published, IDFVersionUpdater will add the support of that version.

IDFVersionUpdater itself is good. At least, it works. Well, for most of the time. The original IDFVersionUpdater program is a GUI program and does not provide command line interface. This makes it hard to update lots of IDF files programmatically. The good news is that IDFVersionUpdater itself Since is also a wrapper to call different standlone transition program for each single version update. We can easily develop our own wrapper in R. For example, when updating your model from EnergyPlus v8.7 to v8.9, Transition-V8-7-0-to-V8-8-0 will be called, and then Transition-V8-8-0-to-V8-9-0.

Introduce version_updater()

version_updater() is eplusr’s version of IDFVersionUpdater. Below is the simplified pseudo code for version_updater(). The actual code can be found in the eplusr GitHub repo

version_updater <- function (idf, ver, dir = NULL, keep_all = FALSE) {
    # parse file
    if (!is_idf(idf)) idf <- read_idf(idf)

    if (is.null(dir)) {
        dir <- dirname(idf$path())
    } else if (!dir.exists(dir)){
        dir.create(dir, recursive = TRUE)
    }

    latest_ver <- avail_eplus()[avail_eplus()[, 1:2] >= ver[, 1:2]]
    if (!length(latest_ver)) {
        stop("error_updater_not_avail", paste0("EnergyPlus v", ver, " or newer are not installed."))
    }

    # save the original file with trailing version number
    original <- paste0(tools::file_path_sans_ext(basename(idf$path())), "V", idf$version()[, 1L], idf$version()[, 2L], "0.idf")
    # clone original
    idf <- idf$clone(TRUE)
    idf$save(file.path(dir, original), overwrite = TRUE)

    # get the directory of IDFVersionUpdater
    # avoid to use IDFVersionUpdater v9.0 as there are fital errors
    if (length(latest_ver[latest_ver[, 1:2] != 9.0])) latest_ver <- latest_ver[latest_ver[, 1:2] != 9.0]
    path_updater <- file.path(eplus_config(max(latest_ver))$dir, "PreProcess/IDFVersionUpdater")

    # get upper versions toward target version
    vers <- trans_upper_versions(idf, ver)

    # get fun names
    exe <- if (is_windows()) ".exe" else NULL
    from <- vers[-length(vers)]
    to <- vers[-1L]
    trans_exe <- paste0("Transition-",
        "V", from[, 1L], "-", from[, 2L], "-0", "-to-",
        "V",   to[, 1L], "-",   to[, 2L], "-0", exe
    )

    while (idf$version()[, 1:2] != max(to)[, 1:2]) {
        # restore paths
        paths[names(paths) == as.character(idf$version()[, 1:2])] <- idf$path()

        # restore models
        models[names(models) == as.character(idf$version()[, 1:2])] <- list(idf)

        # get transition program path
        current_exe <- trans_exe[from[, 1:2] == idf$version()[, 1:2]]
        toward <- to[from[, 1:2] == idf$version()[, 1:2]]

        trans_path <- file.path(path_updater, current_exe)

        job <- processx::run(trans_path, idf$path(), wd = path_updater)

        # delete the new IDF file with old name since there is another new IDF file
        # with ".idfold" extenstion
        unlink(idf$path(), force = TRUE)

        # read error file
        path_err <- paste0(tools::file_path_sans_ext(idf$path()), ".VCpErr")
        err <- read_err(path_err)
        # remove VCpErr file generated
        unlink(path_err, force = TRUE)

        # rename the old file
        file.rename(paste0(tools::file_path_sans_ext(idf$path()), ".idfold"), idf$path())

        # name of the new file
        path_new <- paste0(tools::file_path_sans_ext(idf$path()), ".idfnew")
        # replace the old Idf object
        idf <- read_idf(path_new)

        # resave using eplusr
        new_name <- paste0(stri_sub(tools::file_path_sans_ext(path_new), to = -4L), toward[, 1L], toward[, 2L], "0.idf")
        idf$save(new_name, overwrite = TRUE)
        # rename the orignal new file
        unlink(path_new, force = TRUE)

        # remove log file generated
        unlink(file.path(path_updater, c("fort.6", "Energy+.ini", "Transition.audit")), force = TRUE)

        # restore paths
        paths[names(paths) == as.character(idf$version()[, 1:2])] <- idf$path()

        # restore models
        models[names(models) == as.character(idf$version()[, 1:2])] <- list(idf)

        # restore errors
        errors[names(errors) == as.character(idf$version()[, 1:2])] <- list(err)
    }

    if (!keep_all) {
        unlink(paths[-length(paths)], force = TRUE)
        models <- models[[length(models)]]
    }

    attr(models, "errors") <- errors
    models
}

version_updater() itself is not complicated. Compared to IDFVersionUpdater, version_updater() has some improvements:

Slow

So far so good. version_updater() works as expected as IDFVersionUpdater.

path <- file.path(eplus_config(8.4)$dir, "ExampleFiles/RefBldgLargeOfficeNew2004_Chicago.idf")

However, I found that even though the transition programs are written in Fortran, it could take several minutes to complete, which surprises me a little bit.

(t_energyplus <- system.time(energyplus <- version_updater(path, 9.1)))
##    user  system elapsed 
##   30.20    4.78  231.50

I know nothing about Fortran and I am unable to discover the reason why it runs so slow and to improve it. So I decided to dig a litter bit deeper to find how the transition works and tries to develop my own transition function to do the task.

How the transition works

When I first try to develop the transition function, I am a little bit too ambitious. I planned to develop version downgrade support. However, after I read the source code of IDFVersionUpdater, it turns out to be impossible. Because during version updates, some classes are removed, some classes are splited into several classes, some fields are removed and there is no way to extract those information back. For example, from EnergyPlus v8.8 to v8.9, one single object in class GroundHeatExchanger:Vertical will be splitted into four different objects in class GroundHeatExchanger:System, GroundHeatExchanger:Vertical:Properties, Site:GroundTemperature:Undisturbed:KusudaAchenbach, and GroundHeatExchanger:ResponseFactors, respectively.

A basic transition on a single class is a combination of differnt transition action. Below I will describe most typical ones together with the related transition program source code:

The combinations of those four actions together builds a skeleton of an EnergyPlus transition program, together with some pre-processes and post-processes.

In eplusr, all IDF data are stored as data.tables. So transition means to write a function to perform the similar actions on field values stored in data.tables. I wrote a function called trans_action() for this purpose.

For example, the equivalent transition implemented in R for Exterior:FuelEquipment objects demonstrated in the Reset code block would be:

# Insert
dt1 <- trans_action(idf, "OtherEquipment", insert = list(2L, "None"))

# Reset
dt2 <- trans_action(idf, "Exterior:FuelEquipment",
    reset = list(2L, "Gas", "NaturalGas"),
    reset = list(2L, "LPG", "PropaneGas")
)

# Delete
dt3 <- trans_action(idf, "HVACTemplate:System:Unitary", delete = list(40L))

# Offset
dt4 <- trans_action(idf, "Daylighting:Controls",
    offset = list(20L, 4L),
    reset = list(3, "SplitFlux")
)

Having the updated IDF data, we can then easily insert them to the next new version of IDF by simply doing new_idf$load(dt).

Introduce transition()

Having a clear mental model on how the transition works, the most cumbersome work left is to translate all those actions written in Fortran into R and write tests to make sure that the R implement should be the same as IDFVersionUpdater. It takes me days to do that. In the end, the transition.R has more than 2700 lines. You can try it out using by install the development of eplusr doing remotes::install_github("hongyuanjia/eplusr").

transition() takes similar arguments as version_updater() except it has an additional argument save to control wheter to save the resultant Idf objects to IDF files or not.

(t_eplusr <- system.time(eplusr <- transition(path, 9.1)))
##    user  system elapsed 
##    8.56    0.82    9.66

It is about 24 times faster. To be honest, not that fast as I expected. Lots of time spent on parsing all new Idd objects. After that, the transition should be a little bit faster.

Fast is always good. However, the most important thing is to ensure that transition() can provide reasonable results. Here we compare the results.

As we can see from below, there are several differences between results from transition() and version_updater():

diffobj::diffChr(eplusr$to_string(), energyplus$to_string(), mode = "sidebyside")
@@ 167,5 @@
@@ 167,5 @@
 
!- =========== ALL OBJECTS IN CLASS: SIZINGPERIOD:DESIGNDAY ===========
 
!- =========== ALL OBJECTS IN CLASS: SIZINGPERIOD:DESIGNDAY ===========
 
 
 
 
<
! CHICAGO_IL_USA Annual Heating 99.6%, MaxDB=-20.6°C
>
! CHICAGO_IL_USA Annual Heating 99.6%, MaxDB=-20.6闂傚倷鑳堕、濠傗枍閿濆鍋傞柨鐕傛嫹
 
SizingPeriod:DesignDay,
 
SizingPeriod:DesignDay,
 
CHICAGO Ann Htg 99.6% Condns DB, !- Name
 
CHICAGO Ann Htg 99.6% Condns DB, !- Name
@@ 196,5 @@
@@ 196,5 @@
 
0; !- Sky Clearness
 
0; !- Sky Clearness
 
 
 
 
<
! CHICAGO_IL_USA Annual Cooling (WB=>MDB) .4%, MDB=31.2°C WB=25.5°C
>
! CHICAGO_IL_USA Annual Cooling (WB=>MDB) .4%, MDB=31.2闂傚倷鑳堕、濠傗枍閿濆鍋傞柨鐕傛嫹 WB=25.5闂傚倷鑳堕、濠傗枍閿濆鍋傞柨鐕傛嫹
 
SizingPeriod:DesignDay,
 
SizingPeriod:DesignDay,
 
CHICAGO Ann Clg .4% Condns WB=>MDB, !- Name
 
CHICAGO Ann Clg .4% Condns WB=>MDB, !- Name
@@ 241,5 @@
@@ 241,6 @@
 
No, !- Apply Weekend Holiday Rule
 
No, !- Apply Weekend Holiday Rule
 
Yes, !- Use Weather File Rain Indicators
 
Yes, !- Use Weather File Rain Indicators
<
Yes; !- Use Weather File Snow Indicators
>
Yes, !- Use Weather File Snow Indicators
~
>
; !- Treat Weather as Actual
 
 
 
 
 
 
 
 
@@ 10873,30 @@
@@ 10874,6 @@
 
Carbon Equivalent:Facility, !- Variable or Meter 7 Name
 
Carbon Equivalent:Facility, !- Variable or Meter 7 Name
 
SumOrAverage; !- Aggregation Type for Variable or Meter 7
 
SumOrAverage; !- Aggregation Type for Variable or Meter 7
<
 
~
<
Output:Table:Monthly,
~
<
Boiler Part Load Performance, !- Name
~
<
2, !- Digits After Decimal
~
<
Boiler Part Load Ratio, !- Variable or Meter 1 Name
~
<
SumOrAverage, !- Aggregation Type for Variable or Meter 1
~
<
Boiler Part Load Ratio, !- Variable or Meter 2 Name
~
<
Maximum; !- Aggregation Type for Variable or Meter 2
~
<
 
~
<
Output:Table:Monthly,
~
<
Chiller Part Load Performance, !- Name
~
<
2, !- Digits After Decimal
~
<
Chiller Part Load Ratio, !- Variable or Meter 1 Name
~
<
SumOrAverage, !- Aggregation Type for Variable or Meter 1
~
<
Chiller Part Load Ratio, !- Variable or Meter 2 Name
~
<
Maximum; !- Aggregation Type for Variable or Meter 2
~
 
 
 
 
 
Output:Table:Monthly,
 
Output:Table:Monthly,
<
Fan Part Load Performance, !- Name
~
<
0, !- Digits After Decimal
~
<
Fan Electric Power, !- Variable or Meter 1 Name
~
<
SumOrAverage, !- Aggregation Type for Variable or Meter 1
~
<
Fan Electric Power, !- Variable or Meter 2 Name
~
<
Maximum; !- Aggregation Type for Variable or Meter 2
~
<
 
~
<
Output:Table:Monthly,
~
 
Components of Peak Electrical Demand, !- Name
 
Components of Peak Electrical Demand, !- Name
 
3, !- Digits After Decimal
 
3, !- Digits After Decimal
@@ 10936,5 @@
@@ 10913,29 @@
 
ValueWhenMaximumOrMinimum; !- Aggregation Type for Variable or Meter 17
 
ValueWhenMaximumOrMinimum; !- Aggregation Type for Variable or Meter 17
 
 
 
 
~
>
Output:Table:Monthly,
~
>
Boiler Part Load Performance, !- Name
~
>
2, !- Digits After Decimal
~
>
Boiler Part Load Ratio, !- Variable or Meter 1 Name
~
>
SumOrAverage, !- Aggregation Type for Variable or Meter 1
~
>
Boiler Part Load Ratio, !- Variable or Meter 2 Name
~
>
Maximum; !- Aggregation Type for Variable or Meter 2
 
 
 
 
~
>
Output:Table:Monthly,
~
>
Chiller Part Load Performance, !- Name
~
>
2, !- Digits After Decimal
~
>
Chiller Part Load Ratio, !- Variable or Meter 1 Name
~
>
SumOrAverage, !- Aggregation Type for Variable or Meter 1
~
>
Chiller Part Load Ratio, !- Variable or Meter 2 Name
~
>
Maximum; !- Aggregation Type for Variable or Meter 2
~
>
 
~
>
Output:Table:Monthly,
~
>
Fan Part Load Performance, !- Name
~
>
0, !- Digits After Decimal
~
>
Fan Electric Power, !- Variable or Meter 1 Name
~
>
SumOrAverage, !- Aggregation Type for Variable or Meter 1
~
>
Fan Electric Power, !- Variable or Meter 2 Name
~
>
Maximum; !- Aggregation Type for Variable or Meter 2
~
>
 
~
>
 
 
!- =========== ALL OBJECTS IN CLASS: OUTPUTCONTROL:TABLE:STYLE ===========
 
!- =========== ALL OBJECTS IN CLASS: OUTPUTCONTROL:TABLE:STYLE ===========