diff --git a/.gitmodules b/.gitmodules index 528df87b..80472217 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "geomeppy"] path = geomeppy - url = git@github.com:samuelduchesne/geomeppy.git + url = https://github.com/samuelduchesne/geomeppy.git diff --git a/archetypal/eplus_interface/basement.py b/archetypal/eplus_interface/basement.py index c3be755b..b6e8f5d7 100644 --- a/archetypal/eplus_interface/basement.py +++ b/archetypal/eplus_interface/basement.py @@ -50,7 +50,6 @@ def run(self): # get version from IDF object or by parsing the IDF file for it # Move files into place - # copy "%wthrfile%.epw" in.epw self.epw = self.idf.epw.copy(self.run_dir / "in.epw").expand() self.idfname = Path(self.idf.savecopy(self.run_dir / "in.idf")).expand() self.idd = self.idf.iddname.copy(self.run_dir).expand() @@ -60,7 +59,7 @@ def run(self): basemenet_exe = shutil.which("Basement", path=self.eplus_home) if basemenet_exe is None: log( - f"The Basement program could not be found at " f"'{self.eplus_home}", + f"The Basement program could not be found at '{self.eplus_home}'", lg.WARNING, ) return @@ -90,7 +89,7 @@ def run(self): self.msg_callback(f"Weather File: {self.epw}") # Run Slab Program - with logging_redirect_tqdm(loggers=[lg.getLogger(self.idf.name)]): + with logging_redirect_tqdm(loggers=[lg.getLogger("archetypal")]): with tqdm( unit_scale=True, miniters=1, @@ -109,10 +108,16 @@ def run(self): "Begin Basement Temperature Calculation processing . . ." ) - for line in self.p.stdout: - self.msg_callback(line.decode("utf-8").strip("\n")) + # Read stdout line by line + for line in iter(self.p.stdout.readline, b""): + decoded_line = line.decode("utf-8").strip() + self.msg_callback(decoded_line) progress.update() + # Process stderr after stdout is fully read + stderr = self.p.stderr.read() + stderr_lines = stderr.decode("utf-8").splitlines() + # We explicitly close stdout self.p.stdout.close() @@ -121,20 +126,21 @@ def run(self): # Communicate callbacks if self.cancelled: - self.msg_callback("RunSlab cancelled") + self.msg_callback("Basement cancelled") # self.cancelled_callback(self.std_out, self.std_err) else: if self.p.returncode == 0: self.msg_callback( - "RunSlab completed in {:,.2f} seconds".format( + "Basement completed in {:,.2f} seconds".format( time.time() - start_time ) ) self.success_callback() - for line in self.p.stderr: - self.msg_callback(line.decode("utf-8")) + for line in stderr_lines: + self.msg_callback(line) else: - self.msg_callback("RunSlab failed") + self.msg_callback("Basement failed") + self.msg_callback("\n".join(stderr_lines), level=lg.ERROR) self.failure_callback() def msg_callback(self, *args, **kwargs): diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index 64fbca9c..6341db13 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -4,6 +4,7 @@ import shutil import subprocess import time +from io import StringIO from threading import Thread from packaging.version import Version @@ -12,7 +13,6 @@ from tqdm.contrib.logging import logging_redirect_tqdm from archetypal.eplus_interface.exceptions import EnergyPlusProcessError -from archetypal.eplus_interface.version import EnergyPlusVersion from archetypal.utils import log @@ -41,59 +41,66 @@ def __init__(self, idf, tmp): @property def cmd(self): """Get the command.""" - cmd_path = Path(shutil.which("Slab", path=self.run_dir)) - return [cmd_path] + # if platform is windows + return [self.slabexe] def run(self): - """Wrapper around the EnergyPlus command line interface.""" + """Wrapper around the Slab command line interface.""" self.cancelled = False - # get version from IDF object or by parsing the IDF file for it # Move files into place self.epw = self.idf.epw.copy(self.run_dir / "in.epw").expand() self.idfname = Path(self.idf.savecopy(self.run_dir / "in.idf")).expand() self.idd = self.idf.iddname.copy(self.run_dir).expand() - # Get executable using shutil.which (determines the extension based on - # the platform, eg: .exe. And copy the executable to tmp + # Get executable using shutil.which slab_exe = shutil.which("Slab", path=self.eplus_home) if slab_exe is None: log( - f"The Slab program could not be found at " f"'{self.eplus_home}'", + f"The Slab program could not be found at '{self.eplus_home}'", lg.WARNING, ) return - self.slabexe = Path(slab_exe).copy(self.run_dir) + else: + slab_exe = (self.eplus_home / slab_exe).expand() + self.slabexe = slab_exe self.slabidd = (self.eplus_home / "SlabGHT.idd").copy(self.run_dir) + self.outfile = self.idf.name - # The GHTin.idf file is copied from the self.include list (added by - # ExpandObjects. If self.include is empty, no need to run Slab. + # The GHTin.idf file is copied from the self.include list self.include = [Path(file).copy(self.run_dir) for file in self.idf.include] if not self.include: self.cleanup_callback() return # Run Slab Program - with logging_redirect_tqdm(loggers=[lg.getLogger(self.idf.name)]): + with logging_redirect_tqdm(loggers=[lg.getLogger("archetypal")]): with tqdm( unit_scale=True, miniters=1, - desc=f"RunSlab #{self.idf.position}-{self.idf.name}", + desc=f"{self.slabexe} #{self.idf.position}-{self.idf.name}", position=self.idf.position, ) as progress: self.p = subprocess.Popen( self.cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True, # can use shell - cwd=self.run_dir.abspath(), + shell=False, + cwd=self.run_dir, ) start_time = time.time() self.msg_callback("Begin Slab Temperature Calculation processing . . .") - for line in self.p.stdout: - self.msg_callback(line.decode("utf-8").strip("\n")) + + # Read stdout line by line + for line in iter(self.p.stdout.readline, b""): + decoded_line = line.decode("utf-8").strip() + self.msg_callback(decoded_line) progress.update() + # Process stderr after stdout is fully read + stderr = self.p.stderr.read() + stderr_lines = stderr.decode("utf-8").splitlines() + # We explicitly close stdout self.p.stdout.close() @@ -102,20 +109,20 @@ def run(self): # Communicate callbacks if self.cancelled: - self.msg_callback("RunSlab cancelled") - # self.cancelled_callback(self.std_out, self.std_err) + self.msg_callback("Slab cancelled") else: if self.p.returncode == 0: self.msg_callback( - "RunSlab completed in {:,.2f} seconds".format( + "Slab completed in {:,.2f} seconds".format( time.time() - start_time ) ) self.success_callback() - for line in self.p.stderr: - self.msg_callback(line.decode("utf-8")) + for line in stderr_lines: + self.msg_callback(line) else: - self.msg_callback("RunSlab failed") + self.msg_callback("Slab failed", level=lg.ERROR) + self.msg_callback("\n".join(stderr_lines), level=lg.ERROR) self.failure_callback() def msg_callback(self, *args, **kwargs): @@ -124,16 +131,27 @@ def msg_callback(self, *args, **kwargs): def success_callback(self): """Parse surface temperature and append to IDF file.""" - temp_schedule = self.run_dir / "SLABSurfaceTemps.txt" - if temp_schedule.exists(): - with open(self.idf.idfname, "a") as outfile: - with open(temp_schedule) as infile: - next(infile) # Skipping first line - next(infile) # Skipping second line - for line in infile: - outfile.write(line) - # invalidate attributes dependant on idfname, since it has changed - self.idf._reset_dependant_vars("idfname") + for temp_schedule in self.run_dir.glob("SLABSurfaceTemps*"): + if temp_schedule.exists(): + slab_models = self.idf.__class__( + StringIO(open(temp_schedule, "r").read()), + file_version=self.idf.file_version, + as_version=self.idf.as_version, + prep_outputs=False, + ) + # Loop on all objects and using self.newidfobject + added_objects = [] + for sequence in slab_models.idfobjects.values(): + if sequence: + for obj in sequence: + data = obj.to_dict() + key = data.pop("key") + added_objects.append( + self.idf.newidfobject(key=key.upper(), **data) + ) + del slab_models # remove loaded_string model + else: + self.msg_callback("No SLABSurfaceTemps.txt file found.", level=lg.ERROR) self.cleanup_callback() def cleanup_callback(self): diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index 66d1ec83..5ea1655a 100644 --- a/archetypal/idfclass/idf.py +++ b/archetypal/idfclass/idf.py @@ -383,9 +383,10 @@ def from_example_files(cls, example_name, epw=None, **kwargs): from pathlib import Path as Pathlib example_name = Path(example_name) - example_files_dir: Path = ( - EnergyPlusVersion.current().current_install_dir / "ExampleFiles" + eplus_version = EnergyPlusVersion( + kwargs.get("as_version", EnergyPlusVersion.current()) ) + example_files_dir: Path = eplus_version.current_install_dir / "ExampleFiles" try: file = next( iter(Pathlib(example_files_dir).rglob(f"{example_name.stem}.idf")) @@ -399,9 +400,7 @@ def from_example_files(cls, example_name, epw=None, **kwargs): epw = Path(epw) if not epw.exists(): - dir_weather_data_ = ( - EnergyPlusVersion.current().current_install_dir / "WeatherData" - ) + dir_weather_data_ = eplus_version.current_install_dir / "WeatherData" try: epw = next( iter(Pathlib(dir_weather_data_).rglob(f"{epw.stem}.epw")) @@ -1485,7 +1484,8 @@ def simulate(self, force=False, **kwargs): except KeyboardInterrupt: slab_thread.stop() finally: - tmp.rmtree(ignore_errors=True) + if not self.keep_data_err: + tmp.rmtree(ignore_errors=True) e = slab_thread.exception if e is not None: raise e diff --git a/archetypal/utils.py b/archetypal/utils.py index 7d2b8166..4cd12639 100644 --- a/archetypal/utils.py +++ b/archetypal/utils.py @@ -163,13 +163,13 @@ def get_logger(level=None, name=None, filename=None, log_dir=None): todays_date = dt.datetime.today().strftime("%Y_%m_%d") if not log_dir: - log_dir = settings.logs_folder + log_dir: Path = settings.logs_folder log_filename = log_dir / "{}_{}.log".format(filename, todays_date) # if the logs folder does not already exist, create it if not log_dir.exists(): - log_dir.makedirs_p() + os.mkdir(log_dir) # create file handler and log formatter and set them up formatter = lg.Formatter( "%(asctime)s [%(process)d] %(levelname)s - %(name)s - %(" "message)s" diff --git a/tests/test_idfclass.py b/tests/test_idfclass.py index f3db7bf7..11ed7888 100644 --- a/tests/test_idfclass.py +++ b/tests/test_idfclass.py @@ -78,6 +78,21 @@ def test_copy_saveas(self, idf_model, tmp_path): # assert saveas returns another object assert idf_copy.saveas(tmp_path / "in.idf", inplace=False) is not idf_copy + def test_copy_saveas(self, idf_model, tmp_path): + """Test making a copy of self and two ways of saving as (inplace or not).""" + idf_copy = idf_model.copy() # make a copy of self + + assert idf_copy is not idf_model + + # assert saveas modifies self inplace. + id_before = id(idf_copy) + idf_copy.saveas(tmp_path / "in.idf", inplace=True) + id_after = id(idf_copy) + assert id_after == id_before + + # assert saveas returns another object + assert idf_copy.saveas(tmp_path / "in.idf", inplace=False) is not idf_copy + def test_default_version_none(self): file = ( "tests/input_data/necb/NECB 2011-FullServiceRestaurant-NECB HDD "