"""
Functions related to under-the-hood interactions with energyplus.
"""
# Python Core Libraries
import json
import os
import platform
import shutil
import subprocess
import tempfile
import warnings
from pathlib import Path
# External Libraries
from deprecated.sphinx import deprecated
# BESOS Imports
from besos import eppy_funcs as ef
from besos import config
from besos import objectives
from besos.errors import ModeError, InstallationError
from besos.besostypes import PathLike
# TODO: Make this a method of the building class when we add it
[docs]def get_idf_version(building):
"""Get energyplus version from idf or json building
:param building: IDF object to find version of.
:returns: String of version (ex. '9.3.0')
"""
mode = ef.get_mode(building)
if mode == "idf":
return building.idfobjects["VERSION"][0].Version_Identifier
elif mode == "json":
try:
return building["Version"]["Version 1"]["version_identifier"]
except KeyError:
try:
return building["epJSON_schema_version"]
except:
raise ModeError(
mode="json",
message=f"Cannot find IDF version, check epJSON formatting.",
)
else:
raise ModeError(mode)
[docs]def get_idf_version_pth(path):
"""Get energyplus version from idf or epJSON path
:param path: Path to idf to epJSON file.
:returns: String of version (ex. '9.3.0')
"""
path = str(path)
version_lines = (
"" # string containing the important lines of the json file to later
)
version = ""
flag = False # used to save the lines following 'Version' to version_lines
if path.endswith(".idf"):
with open(path, "r") as read_obj: # opening idf file to read
# Read all lines in the file one by one
for line in read_obj:
if flag:
version_lines += line.rstrip(
"\n"
) # adds the line after 'Version,' is seen in case of line break
break
# For each line, check if line contains 'Version,'
if ("Version," in line or "VERSION," in line) and not line.startswith(
"!"
):
version_lines += line.rstrip("\n")
flag = True
version = (
version_lines.split(";")[0].split(",")[-1].replace(" ", "").replace("=", "")
)
elif path.endswith(".epJSON"):
readobj = open(path, "r")
jsonText = readobj.read()
readobj.close()
jsonObj = json.loads(jsonText)
try:
version = jsonObj["Version"]["Version 1"]["version_identifier"]
except KeyError:
try:
version = jsonObj["epJSON_schema_version"]
except KeyError:
message = f"Version object not found within epJSON file, consider updating epJSON file\nepJSON File: {path}"
raise ModeError("json", message=message)
return version
[docs]def has_hvac_templates(building) -> bool:
"""Returns whether or not the building contains HVACTemplate objects
https://bigladdersoftware.com/epx/docs/8-0/input-output-reference/page-061.html
:param building:
:return: True if at leas one HVACTemplate object is present in the building.
"""
mode = ef.get_mode(building)
prefix = ef.convert_format("HVACTemplate", "class", mode)
if mode == "idf":
return any(
k for k, v in building.idfobjects.items() if k.startswith(prefix) and v
)
else:
return any(k for k in building if k.startswith(prefix))
[docs]def run_building(
building,
out_dir=config.out_dir,
version=None,
**eplus_args,
):
"""Run energy plus on a building object and return results
:param building_path: Path to building file
:param out_dir: Path to store EnergyPlus output files, if out_dir is not defined the results will not be saved
:param version: Version of building file
:param ep_path: Path to EnergyPlus if installed in unexpected directory
:param expand_objects: Boolean if '--expandobjects' option to be appended to command
:returns: A dictionary of EnergyPlus outputs
"""
# backwards compatibility
if version:
warnings.warn(
"the version argument is deprecated for run_building,"
" and will be removed in the future",
FutureWarning,
)
assert version == get_idf_version(building), "Incorrect version"
with tempfile.TemporaryDirectory(dir=Path.home(), prefix=".besos_") as temp_dir:
if out_dir is None:
out_dir = temp_dir
try:
building_path = Path(temp_dir, "in.idf").resolve()
building.saveas(str(building_path))
except AttributeError:
building_path = Path(temp_dir, "in.epJSON").resolve()
with open(str(building_path), "w") as f:
json.dump(building, f)
expand_objects = has_hvac_templates(building)
run_energyplus(
building_path,
out_dir=out_dir,
version=get_idf_version(building),
expand_objects=expand_objects,
**eplus_args,
)
return objectives.read_eso(out_dir)
[docs]def run_energyplus(
building_path: PathLike,
out_dir: PathLike = config.out_dir,
epw: PathLike = config.files["epw"],
err_dir: PathLike = config.err_dir,
schema_file=None,
error_mode="Silent",
version=None,
ep_path=None,
expand_objects: bool = False,
*,
stdout_mode="Silent",
):
"""Run EnergyPlus. This method is intended to work as similar to the cli tool as possible
:param building_path: Path to building file
:param out_dir: Path to store EnergyPlus output files
:param epw: Path to epw file
:param err_dir: Path to store EnergyPlus error files
:param schema_file: Path to data dictionary for EnergyPlus
:param stdout_mode: Stdout mode selection, options are "Verbose" and "Silent"
:param error_mode: Error mode selection
:param version: Version of building file
:param ep_path: Path to EnergyPlus if installed in unexpected directory
:param expand_objects: Boolean if '--expandobjects' option to be appended
:param stdout_mode: Stdout mode selection. One of {"Silent", "Verbose"} raise warning otherwise.
Silent: EnergyPlus stdout output is suppressed. This is the default.
Verbose: EnergyPlus output is printed to stdout.
:returns: None
"""
if version is None:
version = get_idf_version_pth(building_path)
ep_exe_path, ep_directory = get_ep_path(version, ep_path)
schema_file = schema_file or Path(ep_directory, "Energy+.idd")
cmd = [
ep_exe_path,
"--idd",
schema_file,
"--weather",
epw,
]
if out_dir:
cmd += ["--output-directory", out_dir]
if expand_objects:
cmd.append("--expandobjects")
cmd.append(building_path)
needs_shell = platform.system() == "Windows"
# validate stdout_mode
stdout_mode_options = {"Silent", "Verbose"}
warning = (
"Invalid value for stdout_mode. "
'Must be one of {"Silent", "Verbose"}. '
'Using default value of "Silent". '
)
stdout_arg = {"stdout": subprocess.DEVNULL}
if stdout_mode not in stdout_mode_options:
warnings.warn(warning)
elif stdout_mode == "Verbose":
stdout_arg = {}
try:
if stdout_arg:
print("Running EnergyPlus with stdout output suppressed...")
subprocess.run(cmd, check=True, shell=needs_shell, **stdout_arg)
except subprocess.CalledProcessError as e:
# TODO: This log is excessively noisy. Can we cut it down to just the command's stderr?
if error_mode != "Silent":
# print eplus error
filename = Path(out_dir, "eplusout.err")
if os.path.exists(filename):
err_file = open(filename, "r")
for line in err_file:
print(line)
print()
err_file.close()
if err_dir is not None and out_dir != err_dir:
# copy eplus error files to err_dir
if os.path.exists(err_dir):
shutil.rmtree(err_dir)
shutil.copytree(out_dir, err_dir)
raise e
[docs]def get_ep_path(idf_version, ep_path=None):
"""get EnergyPlus installation path by idf_version
:param idf_version: idf_version of EnergyPlus
:param ep_path: Optional - path to EnergyPlus if installed in different location then expected
:returns: (path to EnergyPlus executable, path to EnergyPlus)
"""
if "." in idf_version:
if idf_version == "9.0" or idf_version == "9.0.1":
idf_version = "9-0-1"
else:
idf_version = idf_version.replace(".", "-") + "-0"
if ep_path is not None:
ep_directory = os.path.abspath(ep_path)
if not os.path.exists(ep_directory):
message = f"'ep_path' does not exist.\n'ep_path': {ep_path}"
raise InstallationError(message=message)
try:
cmd = [os.path.join(ep_directory, "energyplus"), "--version"]
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()
ep_path_version = (
str(out).split("Version ")[-1].split("-")[0].replace(".", "-")
)
except:
message = f"'ep_path' provided does not contain an energyplus file\n'ep_path': {ep_path}"
raise InstallationError(message=message)
if ep_path_version != idf_version:
message = f"'ep_path' provided is a different version of EnergyPlus then 'idf_version' argument\n'ep_path': {ep_path}\n'idf_version': {idf_version}"
raise InstallationError(message=message)
if platform.system() == "Windows":
ep_exe = os.path.join(ep_directory, "energyplus.exe")
else:
ep_exe = os.path.join(ep_directory, "energyplus")
else:
# this is duplicated from eppy.runner.run_functions.paths_from_version
if platform.system() == "Windows":
ep_directory = "C:/EnergyPlusV{idf_version}".format(idf_version=idf_version)
ep_exe = os.path.join(ep_directory, "energyplus.exe")
elif platform.system() == "Linux":
ep_directory = "/usr/local/EnergyPlus-{idf_version}".format(
idf_version=idf_version
)
ep_exe = os.path.join(ep_directory, "energyplus")
else:
ep_directory = "/Applications/EnergyPlus-{idf_version}".format(
idf_version=idf_version
)
ep_exe = os.path.join(ep_directory, "energyplus")
if not os.path.exists(ep_directory) or not os.path.exists(ep_exe):
raise InstallationError(version=idf_version)
return ep_exe, ep_directory
def print_available_outputs(
building,
version=None,
name=None,
frequency=None,
):
# backwards compatibility
if version:
warnings.warn(
"the version argument is deprecated for print_available_outputs,"
" and will be removed in the future",
FutureWarning,
)
assert version == get_idf_version(building), "Incorrect version"
if name is not None:
name = name.lower()
if frequency is not None:
frequency = frequency.lower()
results = run_building(building, stdout_mode="Verbose")
for key in results.keys():
if name is not None:
if name not in key[0].lower():
continue
if frequency is not None and key[1].lower() != frequency:
continue
elif frequency is not None:
if key[1].lower() != frequency:
continue
print(list(key))