From 0fa77d28bd3706740c56b2d9ed8e328d8c0148a2 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 25 Oct 2024 16:57:22 -0400 Subject: [PATCH] More robust linting & formating has been added to the code base (#511) * fix RUF rules * fix cannot be set on an instance * code satisfies TRY rules (#512) * fir TRY rules * allow pytest as function in tox * fixes an issue where p is not initialized * fix tests * code satisfies UP rules (#513) * fix UP rules * fix typing union on py39 * fix * fix round * fix union typing * fix round * add eval-type-backport = {version = "^0.2.0", python = "3.9"} * Code satisfies E rules (#514) * Code satisfies simplify rules (#516) * fir TRY rules * allow pytest as function in tox * fixes an issue where p is not initialized * fix tests * fix UP rules * fix typing union on py39 * fix E rules * fix * fix SIM rules * fix test don't show plot in pytest fix union error mock test rm test path * code satisfies C4 rules (#517) * fir TRY rules * allow pytest as function in tox * fixes an issue where p is not initialized * fix tests * fix UP rules * fix typing union on py39 * fix E rules * fix * fix SIM rules * fix C4 rules * fix types * fix typing * code satisfies A rules (#518) * fir TRY rules * allow pytest as function in tox * fixes an issue where p is not initialized * fix tests * fix UP rules * fix typing union on py39 * fix E rules * fix * fix SIM rules * fix C4 rules * fix A rules * fix typing * fix type to format change * code satisfies B rules (#519) * fix B rules * fix type * idf.simulate return self * fix test * zip for py39 no strict field * config * fix RUF rules fix cannot be set on an instance * code satisfies TRY rules (#512) * fir TRY rules * allow pytest as function in tox * fixes an issue where p is not initialized * fix tests * code satisfies UP rules (#513) * fix UP rules * fix typing union on py39 * fix * fix round * fix union typing * fix round * add eval-type-backport = {version = "^0.2.0", python = "3.9"} * Code satisfies E rules (#514) * Code satisfies simplify rules (#516) * fir TRY rules * allow pytest as function in tox * fixes an issue where p is not initialized * fix tests * fix UP rules * fix typing union on py39 * fix E rules * fix * fix SIM rules * fix test don't show plot in pytest fix union error mock test rm test path * code satisfies C4 rules (#517) * fir TRY rules * allow pytest as function in tox * fixes an issue where p is not initialized * fix tests * fix UP rules * fix typing union on py39 * fix E rules * fix * fix SIM rules * fix C4 rules * fix types * fix typing * code satisfies A rules (#518) * fir TRY rules * allow pytest as function in tox * fixes an issue where p is not initialized * fix tests * fix UP rules * fix typing union on py39 * fix E rules * fix * fix SIM rules * fix C4 rules * fix A rules * fix typing * fix type to format change * code satisfies B rules (#519) * fix B rules * fix type * idf.simulate return self * fix test * zip for py39 no strict field * config * robust simulate --- .gitignore | 2 + Makefile | 2 +- archetypal/__init__.py | 114 +++--- archetypal/cli.py | 40 +- archetypal/dataportal.py | 32 +- archetypal/eplus_interface/basement.py | 13 +- archetypal/eplus_interface/energy_plus.py | 91 +++-- archetypal/eplus_interface/exceptions.py | 2 +- archetypal/eplus_interface/slab.py | 13 +- archetypal/eplus_interface/transition.py | 13 +- archetypal/eplus_interface/version.py | 33 +- archetypal/idfclass/end_use_balance.py | 6 +- archetypal/idfclass/extensions.py | 21 +- archetypal/idfclass/idf.py | 382 +++++++++--------- archetypal/idfclass/meters.py | 19 +- archetypal/idfclass/outputs.py | 31 +- archetypal/idfclass/sql.py | 22 +- archetypal/idfclass/util.py | 13 +- archetypal/idfclass/variables.py | 9 +- archetypal/plot.py | 6 +- archetypal/reportdata.py | 49 ++- archetypal/schedule.py | 99 ++--- archetypal/simple_glazing.py | 75 ++-- archetypal/template/building_template.py | 47 +-- archetypal/template/conditioning.py | 284 ++++++------- .../constructions/base_construction.py | 11 +- .../constructions/opaque_construction.py | 44 +- .../constructions/window_construction.py | 29 +- archetypal/template/dhw.py | 32 +- archetypal/template/load.py | 72 ++-- archetypal/template/materials/gas_layer.py | 5 +- archetypal/template/materials/gas_material.py | 39 +- .../template/materials/glazing_material.py | 64 ++- .../template/materials/material_base.py | 2 +- .../template/materials/material_layer.py | 5 +- .../template/materials/nomass_material.py | 52 ++- .../template/materials/opaque_material.py | 56 ++- archetypal/template/schedule.py | 116 +++--- archetypal/template/structure.py | 42 +- archetypal/template/umi_base.py | 30 +- archetypal/template/ventilation.py | 71 ++-- archetypal/template/window_setting.py | 119 +++--- archetypal/template/zone_construction_set.py | 92 ++--- archetypal/template/zonedefinition.py | 82 ++-- archetypal/umi_template.py | 122 +++--- archetypal/utils.py | 32 +- archetypal/zone_graph.py | 17 +- docs/conf.py | 4 +- docs/examples/parallel_process.py | 16 +- poetry.lock | 33 +- pyproject.toml | 37 +- tests/conftest.py | 11 +- tests/test_dataportals.py | 8 +- tests/test_energypandas.py | 4 +- tests/test_idfclass.py | 11 +- tests/test_schedules.py | 5 +- tests/test_template.py | 27 +- tests/test_umi.py | 30 +- tests/test_zonegraph.py | 14 +- tox.ini | 2 +- 60 files changed, 1334 insertions(+), 1420 deletions(-) diff --git a/.gitignore b/.gitignore index b66c3dddb..72b572e8a 100644 --- a/.gitignore +++ b/.gitignore @@ -236,3 +236,5 @@ fabric.properties docs/reference/ .idea/ + +tests/tests/.temp/ diff --git a/Makefile b/Makefile index 1007a1357..ca6da274d 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ check: ## Run code quality tools. .PHONY: test test: ## Test the code with pytest @echo "🚀 Testing code: Running pytest" - @poetry run pytest tests --cov --cov-config=pyproject.toml --cov-report=xml + @poetry run pytest -n auto tests --cov --cov-config=pyproject.toml --cov-report=xml .PHONY: build build: clean-build ## Build wheel file using poetry diff --git a/archetypal/__init__.py b/archetypal/__init__.py index 61b2d01a3..cc17c7303 100644 --- a/archetypal/__init__.py +++ b/archetypal/__init__.py @@ -4,9 +4,11 @@ # License: MIT, see full license in LICENSE.txt # Web: https://github.com/samuelduchesne/archetypal ################################################################################ +from __future__ import annotations + import logging as lg from pathlib import Path -from typing import Any, List, Literal, Optional +from typing import Any, ClassVar, Literal from energy_pandas.units import unit_registry @@ -28,7 +30,7 @@ class ZoneWeight: """Zone weights for Umi Templates""" - weight_attr = {0: "area", 1: "volume"} + weight_attr: ClassVar[dict] = {0: "area", 1: "volume"} def __init__(self, n=0): self._weight_attr = self.weight_attr[n] @@ -67,96 +69,98 @@ class Settings(BaseSettings, arbitrary_types_allowed=True, validate_assignment=T log_filename: str = Field("archetypal") # usual idfobjects - useful_idf_objects: List[str] = [ - "WINDOWMATERIAL:GAS", - "WINDOWMATERIAL:GLAZING", - "WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM", - "MATERIAL", - "MATERIAL:NOMASS", - "CONSTRUCTION", - "BUILDINGSURFACE:DETAILED", - "FENESTRATIONSURFACE:DETAILED", - "SCHEDULE:DAY:INTERVAL", - "SCHEDULE:WEEK:DAILY", - "SCHEDULE:YEAR", - ] + useful_idf_objects: list[str] = Field( + [ + "WINDOWMATERIAL:GAS", + "WINDOWMATERIAL:GLAZING", + "WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM", + "MATERIAL", + "MATERIAL:NOMASS", + "CONSTRUCTION", + "BUILDINGSURFACE:DETAILED", + "FENESTRATIONSURFACE:DETAILED", + "SCHEDULE:DAY:INTERVAL", + "SCHEDULE:WEEK:DAILY", + "SCHEDULE:YEAR", + ] + ) # List of Available SQLite Tables # Ref: https://bigladdersoftware.com/epx/docs/8-3/output-details-and-examples # /eplusout.sql.html#schedules-table - available_sqlite_tables: dict = dict( - ComponentSizes={"PrimaryKey": ["ComponentSizesIndex"], "ParseDates": []}, - ConstructionLayers={"PrimaryKey": ["ConstructionIndex"], "ParseDates": []}, - Constructions={"PrimaryKey": ["ConstructionIndex"], "ParseDates": []}, - Materials={"PrimaryKey": ["MaterialIndex"], "ParseDates": []}, - NominalBaseboardHeaters={ + available_sqlite_tables: ClassVar[dict] = { + "ComponentSizes": {"PrimaryKey": ["ComponentSizesIndex"], "ParseDates": []}, + "ConstructionLayers": {"PrimaryKey": ["ConstructionIndex"], "ParseDates": []}, + "Constructions": {"PrimaryKey": ["ConstructionIndex"], "ParseDates": []}, + "Materials": {"PrimaryKey": ["MaterialIndex"], "ParseDates": []}, + "NominalBaseboardHeaters": { "PrimaryKey": ["NominalBaseboardHeaterIndex"], "ParseDates": [], }, - NominalElectricEquipment={ + "NominalElectricEquipment": { "PrimaryKey": ["NominalElectricEquipmentIndex"], "ParseDates": [], }, - NominalGasEquipment={ + "NominalGasEquipment": { "PrimaryKey": ["NominalGasEquipmentIndex"], "ParseDates": [], }, - NominalHotWaterEquipment={ + "NominalHotWaterEquipment": { "PrimaryKey": ["NominalHotWaterEquipmentIndex"], "ParseDates": [], }, - NominalInfiltration={ + "NominalInfiltration": { "PrimaryKey": ["NominalInfiltrationIndex"], "ParseDates": [], }, - NominalLighting={"PrimaryKey": ["NominalLightingIndex"], "ParseDates": []}, - NominalOtherEquipment={ + "NominalLighting": {"PrimaryKey": ["NominalLightingIndex"], "ParseDates": []}, + "NominalOtherEquipment": { "PrimaryKey": ["NominalOtherEquipmentIndex"], "ParseDates": [], }, - NominalPeople={"PrimaryKey": ["NominalPeopleIndex"], "ParseDates": []}, - NominalSteamEquipment={ + "NominalPeople": {"PrimaryKey": ["NominalPeopleIndex"], "ParseDates": []}, + "NominalSteamEquipment": { "PrimaryKey": ["NominalSteamEquipmentIndex"], "ParseDates": [], }, - NominalVentilation={ + "NominalVentilation": { "PrimaryKey": ["NominalVentilationIndex"], "ParseDates": [], }, - ReportData={"PrimaryKey": ["ReportDataIndex"], "ParseDates": []}, - ReportDataDictionary={ + "ReportData": {"PrimaryKey": ["ReportDataIndex"], "ParseDates": []}, + "ReportDataDictionary": { "PrimaryKey": ["ReportDataDictionaryIndex"], "ParseDates": [], }, - ReportExtendedData={ + "ReportExtendedData": { "PrimaryKey": ["ReportExtendedDataIndex"], "ParseDates": [], }, - RoomAirModels={"PrimaryKey": ["ZoneIndex"], "ParseDates": []}, - Schedules={"PrimaryKey": ["ScheduleIndex"], "ParseDates": []}, - Surfaces={"PrimaryKey": ["SurfaceIndex"], "ParseDates": []}, - SystemSizes={ + "RoomAirModels": {"PrimaryKey": ["ZoneIndex"], "ParseDates": []}, + "Schedules": {"PrimaryKey": ["ScheduleIndex"], "ParseDates": []}, + "Surfaces": {"PrimaryKey": ["SurfaceIndex"], "ParseDates": []}, + "SystemSizes": { "PrimaryKey": ["SystemSizesIndex"], "ParseDates": {"PeakHrMin": "%m/%d %H:%M:%S"}, }, - Time={"PrimaryKey": ["TimeIndex"], "ParseDates": []}, - ZoneGroups={"PrimaryKey": ["ZoneGroupIndex"], "ParseDates": []}, - Zones={"PrimaryKey": ["ZoneIndex"], "ParseDates": []}, - ZoneLists={"PrimaryKey": ["ZoneListIndex"], "ParseDates": []}, - ZoneSizes={"PrimaryKey": ["ZoneSizesIndex"], "ParseDates": []}, - ZoneInfoZoneLists={"PrimaryKey": ["ZoneListIndex"], "ParseDates": []}, - Simulations={ + "Time": {"PrimaryKey": ["TimeIndex"], "ParseDates": []}, + "ZoneGroups": {"PrimaryKey": ["ZoneGroupIndex"], "ParseDates": []}, + "Zones": {"PrimaryKey": ["ZoneIndex"], "ParseDates": []}, + "ZoneLists": {"PrimaryKey": ["ZoneListIndex"], "ParseDates": []}, + "ZoneSizes": {"PrimaryKey": ["ZoneSizesIndex"], "ParseDates": []}, + "ZoneInfoZoneLists": {"PrimaryKey": ["ZoneListIndex"], "ParseDates": []}, + "Simulations": { "PrimaryKey": ["SimulationIndex"], "ParseDates": {"TimeStamp": {"format": "YMD=%Y.%m.%d %H:%M"}}, }, - EnvironmentPeriods={"PrimaryKey": ["EnvironmentPeriodIndex"], "ParseDates": []}, - TabularData={"PrimaryKey": ["TabularDataIndex"], "ParseDates": []}, - Strings={"PrimaryKey": ["StringIndex"], "ParseDates": []}, - StringTypes={"PrimaryKey": ["StringTypeIndex"], "ParseDates": []}, - TabularDataWithStrings={"PrimaryKey": ["TabularDataIndex"], "ParseDates": []}, - Errors={"PrimaryKey": ["ErrorIndex"], "ParseDates": []}, - ) + "EnvironmentPeriods": {"PrimaryKey": ["EnvironmentPeriodIndex"], "ParseDates": []}, + "TabularData": {"PrimaryKey": ["TabularDataIndex"], "ParseDates": []}, + "Strings": {"PrimaryKey": ["StringIndex"], "ParseDates": []}, + "StringTypes": {"PrimaryKey": ["StringTypeIndex"], "ParseDates": []}, + "TabularDataWithStrings": {"PrimaryKey": ["TabularDataIndex"], "ParseDates": []}, + "Errors": {"PrimaryKey": ["ErrorIndex"], "ParseDates": []}, + } zone_weight: ZoneWeight = ZoneWeight(n=0) @@ -167,7 +171,7 @@ class Settings(BaseSettings, arbitrary_types_allowed=True, validate_assignment=T "for ENERGYPLUS_VERSION in os.environ", ) - energyplus_location: Optional[DirectoryPath] = Field( + energyplus_location: DirectoryPath | None = Field( None, validation_alias="ENERGYPLUS_LOCATION", description="Root directory of the EnergyPlus install.", @@ -194,9 +198,9 @@ def initialize_units(cls, v): # After settings are loaded, import other modules from .eplus_interface.version import EnergyPlusVersion # noqa: E402 from .idfclass import IDF # noqa: E402 -from .umi_template import ( - BuildingTemplate, # noqa: E402 - UmiTemplateLibrary, # noqa: E402 +from .umi_template import ( # noqa: E402 + BuildingTemplate, + UmiTemplateLibrary, ) from .utils import clear_cache, config, parallel_process # noqa: E402 diff --git a/archetypal/cli.py b/archetypal/cli.py index cb6cecbd1..bfd78755f 100644 --- a/archetypal/cli.py +++ b/archetypal/cli.py @@ -18,7 +18,7 @@ from .eplus_interface.exceptions import EnergyPlusVersionError from .eplus_interface.version import EnergyPlusVersion -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]} class CliConfig: @@ -234,17 +234,18 @@ def reduce(ctx, idf, output, weather, cores, all_zones, as_version): def validate_energyplusversion(ctx, param, value): try: return EnergyPlusVersion(value) - except EnergyPlusVersionError: - raise click.BadParameter("invalid energyplus version") + except EnergyPlusVersionError as e: + raise click.BadParameter("invalid energyplus version") from e def validate_paths(ctx, param, value): try: file_paths = set_filepaths(value) file_list = "\n".join([f"{i}. " + str(file.name) for i, file in enumerate(file_paths)]) + except FileNotFoundError as e: + raise click.BadParameter("no files were found.") from e + else: return file_paths, file_list - except FileNotFoundError: - raise click.BadParameter("no files were found.") @cli.command() @@ -291,22 +292,19 @@ def transition(idf, to_version, cores, yes): log( f"executing {len(file_paths)} file(s):\n{file_list}", ) - if not yes: - overwrite = click.confirm("Would you like to overwrite the file(s)?") - else: - overwrite = False + overwrite = click.confirm("Would you like to overwrite the file(s)?") if not yes else False start_time = time.time() to_version = to_version.dash rundict = { - file: dict( - idfname=file, - as_version=to_version, - check_required=False, - check_length=False, - overwrite=overwrite, - prep_outputs=False, - ) + file: { + "idfname": file, + "as_version": to_version, + "check_required": False, + "check_length": False, + "overwrite": overwrite, + "prep_outputs": False, + } for i, file in enumerate(file_paths) } results = parallel_process( @@ -349,12 +347,12 @@ def set_filepaths(idf): set of Path: The set of a list of paths """ if not isinstance(idf, (list, tuple)): - raise ValueError("A list must be passed") + raise TypeError("A list must be passed") idf = tuple(Path(file_or_path).expand() for file_or_path in idf) # make Paths file_paths = () # Placeholder for tuple of paths for file_or_path in idf: if file_or_path.isfile(): # if a file, concatenate into file_paths - file_paths += tuple([file_or_path]) + file_paths += (file_or_path,) elif file_or_path.isdir(): # if a directory, walkdir (recursive) and get *.idf file_paths += tuple(file_or_path.walkfiles("*.idf")) else: @@ -366,11 +364,11 @@ def set_filepaths(idf): settings.logs_folder, ] top = file_or_path.abspath().dirname() - for root, dirs, files in walkdirs(top, excluded_dirs): + for root, _, _ in walkdirs(top, excluded_dirs): pattern = file_or_path.basename() file_paths += tuple(Path(root).files(pattern)) - file_paths = set([f.relpath().expand() for f in file_paths]) # Only keep unique + file_paths = {f.relpath().expand() for f in file_paths} # Only keep unique # values if file_paths: return file_paths diff --git a/archetypal/dataportal.py b/archetypal/dataportal.py index 5b81f649d..f769176f3 100644 --- a/archetypal/dataportal.py +++ b/archetypal/dataportal.py @@ -168,23 +168,25 @@ def tabula_building_details_sheet( code_num, code_variantnumber, ) = code_building.split(".") - except ValueError: + except ValueError as e: msg = ( f'the query "{code_building}" is missing a parameter. Make sure the ' '"code_building" has the form: ' "AT.MT.AB.02.Gen.ReEx.001.001" ) log(msg, lg.ERROR) - raise ValueError(msg) + raise ValueError(msg) from e # Check code country code_country = _resolve_codecountry(code_country) # Check code_buildingsizeclass - if code_buildingsizeclass.upper() not in ["SFH", "TH", "MFH", "AB"]: - raise ValueError( - 'specified code_buildingsizeclass "{}" not supported. Available ' 'values are "SFH", "TH", ' '"MFH" or "AB"' + if code_buildingsizeclass.upper() not in {"SFH", "TH", "MFH", "AB"}: + msg = ( + f'specified code_buildingsizeclass "{code_buildingsizeclass}" not supported. ' + 'Available values are "SFH", "TH", "MFH" or "AB"' ) + raise ValueError(msg) # Check numericals if not isinstance(code_construcionyearclass, str): code_construcionyearclass = str(code_construcionyearclass).zfill(2) @@ -528,12 +530,11 @@ def nrel_bcl_api_request(data): return response_json -def stat_can_request(type, lang="E", dguid="2016A000011124", topic=0, notes=0, stat=0): - """Send a request to the StatCan API via HTTP GET and return the JSON - response. +def stat_can_request(response_format, lang="E", dguid="2016A000011124", topic=0, notes=0, stat=0): + """Send a request to the StatCan API via HTTP GET and return the JSON response. Args: - type (str): "json" or "xml". json = json response format and xml = xml + response_format (str): "json" or "xml". json = json response format and xml = xml response format. lang (str): "E" or "F". E = English and F = French. dguid (str): Dissemination Geography Unique Identifier - DGUID. It is an @@ -558,7 +559,7 @@ def stat_can_request(type, lang="E", dguid="2016A000011124", topic=0, notes=0, s """ prepared_url = ( "https://www12.statcan.gc.ca/rest/census-recensement" - f"/CPR2016.{type}?lang={lang}&dguid={dguid}&topic=" + f"/CPR2016.{response_format}?lang={lang}&dguid={dguid}&topic=" f"{topic}¬es={notes}&stat={stat}" ) @@ -610,10 +611,11 @@ def stat_can_request(type, lang="E", dguid="2016A000011124", topic=0, notes=0, s return response_json -def stat_can_geo_request(type="json", lang="E", geos="PR", cpt="00"): - """ +def stat_can_geo_request(response_format="json", lang="E", geos="PR", cpt="00"): + """Send a request to the StatCan API via HTTP GET and return the JSON response. + Args: - type (str): "json" or "xml". json = json response format and xml = xml + response_format (str): "json" or "xml". json = json response format and xml = xml response format. lang (str): "E" or "F". where: E = English F = French. geos (str): one geographic level code (default = PR). where: CD = Census @@ -630,9 +632,7 @@ def stat_can_geo_request(type="json", lang="E", geos="PR", cpt="00"): 35 = Ontario 46 = Manitoba 47 = Saskatchewan 48 = Alberta 59 = British Columbia 60 = Yukon 61 = Northwest Territories 62 = Nunavut. """ - prepared_url = ( - f"https://www12.statcan.gc.ca/rest/census-recensement/CR2016Geo.{type}?lang={lang}&geos={geos}&cpt={cpt}" - ) + prepared_url = f"https://www12.statcan.gc.ca/rest/census-recensement/CR2016Geo.{response_format}?lang={lang}&geos={geos}&cpt={cpt}" cached_response_json = get_from_cache(prepared_url) diff --git a/archetypal/eplus_interface/basement.py b/archetypal/eplus_interface/basement.py index b5405de1f..052cbfc71 100644 --- a/archetypal/eplus_interface/basement.py +++ b/archetypal/eplus_interface/basement.py @@ -114,12 +114,13 @@ def success_callback(self): """Parse surface temperature and append to IDF file.""" for ep_objects in self.run_dir.glob("EPObjects*"): if ep_objects.exists(): - basement_models = self.idf.__class__( - StringIO(open(ep_objects).read()), - file_version=self.idf.file_version, - as_version=self.idf.as_version, - prep_outputs=False, - ) + with open(ep_objects) as f: + basement_models = self.idf.__class__( + StringIO(f.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 basement_models.idfobjects.values(): diff --git a/archetypal/eplus_interface/energy_plus.py b/archetypal/eplus_interface/energy_plus.py index 87cfccf3b..5b77e15de 100644 --- a/archetypal/eplus_interface/energy_plus.py +++ b/archetypal/eplus_interface/energy_plus.py @@ -45,7 +45,7 @@ def __init__( annual=False, convert=False, design_day=False, - help=False, + help=False, # noqa: A002 idd=None, epmacro=False, output_prefix="eplus", @@ -141,7 +141,7 @@ def __init__(self, idf, tmp): tmp (str or Path): The directory in which the process will be launched. """ super().__init__() - self.p: subprocess.Popen + self.p: subprocess.Popen = None self.std_out = None self.std_err = None self.idf = idf @@ -152,11 +152,10 @@ def __init__(self, idf, tmp): self.tmp = tmp def stop(self): - if self.p.poll() is None: - self.msg_callback("Attempting to cancel simulation ...") - self.cancelled = True - self.p.kill() - self.cancelled_callback(self.std_out, self.std_err) + self.msg_callback("Attempting to cancel simulation ...") + self.cancelled = True + self.p.kill() + self.cancelled_callback(self.std_out, self.std_err) def run(self): """Wrapper around the EnergyPlus command line interface. @@ -194,50 +193,50 @@ def run(self): self.cmd = eplus_exe.cmd() except EnergyPlusVersionError as e: self.exception = e - self.p.kill() # kill process to be sure + if self.p: + self.p.terminate() # terminate process to be sure return - with logging_redirect_tqdm(loggers=[lg.getLogger("archetypal")]): + with logging_redirect_tqdm(loggers=[lg.getLogger("archetypal")]), tqdm( + unit_scale=False, + total=self.idf.energyplus_its if self.idf.energyplus_its > 0 else None, + miniters=1, + desc=f"{eplus_exe.eplus_exe_path} #{self.idf.position}-{self.idf.name}" + if self.idf.position + else f"{eplus_exe.eplus_exe_path} {self.idf.name}", + position=self.idf.position, + ) as progress: # Start process with tqdm bar - with tqdm( - unit_scale=False, - total=self.idf.energyplus_its if self.idf.energyplus_its > 0 else None, - miniters=1, - desc=f"{eplus_exe.eplus_exe_path} #{self.idf.position}-{self.idf.name}" - if self.idf.position - else f"{eplus_exe.eplus_exe_path} {self.idf.name}", - position=self.idf.position, - ) as progress: - self.p = subprocess.Popen( - self.cmd, - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - start_time = time.time() - self.msg_callback("Simulation started") - self.idf._energyplus_its = 0 # reset counter - for line in self.p.stdout: - self.msg_callback(line.decode("utf-8").strip("\n")) - self.idf._energyplus_its += 1 - progress.update() + self.p = subprocess.Popen( + self.cmd, + shell=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + start_time = time.time() + self.msg_callback("Simulation started") + self.idf._energyplus_its = 0 # reset counter + for line in self.p.stdout: + self.msg_callback(line.decode("utf-8").strip("\n")) + self.idf._energyplus_its += 1 + progress.update() - # We explicitly close stdout - self.p.stdout.close() + # We explicitly close stdout + self.p.stdout.close() - # Wait for process to complete - self.p.wait() + # Wait for process to complete + self.p.wait() - # Communicate callbacks - if self.cancelled: - self.msg_callback("Simulation cancelled") - self.cancelled_callback(self.std_out, self.std_err) + # Communicate callbacks + if self.cancelled: + self.msg_callback("Simulation cancelled") + self.cancelled_callback(self.std_out, self.std_err) + else: + if self.p.returncode == 0: + self.msg_callback(f"EnergyPlus Completed in {time.time() - start_time:,.2f} seconds") + self.success_callback() else: - if self.p.returncode == 0: - self.msg_callback(f"EnergyPlus Completed in {time.time() - start_time:,.2f} seconds") - self.success_callback() - else: - self.msg_callback("Simulation failed") - self.failure_callback() + self.msg_callback("Simulation failed") + self.failure_callback() def msg_callback(self, *args, **kwargs): msg, *_ = args @@ -255,7 +254,7 @@ def success_callback(self): pass else: log( - "Files generated at the end of the simulation: %s" % "\n".join(save_dir.files()), + "Files generated at the end of the simulation: {}".format("\n".join(save_dir.files())), lg.DEBUG, name=self.name, ) diff --git a/archetypal/eplus_interface/exceptions.py b/archetypal/eplus_interface/exceptions.py index 03323a27e..b5037c1fb 100644 --- a/archetypal/eplus_interface/exceptions.py +++ b/archetypal/eplus_interface/exceptions.py @@ -36,7 +36,7 @@ class EnergyPlusVersionError(Exception): """EnergyPlus Version call error""" def __init__(self, msg=None, idf_file=None, idf_version=None, ep_version=None): - super(EnergyPlusVersionError, self).__init__(None) + super().__init__(None) self.msg = msg self.idf_file = idf_file self.idf_version = idf_version diff --git a/archetypal/eplus_interface/slab.py b/archetypal/eplus_interface/slab.py index e5bb998b5..1e793628e 100644 --- a/archetypal/eplus_interface/slab.py +++ b/archetypal/eplus_interface/slab.py @@ -114,12 +114,13 @@ 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(): - slab_models = self.idf.__class__( - StringIO(open(temp_schedule).read()), - file_version=self.idf.file_version, - as_version=self.idf.as_version, - prep_outputs=False, - ) + with open(temp_schedule) as f: + slab_models = self.idf.__class__( + StringIO(f.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(): diff --git a/archetypal/eplus_interface/transition.py b/archetypal/eplus_interface/transition.py index 0d158f297..ca917a421 100644 --- a/archetypal/eplus_interface/transition.py +++ b/archetypal/eplus_interface/transition.py @@ -103,8 +103,8 @@ def copytree(src, dst, symlinks=False, ignore=None): if self._trans_exec is None: copytree(self.idf.idfversionupdater_dir, self.running_directory) self._trans_exec = { - EnergyPlusVersion(re.search(r"to-V(([\d]*?)-([\d]*?)-([\d]))", exec).group(1)): exec - for exec in self.running_directory.files("Transition-V*") + EnergyPlusVersion(re.search(r"to-V(([\d]*?)-([\d]*?)-([\d]))", execution).group(1)): execution + for execution in self.running_directory.files("Transition-V*") } return self._trans_exec @@ -116,8 +116,7 @@ def transitions(self) -> list: @property def transitions_generator(self): """Generate transitions.""" - for transition in self.transitions: - yield transition + yield from self.transitions def __str__(self): """Return string representation.""" @@ -143,7 +142,7 @@ class TransitionThread(Thread): def __init__(self, idf, tmp, overwrite=False): """Initialize Thread.""" - super(TransitionThread, self).__init__() + super().__init__() self.overwrite = overwrite self.p = None self.std_out = None @@ -240,8 +239,8 @@ def stop(self): def trans_exec(self) -> dict: """Return dict of {EnergyPlusVersion, executable} for each transitions.""" return { - EnergyPlusVersion(re.search(r"to-V(([\d]*?)-([\d]*?)-([\d]))", exec).group(1)): exec - for exec in self.idf.idfversionupdater_dir.files("Transition-V*") + EnergyPlusVersion(re.search(r"to-V(([\d]*?)-([\d]*?)-([\d]))", execution).group(1)): execution + for execution in self.idf.idfversionupdater_dir.files("Transition-V*") } @property diff --git a/archetypal/eplus_interface/version.py b/archetypal/eplus_interface/version.py index bb11956c1..3cf70e591 100644 --- a/archetypal/eplus_interface/version.py +++ b/archetypal/eplus_interface/version.py @@ -60,7 +60,7 @@ def __init__(self, version): version = ".".join(map(str, (version.major, version.minor, version.micro))) if isinstance(version, str) and "-" in version: version = version.replace("-", ".") - super(EnergyPlusVersion, self).__init__(version) + super().__init__(version) if self.dash not in self.valid_versions: raise InvalidEnergyPlusVersion() @@ -71,7 +71,7 @@ def latest(cls): # check if any EnergyPlus install exists if not eplus_homes: - raise Exception( + raise EnergyPlusVersionError( "No EnergyPlus installation found. Make sure you have EnergyPlus " "installed. Go to https://energyplus.net/downloads to download the " "latest version of EnergyPlus." @@ -109,25 +109,18 @@ def current_install_dir(self): """Get the current installation directory for this EnergyPlus version.""" try: return self.install_locations[self.dash] - except KeyError: - raise EnergyPlusVersionError(f"EnergyPlusVersion {self.dash} is not installed.") + except KeyError as e: + raise EnergyPlusVersionError(f"EnergyPlusVersion {self.dash} is not installed.") from e @property - def tuple(self) -> tuple: + def tuple(self) -> tuple[int, int, int]: """Return the version number as a tuple: (major, minor, micro).""" return self.major, self.minor, self.micro @property def valid_versions(self) -> set: """List the idd file version found on this machine.""" - if not self.valid_idd_paths: - # Little hack in case E+ is not installed - _choices = { - settings.ep_version, - } - else: - _choices = set(self.valid_idd_paths.keys()) - + _choices = {settings.ep_version} if not self.valid_idd_paths else set(self.valid_idd_paths.keys()) return _choices @property @@ -161,7 +154,7 @@ def valid_idd_paths(self, value): if not value: try: basedirs_ = [] - for version, basedir in self.install_locations.items(): + for _, basedir in self.install_locations.items(): updater_ = basedir / "PreProcess" / "IDFVersionUpdater" if updater_.exists(): basedirs_.append(updater_.files("*.idd")) @@ -183,9 +176,9 @@ def valid_idd_paths(self, value): if match is None: # Match the version in the whole path match = re.search(r"\d+(-\d+)+", iddname) - version = match.group() + _ = match.group() - value[version] = iddname + value[_] = iddname self._valid_paths = dict(sorted(value.items())) @classmethod @@ -228,8 +221,9 @@ def get_eplus_basedirs(): return Path("/Applications").dirs("EnergyPlus*") else: warnings.warn( - "Archetypal is not compatible with %s. It is only compatible " - "with Windows, Linux or MacOs" % platform.system() + f"Archetypal is not compatible with {platform.system()}. It is only compatible " + "with Windows, Linux or MacOs", + stacklevel=2, ) @@ -246,5 +240,6 @@ def warn_if_not_compatible(): warnings.warn( "No installation of EnergyPlus could be detected on this " "machine. Please install EnergyPlus from https://energyplus.net before " - "using archetypal" + "using archetypal", + stacklevel=2, ) diff --git a/archetypal/idfclass/end_use_balance.py b/archetypal/idfclass/end_use_balance.py index ba7a5917e..8708f7f23 100644 --- a/archetypal/idfclass/end_use_balance.py +++ b/archetypal/idfclass/end_use_balance.py @@ -263,7 +263,7 @@ def apply_multipliers(cls, data, idf): multipliers = idf key = "KeyValue" else: - raise ValueError + raise TypeError return data.mul(multipliers, level=key, axis=1) @classmethod @@ -419,7 +419,7 @@ def separate_gains_and_losses(self, component, level="Key_Name") -> EnergyDataFr Returns: """ - assert component in self.__dict__.keys(), f"{component} is not a valid attribute of EndUseBalance." + assert component in self.__dict__, f"{component} is not a valid attribute of EndUseBalance." component_df = getattr(self, component) assert not component_df.empty, "Expected a component that is not empty." print(component) @@ -500,7 +500,7 @@ def to_df(self, separate_gains_and_losses=False, level="KeyValue"): component_df = getattr(self, component) if not component_df.empty: summary_by_component[component] = component_df.sum(level=level, axis=1).sort_index(axis=1) - for (zone_name, surface_type), data in self.opaque_flow.groupby( + for (_zone_name, surface_type), data in self.opaque_flow.groupby( level=["Zone_Name", "Surface_Type"], axis=1 ): summary_by_component[surface_type] = data.sum(level="Zone_Name", axis=1).sort_index(axis=1) diff --git a/archetypal/idfclass/extensions.py b/archetypal/idfclass/extensions.py index 4381c7484..7ad3510ed 100644 --- a/archetypal/idfclass/extensions.py +++ b/archetypal/idfclass/extensions.py @@ -1,5 +1,6 @@ """Eppy extensions module.""" +import contextlib import copy from eppy.bunch_subclass import BadEPFieldError @@ -31,7 +32,7 @@ def nameexists(self: EpBunch): @extend_class(EpBunch) def get_default(self: EpBunch, name): """Return the default value of a field""" - if "default" in self.getfieldidd(name).keys(): + if "default" in self.getfieldidd(name): _type = _parse_idd_type(self, name) default_ = next(iter(self.getfieldidd_item(name, "default")), None) return _type(default_) @@ -42,7 +43,7 @@ def get_default(self: EpBunch, name): @extend_class(EpBunch) def to_dict(self: EpBunch): """Get the dict representation of the EpBunch.""" - return {k: v for k, v in zip(self.fieldnames, self.fieldvalues)} + return dict(zip(self.fieldnames, self.fieldvalues)) @extend_class(EpBunch) @@ -85,10 +86,8 @@ def makedict(self: Eplusdata, dictfile, fnamefobject): dt, dtls = self.initdict(dictfile) fnamefobject.seek(0) # make sure to read from the beginning astr = fnamefobject.read() - try: + with contextlib.suppress(AttributeError): astr = astr.decode("ISO-8859-2") - except AttributeError: - pass nocom = removecomment(astr, "!") idfst = nocom # alist = string.split(idfst, ';') @@ -112,7 +111,7 @@ def makedict(self: Eplusdata, dictfile, fnamefobject): # scream if node == "": continue - log("this node -%s-is not present in base dictionary" % node) + log(f"this node -{node}-is not present in base dictionary") self.dt, self.dtls = dt, dtls return dt, dtls @@ -131,14 +130,8 @@ def _parse_idd_type(epbunch, name): - node -> str (name used in connecting HVAC components) """ _type = next(iter(epbunch.getfieldidd_item(name, "type")), "").lower() - if _type == "real": - return float - elif _type == "alpha": - return str - elif _type == "integer": - return int - else: - return str + type_mapping = {"real": float, "alpha": str, "integer": int} + return type_mapping.get(_type, str) # relationship between epbunch output frequency and db. diff --git a/archetypal/idfclass/idf.py b/archetypal/idfclass/idf.py index d76c1b954..e4a6c7a9f 100644 --- a/archetypal/idfclass/idf.py +++ b/archetypal/idfclass/idf.py @@ -4,6 +4,9 @@ different forms. """ +from __future__ import annotations + +import contextlib import io import itertools import logging @@ -18,14 +21,14 @@ import uuid import warnings from collections import defaultdict +from collections.abc import Iterable from io import IOBase, StringIO from itertools import chain from math import isclose -from typing import IO, Iterable, Literal, Optional, Tuple, Union - -ReportingFrequency = Literal["Annual", "Monthly", "Daily", "Hourly", "Timestep"] +from typing import IO, ClassVar, Literal import eppy +import numpy as np import pandas as pd from energy_pandas import EnergySeries from eppy.bunch_subclass import BadEPFieldError @@ -35,6 +38,7 @@ from pandas import DataFrame, Series from pandas.errors import ParserError from path import Path +from sigfig import round from tabulate import tabulate from tqdm.auto import tqdm @@ -61,6 +65,8 @@ from geomeppy.patches import EpBunch, idfreader1, obj2bunch from geomeppy.recipes import _is_window, window_vertices_given_wall +ReportingFrequency = Literal["Annual", "Monthly", "Daily", "Hourly", "Timestep"] + def find_and_launch(app_name, app_path_guess, file_path): app_path = shutil.which(app_name, path=app_path_guess) @@ -74,6 +80,30 @@ def find_and_launch(app_name, app_path_guess, file_path): ) +class SimulationNotRunError(Exception): + """Exception raised when simulation has not been run.""" + + def __init__(self): + super().__init__("Call IDF.simulate() at least once to get a list of possible meters") + + +class ScheduleNotFoundError(Exception): + """Exception raised when a schedule is not found in the IDF file.""" + + def __init__(self, name, sch_type, idf_name): + self.name = name + self.sch_type = sch_type + self.idf_name = idf_name + super().__init__(f'Unable to find schedule "{name}" of type "{sch_type}" in idf file "{idf_name}"') + + +class ModelInRelativeCoordinatesError(Exception): + """Exception raised when the model is in relative coordinates and must be translated to world coordinates.""" + + def __init__(self): + super().__init__("Model is in relative coordinates and must be translated to world using IDF.to_world().") + + class IDF(GeomIDF): """Class for loading and parsing idf models. @@ -83,14 +113,14 @@ class IDF(GeomIDF): eppy.modeleditor.IDF class. """ - IDD = {} - IDDINDEX = {} - BLOCK = {} + IDD: ClassVar[dict] = {} + IDDINDEX: ClassVar[dict] = {} + BLOCK: ClassVar[dict] = {} OUTPUTTYPES = ("standard", "nocomment1", "nocomment2", "compressed") # dependencies: dict of - _dependencies = { + _dependencies: ClassVar[dict] = { "idd_info": ["iddname", "idfname"], "idd_index": ["iddname", "idfname"], "idd_version": ["iddname", "idfname"], @@ -145,8 +175,8 @@ class IDF(GeomIDF): "energyplus_its": ["annual", "design_day"], "tmp_dir": ["idfobjects"], } - _independant_vars = set(chain(*list(_dependencies.values()))) - _dependant_vars = set(_dependencies.keys()) + _independant_vars: ClassVar[set] = set(chain(*list(_dependencies.values()))) + _dependant_vars: ClassVar[set] = set(_dependencies.keys()) _initial_postition = itertools.count(start=1) @@ -177,13 +207,13 @@ def __set_on_dependencies(self, key, value): if key in self._independant_vars: self._reset_dependant_vars(key) key = f"_{key}" - super(IDF, self).__setattr__(key, value) + super().__setattr__(key, value) def __init__( self, - idfname: Optional[Union[str, IO, Path]] = None, + idfname: str | IO | Path | None = None, epw=None, - as_version: Union[str, EnergyPlusVersion] = None, + as_version: str | EnergyPlusVersion = None, annual=False, design_day=False, expandobjects=False, @@ -202,7 +232,7 @@ def __init__( output_directory=None, outputtype="standard", encoding=None, - iddname: Optional[Union[str, IO, Path]] = None, + iddname: str | IO | Path | None = None, reporting_frequency: ReportingFrequency = "Monthly", **kwargs, ): @@ -295,33 +325,28 @@ def __init__( self.outputtype = outputtype self.original_idfname = self.idfname # Save original - try: - # load the idf object by asserting self.idd_info - assert self.idd_info - except Exception as e: - raise e - else: - self._original_cache = hash_model(self) - if self.as_version is not None: - if self.file_version < self.as_version: - self.upgrade(to_version=self.as_version, overwrite=False) - finally: - # Set model outputs - self._outputs = Outputs( - idf=self, - include_html=False, - include_sqlite=False, - reporting_frequency=reporting_frequency, - ) - if self.prep_outputs: - self._outputs.include_html = True - self._outputs.include_sqlite = True - self._outputs.add_basics() - if isinstance(self.prep_outputs, list): - self._outputs.add_custom(outputs=self.prep_outputs) - self._outputs.add_profile_gas_elect_outputs() - self._outputs.add_umi_template_outputs() - self._outputs.apply() + if not self.idd_info: + raise ValueError("IDD info is not loaded") + self._original_cache = hash_model(self) + if self.as_version is not None and self.file_version < self.as_version: + self.upgrade(to_version=self.as_version, overwrite=False) + + # Set model outputs + self._outputs = Outputs( + idf=self, + include_html=False, + include_sqlite=False, + reporting_frequency=reporting_frequency, + ) + if self.prep_outputs: + self._outputs.include_html = True + self._outputs.include_sqlite = True + self._outputs.add_basics() + if isinstance(self.prep_outputs, list): + self._outputs.add_custom(outputs=self.prep_outputs) + self._outputs.add_profile_gas_elect_outputs() + self._outputs.add_umi_template_outputs() + self._outputs.apply() @property def outputtype(self): @@ -382,9 +407,9 @@ def from_example_files(cls, example_name, epw=None, **kwargs): example_files_dir: Path = eplus_version.current_install_dir / "ExampleFiles" try: file = next(iter(Pathlib(example_files_dir).rglob(f"{example_name.stem}.idf"))) - except StopIteration: - full_list = list(map(lambda x: str(x.name), example_files_dir.files("*.idf"))) - raise ValueError(f"Choose from: {sorted(full_list)}") + except StopIteration as e: + full_list = [str(x.name) for x in example_files_dir.files("*.idf")] + raise ValueError(f"Choose from: {sorted(full_list)}") from e if epw is not None: epw = Path(epw) @@ -392,9 +417,9 @@ def from_example_files(cls, example_name, epw=None, **kwargs): dir_weather_data_ = eplus_version.current_install_dir / "WeatherData" try: epw = next(iter(Pathlib(dir_weather_data_).rglob(f"{epw.stem}.epw"))) - except StopIteration: - full_list = list(map(lambda x: str(x.name), dir_weather_data_.files("*.epw"))) - raise ValueError(f"Choose EPW from: {sorted(full_list)}") + except StopIteration as e: + full_list = [str(x.name) for x in dir_weather_data_.files("*.epw")] + raise ValueError(f"Choose EPW from: {sorted(full_list)}") from e return cls(file, epw=epw, **kwargs) def setiddname(self, iddname, testing=False): @@ -506,12 +531,11 @@ def idd_version(self) -> tuple: def iddname(self) -> Path: """Get or set the iddname path used to parse the idf model.""" if self._iddname is None: - if self.as_version is not None: - if self.file_version > self.as_version: - raise EnergyPlusVersionError( - f"{self.as_version} cannot be lower then " - f"the version number set in the file: {self.file_version}" - ) + if self.as_version is not None and self.file_version > self.as_version: + raise EnergyPlusVersionError( + f"{self.as_version} cannot be lower then " + f"the version number set in the file: {self.file_version}" + ) self._iddname = self.file_version.current_idd_path return self._iddname @@ -593,7 +617,7 @@ def output_suffix(self, value): self._output_suffix = value @property - def idfname(self) -> Union[Path, StringIO]: + def idfname(self) -> Path | StringIO: """Path: The path of the active (parsed) idf model.""" if self._idfname is None: if self.as_version is None: @@ -810,7 +834,7 @@ def sim_id(self) -> str: # endregion @property - def sim_info(self) -> Optional[DataFrame]: + def sim_info(self) -> DataFrame | None: """DataFrame: Unique number generated for a simulation.""" if self.sql_file is not None: with sqlite3.connect(self.sql_file) as conn: @@ -821,7 +845,7 @@ def sim_info(self) -> Optional[DataFrame]: return None @property - def sim_timestamp(self) -> Union[str, Series]: + def sim_timestamp(self) -> str | Series: """Return the simulation timestamp or "Never" if not ran yet.""" if self.sim_info is None: return "Never" @@ -873,8 +897,8 @@ def sql(self) -> dict: if sql_object not in self.idfobjects["Output:SQLite".upper()]: self.addidfobject(sql_object) return self.simulate().sql() - except Exception as e: - raise e + except Exception: + raise else: self._sql = sql_dict return self._sql @@ -952,7 +976,7 @@ def open_mtd(self): """Open .mtd file in browser. This file contains the “meter details” for the run. This shows what report - variables are on which meters and vice versa – which meters contain what + variables are on which meters and vice versa - which meters contain what report variables. """ import webbrowser @@ -1003,12 +1027,11 @@ def net_conditioned_building_area(self) -> float: zone: EpBunch for zone in zones: for surface in zone.zonesurfaces: - if hasattr(surface, "tilt"): - if surface.tilt == 180.0: - part_of = int(zone.Part_of_Total_Floor_Area.upper() != "NO") - multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) + if hasattr(surface, "tilt") and surface.tilt == 180.0: + part_of = int(zone.Part_of_Total_Floor_Area.upper() != "NO") + multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) - area += surface.area * multiplier * part_of + area += surface.area * multiplier * part_of self._area_conditioned = area return self._area_conditioned @@ -1033,12 +1056,11 @@ def unconditioned_building_area(self) -> float: zone: EpBunch for zone in zones: for surface in zone.zonesurfaces: - if hasattr(surface, "tilt"): - if surface.tilt == 180.0: - part_of = int(zone.Part_of_Total_Floor_Area.upper() == "NO") - multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) + if hasattr(surface, "tilt") and surface.tilt == 180.0: + part_of = int(zone.Part_of_Total_Floor_Area.upper() == "NO") + multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) - area += surface.area * multiplier * part_of + area += surface.area * multiplier * part_of self._area_unconditioned = area return self._area_unconditioned @@ -1062,11 +1084,10 @@ def total_building_area(self) -> float: zone: EpBunch for zone in zones: for surface in zone.zonesurfaces: - if hasattr(surface, "tilt"): - if surface.tilt == 180.0: - multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) + if hasattr(surface, "tilt") and surface.tilt == 180.0: + multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) - area += surface.area * multiplier + area += surface.area * multiplier self._area_total = area return self._area_total @@ -1122,10 +1143,13 @@ def partition_ratio(self) -> float: for surf in zone.zonesurfaces if surf.key.upper() not in ["INTERNALMASS", "WINDOWSHADINGCONTROL"] ]: - if hasattr(surface, "tilt"): - if surface.tilt == 90.0 and surface.Outside_Boundary_Condition != "Outdoors": - multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) - partition_lineal += surface.width * multiplier + if ( + hasattr(surface, "tilt") + and surface.tilt == 90.0 + and surface.Outside_Boundary_Condition != "Outdoors" + ): + multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) + partition_lineal += surface.width * multiplier self._partition_ratio = partition_lineal / max( self.net_conditioned_building_area, self.unconditioned_building_area ) @@ -1222,8 +1246,8 @@ def meters(self) -> Meters: if self._meters is None: try: self.simulation_dir.files("*.mdd") - except FileNotFoundError: - raise Exception("call IDF.simulate() at least once to get a list of possible meters") + except FileNotFoundError as e: + raise SimulationNotRunError() from e else: self._meters = Meters(self) return self._meters @@ -1338,7 +1362,7 @@ def simulate(self, force=False, **kwargs): """ # First, update keys with new values for key, value in kwargs.items(): - if f"_{key}" in self.__dict__.keys(): + if f"_{key}" in self.__dict__: setattr(self, key, value) else: log( @@ -1349,14 +1373,13 @@ def simulate(self, force=False, **kwargs): if self.simulation_dir.exists() and not force: # don't simulate if results exists return self - if self.as_version is not None: - if self.as_version != EnergyPlusVersion(self.idd_version): - raise EnergyPlusVersionError( - None, - self.idfname, - EnergyPlusVersion(self.idd_version), - self.as_version, - ) + if self.as_version is not None and self.as_version != EnergyPlusVersion(self.idd_version): + raise EnergyPlusVersionError( + None, + self.idfname, + EnergyPlusVersion(self.idd_version), + self.as_version, + ) include = self.include if isinstance(include, str): @@ -1390,7 +1413,7 @@ def simulate(self, force=False, **kwargs): e = expandobjects_thread.exception if e is not None: raise e - if expandobjects_thread.cancelled: + elif expandobjects_thread.cancelled: return self # Run the Basement preprocessor program if necessary @@ -1409,7 +1432,7 @@ def simulate(self, force=False, **kwargs): e = basement_thread.exception if e is not None: raise e - if basement_thread.cancelled: + elif basement_thread.cancelled: return self # Run the Slab preprocessor program if necessary @@ -1429,7 +1452,7 @@ def simulate(self, force=False, **kwargs): e = slab_thread.exception if e is not None: raise e - if slab_thread.cancelled: + elif slab_thread.cancelled: return self # Run the energyplus program @@ -1448,7 +1471,9 @@ def simulate(self, force=False, **kwargs): e = running_simulation_thread.exception if e is not None: raise e - return self + elif running_simulation_thread.cancelled: + return self + return self def savecopy(self, filename, lineendings="default", encoding="latin-1"): """Save a copy of the file with the filename passed. @@ -1464,7 +1489,7 @@ def savecopy(self, filename, lineendings="default", encoding="latin-1"): Returns: Path: The new file path. """ - super(IDF, self).save(filename, lineendings, encoding) + super().save(filename, lineendings, encoding) return Path(filename) def copy(self): @@ -1491,7 +1516,7 @@ def save(self, lineendings="default", encoding="latin-1", **kwargs): Returns: IDF: The IDF model """ - super(IDF, self).save(filename=self.idfname, lineendings=lineendings, encoding=encoding) + super().save(filename=self.idfname, lineendings=lineendings, encoding=encoding) log(f"saved '{self.name}' at '{self.idfname}'") return self @@ -1514,14 +1539,12 @@ def saveas(self, filename, lineendings="default", encoding="latin-1", inplace=Fa Returns: IDF: A new IDF object based on the new location file. """ - super(IDF, self).save(filename=filename, lineendings=lineendings, encoding=encoding) + super().save(filename=filename, lineendings=lineendings, encoding=encoding) import inspect sig = inspect.signature(IDF.__init__) - kwargs = { - key: getattr(self, key) for key in [a for a in sig.parameters] if key not in ["self", "idfname", "kwargs"] - } + kwargs = {key: getattr(self, key) for key in list(sig.parameters) if key not in ["self", "idfname", "kwargs"]} as_idf = IDF(filename, **kwargs) # copy simulation_dir over to new location @@ -1535,12 +1558,10 @@ def saveas(self, filename, lineendings="default", encoding="latin-1", inplace=Fa name = Path(name).basename() else: name = file.basename() - try: - file.copy(as_idf.simulation_dir / name) - except shutil.SameFileError: + with contextlib.suppress(shutil.SameFileError): # A copy of self would have the same files in the simdir and # throw an error. - pass + file.copy(as_idf.simulation_dir / name) if inplace: # If inplace, replace content of self with content of as_idf. self.__dict__.update(as_idf.__dict__) @@ -1584,8 +1605,8 @@ def process_results(self): for file in self.simulation_dir.files(glob) ] ) - except FileNotFoundError: - raise ValueError("No results to process. Have you called IDF.simulate()?") + except FileNotFoundError as e: + raise SimulationNotRunError() from e else: return results @@ -1695,7 +1716,6 @@ def wwr(self, azimuth_threshold=10, round_to=10): def roundto(x, to=10.0): """Round up to closest `to` number.""" - from builtins import round if to and not math.isnan(x): return int(round(x / to)) * to @@ -1712,16 +1732,14 @@ def roundto(x, to=10.0): for surface in [ surf for surf in zone.zonesurfaces if surf.key.upper() not in ["INTERNALMASS", "WINDOWSHADINGCONTROL"] ]: - if isclose(surface.tilt, 90, abs_tol=10): - if surface.Outside_Boundary_Condition.lower() == "outdoors": - surf_azim = roundto(surface.azimuth, to=azimuth_threshold) - total_surface_area[surf_azim] += surface.area * multiplier + if isclose(surface.tilt, 90, abs_tol=10) and surface.Outside_Boundary_Condition.lower() == "outdoors": + surf_azim = roundto(surface.azimuth, to=azimuth_threshold) + total_surface_area[surf_azim] += surface.area * multiplier for subsurface in surface.subsurfaces: if hasattr(subsurface, "tilt"): - if isclose(subsurface.tilt, 90, abs_tol=10): - if subsurface.Surface_Type.lower() == "window": - surf_azim = roundto(subsurface.azimuth, to=azimuth_threshold) - total_window_area[surf_azim] += subsurface.area * multiplier + if isclose(subsurface.tilt, 90, abs_tol=10) and subsurface.Surface_Type.lower() == "window": + surf_azim = roundto(subsurface.azimuth, to=azimuth_threshold) + total_window_area[surf_azim] += subsurface.area * multiplier if isclose(subsurface.tilt, 180, abs_tol=80): total_window_area["sky"] += subsurface.area * multiplier # Fix azimuth = 360 which is the same as azimuth 0 @@ -1730,7 +1748,6 @@ def roundto(x, to=10.0): # Create dataframe with wall_area, window_area and wwr as columns and azimuth # as indexes - from sigfig import round df = ( pd.DataFrame({"wall_area": total_surface_area, "window_area": total_window_area}) @@ -1739,8 +1756,15 @@ def roundto(x, to=10.0): ) df.wall_area = df.wall_area.apply(round, decimals=1) df.window_area = df.window_area.apply(round, decimals=1) - df["wwr"] = (df.window_area / df.wall_area).fillna(0).apply(round, 2) - df["wwr_rounded_%"] = (df.window_area / df.wall_area * 100).fillna(0).apply(lambda x: roundto(x, to=round_to)) + df["wwr"] = ( + (df.window_area / df.wall_area).replace([np.inf, -np.inf], np.nan).fillna(0).apply(round, decimals=2) + ) + df["wwr_rounded_%"] = ( + (df.window_area / df.wall_area * 100) + .replace([np.inf, -np.inf], np.nan) + .fillna(0) + .apply(lambda x: roundto(x, to=round_to)) + ) return df def space_heating_profile( @@ -1873,7 +1897,7 @@ def custom_profile( log(f"Retrieved {name} in {time.time() - start_time:,.2f} seconds") return series - def newidfobject(self, key, **kwargs) -> Optional[EpBunch]: + def newidfobject(self, key, **kwargs) -> EpBunch | None: """Define EpBunch object and add to model. The function will test if the object exists to prevent duplicates. @@ -1895,50 +1919,48 @@ def newidfobject(self, key, **kwargs) -> Optional[EpBunch]: Returns: EpBunch: the object, if successful None: If an error occured. + Raises: + BadEPFieldError: If a field is not valid. """ # get list of objects existing_objs = self.idfobjects[key] # a list # create new object - try: - new_object = self.anidfobject(key, **kwargs) - except BadEPFieldError as e: - raise e - else: - # 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(*(d.objidd[0].keys() for d in existing_objs)): - for obj in existing_objs: - self.removeidfobject(obj) - 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 the existing one. - log( - f"object '{new_object}' already exists in {self.name}. " f"Skipping.", - lg.DEBUG, - ) - 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()) + new_object = self.anidfobject(key, **kwargs) + # 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(*(d.objidd[0].keys() for d in existing_objs)): + for obj in existing_objs: self.removeidfobject(obj) - self.addidfobject(new_object) log( - f"{obj} exists but has different attributes; Removed and replaced " f"with {new_object}", + f"{obj} is a 'unique-object'; Removed and replaced with" f" {new_object}", lg.DEBUG, ) - return new_object - else: - # add to model and return - self.addidfobject(new_object) - log(f"object '{new_object}' added to '{self.name}'", lg.DEBUG) - return new_object + self.addidfobject(new_object) + return new_object + if new_object in existing_objs: + # If obj already exists, simply return the existing one. + log( + f"object '{new_object}' already exists in {self.name}. " f"Skipping.", + lg.DEBUG, + ) + 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()) + self.removeidfobject(obj) + self.addidfobject(new_object) + log( + f"{obj} exists but has different attributes; Removed and replaced " f"with {new_object}", + lg.DEBUG, + ) + return new_object + else: + # add to model and return + self.addidfobject(new_object) + log(f"object '{new_object}' added to '{self.name}'", lg.DEBUG) + return new_object def addidfobject(self, new_object) -> EpBunch: """Add an IDF object to the model. @@ -2018,8 +2040,9 @@ def anidfobject(self, key: str, aname: str = "", **kwargs) -> EpBunch: abunch = obj2bunch(self.model, self.idd_info, obj) if aname: warnings.warn( - "The aname parameter should no longer be used (%s)." % aname, + f"The aname parameter should no longer be used ({aname}).", UserWarning, + stacklevel=2, ) namebunch(abunch, aname) for k, v in kwargs.items(): @@ -2034,7 +2057,7 @@ def anidfobject(self, key: str, aname: str = "", **kwargs) -> EpBunch: elif str(e) == "unknown field People_per_Zone_Floor_Area": abunch["People_per_Floor_Area"] = v else: - raise e + raise abunch.theidf = self return abunch @@ -2071,8 +2094,8 @@ def get_schedule_epbunch(self, name, sch_type=None): if sch_type is None: try: return self.schedules_dict[name.upper()] - except KeyError: - raise KeyError(f'Unable to find schedule "{name}" of type "{sch_type}" ' f'in idf file "{self.name}"') + except KeyError as e: + raise ScheduleNotFoundError(name, sch_type, self.name) from e else: return self.getobject(sch_type.upper(), name) @@ -2132,7 +2155,7 @@ def _get_used_schedules(self, yearly_only=False): if obj.key.upper() not in schedule_types: for fieldvalue in obj.fieldvalues: try: - if fieldvalue.upper() in all_schedules.keys() and fieldvalue not in used_schedules: + if fieldvalue.upper() in all_schedules and fieldvalue not in used_schedules: used_schedules.append(fieldvalue) except (KeyError, AttributeError): pass @@ -2182,24 +2205,24 @@ def rename(self, objkey, objname, newname): for refname in refnames: objlists = eppy.modeleditor.getallobjlists(self, refname) # [('OBJKEY', refname, fieldindexlist), ...] - for robjkey, refname, fieldindexlist in objlists: + for robjkey, _refname, fieldindexlist in objlists: idfobjects = self.idfobjects[robjkey] for idfobject in idfobjects: for findex in fieldindexlist: # for each field if idfobject[idfobject.objls[findex]].lower() == objname.lower(): idfobject[idfobject.objls[findex]] = newname theobject = self.getobject(objkey, objname) - fieldname = [item for item in theobject.objls if item.endswith("Name")][0] + fieldname = next(item for item in theobject.objls if item.endswith("Name")) theobject[fieldname] = newname return theobject def set_wwr( self, - wwr: float = None, - construction: Optional[str] = None, + wwr: float | None = None, + construction: str | None = None, force: bool = False, - wwr_map: Optional[dict] = None, - surfaces: Optional[Iterable] = None, + wwr_map: dict | None = None, + surfaces: Iterable | None = None, ): """Set Window-to-Wall Ratio of external walls. @@ -2225,7 +2248,7 @@ def set_wwr( # reviewed as of 2021-11-10. try: - ggr: Optional[Idf_MSequence] = self.idfobjects["GLOBALGEOMETRYRULES"][0] + ggr: Idf_MSequence | None = self.idfobjects["GLOBALGEOMETRYRULES"][0] except IndexError: ggr = None @@ -2253,12 +2276,12 @@ def set_wwr( continue # remove all subsurfaces for ss in wall_subsurfaces: - self.rename(ss.key.upper(), ss.Name, "%s window" % wall.Name) + self.rename(ss.key.upper(), ss.Name, f"{wall.Name} window") self.removeidfobject(ss) coords = window_vertices_given_wall(wall, wwr) window = self.newidfobject( "FENESTRATIONSURFACE:DETAILED", - Name="%s window" % wall.Name, + Name=f"{wall.Name} window", Surface_Type="Window", Construction_Name=construction or "", Building_Surface_Name=wall.Name, @@ -2353,7 +2376,7 @@ def to_world(self): if "world" in [o.Coordinate_System.lower() for o in self.idfobjects["GLOBALGEOMETRYRULES"]] or self.translated: log("Model already set as World coordinates", level=lg.WARNING) return - zone_angles = set(z.Direction_of_Relative_North or 0 for z in self.idfobjects["ZONE"]) + zone_angles = {z.Direction_of_Relative_North or 0 for z in self.idfobjects["ZONE"]} # If Zones have Direction_of_Relative_North != 0, model needs to be rotated # before translation. if all(angle != 0 for angle in zone_angles): @@ -2375,7 +2398,7 @@ def to_world(self): } surfaces = {s.Name.upper(): s for s in self.getsurfaces()} subsurfaces = self.getsubsurfaces() - daylighting_refpoints = [p for p in self.idfobjects["DAYLIGHTING:REFERENCEPOINT"]] + daylighting_refpoints = list(self.idfobjects["DAYLIGHTING:REFERENCEPOINT"]) attached_shading_surf_names = [] for g in self.idd_index["ref2names"]["AttachedShadingSurfNames"]: for item in self.idfobjects[g]: @@ -2385,7 +2408,7 @@ def to_world(self): for subsurf in subsurfaces: zone_name = surfaces[subsurf.Building_Surface_Name.upper()].Zone_Name translate([subsurf], zone_origin[zone_name.upper()]) - for surf_name, surf in surfaces.items(): + for _surf_name, surf in surfaces.items(): translate([surf], zone_origin[surf.Zone_Name.upper()]) for day in daylighting_refpoints: zone_name = day.Zone_or_Space_Name @@ -2437,7 +2460,7 @@ def view_model( "relative" in [o.Coordinate_System.lower() for o in self.idfobjects["GLOBALGEOMETRYRULES"]] and self.coords_are_truly_relative ): - raise Exception("Model is in relative coordinates and must be translated to world using IDF.to_world().") + raise ModelInRelativeCoordinatesError() view_idf(idf=self, test=~show) fig = plt.gcf() @@ -2470,7 +2493,7 @@ def coords_are_truly_relative(self): all_zone_origin_at_0 = False return ggr_asks_for_relative and not all_zone_origin_at_0 - def rotate(self, angle: Optional[float] = None, anchor: Tuple[float, float, float] = None): + def rotate(self, angle: float | None = None, anchor: tuple[float, float, float] | None = None): """Rotate the IDF counterclockwise around `anchor` by the angle given (degrees). IF angle is None, rotates to Direction_of_Relative_North specified in Zone @@ -2479,7 +2502,7 @@ def rotate(self, angle: Optional[float] = None, anchor: Tuple[float, float, floa if not angle: bldg_angle = self.idfobjects["BUILDING"][0].North_Axis or 0 log(f"Building North Axis = {bldg_angle}", level=lg.DEBUG) - zone_angles = set(z.Direction_of_Relative_North for z in self.idfobjects["ZONE"]) + zone_angles = {z.Direction_of_Relative_North for z in self.idfobjects["ZONE"]} assert len(zone_angles) == 1, "Not all zone have the same Direction_of_Relative_North" zone_angle, *_ = zone_angles zone_angle = zone_angle or 0 @@ -2490,7 +2513,7 @@ def rotate(self, angle: Optional[float] = None, anchor: Tuple[float, float, floa anchor = Vector3D(*anchor) # Rotate the building - super(IDF, self).rotate(angle, anchor=anchor) + super().rotate(angle, anchor=anchor) log(f"Geometries rotated by {angle} degrees around " f"{anchor or 'building centroid'}") # after building is rotate, change the north axis and zone direction to zero. @@ -2500,14 +2523,14 @@ def rotate(self, angle: Optional[float] = None, anchor: Tuple[float, float, floa # Mark the model as rotated self.rotated = True - def translate(self, vector: Tuple[float, float, float]): + def translate(self, vector: tuple[float, float, float]): """Move the IDF in the direction given by a vector.""" if isinstance(vector, tuple): from geomeppy.geom.vectors import Vector2D vector = Vector2D(*vector) - super(IDF, self).translate(vector=vector) + super().translate(vector=vector) self.translated = True @property @@ -2561,11 +2584,10 @@ def total_envelope_area(self): zone: EpBunch for zone in zones: for surface in zone.zonesurfaces: - if hasattr(surface, "tilt"): - if surface.tilt == 180.0: - multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) + if hasattr(surface, "tilt") and surface.tilt == 180.0: + multiplier = float(zone.Multiplier if zone.Multiplier != "" else 1) - area += surface.area * multiplier + area += surface.area * multiplier self._area_total = area for surface in self.getsurfaces(): if surface.Outside_Boundary_Condition.lower() in ["adiabatic", "surface"]: @@ -2590,13 +2612,13 @@ def _process_csv(file, working_dir, simulname): tables_out.makedirs_p() file.copy(tables_out / "%s_%s.csv" % (file.basename().stripext(), simulname)) return - log("try to store file %s in DataFrame" % file) + log(f"try to store file {file} in DataFrame") try: df = pd.read_csv(file, sep=",", encoding="us-ascii") except ParserError: pass else: - log("file %s stored" % file) + log(f"file {file} stored") return df diff --git a/archetypal/idfclass/meters.py b/archetypal/idfclass/meters.py index 9ef70b037..127a970a9 100644 --- a/archetypal/idfclass/meters.py +++ b/archetypal/idfclass/meters.py @@ -98,10 +98,7 @@ def values( # the environment_type is specified by the simulationcontrol. try: for ctrl in self._idf.idfobjects["SIMULATIONCONTROL"]: - if ctrl.Run_Simulation_for_Weather_File_Run_Periods.lower() == "yes": - environment_type = 3 - else: - environment_type = 1 + environment_type = 3 if ctrl.Run_Simulation_for_Weather_File_Run_Periods.lower() == "yes" else 1 except (KeyError, IndexError, AttributeError): reporting_frequency = 3 report = ReportData.from_sqlite( @@ -136,7 +133,7 @@ def __init__(self, idf, meters_dict: dict): self._idf = idf self._properties = {} - for i, meter in meters_dict.items(): + for _i, meter in meters_dict.items(): meter_name = meter["Key_Name"].replace(":", "__").replace(" ", "_") self._properties[meter_name] = Meter(idf, meter) setattr(self, meter_name, self._properties[meter_name]) @@ -153,11 +150,10 @@ def __repr__(self): for i in inspect.getmembers(self): # to remove private and protected # functions - if not i[0].startswith("_"): + if not i[0].startswith("_") and not inspect.ismethod(i[1]): # To remove other methods that # do not start with an underscore - if not inspect.ismethod(i[1]): - members.append(i) + members.append(i) return f"{len(members)} available meters" @@ -212,9 +208,6 @@ def __repr__(self): for i in inspect.getmembers(self): # to remove private and protected # functions - if not i[0].startswith("_"): - # To remove other methods that - # do not start with an underscore - if not inspect.ismethod(i[1]): - members.append(i) + if not i[0].startswith("_") and not inspect.ismethod(i[1]): + members.append(i) return tabulate(members, headers=("Available subgroups", "Preview")) diff --git a/archetypal/idfclass/outputs.py b/archetypal/idfclass/outputs.py index adcd4b8ba..1d27cbf1e 100644 --- a/archetypal/idfclass/outputs.py +++ b/archetypal/idfclass/outputs.py @@ -1,4 +1,7 @@ -from typing import Iterable +from __future__ import annotations + +from collections.abc import Iterable +from typing import Literal from archetypal.idfclass.end_use_balance import EndUseBalance from archetypal.idfclass.extensions import get_name_attribute @@ -115,10 +118,10 @@ def __init__( """ self.idf = idf self.reporting_frequency = reporting_frequency - self.output_variables = set(a.Variable_Name for a in idf.idfobjects["Output:Variable".upper()]) - self.output_meters = set( + self.output_variables = {a.Variable_Name for a in idf.idfobjects["Output:Variable".upper()]} + self.output_meters = { (get_name_attribute(a), a.Reporting_Frequency) for a in idf.idfobjects["Output:Meter".upper()] - ) + } self.other_outputs = outputs self.output_variables += tuple((v, reporting_frequency) for v in variables) @@ -289,12 +292,12 @@ def add_basics(self): def add_schedules(self): """Adds Schedules object""" - outputs = [{"key": "Output:Schedules".upper(), **dict(Key_Field="Hourly")}] + outputs = [{"key": "Output:Schedules".upper(), **{"Key_Field": "Hourly"}}] for output in outputs: self._other_outputs.append(output) return self - def add_meter_variables(self, format="IDF"): + def add_meter_variables(self, key_field: Literal["IDF", "regular"] = "IDF"): """Generate .mdd file at end of simulation. This file (from the Output:VariableDictionary, regular; and Output:VariableDictionary, IDF; commands) shows all the report meters along with their “availability” @@ -304,12 +307,12 @@ def add_meter_variables(self, format="IDF"): Output Reference) and IDF (ready to be copied and pasted into your Input File). Args: - format (str): Choices are "IDF" and "regul + key_field (str): Choices are IDF, regular Returns: Outputs: self """ - outputs = [dict(key="Output:VariableDictionary".upper(), Key_Field=format)] + outputs = [{"key": "Output:VariableDictionary".upper(), "Key_Field": key_field}] for output in outputs: self._other_outputs.append(output) return self @@ -338,7 +341,7 @@ def add_summary_report(self, summary="AllSummary"): outputs = [ { "key": "Output:Table:SummaryReports".upper(), - **dict(Report_1_Name=summary), + **{"Report_1_Name": summary}, } ] for output in outputs: @@ -361,7 +364,7 @@ def add_sql(self, sql_output_style="SimpleAndTabular"): Returns: Outputs: self """ - outputs = [{"key": "Output:SQLite".upper(), **dict(Option_Type=sql_output_style)}] + outputs = [{"key": "Output:SQLite".upper(), **{"Option_Type": sql_output_style}}] for output in outputs: self._other_outputs.append(output) @@ -390,7 +393,7 @@ def add_output_control(self, output_control_table_style="CommaAndHTML"): outputs = [ { "key": "OutputControl:Table:Style".upper(), - **dict(Column_Separator=output_control_table_style), + **{"Column_Separator": output_control_table_style}, } ] @@ -452,7 +455,7 @@ def add_dxf(self): outputs = [ { "key": "Output:Surfaces:Drawing".upper(), - **dict(Report_Type="DXF", Report_Specifications_1="ThickPolyline"), + **{"Report_Type": "DXF", "Report_Specifications_1": "ThickPolyline"}, } ] for output in outputs: @@ -704,12 +707,12 @@ def apply(self): for variable, reporting_frequency in self.output_variables: self.idf.newidfobject( key="Output:Variable".upper(), - **dict(Variable_Name=variable, Reporting_Frequency=reporting_frequency), + **{"Variable_Name": variable, "Reporting_Frequency": reporting_frequency}, ) for meter, reporting_frequency in self.output_meters: self.idf.newidfobject( key="Output:Meter".upper(), - **dict(Key_Name=meter, Reporting_Frequency=reporting_frequency), + **{"Key_Name": meter, "Reporting_Frequency": reporting_frequency}, ) for output in self.other_outputs: key = output.pop("key", None) diff --git a/archetypal/idfclass/sql.py b/archetypal/idfclass/sql.py index 3091559c4..d5a01fe3f 100644 --- a/archetypal/idfclass/sql.py +++ b/archetypal/idfclass/sql.py @@ -1,16 +1,18 @@ """Module for parsing EnergyPlus SQLite result files into DataFrames.""" +from __future__ import annotations + import logging +from collections.abc import Sequence from datetime import timedelta from sqlite3 import connect -from typing import List, Optional, Sequence, Union +from typing import Literal import numpy as np import pandas as pd from energy_pandas import EnergyDataFrame from pandas import to_datetime from path import Path -from typing_extensions import Literal from archetypal.utils import log @@ -38,7 +40,7 @@ def __init__(self, file_path, output_name, reporting_frequency): self.output_name = output_name self.reporting_frequency = reporting_frequency - def values(self, environment_type: int = 3, units: str = None) -> EnergyDataFrame: + def values(self, environment_type: int = 3, units: str | None = None) -> EnergyDataFrame: """Get the time series values as an EnergyDataFrame. Args: @@ -80,7 +82,7 @@ def values(self, environment_type: int = 3, units: str = None) -> EnergyDataFram class _SqlOutputs: """Represents all the available outputs from the Sql file.""" - def __init__(self, file_path: str, available_outputs: List[tuple]): + def __init__(self, file_path: str, available_outputs: list[tuple]): self._available_outputs = available_outputs self._properties = {} @@ -146,7 +148,7 @@ def tabular_data_keys(self): return self._tabular_data_keys @property - def available_outputs(self) -> List[tuple]: + def available_outputs(self) -> list[tuple]: """Get tuples (OutputName, ReportingFrequency) that can be requested. Any of these outputs when input to data_collections_by_output_name will @@ -227,9 +229,9 @@ def full_html_report(self): def timeseries_by_name( self, - variable_or_meter: Union[str, Sequence], - reporting_frequency: Union[_REPORTING_FREQUENCIES] = "Hourly", - environment_type: Union[Literal[1, 2, 3]] = 3, + variable_or_meter: str | Sequence, + reporting_frequency: _REPORTING_FREQUENCIES = "Hourly", + environment_type: Literal[1, 2, 3] = 3, ) -> EnergyDataFrame: """Get an EnergyDataFrame for specified meters and/or variables. @@ -323,7 +325,7 @@ def timeseries_by_name( return data def tabular_data_by_name( - self, report_name: str, table_name: str, report_for_string: Optional[str] = None + self, report_name: str, table_name: str, report_for_string: str | None = None ) -> pd.DataFrame: """Get (ReportName, TableName) data as DataFrame. @@ -369,7 +371,7 @@ def tabular_data_by_name( pivoted = pivoted.apply(pd.to_numeric, errors="ignore") return pivoted - def _extract_available_outputs(self) -> List: + def _extract_available_outputs(self) -> list: """Extract the list of all available outputs from the SQLite file.""" with connect(self.file_path) as conn: cols = "Name, ReportingFrequency" diff --git a/archetypal/idfclass/util.py b/archetypal/idfclass/util.py index a5842ee9f..2b728805b 100644 --- a/archetypal/idfclass/util.py +++ b/archetypal/idfclass/util.py @@ -1,11 +1,12 @@ """IdfClass utilities.""" +from __future__ import annotations + import hashlib import io import os from collections import OrderedDict from io import StringIO -from typing import List, Union from packaging.version import Version @@ -59,7 +60,7 @@ def hash_model(idfname, **kwargs): hasher.update(buf) # Hashing the kwargs as well - for k, v in kwargs.items(): + for _k, v in kwargs.items(): if isinstance(v, (str, bool)): hasher.update(v.__str__().encode("utf-8")) elif isinstance(v, list): @@ -71,7 +72,7 @@ def hash_model(idfname, **kwargs): return hasher.hexdigest() -def get_idf_version(file: Union[str, io.StringIO], doted=True, encoding=None): +def get_idf_version(file: str | io.StringIO, doted=True, encoding=None): """Get idf version quickly by reading first few lines of idf file containing the 'VERSION' identifier @@ -92,7 +93,7 @@ def get_idf_version(file: Union[str, io.StringIO], doted=True, encoding=None): with open(file, encoding=encoding) as f: txt = f.read() - versions: List = re.findall(r"(?s)(?<=Version,).*?(?=;)", txt, re.IGNORECASE) + versions: list = re.findall(r"(?s)(?<=Version,).*?(?=;)", txt, re.IGNORECASE) for v in versions: version = Version(v.strip()) if doted: @@ -111,9 +112,9 @@ def getoldiddfile(versionid): vlist = versionid.split(".") if len(vlist) == 1: - vlist = vlist + ["0", "0"] + vlist = [*vlist, "0", "0"] elif len(vlist) == 2: - vlist = vlist + ["0"] + vlist = [*vlist, "0"] ver_str = "-".join(vlist) eplus_exe, _ = eppy.runner.run_functions.install_paths(ver_str) eplusfolder = os.path.dirname(eplus_exe) diff --git a/archetypal/idfclass/variables.py b/archetypal/idfclass/variables.py index 921a90253..d6ea86195 100644 --- a/archetypal/idfclass/variables.py +++ b/archetypal/idfclass/variables.py @@ -1,7 +1,7 @@ """EnergyPlus variables module.""" import logging -from typing import Iterable +from collections.abc import Iterable import pandas as pd from energy_pandas import EnergyDataFrame @@ -77,10 +77,7 @@ def values( # the environment_type is specified by the simulationcontrol. try: for ctrl in self._idf.idfobjects["SIMULATIONCONTROL"]: - if ctrl.Run_Simulation_for_Weather_File_Run_Periods.lower() == "yes": - environment_type = 3 - else: - environment_type = 1 + environment_type = 3 if ctrl.Run_Simulation_for_Weather_File_Run_Periods.lower() == "yes" else 1 except (KeyError, IndexError, AttributeError): reporting_frequency = 3 report = ReportData.from_sqlite( @@ -113,7 +110,7 @@ def __init__(self, idf, variables_dict: dict): self._idf = idf self._properties = {} - for i, variable in variables_dict.items(): + for _i, variable in variables_dict.items(): variable_name = self.normalize_output_name(variable["Variable_Name"]) self._properties[variable_name] = Variable(idf, variable) setattr(self, variable_name, self._properties[variable_name]) diff --git a/archetypal/plot.py b/archetypal/plot.py index 1b5128137..592c2b8ab 100644 --- a/archetypal/plot.py +++ b/archetypal/plot.py @@ -56,7 +56,7 @@ def save_and_show(fig, ax, save, show, close, filename, file_format, dpi, axis_o os.makedirs(settings.imgs_folder) path_filename = os.path.join(settings.imgs_folder, os.extsep.join([filename, file_format])) - if not isinstance(ax, (np.ndarray, list)): + if not isinstance(ax, np.ndarray | list): ax = [ax] if file_format == "svg": fig.patch.set_alpha(0.0) @@ -71,10 +71,10 @@ def save_and_show(fig, ax, save, show, close, filename, file_format, dpi, axis_o if extent is None: if len(ax) == 1: if axis_off: - for ax in ax: + for _ax in ax: # if axis is turned off, constrain the saved # figure's extent to the interior of the axis - extent = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted()) + extent = _ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted()) else: pass fig.savefig( diff --git a/archetypal/reportdata.py b/archetypal/reportdata.py index 7cd7cc3f4..9e411f288 100644 --- a/archetypal/reportdata.py +++ b/archetypal/reportdata.py @@ -1,8 +1,9 @@ """""" +from __future__ import annotations + import functools import time -from sqlite3 import OperationalError import numpy as np from pandas import DataFrame, read_sql_query, to_numeric @@ -119,17 +120,17 @@ def from_sqlite( params = {"warmup_flag": warmup_flag} if table_name: conditions, table_name = cls.multiple_conditions("table_name", table_name, "Name") - sql_query = sql_query.replace(";", """ AND (%s);""" % conditions) + sql_query = sql_query.replace(";", f""" AND ({conditions});""") params.update(table_name) if environment_type: conditions, env_name = cls.multiple_conditions("env_name", environment_type, "EnvironmentType") - sql_query = sql_query.replace(";", """ AND (%s);""" % conditions) + sql_query = sql_query.replace(";", f""" AND ({conditions});""") params.update(env_name) if reporting_frequency: conditions, reporting_frequency = cls.multiple_conditions( "reporting_frequency", reporting_frequency, "ReportingFrequency" ) - sql_query = sql_query.replace(";", """ AND (%s);""" % conditions) + sql_query = sql_query.replace(";", f""" AND ({conditions});""") params.update(reporting_frequency) df = cls.execute(conn, sql_query, params) return cls(df) @@ -139,21 +140,27 @@ def multiple_conditions(basename, cond_names, var_name): if not isinstance(cond_names, (list, tuple)): cond_names = [cond_names] cond_names = set(cond_names) - cond_names = {"%s_%s" % (basename, i): name for i, name in enumerate(cond_names)} - conditions = " OR ".join(["%s = @%s" % (var_name, cond_name) for cond_name in cond_names]) + cond_names = {f"{basename}_{i}": name for i, name in enumerate(cond_names)} + conditions = " OR ".join([f"{var_name} = @{cond_name}" for cond_name in cond_names]) return conditions, cond_names @staticmethod def execute(conn, sql_query, params): - try: - # Try regular str read, could fail if wrong encoding - conn.text_factory = str - df = read_sql_query(sql_query, conn, params=params, coerce_float=True) - except OperationalError as e: - # Wring encoding found, the load bytes and decode object - # columns only - raise e - return df + """Execute a sql query and return a DataFrame. + + Args: + conn (sqlite3.Connection): The connection to the sqlite3 database. + sql_query (str): The sql query to execute. + params (dict): The parameters to pass to the query. + + Returns: + DataFrame: a pandas DataFrame. + Raises: + OperationalError: If the encoding is not correct. + """ + # Try regular str read, could fail if wrong encoding + conn.text_factory = str + return read_sql_query(sql_query, conn, params=params, coerce_float=True) @property def _constructor(self): @@ -172,7 +179,7 @@ def filter_report_data( reportdatadictionaryindex=None, value=None, ismeter=None, - type=None, + report_type=None, indexgroup=None, timesteptype=None, keyvalue=None, @@ -193,7 +200,7 @@ def filter_report_data( reportdatadictionaryindex (str or tuple): value (str or tuple): ismeter (str or tuple): - type (str or tuple): + report_type (str or tuple): indexgroup (str or tuple): timesteptype (str or tuple): keyvalue (str or tuple): @@ -272,11 +279,11 @@ def filter_report_data( else self[self.ISMETER] == ismeter ) c_n.append(c_6) - if type: + if report_type: c_7 = ( - conjunction(*[self[self.TYPE] == type for type in type], logical=np.logical_or) - if isinstance(type, tuple) - else self[self.TYPE] == type + conjunction(*[self[self.TYPE] == t for t in report_type], logical=np.logical_or) + if isinstance(report_type, tuple) + else self[self.TYPE] == report_type ) c_n.append(c_7) if indexgroup: diff --git a/archetypal/schedule.py b/archetypal/schedule.py index ba50a8a15..6995f203c 100644 --- a/archetypal/schedule.py +++ b/archetypal/schedule.py @@ -1,18 +1,20 @@ """archetypal Schedule module.""" +from __future__ import annotations + +import contextlib import functools import io import logging as lg from datetime import datetime, timedelta from itertools import groupby -from typing import FrozenSet, List, Union +from typing import Literal import matplotlib.pyplot as plt 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 @@ -285,7 +287,7 @@ def get_compact_weekly_ep_schedule_values(epbunch, start_date, index=None, stric if not weekly_schedules.loc[how].empty: # Loop through days and replace with day:schedule values days = [] - for name, day in weekly_schedules.loc[how].groupby(pd.Grouper(freq="D")): + for _name, day in weekly_schedules.loc[how].groupby(pd.Grouper(freq="D")): if not day.empty: ref = epbunch.get_referenced_object(f"ScheduleDay_Name_{i + 1}") day.loc[:] = _ScheduleParser.get_schedule_values( @@ -421,7 +423,7 @@ def get_compact_ep_schedule_values(epbunch, start_date, strict) -> np.ndarray: from_time = "00:00" how_interpolate = None for field in fields: - if any([spe in field.lower() for spe in field_sets]): + if any(spe in field.lower() for spe in field_sets): f_set, hour, minute, value = _ScheduleParser._field_interpreter(field, epbunch.Name) if f_set.lower() == "through": @@ -562,7 +564,7 @@ def get_yearly_ep_schedule_values(cls, epbunch, start_date, strict) -> np.ndarra how = pd.IndexSlice[start_date:end_date] weeks = [] - for name, week in hourly_values.loc[how].groupby(pd.Grouper(freq="168h")): + for _name, week in hourly_values.loc[how].groupby(pd.Grouper(freq="168h")): if not week.empty: try: week.loc[:] = cls.get_schedule_values( @@ -674,7 +676,7 @@ def _field_interpreter(field, name): elif keywords: # get epBunch of the sizing period statement = " ".join(keywords) - f_set = [s for s in field.split() if "for" in s.lower()][0] + f_set = next(s for s in field.split() if "for" in s.lower()) value = statement.strip() hour = None minute = None @@ -816,12 +818,7 @@ def _field_set(schedule_epbunch, field, start_date, slicer_=None, strict=False): elif field.lower() == "saturday": # return only Saturdays return lambda x: x.index.dayofweek == 5 - elif field.lower() == "summerdesignday": - # return _ScheduleParser.design_day( - # schedule_epbunch, field, slicer_, start_date, strict - # ) - return None - elif field.lower() == "winterdesignday": + elif field.lower() == "summerdesignday" or field.lower() == "winterdesignday": # return _ScheduleParser.design_day( # schedule_epbunch, field, slicer_, start_date, strict # ) @@ -867,7 +864,7 @@ def _date_field_interpretation(field, start_date): date = _ScheduleParser._parse_fancy_string(field, start_date) except Exception as e: msg = f"the schedule contains a " f"Field that is not understood: '{field}'" - raise ValueError(msg, e) + raise ValueError(msg, e) from e else: return date else: @@ -936,15 +933,18 @@ def special_day(schedule_epbunch, field, slicer_, strict, start_date): special_day_types = ["holiday", "customday1", "customday2"] dds = schedule_epbunch.theidf.idfobjects["RunPeriodControl:SpecialDays".upper()] - dd = [ - dd for dd in dds if dd.Special_Day_Type.lower() == field or dd.Special_Day_Type.lower() in special_day_types + special_days = [ + special_day + for special_day in dds + if special_day.Special_Day_Type.lower() == field + or special_day.Special_Day_Type.lower() in special_day_types ] - if len(dd) > 0: - for dd in dd: + if len(special_days) > 0: + for special_day in special_days: # can have more than one special day types - field = dd.Start_Date + field = special_day.Start_Date special_day_start_date = _ScheduleParser._date_field_interpretation(field, start_date) - duration = int(dd.Duration) + duration = int(special_day.Duration) to_date = special_day_start_date + timedelta(days=duration) + timedelta(hours=-1) sp_slicer_.loc[special_day_start_date:to_date] = True @@ -972,12 +972,12 @@ def design_day(schedule_epbunch, field, slicer_, start_date, strict): sp_slicer_ = slicer_.copy() sp_slicer_.loc[:] = False dds = schedule_epbunch.theidf.idfobjects["SizingPeriod:DesignDay".upper()] - dd = [dd for dd in dds if dd.Day_Type.lower() == field] - if len(dd) > 0: - for dd in dd: + design_days = [dd for dd in dds if dd.Day_Type.lower() == field] + if len(design_days) > 0: + for design_day in design_days: # should have found only one design day matching the Day Type - month = dd.Month - day = dd.Day_of_Month + month = design_day.Month + day = design_day.Day_of_Month data = str(month) + "/" + str(day) ep_start_date = _ScheduleParser._date_field_interpretation(data, start_date) ep_orig = datetime(start_date.year, 1, 1) @@ -996,7 +996,7 @@ def design_day(schedule_epbunch, field, slicer_, start_date, strict): f"needed for schedule with Day Type '{field.capitalize()}'" ) raise ValueError(msg) - data = [dd[0].Month, dd[0].Day_of_Month] + data = [design_days[0].Month, design_days[0].Day_of_Month] date = "/".join([str(item).zfill(2) for item in data]) date = _ScheduleParser._date_field_interpretation(date, start_date) return lambda x: x.index == date @@ -1008,10 +1008,10 @@ class Schedule: def __init__( self, Name: str, - start_day_of_the_week: FrozenSet[Literal[0, 1, 2, 3, 4, 5, 6]] = 0, + start_day_of_the_week: frozenset[Literal[0, 1, 2, 3, 4, 5, 6]] = 0, strict: bool = False, - Type: Union[str, ScheduleTypeLimits] = None, - Values: Union[List[Union[int, float]], np.ndarray] = None, + Type: str | ScheduleTypeLimits = None, + Values: list[int | float] | np.ndarray = None, **kwargs, ): """Initialize object. @@ -1032,10 +1032,9 @@ def __init__( Values (ndarray): A 24 or 8760 list of schedule values. **kwargs: """ - try: - super(Schedule, self).__init__(Name, **kwargs) - except Exception: - pass # todo: make this more robust + with contextlib.suppress(Exception): + # todo: make this more robust + super().__init__(Name, **kwargs) self.Name = Name self.strict = strict self.startDayOfTheWeek = start_day_of_the_week @@ -1093,7 +1092,7 @@ def Name(self, value): def from_values( cls, Name: str, - Values: List[Union[float, int]], + Values: list[float | int], Type: str = "Fraction", **kwargs, ): @@ -1179,10 +1178,7 @@ def mean(self): def series(self): """Return an :class:`EnergySeries`.""" index = pd.date_range(start=self.startDate, periods=self.all_values.size, freq="1h") - if self.Type is not None: - units = self.Type.UnitType - else: - units = None + units = self.Type.UnitType if self.Type is not None else None return EnergySeries(self.all_values, index=index, name=self.Name, units=units) @staticmethod @@ -1209,7 +1205,7 @@ def scale(self, diversity=0.1): self.Values = new_values return self - def replace(self, new_values: Union[pd.Series]): + def replace(self, new_values: 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 @@ -1338,8 +1334,8 @@ def to_year_week_day(self): return_inverse=True, return_counts=True, ) - except ValueError: - raise ValueError("Looks like the idf model needs to be rerun with 'annual=True'") + except ValueError as e: + raise ValueError("Looks like the idf model needs to be rerun with 'annual=True'") from e # We use the calendar module to set the week days order import calendar @@ -1374,8 +1370,8 @@ def to_year_week_day(self): blocks = {} from_date = datetime(self.year, 1, 1) bincount = [sum(1 for _ in group) for key, group in groupby(nws + 1) if key] - week_order = {i: v for i, v in enumerate(np.array([key for key, group in groupby(nws + 1) if key]) - 1)} - for i, (week_n, count) in enumerate(zip(week_order, bincount)): + week_order = dict(enumerate(np.array([key for key, group in groupby(nws + 1) if key]) - 1)) + for i, (_, count) in enumerate(zip(week_order, bincount)): week_id = list(dict_week)[week_order[i]] to_date = from_date + timedelta(days=int(count * 7), hours=-1) blocks[i] = YearSchedulePart( @@ -1463,10 +1459,7 @@ def combine(self, other, weights=None, quantity=None): # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # check if the schedule is the same @@ -1490,16 +1483,8 @@ def _conjunction(*conditions, logical=np.logical_and): def _separator(sep): - if sep == "Comma": - return "," - elif sep == "Tab": - return "\t" - elif sep == "Fixed": - return None - elif sep == "Semicolon": - return ";" - else: - return "," + separators = {"Comma": ",", "Tab": "\t", "Fixed": None, "Semicolon": ";"} + return separators.get(sep, ",") def _how(how): @@ -1513,7 +1498,7 @@ def _how(how): return "max" -def get_year_for_first_weekday(weekday: FrozenSet[Literal[0, 1, 2, 3, 4, 5, 6]] = 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: diff --git a/archetypal/simple_glazing.py b/archetypal/simple_glazing.py index 928026790..5eb56d320 100644 --- a/archetypal/simple_glazing.py +++ b/archetypal/simple_glazing.py @@ -55,7 +55,7 @@ def calc_simple_glazing(shgc, u_factor, visible_transmittance=None): "various resistances can be negative." ) - dict = {} + glazing = {} # Step 1. Determine glass-to-glass Resistance. @@ -76,7 +76,7 @@ def calc_simple_glazing(shgc, u_factor, visible_transmittance=None): # Step 4. Determine Layer Solar Transmittance - # The layer’s solar transmittance at normal incidence, `T_sol`, + # The layer's solar transmittance at normal incidence, `T_sol`, # is calculated using correlations that are a function of SHGC and U-Factor. T_sol = t_sol(shgc, u_factor) @@ -96,58 +96,55 @@ def calc_simple_glazing(shgc, u_factor, visible_transmittance=None): # as one of the simple performance indices. # If the user does not enter a value, then the visible properties are the # same as the solar properties. - if visible_transmittance: - T_vis = visible_transmittance - else: - T_vis = T_sol + T_vis = visible_transmittance if visible_transmittance else T_sol R_vis_b = r_vis_b(T_vis) R_vis_f = r_vis_f(T_vis) # sanity checks if T_vis + R_vis_f >= 1.0: - warnings.warn("T_vis + R_vis_f > 1", UserWarning) + warnings.warn("T_vis + R_vis_f > 1", UserWarning, stacklevel=2) T_vis -= (T_vis + R_vis_f - 1) * 1.1 if T_vis + R_vis_b >= 1.0: - warnings.warn("T_vis + R_vis_b > 1", UserWarning) + warnings.warn("T_vis + R_vis_b > 1", UserWarning, stacklevel=2) T_vis -= (T_vis + R_vis_b - 1) * 1.1 # Last Step. Saving results to dict - dict["SolarHeatGainCoefficient"] = shgc - dict["UFactor"] = u_factor - dict["Conductivity"] = Lambda_eff - dict["Thickness"] = Thickness - dict["SolarTransmittance"] = T_sol - dict["SolarReflectanceFront"] = R_s_f - dict["SolarReflectanceBack"] = R_s_b - dict["IRTransmittance"] = 0.0 - dict["VisibleTransmittance"] = T_vis - dict["VisibleReflectanceFront"] = R_vis_f - dict["VisibleReflectanceBack"] = R_vis_b - dict["IREmissivityFront"] = 0.84 - dict["IREmissivityBack"] = 0.84 - dict["DirtFactor"] = 1.0 # Clean glass - - dict["Cost"] = 0 - dict["Density"] = 2500 - dict["EmbodiedCarbon"] = 0 - dict["EmbodiedCarbonStdDev"] = 0 - dict["EmbodiedEnergy"] = 0 - dict["EmbodiedEnergyStdDev"] = 0 - dict["Life"] = 1 - dict["SubstitutionRatePattern"] = [1.0] - dict["SubstitutionTimestep"] = 0 - dict["TransportCarbon"] = 0 - dict["TransportDistance"] = 0 - dict["TransportEnergy"] = 0 - dict["Type"] = "Uncoated" # TODO Further investigation necessary - - dict["Comments"] = ( + glazing["SolarHeatGainCoefficient"] = shgc + glazing["UFactor"] = u_factor + glazing["Conductivity"] = Lambda_eff + glazing["Thickness"] = Thickness + glazing["SolarTransmittance"] = T_sol + glazing["SolarReflectanceFront"] = R_s_f + glazing["SolarReflectanceBack"] = R_s_b + glazing["IRTransmittance"] = 0.0 + glazing["VisibleTransmittance"] = T_vis + glazing["VisibleReflectanceFront"] = R_vis_f + glazing["VisibleReflectanceBack"] = R_vis_b + glazing["IREmissivityFront"] = 0.84 + glazing["IREmissivityBack"] = 0.84 + glazing["DirtFactor"] = 1.0 # Clean glass + + glazing["Cost"] = 0 + glazing["Density"] = 2500 + glazing["EmbodiedCarbon"] = 0 + glazing["EmbodiedCarbonStdDev"] = 0 + glazing["EmbodiedEnergy"] = 0 + glazing["EmbodiedEnergyStdDev"] = 0 + glazing["Life"] = 1 + glazing["SubstitutionRatePattern"] = [1.0] + glazing["SubstitutionTimestep"] = 0 + glazing["TransportCarbon"] = 0 + glazing["TransportDistance"] = 0 + glazing["TransportEnergy"] = 0 + glazing["Type"] = "Uncoated" # TODO Further investigation necessary + + glazing["Comments"] = ( "Properties calculated from Simple Glazing System with " f"SHGC={shgc:.3f}, UFactor={u_factor:.3f} and Tvis={T_vis:.3f}" ) - return dict + return glazing # region Step 1. Determine glass-to-glass Resistance diff --git a/archetypal/template/building_template.py b/archetypal/template/building_template.py index 010001969..73f25a109 100644 --- a/archetypal/template/building_template.py +++ b/archetypal/template/building_template.py @@ -9,6 +9,7 @@ import logging as lg import time from itertools import chain, repeat +from typing import ClassVar import networkx from path import Path @@ -32,7 +33,7 @@ class BuildingTemplate(UmiBase): .. image:: ../images/template/buildingtemplate.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["BuildingTemplate"]] = [] __slots__ = ( "_partition_ratio", @@ -100,7 +101,7 @@ def __init__( Version (str): Version number. **kwargs: other optional keywords passed to other constructors. """ - super(BuildingTemplate, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.PartitionRatio = PartitionRatio self.Lifespan = Lifespan self.Core = Core @@ -474,10 +475,10 @@ def _graph_reduce(self, G): ZoneDefinition: The reduced zone """ if len(G) < 1: - log("No zones for building graph %s" % G.name) + log(f"No zones for building graph {G.name}") return None else: - log("starting reduce process for building %s" % self.Name) + log(f"starting reduce process for building {self.Name}") start_time = time.time() # start from the highest degree node @@ -559,25 +560,25 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Core=self.Core, - Lifespan=self.Lifespan, - PartitionRatio=self.PartitionRatio, - Perimeter=self.Perimeter, - Structure=self.Structure, - Windows=self.Windows, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - YearFrom=self.YearFrom, - YearTo=self.YearTo, - Country=self.Country, - ClimateZone=self.ClimateZone, - Authors=self.Authors, - AuthorEmails=self.AuthorEmails, - Version=self.Version, - ) + return { + "Core": self.Core, + "Lifespan": self.Lifespan, + "PartitionRatio": self.PartitionRatio, + "Perimeter": self.Perimeter, + "Structure": self.Structure, + "Windows": self.Windows, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + "YearFrom": self.YearFrom, + "YearTo": self.YearTo, + "Country": self.Country, + "ClimateZone": self.ClimateZone, + "Authors": self.Authors, + "AuthorEmails": self.AuthorEmails, + "Version": self.Version, + } def get_ref(self, ref): """Get item matching reference id. diff --git a/archetypal/template/conditioning.py b/archetypal/template/conditioning.py index fbf287570..ca62800b9 100644 --- a/archetypal/template/conditioning.py +++ b/archetypal/template/conditioning.py @@ -1,10 +1,12 @@ """archetypal ZoneConditioning.""" import collections +import contextlib import logging as lg import math import sqlite3 from enum import Enum +from typing import TYPE_CHECKING, ClassVar import numpy as np from sigfig import round @@ -14,7 +16,11 @@ from archetypal.reportdata import ReportData from archetypal.template.schedule import UmiSchedule from archetypal.template.umi_base import UmiBase -from archetypal.utils import float_round, log +from archetypal.utils import log +from geomeppy.patches import EpBunch + +if TYPE_CHECKING: + from archetypal.template import ZoneDefinition class UmiBaseEnum(Enum): @@ -89,7 +95,7 @@ class ZoneConditioning(UmiBase): .. image:: ../images/template/zoninfo-conditioning.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["ZoneConditioning"]] = [] __slots__ = ( "_cooling_setpoint", @@ -259,7 +265,7 @@ def __init__( **kwargs: Other arguments passed to the base class :class:`archetypal.template.UmiBase` """ - super(ZoneConditioning, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.MechVentSchedule = MechVentSchedule self.HeatingSchedule = HeatingSchedule self.CoolingSchedule = CoolingSchedule @@ -691,12 +697,12 @@ def to_dict(self): return data_dict @classmethod - def from_zone(cls, zone, zone_ep, nolimit=False, **kwargs): + def from_zone(cls, zone: "ZoneDefinition", zone_ep: EpBunch, nolimit: bool = False, **kwargs): """Create a ZoneConditioning object from a zone. Args: - zone_ep: - zone (archetypal.template.zone.Zone): zone to gets information from. + zone (ZoneDefinition): The zone object. + zone_ep (EpBunch): The EnergyPlus object. """ # If Zone is not part of Conditioned Area, it should not have a ZoneLoad object. if zone.is_part_of_conditioned_floor_area and zone.is_part_of_total_floor_area: @@ -713,7 +719,7 @@ def from_zone(cls, zone, zone_ep, nolimit=False, **kwargs): else: return None - def _set_economizer(self, zone, zone_ep): + def _set_economizer(self, zone: "ZoneDefinition", zone_ep: EpBunch): """Set economizer parameters. Todo: @@ -724,32 +730,28 @@ def _set_economizer(self, zone, zone_ep): https://github.com/MITSustainableDesignLab/basilisk/issues/32 Args: - zone_ep: - zone (Zone): The zone object. + zone (ZoneDefinition): The zone object. + zone_ep (EpBunch): The EnergyPlus object. """ # Economizer controllers_in_idf = zone_ep.theidf.idfobjects["Controller:OutdoorAir".upper()] self.EconomizerType = EconomizerTypes.NoEconomizer # default value - for object in controllers_in_idf: - if object.Economizer_Control_Type == "NoEconomizer": + for obj in controllers_in_idf: + if obj.Economizer_Control_Type == "NoEconomizer": self.EconomizerType = EconomizerTypes.NoEconomizer - elif object.Economizer_Control_Type == "DifferentialEnthalphy": + elif obj.Economizer_Control_Type == "DifferentialEnthalphy": self.EconomizerType = EconomizerTypes.DifferentialEnthalphy - elif object.Economizer_Control_Type == "DifferentialDryBulb": - self.EconomizerType = EconomizerTypes.DifferentialDryBulb - elif object.Economizer_Control_Type == "FixedDryBulb": + elif obj.Economizer_Control_Type == "DifferentialDryBulb" or obj.Economizer_Control_Type == "FixedDryBulb": self.EconomizerType = EconomizerTypes.DifferentialDryBulb - elif object.Economizer_Control_Type == "FixedEnthalpy": + elif obj.Economizer_Control_Type == "FixedEnthalpy" or obj.Economizer_Control_Type == "ElectronicEnthalpy": self.EconomizerType = EconomizerTypes.DifferentialEnthalphy - elif object.Economizer_Control_Type == "ElectronicEnthalpy": - self.EconomizerType = EconomizerTypes.DifferentialEnthalphy - elif object.Economizer_Control_Type == "FixedDewPointAndDryBulb": + elif obj.Economizer_Control_Type == "FixedDewPointAndDryBulb": self.EconomizerType = EconomizerTypes.DifferentialDryBulb - elif object.Economizer_Control_Type == "DifferentialDryBulbAndEnthalpy": + elif obj.Economizer_Control_Type == "DifferentialDryBulbAndEnthalpy": self.EconomizerType = EconomizerTypes.DifferentialEnthalphy - def _set_mechanical_ventilation(self, zone, zone_ep): + def _set_mechanical_ventilation(self, zone: "ZoneDefinition", zone_ep: EpBunch): """Set mechanical ventilation settings. Notes: Mechanical Ventilation in UMI (or Archsim-based models) is applied to @@ -766,8 +768,8 @@ def _set_mechanical_ventilation(self, zone, zone_ep): no `DesignSpecification:OutdoorAir`) and 2) models with Args: - zone_ep: - zone (Zone): The zone object. + zone (ZoneDefinition): The zone object. + zone_ep (EpBunch): The EnergyPlus object. """ # For models with ZoneSizes try: @@ -777,7 +779,7 @@ def _set_mechanical_ventilation(self, zone, zone_ep): self.MinFreshAirPerArea, self.MinFreshAirPerPerson, self.MechVentSchedule, - ) = self.fresh_air_from_zone_sizes(zone) + ) = self.fresh_air_from_zone_sizes(zone, zone_ep) except (ValueError, StopIteration): ( self.IsMechVentOn, @@ -793,11 +795,12 @@ def _set_mechanical_ventilation(self, zone, zone_ep): self.MechVentSchedule = None @staticmethod - def get_equipment_list(zone, zone_ep): + def get_equipment_list(zone: "ZoneDefinition", zone_ep: EpBunch): """Get zone equipment list. Args: - zone_ep: + zone (ZoneDefinition): The zone object. + zone_ep (EpBunch): The EnergyPlus object. """ connections = zone_ep.getreferingobjs(iddgroups=["Zone HVAC Equipment Connections"], fields=["Zone_Name"]) referenced_object = next(iter(connections)).get_referenced_object("Zone_Conditioning_Equipment_List_Name") @@ -807,7 +810,7 @@ def get_equipment_list(zone, zone_ep): [referenced_object.get_referenced_object(f"Zone_Equipment_{i}_Name") for i in range(1, 19)], ) - def fresh_air_from_ideal_loads(self, zone, zone_ep): + def fresh_air_from_ideal_loads(self, zone: "ZoneDefinition", zone_ep: EpBunch): """Resolve fresh air requirements for Ideal Loads Air System. Args: @@ -826,11 +829,12 @@ def fresh_air_from_ideal_loads(self, zone, zone_ep): mechvent_schedule = self._mechanical_schedule_from_outdoorair_object(oa_spec, zone) return True, oa_area, oa_person, mechvent_schedule - def fresh_air_from_zone_sizes(self, zone): + def fresh_air_from_zone_sizes(self, zone: "ZoneDefinition", zone_ep: EpBunch): """Return the Mechanical Ventilation from the ZoneSizes Table in the sql db. Args: - zone (ZoneDefinition): + zone (ZoneDefinition): The zone object. + zone_ep (EpBunch): The EnergyPlus object. Returns: 4-tuple: (IsMechVentOn, MinFreshAirPerArea, MinFreshAirPerPerson, MechVentSchedule) @@ -840,7 +844,7 @@ def fresh_air_from_zone_sizes(self, zone): import pandas as pd # create database connection with sqlite3 - with sqlite3.connect(zone.idf.sql_file) as conn: + with sqlite3.connect(zone_ep.theidf.sql_file) as conn: sql_query = f""" select t.ColumnName, t.Value from TabularDataWithStrings t @@ -850,23 +854,18 @@ def fresh_air_from_zone_sizes(self, zone): oa_design = oa["Minimum Outdoor Air Flow Rate"] # m3/s isoa = oa["Calculated Design Air Flow"] > 0 # True if ach > 0 oa_area = oa_design / zone.area - if zone.occupants > 0: - oa_person = oa_design / zone.occupants - else: - oa_person = np.nan + oa_person = oa_design / zone.occupants if zone.occupants > 0 else np.nan - designobjs = zone._epbunch.getreferingobjs( - iddgroups=["HVAC Design Objects"], fields=["Zone_or_ZoneList_Name"] - ) - obj = next(iter(eq for eq in designobjs if eq.key.lower() == "sizing:zone")) - oa_spec = obj.get_referenced_object("Design_Specification_Outdoor_Air_Object_Name") + designobjs = zone_ep.getreferingobjs(iddgroups=["HVAC Design Objects"], fields=["Zone_or_ZoneList_Name"]) + obj: EpBunch = next(iter(eq for eq in designobjs if eq.key.lower() == "sizing:zone")) + oa_spec: EpBunch = obj.get_referenced_object("Design_Specification_Outdoor_Air_Object_Name") mechvent_schedule = self._mechanical_schedule_from_outdoorair_object(oa_spec, zone) return isoa, oa_area, oa_person, mechvent_schedule - def _mechanical_schedule_from_outdoorair_object(self, oa_spec, zone) -> UmiSchedule: + def _mechanical_schedule_from_outdoorair_object(self, oa_spec: EpBunch, zone: "ZoneDefinition") -> UmiSchedule: """Get mechanical ventilation schedule for zone and OutdoorAir:DesignSpec.""" if oa_spec.Outdoor_Air_Schedule_Name != "": - epbunch = zone.idf.schedules_dict[oa_spec.Outdoor_Air_Schedule_Name.upper()] + epbunch = oa_spec.theidf.schedules_dict[oa_spec.Outdoor_Air_Schedule_Name.upper()] umi_schedule = UmiSchedule.from_epbunch(epbunch) log( f"Mechanical Ventilation Schedule set as {UmiSchedule} for " f"zone {zone.Name}", @@ -878,7 +877,7 @@ def _mechanical_schedule_from_outdoorair_object(self, oa_spec, zone) -> UmiSched # Try to get try: values = ( - self.idf.variables.OutputVariable["Air_System_Outdoor_Air_Minimum_Flow_Fraction"] + oa_spec.theidf.variables.OutputVariable["Air_System_Outdoor_Air_Minimum_Flow_Fraction"] .values() # return values .mean(axis=1) # for more than one system, return mean .values # get numpy array @@ -896,10 +895,10 @@ def _mechanical_schedule_from_outdoorair_object(self, oa_spec, zone) -> UmiSched return UmiSchedule.from_values( Name="AirSystemOutdoorAirMinimumFlowFraction", Values=values, - idf=zone.idf, + idf=oa_spec.theidf, ) - def _set_zone_cops(self, zone, zone_ep, nolimit=False): + def _set_zone_cops(self, zone: "ZoneDefinition", zone_ep: EpBunch, nolimit: bool = False): """Set the zone COPs. Todo: @@ -921,10 +920,9 @@ def _set_zone_cops(self, zone, zone_ep, nolimit=False): ) total_input_heating_energy = 0 for meter in heating_meters: - try: + with contextlib.suppress(KeyError): + # pass if meter does not exist for model total_input_heating_energy += zone_ep.theidf.meters.OutputMeter[meter].values("kWh").sum() - except KeyError: - pass # pass if meter does not exist for model heating_energy_transfer_meters = ( "HeatingCoils__EnergyTransfer", @@ -932,17 +930,14 @@ def _set_zone_cops(self, zone, zone_ep, nolimit=False): ) total_output_heating_energy = 0 for meter in heating_energy_transfer_meters: - try: + with contextlib.suppress(KeyError): + # pass if meter does not exist for model total_output_heating_energy += zone_ep.theidf.meters.OutputMeter[meter].values("kWh").sum() - except KeyError: - pass # pass if meter does not exist for model if total_output_heating_energy == 0: # IdealLoadsAirSystem - try: + with contextlib.suppress(KeyError): total_output_heating_energy += ( zone_ep.theidf.meters.OutputMeter["Heating__EnergyTransfer"].values("kWh").sum() ) - except KeyError: - pass cooling_meters = ( "Cooling__Electricity", @@ -953,10 +948,9 @@ def _set_zone_cops(self, zone, zone_ep, nolimit=False): ) total_input_cooling_energy = 0 for meter in cooling_meters: - try: + with contextlib.suppress(KeyError): + # pass if meter does not exist for model total_input_cooling_energy += zone_ep.theidf.meters.OutputMeter[meter].values("kWh").sum() - except KeyError: - pass # pass if meter does not exist for model cooling_energy_transfer_meters = ( "CoolingCoils__EnergyTransfer", @@ -964,17 +958,14 @@ def _set_zone_cops(self, zone, zone_ep, nolimit=False): ) total_output_cooling_energy = 0 for meter in cooling_energy_transfer_meters: - try: + with contextlib.suppress(KeyError): + # pass if meter does not exist for model total_output_cooling_energy += zone_ep.theidf.meters.OutputMeter[meter].values("kWh").sum() - except KeyError: - pass # pass if meter does not exist for model if total_output_cooling_energy == 0: # IdealLoadsAirSystem - try: + with contextlib.suppress(KeyError): total_output_cooling_energy += ( zone_ep.theidf.meters.OutputMeter["Cooling__EnergyTransfer"].values("kWh").sum() ) - except KeyError: - pass ratio_cooling = total_output_cooling_energy / (total_output_cooling_energy + total_output_heating_energy) ratio_heating = total_output_heating_energy / (total_output_cooling_energy + total_output_heating_energy) @@ -1038,7 +1029,7 @@ def _set_zone_cops(self, zone, zone_ep, nolimit=False): if math.isnan(cooling_cop): self.CoolingCoeffOfPerf = 1 - def _set_thermostat_setpoints(self, zone, zone_ep): + def _set_thermostat_setpoints(self, zone: "ZoneDefinition", zone_ep: EpBunch): """Set the thermostat settings and schedules for this zone. Args: @@ -1111,7 +1102,7 @@ def _set_thermostat_setpoints(self, zone, zone_ep): else: self.IsCoolingOn = True - def _set_heat_recovery(self, zone, zone_ep): + def _set_heat_recovery(self, zone: "ZoneDefinition", zone_ep: EpBunch): """Set the heat recovery parameters for this zone. Heat Recovery Parameters: @@ -1152,13 +1143,13 @@ def _set_heat_recovery(self, zone, zone_ep): comment = "" # iterate over those objects. If the list is empty, it will simply pass. - for object in heat_recovery_in_idf: - if object.key.upper() == "HeatExchanger:AirToAir:FlatPlate".upper(): + for obj in heat_recovery_in_idf: + if obj.key.upper() == "HeatExchanger:AirToAir:FlatPlate".upper(): # Do HeatExchanger:AirToAir:FlatPlate - nsaot = object.Nominal_Supply_Air_Outlet_Temperature - nsait = object.Nominal_Supply_Air_Inlet_Temperature - n2ait = object.Nominal_Secondary_Air_Inlet_Temperature + nsaot = obj.Nominal_Supply_Air_Outlet_Temperature + nsait = obj.Nominal_Supply_Air_Inlet_Temperature + n2ait = obj.Nominal_Secondary_Air_Inlet_Temperature HeatRecoveryEfficiencySensible = (nsaot - nsait) / (n2ait - nsait) # Hypotheses: HeatRecoveryEfficiencySensible - 0.05 HeatRecoveryEfficiencyLatent = HeatRecoveryEfficiencySensible - 0.05 @@ -1170,25 +1161,25 @@ def _set_heat_recovery(self, zone, zone_ep): "Supply Air Inlet T°C)" ) - elif object.key.upper() == "HeatExchanger:AirToAir:SensibleAndLatent".upper(): + elif obj.key.upper() == "HeatExchanger:AirToAir:SensibleAndLatent".upper(): # Do HeatExchanger:AirToAir:SensibleAndLatent calculation - HeatRecoveryEfficiencyLatent = object.Latent_Effectiveness_at_100_Heating_Air_Flow - HeatRecoveryEfficiencySensible = object.Sensible_Effectiveness_at_100_Heating_Air_Flow + HeatRecoveryEfficiencyLatent = obj.Latent_Effectiveness_at_100_Heating_Air_Flow + HeatRecoveryEfficiencySensible = obj.Sensible_Effectiveness_at_100_Heating_Air_Flow HeatRecoveryType = HeatRecoveryTypes.Enthalpy - elif object.key.upper() == "HeatExchanger:Desiccant:BalancedFlow".upper(): + elif obj.key.upper() == "HeatExchanger:Desiccant:BalancedFlow".upper(): # Do HeatExchanger:Dessicant:BalancedFlow # Use default values HeatRecoveryEfficiencyLatent = 0.65 HeatRecoveryEfficiencySensible = 0.7 HeatRecoveryType = HeatRecoveryTypes.Enthalpy - elif object.key.upper() == "HeatExchanger:Desiccant:BalancedFlow:PerformanceDataType1".upper(): + elif obj.key.upper() == "HeatExchanger:Desiccant:BalancedFlow:PerformanceDataType1".upper(): # This is not an actual HeatExchanger, pass pass else: - msg = f'Heat exchanger object "{object}" is not ' "implemented" + msg = f'Heat exchanger object "{obj}" is not ' "implemented" raise NotImplementedError(msg) self.HeatRecoveryEfficiencyLatent = HeatRecoveryEfficiencyLatent @@ -1197,7 +1188,7 @@ def _set_heat_recovery(self, zone, zone_ep): self.Comments += comment @staticmethod - def _get_recoverty_effectiveness(object, zone, zone_ep): + def _get_recoverty_effectiveness(obj, zone, zone_ep): rd = ReportData.from_sql_dict(zone_ep.theidf.sql()) effectiveness = ( rd.filter_report_data( @@ -1211,8 +1202,8 @@ def _get_recoverty_effectiveness(object, zone, zone_ep): .Value.mean() .unstack(level=-1) ) - HeatRecoveryEfficiencySensible = effectiveness.loc[object.Name.upper(), "Heat Exchanger Sensible Effectiveness"] - HeatRecoveryEfficiencyLatent = effectiveness.loc[object.Name.upper(), "Heat Exchanger Latent Effectiveness"] + HeatRecoveryEfficiencySensible = effectiveness.loc[obj.Name.upper(), "Heat Exchanger Sensible Effectiveness"] + HeatRecoveryEfficiencyLatent = effectiveness.loc[obj.Name.upper(), "Heat Exchanger Latent Effectiveness"] return HeatRecoveryEfficiencyLatent, HeatRecoveryEfficiencySensible @staticmethod @@ -1238,32 +1229,6 @@ def _get_design_limits(zone, zone_size, load_name, nolimit=False): LimitType = IdealSystemLimit.NoLimit return LimitType, cap, flow - @staticmethod - def _get_cop(zone, energy_in_list, energy_out_variable_name): - """Calculate COP for heating or cooling systems. - - Args: - zone (archetypal.template.zone.Zone): zone to gets information from - energy_in_list (str or tuple): list of the energy sources for a - system (e.g. [Heating:Electricity, Heating:Gas] for heating - system) - energy_out_variable_name (str or tuple): Name of the output in the - sql for the energy given to the zone from the system (e.g. 'Air - System Total Heating Energy') - """ - from archetypal.reportdata import ReportData - - rd = ReportData.from_sql_dict(zone.idf.sql()) - energy_out = rd.filter_report_data(name=tuple(energy_out_variable_name)) - energy_in = rd.filter_report_data(name=tuple(energy_in_list)) - - outs = energy_out.groupby("KeyValue").Value.sum() - ins = energy_in.Value.sum() - - cop = float_round(outs.sum() / ins, 3) - - return cop - def combine(self, other, weights=None): """Combine two ZoneConditioning objects together. @@ -1285,10 +1250,7 @@ def combine(self, other, weights=None): return other # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # Check if other is not the same as self @@ -1300,31 +1262,33 @@ def combine(self, other, weights=None): if not weights: weights = [self.area, other.area] - new_attr = dict( - CoolingCoeffOfPerf=UmiBase.float_mean(self, other, "CoolingCoeffOfPerf", weights), - CoolingLimitType=max(self.CoolingLimitType, other.CoolingLimitType), - CoolingSetpoint=UmiBase.float_mean(self, other, "CoolingSetpoint", weights), - EconomizerType=max(self.EconomizerType, other.EconomizerType), - HeatRecoveryEfficiencyLatent=UmiBase.float_mean(self, other, "HeatRecoveryEfficiencyLatent", weights), - HeatRecoveryEfficiencySensible=UmiBase.float_mean(self, other, "HeatRecoveryEfficiencySensible", weights), - HeatRecoveryType=max(self.HeatRecoveryType, other.HeatRecoveryType), - HeatingCoeffOfPerf=UmiBase.float_mean(self, other, "HeatingCoeffOfPerf", weights), - HeatingLimitType=max(self.HeatingLimitType, other.HeatingLimitType), - HeatingSetpoint=UmiBase.float_mean(self, other, "HeatingSetpoint", weights), - IsCoolingOn=any((self.IsCoolingOn, other.IsCoolingOn)), - IsHeatingOn=any((self.IsHeatingOn, other.IsHeatingOn)), - IsMechVentOn=any((self.IsMechVentOn, other.IsMechVentOn)), - MaxCoolFlow=UmiBase.float_mean(self, other, "MaxCoolFlow", weights), - MaxCoolingCapacity=UmiBase.float_mean(self, other, "MaxCoolingCapacity", weights), - MaxHeatFlow=UmiBase.float_mean(self, other, "MaxHeatFlow", weights), - MaxHeatingCapacity=UmiBase.float_mean(self, other, "MaxHeatingCapacity", weights), - MinFreshAirPerArea=UmiBase.float_mean(self, other, "MinFreshAirPerArea", weights), - MinFreshAirPerPerson=UmiBase.float_mean(self, other, "MinFreshAirPerPerson", weights), - HeatingSchedule=UmiSchedule.combine(self.HeatingSchedule, other.HeatingSchedule, weights), - CoolingSchedule=UmiSchedule.combine(self.CoolingSchedule, other.CoolingSchedule, weights), - MechVentSchedule=UmiSchedule.combine(self.MechVentSchedule, other.MechVentSchedule, weights), - area=1 if self.area + other.area == 2 else self.area + other.area, - ) + new_attr = { + "CoolingCoeffOfPerf": UmiBase.float_mean(self, other, "CoolingCoeffOfPerf", weights), + "CoolingLimitType": max(self.CoolingLimitType, other.CoolingLimitType), + "CoolingSetpoint": UmiBase.float_mean(self, other, "CoolingSetpoint", weights), + "EconomizerType": max(self.EconomizerType, other.EconomizerType), + "HeatRecoveryEfficiencyLatent": UmiBase.float_mean(self, other, "HeatRecoveryEfficiencyLatent", weights), + "HeatRecoveryEfficiencySensible": UmiBase.float_mean( + self, other, "HeatRecoveryEfficiencySensible", weights + ), + "HeatRecoveryType": max(self.HeatRecoveryType, other.HeatRecoveryType), + "HeatingCoeffOfPerf": UmiBase.float_mean(self, other, "HeatingCoeffOfPerf", weights), + "HeatingLimitType": max(self.HeatingLimitType, other.HeatingLimitType), + "HeatingSetpoint": UmiBase.float_mean(self, other, "HeatingSetpoint", weights), + "IsCoolingOn": any((self.IsCoolingOn, other.IsCoolingOn)), + "IsHeatingOn": any((self.IsHeatingOn, other.IsHeatingOn)), + "IsMechVentOn": any((self.IsMechVentOn, other.IsMechVentOn)), + "MaxCoolFlow": UmiBase.float_mean(self, other, "MaxCoolFlow", weights), + "MaxCoolingCapacity": UmiBase.float_mean(self, other, "MaxCoolingCapacity", weights), + "MaxHeatFlow": UmiBase.float_mean(self, other, "MaxHeatFlow", weights), + "MaxHeatingCapacity": UmiBase.float_mean(self, other, "MaxHeatingCapacity", weights), + "MinFreshAirPerArea": UmiBase.float_mean(self, other, "MinFreshAirPerArea", weights), + "MinFreshAirPerPerson": UmiBase.float_mean(self, other, "MinFreshAirPerPerson", weights), + "HeatingSchedule": UmiSchedule.combine(self.HeatingSchedule, other.HeatingSchedule, weights), + "CoolingSchedule": UmiSchedule.combine(self.CoolingSchedule, other.CoolingSchedule, weights), + "MechVentSchedule": UmiSchedule.combine(self.MechVentSchedule, other.MechVentSchedule, weights), + "area": 1 if self.area + other.area == 2 else self.area + other.area, + } # create a new object with the previous attributes new_obj = self.__class__(**meta, **new_attr, allow_duplicates=self.allow_duplicates) new_obj.predecessors.update(self.predecessors + other.predecessors) @@ -1355,33 +1319,33 @@ def mapping(self, validate=False): if validate: self.validate() - base = super(ZoneConditioning, self).mapping(validate=validate) - data = dict( - CoolingSchedule=self.CoolingSchedule, - CoolingCoeffOfPerf=self.CoolingCoeffOfPerf, - CoolingSetpoint=self.CoolingSetpoint, - CoolingLimitType=self.CoolingLimitType, - CoolingFuelType=self.CoolingFuelType, - EconomizerType=self.EconomizerType, - HeatingCoeffOfPerf=self.HeatingCoeffOfPerf, - HeatingLimitType=self.HeatingLimitType, - HeatingFuelType=self.HeatingFuelType, - HeatingSchedule=self.HeatingSchedule, - HeatingSetpoint=self.HeatingSetpoint, - HeatRecoveryEfficiencyLatent=self.HeatRecoveryEfficiencyLatent, - HeatRecoveryEfficiencySensible=self.HeatRecoveryEfficiencySensible, - HeatRecoveryType=self.HeatRecoveryType, - IsCoolingOn=self.IsCoolingOn, - IsHeatingOn=self.IsHeatingOn, - IsMechVentOn=self.IsMechVentOn, - MaxCoolFlow=self.MaxCoolFlow, - MaxCoolingCapacity=self.MaxCoolingCapacity, - MaxHeatFlow=self.MaxHeatFlow, - MaxHeatingCapacity=self.MaxHeatingCapacity, - MechVentSchedule=self.MechVentSchedule, - MinFreshAirPerArea=self.MinFreshAirPerArea, - MinFreshAirPerPerson=self.MinFreshAirPerPerson, - ) + base = super().mapping(validate=validate) + data = { + "CoolingSchedule": self.CoolingSchedule, + "CoolingCoeffOfPerf": self.CoolingCoeffOfPerf, + "CoolingSetpoint": self.CoolingSetpoint, + "CoolingLimitType": self.CoolingLimitType, + "CoolingFuelType": self.CoolingFuelType, + "EconomizerType": self.EconomizerType, + "HeatingCoeffOfPerf": self.HeatingCoeffOfPerf, + "HeatingLimitType": self.HeatingLimitType, + "HeatingFuelType": self.HeatingFuelType, + "HeatingSchedule": self.HeatingSchedule, + "HeatingSetpoint": self.HeatingSetpoint, + "HeatRecoveryEfficiencyLatent": self.HeatRecoveryEfficiencyLatent, + "HeatRecoveryEfficiencySensible": self.HeatRecoveryEfficiencySensible, + "HeatRecoveryType": self.HeatRecoveryType, + "IsCoolingOn": self.IsCoolingOn, + "IsHeatingOn": self.IsHeatingOn, + "IsMechVentOn": self.IsMechVentOn, + "MaxCoolFlow": self.MaxCoolFlow, + "MaxCoolingCapacity": self.MaxCoolingCapacity, + "MaxHeatFlow": self.MaxHeatFlow, + "MaxHeatingCapacity": self.MaxHeatingCapacity, + "MechVentSchedule": self.MechVentSchedule, + "MinFreshAirPerArea": self.MinFreshAirPerArea, + "MinFreshAirPerPerson": self.MinFreshAirPerPerson, + } data.update(base) return data diff --git a/archetypal/template/constructions/base_construction.py b/archetypal/template/constructions/base_construction.py index baa6c0ac3..8cdf10242 100644 --- a/archetypal/template/constructions/base_construction.py +++ b/archetypal/template/constructions/base_construction.py @@ -6,8 +6,9 @@ archetypal.template module. """ +from __future__ import annotations + import math -from typing import List, Union from validator_collection import validators @@ -52,7 +53,7 @@ def __init__( DisassemblyEnergy (float): disassembly energy [MJ/m2]. **kwargs: keywords passed to UmiBase. """ - super(ConstructionBase, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.AssemblyCarbon = AssemblyCarbon self.AssemblyCost = AssemblyCost self.AssemblyEnergy = AssemblyEnergy @@ -153,11 +154,11 @@ def __init__(self, Name, Layers, **kwargs): **kwargs: Keywords passed to the :class:`ConstructionBase` constructor. """ - super(LayeredConstruction, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.Layers = Layers @property - def Layers(self) -> List[Union[MaterialLayer, GasLayer]]: + def Layers(self) -> list[MaterialLayer | GasLayer]: """Get or set the material layers.""" return self._layers @@ -319,4 +320,4 @@ def __eq__(self, other): @property def children(self): - return tuple(l.Material for l in self.Layers) + return tuple(layer.Material for layer in self.Layers) diff --git a/archetypal/template/constructions/opaque_construction.py b/archetypal/template/constructions/opaque_construction.py index 376651b95..9a4912d6d 100644 --- a/archetypal/template/constructions/opaque_construction.py +++ b/archetypal/template/constructions/opaque_construction.py @@ -2,6 +2,7 @@ import collections import uuid +from typing import ClassVar import numpy as np from eppy.bunch_subclass import BadEPFieldError @@ -31,7 +32,7 @@ class OpaqueConstruction(LayeredConstruction): * solar_reflectance_index """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["OpaqueConstruction"]] = [] __slots__ = ("area",) @@ -44,7 +45,7 @@ def __init__(self, Name, Layers, **kwargs): **kwargs: Other attributes passed to parent constructors such as :class:`ConstructionBase`. """ - super(OpaqueConstruction, self).__init__(Name, Layers, **kwargs) + super().__init__(Name, Layers, **kwargs) self.area = 1 # Only at the end append self to _CREATED_OBJECTS @@ -57,13 +58,13 @@ def r_value(self): Note that, when setting the R-value, the thickness of the inferred insulation layer will be adjusted. """ - return super(OpaqueConstruction, self).r_value + return super().r_value @r_value.setter def r_value(self, value): # First, find the insulation layer i = self.infer_insulation_layer() - all_layers_except_insulation_layer = [a for a in self.Layers] + all_layers_except_insulation_layer = list(self.Layers) all_layers_except_insulation_layer.pop(i) insulation_layer: MaterialLayer = self.Layers[i] @@ -101,7 +102,7 @@ def equivalent_heat_capacity_per_unit_volume(self): the n parallel layers of the composite wall." [ref]_ .. [ref] Tsilingiris, P. T. (2004). On the thermal time constant of - structural walls. Applied Thermal Engineering, 24(5–6), 743–757. + structural walls. Applied Thermal Engineering, 24(5-6), 743-757. https://doi.org/10.1016/j.applthermaleng.2003.10.015 """ return (1 / self.total_thickness) * sum( @@ -186,10 +187,7 @@ def combine(self, other, method="dominant_wall", allow_duplicates=False): # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # Check if other is not the same as self @@ -224,7 +222,7 @@ def dominant_wall(self, other, weights): other: weights: """ - oc = [x for _, x in sorted(zip([2, 1], [self, other]), key=lambda pair: pair[0], reverse=True)][0] + oc = next(x for _, x in sorted(zip([2, 1], [self, other]), key=lambda pair: pair[0], reverse=True)) return oc def _constant_ufactor(self, other, weights=None): @@ -466,18 +464,18 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Layers=self.Layers, - AssemblyCarbon=self.AssemblyCarbon, - AssemblyCost=self.AssemblyCost, - AssemblyEnergy=self.AssemblyEnergy, - DisassemblyCarbon=self.DisassemblyCarbon, - DisassemblyEnergy=self.DisassemblyEnergy, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "Layers": self.Layers, + "AssemblyCarbon": self.AssemblyCarbon, + "AssemblyCost": self.AssemblyCost, + "AssemblyEnergy": self.AssemblyEnergy, + "DisassemblyCarbon": self.DisassemblyCarbon, + "DisassemblyEnergy": self.DisassemblyEnergy, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } @classmethod def generic(cls, **kwargs): @@ -514,7 +512,7 @@ def __eq__(self, other): def __copy__(self): """Create a copy of self.""" - new_con = self.__class__(Name=self.Name, Layers=[a for a in self.Layers]) + new_con = self.__class__(Name=self.Name, Layers=list(self.Layers)) return new_con def to_epbunch(self, idf): diff --git a/archetypal/template/constructions/window_construction.py b/archetypal/template/constructions/window_construction.py index a0e1d3006..d5467612d 100644 --- a/archetypal/template/constructions/window_construction.py +++ b/archetypal/template/constructions/window_construction.py @@ -9,6 +9,7 @@ import collections from enum import Enum +from typing import ClassVar from validator_collection import validators @@ -63,7 +64,7 @@ class WindowConstruction(LayeredConstruction): .. image:: ../images/template/constructions-window.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["WindowConstruction"]] = [] _CATEGORIES = ("single", "double", "triple", "quadruple") @@ -79,7 +80,7 @@ def __init__(self, Name, Layers, Category="Double", **kwargs): Category (str): "Single", "Double" or "Triple". **kwargs: Other keywords passed to the constructor. """ - super(WindowConstruction, self).__init__( + super().__init__( Name, Layers, Category=Category, @@ -338,18 +339,18 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Layers=self.Layers, - AssemblyCarbon=self.AssemblyCarbon, - AssemblyCost=self.AssemblyCost, - AssemblyEnergy=self.AssemblyEnergy, - DisassemblyCarbon=self.DisassemblyCarbon, - DisassemblyEnergy=self.DisassemblyEnergy, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "Layers": self.Layers, + "AssemblyCarbon": self.AssemblyCarbon, + "AssemblyCost": self.AssemblyCost, + "AssemblyEnergy": self.AssemblyEnergy, + "DisassemblyCarbon": self.DisassemblyCarbon, + "DisassemblyEnergy": self.DisassemblyEnergy, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def combine(self, other, weights=None): """Append other to self. Return self + other as a new object. diff --git a/archetypal/template/dhw.py b/archetypal/template/dhw.py index 383fd8c8b..0d1231325 100644 --- a/archetypal/template/dhw.py +++ b/archetypal/template/dhw.py @@ -2,6 +2,7 @@ import collections from statistics import mean +from typing import ClassVar import numpy as np from eppy import modeleditor @@ -20,7 +21,7 @@ class DomesticHotWaterSetting(UmiBase): .. image:: ../images/template/zoneinfo-dhw.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["DomesticHotWaterSetting"]] = [] __slots__ = ( "_flow_rate_per_floor_area", @@ -55,7 +56,7 @@ def __init__( mains [degC]. **kwargs: keywords passed to parent constructors. """ - super(DomesticHotWaterSetting, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.FlowRatePerFloorArea = FlowRatePerFloorArea self.IsOn = IsOn self.WaterSupplyTemperature = WaterSupplyTemperature @@ -357,10 +358,7 @@ def combine(self, other, **kwargs): # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # Check if other is not the same as self @@ -455,17 +453,17 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - FlowRatePerFloorArea=self.FlowRatePerFloorArea, - IsOn=self.IsOn, - WaterSchedule=self.WaterSchedule, - WaterSupplyTemperature=self.WaterSupplyTemperature, - WaterTemperatureInlet=self.WaterTemperatureInlet, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "FlowRatePerFloorArea": self.FlowRatePerFloorArea, + "IsOn": self.IsOn, + "WaterSchedule": self.WaterSchedule, + "WaterSupplyTemperature": self.WaterSupplyTemperature, + "WaterTemperatureInlet": self.WaterTemperatureInlet, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def duplicate(self): """Get copy of self.""" diff --git a/archetypal/template/load.py b/archetypal/template/load.py index 51243bad8..ab2f87667 100644 --- a/archetypal/template/load.py +++ b/archetypal/template/load.py @@ -5,6 +5,7 @@ import math import sqlite3 from enum import Enum +from typing import ClassVar import numpy as np import pandas as pd @@ -43,7 +44,7 @@ class ZoneLoad(UmiBase): .. image:: ../images/template/zoneinfo-loads.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["ZoneLoad"]] = [] __slots__ = ( "_dimming_type", @@ -118,7 +119,7 @@ def __init__( area (float): The floor area assiciated to this zone load object. **kwargs: Other keywords passed to the parent constructor :class:`UmiBase`. """ - super(ZoneLoad, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.EquipmentPowerDensity = EquipmentPowerDensity self.EquipmentAvailabilitySchedule = EquipmentAvailabilitySchedule @@ -364,7 +365,7 @@ def from_zone(cls, zone, zone_ep, **kwargs): # Verify if Equipment in zone # create database connection with sqlite3 - with sqlite3.connect(str(zone_ep.theidf.sql_file)) as conn: + with sqlite3.connect(zone_ep.theidf.sql_file) as conn: sql_query = "select ifnull(ZoneIndex, null) from Zones where ZoneName=?" t = (zone.Name.upper(),) c = conn.cursor() @@ -507,10 +508,7 @@ def combine(self, other, weights=None): # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # Check if other is not the same as self @@ -533,34 +531,34 @@ def combine(self, other, weights=None): ) ) - new_attr = dict( - DimmingType=max(self.DimmingType, other.DimmingType), - EquipmentAvailabilitySchedule=UmiSchedule.combine( + new_attr = { + "DimmingType": max(self.DimmingType, other.DimmingType), + "EquipmentAvailabilitySchedule": UmiSchedule.combine( self.EquipmentAvailabilitySchedule, other.EquipmentAvailabilitySchedule, weights=[self.area, other.area], quantity=True, ), - EquipmentPowerDensity=self.float_mean(other, "EquipmentPowerDensity", weights), - IlluminanceTarget=self.float_mean(other, "IlluminanceTarget", weights), - LightingPowerDensity=self.float_mean(other, "LightingPowerDensity", weights), - LightsAvailabilitySchedule=UmiSchedule.combine( + "EquipmentPowerDensity": self.float_mean(other, "EquipmentPowerDensity", weights), + "IlluminanceTarget": self.float_mean(other, "IlluminanceTarget", weights), + "LightingPowerDensity": self.float_mean(other, "LightingPowerDensity", weights), + "LightsAvailabilitySchedule": UmiSchedule.combine( self.LightsAvailabilitySchedule, other.LightsAvailabilitySchedule, weights=[self.area, other.area], quantity=True, ), - OccupancySchedule=UmiSchedule.combine( + "OccupancySchedule": UmiSchedule.combine( self.OccupancySchedule, other.OccupancySchedule, weights=[self.area, other.area], quantity=True, ), - IsEquipmentOn=any([self.IsEquipmentOn, other.IsEquipmentOn]), - IsLightingOn=any([self.IsLightingOn, other.IsLightingOn]), - IsPeopleOn=any([self.IsPeopleOn, other.IsPeopleOn]), - PeopleDensity=self.float_mean(other, "PeopleDensity", weights), - ) + "IsEquipmentOn": any([self.IsEquipmentOn, other.IsEquipmentOn]), + "IsLightingOn": any([self.IsLightingOn, other.IsLightingOn]), + "IsPeopleOn": any([self.IsPeopleOn, other.IsPeopleOn]), + "PeopleDensity": self.float_mean(other, "PeopleDensity", weights), + } new_obj = self.__class__(**meta, **new_attr, allow_duplicates=self.allow_duplicates) new_obj.area = self.area + other.area @@ -604,23 +602,23 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - DimmingType=self.DimmingType, - EquipmentAvailabilitySchedule=self.EquipmentAvailabilitySchedule, - EquipmentPowerDensity=self.EquipmentPowerDensity, - IlluminanceTarget=self.IlluminanceTarget, - LightingPowerDensity=self.LightingPowerDensity, - LightsAvailabilitySchedule=self.LightsAvailabilitySchedule, - OccupancySchedule=self.OccupancySchedule, - IsEquipmentOn=self.IsEquipmentOn, - IsLightingOn=self.IsLightingOn, - IsPeopleOn=self.IsPeopleOn, - PeopleDensity=self.PeopleDensity, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "DimmingType": self.DimmingType, + "EquipmentAvailabilitySchedule": self.EquipmentAvailabilitySchedule, + "EquipmentPowerDensity": self.EquipmentPowerDensity, + "IlluminanceTarget": self.IlluminanceTarget, + "LightingPowerDensity": self.LightingPowerDensity, + "LightsAvailabilitySchedule": self.LightsAvailabilitySchedule, + "OccupancySchedule": self.OccupancySchedule, + "IsEquipmentOn": self.IsEquipmentOn, + "IsLightingOn": self.IsLightingOn, + "IsPeopleOn": self.IsPeopleOn, + "PeopleDensity": self.PeopleDensity, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def to_dict(self): """Return ZoneLoad dictionary representation.""" diff --git a/archetypal/template/materials/gas_layer.py b/archetypal/template/materials/gas_layer.py index 5e5641e4d..09fc591d7 100644 --- a/archetypal/template/materials/gas_layer.py +++ b/archetypal/template/materials/gas_layer.py @@ -331,7 +331,7 @@ def to_epbunch(self, idf): def mapping(self): """Get a dict based on the object properties, useful for dict repr.""" - return dict(Material=self.Material, Thickness=self.Thickness) + return {"Material": self.Material, "Thickness": self.Thickness} def get_unique(self): """Return the first of all the created objects that is equivalent to self.""" @@ -354,8 +354,7 @@ def __repr__(self): def __iter__(self): """Iterate over attributes. Yields tuple of (keys, value).""" - for k, v in self.mapping().items(): - yield k, v + yield from self.mapping().items() def duplicate(self): """Get copy of self.""" diff --git a/archetypal/template/materials/gas_material.py b/archetypal/template/materials/gas_material.py index ecbbda0df..0c420a25c 100644 --- a/archetypal/template/materials/gas_material.py +++ b/archetypal/template/materials/gas_material.py @@ -1,6 +1,7 @@ """GasMaterial module.""" import collections +from typing import ClassVar import numpy as np from sigfig import round @@ -15,7 +16,7 @@ class GasMaterial(MaterialBase): .. image:: ../images/template/materials-gas.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["GasMaterial"]] = [] __slots__ = ("_type", "_conductivity", "_density") @@ -34,7 +35,7 @@ def __init__(self, Name, Conductivity=None, Density=None, Category="Gases", **kw **kwargs: keywords passed to the MaterialBase constructor. """ self.Name = Name - super(GasMaterial, self).__init__(self.Name, Category=Category, **kwargs) + super().__init__(self.Name, Category=Category, **kwargs) self.Type = Name.upper() self.Conductivity = Conductivity self.Density = Density @@ -187,23 +188,23 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Category=self.Category, - Type=self.Type, - Conductivity=self.Conductivity, - Cost=self.Cost, - Density=self.Density, - EmbodiedCarbon=self.EmbodiedCarbon, - EmbodiedEnergy=self.EmbodiedEnergy, - SubstitutionRatePattern=self.SubstitutionRatePattern, - SubstitutionTimestep=self.SubstitutionTimestep, - TransportCarbon=self.TransportCarbon, - TransportDistance=self.TransportDistance, - TransportEnergy=self.TransportEnergy, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "Category": self.Category, + "Type": self.Type, + "Conductivity": self.Conductivity, + "Cost": self.Cost, + "Density": self.Density, + "EmbodiedCarbon": self.EmbodiedCarbon, + "EmbodiedEnergy": self.EmbodiedEnergy, + "SubstitutionRatePattern": self.SubstitutionRatePattern, + "SubstitutionTimestep": self.SubstitutionTimestep, + "TransportCarbon": self.TransportCarbon, + "TransportDistance": self.TransportDistance, + "TransportEnergy": self.TransportEnergy, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def density_at_temperature(self, t_kelvin, pressure=101325): """Get the density of the gas [kg/m3] at a given temperature and pressure. diff --git a/archetypal/template/materials/glazing_material.py b/archetypal/template/materials/glazing_material.py index 7a71c611b..d0d25fa4d 100644 --- a/archetypal/template/materials/glazing_material.py +++ b/archetypal/template/materials/glazing_material.py @@ -1,6 +1,7 @@ """archetypal GlazingMaterial.""" import collections +from typing import ClassVar from sigfig import round from validator_collection import validators @@ -18,7 +19,7 @@ class GlazingMaterial(MaterialBase): """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["GlazingMaterial"]] = [] __slots__ = ( "_ir_emissivity_back", @@ -86,7 +87,7 @@ def __init__( **kwargs: keywords passed to the :class:`MaterialBase` constructor. For more info, see :class:`MaterialBase`. """ - super(GlazingMaterial, self).__init__(Name, Cost=Cost, **kwargs) + super().__init__(Name, Cost=Cost, **kwargs) self._solar_reflectance_front = 0 self._solar_reflectance_back = None @@ -237,10 +238,7 @@ def combine(self, other, weights=None, allow_duplicates=False): """ # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # Check if other is not the same as self @@ -266,7 +264,7 @@ def combine(self, other, weights=None, allow_duplicates=False): pass else: raise NotImplementedError - [new_attr.pop(key, None) for key in meta.keys()] # meta handles these + [new_attr.pop(key, None) for key in meta] # meta handles these # keywords. # create a new object from combined attributes new_obj = self.__class__(**meta, **new_attr) @@ -365,32 +363,32 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - DirtFactor=self.DirtFactor, - IREmissivityBack=self.IREmissivityBack, - IREmissivityFront=self.IREmissivityFront, - IRTransmittance=self.IRTransmittance, - SolarReflectanceBack=self.SolarReflectanceBack, - SolarReflectanceFront=self.SolarReflectanceFront, - SolarTransmittance=self.SolarTransmittance, - VisibleReflectanceBack=self.VisibleReflectanceBack, - VisibleReflectanceFront=self.VisibleReflectanceFront, - VisibleTransmittance=self.VisibleTransmittance, - Conductivity=self.Conductivity, - Cost=self.Cost, - Density=self.Density, - EmbodiedCarbon=self.EmbodiedCarbon, - EmbodiedEnergy=self.EmbodiedEnergy, - SubstitutionRatePattern=self.SubstitutionRatePattern, - SubstitutionTimestep=self.SubstitutionTimestep, - TransportCarbon=self.TransportCarbon, - TransportDistance=self.TransportDistance, - TransportEnergy=self.TransportEnergy, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "DirtFactor": self.DirtFactor, + "IREmissivityBack": self.IREmissivityBack, + "IREmissivityFront": self.IREmissivityFront, + "IRTransmittance": self.IRTransmittance, + "SolarReflectanceBack": self.SolarReflectanceBack, + "SolarReflectanceFront": self.SolarReflectanceFront, + "SolarTransmittance": self.SolarTransmittance, + "VisibleReflectanceBack": self.VisibleReflectanceBack, + "VisibleReflectanceFront": self.VisibleReflectanceFront, + "VisibleTransmittance": self.VisibleTransmittance, + "Conductivity": self.Conductivity, + "Cost": self.Cost, + "Density": self.Density, + "EmbodiedCarbon": self.EmbodiedCarbon, + "EmbodiedEnergy": self.EmbodiedEnergy, + "SubstitutionRatePattern": self.SubstitutionRatePattern, + "SubstitutionTimestep": self.SubstitutionTimestep, + "TransportCarbon": self.TransportCarbon, + "TransportDistance": self.TransportDistance, + "TransportEnergy": self.TransportEnergy, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } @classmethod def from_dict(cls, data, **kwargs): diff --git a/archetypal/template/materials/material_base.py b/archetypal/template/materials/material_base.py index 8cf0f587b..e6c004e90 100644 --- a/archetypal/template/materials/material_base.py +++ b/archetypal/template/materials/material_base.py @@ -71,7 +71,7 @@ def __init__( **kwargs: Keywords passed to the :class:`UmiBase` class. See :class:`UmiBase` for more details. """ - super(MaterialBase, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.Cost = Cost self.EmbodiedCarbon = EmbodiedCarbon self.EmbodiedEnergy = EmbodiedEnergy diff --git a/archetypal/template/materials/material_layer.py b/archetypal/template/materials/material_layer.py index c68851906..4f89e013c 100644 --- a/archetypal/template/materials/material_layer.py +++ b/archetypal/template/materials/material_layer.py @@ -127,7 +127,7 @@ def to_epbunch(self, idf): def mapping(self): """Get a dict based on the object properties, useful for dict repr.""" - return dict(Material=self.Material, Thickness=self.Thickness) + return {"Material": self.Material, "Thickness": self.Thickness} def get_unique(self): """Return the first of all the created objects that is equivalent to self.""" @@ -155,8 +155,7 @@ def __repr__(self): def __iter__(self): """Iterate over attributes. Yields tuple of (keys, value).""" - for k, v in self.mapping().items(): - yield k, v + yield from self.mapping().items() def duplicate(self): """Get copy of self.""" diff --git a/archetypal/template/materials/nomass_material.py b/archetypal/template/materials/nomass_material.py index 85dc5b975..4c535674d 100644 --- a/archetypal/template/materials/nomass_material.py +++ b/archetypal/template/materials/nomass_material.py @@ -1,6 +1,7 @@ """archetypal OpaqueMaterial.""" import collections +from typing import ClassVar from sigfig import round from validator_collection import validators @@ -13,7 +14,7 @@ class NoMassMaterial(MaterialBase): """Use this component to create a custom no mass material.""" - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["NoMassMaterial"]] = [] _ROUGHNESS_TYPES = ( "VeryRough", @@ -70,7 +71,7 @@ def __init__( stagnant air [%]. **kwargs: keywords passed to parent constructors. """ - super(NoMassMaterial, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.r_value = RValue self.Roughness = Roughness self.SolarAbsorptance = SolarAbsorptance @@ -163,10 +164,7 @@ def combine(self, other, weights=None, allow_duplicates=False): """ # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # Check if other is not the same as self @@ -273,7 +271,7 @@ def from_epbunch(cls, epbunch, **kwargs): material into EnergyPlus, internally the properties of this layer are converted to approximate the properties of air (density, specific heat, and conductivity) with the thickness adjusted to - maintain the user’s desired R-Value. This allowed such layers to be + maintain the user's desired R-Value. This allowed such layers to be handled internally in the same way as other layers without any additional changes to the code. This solution was deemed accurate enough as air has very little thermal mass and it made the coding of @@ -383,26 +381,26 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - RValue=self.r_value, - MoistureDiffusionResistance=self.MoistureDiffusionResistance, - Roughness=self.Roughness, - SolarAbsorptance=self.SolarAbsorptance, - ThermalEmittance=self.ThermalEmittance, - VisibleAbsorptance=self.VisibleAbsorptance, - Cost=self.Cost, - EmbodiedCarbon=self.EmbodiedCarbon, - EmbodiedEnergy=self.EmbodiedEnergy, - SubstitutionRatePattern=self.SubstitutionRatePattern, - SubstitutionTimestep=self.SubstitutionTimestep, - TransportCarbon=self.TransportCarbon, - TransportDistance=self.TransportDistance, - TransportEnergy=self.TransportEnergy, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "RValue": self.r_value, + "MoistureDiffusionResistance": self.MoistureDiffusionResistance, + "Roughness": self.Roughness, + "SolarAbsorptance": self.SolarAbsorptance, + "ThermalEmittance": self.ThermalEmittance, + "VisibleAbsorptance": self.VisibleAbsorptance, + "Cost": self.Cost, + "EmbodiedCarbon": self.EmbodiedCarbon, + "EmbodiedEnergy": self.EmbodiedEnergy, + "SubstitutionRatePattern": self.SubstitutionRatePattern, + "SubstitutionTimestep": self.SubstitutionTimestep, + "TransportCarbon": self.TransportCarbon, + "TransportDistance": self.TransportDistance, + "TransportEnergy": self.TransportEnergy, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def duplicate(self): """Get copy of self.""" diff --git a/archetypal/template/materials/opaque_material.py b/archetypal/template/materials/opaque_material.py index a4fe3f279..9581a6711 100644 --- a/archetypal/template/materials/opaque_material.py +++ b/archetypal/template/materials/opaque_material.py @@ -1,6 +1,7 @@ """archetypal OpaqueMaterial.""" import collections +from typing import ClassVar from eppy.bunch_subclass import EpBunch from validator_collection import validators @@ -16,7 +17,7 @@ class OpaqueMaterial(MaterialBase): .. image:: ../images/template/materials-opaque.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["OpaqueMaterial"]] = [] _ROUGHNESS_TYPES = ( "VeryRough", @@ -98,7 +99,7 @@ def __init__( stagnant air [%]. **kwargs: keywords passed to parent constructors. """ - super(OpaqueMaterial, self).__init__( + super().__init__( Name, Cost=Cost, EmbodiedCarbon=EmbodiedCarbon, @@ -252,10 +253,7 @@ def combine(self, other, weights=None, allow_duplicates=False): """ # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # Check if other is not the same as self @@ -374,7 +372,7 @@ def from_epbunch(cls, epbunch, **kwargs): material into EnergyPlus, internally the properties of this layer are converted to approximate the properties of air (density, specific heat, and conductivity) with the thickness adjusted to - maintain the user’s desired R-Value. This allowed such layers to be + maintain the user's desired R-Value. This allowed such layers to be handled internally in the same way as other layers without any additional changes to the code. This solution was deemed accurate enough as air has very little thermal mass and it made the coding of @@ -526,28 +524,28 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - MoistureDiffusionResistance=self.MoistureDiffusionResistance, - Roughness=self.Roughness, - SolarAbsorptance=self.SolarAbsorptance, - SpecificHeat=self.SpecificHeat, - ThermalEmittance=self.ThermalEmittance, - VisibleAbsorptance=self.VisibleAbsorptance, - Conductivity=self.Conductivity, - Cost=self.Cost, - Density=self.Density, - EmbodiedCarbon=self.EmbodiedCarbon, - EmbodiedEnergy=self.EmbodiedEnergy, - SubstitutionRatePattern=self.SubstitutionRatePattern, - SubstitutionTimestep=self.SubstitutionTimestep, - TransportCarbon=self.TransportCarbon, - TransportDistance=self.TransportDistance, - TransportEnergy=self.TransportEnergy, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "MoistureDiffusionResistance": self.MoistureDiffusionResistance, + "Roughness": self.Roughness, + "SolarAbsorptance": self.SolarAbsorptance, + "SpecificHeat": self.SpecificHeat, + "ThermalEmittance": self.ThermalEmittance, + "VisibleAbsorptance": self.VisibleAbsorptance, + "Conductivity": self.Conductivity, + "Cost": self.Cost, + "Density": self.Density, + "EmbodiedCarbon": self.EmbodiedCarbon, + "EmbodiedEnergy": self.EmbodiedEnergy, + "SubstitutionRatePattern": self.SubstitutionRatePattern, + "SubstitutionTimestep": self.SubstitutionTimestep, + "TransportCarbon": self.TransportCarbon, + "TransportDistance": self.TransportDistance, + "TransportEnergy": self.TransportEnergy, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def duplicate(self): """Get copy of self.""" diff --git a/archetypal/template/schedule.py b/archetypal/template/schedule.py index 9d515b97b..138c6a1a4 100644 --- a/archetypal/template/schedule.py +++ b/archetypal/template/schedule.py @@ -4,7 +4,7 @@ import collections import hashlib from datetime import datetime -from typing import List +from typing import ClassVar import numpy as np import pandas as pd @@ -18,7 +18,7 @@ class UmiSchedule(Schedule, UmiBase): """Class that handles Schedules.""" - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["UmiSchedule"]] = [] __slots__ = ("_quantity",) @@ -30,7 +30,7 @@ def __init__(self, Name, quantity=None, **kwargs): quantity: **kwargs: """ - super(UmiSchedule, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.quantity = quantity # Only at the end append self to _CREATED_OBJECTS @@ -57,7 +57,7 @@ def constant_schedule(cls, value=1, Name="AlwaysOn", Type="Fraction", **kwargs): **kwargs: """ value = validators.float(value) - return super(UmiSchedule, cls).constant_schedule(value=value, Name=Name, Type=Type, **kwargs) + return super().constant_schedule(value=value, Name=Name, Type=Type, **kwargs) @classmethod def random(cls, Name="AlwaysOn", Type="Fraction", **kwargs): @@ -83,7 +83,7 @@ def from_values(cls, Name, Values, Type="Fraction", **kwargs): Type: **kwargs: """ - return super(UmiSchedule, cls).from_values(Name=Name, Values=Values, Type=Type, **kwargs) + return super().from_values(Name=Name, Values=Values, Type=Type, **kwargs) def combine(self, other, weights=None, quantity=None): """Combine two UmiSchedule objects together. @@ -114,10 +114,7 @@ def combine(self, other, weights=None, quantity=None): return other if not isinstance(other, UmiSchedule): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # check if the schedule is the same @@ -240,13 +237,13 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Category=self.Category, - Type=self.Type, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "Category": self.Category, + "Type": self.Type, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def get_ref(self, ref): """Get item matching reference id. @@ -271,13 +268,13 @@ def __repr__(self): """Return a representation of self.""" name = self.Name resample = self.series.resample("D") - min = resample.min().mean() + low = resample.min().mean() mean = resample.mean().mean() - max = resample.max().mean() + high = resample.max().mean() return ( name + ": " - + f"mean daily min:{min:.2f} mean:{mean:.2f} max:{max:.2f} " + + f"mean daily min:{low:.2f} mean:{mean:.2f} max:{high:.2f} " + (f"quantity {self.quantity}" if self.quantity is not None else "") ) @@ -440,13 +437,13 @@ def __repr__(self): def mapping(self): """Get a dict based on the object properties, useful for dict repr.""" - return dict( - FromDay=self.FromDay, - FromMonth=self.FromMonth, - ToDay=self.ToDay, - ToMonth=self.ToMonth, - Schedule=self.Schedule, - ) + return { + "FromDay": self.FromDay, + "FromMonth": self.FromMonth, + "ToDay": self.ToDay, + "ToMonth": self.ToMonth, + "Schedule": self.Schedule, + } def get_unique(self): """Return the first of all the created objects that is equivalent to self.""" @@ -469,8 +466,7 @@ def __eq__(self, other): def __iter__(self): """Iterate over attributes. Yields tuple of (keys, value).""" - for k, v in self.mapping().items(): - yield k, v + yield from self.mapping().items() def __hash__(self): """Return the hash value of self.""" @@ -491,7 +487,7 @@ def __init__(self, Name, Values, Category="Day", **kwargs): Category (str): category identification (default: "Day"). **kwargs: Keywords passed to the :class:`UmiSchedule` constructor. """ - super(DaySchedule, self).__init__(Category=Category, Name=Name, Values=Values, **kwargs) + super().__init__(Category=Category, Name=Name, Values=Values, **kwargs) @property def all_values(self) -> np.ndarray: @@ -603,14 +599,14 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Category=self.Category, - Type=self.Type, - Values=self.all_values.round(3).tolist(), - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "Category": self.Category, + "Type": self.Type, + "Values": self.all_values.round(3).tolist(), + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def to_ref(self): """Return a ref pointer to self.""" @@ -634,7 +630,7 @@ def __eq__(self, other): def __hash__(self): """Return the hash value of self.""" - return super(DaySchedule, self).__hash__() + return super().__hash__() def __copy__(self): """Create a copy of self.""" @@ -701,7 +697,7 @@ def __init__(self, Name, Days=None, Category="Week", **kwargs): Days (list of DaySchedule): list of :class:`DaySchedule`. **kwargs: """ - super(WeekSchedule, self).__init__(Name, Category=Category, **kwargs) + super().__init__(Name, Category=Category, **kwargs) self.Days = Days @property @@ -782,14 +778,14 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Category=self.Category, - Days=self.Days, - Type=self.Type, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "Category": self.Category, + "Days": self.Days, + "Type": self.Type, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } @classmethod def get_days(cls, epbunch, **kwargs): @@ -846,7 +842,7 @@ def __eq__(self, other): def __hash__(self): """Return the hash value of self.""" - return super(WeekSchedule, self).__hash__() + return super().__hash__() def __copy__(self): """Create a copy of self.""" @@ -900,7 +896,7 @@ def __init__(self, Name, Type="Fraction", Parts=None, Category="Year", **kwargs) self.Parts = self._get_parts(self.epbunch) else: self.Parts = Parts - super(YearSchedule, self).__init__(Name=Name, Type=Type, schType="Schedule:Year", Category=Category, **kwargs) + super().__init__(Name=Name, Type=Type, schType="Schedule:Year", Category=Category, **kwargs) def __eq__(self, other): """Assert self is equivalent to other.""" @@ -911,7 +907,7 @@ def __eq__(self, other): def __hash__(self): """Return the hash value of self.""" - return super(YearSchedule, self).__hash__() + return super().__hash__() @property def all_values(self) -> np.ndarray: @@ -940,7 +936,7 @@ def from_dict(cls, data, week_schedules, **kwargs): keys. **kwargs: keywords passed to the constructor. """ - Parts: List[YearSchedulePart] = [ + Parts: list[YearSchedulePart] = [ YearSchedulePart.from_dict(data, week_schedules) for data in data.pop("Parts", None) ] _id = data.pop("$id") @@ -977,7 +973,7 @@ def to_epbunch(self, idf): Returns: EpBunch: The EpBunch object added to the idf model. """ - new_dict = dict(Name=self.Name, Schedule_Type_Limits_Name=self.Type.to_epbunch(idf).Name) + new_dict = {"Name": self.Name, "Schedule_Type_Limits_Name": self.Type.to_epbunch(idf).Name} for i, part in enumerate(self.Parts): new_dict.update( { @@ -1001,14 +997,14 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Category=self.Category, - Parts=self.Parts, - Type=self.Type, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "Category": self.Category, + "Parts": self.Parts, + "Type": self.Type, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def _get_parts(self, epbunch): parts = [] diff --git a/archetypal/template/structure.py b/archetypal/template/structure.py index 988e7666a..727fd359f 100644 --- a/archetypal/template/structure.py +++ b/archetypal/template/structure.py @@ -1,6 +1,7 @@ """archetypal StructureInformation.""" import collections +from typing import ClassVar from validator_collection import validators @@ -74,8 +75,7 @@ def __eq__(self, other): def __iter__(self): """Iterate over attributes. Yields tuple of (keys, value).""" - for k, v in self.mapping().items(): - yield k, v + yield from self.mapping().items() def to_dict(self): """Return MassRatio dictionary representation.""" @@ -87,11 +87,11 @@ def to_dict(self): def mapping(self): """Get a dict based on the object properties, useful for dict repr.""" - return dict( - HighLoadRatio=self.HighLoadRatio, - Material=self.Material, - NormalRatio=self.NormalRatio, - ) + return { + "HighLoadRatio": self.HighLoadRatio, + "Material": self.Material, + "NormalRatio": self.NormalRatio, + } def get_unique(self): """Return the first of all the created objects that is equivalent to self.""" @@ -137,7 +137,7 @@ class StructureInformation(ConstructionBase): .. image:: ../images/template/constructions-structure.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["StructureInformation"]] = [] __slots__ = ("_mass_ratios",) @@ -148,7 +148,7 @@ def __init__(self, Name, MassRatios, **kwargs): MassRatios (list of MassRatio): MassRatio object. **kwargs: keywords passed to the ConstructionBase constructor. """ - super(StructureInformation, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.MassRatios = MassRatios # Only at the end append self to _CREATED_OBJECTS @@ -220,18 +220,18 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - MassRatios=self.MassRatios, - AssemblyCarbon=self.AssemblyCarbon, - AssemblyCost=self.AssemblyCost, - AssemblyEnergy=self.AssemblyEnergy, - DisassemblyCarbon=self.DisassemblyCarbon, - DisassemblyEnergy=self.DisassemblyEnergy, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "MassRatios": self.MassRatios, + "AssemblyCarbon": self.AssemblyCarbon, + "AssemblyCost": self.AssemblyCost, + "AssemblyEnergy": self.AssemblyEnergy, + "DisassemblyCarbon": self.DisassemblyCarbon, + "DisassemblyEnergy": self.DisassemblyEnergy, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def duplicate(self): """Get copy of self.""" diff --git a/archetypal/template/umi_base.py b/archetypal/template/umi_base.py index b50d54a9b..6e75c56b4 100644 --- a/archetypal/template/umi_base.py +++ b/archetypal/template/umi_base.py @@ -3,6 +3,7 @@ import itertools import math from collections.abc import Hashable, MutableSet +from typing import ClassVar import numpy as np from validator_collection import validators @@ -19,9 +20,9 @@ def _resolve_combined_names(predecessors): """ # all_names = [obj.Name for obj in predecessors] - class_ = list(set([obj.__class__.__name__ for obj in predecessors]))[0] + class_ = next(iter({obj.__class__.__name__ for obj in predecessors})) - return "Combined_%s_%s" % ( + return "Combined_{}_{}".format( class_, str(hash(pre.Name for pre in predecessors)).strip("-"), ) @@ -200,7 +201,7 @@ def combine_meta(self, predecessors): "Name": _resolve_combined_names(predecessors), "Comments": ( "Object composed of a combination of these objects:\n{}".format( - "\n- ".join(set(obj.Name for obj in predecessors)) + "\n- ".join({obj.Name for obj in predecessors}) ) ), "Category": ", ".join(set(itertools.chain(*[obj.Category.split(", ") for obj in predecessors]))), @@ -243,8 +244,7 @@ def __str__(self): def __iter__(self): """Iterate over attributes. Yields tuple of (keys, value).""" - for attr, value in self.mapping().items(): - yield attr, value + yield from self.mapping().items() def __getitem__(self, item): return getattr(self, item) @@ -353,9 +353,9 @@ def extend(self, other, allow_duplicates): if other is None: return self self._CREATED_OBJECTS.remove(self) - id = self.id + uid = self.id new_obj = self.combine(other, allow_duplicates=allow_duplicates) - new_obj.id = id + new_obj.id = uid for key in self.mapping(validate=False): setattr(self, key, getattr(new_obj, key)) return self @@ -374,13 +374,13 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( + return { # id=self.id, - Name=self.Name, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - ) + "Name": self.Name, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + } def get_unique(self): """Return first object matching equality in the list of instantiated objects.""" @@ -466,13 +466,13 @@ def Name(self): @property def comments(self): """Get object comments.""" - return f"Object composed of a combination of these objects:\n{set(obj.Name for obj in self)}" + return f"Object composed of a combination of these objects:\n{ {obj.Name for obj in self} }" class UniqueName(str): """Attribute unique user-defined names for :class:`UmiBase`.""" - existing = {} + existing: ClassVar[set[str]] = {} def __new__(cls, content): """Pick a name. Will increment the name if already used.""" diff --git a/archetypal/template/ventilation.py b/archetypal/template/ventilation.py index fd91ad143..9f3a81d96 100644 --- a/archetypal/template/ventilation.py +++ b/archetypal/template/ventilation.py @@ -3,6 +3,7 @@ import collections import logging as lg from enum import Enum +from typing import ClassVar import numpy as np import pandas as pd @@ -63,7 +64,7 @@ class VentilationSetting(UmiBase): .. image:: ../images/template/zoneinfo-ventilation.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["VentilationSetting"]] = [] __slots__ = ( "_infiltration", @@ -179,7 +180,7 @@ def __init__( ventilation types which employ only a single fan. **kwargs: keywords passed to the constructor. """ - super(VentilationSetting, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.Infiltration = Infiltration self.IsInfiltrationOn = IsInfiltrationOn @@ -598,10 +599,7 @@ def combine(self, other, **kwargs): # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) meta = self._get_predecessors_meta(other) @@ -659,27 +657,27 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Afn=self.Afn, - IsBuoyancyOn=self.IsBuoyancyOn, - Infiltration=self.Infiltration, - IsInfiltrationOn=self.IsInfiltrationOn, - IsNatVentOn=self.IsNatVentOn, - IsScheduledVentilationOn=self.IsScheduledVentilationOn, - NatVentMaxRelHumidity=self.NatVentMaxRelHumidity, - NatVentMaxOutdoorAirTemp=self.NatVentMaxOutdoorAirTemp, - NatVentMinOutdoorAirTemp=self.NatVentMinOutdoorAirTemp, - NatVentSchedule=self.NatVentSchedule, - NatVentZoneTempSetpoint=self.NatVentZoneTempSetpoint, - ScheduledVentilationAch=self.ScheduledVentilationAch, - ScheduledVentilationSchedule=self.ScheduledVentilationSchedule, - ScheduledVentilationSetpoint=self.ScheduledVentilationSetpoint, - IsWindOn=self.IsWindOn, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "Afn": self.Afn, + "IsBuoyancyOn": self.IsBuoyancyOn, + "Infiltration": self.Infiltration, + "IsInfiltrationOn": self.IsInfiltrationOn, + "IsNatVentOn": self.IsNatVentOn, + "IsScheduledVentilationOn": self.IsScheduledVentilationOn, + "NatVentMaxRelHumidity": self.NatVentMaxRelHumidity, + "NatVentMaxOutdoorAirTemp": self.NatVentMaxOutdoorAirTemp, + "NatVentMinOutdoorAirTemp": self.NatVentMinOutdoorAirTemp, + "NatVentSchedule": self.NatVentSchedule, + "NatVentZoneTempSetpoint": self.NatVentZoneTempSetpoint, + "ScheduledVentilationAch": self.ScheduledVentilationAch, + "ScheduledVentilationSchedule": self.ScheduledVentilationSchedule, + "ScheduledVentilationSetpoint": self.ScheduledVentilationSetpoint, + "IsWindOn": self.IsWindOn, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def duplicate(self): """Get copy of self.""" @@ -928,19 +926,18 @@ def do_natural_ventilation(index, nat_df, zone, zone_ep): IsNatVentOn = any(nat_df.loc[index, "Name"]) schedule_name_ = nat_df.loc[index, "Schedule Name"] quantity = nat_df.loc[index, "Volume Flow Rate/Floor Area {m3/s/m2}"] - if schedule_name_.upper() in zone.idf.schedules_dict: - epbunch = zone.idf.schedules_dict[schedule_name_.upper()] + if schedule_name_.upper() in zone_ep.theidf.schedules_dict: + epbunch = zone_ep.theidf.schedules_dict[schedule_name_.upper()] NatVentSchedule = UmiSchedule.from_epbunch(epbunch, quantity=quantity) else: - raise KeyError - except KeyError: - # todo: For some reason, a ZoneVentilation:WindandStackOpenArea - # 'Opening Area Fraction Schedule Name' is read as Constant-0.0 - # in the nat_df. For the mean time, a zone containing such an - # object will be turned on with an AlwaysOn schedule. - IsNatVentOn = True - NatVentSchedule = UmiSchedule.constant_schedule(allow_duplicates=True) + # todo: For some reason, a ZoneVentilation:WindandStackOpenArea + # 'Opening Area Fraction Schedule Name' is read as Constant-0.0 + # in the nat_df. For the mean time, a zone containing such an + # object will be turned on with an AlwaysOn schedule. + IsNatVentOn = True + NatVentSchedule = UmiSchedule.constant_schedule(allow_duplicates=True) except Exception: + log("Error in reading the natural ventilation schedule. Reverting to defaults.", lg.ERROR) IsNatVentOn = False NatVentSchedule = UmiSchedule.constant_schedule(allow_duplicates=True) finally: diff --git a/archetypal/template/window_setting.py b/archetypal/template/window_setting.py index d2afe9a73..705369820 100644 --- a/archetypal/template/window_setting.py +++ b/archetypal/template/window_setting.py @@ -4,6 +4,7 @@ import logging as lg from copy import copy from functools import reduce +from typing import ClassVar from validator_collection import checkers, validators @@ -36,7 +37,7 @@ class WindowSetting(UmiBase): .. _eppy : https://eppy.readthedocs.io/en/latest/ """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["WindowSetting"]] = [] __slots__ = ( "_operable_area", @@ -111,7 +112,7 @@ def __init__( Default = 0.001 m3/m2. **kwargs: other keywords passed to the constructor. """ - super(WindowSetting, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.ShadingSystemAvailabilitySchedule = ShadingSystemAvailabilitySchedule self.Construction = Construction @@ -343,7 +344,7 @@ def __add__(self, other): def __repr__(self): """Return a representation of self.""" - return super(WindowSetting, self).__repr__() + return super().__repr__() def __str__(self): """Return string representation.""" @@ -577,25 +578,12 @@ def from_surface(cls, surface, **kwargs): f'defaults for object "{cls.mro()[0].__name__}"', lg.WARNING, ) - elif leak.key.upper() == "AIRFLOWNETWORK:MULTIZONE:SURFACE:CRACK": - log( - f'"{leak.key}" is not fully supported. Rerverting to ' - f'defaults for object "{cls.mro()[0].__name__}"', - lg.WARNING, - ) - elif leak.key.upper() == "AIRFLOWNETWORK:MULTIZONE:COMPONENT:DETAILEDOPENING": - log( - f'"{leak.key}" is not fully supported. Rerverting to ' - f'defaults for object "{cls.mro()[0].__name__}"', - lg.WARNING, - ) - elif leak.key.upper() == "AIRFLOWNETWORK:MULTIZONE:COMPONENT:ZONEEXHAUSTFAN": - log( - f'"{leak.key}" is not fully supported. Rerverting to ' - f'defaults for object "{cls.mro()[0].__name__}"', - lg.WARNING, - ) - elif leak.key.upper() == "AIRFLOWNETWORK:MULTIZONE:COMPONENT:SIMPLEOPENING": + elif ( + leak.key.upper() == "AIRFLOWNETWORK:MULTIZONE:SURFACE:CRACK" + or leak.key.upper() == "AIRFLOWNETWORK:MULTIZONE:COMPONENT:DETAILEDOPENING" + or leak.key.upper() == "AIRFLOWNETWORK:MULTIZONE:COMPONENT:ZONEEXHAUSTFAN" + or leak.key.upper() == "AIRFLOWNETWORK:MULTIZONE:COMPONENT:SIMPLEOPENING" + ): log( f'"{leak.key}" is not fully supported. Rerverting to ' f'defaults for object "{cls.mro()[0].__name__}"', @@ -670,10 +658,7 @@ def combine(self, other, weights=None, allow_duplicates=False): return other if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) # Check if other is not the same as self @@ -684,32 +669,34 @@ def combine(self, other, weights=None, allow_duplicates=False): log(f'using 1 as weighting factor in "{self.__class__.__name__}" ' "combine.") weights = [1.0, 1.0] meta = self._get_predecessors_meta(other) - new_attr = dict( - Construction=WindowConstruction.combine(self.Construction, other.Construction, weights), - AfnDischargeC=self.float_mean(other, "AfnDischargeC", weights), - AfnTempSetpoint=self.float_mean(other, "AfnTempSetpoint", weights), - AfnWindowAvailability=UmiSchedule.combine(self.AfnWindowAvailability, other.AfnWindowAvailability, weights), - IsShadingSystemOn=any([self.IsShadingSystemOn, other.IsShadingSystemOn]), - IsVirtualPartition=any([self.IsVirtualPartition, other.IsVirtualPartition]), - IsZoneMixingOn=any([self.IsZoneMixingOn, other.IsZoneMixingOn]), - OperableArea=self.float_mean(other, "OperableArea", weights), - ShadingSystemSetpoint=self.float_mean(other, "ShadingSystemSetpoint", weights), - ShadingSystemTransmittance=self.float_mean(other, "ShadingSystemTransmittance", weights), - ShadingSystemType=max(self.ShadingSystemType, other.ShadingSystemType), - ZoneMixingDeltaTemperature=self.float_mean(other, "ZoneMixingDeltaTemperature", weights), - ZoneMixingFlowRate=self.float_mean(other, "ZoneMixingFlowRate", weights), - ZoneMixingAvailabilitySchedule=UmiSchedule.combine( + new_attr = { + "Construction": WindowConstruction.combine(self.Construction, other.Construction, weights), + "AfnDischargeC": self.float_mean(other, "AfnDischargeC", weights), + "AfnTempSetpoint": self.float_mean(other, "AfnTempSetpoint", weights), + "AfnWindowAvailability": UmiSchedule.combine( + self.AfnWindowAvailability, other.AfnWindowAvailability, weights + ), + "IsShadingSystemOn": any([self.IsShadingSystemOn, other.IsShadingSystemOn]), + "IsVirtualPartition": any([self.IsVirtualPartition, other.IsVirtualPartition]), + "IsZoneMixingOn": any([self.IsZoneMixingOn, other.IsZoneMixingOn]), + "OperableArea": self.float_mean(other, "OperableArea", weights), + "ShadingSystemSetpoint": self.float_mean(other, "ShadingSystemSetpoint", weights), + "ShadingSystemTransmittance": self.float_mean(other, "ShadingSystemTransmittance", weights), + "ShadingSystemType": max(self.ShadingSystemType, other.ShadingSystemType), + "ZoneMixingDeltaTemperature": self.float_mean(other, "ZoneMixingDeltaTemperature", weights), + "ZoneMixingFlowRate": self.float_mean(other, "ZoneMixingFlowRate", weights), + "ZoneMixingAvailabilitySchedule": UmiSchedule.combine( self.ZoneMixingAvailabilitySchedule, other.ZoneMixingAvailabilitySchedule, weights, ), - ShadingSystemAvailabilitySchedule=UmiSchedule.combine( + "ShadingSystemAvailabilitySchedule": UmiSchedule.combine( self.ShadingSystemAvailabilitySchedule, other.ShadingSystemAvailabilitySchedule, weights, ), - Type=max(self.Type, other.Type), - ) + "Type": max(self.Type, other.Type), + } new_obj = WindowSetting(**meta, **new_attr) new_obj.predecessors.update(self.predecessors + other.predecessors) return new_obj @@ -820,28 +807,28 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - AfnDischargeC=self.AfnDischargeC, - AfnTempSetpoint=self.AfnTempSetpoint, - AfnWindowAvailability=self.AfnWindowAvailability, - Construction=self.Construction, - IsShadingSystemOn=self.IsShadingSystemOn, - IsVirtualPartition=self.IsVirtualPartition, - IsZoneMixingOn=self.IsZoneMixingOn, - OperableArea=self.OperableArea, - ShadingSystemAvailabilitySchedule=self.ShadingSystemAvailabilitySchedule, - ShadingSystemSetpoint=self.ShadingSystemSetpoint, - ShadingSystemTransmittance=self.ShadingSystemTransmittance, - ShadingSystemType=self.ShadingSystemType, - Type=self.Type, - ZoneMixingAvailabilitySchedule=self.ZoneMixingAvailabilitySchedule, - ZoneMixingDeltaTemperature=self.ZoneMixingDeltaTemperature, - ZoneMixingFlowRate=self.ZoneMixingFlowRate, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "AfnDischargeC": self.AfnDischargeC, + "AfnTempSetpoint": self.AfnTempSetpoint, + "AfnWindowAvailability": self.AfnWindowAvailability, + "Construction": self.Construction, + "IsShadingSystemOn": self.IsShadingSystemOn, + "IsVirtualPartition": self.IsVirtualPartition, + "IsZoneMixingOn": self.IsZoneMixingOn, + "OperableArea": self.OperableArea, + "ShadingSystemAvailabilitySchedule": self.ShadingSystemAvailabilitySchedule, + "ShadingSystemSetpoint": self.ShadingSystemSetpoint, + "ShadingSystemTransmittance": self.ShadingSystemTransmittance, + "ShadingSystemType": self.ShadingSystemType, + "Type": self.Type, + "ZoneMixingAvailabilitySchedule": self.ZoneMixingAvailabilitySchedule, + "ZoneMixingDeltaTemperature": self.ZoneMixingDeltaTemperature, + "ZoneMixingFlowRate": self.ZoneMixingFlowRate, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } @property def children(self): diff --git a/archetypal/template/zone_construction_set.py b/archetypal/template/zone_construction_set.py index c711391f9..054d6d1f9 100644 --- a/archetypal/template/zone_construction_set.py +++ b/archetypal/template/zone_construction_set.py @@ -2,6 +2,7 @@ import collections import logging as lg +from typing import TYPE_CHECKING, ClassVar from validator_collection import validators @@ -9,11 +10,14 @@ from archetypal.template.umi_base import UmiBase from archetypal.utils import log, reduce, timeit +if TYPE_CHECKING: + from archetypal.template import ZoneDefinition + class ZoneConstructionSet(UmiBase): """ZoneConstructionSet class.""" - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["ZoneConstructionSet"]] = [] __slots__ = ( "_facade", @@ -68,7 +72,7 @@ def __init__( IsSlabAdiabatic (bool): If True, surface is adiabatic. **kwargs: """ - super(ZoneConstructionSet, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.Slab = Slab self.IsSlabAdiabatic = IsSlabAdiabatic self.Roof = Roof @@ -230,11 +234,11 @@ def volume(self, value): @classmethod @timeit - def from_zone(cls, zone, **kwargs): + def from_zone(cls, zone: "ZoneDefinition", **kwargs): """Create a ZoneConstructionSet from a ZoneDefinition object. Args: - zone (ZoneDefinition): + zone (ZoneDefinition): The zone object. """ name = zone.Name + "_ZoneConstructionSet" # dispatch surfaces @@ -261,30 +265,15 @@ def from_zone(cls, zone, **kwargs): # Returning a set() for each groups of Constructions. facades = set(facade) - if facades: - facade = reduce(OpaqueConstruction.combine, facades) - else: - facade = None + facade = reduce(OpaqueConstruction.combine, facades) if facades else None grounds = set(ground) - if grounds: - ground = reduce(OpaqueConstruction.combine, grounds) - else: - ground = None + ground = reduce(OpaqueConstruction.combine, grounds) if grounds else None partitions = set(partition) - if partitions: - partition = reduce(OpaqueConstruction.combine, partitions) - else: - partition = None + partition = reduce(OpaqueConstruction.combine, partitions) if partitions else None roofs = set(roof) - if roofs: - roof = reduce(OpaqueConstruction.combine, roofs) - else: - roof = None + roof = reduce(OpaqueConstruction.combine, roofs) if roofs else None slabs = set(slab) - if slabs: - slab = reduce(OpaqueConstruction.combine, slabs) - else: - slab = None + slab = reduce(OpaqueConstruction.combine, slabs) if slabs else None z_set = cls( Facade=facade, @@ -382,10 +371,7 @@ def combine(self, other, weights=None, **kwargs): # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) meta = self._get_predecessors_meta(other) @@ -471,22 +457,22 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Facade=self.Facade, - Ground=self.Ground, - Partition=self.Partition, - Roof=self.Roof, - Slab=self.Slab, - IsFacadeAdiabatic=self.IsFacadeAdiabatic, - IsGroundAdiabatic=self.IsGroundAdiabatic, - IsPartitionAdiabatic=self.IsPartitionAdiabatic, - IsRoofAdiabatic=self.IsRoofAdiabatic, - IsSlabAdiabatic=self.IsSlabAdiabatic, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - ) + return { + "Facade": self.Facade, + "Ground": self.Ground, + "Partition": self.Partition, + "Roof": self.Roof, + "Slab": self.Slab, + "IsFacadeAdiabatic": self.IsFacadeAdiabatic, + "IsGroundAdiabatic": self.IsGroundAdiabatic, + "IsPartitionAdiabatic": self.IsPartitionAdiabatic, + "IsRoofAdiabatic": self.IsRoofAdiabatic, + "IsSlabAdiabatic": self.IsSlabAdiabatic, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + } def duplicate(self): """Get copy of self.""" @@ -581,14 +567,14 @@ def resolved_surface(self): return self._dispatch[a, b](self.surf) except KeyError as e: raise NotImplementedError( - "surface '%s' in zone '%s' not supported by surface dispatcher " - "with keys %s" % (self.surf.Name, self.zone.Name, e) - ) + f"surface '{self.surf.Name}' in zone '{self.zone.Name}' not supported by surface dispatcher " + f"with keys {e}" + ) from e @staticmethod def _do_facade(surf): log( - 'surface "%s" assigned as a Facade' % surf.Name, + f'surface "{surf.Name}" assigned as a Facade', lg.DEBUG, name=surf.theidf.name, ) @@ -600,7 +586,7 @@ def _do_facade(surf): @staticmethod def _do_ground(surf): log( - 'surface "%s" assigned as a Ground' % surf.Name, + f'surface "{surf.Name}" assigned as a Ground', lg.DEBUG, name=surf.theidf.name, ) @@ -617,7 +603,7 @@ def _do_partition(surf): oc.area = surf.area oc.Category = "Partition" log( - 'surface "%s" assigned as a Partition' % surf.Name, + f'surface "{surf.Name}" assigned as a Partition', lg.DEBUG, name=surf.theidf.name, ) @@ -631,7 +617,7 @@ def _do_partition(surf): @staticmethod def _do_roof(surf): log( - 'surface "%s" assigned as a Roof' % surf.Name, + f'surface "{surf.Name}" assigned as a Roof', lg.DEBUG, name=surf.theidf.name, ) @@ -643,7 +629,7 @@ def _do_roof(surf): @staticmethod def _do_slab(surf): log( - 'surface "%s" assigned as a Slab' % surf.Name, + f'surface "{surf.Name}" assigned as a Slab', lg.DEBUG, name=surf.theidf.name, ) @@ -655,7 +641,7 @@ def _do_slab(surf): @staticmethod def _do_basement(surf): log( - 'surface "%s" ignored because basement facades are not supported' % surf.Name, + f'surface "{surf.Name}" ignored because basement facades are not supported', lg.WARNING, name=surf.theidf.name, ) diff --git a/archetypal/template/zonedefinition.py b/archetypal/template/zonedefinition.py index 48b82a00a..7827d374a 100644 --- a/archetypal/template/zonedefinition.py +++ b/archetypal/template/zonedefinition.py @@ -3,6 +3,7 @@ import collections import sqlite3 import time +from typing import ClassVar from eppy.bunch_subclass import BadEPFieldError from sigfig import round @@ -26,7 +27,7 @@ class ZoneDefinition(UmiBase): .. image:: ../images/template/zoneinfo-zone.png """ - _CREATED_OBJECTS = [] + _CREATED_OBJECTS: ClassVar[list["ZoneDefinition"]] = [] __slots__ = ( "_internal_mass_exposed_per_floor_area", @@ -97,7 +98,7 @@ def __init__( occupants (float): **kwargs: """ - super(ZoneDefinition, self).__init__(Name, **kwargs) + super().__init__(Name, **kwargs) self.Ventilation = Ventilation self.Loads = Loads @@ -585,10 +586,7 @@ def combine(self, other, weights=None, allow_duplicates=False): # Check if other is the same type as self if not isinstance(other, self.__class__): - msg = "Cannot combine %s with %s" % ( - self.__class__.__name__, - other.__class__.__name__, - ) + msg = f"Cannot combine {self.__class__.__name__} with {other.__class__.__name__}" raise NotImplementedError(msg) meta = self._get_predecessors_meta(other) @@ -607,20 +605,20 @@ def combine(self, other, weights=None, allow_duplicates=False): ) ) - new_attr = dict( - Conditioning=ZoneConditioning.combine(self.Conditioning, other.Conditioning, weights), - Constructions=ZoneConstructionSet.combine(self.Constructions, other.Constructions, weights), - Ventilation=VentilationSetting.combine(self.Ventilation, other.Ventilation), - Windows=WindowSetting.combine(self.Windows, other.Windows, weights), - DaylightMeshResolution=self.float_mean(other, "DaylightMeshResolution", weights=weights), - DaylightWorkplaneHeight=self.float_mean(other, "DaylightWorkplaneHeight", weights), - DomesticHotWater=DomesticHotWaterSetting.combine(self.DomesticHotWater, other.DomesticHotWater), - InternalMassConstruction=OpaqueConstruction.combine( + new_attr = { + "Conditioning": ZoneConditioning.combine(self.Conditioning, other.Conditioning, weights), + "Constructions": ZoneConstructionSet.combine(self.Constructions, other.Constructions, weights), + "Ventilation": VentilationSetting.combine(self.Ventilation, other.Ventilation), + "Windows": WindowSetting.combine(self.Windows, other.Windows, weights), + "DaylightMeshResolution": self.float_mean(other, "DaylightMeshResolution", weights=weights), + "DaylightWorkplaneHeight": self.float_mean(other, "DaylightWorkplaneHeight", weights), + "DomesticHotWater": DomesticHotWaterSetting.combine(self.DomesticHotWater, other.DomesticHotWater), + "InternalMassConstruction": OpaqueConstruction.combine( self.InternalMassConstruction, other.InternalMassConstruction ), - InternalMassExposedPerFloorArea=self.float_mean(other, "InternalMassExposedPerFloorArea", weights), - Loads=ZoneLoad.combine(self.Loads, other.Loads, weights), - ) + "InternalMassExposedPerFloorArea": self.float_mean(other, "InternalMassExposedPerFloorArea", weights), + "Loads": ZoneLoad.combine(self.Loads, other.Loads, weights), + } new_obj = ZoneDefinition(**meta, **new_attr) # transfer aggregated values [volume, area, occupants] to new combined zone @@ -663,30 +661,30 @@ def mapping(self, validate=False): if validate: self.validate() - return dict( - Conditioning=self.Conditioning, - Constructions=self.Constructions, - DaylightMeshResolution=self.DaylightMeshResolution, - DaylightWorkplaneHeight=self.DaylightWorkplaneHeight, - DomesticHotWater=self.DomesticHotWater, - InternalMassConstruction=self.InternalMassConstruction, - InternalMassExposedPerFloorArea=self.InternalMassExposedPerFloorArea, - Windows=self.Windows, - Loads=self.Loads, - Ventilation=self.Ventilation, - Category=self.Category, - Comments=self.Comments, - DataSource=self.DataSource, - Name=self.Name, - area=self.area, - volume=self.volume, - occupants=self.occupants, - is_part_of_conditioned_floor_area=self.is_part_of_conditioned_floor_area, - is_part_of_total_floor_area=self.is_part_of_total_floor_area, - multiplier=self.multiplier, - zone_surfaces=self.zone_surfaces, - is_core=self.is_core, - ) + return { + "Conditioning": self.Conditioning, + "Constructions": self.Constructions, + "DaylightMeshResolution": self.DaylightMeshResolution, + "DaylightWorkplaneHeight": self.DaylightWorkplaneHeight, + "DomesticHotWater": self.DomesticHotWater, + "InternalMassConstruction": self.InternalMassConstruction, + "InternalMassExposedPerFloorArea": self.InternalMassExposedPerFloorArea, + "Windows": self.Windows, + "Loads": self.Loads, + "Ventilation": self.Ventilation, + "Category": self.Category, + "Comments": self.Comments, + "DataSource": self.DataSource, + "Name": self.Name, + "area": self.area, + "volume": self.volume, + "occupants": self.occupants, + "is_part_of_conditioned_floor_area": self.is_part_of_conditioned_floor_area, + "is_part_of_total_floor_area": self.is_part_of_total_floor_area, + "multiplier": self.multiplier, + "zone_surfaces": self.zone_surfaces, + "is_core": self.is_core, + } def __add__(self, other): """Return a combination of self and other.""" diff --git a/archetypal/umi_template.py b/archetypal/umi_template.py index 97662d3ca..773210922 100644 --- a/archetypal/umi_template.py +++ b/archetypal/umi_template.py @@ -1,10 +1,12 @@ """UmiTemplateLibrary Module.""" +from __future__ import annotations + import json import logging as lg -from collections import OrderedDict +from collections import OrderedDict, defaultdict from concurrent.futures.thread import ThreadPoolExecutor -from typing import List +from typing import ClassVar, Union import networkx as nx from pandas.io.common import get_handle @@ -40,6 +42,13 @@ from archetypal.utils import CustomJSONEncoder, log, parallel_process +class AllFailedError(Exception): + """Exception raised when all BuildingTemplates failed to be created.""" + + def __init__(self, results): + super().__init__([res for res in results.values() if isinstance(res, Exception)]) + + class UmiTemplateLibrary: """Handles parsing and creating Template Library Files for UMI for Rhino. @@ -47,7 +56,7 @@ class UmiTemplateLibrary: - See :meth:`from_idf_files` to create a library by converting existing IDF models. """ - _LIB_GROUPS = [ + _LIB_GROUPS: ClassVar[list[str]] = [ "GasMaterials", "GlazingMaterials", "OpaqueMaterials", @@ -150,9 +159,9 @@ def __iter__(self): def __getitem__(self, item): return self.__dict__[item] - def __add__(self, other: "UmiTemplateLibrary"): + def __add__(self, other: UmiTemplateLibrary): """Combined""" - for key, group in other: + for _, group in other: # for each group items for component in group: component.id = None # Reset the component's id @@ -171,7 +180,7 @@ def _clear_components_list(self, except_groups=None): except_groups = [] exception = ["BuildingTemplates"] exception.extend(except_groups) - for key, group in self: + for key, _ in self: if key not in exception: setattr(self, key, []) @@ -179,7 +188,7 @@ def _clear_components_list(self, except_groups=None): def object_list(self): """Get list of all objects in self, including orphaned objects.""" objs = [] - for name, group in self: + for _, group in self: objs.extend(group) return objs @@ -262,12 +271,12 @@ def from_idf_files( # If all exceptions, raise them for debugging if all(isinstance(x, Exception) for x in results.values()): - raise Exception([res for res in results.values() if isinstance(res, Exception)]) + raise AllFailedError(results) umi_template.BuildingTemplates = [res for res in results.values() if not isinstance(res, Exception)] if keep_all_zones: - _zones = set(obj.get_unique() for obj in ZoneDefinition._CREATED_OBJECTS) + _zones = {obj.get_unique() for obj in ZoneDefinition._CREATED_OBJECTS} for zone in _zones: umi_template.ZoneDefinitions.append(zone) exceptions = [ZoneDefinition.__name__] @@ -580,52 +589,35 @@ def to_json( def to_dict(self): """Return UmiTemplateLibrary dictionary representation.""" - # First, reset existing name - - # Create ordered dict with empty list data_dict = OrderedDict([(key, []) for key in self._LIB_GROUPS]) - # create dict values for group_name, group in self: - # reset unique names for group UniqueName.existing = {} - obj: UmiBase for obj in group: - try: - data = obj.to_dict() - except AttributeError as e: - raise AttributeError(f"{e} for {obj}") + data = obj.to_dict() data.update({"Name": UniqueName(data.get("Name"))}) - data_dict.setdefault(group_name, []).append(data) + data_dict[group_name].append(data) - if not data_dict.get("GasMaterials"): - # Umi needs at least one gas material even if it is not necessary. + if not data_dict["GasMaterials"]: data = GasMaterial(Name="AIR").to_dict() data.update({"Name": UniqueName(data.get("Name"))}) - data_dict.get("GasMaterials").append(data) + data_dict["GasMaterials"].append(data) data_dict.move_to_end("GasMaterials", last=False) - # Correct naming convention and reorder categories - for key in tuple(data_dict.keys()): - v = data_dict[key] - del data_dict[key] + for key in list(data_dict.keys()): if key == "ZoneDefinitions": - key = "Zones" - if key == "StructureInformations": - key = "StructureDefinitions" - data_dict[key] = v + data_dict["Zones"] = data_dict.pop(key) + elif key == "StructureInformations": + data_dict["StructureDefinitions"] = data_dict.pop(key) - # Validate assert no_duplicates(data_dict, attribute="Name") - # Sort values for key in data_dict: - # Sort the list elements by their Name data_dict[key] = sorted(data_dict[key], key=lambda x: x.get("Name")) return data_dict - def unique_components(self, *args: str, exceptions: List[str] = None, keep_orphaned=False): + def unique_components(self, *args: str, exceptions: list[str] | None = None, keep_orphaned=False): """Keep only unique components. Starts by clearing all objects in self except self.BuildingTemplates. @@ -670,9 +662,8 @@ def unique_components(self, *args: str, exceptions: List[str] = None, keep_orpha for component in group: # travers each object using generator for parent, key, obj in parent_key_child_traversal(component): - if obj.__class__.__name__ + "s" in inclusion: - if key: - setattr(parent, key, obj.get_unique()) # set unique object on key + if obj.__class__.__name__ + "s" in inclusion and key: + setattr(parent, key, obj.get_unique()) # set unique object on key self.update_components_list(exceptions=exceptions) # Update the components list if keep_orphaned: @@ -747,48 +738,39 @@ def to_graph(self, include_orphans=False): return G -def no_duplicates(file, attribute="Name"): - """Assert whether or not dict has duplicated Names. - - `attribute` can be another attribute name like "$id". - - Args: - file (str or dict): Path of the json file or dict containing umi objects groups - attribute (str): Attribute to search for duplicates in json UMI structure. - eg. : "$id", "Name". - - Returns: - bool: True if no duplicates. - - Raises: - Exception if duplicates found. - """ - import json - from collections import defaultdict +def no_duplicates(file: Union[str, dict], attribute="Name"): + """Assert whether or not dict has duplicated Names.""" if isinstance(file, str): - data = json.loads(open(file).read()) + with open(file) as f: + data = json.loads(f.read()) else: data = file - ids = {} + ids = defaultdict(lambda: defaultdict(int)) + for key, value in data.items(): - ids[key] = defaultdict(int) for component in value: - try: - _id = component[attribute] - except KeyError: - pass # BuildingTemplate does not have an id - else: + _id = component.get(attribute) + if _id: ids[key][_id] += 1 + dups = { - key: dict(filter(lambda x: x[1] > 1, values.items())) + key: {k: v for k, v in values.items() if v > 1} for key, values in ids.items() - if dict(filter(lambda x: x[1] > 1, values.items())) + if any(v > 1 for v in values.values()) } - if any(dups.values()): - raise Exception(f"Duplicate {attribute} found: {dups}") - else: - return True + + if dups: + raise DuplicateAttributeError(attribute, dups) + return True + + +class DuplicateAttributeError(Exception): + """Exception raised for duplicate attributes in UMI objects.""" + + def __init__(self, attribute, duplicates): + """Initialize a DuplicateAttributeError.""" + super().__init__(f"Duplicate {attribute} found: {duplicates}") DEEP_OBJECTS = (UmiBase, MaterialLayer, GasLayer, YearSchedulePart, MassRatio, list) diff --git a/archetypal/utils.py b/archetypal/utils.py index 5641b24f9..2da7584c5 100644 --- a/archetypal/utils.py +++ b/archetypal/utils.py @@ -223,10 +223,7 @@ def weighted_mean(series, df, weighting_variable): # of multipling them together. if not isinstance(weighting_variable, list): weighting_variable = [weighting_variable] - try: - weights = df.loc[series.index, weighting_variable].astype("float").prod(axis=1) - except Exception: - raise + weights = df.loc[series.index, weighting_variable].astype("float").prod(axis=1) # Try to average try: @@ -402,7 +399,7 @@ def angle(v1, v2, acute=True): angle (float): angle between the 2 vectors in degree """ angle = np.arccos(np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))) - if acute == True: + if acute is True: return angle else: return 2 * np.pi - angle @@ -438,7 +435,7 @@ def timeit(method): def timed(*args, **kwargs): ts = time.time() - log("Executing %r..." % method.__qualname__) + log(f"Executing {method.__qualname__!r}...") result = method(*args, **kwargs) te = time.time() @@ -446,14 +443,14 @@ def timed(*args, **kwargs): try: try: name = result.Name - except: + except Exception: name = result.__qualname__ - except: + except Exception: name = str(result) if tt > 0.001: - log("Completed %r for %r in %.3f s" % (method.__qualname__, name, tt)) + log(f"Completed {method.__qualname__!r} for {name!r} in {tt:.3f} s") else: - log("Completed %r for %r in %.3f ms" % (method.__qualname__, name, tt * 1000)) + log(f"Completed {method.__qualname__!r} for {name!r} in {tt * 1000:.3f} ms") return result return timed @@ -463,10 +460,7 @@ def lcm(x, y): """This function takes two integers and returns the least common multiple.""" # choose the greater number - if x > y: - greater = x - else: - greater = y + greater = x if x > y else y while True: if (greater % x == 0) and (greater % y == 0): @@ -515,23 +509,23 @@ def recursive_len(item): Returns: Total number of elements in nested list """ - if type(item) == list: + if isinstance(item, list): return sum(recursive_len(subitem) for subitem in item) else: return 1 -def rotate(l, n): +def rotate(items, n): """Shift list elements to the left Args: - l (list): list to rotate + items (list): list to rotate n (int): number to shift list to the left Returns: list: shifted list. """ - return l[n:] + l[:n] + return items[n:] + items[:n] def parallel_process( @@ -635,7 +629,7 @@ def parallel_process( except Exception as e: if debug: lg.warning(str(e)) - raise e + raise result_done = e # Append to the list of results out[filename] = result_done diff --git a/archetypal/zone_graph.py b/archetypal/zone_graph.py index 4e398121f..cd4cdb703 100644 --- a/archetypal/zone_graph.py +++ b/archetypal/zone_graph.py @@ -110,7 +110,7 @@ def from_idf(cls, idf, log_adj_report=True, **kwargs): this_cstr = surface["Construction_Name"] their_cstr = adj_surf["Construction_Name"] is_diff_cstr = surface["Construction_Name"] != adj_surf["Construction_Name"] - except: + except Exception: this_cstr, their_cstr, is_diff_cstr = None, None, None # create edge from this zone to the adjacent zone G.add_edge( @@ -125,7 +125,7 @@ def from_idf(cls, idf, log_adj_report=True, **kwargs): else: pass if log_adj_report: - msg = "Printing Adjacency Report for zone %s\n" % zone.Name + msg = f"Printing Adjacency Report for zone {zone.Name}\n" msg += tabulate.tabulate(adj_report, headers="keys") log(msg) @@ -148,7 +148,7 @@ def __init__(self, incoming_graph_data=None, **attr): attr: keyword arguments, optional (default= no attributes) Attributes to add to graph as key=value pairs. """ - super(ZoneGraph, self).__init__(incoming_graph_data=incoming_graph_data, **attr) + super().__init__(incoming_graph_data=incoming_graph_data, **attr) def plot_graph3d( self, @@ -312,7 +312,7 @@ def avg(zone: EpBunch): # Loop on the list of edges to get the x,y,z, coordinates of the # connected nodes # Those two points are the extrema of the line to be plotted - for i, j in enumerate(self.edges()): + for _, j in enumerate(self.edges()): x = np.array((pos[j[0]][0], pos[j[1]][0])) y = np.array((pos[j[0]][1], pos[j[1]][1])) z = np.array((pos[j[0]][2], pos[j[1]][2])) @@ -428,8 +428,8 @@ def plot_graph2d( """ try: import matplotlib.pyplot as plt - except ImportError: - raise ImportError("Matplotlib required for draw()") + except ImportError as e: + raise ImportError("Matplotlib required for draw()") from e except RuntimeError: log("Matplotlib unable to open display", lg.WARNING) raise @@ -460,10 +460,7 @@ def plot_graph2d( # choose nodes and color for each iteration nlist = [nt] label = getattr(nt, "Name", nt) - if color_nodes: - node_color = [colors[nt]] - else: - node_color = "#1f78b4" + node_color = [colors[nt]] if color_nodes else "#1f78b4" # draw the graph sc = networkx.draw_networkx_nodes( tree, diff --git a/docs/conf.py b/docs/conf.py index 7b27fe52b..ee00cc4cc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,7 +24,7 @@ # -- Project information ----------------------------------------------------- project = "archetypal" -copyright = f"{datetime.datetime.now().year}, Samuel Letellier-Duchesne" +legal = f"{datetime.datetime.now().year}, Samuel Letellier-Duchesne" author = "Samuel Letellier-Duchesne" # The full version, including alpha/beta/rc tags @@ -193,7 +193,7 @@ def setup(app): epub_title = project epub_author = author epub_publisher = author -epub_copyright = copyright +epub_copyright = legal # The unique identifier of the text. This can be a ISBN number # or the project homepage. diff --git a/docs/examples/parallel_process.py b/docs/examples/parallel_process.py index 2a8080fe6..105e325ed 100644 --- a/docs/examples/parallel_process.py +++ b/docs/examples/parallel_process.py @@ -17,14 +17,14 @@ def main(): # setup the runner. We'll use the DataFrame index as keys (k). rundict = { - k: dict( - eplus_file=str(file), - prep_outputs=True, - epw=str(epw), - expandobjects=False, - verbose="v", - design_day=True, - ) + k: { + "eplus_file": str(file), + "prep_outputs": True, + "epw": str(epw), + "expandobjects": False, + "verbose": "v", + "design_day": True, + } for k, file in idfs.file.to_dict().items() } diff --git a/poetry.lock b/poetry.lock index a70d29bb3..92548a347 100644 --- a/poetry.lock +++ b/poetry.lock @@ -669,6 +669,20 @@ files = [ {file = "esoreader-1.2.3.tar.gz", hash = "sha256:6b7a554cde36c90f8eea45273df5cec3a27072d433c9ce56c72a55e13b69407d"}, ] +[[package]] +name = "eval-type-backport" +version = "0.2.0" +description = "Like `typing._eval_type`, but lets older Python versions use newer typing features." +optional = false +python-versions = ">=3.8" +files = [ + {file = "eval_type_backport-0.2.0-py3-none-any.whl", hash = "sha256:ac2f73d30d40c5a30a80b8739a789d6bb5e49fdffa66d7912667e2015d9c9933"}, + {file = "eval_type_backport-0.2.0.tar.gz", hash = "sha256:68796cfbc7371ebf923f03bdf7bef415f3ec098aeced24e054b253a0e78f7b37"}, +] + +[package.extras] +tests = ["pytest"] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -2303,6 +2317,23 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "pytest-xdist" version = "3.6.1" @@ -3475,4 +3506,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "53ebd4fa6361caba2c172815baaeb8d46fa44c1962eeac5bdc877032f00ac756" +content-hash = "f93c1e2f9c17b4e8ec114a7af9f22b4f20054da8cae75c1c6f6b5430ee89be66" diff --git a/pyproject.toml b/pyproject.toml index 5bbffbeb8..ebb811f1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ numpy = [ {version = "^2.0.2", python = "<=3.9"}, {version = "^2.1.1", python = "3.10"} ] +eval-type-backport = {version = "^0.2.0", python = "3.9"} # geomeppy.dependencies shapely = "^2.0.4" @@ -63,6 +64,7 @@ transforms3d = "^0.4.1" pytest = "^8.2.2" pytest-cov = "^5.0.0" pytest-xdist = "^3.6.1" +pytest-mock = "^3.14.0" tox = "^4.19.0" pre-commit = "^3.8.0" @@ -85,7 +87,7 @@ archetypal = "archetypal.cli:cli" testpaths = ["tests"] [tool.ruff] -target-version = "py310" +target-version = "py39" line-length = 120 fix = true select = [ @@ -94,32 +96,32 @@ select = [ # flake8-bandit # "S", # flake8-bugbear - # "B", + "B", # flake8-builtins - # "A", + "A", # flake8-comprehensions - # "C4", + "C4", # flake8-debugger "T10", # flake8-simplify - # "SIM", + "SIM", # isort "I", # mccabe "C90", # pycodestyle - #"E", + "E", "W", # pyflakes - "F", + "F", # pygrep-hooks "PGH", # pyupgrade - # "UP", + "UP", # ruff - # "RUF", + "RUF", # tryceratops - # "TRY", + "TRY", ] ignore = [ # LineTooLong @@ -127,7 +129,13 @@ ignore = [ # DoNotAssignLambda "E731", # Too Complex - "C901" + "C901", + # raise-vanilla-args + "TRY003", + # Checks for uses of isinstance and issubclass that take a tuple of types for comparison. + "UP038", + # Python builtin is shadowed by class attribute {name} from {row} + "A003" ] exclude = ["tests/input_data/*", "docker/trnsidf/*", "geomeppy"] @@ -143,3 +151,10 @@ source = ["archetypal"] [tool.ruff.per-file-ignores] "tests/*" = ["S101"] + +[tool.ruff.lint] +allowed-confusables = ["ρ"] + +[tool.ruff.lint.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true diff --git a/tests/conftest.py b/tests/conftest.py index 5e779db76..71a065e3c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import glob import os import sys from pathlib import Path @@ -30,14 +29,6 @@ def scratch_then_cache(request): d.rmtree_p() -samples_ = ["regular", "umi_samples"] # ['problematic', 'regular', 'umi_samples'] - - -@pytest.fixture(params=samples_, ids=samples_, scope="session") -def idf_source(request): - return glob.glob(f"tests/input_data/{request.param}/*.idf") - - @pytest.fixture(scope="session") def config(): utils.config( @@ -72,7 +63,7 @@ def pytest_runtest_setup(item): supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers()) plat = sys.platform if supported_platforms and plat not in supported_platforms: - pytest.skip("cannot run on platform %s" % (plat)) + pytest.skip(f"cannot run on platform {plat}") # dynamically define files to be ignored diff --git a/tests/test_dataportals.py b/tests/test_dataportals.py index 614ed2ddb..c5facb064 100644 --- a/tests/test_dataportals.py +++ b/tests/test_dataportals.py @@ -220,7 +220,7 @@ def test_download_and_load_bld_window(config): def test_statcan(config): - data = dict(type="json", lang="E", dguid="2016A000011124", topic=5, notes=0) + data = {"response_format": "json", "lang": "E", "dguid": "2016A000011124", "topic": 5, "notes": 0} response = dataportal.stat_can_request(**data) print(response) @@ -230,7 +230,7 @@ def test_statcan(config): def test_statcan_error(config): # Tests statcan with error in inputs - data = dict(type="json", lang="E", dguid="wrong_string", topic=5, notes=0) + data = {"response_format": "json", "lang": "E", "dguid": "wrong_string", "topic": 5, "notes": 0} response = dataportal.stat_can_request(**data) print(response) @@ -239,7 +239,7 @@ def test_statcan_error(config): def test_statcan_geo(config): - data = dict(type="json", lang="E", geos="PR", cpt="00") + data = {"response_format": "json", "lang": "E", "geos": "PR", "cpt": "00"} response = dataportal.stat_can_geo_request(**data) print(response) @@ -249,7 +249,7 @@ def test_statcan_geo(config): def test_statcan_geo_error(config): # Tests statcan_geo with error in inputs - data = dict(type="json", lang="E", geos="wrong_string", cpt="00") + data = {"response_format": "json", "lang": "E", "geos": "wrong_string", "cpt": "00"} response = dataportal.stat_can_geo_request(**data) print(response) diff --git a/tests/test_energypandas.py b/tests/test_energypandas.py index 5600b26a8..efaf1bcdc 100644 --- a/tests/test_energypandas.py +++ b/tests/test_energypandas.py @@ -74,7 +74,7 @@ def test_discretize(self, rd_es): rd_es.discretize_tsam(noTypicalPeriods=1, inplace=True) assert_almost_equal(res.sum(), 2.118713381170598, decimal=3) # check that the type is maintained - assert type(rd_es) == EnergySeries + assert isinstance(res, EnergySeries) class TestEnergyDataFrame: @@ -88,7 +88,7 @@ def test_discretize(self, rd_edf): rd_edf.discretize_tsam(noTypicalPeriods=1, inplace=True) assert hasattr(rd_edf, "agg") # check that the type is maintained - assert type(rd_edf) == EnergyDataFrame + assert isinstance(rd_edf, EnergyDataFrame) def test_plot_2d(self, rd_edf): fig, ax = rd_edf.plot2d( diff --git a/tests/test_idfclass.py b/tests/test_idfclass.py index 21fcd62e4..5305f9e29 100644 --- a/tests/test_idfclass.py +++ b/tests/test_idfclass.py @@ -9,6 +9,7 @@ InvalidEnergyPlusVersion, ) from archetypal.eplus_interface.version import EnergyPlusVersion +from archetypal.idfclass.idf import SimulationNotRunError from archetypal.utils import parallel_process from .conftest import data_dir @@ -110,8 +111,10 @@ def test_specific_version(self, config, natvent_v9_1_0): assert natvent_v9_1_0.idd_version == (9, 1, 0) assert natvent_v9_1_0.file_version == EnergyPlusVersion("9-1-0") - def test_specific_version_error_simulate(self, natvent_v9_1_0): - with pytest.raises(EnergyPlusVersionError): + def test_specific_version_error_simulate(self, natvent_v9_1_0, mocker): + with mocker.patch( + "archetypal.eplus_interface.energy_plus.EnergyPlusExe.get_exe_path", side_effect=EnergyPlusVersionError() + ), pytest.raises(EnergyPlusVersionError): natvent_v9_1_0.simulate() def test_version(self, natvent_v9_1_0): @@ -145,6 +148,8 @@ def test_sql(self, idf_model): assert idf_model.sql_file.exists() def test_processed_results(self, idf_model): + if not idf_model.simulation_dir.exists(): + idf_model.simulate() assert idf_model.process_results() def test_partition_ratio(self, idf_model): @@ -345,7 +350,7 @@ def shoebox_res(self, config): def test_retrieve_meters_nosim(self, config, shoebox_res): shoebox_res.simulation_dir.rmtree_p() - with pytest.raises(Exception): + with pytest.raises(SimulationNotRunError): print(shoebox_res.meters) def test_retrieve_meters(self, config, shoebox_res): diff --git a/tests/test_schedules.py b/tests/test_schedules.py index 25685bf90..0e7dddd15 100644 --- a/tests/test_schedules.py +++ b/tests/test_schedules.py @@ -111,7 +111,7 @@ def test_replace(self): def schedules_idf(): - config(cache_folder=os.getenv("ARCHETYPAL_CACHE") or data_dir / ".temp/cache") + config(cache_folder=os.getenv("ARCHETYPAL_CACHE") or (data_dir / "../.temp/cache").resolve()) idf = IDF( idf_file, epw=data_dir / "CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw", @@ -124,7 +124,7 @@ def schedules_idf(): idf = schedules_idf() schedules_dict = idf._get_all_schedules(yearly_only=True) schedules = list(schedules_dict.values()) -ids = [key.replace(" ", "_") for key in schedules_dict.keys()] +ids = [key.replace(" ", "_") for key in schedules_dict] schedules = [ pytest.param(schedule, marks=pytest.mark.xfail(reason="Can't quite capture all possibilities with special days")) @@ -188,7 +188,6 @@ def test_ep_versus_schedule(schedule_parametrized): new.series[index_slice].plot(ax=ax, legend=True, drawstyle="steps-post", linestyle="dotted") expected.loc[index_slice].plot(label="E+", legend=True, ax=ax, drawstyle="steps-post", linestyle="dashdot") ax.set_title(orig.Name.capitalize()) - plt.show() print(pd.DataFrame({"actual": orig.series[mask], "expected": expected[mask]})) np.testing.assert_array_almost_equal(orig.all_values, expected, verbose=True) diff --git a/tests/test_template.py b/tests/test_template.py index d2694e664..6daefce6a 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1001,9 +1001,9 @@ def concrete_layer(self): @pytest.fixture() def facebrick_and_concrete(self, face_brick, thermal_insulation, hollow_concrete_block, plaster): - """A :class:Construction based on the `Facebrick–concrete wall` from: On + """A :class:Construction based on the `Facebrick-concrete wall` from: On the thermal time constant of structural walls. Applied Thermal - Engineering, 24(5–6), 743–757. + Engineering, 24(5-6), 743-757. https://doi.org/10.1016/j.applthermaleng.2003.10.015 """ layers = [ @@ -1012,15 +1012,15 @@ def facebrick_and_concrete(self, face_brick, thermal_insulation, hollow_concrete MaterialLayer(hollow_concrete_block, 0.2), MaterialLayer(plaster, 0.02), ] - oc_a = OpaqueConstruction(Layers=layers, Name="Facebrick–concrete wall") + oc_a = OpaqueConstruction(Layers=layers, Name="Facebrick-concrete wall") yield oc_a @pytest.fixture() def insulated_concrete_wall(self, face_brick, thermal_insulation, concrete_layer, plaster): - """A :class:Construction based on the `Facebrick–concrete wall` from: On + """A :class:Construction based on the `Facebrick-concrete wall` from: On the thermal time constant of structural walls. Applied Thermal - Engineering, 24(5–6), 743–757. + Engineering, 24(5-6), 743-757. https://doi.org/10.1016/j.applthermaleng.2003.10.015 """ layers = [ @@ -1156,7 +1156,7 @@ def test_hash_eq_opaq_constr(self, construction_a, construction_b): def test_real_word_construction(self, facebrick_and_concrete, insulated_concrete_wall): """This test is based on wall constructions, materials and results from: Tsilingiris, P. T. (2004). On the thermal time constant of structural - walls. Applied Thermal Engineering, 24(5–6), 743–757. + walls. Applied Thermal Engineering, 24(5-6), 743-757. https://doi.org/10.1016/j.applthermaleng.2003.10.015 Args: @@ -1504,13 +1504,13 @@ def test_shgc(self, b_glass_clear_3): temperature, r_values = triple.temperature_profile( outside_temperature=-18, inside_temperature=21, wind_speed=5.5 ) - assert [-18, -16.3, -16.1, 13.6, 13.8, 21.0] == pytest.approx(temperature, 1e-1) + assert pytest.approx(temperature, 1e-1) == [-18, -16.3, -16.1, 13.6, 13.8, 21.0] print(temperature, r_values) shgc = triple.shgc("summer") _, temperature = triple.heat_balance("summer") print("shgc:", shgc) - assert [32, 32.9, 32.9, 31.9, 31.8, 24.0] == pytest.approx(temperature, 1e-1) + assert pytest.approx(temperature, 1e-1) == [32, 32.9, 32.9, 31.9, 31.8, 24.0] print(temperature, r_values) # m2-K/W @@ -2089,19 +2089,20 @@ def test_naturalVentilation_from_zone(self, ventilatontests): zone_ep = idf.getobject("ZONE", "ZONE 1") z = ZoneDefinition.from_epbunch(ep_bunch=zone_ep, construct_parents=False) ventilation_setting = VentilationSetting.from_zone(z, zone_ep) - assert ventilation_setting.IsNatVentOn == False + assert ventilation_setting.IsNatVentOn is True + assert ventilation_setting.IsScheduledVentilationOn is False if idf_name == "VentilationSimpleTest.idf": zone_ep = idf.getobject("ZONE", "ZONE 2") z = ZoneDefinition.from_epbunch(ep_bunch=zone_ep, construct_parents=False) ventilation_setting = VentilationSetting.from_zone(z, zone_ep) - assert ventilation_setting.IsNatVentOn == True - assert ventilation_setting.IsScheduledVentilationOn == True + assert ventilation_setting.IsNatVentOn is False + assert ventilation_setting.IsScheduledVentilationOn is True if idf_name == "RefBldgWarehouseNew2004_Chicago.idf": zone_ep = idf.getobject("ZONE", "Office") z = ZoneDefinition.from_epbunch(ep_bunch=zone_ep, construct_parents=False) ventilation_setting = VentilationSetting.from_zone(z, zone_ep) - assert ventilation_setting.IsNatVentOn == False - assert ventilation_setting.IsScheduledVentilationOn == False + assert ventilation_setting.IsNatVentOn is False + assert ventilation_setting.IsScheduledVentilationOn is False def test_ventilationSetting_from_to_dict(self): """Make dict with `to_dict` and load again with `from_dict`.""" diff --git a/tests/test_umi.py b/tests/test_umi.py index d95a319f9..3403d9493 100644 --- a/tests/test_umi.py +++ b/tests/test_umi.py @@ -1,9 +1,9 @@ import collections import json import os +from typing import ClassVar import pytest -from path import Path from archetypal import IDF, settings from archetypal.eplus_interface import EnergyPlusVersion @@ -33,7 +33,7 @@ class TestUmiTemplate: """Test suite for the UmiTemplateLibrary class""" @pytest.fixture(scope="function") - def two_identical_libraries(self): + def two_identical_libraries(self, config): """Yield two identical libraries. Scope of this fixture is `function`.""" file = data_dir / "umi_samples/BostonTemplateLibrary_nodup.json" yield UmiTemplateLibrary.open(file), UmiTemplateLibrary.open(file) @@ -74,7 +74,7 @@ def test_unique_components(self, two_identical_libraries): # missing S. c.unique_components("OpaqueMaterial") - def test_graph(self): + def test_graph(self, config): """Test initialization of networkx DiGraph""" file = data_dir / "umi_samples/BostonTemplateLibrary_2.json" @@ -86,7 +86,7 @@ def test_graph(self): G = a.to_graph(include_orphans=True) assert len(G) > n_nodes - def test_template_to_template(self): + def test_template_to_template(self, config): """load the json into UmiTemplateLibrary object, then convert back to json and compare""" @@ -160,7 +160,7 @@ def read_json(file): return data_dict @pytest.fixture() - def idf(self): + def idf(self, config): yield IDF(prep_outputs=False) @pytest.fixture() @@ -623,30 +623,12 @@ def test_necb_parallel(self, config): assert no_duplicates(template.to_dict(), attribute="Name") assert no_duplicates(template.to_dict(), attribute="$id") - office = [ + office: ClassVar[list[str]] = [ data_dir / "necb/NECB 2011-SmallOffice-NECB HDD Method-CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw.idf", data_dir / "necb/NECB 2011-MediumOffice-NECB HDD Method-CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw.idf", data_dir / "necb/NECB 2011-LargeOffice-NECB HDD Method-CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw.idf", ] - @pytest.mark.skipif( - os.environ.get("CI", "False").lower() == "true", - reason="Skipping this test on CI environment", - ) - @pytest.mark.parametrize("file", Path(data_dir / "problematic").files("*CZ5A*.idf")) - def test_cz5a_serial(self, file, config): - settings.log_console = True - w = data_dir / "CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw" - template = UmiTemplateLibrary.from_idf_files( - name=file.stem, - idf_files=[file], - as_version="9-2-0", - weather=w, - processors=1, - ) - assert no_duplicates(template.to_dict(), attribute="Name") - assert no_duplicates(template.to_dict(), attribute="$id") - @pytest.fixture(scope="session") def climatestudio(config): diff --git a/tests/test_zonegraph.py b/tests/test_zonegraph.py index 407e8ffa8..59fcaad0b 100644 --- a/tests/test_zonegraph.py +++ b/tests/test_zonegraph.py @@ -29,7 +29,7 @@ def test_traverse_graph(self, small_office): assert G @pytest.fixture(scope="class") - def G(self, config, small_office): + def G(self, small_office): """ Args: config: @@ -40,7 +40,7 @@ def G(self, config, small_office): yield ZoneGraph.from_idf(idf) @pytest.mark.parametrize("adj_report", [True, False]) - def test_graph(self, config, small_office, adj_report): + def test_graph(self, small_office, adj_report): """Test the creation of a BuildingTemplate zone graph. Parametrize the creation of the adjacency report @@ -61,7 +61,7 @@ def test_graph(self, config, small_office, adj_report): EpBunch, ) - def test_graph_info(self, config, G): + def test_graph_info(self, G): """test the info method on a ZoneGraph Args: @@ -69,7 +69,7 @@ def test_graph_info(self, config, G): """ G.info() - def test_viewgraph2d(self, config, G): + def test_viewgraph2d(self, G): """test the visualization of the zonegraph in 2d Args: @@ -92,7 +92,7 @@ def test_viewgraph2d(self, config, G): ) @pytest.mark.parametrize("annotate", [True, "Name", ("core", None)]) - def test_viewgraph3d(self, config, G, annotate): + def test_viewgraph3d(self, G, annotate): """test the visualization of the zonegraph in 3d Args: @@ -106,7 +106,7 @@ def test_viewgraph3d(self, config, G, annotate): show=False, ) - def test_core_graph(self, config, G): + def test_core_graph(self, G): """ Args: G: @@ -116,7 +116,7 @@ def test_core_graph(self, config, G): assert len(H) == 1 # assert G has no nodes since Warehouse does not have a # core zone - def test_perim_graph(self, config, G): + def test_perim_graph(self, G): """ Args: G: diff --git a/tox.ini b/tox.ini index a03074fba..b47acb9fb 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ python = [testenv] passenv = PYTHON_VERSION -allowlist_externals = poetry +allowlist_externals = poetry, pytest setenv = ARCHETYPAL_DATA = {envtmpdir}/cache ARCHETYPAL_LOGS = {envtmpdir}/logs