Skip to content

Commit f5a3cd4

Browse files
samuelduchesneSamuel Letellier-Duchesnezberzolla
authored
An issue simulating models with the Slab PreProcessor has been fixed (#505)
* adds IDF.copy() and IDF.saveas(inplace=True) (#254) * Adjusts svg repr to the min/max values of the schedule (#255) * Graceful warning when Slab or Basement program is not found * Adds KeyBoardInterupt to IDF Thread * catches more variations of unit name * Adds ability to scale a schedule * Fixes fallback limits for Schedule.plot2d() when Type is not defined * Type can be specified in Schedule.from_values constructor * plot2d is prettier by default * more Typing * Return existing object when new_object is there (#257) * Adds ability to replace schedule values without affecting the full load hours * more robust IDF.name property * Keep sim files when error occurs (#276) * updates requests requirement from ~=2.25.1 to >=2.26 (#292) * typo * this * gitsubmodule as https * p * pp * this * error * catch outfile * logging * Revert "logging" This reverts commit bef3e4b. * keep * as model * better logging * comments --------- Co-authored-by: Samuel Letellier-Duchesne <samueld@mit.edu> Co-authored-by: Zach Berzolla <53047789+zberzolla@users.noreply.github.com>
1 parent 5393b96 commit f5a3cd4

File tree

6 files changed

+90
-54
lines changed

6 files changed

+90
-54
lines changed

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[submodule "geomeppy"]
22
path = geomeppy
3-
url = git@github.com:samuelduchesne/geomeppy.git
3+
url = https://github.com/samuelduchesne/geomeppy.git

archetypal/eplus_interface/basement.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from tqdm.auto import tqdm
1212
from tqdm.contrib.logging import logging_redirect_tqdm
1313

14-
from ..utils import log
14+
from archetypal.utils import log
1515

1616
from ..eplus_interface.exceptions import EnergyPlusProcessError
1717

@@ -47,20 +47,17 @@ def cmd(self):
4747
def run(self):
4848
"""Wrapper around the Basement command line interface."""
4949
self.cancelled = False
50-
# get version from IDF object or by parsing the IDF file for it
5150

5251
# Move files into place
53-
# copy "%wthrfile%.epw" in.epw
5452
self.epw = self.idf.epw.copy(self.run_dir / "in.epw").expand()
5553
self.idfname = Path(self.idf.savecopy(self.run_dir / "in.idf")).expand()
5654
self.idd = self.idf.iddname.copy(self.run_dir).expand()
5755

58-
# Get executable using shutil.which (determines the extension based on
59-
# the platform, eg: .exe. And copy the executable to tmp
56+
# Get executable using shutil.which
6057
basemenet_exe = shutil.which("Basement", path=self.eplus_home)
6158
if basemenet_exe is None:
6259
log(
63-
f"The Basement program could not be found at " f"'{self.eplus_home}",
60+
f"The Basement program could not be found at '{self.eplus_home}'",
6461
lg.WARNING,
6562
)
6663
return
@@ -70,9 +67,7 @@ def run(self):
7067
self.basement_idd = (self.eplus_home / "BasementGHT.idd").copy(self.run_dir)
7168
self.outfile = self.idf.name
7269

73-
# The BasementGHTin.idf file is copied from the self.include list (
74-
# added by ExpandObjects. If self.include is empty, no need to run
75-
# Basement.
70+
# The BasementGHTin.idf file is copied from the self.include list
7671
self.include = [Path(file).copy(self.run_dir) for file in self.idf.include]
7772
if "BasementGHTIn.idf" not in self.include:
7873
self.cleanup_callback()
@@ -90,7 +85,7 @@ def run(self):
9085
self.msg_callback(f"Weather File: {self.epw}")
9186

9287
# Run Slab Program
93-
with logging_redirect_tqdm(loggers=[lg.getLogger(self.idf.name)]):
88+
with logging_redirect_tqdm(loggers=[lg.getLogger("archetypal")]):
9489
with tqdm(
9590
unit_scale=True,
9691
miniters=1,
@@ -109,10 +104,16 @@ def run(self):
109104
"Begin Basement Temperature Calculation processing . . ."
110105
)
111106

112-
for line in self.p.stdout:
113-
self.msg_callback(line.decode("utf-8").strip("\n"))
107+
# Read stdout line by line
108+
for line in iter(self.p.stdout.readline, b""):
109+
decoded_line = line.decode("utf-8").strip()
110+
self.msg_callback(decoded_line)
114111
progress.update()
115112

113+
# Process stderr after stdout is fully read
114+
stderr = self.p.stderr.read()
115+
stderr_lines = stderr.decode("utf-8").splitlines()
116+
116117
# We explicitly close stdout
117118
self.p.stdout.close()
118119

@@ -121,20 +122,21 @@ def run(self):
121122

122123
# Communicate callbacks
123124
if self.cancelled:
124-
self.msg_callback("RunSlab cancelled")
125+
self.msg_callback("Basement cancelled")
125126
# self.cancelled_callback(self.std_out, self.std_err)
126127
else:
127128
if self.p.returncode == 0:
128129
self.msg_callback(
129-
"RunSlab completed in {:,.2f} seconds".format(
130+
"Basement completed in {:,.2f} seconds".format(
130131
time.time() - start_time
131132
)
132133
)
133134
self.success_callback()
134-
for line in self.p.stderr:
135-
self.msg_callback(line.decode("utf-8"))
135+
for line in stderr_lines:
136+
self.msg_callback(line)
136137
else:
137-
self.msg_callback("RunSlab failed")
138+
self.msg_callback("Basement failed")
139+
self.msg_callback("\n".join(stderr_lines), level=lg.ERROR)
138140
self.failure_callback()
139141

140142
def msg_callback(self, *args, **kwargs):

archetypal/eplus_interface/slab.py

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import shutil
55
import subprocess
66
import time
7+
from io import StringIO
78
from threading import Thread
89

910
from packaging.version import Version
@@ -12,7 +13,6 @@
1213
from tqdm.contrib.logging import logging_redirect_tqdm
1314

1415
from archetypal.eplus_interface.exceptions import EnergyPlusProcessError
15-
from archetypal.eplus_interface.version import EnergyPlusVersion
1616
from archetypal.utils import log
1717

1818

@@ -41,59 +41,66 @@ def __init__(self, idf, tmp):
4141
@property
4242
def cmd(self):
4343
"""Get the command."""
44-
cmd_path = Path(shutil.which("Slab", path=self.run_dir))
45-
return [cmd_path]
44+
# if platform is windows
45+
return [self.slabexe]
4646

4747
def run(self):
48-
"""Wrapper around the EnergyPlus command line interface."""
48+
"""Wrapper around the Slab command line interface."""
4949
self.cancelled = False
50-
# get version from IDF object or by parsing the IDF file for it
5150

5251
# Move files into place
5352
self.epw = self.idf.epw.copy(self.run_dir / "in.epw").expand()
5453
self.idfname = Path(self.idf.savecopy(self.run_dir / "in.idf")).expand()
5554
self.idd = self.idf.iddname.copy(self.run_dir).expand()
5655

57-
# Get executable using shutil.which (determines the extension based on
58-
# the platform, eg: .exe. And copy the executable to tmp
56+
# Get executable using shutil.which
5957
slab_exe = shutil.which("Slab", path=self.eplus_home)
6058
if slab_exe is None:
6159
log(
62-
f"The Slab program could not be found at " f"'{self.eplus_home}'",
60+
f"The Slab program could not be found at '{self.eplus_home}'",
6361
lg.WARNING,
6462
)
6563
return
66-
self.slabexe = Path(slab_exe).copy(self.run_dir)
64+
else:
65+
slab_exe = (self.eplus_home / slab_exe).expand()
66+
self.slabexe = slab_exe
6767
self.slabidd = (self.eplus_home / "SlabGHT.idd").copy(self.run_dir)
68+
self.outfile = self.idf.name
6869

69-
# The GHTin.idf file is copied from the self.include list (added by
70-
# ExpandObjects. If self.include is empty, no need to run Slab.
70+
# The GHTin.idf file is copied from the self.include list
7171
self.include = [Path(file).copy(self.run_dir) for file in self.idf.include]
7272
if not self.include:
7373
self.cleanup_callback()
7474
return
7575

7676
# Run Slab Program
77-
with logging_redirect_tqdm(loggers=[lg.getLogger(self.idf.name)]):
77+
with logging_redirect_tqdm(loggers=[lg.getLogger("archetypal")]):
7878
with tqdm(
7979
unit_scale=True,
8080
miniters=1,
81-
desc=f"RunSlab #{self.idf.position}-{self.idf.name}",
81+
desc=f"{self.slabexe} #{self.idf.position}-{self.idf.name}",
8282
position=self.idf.position,
8383
) as progress:
8484
self.p = subprocess.Popen(
8585
self.cmd,
8686
stdout=subprocess.PIPE,
8787
stderr=subprocess.PIPE,
88-
shell=True, # can use shell
89-
cwd=self.run_dir.abspath(),
88+
shell=False,
89+
cwd=self.run_dir,
9090
)
9191
start_time = time.time()
9292
self.msg_callback("Begin Slab Temperature Calculation processing . . .")
93-
for line in self.p.stdout:
94-
self.msg_callback(line.decode("utf-8").strip("\n"))
93+
94+
# Read stdout line by line
95+
for line in iter(self.p.stdout.readline, b""):
96+
decoded_line = line.decode("utf-8").strip()
97+
self.msg_callback(decoded_line)
9598
progress.update()
9699

100+
# Process stderr after stdout is fully read
101+
stderr = self.p.stderr.read()
102+
stderr_lines = stderr.decode("utf-8").splitlines()
103+
97104
# We explicitly close stdout
98105
self.p.stdout.close()
99106

@@ -102,20 +109,20 @@ def run(self):
102109

103110
# Communicate callbacks
104111
if self.cancelled:
105-
self.msg_callback("RunSlab cancelled")
106-
# self.cancelled_callback(self.std_out, self.std_err)
112+
self.msg_callback("Slab cancelled")
107113
else:
108114
if self.p.returncode == 0:
109115
self.msg_callback(
110-
"RunSlab completed in {:,.2f} seconds".format(
116+
"Slab completed in {:,.2f} seconds".format(
111117
time.time() - start_time
112118
)
113119
)
114120
self.success_callback()
115-
for line in self.p.stderr:
116-
self.msg_callback(line.decode("utf-8"))
121+
for line in stderr_lines:
122+
self.msg_callback(line)
117123
else:
118-
self.msg_callback("RunSlab failed")
124+
self.msg_callback("Slab failed", level=lg.ERROR)
125+
self.msg_callback("\n".join(stderr_lines), level=lg.ERROR)
119126
self.failure_callback()
120127

121128
def msg_callback(self, *args, **kwargs):
@@ -124,16 +131,27 @@ def msg_callback(self, *args, **kwargs):
124131

125132
def success_callback(self):
126133
"""Parse surface temperature and append to IDF file."""
127-
temp_schedule = self.run_dir / "SLABSurfaceTemps.txt"
128-
if temp_schedule.exists():
129-
with open(self.idf.idfname, "a") as outfile:
130-
with open(temp_schedule) as infile:
131-
next(infile) # Skipping first line
132-
next(infile) # Skipping second line
133-
for line in infile:
134-
outfile.write(line)
135-
# invalidate attributes dependant on idfname, since it has changed
136-
self.idf._reset_dependant_vars("idfname")
134+
for temp_schedule in self.run_dir.glob("SLABSurfaceTemps*"):
135+
if temp_schedule.exists():
136+
slab_models = self.idf.__class__(
137+
StringIO(open(temp_schedule, "r").read()),
138+
file_version=self.idf.file_version,
139+
as_version=self.idf.as_version,
140+
prep_outputs=False,
141+
)
142+
# Loop on all objects and using self.newidfobject
143+
added_objects = []
144+
for sequence in slab_models.idfobjects.values():
145+
if sequence:
146+
for obj in sequence:
147+
data = obj.to_dict()
148+
key = data.pop("key")
149+
added_objects.append(
150+
self.idf.newidfobject(key=key.upper(), **data)
151+
)
152+
del slab_models # remove loaded_string model
153+
else:
154+
self.msg_callback("No SLABSurfaceTemps.txt file found.", level=lg.ERROR)
137155
self.cleanup_callback()
138156

139157
def cleanup_callback(self):

archetypal/idfclass/idf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1485,7 +1485,8 @@ def simulate(self, force=False, **kwargs):
14851485
except KeyboardInterrupt:
14861486
slab_thread.stop()
14871487
finally:
1488-
tmp.rmtree(ignore_errors=True)
1488+
if not self.keep_data_err:
1489+
tmp.rmtree(ignore_errors=True)
14891490
e = slab_thread.exception
14901491
if e is not None:
14911492
raise e

archetypal/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,13 @@ def get_logger(level=None, name=None, filename=None, log_dir=None):
163163
todays_date = dt.datetime.today().strftime("%Y_%m_%d")
164164

165165
if not log_dir:
166-
log_dir = settings.logs_folder
166+
log_dir: Path = settings.logs_folder
167167

168168
log_filename = log_dir / "{}_{}.log".format(filename, todays_date)
169169

170170
# if the logs folder does not already exist, create it
171171
if not log_dir.exists():
172-
log_dir.makedirs_p()
172+
os.mkdir(log_dir)
173173
# create file handler and log formatter and set them up
174174
formatter = lg.Formatter(
175175
"%(asctime)s [%(process)d] %(levelname)s - %(name)s - %(" "message)s"

tests/test_idfclass.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ def test_copy_saveas(self, idf_model, tmp_path):
7878
# assert saveas returns another object
7979
assert idf_copy.saveas(tmp_path / "in.idf", inplace=False) is not idf_copy
8080

81+
def test_copy_saveas(self, idf_model, tmp_path):
82+
"""Test making a copy of self and two ways of saving as (inplace or not)."""
83+
idf_copy = idf_model.copy() # make a copy of self
84+
85+
assert idf_copy is not idf_model
86+
87+
# assert saveas modifies self inplace.
88+
id_before = id(idf_copy)
89+
idf_copy.saveas(tmp_path / "in.idf", inplace=True)
90+
id_after = id(idf_copy)
91+
assert id_after == id_before
92+
93+
# assert saveas returns another object
94+
assert idf_copy.saveas(tmp_path / "in.idf", inplace=False) is not idf_copy
95+
8196
def test_default_version_none(self):
8297
file = (
8398
"tests/input_data/necb/NECB 2011-FullServiceRestaurant-NECB HDD "

0 commit comments

Comments
 (0)