From f6b9c53c5c85b3644f83c00b468320bc80c351b9 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 3 Dec 2021 14:03:33 -0500 Subject: [PATCH 01/30] adds IDF.copy() and IDF.saveas(inplace=True) (#254) --- archetypal/idfclass/idf.py | 35 +++++++++++++++++++++++++++++++---- tests/test_idfclass.py | 15 +++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index 57b0aa611..04683a13e 100644 --- a/archetypal/idfclass/idf.py +++ b/archetypal/idfclass/idf.py @@ -3,7 +3,7 @@ Various functions for processing EnergyPlus models and retrieving results in different forms. """ - +import io import itertools import logging as lg import math @@ -333,6 +333,10 @@ def __repr__(self): body += sim_info return f"<{body}>" + def __copy__(self): + """Get a copy of self.""" + return self.copy() + @classmethod def from_example_files(cls, example_name, epw=None, **kwargs): """Load an IDF model from the ExampleFiles folder by name. @@ -1427,6 +1431,15 @@ def savecopy(self, filename, lineendings="default", encoding="latin-1"): super(IDF, self).save(filename, lineendings, encoding) return Path(filename) + def copy(self): + """Return a copy of self as an in memory IDF. + + The copy is a new IDF object with the same parameters and arguments as self + but is not attached to an file. Use IDF.saveas("idfname.idf", inplace=True) + to save the copy to a file inplace. self.idfname will now be idfname.idf + """ + return self.saveas(io.StringIO("")) + def save(self, lineendings="default", encoding="latin-1", **kwargs): """Write the IDF model to the text file. @@ -1448,7 +1461,9 @@ def save(self, lineendings="default", encoding="latin-1", **kwargs): log(f"saved '{self.name}' at '{self.idfname}'") return self - def saveas(self, filename, lineendings="default", encoding="latin-1"): + def saveas( + self, filename, lineendings="default", encoding="latin-1", inplace=False + ): """Save the IDF model as. Writes a new text file and load a new instance of the IDF class (new object). @@ -1461,6 +1476,8 @@ def saveas(self, filename, lineendings="default", encoding="latin-1"): the line endings for the current system. encoding (str): Encoding to use for the saved file. The default is 'latin-1' which is compatible with the EnergyPlus IDFEditor. + inplace (bool): If True, applies the new filename to self directly, + else a new object is returned with the new filename. Returns: IDF: A new IDF object based on the new location file. @@ -1490,8 +1507,18 @@ def saveas(self, filename, lineendings="default", encoding="latin-1"): name = Path(name).basename() else: name = file.basename() - file.copy(as_idf.simulation_dir / name) - return as_idf + try: + file.copy(as_idf.simulation_dir / name) + except shutil.SameFileError: + # A copy of self would have the same files in the simdir and + # throw an error. + pass + if inplace: + # If inplace, replace content of self with content of as_idf. + self.__dict__.update(as_idf.__dict__) + else: + # return the new object. + return as_idf def process_results(self): """Return the list of processed results. diff --git a/tests/test_idfclass.py b/tests/test_idfclass.py index 95212c413..3ed8a61b0 100644 --- a/tests/test_idfclass.py +++ b/tests/test_idfclass.py @@ -72,6 +72,21 @@ def wont_transition_correctly(self, config): wf = "tests/input_data/CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw" yield IDF(file, epw=wf, as_version="8.9.0") + 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 " From 6ec65a171c12861b64043692d56961d917f48816 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 3 Dec 2021 14:03:52 -0500 Subject: [PATCH 02/30] Adjusts svg repr to the min/max values of the schedule (#255) --- archetypal/schedule.py | 15 +++++++++++++-- archetypal/settings.py | 7 ++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/archetypal/schedule.py b/archetypal/schedule.py index 89cd6a1c5..5a8c5b1fc 100644 --- a/archetypal/schedule.py +++ b/archetypal/schedule.py @@ -1294,7 +1294,11 @@ def series(self): index = pd.date_range( start=self.startDate, periods=self.all_values.size, freq="1H" ) - return EnergySeries(self.all_values, index=index, name=self.Name) + if self.Type is not None: + units = self.Type.UnitType + else: + units = None + return EnergySeries(self.all_values, index=index, name=self.Name, units=units) @staticmethod def get_schedule_type_limits_name(epbunch): @@ -1489,7 +1493,14 @@ def __mul__(self, other): def _repr_svg_(self): """SVG representation for iPython notebook.""" - fig, ax = self.series.plot2d(cmap="Greys", show=False, figsize=(7, 2), dpi=72) + if self.Type is not None: + vmin = self.Type.LowerLimit + vmax = self.Type.UpperLimit + else: + vmin, vmax = (0, 0) + fig, ax = self.series.plot2d( + cmap="Greys", show=False, figsize=(7, 2), dpi=72, vmin=vmin, vmax=vmax + ) f = io.BytesIO() fig.savefig(f, format="svg") return f.getvalue() diff --git a/archetypal/settings.py b/archetypal/settings.py index fd6a29561..7bef8cbe5 100644 --- a/archetypal/settings.py +++ b/archetypal/settings.py @@ -128,7 +128,12 @@ from energy_pandas.units import unit_registry -unit_registry = unit_registry +additional_units = ( + "Dimensionless = dimensionless = Fraction", + "@alias degC = Temperature", +) +for unit in additional_units: + unit_registry.define(unit) class ZoneWeight(object): From 0ef37ee282eb38e30f43ef9430f0f86b2ce6f7bd Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 3 Dec 2021 15:07:36 -0500 Subject: [PATCH 03/30] Graceful warning when Slab or Basement program is not found --- archetypal/eplus_interface/basement.py | 14 ++++++++++---- archetypal/eplus_interface/slab.py | 14 +++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/archetypal/eplus_interface/basement.py b/archetypal/eplus_interface/basement.py index a69cb5315..b1f73baa7 100644 --- a/archetypal/eplus_interface/basement.py +++ b/archetypal/eplus_interface/basement.py @@ -57,11 +57,17 @@ def run(self): # Get executable using shutil.which (determines the extension based on # the platform, eg: .exe. And copy the executable to tmp - self.basement_exe = Path( - shutil.which( - "Basement", path=self.eplus_home / "PreProcess" / "GrndTempCalc" + basemenet_exe = shutil.which( + "Basement", path=self.eplus_home / "PreProcess" / "GrndTempCalc" + ) + if basemenet_exe is None: + log( + f"The Basement program could not be found at " + f"'{self.eplus_home / 'PreProcess' / 'GrndTempCalc'}'", + lg.WARNING, ) - ).copy(self.run_dir) + return + self.basement_exe = Path(basemenet_exe).copy(self.run_dir) self.basement_idd = ( self.eplus_home / "PreProcess" / "GrndTempCalc" / "BasementGHT.idd" ).copy(self.run_dir) diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index 56a2e53da..0e1a13771 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -58,9 +58,17 @@ def run(self): # Get executable using shutil.which (determines the extension based on # the platform, eg: .exe. And copy the executable to tmp - self.slabexe = Path( - shutil.which("Slab", path=self.eplus_home / "PreProcess" / "GrndTempCalc") - ).copy(self.run_dir) + slab_exe = shutil.which( + "Slab", path=self.eplus_home / "PreProcess" / "GrndTempCalc" + ) + if slab_exe is None: + log( + f"The Slab program could not be found at " + f"'{self.eplus_home / 'PreProcess' / 'GrndTempCalc'}'", + lg.WARNING, + ) + return + self.slabexe = Path(slab_exe).copy(self.run_dir) self.slabidd = ( self.eplus_home / "PreProcess" / "GrndTempCalc" / "SlabGHT.idd" ).copy(self.run_dir) From 6d78dee9164e82f754a73a5730a967a0a1c43b7d Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Wed, 8 Dec 2021 12:04:36 -0500 Subject: [PATCH 04/30] Adds KeyBoardInterupt to IDF Thread --- archetypal/eplus_interface/basement.py | 6 ++ archetypal/eplus_interface/expand_objects.py | 6 ++ archetypal/eplus_interface/slab.py | 6 ++ archetypal/idfclass/idf.py | 86 ++++++++++++-------- 4 files changed, 71 insertions(+), 33 deletions(-) diff --git a/archetypal/eplus_interface/basement.py b/archetypal/eplus_interface/basement.py index b1f73baa7..f9f21049a 100644 --- a/archetypal/eplus_interface/basement.py +++ b/archetypal/eplus_interface/basement.py @@ -221,3 +221,9 @@ def eplus_home(self): ) else: return Path(eplus_home) + + def stop(self): + if self.p.poll() is None: + self.msg_callback("Attempting to cancel simulation ...") + self.cancelled = True + self.p.kill() diff --git a/archetypal/eplus_interface/expand_objects.py b/archetypal/eplus_interface/expand_objects.py index 710920f00..4ebd20deb 100644 --- a/archetypal/eplus_interface/expand_objects.py +++ b/archetypal/eplus_interface/expand_objects.py @@ -159,3 +159,9 @@ def eplus_home(self): ) else: return Path(eplus_home) + + def stop(self): + if self.p.poll() is None: + self.msg_callback("Attempting to cancel simulation ...") + self.cancelled = True + self.p.kill() diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index 0e1a13771..68c805da2 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -180,3 +180,9 @@ def eplus_home(self): ) else: return Path(eplus_home) + + def stop(self): + if self.p.poll() is None: + self.msg_callback("Attempting to cancel simulation ...") + self.cancelled = True + self.p.kill() diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index 04683a13e..2999af763 100644 --- a/archetypal/idfclass/idf.py +++ b/archetypal/idfclass/idf.py @@ -1361,14 +1361,19 @@ def simulate(self, force=False, **kwargs): ).mkdir() # Run the ExpandObjects preprocessor program expandobjects_thread = ExpandObjectsThread(self, tmp) - expandobjects_thread.start() - expandobjects_thread.join() - while expandobjects_thread.is_alive(): - time.sleep(1) - tmp.rmtree(ignore_errors=True) - e = expandobjects_thread.exception - if e is not None: - raise e + try: + expandobjects_thread.start() + expandobjects_thread.join() + # Give time to the subprocess to finish completely + while expandobjects_thread.is_alive(): + time.sleep(1) + except (KeyboardInterrupt, SystemExit): + expandobjects_thread.stop() + finally: + tmp.rmtree(ignore_errors=True) + e = expandobjects_thread.exception + if e is not None: + raise e # Run the Basement preprocessor program if necessary tmp = ( @@ -1376,43 +1381,58 @@ def simulate(self, force=False, **kwargs): + str(uuid.uuid1())[0:8] ).mkdir() basement_thread = BasementThread(self, tmp) - basement_thread.start() - basement_thread.join() - while basement_thread.is_alive(): - time.sleep(1) - tmp.rmtree(ignore_errors=True) - e = basement_thread.exception - if e is not None: - raise e + try: + basement_thread.start() + basement_thread.join() + # Give time to the subprocess to finish completely + while basement_thread.is_alive(): + time.sleep(1) + except KeyboardInterrupt: + basement_thread.stop() + finally: + tmp.rmtree(ignore_errors=True) + e = basement_thread.exception + if e is not None: + raise e # Run the Slab preprocessor program if necessary tmp = ( self.output_directory.makedirs_p() / "runSlab_run_" + str(uuid.uuid1())[0:8] ).mkdir() slab_thread = SlabThread(self, tmp) - slab_thread.start() - slab_thread.join() - while slab_thread.is_alive(): - time.sleep(1) - tmp.rmtree(ignore_errors=True) - e = slab_thread.exception - if e is not None: - raise e + try: + slab_thread.start() + slab_thread.join() + # Give time to the subprocess to finish completely + while slab_thread.is_alive(): + time.sleep(1) + except KeyboardInterrupt: + slab_thread.stop() + finally: + tmp.rmtree(ignore_errors=True) + e = slab_thread.exception + if e is not None: + raise e # Run the energyplus program tmp = ( self.output_directory.makedirs_p() / "eplus_run_" + str(uuid.uuid1())[0:8] ).mkdir() running_simulation_thread = EnergyPlusThread(self, tmp) - running_simulation_thread.start() - running_simulation_thread.join() - while running_simulation_thread.is_alive(): - time.sleep(1) - tmp.rmtree(ignore_errors=True) - e = running_simulation_thread.exception - if e is not None: - raise e - return self + try: + running_simulation_thread.start() + running_simulation_thread.join() + # Give time to the subprocess to finish completely + while running_simulation_thread.is_alive(): + time.sleep(1) + except KeyboardInterrupt: + running_simulation_thread.stop() + finally: + tmp.rmtree(ignore_errors=True) + e = running_simulation_thread.exception + if e is not None: + raise e + return self def savecopy(self, filename, lineendings="default", encoding="latin-1"): """Save a copy of the file with the filename passed. From 53db07634d1654b59bd59f2621029683fa2c02e5 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Wed, 8 Dec 2021 13:07:07 -0500 Subject: [PATCH 05/30] catches more variations of unit name --- archetypal/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/archetypal/settings.py b/archetypal/settings.py index 7bef8cbe5..31e254574 100644 --- a/archetypal/settings.py +++ b/archetypal/settings.py @@ -129,8 +129,8 @@ from energy_pandas.units import unit_registry additional_units = ( - "Dimensionless = dimensionless = Fraction", - "@alias degC = Temperature", + "Dimensionless = dimensionless = Fraction = fraction", + "@alias degC = Temperature = temperature", ) for unit in additional_units: unit_registry.define(unit) From 2453bd497263d571433e8c9d5a25208e4fe85581 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Wed, 8 Dec 2021 13:07:24 -0500 Subject: [PATCH 06/30] Adds ability to scale a schedule --- archetypal/schedule.py | 8 ++++++++ tests/test_schedules.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/archetypal/schedule.py b/archetypal/schedule.py index 5a8c5b1fc..a71fb93ad 100644 --- a/archetypal/schedule.py +++ b/archetypal/schedule.py @@ -1316,6 +1316,14 @@ def startDate(self): year = get_year_for_first_weekday(self.startDayOfTheWeek) return datetime(year, 1, 1) + def scale(self, diversity=0.1): + """Scale the schedule values by a diversity factor around the average.""" + average = np.average(self.Values) + new_values = ((average - self.Values) * diversity) + self.Values + + self.Values = new_values + return self + def plot(self, **kwargs): """Plot the schedule. Implements the .loc accessor on the series object. diff --git a/tests/test_schedules.py b/tests/test_schedules.py index a87dd7b14..54d0e7b38 100644 --- a/tests/test_schedules.py +++ b/tests/test_schedules.py @@ -47,6 +47,14 @@ def schedules_in_necb_specific(self, config): s = Schedule.from_epbunch(epbunch, start_day_of_the_week=0) yield s + def test_scale(self, schedules_in_necb_specific): + before_sum = sum(schedules_in_necb_specific.Values) + ax = schedules_in_necb_specific.series.iloc[0:24].plot() + assert pytest.approx( + before_sum, sum(schedules_in_necb_specific.scale(0.1).Values) + ) + schedules_in_necb_specific.series.iloc[0:24].plot(ax=ax) + def test_plot(self, schedules_in_necb_specific): schedules_in_necb_specific.plot(drawstyle="steps-post") From 3ec77f95aa74307ad8c54e704e82b5f313352d5d Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Thu, 9 Dec 2021 11:50:29 -0500 Subject: [PATCH 07/30] Fixes fallback limits for Schedule.plot2d() when Type is not defined --- archetypal/schedule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archetypal/schedule.py b/archetypal/schedule.py index a71fb93ad..8580bd7d9 100644 --- a/archetypal/schedule.py +++ b/archetypal/schedule.py @@ -1505,7 +1505,7 @@ def _repr_svg_(self): vmin = self.Type.LowerLimit vmax = self.Type.UpperLimit else: - vmin, vmax = (0, 0) + vmin, vmax = (None, None) fig, ax = self.series.plot2d( cmap="Greys", show=False, figsize=(7, 2), dpi=72, vmin=vmin, vmax=vmax ) From 8718452f48703128455642b8ce6e4484c94036b2 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Thu, 9 Dec 2021 11:52:04 -0500 Subject: [PATCH 08/30] Type can be specified in Schedule.from_values constructor --- archetypal/schedule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archetypal/schedule.py b/archetypal/schedule.py index 8580bd7d9..a9003c2f5 100644 --- a/archetypal/schedule.py +++ b/archetypal/schedule.py @@ -1204,7 +1204,7 @@ def from_values(cls, Name, Values, Type="Fraction", **kwargs): Type: **kwargs: """ - return cls(Name=Name, Values=Values, Type="Fraction", **kwargs) + return cls(Name=Name, Values=Values, Type=Type, **kwargs) @classmethod def from_epbunch(cls, epbunch, strict=False, Type=None, **kwargs): From fb3b7415260cb7d83c54dd2ef113a535f7d27559 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Thu, 9 Dec 2021 11:52:45 -0500 Subject: [PATCH 09/30] plot2d is prettier by default --- archetypal/schedule.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/archetypal/schedule.py b/archetypal/schedule.py index a9003c2f5..3dd8ae381 100644 --- a/archetypal/schedule.py +++ b/archetypal/schedule.py @@ -1346,7 +1346,22 @@ def plot(self, **kwargs): def plot2d(self, **kwargs): """Plot the carpet plot of the schedule.""" - return self.series.plot2d(**kwargs) + if self.Type is not None: + vmin = self.Type.LowerLimit + vmax = self.Type.UpperLimit + else: + vmin, vmax = (None, None) + + pretty_plot_kwargs = { + "cmap": "Greys", + "show": True, + "figsize": (7, 2), + "dpi": 72, + "vmin": vmin, + "vmax": vmax, + } + pretty_plot_kwargs.update(kwargs) + return self.series.plot2d(**pretty_plot_kwargs) plot2d.__doc__ += EnergySeries.plot2d.__doc__ From ef2331e576588f5eb7b32ddf715b4c2f7b1d45d6 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Thu, 9 Dec 2021 11:52:55 -0500 Subject: [PATCH 10/30] more Typing --- archetypal/schedule.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/archetypal/schedule.py b/archetypal/schedule.py index 3dd8ae381..fee62d816 100644 --- a/archetypal/schedule.py +++ b/archetypal/schedule.py @@ -5,11 +5,13 @@ import logging as lg from datetime import datetime, timedelta from itertools import groupby +from typing import FrozenSet, Union import numpy as np import pandas as pd from energy_pandas import EnergySeries from eppy.bunch_subclass import BadEPFieldError +from typing_extensions import Literal from validator_collection import checkers, validators from archetypal.utils import log @@ -1112,11 +1114,11 @@ class Schedule: def __init__( self, - Name, - start_day_of_the_week=0, - strict=False, - Type=None, - Values=None, + Name: str, + start_day_of_the_week: FrozenSet[Literal[0, 1, 2, 3, 4, 5, 6]] = 0, + strict: bool = False, + Type: Union[str, ScheduleTypeLimits] = None, + Values: np.ndarray = None, **kwargs, ): """Initialize object. @@ -1603,8 +1605,16 @@ def _how(how): return "max" -def get_year_for_first_weekday(weekday=0): - """Get the year that starts on 'weekday', eg. Monday=0.""" +def get_year_for_first_weekday(weekday: FrozenSet[Literal[0, 1, 2, 3, 4, 5, 6]] = 0): + """Get the year that starts on 'weekday', eg. Monday=0. + + Args: + weekday (int): 0-based day of week (Monday=0). Default is + None which looks for the start day in the IDF model. + + Returns: + (int): The year number for which the first starts on :attr:`weekday`. + """ import calendar if weekday > 6: From ac47fa1b34bc7929f5972f6026ba1671f392ac70 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 10 Dec 2021 10:01:15 -0500 Subject: [PATCH 11/30] Return existing object when new_object is there (#257) --- archetypal/idfclass/idf.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index 2999af763..29bc1a862 100644 --- a/archetypal/idfclass/idf.py +++ b/archetypal/idfclass/idf.py @@ -1922,7 +1922,7 @@ def newidfobject(self, key, **kwargs) -> Optional[EpBunch]: except BadEPFieldError as e: raise e else: - # If object is supposed to be 'unique-object', deletes all objects to be + # If object is supposed to be 'unique-object', delete all objects to be # sure there is only one of them when creating new object # (see following line) if "unique-object" in set().union( @@ -1930,22 +1930,23 @@ def newidfobject(self, key, **kwargs) -> Optional[EpBunch]: ): for obj in existing_objs: self.removeidfobject(obj) - self.addidfobject(new_object) log( f"{obj} is a 'unique-object'; Removed and replaced with" f" {new_object}", lg.DEBUG, ) + self.addidfobject(new_object) return new_object if new_object in existing_objs: - # If obj already exists, simply return + # If obj already exists, simply return the existing one. log( f"object '{new_object}' already exists in {self.name}. " f"Skipping.", lg.DEBUG, ) - return new_object + return next(x for x in existing_objs if x == new_object) elif new_object not in existing_objs and new_object.nameexists(): + # Object does not exist (because not equal) but Name exists. obj = self.getobject( key=new_object.key.upper(), name=new_object.Name.upper() ) From b4b9a38184d29ebdf41ec8573b0247393fa89391 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Mon, 20 Dec 2021 09:11:05 -0500 Subject: [PATCH 12/30] Adds ability to replace schedule values without affecting the full load hours --- archetypal/schedule.py | 64 ++++++++++++++++++++++++++++++++++------- tests/test_schedules.py | 14 +++++++++ 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/archetypal/schedule.py b/archetypal/schedule.py index fee62d816..fc76894d6 100644 --- a/archetypal/schedule.py +++ b/archetypal/schedule.py @@ -5,7 +5,7 @@ import logging as lg from datetime import datetime, timedelta from itertools import groupby -from typing import FrozenSet, Union +from typing import FrozenSet, Union, List import numpy as np import pandas as pd @@ -1118,7 +1118,7 @@ def __init__( start_day_of_the_week: FrozenSet[Literal[0, 1, 2, 3, 4, 5, 6]] = 0, strict: bool = False, Type: Union[str, ScheduleTypeLimits] = None, - Values: np.ndarray = None, + Values: Union[List[Union[int, float]], np.ndarray] = None, **kwargs, ): """Initialize object. @@ -1197,15 +1197,14 @@ def Name(self, value): self._name = value @classmethod - def from_values(cls, Name, Values, Type="Fraction", **kwargs): - """Create a Schedule from a list of Values. - - Args: - Name: - Values: - Type: - **kwargs: - """ + def from_values( + cls, + Name: str, + Values: List[Union[float, int]], + Type: str = "Fraction", + **kwargs, + ): + """Create a Schedule from a list of Values.""" return cls(Name=Name, Values=Values, Type=Type, **kwargs) @classmethod @@ -1326,6 +1325,49 @@ def scale(self, diversity=0.1): self.Values = new_values return self + def replace(self, new_values: Union[pd.Series]): + """Replace values with new values while keeping the full load hours constant. + + Time steps that are not specified in `new_values` will be adjusted to keep + the full load hours of the schedule constant. No check whether the new schedule + stays between the bounds set by self.Type is done. Be aware. + + """ + assert isinstance(new_values.index, pd.DatetimeIndex), ( + "The index of `new_values` must be a `pandas.DatetimeIndex`. Instead, " + f"`{type(new_values.index)}` was provided." + ) + assert not self.series.index.difference(new_values.index).empty, ( + "There is no overlap between self.index and new_values.index. Please " + "check your dates." + ) + + # create a copy of self.series as orig. + orig = self.series.copy() + + new_data = new_values.values + + # get the new_values index + idx = new_values.index + + # compute the difference in values with the original data and the new data. + diff = orig.loc[idx] - new_data.reshape(-1) + + # replace the original data with new values at their location. + orig.loc[idx] = new_values + + # adjust remaining time steps with the average difference. Inplace. + orig.loc[orig.index.difference(idx)] += diff.sum() / len( + orig.index.difference(idx) + ) + new = orig + + # assert the sum has not changed as a sanity check. + np.testing.assert_array_almost_equal(self.series.sum(), new.sum()) + + # replace values of self with new values. + self.Values = new.tolist() + def plot(self, **kwargs): """Plot the schedule. Implements the .loc accessor on the series object. diff --git a/tests/test_schedules.py b/tests/test_schedules.py index 54d0e7b38..04e1a6399 100644 --- a/tests/test_schedules.py +++ b/tests/test_schedules.py @@ -95,6 +95,20 @@ def test_from_values(self, new_idf): ) assert len(heating_sched.all_values) == 8760 + def test_replace(self): + """Test replacing values while keeping full load hours constant.""" + sch = Schedule.from_values("Test", [1] * 6 + [0.5] * 12 + [1] * 6) + new = pd.Series([1, 1], index=sch.series.index[11:13]) + + orig = sch.series.sum() + + sch.replace(new) + + new = sch.series.sum() + + # assert the full load hours (sum) has not changed. + assert new == pytest.approx(orig) + idf_file = "tests/input_data/schedules/test_multizone_EP.idf" From 9e677dd8d28f29b55f8b91243061673fef4a0bce Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Thu, 20 Jan 2022 12:55:04 -0500 Subject: [PATCH 13/30] more robust IDF.name property --- archetypal/idfclass/idf.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index 29bc1a862..817ac564a 100644 --- a/archetypal/idfclass/idf.py +++ b/archetypal/idfclass/idf.py @@ -252,7 +252,13 @@ def __init__( self.prep_outputs = prep_outputs self._position = position self.output_prefix = None - self.name = self.idfname.basename() if isinstance(self.idfname, Path) else name + self.name = ( + name + if name is not None + else self.idfname.basename() + if isinstance(self.idfname, Path) + else None + ) self.output_directory = output_directory # Set dependants to None @@ -903,6 +909,14 @@ def open_last_simulation(self): app_path_guess = self.file_version.current_install_dir find_and_launch("EP-Launch", app_path_guess, filepath.abspath()) + def open_err(self): + """Open last simulation err file in texteditor.""" + import webbrowser + + filepath, *_ = self.simulation_dir.files("*.err") + + webbrowser.open(filepath.abspath()) + def open_mdd(self): """Open .mdd file in browser. From 818f0edf834212789ca6d0639ead8e83166665cf Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Tue, 1 Feb 2022 16:22:27 -0500 Subject: [PATCH 14/30] Keep sim files when error occurs (#276) --- archetypal/eplus_interface/energy_plus.py | 2 +- archetypal/idfclass/idf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/archetypal/eplus_interface/energy_plus.py b/archetypal/eplus_interface/energy_plus.py index 1c9abd80c..100115ce3 100644 --- a/archetypal/eplus_interface/energy_plus.py +++ b/archetypal/eplus_interface/energy_plus.py @@ -270,7 +270,7 @@ def failure_callback(self): with open(error_filename, "r") as stderr: stderr_r = stderr.read() if self.idf.keep_data_err: - failed_dir = self.idf.simulation_dir.mkdir_p() / "failed" + failed_dir = self.idf.simulation_dir.mkdir_p() try: failed_dir.rmtree_p() except PermissionError as e: diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index 817ac564a..afab4b09d 100644 --- a/archetypal/idfclass/idf.py +++ b/archetypal/idfclass/idf.py @@ -194,7 +194,7 @@ def __init__( output_suffix="L", epmacro=False, keep_data=True, - keep_data_err=False, + keep_data_err=True, position=0, name=None, output_directory=None, From 2e21b69371f752a4b714b8125c270da809e2957a Mon Sep 17 00:00:00 2001 From: Zach Berzolla <53047789+zberzolla@users.noreply.github.com> Date: Tue, 19 Apr 2022 11:39:34 -0400 Subject: [PATCH 15/30] updates requests requirement from ~=2.25.1 to >=2.26 (#292) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 504199bc8..eb8def07f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ click~=8.0.1 outdated~=0.2.1 deprecation~=2.1.0 sigfig~=1.1.9 -requests~=2.25.1 +requests>=2.26.0 packaging~=21.0 pytest~=6.2.4 setuptools~=56.2.0 From f8f29b0a7f59404d844aa34ec9c2a74f9213daa2 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 13:56:48 -0400 Subject: [PATCH 16/30] typo --- archetypal/eplus_interface/basement.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/archetypal/eplus_interface/basement.py b/archetypal/eplus_interface/basement.py index c3be755b1..ac18ad7f3 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 From eb91e4b377e0d8a9040edbdee7f9cbb5c60ecc53 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 13:58:40 -0400 Subject: [PATCH 17/30] this --- archetypal/eplus_interface/slab.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index 64fbca9ca..e11d91da8 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -1,6 +1,7 @@ """Slab module""" import logging as lg +import os import shutil import subprocess import time @@ -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,33 +41,32 @@ 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) - # 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() @@ -132,7 +131,7 @@ def success_callback(self): next(infile) # Skipping second line for line in infile: outfile.write(line) - # invalidate attributes dependant on idfname, since it has changed + # invalidate attributes dependent on idfname, since it has changed self.idf._reset_dependant_vars("idfname") self.cleanup_callback() From 4325fdb86b99c5379f33600aa76c81d1150cffc4 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 14:03:45 -0400 Subject: [PATCH 18/30] gitsubmodule as https --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 528df87be..804722178 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 From ec5157548f618a0b1833edae3facb3b357e88c74 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 14:12:51 -0400 Subject: [PATCH 19/30] p --- archetypal/eplus_interface/slab.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index e11d91da8..6dcb9ac9a 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -77,21 +77,21 @@ def run(self): 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(), - ) - 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")) - progress.update() + try: + print("Running...", self.cmd, "from", self.run_dir) + self.p = subprocess.Popen( + self.cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + cwd=self.run_dir, + ) + start_time = time.time() + self.msg_callback( + "Begin Slab Temperature Calculation processing . . ." # We explicitly close stdout self.p.stdout.close() From a1e1ce6f627d7eb3a0c62f855eb313f4738b72ad Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 14:20:30 -0400 Subject: [PATCH 20/30] pp --- archetypal/eplus_interface/slab.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index 6dcb9ac9a..30ddc4701 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -116,6 +116,9 @@ def run(self): else: self.msg_callback("RunSlab failed") self.failure_callback() + except Exception as e: + self.msg_callback(f"An error occurred: {str(e)}") + self.failure_callback() def msg_callback(self, *args, **kwargs): """Pass message to logger.""" From 26f54aa55f565b0e8d4644feef13c0cd1b75b947 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 14:21:03 -0400 Subject: [PATCH 21/30] this --- archetypal/eplus_interface/slab.py | 55 ++++++++++++++++++------------ archetypal/utils.py | 4 +-- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index 30ddc4701..873fa9ef3 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -65,6 +65,7 @@ def run(self): 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 self.include = [Path(file).copy(self.run_dir) for file in self.idf.include] @@ -92,30 +93,40 @@ def run(self): start_time = time.time() self.msg_callback( "Begin Slab Temperature Calculation processing . . ." + ) + stdout, stderr = self.p.communicate() - # We explicitly close stdout - self.p.stdout.close() - - # Wait for process to complete - self.p.wait() - - # Communicate callbacks - if self.cancelled: - self.msg_callback("RunSlab 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( - time.time() - start_time - ) - ) - self.success_callback() - for line in self.p.stderr: - self.msg_callback(line.decode("utf-8")) + # Process stdout + stdout_lines = stdout.decode("utf-8").splitlines() + for line in stdout_lines: + self.msg_callback(line) + progress.update() + + # Process stderr + stderr_lines = stderr.decode("utf-8").splitlines() + + # Communicate callbacks + if self.cancelled: + self.msg_callback("RunSlab cancelled") else: - self.msg_callback("RunSlab failed") - self.failure_callback() + if self.p.returncode == 0: + self.msg_callback( + "RunSlab completed in {:,.2f} seconds".format( + time.time() - start_time + ) + ) + self.success_callback() + else: + self.msg_callback("RunSlab failed") + self.msg_callback( + "Standard Output:\n" + "\n".join(stdout_lines), + lg.INFO, + ) + self.msg_callback( + "Standard Error:\n" + "\n".join(stderr_lines), + level=lg.ERROR, + ) + self.failure_callback() except Exception as e: self.msg_callback(f"An error occurred: {str(e)}") self.failure_callback() diff --git a/archetypal/utils.py b/archetypal/utils.py index 7d2b8166c..4cd12639d 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" From 3286240285030d812c7c5e33e5f72adf7dca45a7 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 14:23:52 -0400 Subject: [PATCH 22/30] error --- archetypal/eplus_interface/slab.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index 873fa9ef3..d409b9dd6 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -122,10 +122,9 @@ def run(self): "Standard Output:\n" + "\n".join(stdout_lines), lg.INFO, ) - self.msg_callback( - "Standard Error:\n" + "\n".join(stderr_lines), - level=lg.ERROR, - ) + print( + "Standard Error:\n" + "\n".join(stderr_lines) + ) # todo: revert to msg_callback self.failure_callback() except Exception as e: self.msg_callback(f"An error occurred: {str(e)}") From 44f49a87a25720c50731fd23faae59984d87a8e4 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 14:29:46 -0400 Subject: [PATCH 23/30] catch outfile --- archetypal/eplus_interface/slab.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index d409b9dd6..b637a523b 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -136,16 +136,18 @@ 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 dependent 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(): + with open(self.outfile, "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 dependent on idfname, since it has changed + self.idf._reset_dependant_vars("idfname") + else: + self.msg_callback("No SLABSurfaceTemps.txt file found.", level=lg.ERROR) self.cleanup_callback() def cleanup_callback(self): From bef3e4b7d1ae5a38bc894b636da5ebf31397bae8 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 14:46:27 -0400 Subject: [PATCH 24/30] logging --- archetypal/eplus_interface/basement.py | 8 +-- archetypal/eplus_interface/slab.py | 89 ++++++++++++-------------- 2 files changed, 45 insertions(+), 52 deletions(-) diff --git a/archetypal/eplus_interface/basement.py b/archetypal/eplus_interface/basement.py index ac18ad7f3..b284f4618 100644 --- a/archetypal/eplus_interface/basement.py +++ b/archetypal/eplus_interface/basement.py @@ -89,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, @@ -120,12 +120,12 @@ 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 ) ) @@ -133,7 +133,7 @@ def run(self): for line in self.p.stderr: self.msg_callback(line.decode("utf-8")) else: - self.msg_callback("RunSlab failed") + self.msg_callback("Basement failed") self.failure_callback() def msg_callback(self, *args, **kwargs): diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index b637a523b..36c7cd30f 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -74,61 +74,54 @@ def run(self): 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"{self.slabexe} #{self.idf.position}-{self.idf.name}", position=self.idf.position, ) as progress: - try: - print("Running...", self.cmd, "from", self.run_dir) - self.p = subprocess.Popen( - self.cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=False, - cwd=self.run_dir, - ) - start_time = time.time() - self.msg_callback( - "Begin Slab Temperature Calculation processing . . ." - ) - stdout, stderr = self.p.communicate() - - # Process stdout - stdout_lines = stdout.decode("utf-8").splitlines() - for line in stdout_lines: - self.msg_callback(line) - progress.update() - - # Process stderr - stderr_lines = stderr.decode("utf-8").splitlines() - - # Communicate callbacks - if self.cancelled: - self.msg_callback("RunSlab cancelled") - else: - if self.p.returncode == 0: - self.msg_callback( - "RunSlab completed in {:,.2f} seconds".format( - time.time() - start_time - ) - ) - self.success_callback() - else: - self.msg_callback("RunSlab failed") - self.msg_callback( - "Standard Output:\n" + "\n".join(stdout_lines), - lg.INFO, + self.p = subprocess.Popen( + self.cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + cwd=self.run_dir, + ) + start_time = time.time() + self.msg_callback("Begin Slab Temperature Calculation processing . . .") + + # 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() + + # Wait for process to complete + self.p.wait() + + # Communicate callbacks + if self.cancelled: + self.msg_callback("Slab cancelled") + else: + if self.p.returncode == 0: + self.msg_callback( + "Slab completed in {:,.2f} seconds".format( + time.time() - start_time ) - print( - "Standard Error:\n" + "\n".join(stderr_lines) - ) # todo: revert to msg_callback - self.failure_callback() - except Exception as e: - self.msg_callback(f"An error occurred: {str(e)}") - self.failure_callback() + ) + self.success_callback() + else: + 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): """Pass message to logger.""" From fb1efb05bb4e8d4cf1080408e03d80fdf209779f Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 14:52:08 -0400 Subject: [PATCH 25/30] Revert "logging" This reverts commit bef3e4b7d1ae5a38bc894b636da5ebf31397bae8. --- archetypal/eplus_interface/basement.py | 8 +-- archetypal/eplus_interface/slab.py | 89 ++++++++++++++------------ 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/archetypal/eplus_interface/basement.py b/archetypal/eplus_interface/basement.py index b284f4618..ac18ad7f3 100644 --- a/archetypal/eplus_interface/basement.py +++ b/archetypal/eplus_interface/basement.py @@ -89,7 +89,7 @@ def run(self): self.msg_callback(f"Weather File: {self.epw}") # Run Slab Program - with logging_redirect_tqdm(loggers=[lg.getLogger("archetypal")]): + with logging_redirect_tqdm(loggers=[lg.getLogger(self.idf.name)]): with tqdm( unit_scale=True, miniters=1, @@ -120,12 +120,12 @@ def run(self): # Communicate callbacks if self.cancelled: - self.msg_callback("Basement cancelled") + self.msg_callback("RunSlab cancelled") # self.cancelled_callback(self.std_out, self.std_err) else: if self.p.returncode == 0: self.msg_callback( - "Basement completed in {:,.2f} seconds".format( + "RunSlab completed in {:,.2f} seconds".format( time.time() - start_time ) ) @@ -133,7 +133,7 @@ def run(self): for line in self.p.stderr: self.msg_callback(line.decode("utf-8")) else: - self.msg_callback("Basement failed") + self.msg_callback("RunSlab failed") self.failure_callback() def msg_callback(self, *args, **kwargs): diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index 36c7cd30f..b637a523b 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -74,54 +74,61 @@ def run(self): return # Run Slab Program - with logging_redirect_tqdm(loggers=[lg.getLogger("archetypal")]): + with logging_redirect_tqdm(loggers=[lg.getLogger(self.idf.name)]): with tqdm( unit_scale=True, miniters=1, 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=False, - cwd=self.run_dir, - ) - start_time = time.time() - self.msg_callback("Begin Slab Temperature Calculation processing . . .") - - # 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() - - # Wait for process to complete - self.p.wait() - - # Communicate callbacks - if self.cancelled: - self.msg_callback("Slab cancelled") - else: - if self.p.returncode == 0: - self.msg_callback( - "Slab completed in {:,.2f} seconds".format( - time.time() - start_time - ) - ) - self.success_callback() + try: + print("Running...", self.cmd, "from", self.run_dir) + self.p = subprocess.Popen( + self.cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + cwd=self.run_dir, + ) + start_time = time.time() + self.msg_callback( + "Begin Slab Temperature Calculation processing . . ." + ) + stdout, stderr = self.p.communicate() + + # Process stdout + stdout_lines = stdout.decode("utf-8").splitlines() + for line in stdout_lines: + self.msg_callback(line) + progress.update() + + # Process stderr + stderr_lines = stderr.decode("utf-8").splitlines() + + # Communicate callbacks + if self.cancelled: + self.msg_callback("RunSlab cancelled") else: - self.msg_callback("Slab failed", level=lg.ERROR) - self.msg_callback("\n".join(stderr_lines), level=lg.ERROR) - self.failure_callback() + if self.p.returncode == 0: + self.msg_callback( + "RunSlab completed in {:,.2f} seconds".format( + time.time() - start_time + ) + ) + self.success_callback() + else: + self.msg_callback("RunSlab failed") + self.msg_callback( + "Standard Output:\n" + "\n".join(stdout_lines), + lg.INFO, + ) + print( + "Standard Error:\n" + "\n".join(stderr_lines) + ) # todo: revert to msg_callback + self.failure_callback() + except Exception as e: + self.msg_callback(f"An error occurred: {str(e)}") + self.failure_callback() def msg_callback(self, *args, **kwargs): """Pass message to logger.""" From a3fd384e9d0f9ffa93cb5df18bcacb47258c97ed Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 15:06:32 -0400 Subject: [PATCH 26/30] keep --- archetypal/idfclass/idf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index 66d1ec835..e31a1e74f 100644 --- a/archetypal/idfclass/idf.py +++ b/archetypal/idfclass/idf.py @@ -1485,7 +1485,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 From dd136f78c08ba50ecc92b1d9d5ca1516b854ab1f Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 15:17:34 -0400 Subject: [PATCH 27/30] as model --- archetypal/eplus_interface/slab.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index b637a523b..0c3a553d5 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -1,10 +1,10 @@ """Slab module""" import logging as lg -import os import shutil import subprocess import time +from io import StringIO from threading import Thread from packaging.version import Version @@ -138,14 +138,23 @@ def success_callback(self): """Parse surface temperature and append to IDF file.""" for temp_schedule in self.run_dir.glob("SLABSurfaceTemps*"): if temp_schedule.exists(): - with open(self.outfile, "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 dependent on idfname, since it has changed - self.idf._reset_dependant_vars("idfname") + 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() From d5876a45404f639e8be115d7aabfe3836062dcaa Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 15:27:53 -0400 Subject: [PATCH 28/30] better logging --- archetypal/eplus_interface/basement.py | 23 ++++--- archetypal/eplus_interface/slab.py | 91 ++++++++++++-------------- 2 files changed, 58 insertions(+), 56 deletions(-) diff --git a/archetypal/eplus_interface/basement.py b/archetypal/eplus_interface/basement.py index ac18ad7f3..b6e8f5d72 100644 --- a/archetypal/eplus_interface/basement.py +++ b/archetypal/eplus_interface/basement.py @@ -89,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, @@ -108,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() @@ -120,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 0c3a553d5..6341db139 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -74,61 +74,56 @@ def run(self): 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"{self.slabexe} #{self.idf.position}-{self.idf.name}", position=self.idf.position, ) as progress: - try: - print("Running...", self.cmd, "from", self.run_dir) - self.p = subprocess.Popen( - self.cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=False, - cwd=self.run_dir, - ) - start_time = time.time() - self.msg_callback( - "Begin Slab Temperature Calculation processing . . ." - ) - stdout, stderr = self.p.communicate() - - # Process stdout - stdout_lines = stdout.decode("utf-8").splitlines() - for line in stdout_lines: - self.msg_callback(line) - progress.update() - - # Process stderr - stderr_lines = stderr.decode("utf-8").splitlines() - - # Communicate callbacks - if self.cancelled: - self.msg_callback("RunSlab cancelled") - else: - if self.p.returncode == 0: - self.msg_callback( - "RunSlab completed in {:,.2f} seconds".format( - time.time() - start_time - ) - ) - self.success_callback() - else: - self.msg_callback("RunSlab failed") - self.msg_callback( - "Standard Output:\n" + "\n".join(stdout_lines), - lg.INFO, + self.p = subprocess.Popen( + self.cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + cwd=self.run_dir, + ) + start_time = time.time() + self.msg_callback("Begin Slab Temperature Calculation processing . . .") + + # 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() + + # Wait for process to complete + self.p.wait() + + # Communicate callbacks + if self.cancelled: + self.msg_callback("Slab cancelled") + else: + if self.p.returncode == 0: + self.msg_callback( + "Slab completed in {:,.2f} seconds".format( + time.time() - start_time ) - print( - "Standard Error:\n" + "\n".join(stderr_lines) - ) # todo: revert to msg_callback - self.failure_callback() - except Exception as e: - self.msg_callback(f"An error occurred: {str(e)}") - self.failure_callback() + ) + self.success_callback() + for line in stderr_lines: + self.msg_callback(line) + else: + 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): """Pass message to logger.""" From c1d163b42622f53e2b7fbc00ea1e279f250bc394 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 13 Sep 2024 15:41:00 -0400 Subject: [PATCH 29/30] allow setting `as_version` in `IDF.from_example_files` to load example from specified version --- archetypal/idfclass/idf.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index e31a1e74f..5ea1655ac 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")) From e1884d0f366be914abdc6aa0bcfd6a3179e13850 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Mon, 16 Sep 2024 20:48:06 -0400 Subject: [PATCH 30/30] Trigger Build