From 77c997078ba58d997bb7a3bd38b72270dec0b40c Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 18 Apr 2024 17:11:58 -0700 Subject: [PATCH 01/27] feat(refactor): Remove old modules and add skeleton for new code --- honeybee_doe2/README.md | 0 honeybee_doe2/__init__.py | 2 +- honeybee_doe2/_base.py | 13 - honeybee_doe2/_extend_honeybee.py | 23 + honeybee_doe2/_extend_honeybee_doe2.py | 55 -- honeybee_doe2/cli/translate.py | 110 ++- honeybee_doe2/construction.py | 1 + honeybee_doe2/geometry/__init__.py | 0 honeybee_doe2/geometry/polygon.py | 84 --- honeybee_doe2/material.py | 1 + honeybee_doe2/properties/__init__.py | 0 .../properties/activitydescription.py | 239 ------ honeybee_doe2/properties/adiabaticfloor.py | 42 -- honeybee_doe2/properties/adiabaticroof.py | 53 -- honeybee_doe2/properties/aperture.py | 50 -- honeybee_doe2/properties/ceiling.py | 61 -- honeybee_doe2/properties/constructions.py | 147 ---- honeybee_doe2/properties/door.py | 50 -- honeybee_doe2/properties/exposedfloor.py | 41 -- honeybee_doe2/properties/face.py | 33 - honeybee_doe2/properties/groundcontact.py | 35 - honeybee_doe2/properties/hvac.py | 147 ---- honeybee_doe2/properties/inputils/__init__.py | 0 honeybee_doe2/properties/inputils/blocks.py | 149 ---- .../properties/inputils/compliance.py | 32 - .../properties/inputils/glass_types.py | 40 - .../properties/inputils/run_period.py | 41 -- honeybee_doe2/properties/inputils/sitebldg.py | 21 - honeybee_doe2/properties/inputils/title.py | 14 - honeybee_doe2/properties/interiorfloor.py | 41 -- honeybee_doe2/properties/materials.py | 144 ---- honeybee_doe2/properties/model.py | 223 ------ honeybee_doe2/properties/roof.py | 47 -- honeybee_doe2/properties/room.py | 296 -------- honeybee_doe2/properties/shades.py | 111 --- honeybee_doe2/properties/story.py | 106 --- honeybee_doe2/properties/switchstatements.py | 690 ------------------ honeybee_doe2/properties/wall.py | 87 --- honeybee_doe2/schedule.py | 1 + honeybee_doe2/utils/__init__.py | 1 - honeybee_doe2/utils/doe_formatters.py | 91 --- honeybee_doe2/utils/vector.py | 7 - honeybee_doe2/writer.py | 496 ++++++------- tests/ceiling_toggle_test.py | 25 - tests/cli_test.py | 25 - tests/exclude_true_test.py | 24 - tests/programtype_test.py | 18 - tests/rac_test.py | 20 - tests/switch_statement_test.py | 52 -- tests/writer_test.py | 63 +- 50 files changed, 333 insertions(+), 3719 deletions(-) delete mode 100644 honeybee_doe2/README.md delete mode 100644 honeybee_doe2/_base.py create mode 100644 honeybee_doe2/_extend_honeybee.py delete mode 100644 honeybee_doe2/_extend_honeybee_doe2.py create mode 100644 honeybee_doe2/construction.py delete mode 100644 honeybee_doe2/geometry/__init__.py delete mode 100644 honeybee_doe2/geometry/polygon.py create mode 100644 honeybee_doe2/material.py delete mode 100644 honeybee_doe2/properties/__init__.py delete mode 100644 honeybee_doe2/properties/activitydescription.py delete mode 100644 honeybee_doe2/properties/adiabaticfloor.py delete mode 100644 honeybee_doe2/properties/adiabaticroof.py delete mode 100644 honeybee_doe2/properties/aperture.py delete mode 100644 honeybee_doe2/properties/ceiling.py delete mode 100644 honeybee_doe2/properties/constructions.py delete mode 100644 honeybee_doe2/properties/door.py delete mode 100644 honeybee_doe2/properties/exposedfloor.py delete mode 100644 honeybee_doe2/properties/face.py delete mode 100644 honeybee_doe2/properties/groundcontact.py delete mode 100644 honeybee_doe2/properties/hvac.py delete mode 100644 honeybee_doe2/properties/inputils/__init__.py delete mode 100644 honeybee_doe2/properties/inputils/blocks.py delete mode 100644 honeybee_doe2/properties/inputils/compliance.py delete mode 100644 honeybee_doe2/properties/inputils/glass_types.py delete mode 100644 honeybee_doe2/properties/inputils/run_period.py delete mode 100644 honeybee_doe2/properties/inputils/sitebldg.py delete mode 100644 honeybee_doe2/properties/inputils/title.py delete mode 100644 honeybee_doe2/properties/interiorfloor.py delete mode 100644 honeybee_doe2/properties/materials.py delete mode 100644 honeybee_doe2/properties/model.py delete mode 100644 honeybee_doe2/properties/roof.py delete mode 100644 honeybee_doe2/properties/room.py delete mode 100644 honeybee_doe2/properties/shades.py delete mode 100644 honeybee_doe2/properties/story.py delete mode 100644 honeybee_doe2/properties/switchstatements.py delete mode 100644 honeybee_doe2/properties/wall.py create mode 100644 honeybee_doe2/schedule.py delete mode 100644 honeybee_doe2/utils/__init__.py delete mode 100644 honeybee_doe2/utils/doe_formatters.py delete mode 100644 honeybee_doe2/utils/vector.py delete mode 100644 tests/ceiling_toggle_test.py delete mode 100644 tests/cli_test.py delete mode 100644 tests/exclude_true_test.py delete mode 100644 tests/programtype_test.py delete mode 100644 tests/rac_test.py delete mode 100644 tests/switch_statement_test.py diff --git a/honeybee_doe2/README.md b/honeybee_doe2/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/honeybee_doe2/__init__.py b/honeybee_doe2/__init__.py index 1793277..47fc11d 100644 --- a/honeybee_doe2/__init__.py +++ b/honeybee_doe2/__init__.py @@ -1 +1 @@ -import honeybee_doe2._extend_honeybee_doe2 +import honeybee_doe2._extend_honeybee diff --git a/honeybee_doe2/_base.py b/honeybee_doe2/_base.py deleted file mode 100644 index 2723135..0000000 --- a/honeybee_doe2/_base.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -# -*- Python Version: 2.7 -*- -"""Base class for Honeybee-Doe2 Objects""" -import uuid - - -class _Base(object): - """doe2 base class""" - - def __init__(self): - self._identifier = uuid.uuid4() - self.user_data = {} - self._display_name = self._identifier diff --git a/honeybee_doe2/_extend_honeybee.py b/honeybee_doe2/_extend_honeybee.py new file mode 100644 index 0000000..017d1d5 --- /dev/null +++ b/honeybee_doe2/_extend_honeybee.py @@ -0,0 +1,23 @@ +# coding=utf-8 + +# import all of the modules for writing geometry to INP +import honeybee.writer.shademesh as shade_mesh_writer +import honeybee.writer.door as door_writer +import honeybee.writer.aperture as aperture_writer +import honeybee.writer.shade as shade_writer +import honeybee.writer.face as face_writer +import honeybee.writer.room as room_writer +import honeybee.writer.model as model_writer +from .writer import model_to_inp, room_to_inp, face_to_inp, shade_to_inp, \ + aperture_to_inp, door_to_inp, shade_mesh_to_inp + +# add writers to the honeybee-core modules +model_writer.inp = model_to_inp +room_writer.inp = room_to_inp +face_writer.inp = face_to_inp +shade_writer.inp = shade_to_inp +aperture_writer.inp = aperture_to_inp +door_writer.inp = door_to_inp +shade_mesh_writer.inp = shade_mesh_to_inp + +# TODO: Extend classes of honeybee-energy with to_inp() methods and inp_identifier() diff --git a/honeybee_doe2/_extend_honeybee_doe2.py b/honeybee_doe2/_extend_honeybee_doe2.py deleted file mode 100644 index 2d66342..0000000 --- a/honeybee_doe2/_extend_honeybee_doe2.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# -*- Python Version: 2.7 -*- -"""This is called during __init__and extends the base honeybee class Properties with a new ._doe2 slot""" - -from honeybee.properties import ( - ModelProperties, - RoomProperties, - FaceProperties, - # ApertureProperties, - # ShadeProperties #* bldg | context disangination important for 90.1 baseline hokeypokey -) - -from .properties.model import ModelDoe2Properties -from .properties.room import RoomDoe2Properties -from .properties.face import FaceDoe2Properties -# from .properties.aperture import ApertureDoe2Properties -# Step 1) -# set a private ._doe2 attribute on each relevant HB-Core Property class to None -setattr(ModelProperties, '_doe2', None) -setattr(RoomProperties, '_doe2', None) -setattr(FaceProperties, '_doe2', None) -#setattr(ApertureProperties, '_doe2', None) -#setattr(ShadeProperties, '_doe2', None) - - -def model_doe2_properties(self): - if self._doe2 is None: - self._doe2 = ModelDoe2Properties(self.host) - return self._doe2 - - -def room_doe2_properties(self): - if self._doe2 is None: - self._doe2 = RoomDoe2Properties(self.host) - return self._doe2 - - -def face_doe2_properties(self): - if self._doe2 is None: - self._doe2 = FaceDoe2Properties(self.host) - return self._doe2 - - -# def aperture_doe2_properties(self): -# if self._doe2 is None: -# self._doe2 = ApertureDoe2Properties(self.host) -# return self._doe2 - - -# Step 3) -# add public .doe2 property methods to the Properties classes -setattr(ModelProperties, 'doe2', property(model_doe2_properties)) -setattr(RoomProperties, 'doe2', property(room_doe2_properties)) -setattr(FaceProperties, 'doe2', property(face_doe2_properties)) -#setattr(ApertureProperties, 'doe2', property(aperture_doe2_properties)) diff --git a/honeybee_doe2/cli/translate.py b/honeybee_doe2/cli/translate.py index 8566be3..7a5b4ef 100644 --- a/honeybee_doe2/cli/translate.py +++ b/honeybee_doe2/cli/translate.py @@ -1,61 +1,93 @@ -"""honeybee-doe2 Translation Commands""" +"""honeybee-doe2 translation commands.""" import click import sys -import pathlib +import os +import json import logging -from ..writer import honeybee_model_to_inp +from ladybug.futil import write_to_file_by_name from honeybee.model import Model _logger = logging.getLogger(__name__) -@click.group(help='Commands for translating Honeybee_Model.hbjson to DOE2.2 *.inp files.') +@click.group(help='Commands for translating Honeybee Model to DOE-2 formats.') def translate(): pass -@translate.command('hbjson-to-inp') -@click.argument('hb-json', type=click.Path( - exists=True, file_okay=True, dir_okay=False, resolve_path=True) -) +@translate.command('model-to-inp') +@click.argument('model-file', type=click.Path( + exists=True, file_okay=True, dir_okay=False, resolve_path=True)) @click.option( - '--hvac-mapping', '-hm', help='HVAC mapping method. Options are room, story, model ' - 'or assigned-hvac.', - default='story', show_default=True, type=click.Choice( - ['room', 'story', 'model', 'assigned-hvac'], case_sensitive=False)) - + '--sim-par-json', '-sp', help='Full path to a honeybee-doe2 SimulationParameter ' + 'JSON that describes all of the settings for the simulation. If unspecified, ' + 'default parameters will be generated.', default=None, show_default=True, + type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)) @click.option( - '--exclude-interior-walls', '-eiw', help='Use this flag to exclude interior walls from export', - default=False, show_default=True, is_flag=True -) - + '--hvac-mapping', '-hm', help='Text to indicate how HVAC systems should be ' + 'assigned to the exported model. Story will assign one HVAC system for each ' + 'distinct level polygon, Model will use only one HVAC system for the whole model ' + 'and AssignedHVAC will follow how the HVAC systems have been assigned to the' + 'Rooms.properties.energy.hvac. Choose from: Room, Story, Model, AssignedHVAC', + default='Story', show_default=True, type=str) @click.option( - '--exclude-interior-ceilings', '-eic', help='Use this flag to exclude interior ceilings from export', - default=False, show_default=True, is_flag=True -) - + '--include-interior-walls/--exclude-interior-walls', ' /-xw', help='Flag to note ' + 'whether interior walls should be excluded from the export.', + default=True, show_default=True) @click.option( - '--switch-statements', '-swi', help='Use this flag to export *.inp file with switch statements', - default=False, show_default=True, is_flag=True -) - -@click.option('--name', '-n', help='Name of the output file.', default='model', - show_default=True - ) + '--include-interior-ceilings/--exclude-interior-ceilings', ' /-xc', help='Flag to ' + 'note whether interior ceilings should be excluded from the export.', + default=True, show_default=True) +@click.option( + '--switch-statements/--verbose-properties', ' /-v', help='Flag to note whether ' + 'program types should be written with switch statements so that they can easily ' + 'be edited in eQuest or a verbose definition of loads should be written for ' + 'each Room/Space.', default=True, show_default=True) +@click.option( + '--name', '-n', help='Deprecated option to set the name of the output file.', + default=None, show_default=True) +@click.option( + '--folder', '-f', help='Deprecated option to set the path to target folder.', + type=click.Path(file_okay=False, resolve_path=True, dir_okay=True), default=None) +@click.option( + '--output-file', '-o', help='Optional INP file path to output the INP string ' + 'of the translation. By default this will be printed out to stdout.', + type=click.File('w'), default='-', show_default=True) +def model_to_inp( + model_file, sim_par_json, hvac_mapping, include_interior_walls, + include_interior_ceilings, name, folder, output_file +): + """Translate a Model (HBJSON) file to an INP file. -@click.option('--folder', '-f', help='Path to target folder.', type=click.Path( - exists=False, file_okay=False, resolve_path=True, dir_okay=True), - default='.', show_default=True -) -def hb_model_to_inp_file(hb_json, hvac_mapping, exclude_interior_walls, exclude_interior_ceilings, switch_statements, name, folder): - """Translate a HBJSON into a DOE2.2 *.inp file.""" + \b + Args: + model_file: Full path to a Honeybee Model file (HBJSON or HBpkl).""" try: - hvac_mapping - hb_model = Model.from_file(hb_json) - folder = pathlib.Path(folder) - folder.mkdir(parents=True, exist_ok=True) - honeybee_model_to_inp(hb_model, hvac_mapping, exclude_interior_walls, exclude_interior_ceilings,switch_statements, folder, name) + # load simulation parameters or generate default ones + #if sim_par_json is not None: + # with open(sim_par_json) as json_file: + # data = json.load(json_file) + # sim_par = SimulationParameter.from_dict(data) + #else: + # sim_par = SimulationParameter() + # sim_par_str = sim_par.to_idf() + sim_par_str = '' + + # re-serialize the Model to Python + model = Model.from_file(model_file) + x_int_w = not include_interior_walls + x_int_c = not include_interior_ceilings + + # create the strings for simulation parameters and model + model_str = model.to.inp(model, hvac_mapping, x_int_w, x_int_c) + inp_str = '\n\n'.join([sim_par_str, model_str]) + + # write out the INP file + if folder is not None and name is not None: + write_to_file_by_name(folder, name, inp_str, True) + else: + output_file.write(inp_str) except Exception as e: _logger.exception(f'Model translation failed:\n{e}') sys.exit(1) diff --git a/honeybee_doe2/construction.py b/honeybee_doe2/construction.py new file mode 100644 index 0000000..39fba67 --- /dev/null +++ b/honeybee_doe2/construction.py @@ -0,0 +1 @@ +"""honeybee-inp construction translators.""" diff --git a/honeybee_doe2/geometry/__init__.py b/honeybee_doe2/geometry/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/honeybee_doe2/geometry/polygon.py b/honeybee_doe2/geometry/polygon.py deleted file mode 100644 index 7c10287..0000000 --- a/honeybee_doe2/geometry/polygon.py +++ /dev/null @@ -1,84 +0,0 @@ -import math -from typing import List - -from honeybee.face import Face -from ladybug_geometry.geometry3d import Vector3D, Plane, Face3D -from ladybug_geometry.geometry2d import Point2D, Polygon2D - -from ..utils.doe_formatters import short_name - - -class DoePolygon(object): - "A Doe2 Polygon." - - def __init__(self, name, vertices, flip=False): - self.name = name - self.vertices = self._remove_duplicate_vertices(vertices, 0.003) - self.flip = flip - - @staticmethod - def _remove_duplicate_vertices(vertices: List[Point2D], tol=0.003): - """Remove identical vertices.""" - pl = Polygon2D(vertices) - pl = pl.remove_colinear_vertices(tol) - return pl.vertices - - @classmethod - def from_face(cls, face: Face, flip=False): - """ - Create a DoePolygon from a Honeybee Face. - - Args: - face: A Face object. - - """ - name = short_name(face.display_name) - - geometry: Face3D = face.geometry - - rel_plane = geometry.plane - # horizontal Face3D; use world XY - angle_tolerance = 0.01 - if rel_plane.n.angle(Vector3D(0, 0, 1)) <= angle_tolerance or \ - rel_plane.n.angle(Vector3D(0, 0, -1)) <= angle_tolerance: - llc_3d = geometry.lower_left_corner - llc = Point2D(llc_3d.x, llc_3d.y) - vertices = [ - Point2D(v[0] - llc.x, v[1] - llc.y) for v in - geometry.lower_left_counter_clockwise_vertices - ] - - if not flip and \ - rel_plane.n.angle(Vector3D(0, 0, -1)) <= angle_tolerance: - # change the order of the vertices. DOE2 expects the vertices to be - # CCW from the top view - vertices = [vertices[0]] + list(reversed(vertices[1:])) - - else: # vertical or tilted Face3D; orient the Y to the world Z - proj_y = Vector3D(0, 0, 1).project(rel_plane.n) - proj_x = proj_y.rotate(rel_plane.n, math.pi / -2) - ref_plane = Plane(rel_plane.n, geometry.lower_left_corner, proj_x) - vertices = [ - Point2D(*ref_plane.xyz_to_xy(pt)) - for pt in geometry.lower_left_counter_clockwise_vertices] - - return cls(name=name, vertices=vertices, flip=flip) - - def to_inp(self, name=None): - """Returns Polygons block input.""" - vertices_template = ' V%d\t\t= ( %f, %f )'.replace('\t', ' ') - vertices = self.vertices - if self.flip: - # underground surface should be flipped - vertices = [Point2D(v.x, -v.y) for v in vertices] - vertices = '\n'.join([ - vertices_template % (i + 1, ver.x, ver.y) - for i, ver in enumerate(vertices) - ]) - name = name or f'{self.name} Plg' - return f'"{name}" = POLYGON\n' \ - f'{vertices}\n' + \ - ' ..' - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/material.py b/honeybee_doe2/material.py new file mode 100644 index 0000000..8ef372e --- /dev/null +++ b/honeybee_doe2/material.py @@ -0,0 +1 @@ +"""honeybee-doe2 material translators.""" diff --git a/honeybee_doe2/properties/__init__.py b/honeybee_doe2/properties/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/honeybee_doe2/properties/activitydescription.py b/honeybee_doe2/properties/activitydescription.py deleted file mode 100644 index 2917c4f..0000000 --- a/honeybee_doe2/properties/activitydescription.py +++ /dev/null @@ -1,239 +0,0 @@ -from dataclasses import dataclass -from enum import Enum -import textwrap -from honeybee_energy.schedule.ruleset import ScheduleRuleset -from typing import List -from typing import Set - -from ..utils.doe_formatters import short_name - - -class DayScheduleType(Enum): - """Enum to classify the type of a DaySchedule.""" - - ONOFF = 'ON/OFF' - """ accepts the binary values 0 and 1, where 0 means the schedule is OFF and 1 - means the schedule is ON. Examples include schedules for fans and - heating/cooling availability. - """ - FRACTION = 'FRACTION' - """ accepts values between and including 0.0 and 1.0. Examples include lighting, - people, etc - """ - MULTIPLIER = 'MULTIPLIER' - """accepts values 0.0 and above. Examples, include lighting, people, etc""" - TEMPERATURE = 'TEMPERATURE' - """ Accepts a value that represents a temperature. Examples include heating and - cooling thermostat schedules. - """ - RADIATION = 'RADIATION' - """ accepts a value that represents a radiative flux, expressed in Btu/ft2-hr or W/m2. - An example is the WINDOW:MAX-SOLAR-SCH. - """ - ONOFFTEMP = 'ON/OFF/TEMP' - """ accepts the binary values 0 and 1, similar to ON/OFF. Any other value is also - acceptable, and is assumed to represent a flag temperature. When a temperature, - the meaning of the value and its action varies by the component referencing the - schedule. For example, in the SYSTEM:HEATING-SCHEDULE, the binary values 0 and - 1 disable and enable heating respectively. Any value other than 0 or 1 represents - the outdoor drybulb temperature below which heating is enabled. - """ - - ONOFFFLAG = 'ON/OFF/FLAG' - """ accepts the binary values 0 and 1, similar to ON/OFF. Any other value is also - acceptable, and is assumed to represent a flag value. When a flag, the meaning of - the value and its action varies by the keyword. - For example, in the SYSTEM:NATURAL-VENT-SCH, a value of 0 forces the - windows closed, and a value of 1 allows windows to be open if the outdoor - temperature is suitable, and a flag value of –1 allows windows to be open if the - outdoor enthalpy is also suitable. - """ - FRACDESIGN = 'FRAC/DESIGN' - """ accepts a value which is the fraction of the design quantity. Typical values range - between 0.0 and 1.0. An entry of –999 causes the schedule to be ignored for that - hour. For example, in the SYSTEM:MIN-AIR-SCH, an entry of 0 or 1 will force the - outdoor-air ratio to be 0% or 100% respectively. A value of –999 will cause the - schedule to be ignored for the hour, and the outdoor-air ratio will be set by other - calculations. - """ - EXPFRACTION = 'EXP-FRACTION' - """ accepts a value between –1 and 1. An example is the WINDOW:SLAT-SCHEDULE.""" - - FLAG = 'FLAG' - """ accepts a flag of any value. The flag value must exactly match a comparison - criterion for the component to be active. For example, in the ELEC-METER:COGEN-TRACK-SCH, a flag value of 1 - means that cogeneration equipment should track the electric load, a value of 2 - means track the thermal load, 3 means track the lesser of the electric or thermal - load, etc. - """ - RESETTEMP = 'RESET-TEMP' - """ specifies that, rather than a 24-hour profile, that the DAY-SCHEDULE - defines a relationship between the outside air temperature and a temperature - setpoint, such as supply air temperature. Refer to the section below on “Reset - Schedules” for more information. Note: TYPE = RESET-TEMP and RESET-RATIO work only in DAY- - SCHEDULE-PD; not DAY-SCHEDULE. They replace the original DAY-RESET-SCH command that was used - prior to the development of user interfaces. - """ - RESETRATIO = 'RESET-RATIO' - """ specifies that, rather than a 24-hour profile, that the DAY-SCHEDULE - defines a relationship between the outside air temperature and a system control - parameter, such as baseboard heating power. Refer to the section below on “Reset - Schedules” for more information. - Note: TYPE = RESET-TEMP and RESET-RATIO work only in DAY- - SCHEDULE-PD; not DAY-SCHEDULE. They replace the original DAY- - RESET-SCH command that was used prior to the development of user - interfaces. - """ - - -@dataclass -class DayScheduleDoe: - name: str = None - values: [float] = None - stype: DayScheduleType = None - - @classmethod - def from_day_schedule(cls, day_schedule, stype): - """Create a DaySchedule from a DaySchedule. - """ - # TODO: format the output to look better, follow indent rules etc. - - mywrap = textwrap.TextWrapper(width=20) - - vals = list(day_schedule.data_collection().values) - vals = ", ".join([str(val) for val in vals]) - vals = mywrap.fill(vals) - - return cls(name=short_name(day_schedule.display_name), - values=vals, - stype=stype) - - def to_inp(self): - obj_lines = [] - obj_lines.append(f'"{self.name}" = DAY-SCHEDULE') - obj_lines.append(f'\n TYPE = {self.stype.value}') - obj_lines.append(f'\n (1,24) ({self.values})') - obj_lines.append(f'\n ..') - - return ''.join([line for line in obj_lines]) - - def __repr__(self): - return self.to_inp() - - -class Days(Enum): - MON = "MON" - """Monday""" - TUE = "TUE" - """Tuesday""" - WED = "WED" - """Wednesday""" - THUR = "THUR" - """Thursday""" - FRI = "FRI" - """Friday""" - SAT = "SAT" - """Saterday""" - SUN = "SUN" - """Sunday""" - HOL = "HOL" - """Holidays""" - WD = "WD" - """Weekdays (Mon-Fri)""" - WEH = "WEH" - """Weekends and holidays (Sat, Sun, Hol)""" - HDD = "HDD" - """Heating Design Day""" - CDD = "CDD" - """Cooling Design Day""" - ALL = "ALL" - """All 10 days in the week schedule (Mon-Sun, Hol, HDD, CDD)""" - - -@dataclass -class WeekScheduleDoe: - name: str = None - stype: str = DayScheduleType - day_schedules: List = None - - @classmethod - def from_schedule_ruleset(cls, stype, schedule_ruleset): - """Create a WeekScheduleDoe from a ScheduleRuleset.""" - myruleset = schedule_ruleset - name = short_name(schedule_ruleset.display_name) - stype = stype - - days_of_the_week = ['monday', 'tuesday', - 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] - - days = [] - for rule in myruleset: - for day in days_of_the_week: - if day in rule.days_applied: - days.append(short_name(rule.schedule_day.display_name)) if rule.schedule_day.display_name \ - is not None else short_name(myruleset.default_day_schedule.display_name) - else: - days.append(short_name(myruleset.default_day_schedule.display_name)) - - if len(myruleset.schedule_rules) == 0: - if len(myruleset.day_schedules) == 3: - for day in days_of_the_week: - days.append(short_name(myruleset.default_day_schedule.display_name)) - - if len(myruleset.day_schedules) == 1: - for day in days_of_the_week: - days.append(short_name(myruleset.default_day_schedule.display_name)) - - if myruleset.holiday_schedule is not None: - days.append(short_name(myruleset.holiday_schedule.display_name)) - else: - days.append(short_name(myruleset.default_day_schedule.display_name)) - - if myruleset.winter_designday_schedule is not None: - days.append(short_name(myruleset.winter_designday_schedule.display_name)) - else: - days.append(short_name(myruleset.default_day_schedule.display_name)) - - if myruleset.summer_designday_schedule is not None: - days.append(short_name(myruleset.summer_designday_schedule.display_name)) - else: - days.append(short_name(myruleset.default_day_schedule.display_name)) - - return cls(name=name, stype=stype, day_schedules=days) - - def to_inp(self): - # TODO: fix the cause of the issue - if len(self.day_schedules) < 8: - print('Invalid day schedule with less than 8 values.') - return '' - - obj_lines = [] - obj_lines.append(f'"{self.name}" = WEEK-SCHEDULE-PD') - obj_lines.append(f'\n TYPE = {self.stype.value}') - obj_lines.append(f'\n DAY-SCHEDULES = ( "{self.day_schedules[0]}", $ Monday') - obj_lines.append(f'\n "{self.day_schedules[1]}", $ Tuesday') - obj_lines.append(f'\n "{self.day_schedules[2]}", $ Wednesday') - obj_lines.append(f'\n "{self.day_schedules[3]}", $ Thursday') - obj_lines.append(f'\n "{self.day_schedules[4]}", $ Friday') - obj_lines.append(f'\n "{self.day_schedules[5]}", $ Saturday') - obj_lines.append(f'\n "{self.day_schedules[6]}", $ Sunday') - obj_lines.append(f'\n "{self.day_schedules[7]}", $ Holiday') - obj_lines.append( - f'\n "{self.day_schedules[8]}", $ Winter Design Day') - obj_lines.append( - f'\n "{self.day_schedules[9]}", $ Summer Design Day') - obj_lines.append(f'\n )') - - obj_lines.append(f'\n ..\n') - - obj_lines.append(f'"{self.name}_" = SCHEDULE-PD') - obj_lines.append(f'\n TYPE = {self.stype.value}') - obj_lines.append(f'\n MONTH = 12') - obj_lines.append(f'\n DAY = 31') - obj_lines.append(f'\n WEEK-SCHEDULES = "{self.name}"') - obj_lines.append(f'\n ..\n') - - return ''.join([line for line in obj_lines]) - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/adiabaticfloor.py b/honeybee_doe2/properties/adiabaticfloor.py deleted file mode 100644 index 115b932..0000000 --- a/honeybee_doe2/properties/adiabaticfloor.py +++ /dev/null @@ -1,42 +0,0 @@ -from ..utils.doe_formatters import short_name -from ..geometry.polygon import DoePolygon - - -class AdiabaticFloor: - def __init__(self, face): - self.face = face - # only flip horizontal floors - flip = True if abs(self.face.altitude + 90) <= 0.01 \ - else False - self.polygon = DoePolygon.from_face(face, flip=flip) - - def to_inp(self, space_origin): - p_name = short_name(self.face.display_name) - - constr = self.face.properties.energy.construction.display_name - tilt = 90 - self.face.altitude - azimuth = 180 if abs(180 - tilt) <= 0.01 else self.face.azimuth - origin_pt = self.face.geometry.lower_left_corner - space_origin - - # create a unique polygon for exposed floor faces - polygon_name = f'{self.face.display_name}_ad Plg' - polygon = self.polygon.to_inp(name=polygon_name) + '\n' - obj_lines = [polygon] - - obj_lines.append('"{}" = INTERIOR-WALL'.format(p_name)) - obj_lines.append('\n POLYGON = "{}"'.format(polygon_name)) - obj_lines.append('\n CONSTRUCTION = "{}_c"'.format(short_name(constr, 30))) - obj_lines.append( - '\n NEXT-TO = "{}"'.format(self.face.parent.display_name)) - obj_lines.append('\n INT-WALL-TYPE = ADIABATIC') - obj_lines.append('\n TILT = {}'.format(tilt)) - obj_lines.append('\n AZIMUTH = {}'.format(azimuth)) - obj_lines.append('\n X = {}'.format(origin_pt.x)) - obj_lines.append('\n Y = {}'.format(origin_pt.y)) - obj_lines.append('\n Z = {}'.format(origin_pt.z)) - obj_lines.append('\n ..\n') - - return ''.join([line for line in obj_lines]) - - def __repr__(self): - return f'DOE2 adiabatic floor: {self.face.display_name}' diff --git a/honeybee_doe2/properties/adiabaticroof.py b/honeybee_doe2/properties/adiabaticroof.py deleted file mode 100644 index bec3d60..0000000 --- a/honeybee_doe2/properties/adiabaticroof.py +++ /dev/null @@ -1,53 +0,0 @@ -from .aperture import Window -from ..utils.doe_formatters import short_name -from ..geometry.polygon import DoePolygon - - -class AdiabaticRoof: - def __init__(self, face): - self.face = face - - def to_inp(self, space_origin): - - polygon = DoePolygon.from_face(self.face, flip=False) - - p_name = f'{short_name(self.face.display_name)}_ad' - - constr = self.face.properties.energy.construction.display_name - tilt = 90 - self.face.altitude - azimuth = 180 if abs(tilt) <= 0.01 else self.face.azimuth - origin_pt = self.face.geometry.lower_left_corner - space_origin - - spc = '' - - obj_lines = [polygon.to_inp(p_name+' Plg')+'\n'] - - obj_lines.append('"{}" = INTERIOR-WALL'.format(p_name)) - obj_lines.append('\n POLYGON = "{}"'.format(p_name+' Plg')) - obj_lines.append('\n CONSTRUCTION = "{}_c"'.format(short_name(constr, 30))) - obj_lines.append( - '\n NEXT-TO = "{}"'.format(self.face.parent.display_name)) - obj_lines.append('\n INT-WALL-TYPE = ADIABATIC') - obj_lines.append('\n TILT = {}'.format(tilt)) - obj_lines.append('\n AZIMUTH = {}'.format(azimuth)) - obj_lines.append('\n X = {}'.format(origin_pt.x)) - obj_lines.append('\n Y = {}'.format(origin_pt.y)) - obj_lines.append('\n Z = {}'.format(origin_pt.z)) - obj_lines.append('\n ..\n') - temp_str = spc.join([line for line in obj_lines]) - - doe_windows = [ - Window(ap, self.face) for ap in self.face.apertures - ] - - nl = '\n' - if doe_windows is not None: - for window in doe_windows: - temp_str += window.to_inp() + nl - return temp_str - else: - return temp_str - - -def __repr__(self): - return f'DOE2 roof: {self.face.display_name}' diff --git a/honeybee_doe2/properties/aperture.py b/honeybee_doe2/properties/aperture.py deleted file mode 100644 index 10d1e77..0000000 --- a/honeybee_doe2/properties/aperture.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- - -import math - -from ladybug_geometry.geometry3d.face import Vector3D, Plane -from ..utils.doe_formatters import short_name - - -class Window: - def __init__(self, aperture, parent): - self.aperture = aperture - self.parent = parent - - def to_inp(self): - """Return an inp window string for an aperture.""" - - glass_type = short_name( - self.aperture.properties.energy.construction.display_name, 32) - - parent_llc = self.parent.geometry.lower_left_corner - rel_plane = self.parent.geometry.plane - apt_llc = self.aperture.geometry.lower_left_corner - apt_urc = self.aperture.geometry.upper_right_corner - - # horizontal faces - # horizontal Face3D; use world XY - angle_tolerance = 0.01 - if rel_plane.n.angle(Vector3D(0, 0, 1)) <= angle_tolerance or \ - rel_plane.n.angle(Vector3D(0, 0, -1)) <= angle_tolerance: - proj_x = Vector3D(1, 0, 0) - else: - proj_y = Vector3D(0, 0, 1).project(rel_plane.n) - proj_x = proj_y.rotate(rel_plane.n, math.pi / -2) - - ref_plane = Plane(rel_plane.n, parent_llc, proj_x) - min_2d = ref_plane.xyz_to_xy(apt_llc) - max_2d = ref_plane.xyz_to_xy(apt_urc) - height = max_2d.y - min_2d.y - width = max_2d.x - min_2d.x - - return \ - '"{}" = WINDOW\n'.format(short_name(self.aperture.display_name)) + \ - "\n X = {}".format(min_2d.x) + \ - "\n Y = {}".format(min_2d.y) + \ - "\n WIDTH = {}".format(width, 3) + \ - "\n HEIGHT = {}".format(height, 3) + \ - '\n GLASS-TYPE = "{}"'.format(glass_type) + "\n ..\n" - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/ceiling.py b/honeybee_doe2/properties/ceiling.py deleted file mode 100644 index 9918421..0000000 --- a/honeybee_doe2/properties/ceiling.py +++ /dev/null @@ -1,61 +0,0 @@ -from ..utils.doe_formatters import short_name -from .aperture import Window -from .door import Door -from honeybee.facetype import Wall, Floor, RoofCeiling -from .wall import WallBoundaryCondition - - - -class DoeCeilign: - def __init__(self, face): - self.face = face - - def to_inp(self, space_origin): - - p_name = short_name(self.face.display_name) - wall_typology = WallBoundaryCondition(self.face.boundary_condition).wall_typology - constr = self.face.properties.energy.construction.display_name - tilt = 90 - self.face.altitude - azimuth = 180 if abs(tilt) <= 0.01 else self.face.azimuth - origin_pt = self.face.geometry.lower_left_corner - space_origin - - spc = '' - obj_lines = [] - - obj_lines.append('"{}" = {}'.format(p_name, wall_typology)) - obj_lines.append('\n POLYGON = "{}"'.format(f'{p_name} Plg')) - obj_lines.append('\n CONSTRUCTION = "{}_c"'.format(short_name(constr, 30))) - obj_lines.append('\n TILT = {}'.format(tilt)) - obj_lines.append('\n AZIMUTH = {}'.format(azimuth)) - obj_lines.append('\n X = {}'.format(origin_pt.x)) - obj_lines.append('\n Y = {}'.format(origin_pt.y)) - obj_lines.append('\n Z = {}'.format(origin_pt.z)) - if wall_typology == 'INTERIOR-WALL' and str( - self.face.boundary_condition) == 'Surface': - if self.face.user_data: - next_to = self.face.user_data['adjacent_room'] - obj_lines.append('\n NEXT-TO = "{}"'.format(next_to)) - else: - print( - f'{self.face.display_name} is an interior face but is missing ' - 'adjacent room info in user data.' - ) - if wall_typology == 'INTERIOR-WALL' and str( - self.face.boundary_condition) == 'Adiabatic': - obj_lines.append('\n INT-WALL-TYPE = ADIABATIC') - next_to = self.face.parent.display_name - obj_lines.append('\n NEXT-TO = "{}"'.format(next_to)) - obj_lines.append('\n ..\n') - - temp_str = spc.join([line for line in obj_lines]) - - doe_windows = [ - Window(ap, self.face).to_inp() for ap in self.face.apertures - ] - - temp_str += '\n'.join(doe_windows) - - return temp_str - - def __repr__(self): - return f'DOE2 Ceiling: {self.face.display_name}' \ No newline at end of file diff --git a/honeybee_doe2/properties/constructions.py b/honeybee_doe2/properties/constructions.py deleted file mode 100644 index ec47904..0000000 --- a/honeybee_doe2/properties/constructions.py +++ /dev/null @@ -1,147 +0,0 @@ -from enum import unique - -from honeybee_energy.construction.opaque import OpaqueConstruction as OpConstr -from .materials import Material, NoMassMaterial, MassMaterial -from .inputils.blocks import mats_layers -from ..utils.doe_formatters import short_name, unit_convertor -from honeybee_energy.material.opaque import EnergyMaterial, EnergyMaterialNoMass -from dataclasses import dataclass -from typing import List - -@dataclass -class UvalueConstruction: - name: str - u_value: float - - @classmethod - def from_hb_construction(cls, construction:OpConstr): - name = short_name(construction.display_name, 30) - u_value = construction.u_value - return cls(name=name, u_value=u_value) - - def to_inp(self, include_materials=False): - objlines = [] - objlines.append(f'"{self.name}_c" = CONSTRUCTION\n') - objlines.append('TYPE = U-VALUE\n') - objlines.append(f'U-VALUE = {self.u_value}\n..\n') - result = ''.join([l for l in objlines]) - return result - - def __repr__ (self): - return self.to_inp() - -@dataclass -class Construction: - name: str - materials: List[Material] - absorptance: float - roughness: int - - @classmethod - def from_hb_construction(cls, construction: OpConstr): - """Create inp construction from HB construction.""" - roughdict = {'VeryRough': 1, 'Rough': 2, 'MediumRough': 3, - 'MediumSmooth': 4, 'Smooth': 5, 'VerySmooth': 6} - if not isinstance(construction, OpConstr): - # this should raise an error but for now I leave it to print until we - # support a handful number of types - print( - f'Unsupported Construction type: {type(construction)}.\n' - 'Please be patient as more features and capabilities are implemented.' - ) - return cls(construction.display_name, [], 0, 0) - - materials = [] - for material in construction._materials: - if material.thickness <= 0.003: - materials.append(NoMassMaterial.from_hb_material(material)) - elif material.thickness > 0.003: - materials.append(Material.from_hb_material(material)) - - - absorptance = construction.materials[0].solar_absorptance - roughness = roughdict[construction.materials[0].roughness] - - cons_name = short_name(construction.display_name, 30) - - return cls(cons_name, materials, absorptance, roughness) - - def to_inp(self, include_materials=True): - - # temporary solution not return values for unsupported construction types - if not self.materials: - return '' - - if include_materials: - block = ['\n'.join(material.to_inp() for material in self.materials)] - else: - block = [] - - - materials = '\n '.join(f'"{material.name}",' - for material in self.materials) - - layers_name = f'"{self.name}_l"' - construction = f'{layers_name} = LAYERS\n' \ - f' MATERIAL = (\n {materials[:-1]}\n )\n' \ - ' ..\n\n' \ - f'"{self.name}_c" = CONSTRUCTION\n' \ - ' TYPE = LAYERS\n' \ - f' ABSORPTANCE = {self.absorptance}\n' \ - f' ROUGHNESS = {self.roughness}\n' \ - f' LAYERS = {layers_name}\n' \ - ' ..\n' - block.append(construction) - - return '\n\n'.join(block) - - def __repr__(self) -> str: - return self.to_inp() - - -@dataclass -class ConstructionCollection: - """Construction object. Contains, materials and layers for *.inp file. - - Returns: - $ Materials / Layers / Constructions *.inp block - """ - constructions: List[Construction] - - @classmethod - def from_hb_constructions(cls, constructions: List[OpConstr]): - unique_constructions = { - construction.display_name: construction for construction in constructions - }.values() - - constructions = [] - for construction in unique_constructions: - if construction.thickness > 0.003: - constructions.append(Construction.from_hb_construction(construction)) - elif construction.thickness < 0.003: - constructions.append(UvalueConstruction.from_hb_construction(construction)) - - return cls(constructions) - - def to_inp(self): - - block = [] - - # collect all the materials and ensure to only include the unique ones - materials = set() - - for construction in self.constructions: - if isinstance(construction, Construction): - for mat in construction.materials: - materials.add(mat.to_inp()) - - block.append(''.join(materials)) - - # add constructions - layers are created as part of each construction definition - for construction in self.constructions: - block.append(construction.to_inp(include_materials=False)) - - return '\n'.join(block) - - def __repr__(self): - return self.to_inp() \ No newline at end of file diff --git a/honeybee_doe2/properties/door.py b/honeybee_doe2/properties/door.py deleted file mode 100644 index ab88b18..0000000 --- a/honeybee_doe2/properties/door.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- - -import math - -from ladybug_geometry.geometry3d.face import Vector3D, Plane -from ..utils.doe_formatters import short_name - - -class Door: - def __init__(self, door, parent): - self.door = door - self.parent = parent - - def to_inp(self): - """Return an inp door string for an door.""" - - construction = short_name( - self.door.properties.energy.construction.display_name, 32) - - parent_llc = self.parent.geometry.lower_left_corner - rel_plane = self.parent.geometry.plane - apt_llc = self.door.geometry.lower_left_corner - apt_urc = self.door.geometry.upper_right_corner - - # horizontal faces - # horizontal Face3D; use world XY - angle_tolerance = 0.01 - if rel_plane.n.angle(Vector3D(0, 0, 1)) <= angle_tolerance or \ - rel_plane.n.angle(Vector3D(0, 0, -1)) <= angle_tolerance: - proj_x = Vector3D(1, 0, 0) - else: - proj_y = Vector3D(0, 0, 1).project(rel_plane.n) - proj_x = proj_y.rotate(rel_plane.n, math.pi / -2) - - ref_plane = Plane(rel_plane.n, parent_llc, proj_x) - min_2d = ref_plane.xyz_to_xy(apt_llc) - max_2d = ref_plane.xyz_to_xy(apt_urc) - height = max_2d.y - min_2d.y - width = max_2d.x - min_2d.x - - return \ - '"{}" = DOOR\n'.format(short_name(self.door.display_name)) + \ - "\n X = {}".format(min_2d.x) + \ - "\n Y = {}".format(min_2d.y) + \ - "\n WIDTH = {}".format(width, 3) + \ - "\n HEIGHT = {}".format(height, 3) + \ - '\n CONSTRUCTION = "Generic Door"\n ..\n' - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/exposedfloor.py b/honeybee_doe2/properties/exposedfloor.py deleted file mode 100644 index 6004d5e..0000000 --- a/honeybee_doe2/properties/exposedfloor.py +++ /dev/null @@ -1,41 +0,0 @@ -from ..utils.doe_formatters import short_name -from ..geometry.polygon import DoePolygon - - -class ExposedFloor: - def __init__(self, face): - self.face = face - # only flip horizontal floors - flip = True if abs(self.face.altitude + 90) <= 0.01 \ - else False - self.polygon = DoePolygon.from_face(face, flip=flip) - - def to_inp(self, space_origin): - - p_name = short_name(self.face.display_name) - - constr = self.face.properties.energy.construction.display_name - tilt = 90 - self.face.altitude - azimuth = 180 if abs(180 - tilt) <= 0.01 else self.face.azimuth - origin_pt = self.face.geometry.lower_left_corner - space_origin - - # create a unique polygon for exposed floor faces - polygon_name = f'{self.face.display_name}_ef Plg' - polygon = self.polygon.to_inp(name=polygon_name) + '\n' - obj_lines = [polygon] - - obj_lines.append('"{}" = EXTERIOR-WALL'.format(p_name)) - obj_lines.append('\n CONSTRUCTION = "{}_c"'.format(short_name(constr, 30))) - obj_lines.append('\n LOCATION = BOTTOM') - obj_lines.append('\n POLYGON = "{}"'.format(polygon_name)) - obj_lines.append('\n TILT = {}'.format(tilt)) - obj_lines.append('\n AZIMUTH = {}'.format(azimuth)) - obj_lines.append('\n X = {}'.format(origin_pt.x)) - obj_lines.append('\n Y = {}'.format(origin_pt.y)) - obj_lines.append('\n Z = {}'.format(origin_pt.z)) - obj_lines.append('\n ..\n') - - return ''.join([line for line in obj_lines]) - - def __repr__(self): - return f'DOE2 Wall: {self.face.display_name}' diff --git a/honeybee_doe2/properties/face.py b/honeybee_doe2/properties/face.py deleted file mode 100644 index 9031ee2..0000000 --- a/honeybee_doe2/properties/face.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# -*- Python Version: 2.7 -*- - -from ..geometry.polygon import DoePolygon - - -class FaceDoe2Properties(object): - - def __init__(self, _host): - self._host = _host - - @property - def host(self): - return self._host - - @property - def poly(self): - """Return a DOE2 Polygon object.""" - my_poly = DoePolygon.from_face(self.host) - return my_poly.to_inp() - - def duplicate(self, new_host=None): - # type: (Any) -> FaceDoe2Properties - _host = new_host or self._host - new_properties_obj = FaceDoe2Properties(_host) - - return new_properties_obj - - def ToString(self): - return self.__repr__() - - def __repr__(self): - return "Face Doe2 Properties: [host: {}]".format(self.host.display_name) diff --git a/honeybee_doe2/properties/groundcontact.py b/honeybee_doe2/properties/groundcontact.py deleted file mode 100644 index bff6cb9..0000000 --- a/honeybee_doe2/properties/groundcontact.py +++ /dev/null @@ -1,35 +0,0 @@ -from ..utils.doe_formatters import short_name -from ..geometry.polygon import DoePolygon - -class GroundFloor: - def __init__(self, face): - self.face = face - # only flip horizontal floors - flip = True if abs(self.face.altitude + 90) <= 0.01 \ - else False - self.polygon = DoePolygon.from_face(face, flip=flip) - - def to_inp(self, space_origin): - azimuth = 180 - origin_pt = self.face.geometry.lower_left_corner - space_origin - - # create a unique polygon for ground floor faces - polygon_name = f'{self.face.display_name}_ug Plg' - polygon = self.polygon.to_inp(name=polygon_name) + '\n' - obj_lines = [polygon] - obj_lines.append( - '"{}" = UNDERGROUND-WALL'.format(short_name(self.face.display_name))) - obj_lines.append('\n CONSTRUCTION = "{}_c"'.format( - short_name(self.face.properties.energy.construction.display_name, 30))) - obj_lines.append('\n LOCATION = BOTTOM') - obj_lines.append('\n POLYGON = "{}"'.format(polygon_name)) - obj_lines.append('\n AZIMUTH = {}'.format(azimuth)) - obj_lines.append('\n X = {}'.format(origin_pt.x)) - obj_lines.append('\n Y = {}'.format(origin_pt.y)) - obj_lines.append('\n Z = {}'.format(origin_pt.z)) - obj_lines.append('\n ..\n') - - return ''.join(obj_lines) - - def __repr__(self): - return f'DOE2 ground floor: {self.face.display_name}' diff --git a/honeybee_doe2/properties/hvac.py b/honeybee_doe2/properties/hvac.py deleted file mode 100644 index c6b86d3..0000000 --- a/honeybee_doe2/properties/hvac.py +++ /dev/null @@ -1,147 +0,0 @@ -from ..utils.doe_formatters import short_name -from .story import Doe2Story -from dataclasses import dataclass -from typing import List -from uuid import uuid4 - -from honeybee.model import Model as HBModel -from honeybee.room import Room -from honeybee.typing import clean_string - - - -@dataclass -class Zone: - - name: str - heating_setpoint: float - cooling_setpoint: float - conditioning: str - - @classmethod - def from_room(cls, room: Room): - if not isinstance(room, Room): - raise ValueError( - f'Unsupported type: {type(room)}\n' - 'Expected honeybee room' - ) - - name = short_name(clean_string(value=room.display_name).replace(' ', '')) - - if room.properties.energy.is_conditioned: - heating_setpoint = room.properties.energy.program_type.setpoint.heating_setpoint * 9. / 5. + 32. - cooling_setpoint = room.properties.energy.program_type.setpoint.cooling_setpoint * 9. / 5. + 32. - else: - heating_setpoint = 72 - cooling_setpoint = 75 - if room.properties.energy.is_conditioned == True: - conditioning = "CONDITIONED" - elif room.properties.energy.is_conditioned == False: - conditioning = "UNCONDITIONED" - - return cls(name=name, heating_setpoint=heating_setpoint, - cooling_setpoint=cooling_setpoint, conditioning=conditioning) - - def to_inp(self): - inp_str = f'"{self.name} Zn" = ZONE\n ' \ - f'TYPE = {self.conditioning}\n ' \ - f'DESIGN-HEAT-T = {self.heating_setpoint}\n ' \ - f'DESIGN-COOL-T = {self.cooling_setpoint}\n ' \ - 'SIZING-OPTION = ADJUST-LOADS\n ' \ - f'SPACE = "{self.name}"\n ..\n' - return inp_str - - def __repr__(self) -> str: - return self.to_inp() - - -@dataclass -class HVACSystem: - """Placeholder HVAC system class, returns each floor as a doe2, - HVAC system, with rooms as zones. - Args: - name: story display name - zones: list of doe2.hvac.Zone objects serviced by the system - Init method(s): - 1. from_model(model: HBModel) -> doe2_system - 2. from_story(story: Doe2Story) -> doe2_system - 3. from_room(room: Room) -> doe2_system - """ - name: str - zones: List[Zone] - - @classmethod - def from_model(cls, model: HBModel): - if not isinstance(model, HBModel): - raise ValueError( - f'Unsupported type: {type(model)}\n' - 'Expected honeybee.model.Model' - ) - name = short_name(model.display_name) - zones = [Zone.from_room(room) for room in model.rooms] - return cls(name=name, zones=zones) - - @classmethod - def from_story(cls, story: Doe2Story): - if not isinstance(story, Doe2Story): - raise ValueError( - f'Unsupported type: {type(story)}\n' - 'Expected Doe2Story' - ) - name = short_name(story.display_name) - zones = [Zone.from_room(room) for room in story.rooms] - return cls(name=name, zones=zones) - - @classmethod - def from_room(cls, room: Room, name: str = None): - if not isinstance(room, Room): - raise ValueError( - f'Unsupported type: {type(room)}\n' - 'Expected honeybee.room.Room' - ) - name = short_name(room.display_name) if name is None else name - zones = [Zone.from_room(room)] - return cls(name=name, zones=zones) - - @classmethod - def from_zone_groups(cls, zone_group: List[Room], name: str): - if not isinstance(zone_group, list): - raise ValueError( - f'Unsupported type: {type(zone_group)}\n' - 'Expected list of honeybee.room.Room' - ) - name = short_name(name) - zones = [Zone.from_room(room) for room in zone_group] - return cls(name=name, zones=zones) - - def to_inp(self): - sys_str = f'"{self.name}_Sys (SUM)" = SYSTEM\n' \ - ' TYPE = SUM\n' \ - ' HEAT-SOURCE = NONE\n' \ - ' SYSTEM-REPORTS = NO\n ..\n' - zones_str = '\n'.join(zone.to_inp() for zone in self.zones) - inp_str = '\n'.join([sys_str, zones_str]) - return inp_str - - def __repr__(self): - return self.to_inp() - - -def hb_hvac_mapper(model): - hvac_systems = [] - hvac_names = [] - - if model.properties.energy.hvacs is not None: - for hvac in model.properties.energy.hvacs: - hvac_names.append(hvac.display_name) - - for name in set(hvac_names): - pre_zones = [] - for room in model.rooms: - if room.properties.energy.hvac.display_name == name: - pre_zones.append(room) - - hvac_systems.append(HVACSystem.from_zone_groups( - zone_group=pre_zones, name=name)) - - return hvac_systems diff --git a/honeybee_doe2/properties/inputils/__init__.py b/honeybee_doe2/properties/inputils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/honeybee_doe2/properties/inputils/blocks.py b/honeybee_doe2/properties/inputils/blocks.py deleted file mode 100644 index 009fae7..0000000 --- a/honeybee_doe2/properties/inputils/blocks.py +++ /dev/null @@ -1,149 +0,0 @@ -"""INP text blocks.""" - -top_level = 'INPUT ..\n\n\n\n' -spacer = '\n\n' -sd_brk = '$ ---------------------------------------------------------\n' -star_brk = '$ *********************************************************\n' -star_blnk = '$ ** **\n' - - -abort_diag = '{sd_brk}$ Abort, Diagnostics\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -global_params = '{sd_brk}$ Global Parameters\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -ttrpddh = '{sd_brk}$ Title, Run Periods, Design Days, Holidays\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -comply = '{sd_brk}$ Compliance Data\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -mats_layers = '{sd_brk}$ Materials / Layers / Constructions\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -glzCode = '{sd_brk}$ Glass Type Codes\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) - -# Creating a generic hard-coded construction for doors. This should be -# updated to use the assigned construction to the door -doorCode = f'{sd_brk}$ Door Construction\n{sd_brk}{spacer}' \ - '"Generic Door" = CONSTRUCTION\n' \ - ' TYPE = U-VALUE\n' \ - ' U-VALUE = 0.5\n..\n' - -glzTyp = '{sd_brk}$ Glass Types\n{sd_brk}{spacer}\n\n'.format( - sd_brk=sd_brk, spacer=spacer) +\ - '"WT1" = GLASS-TYPE\n '\ - 'TYPE = GLASS-TYPE-CODE\n '\ - 'GLASS-TYPE-CODE = "2001"\n '\ - 'C-PRODUCT-TYPE = 0\n '\ - 'C-FRAME-TYPE = 0\n '\ - '..\n\n' -WindowLayers = '{sd_brk}$ Window Layers\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -iLikeLamp = '{sd_brk}$ Lamps / Luminaries / Lighting Systems\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -daySch = '{sd_brk}$ Day Schedules\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -weekSch = '{sd_brk}$ Week Schedules\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -annualSch = '{sd_brk}$ Annual Schedules\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -polygons = '{sd_brk}$ Polygons\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -wallParams = spacer+'{sd_brk}$ Wall Parameters\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -fix_bldg_shade = '{sd_brk}$ Fixed and Building Shades\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -miscCost = '{sd_brk}$ Misc Cost Related Objects\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) - - -perfCurve = star_brk+star_blnk+'$ ** Performance Curves **\n'\ - + star_blnk+star_brk+spacer -floorNspace = star_brk+star_blnk+'$ ** Floors / Spaces / Walls / Windows / Doors **\n'\ - + star_blnk+star_brk+spacer -elecFuelMeter = spacer+star_brk+star_blnk + \ - '$ ** Electric & Fuel Meters **\n' + \ - star_blnk+star_brk+spacer - -elec_meter = '{sd_brk}$ Electric Meters\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -fuel_meter = '{sd_brk}$ Fuel Meters\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -master_meter = '{sd_brk}$ Master Meters\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) - -hvac_circ_loop = star_brk+star_blnk+'$ ** HVAC Circulation Loops / Plant Equipment **\n'\ - + star_blnk+star_brk+spacer - -pumps = '{sd_brk}$ Pumps\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -heat_exch = '{sd_brk}$ Heat Exchangers\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -circ_loop = '{sd_brk}$ Circulation Loops\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -chiller_objs = '{sd_brk}$ Chillers\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -boiler_objs = '{sd_brk}$ Boilers\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -dwh = '{sd_brk}$ Domestic Water Heaters\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -heat_reject = '{sd_brk}$ Heat Rejection\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, - spacer=spacer) -tower_free = '{sd_brk}$ Tower Free Cooling\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -pvmod = '{sd_brk}$ Photovoltaic Modules\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -elecgen = '{sd_brk}$ Electric Generators\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -thermal_store = '{sd_brk}$ Thermal Storage\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -ground_loop_hx = '{sd_brk}$ Ground Loop Heat Exchangers\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -comp_dhw_res = sd_brk + \ - '$ Compliance DHW (residential dwelling units)\n' + \ - sd_brk+spacer - -steam_cld_mtr = star_brk+star_blnk+'$ ** Steam & Chilled Water Meters **\n'\ - + star_blnk+star_brk+spacer - -steam_mtr = '{sd_brk}$ Steam Meters\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -chill_meter = '{sd_brk}$ Chilled Water Meters\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) - -hvac_sys_zone = star_brk+star_blnk+'$ ** HVAC Systems / Zones **\n'\ - + star_blnk+star_brk+spacer - -misc_meter_hvac = star_brk+star_blnk+'$ ** Metering & Misc HVAC **\n'\ - + star_blnk+star_brk+spacer - -equip_controls = '{sd_brk}$ Equipment Controls\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -load_manage = '{sd_brk}$ Load Management\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) - -big_util_rate = star_brk+star_blnk+'$ ** Utility Rates **\n'\ - + star_blnk+star_brk+spacer - -ratchets = '{sd_brk}$ Ratchets\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -block_charge = '{sd_brk}$ Block Charges\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -small_util_rate = '{sd_brk}$ Utility Rates\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) - -output_reporting = star_brk+star_blnk+'$ ** Output Reporting **\n'\ - + star_blnk+star_brk+spacer - -loads_non_hrly = '{sd_brk}$ Loads Non-Hourly Reporting\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -sys_non_hrly = '{sd_brk}$ Systems Non-Hourly Reporting\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -plant_non_hrly = '{sd_brk}$ Plant Non-Hourly Reporting\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -econ_non_hrly = '{sd_brk}$ Economics Non-Hourly Reporting\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) -hourly_rep = '{sd_brk}$ Hourly Reporting\n{sd_brk}{spacer}'.format( - sd_brk=sd_brk, spacer=spacer) - -the_end = '{sd_brk}$ THE END\n{sd_brk}'.format( - sd_brk=sd_brk) + '\nEND ..\nCOMPUTE ..\nSTOP ..\n' diff --git a/honeybee_doe2/properties/inputils/compliance.py b/honeybee_doe2/properties/inputils/compliance.py deleted file mode 100644 index 562b399..0000000 --- a/honeybee_doe2/properties/inputils/compliance.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Compliance Data.""" - - -class ComplianceData(object): - - permit_scope = 0 - proj_name = 'sample_project' - bldg_type = 32 - cons_phase = 0 - nr_dhw_incl = 1 - code_version = 1 - num_floors = 1 - bldg_type_901 = 32 - - def __init__(self): - super(ComplianceData, self).__init__() - - def to_inp(self): - """Return compliance data as an inp string.""" - return '"Compliance Data" = COMPLIANCE\n' \ - ' C-PERMIT-SCOPE = {permit_scope}\n'.format(permit_scope=self.permit_scope) + \ - ' C-PROJ-NAME = *{proj_name}*\n'.format(proj_name=self.proj_name) + \ - ' C-BUILDING-TYPE = {bldg_type}\n'.format(bldg_type=self.bldg_type) + \ - ' C-CONS-PHASE = {cons_phase}\n'.format(cons_phase=self.cons_phase) + \ - ' C-NR-DHW-INCL = {nr_dhw_incl}\n'.format(nr_dhw_incl=self.nr_dhw_incl) + \ - ' C-CODE-VERSION = {code_version}\n'.format(code_version=self.code_version) + \ - ' C-901-NUM-FLRS = {num_floors}\n'.format(num_floors=self.num_floors) + \ - ' C-901-BLDG-TYPE = {bldg_type_901}\n'.format(bldg_type_901=self.bldg_type_901) + \ - ' ..' - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/inputils/glass_types.py b/honeybee_doe2/properties/inputils/glass_types.py deleted file mode 100644 index 5af22b9..0000000 --- a/honeybee_doe2/properties/inputils/glass_types.py +++ /dev/null @@ -1,40 +0,0 @@ -from honeybee_energy.construction.window import WindowConstruction -from honeybee_energy.material.glazing import EnergyWindowMaterialGlazing, \ - EnergyWindowMaterialSimpleGlazSys - -from honeybee_energy.construction.window import WindowConstruction -from ...utils.doe_formatters import short_name, unit_convertor - - -class GlassType(): - """ Doe2 Glass type, (window construction)""" - - def __init__(self, name, shading_coef, glass_cond): - self.name = name - self.shading_coef = shading_coef - self.glass_cond = glass_cond - - @classmethod - def from_hb_window_constr(cls, hb_window_constr): - - simple_window_con = hb_window_constr.to_simple_construction() - simple_window_mat = simple_window_con.materials[0] - - shading_coef = simple_window_mat.shgc / 0.87 - - glass_cond = unit_convertor( - [simple_window_mat.u_factor], 'Btu/h-ft2-F', 'W/m2-K') - - name = simple_window_con.identifier - name = short_name(name, 32) - - return cls(name=name, shading_coef=shading_coef, glass_cond=glass_cond) - - def to_inp(self) -> str: - return '"{}" = GLASS-TYPE\n'.format(short_name(self.name, 32)) + \ - ' TYPE = SHADING-COEF\n' +\ - ' SHADING-COEF = {}\n'.format(self.shading_coef) + \ - ' GLASS-CONDUCT = {}\n ..\n'.format(self.glass_cond) - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/inputils/run_period.py b/honeybee_doe2/properties/inputils/run_period.py deleted file mode 100644 index 7ec09e0..0000000 --- a/honeybee_doe2/properties/inputils/run_period.py +++ /dev/null @@ -1,41 +0,0 @@ - -from ladybug.analysisperiod import AnalysisPeriod - - -class RunPeriod(object): - def __init__( - self, begin_month=1, begin_day=1, begin_year=2022, end_month=12, end_day=31, - end_year=2022): - self.begin_month = begin_month - self.begin_day = begin_day - self.begin_year = begin_year - self.end_month = end_month - self.end_day = end_day - - @classmethod - def from_analysis_period(cls, analysis_period: AnalysisPeriod = None): - analysis_period = analysis_period or AnalysisPeriod() - cls_ = cls() - cls_.begin_month = analysis_period.st_month - cls_.begin_day = analysis_period.st_day - cls_.end_month = analysis_period.end_month - cls_.end_day = analysis_period.end_month - - return cls_ - - def to_inp(self): - """Return run period as an inp string.""" - # standard holidays should be exposed. - return '"Entire Year" = RUN-PERIOD-PD\n ' \ - 'BEGIN-MONTH = 1\n' \ - ' BEGIN-DAY = 1\n' \ - ' BEGIN-YEAR = 2021\n' \ - ' END-MONTH = 12\n' \ - ' END-DAY = 31\n' \ - ' END-YEAR = 2021\n' \ - ' ..\n\n' \ - '"Standard US Holidays" = HOLIDAYS\n ' \ - 'LIBRARY-ENTRY "US"\n ..' - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/inputils/sitebldg.py b/honeybee_doe2/properties/inputils/sitebldg.py deleted file mode 100644 index 4a8c9ca..0000000 --- a/honeybee_doe2/properties/inputils/sitebldg.py +++ /dev/null @@ -1,21 +0,0 @@ -class SiteBldgData(object): - def __init__(self): - super(SiteBldgData, self).__init__() - - def to_inp(self): - """Return Site and Building Data as an inp string""" - return '$' + ('-'*57) + '\n' \ - '$ Site and Building Data\n' \ - '$' + ('-'*57) + '\n\n' \ - '"Site Data" = SITE-PARAMETERS\n ' \ - 'ALTITUDE = 150\n ' \ - 'C-STATE = 21\n ' \ - 'C-WEATHER-FILE = *TMY2\\HARTFOCT.bin* \n ' \ - 'C-COUNTRY = 1\n ' \ - 'C-901-LOCATION = 1092\n ..\n' \ - '"Building Data" = BUILD-PARAMETERS\n ' \ - 'HOLIDAYS = "Standard US Holidays"\n ..\n\n\n' \ - 'PROJECT-DATA\n ..\n\n' - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/inputils/title.py b/honeybee_doe2/properties/inputils/title.py deleted file mode 100644 index 84be84d..0000000 --- a/honeybee_doe2/properties/inputils/title.py +++ /dev/null @@ -1,14 +0,0 @@ - -class Title(object): - def __init__(self, title): - self.title = title - - def to_inp(self): - """Return run period as an inp string.""" - # standard holidays should be exposed. - return 'TITLE\n' \ - ' LINE-1 = *{}*\n'.format(self.title) + \ - ' ..' - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/interiorfloor.py b/honeybee_doe2/properties/interiorfloor.py deleted file mode 100644 index 498d30b..0000000 --- a/honeybee_doe2/properties/interiorfloor.py +++ /dev/null @@ -1,41 +0,0 @@ -from ..utils.doe_formatters import short_name -from ..geometry.polygon import DoePolygon - - -class InteriorFloor: - def __init__(self, face): - self.face = face - # only flip horizontal floors - flip = True if abs(self.face.altitude + 90) <= 0.01 \ - else False - self.polygon = DoePolygon.from_face(face, flip=flip) - - def to_inp(self, space_origin): - p_name = short_name(self.face.display_name) - - constr = self.face.properties.energy.construction.display_name - tilt = 90 - self.face.altitude - azimuth = 180 if abs(180 - tilt) <= 0.01 else self.face.azimuth - origin_pt = self.face.geometry.lower_left_corner - space_origin - - # create a unique polygon for exposed floor faces - polygon_name = f'{self.face.display_name}_ef Plg' - polygon = self.polygon.to_inp(name=polygon_name) + '\n' - obj_lines = [polygon] - - obj_lines.append('"{}" = INTERIOR-WALL'.format(p_name)) - obj_lines.append('\n POLYGON = "{}"'.format(polygon_name)) - obj_lines.append('\n CONSTRUCTION = "{}_c"'.format(short_name(constr, 30))) - obj_lines.append( - '\n NEXT-TO = "{}"'.format(self.face.user_data['adjacent_room'])) - obj_lines.append('\n TILT = {}'.format(tilt)) - obj_lines.append('\n AZIMUTH = {}'.format(azimuth)) - obj_lines.append('\n X = {}'.format(origin_pt.x)) - obj_lines.append('\n Y = {}'.format(origin_pt.y)) - obj_lines.append('\n Z = {}'.format(origin_pt.z)) - obj_lines.append('\n ..\n') - - return ''.join([line for line in obj_lines]) - - def __repr__(self): - return f'DOE2 Wall: {self.face.display_name}' diff --git a/honeybee_doe2/properties/materials.py b/honeybee_doe2/properties/materials.py deleted file mode 100644 index a88eda7..0000000 --- a/honeybee_doe2/properties/materials.py +++ /dev/null @@ -1,144 +0,0 @@ -from enum import Enum -from honeybee_energy.material.opaque import EnergyMaterial, EnergyMaterialNoMass - - -from ..utils.doe_formatters import short_name, unit_convertor -from ladybug.datatype.rvalue import RValue - -class MaterialType(Enum): - """Doe2 material types.""" - mass = 'PROPERTIES' - no_mass = 'RESISTANCE' - - -class NoMassMaterial: - def __init__(self, _name, _resistence): - self._name = _name - self._resistence = _resistence - - @property - def name(self): - return self._name - - @property - def resistence(self): - return self._resistence - - @classmethod - def from_hb_material(cls, material): - """Create a NoMassMaterial from a honeybee energy material no-mass.""" - - _resistence = RValue().to_ip([material.r_value], 'm2-K/W')[0][0] - - return cls(_name=short_name(material.display_name, 32), - _resistence = _resistence) - - def to_inp(self): - spc = '' - obj_lines = [] - obj_lines.append('\n"{self._name}" = MATERIAL'.format(self=self)) - obj_lines.append('\n TYPE = {}'.format(MaterialType.no_mass.value)) - obj_lines.append('\n RESISTANCE = {}'.format(self.resistence)) - obj_lines.append('\n ..\n') - - return spc.join([l for l in obj_lines]) - - def __repr__(self): - return self.to_inp() - - -class MassMaterial: - def __init__(self, _name, _thickness, _conductivity, _density, _specific_heat): - self._name = _name - self._thickness = _thickness - self._conductivity = _conductivity - self._density = _density - self._specific_heat = _specific_heat - - @property - def name(self): - return self._name - - @property - def thickness(self): - return self._thickness - - @property - def conductivity(self): - return self._conductivity - - @property - def density(self): - return self._density - - @property - def specific_heat(self): - return self._specific_heat - - @classmethod - def from_hb_material(cls, material): - """Create a MassMaterial from a honeybee energy material.""" - assert isinstance(material, EnergyMaterial), \ - 'Expected EnergyMaterial. Got {}.'.format(type(material)) - - if unit_convertor([material.thickness], 'ft', 'm') < 0.001: - material.thickness = 0.001 - - return cls(_name=short_name(material.display_name, 32), - _thickness=unit_convertor([material.thickness], - 'ft', 'm'), - _conductivity=unit_convertor( - [material.conductivity], - 'Btu/h-ft2', 'W/m2'), - _density=round(material.density / 16.018, 3), - - _specific_heat=unit_convertor( - [material.specific_heat], - 'Btu/lb', 'J/kg')) - - def to_inp(self): - spc = '' - obj_lines = [] - - obj_lines.append('\n"{self.name}" = MATERIAL'.format(self=self)) - obj_lines.append('\n TYPE = {}'.format(MaterialType.mass.value)) - obj_lines.append('\n THICKNESS = {self.thickness}'.format(self=self)) - obj_lines.append('\n CONDUCTIVITY = {self.conductivity}'.format(self=self)) - obj_lines.append('\n DENSITY = {self.density}'.format(self=self)) - obj_lines.append('\n SPECIFIC-HEAT = {self.specific_heat}'.format(self=self)) - obj_lines.append('\n ..\n') - - return spc.join([l for l in obj_lines]) - - def __repr__(self): - return self.to_inp() - - -class Material: - """Do2 Material object. - refer to: - assets/DOE22Vol2-Dictionary_48r.pdf pg: 97 - """ - - def __init__(self, _material): - self._material = _material - - @property - def material(self): - return self._material - - @classmethod - def from_hb_material(cls, material): - if isinstance(material, EnergyMaterial): - return MassMaterial.from_hb_material(material) - elif isinstance(material, EnergyMaterialNoMass): - return NoMassMaterial.from_hb_material(material) - else: - raise ValueError('{} type is not supported for materials.'.format( - type(material))) - - def to_inp(self): - return self.material.to_inp() - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/model.py b/honeybee_doe2/properties/model.py deleted file mode 100644 index 29c3780..0000000 --- a/honeybee_doe2/properties/model.py +++ /dev/null @@ -1,223 +0,0 @@ -# -*- coding: utf-8 -*- -# -*- Python Version: 2.7 -* - -""" HB-Model Doe2 (eQuest) Properties.""" -from collections import defaultdict - -from honeybee.model import Model -from honeybee.room import Room -from honeybee.face import Face -from honeybee_energy.construction.opaque import OpaqueConstruction -from honeybee_energy.lib.constructionsets import generic_construction_set - -from .inputils import blocks as fb -from .inputils.compliance import ComplianceData -from .inputils.sitebldg import SiteBldgData as sbd -from .inputils.run_period import RunPeriod -from .inputils.title import Title - -from .story import Doe2Story -from .constructions import Construction, ConstructionCollection - -from .hvac import HVACSystem, Zone, hb_hvac_mapper -from .shades import Doe2Shade, Doe2ShadeCollection -from .activitydescription import DayScheduleDoe, DayScheduleType, WeekScheduleDoe - - -class ModelDoe2Properties(object): - """HB-Model Doe2 (eQuest) Properties.""" - - def __init__(self, _host: Model): - self._host = _host - - @property - def host(self): - return self._host - - def duplicate(self, new_host=None): - """_summary_ - Args: - new_host (_type_, optional): _description_. Defaults to None. - Returns: - _type_: _description_ - """ - # type: (Any) -> ModelDoe2Properties - _host = new_host or self._host - new_properties_obj = ModelDoe2Properties(_host) - return new_properties_obj - - @property - def _header(self): - """File header. - NOTE: The header is currently read-only - """ - return '\n'.join([fb.top_level, fb.abort_diag]) - - @property - def stories(self): - stories = [] - model = self.host - tol = model.tolerance - if not model.rooms: - return stories - grouped = Room.group_by_floor_height(model.rooms, 0.1) - for i, story in enumerate(grouped[0]): - stories.append(Doe2Story(story, i, tolerance=tol)) - - return stories - - @property - def header(self): - return '\n'.join([fb.top_level, fb.abort_diag]) - - @property - def mats_cons_layers(self): - return self._make_mats_cons_layers(self.host) - - @staticmethod - def _make_mats_cons_layers(obj): - cons = [] - for construction in generic_construction_set.wall_set.constructions: - if isinstance(construction, OpaqueConstruction): - cons.append(construction) - for construction in generic_construction_set.floor_set.constructions: - if isinstance(construction, OpaqueConstruction): - cons.append(construction) - for construction in generic_construction_set.roof_ceiling_set.constructions: - if isinstance(construction, OpaqueConstruction): - cons.append(construction) - for construction in obj.properties.energy.constructions: - if isinstance(construction, OpaqueConstruction): - cons.append(construction) - return ConstructionCollection.from_hb_constructions(constructions=cons).to_inp() - - @property - def hvac_sys_zones_by_model(self): - hvac_sys = [HVACSystem.from_model(self.host)] - return hvac_sys - - @property - def hvac_sys_zones_by_story(self): - hvac_sys_zones = [HVACSystem.from_story(story) for story in self.stories] - return hvac_sys_zones - - @property - def hvac_sys_zones_by_room(self): - hvac_sys_zones = [HVACSystem.from_room(room) for room in self.host.rooms] - return hvac_sys_zones - - @property - def hvac_sys_zones_by_hb_hvac(self): - return hb_hvac_mapper(self.host) - - @property - def fixed_shades(self): - return self._get_fixed_shades(self.host) - - @staticmethod - def _get_fixed_shades(obj): - if obj.shades is not None: - return Doe2ShadeCollection.from_hb_shades(obj.shades).to_inp() - else: - return None - - @property - def week_scheduels(self): - return self._get_week_scheduels(self.host) - - @staticmethod - def _get_week_scheduels(obj): - - translated_schedules = [] - for room in obj.rooms: - - if room.properties.energy.lighting is not None and room.properties.energy.lighting.schedule.is_single_week: - translated_schedules.append( - WeekScheduleDoe.from_schedule_ruleset( - schedule_ruleset=room.properties.energy.lighting.schedule, - stype=DayScheduleType.FRACTION)) - else: - None - - if room.properties.energy.people is not None and room.properties.energy.people.occupancy_schedule.is_single_week: - translated_schedules.append( - WeekScheduleDoe.from_schedule_ruleset( - schedule_ruleset=room.properties.energy.people.occupancy_schedule, - stype=DayScheduleType.FRACTION)) - else: - None - - if room.properties.energy.electric_equipment is not None and room.properties.energy.electric_equipment.schedule.is_single_week: - translated_schedules.append( - WeekScheduleDoe.from_schedule_ruleset( - schedule_ruleset=room.properties.energy.electric_equipment.schedule, - stype=DayScheduleType.FRACTION)) - else: - None - if room.properties.energy.infiltration is not None and room.properties.energy.infiltration.schedule.is_single_week: - translated_schedules.append( - WeekScheduleDoe.from_schedule_ruleset( - schedule_ruleset=room.properties.energy.infiltration.schedule, - stype=DayScheduleType.MULTIPLIER) - ) - else: - None - - if len(translated_schedules) > 0: - return '\n'.join( - set([schedule.to_inp() for schedule in translated_schedules])) - elif len(translated_schedules) == 0: - return '\n' - - @property - def day_scheduels(self): - return self._get_day_scheduels(self.host) - - @staticmethod - def _get_day_scheduels(obj): - - translated_schedules = [] - for room in obj.rooms: - - if room.properties.energy.lighting is not None: - for sch in room.properties.energy.lighting.schedule.day_schedules: - if room.properties.energy.lighting.schedule.is_single_week: - translated_schedules.append( - DayScheduleDoe.from_day_schedule( - day_schedule=sch, stype=DayScheduleType.FRACTION)) - - if room.properties.energy.people is not None: - for sch in room.properties.energy.people.occupancy_schedule.day_schedules: - if room.properties.energy.people.occupancy_schedule.is_single_week: - translated_schedules.append( - DayScheduleDoe.from_day_schedule( - day_schedule=sch, stype=DayScheduleType.FRACTION)) - - if room.properties.energy.electric_equipment is not None: - for sch in room.properties.energy.electric_equipment.schedule.day_schedules: - if room.properties.energy.electric_equipment.schedule.is_single_week: - translated_schedules.append( - DayScheduleDoe.from_day_schedule( - day_schedule=sch, stype=DayScheduleType.FRACTION)) - - if room.properties.energy.infiltration is not None: - for sch in room.properties.energy.infiltration.schedule.day_schedules: - if room.properties.energy.infiltration.schedule.is_single_week: - translated_schedules.append( - DayScheduleDoe.from_day_schedule( - day_schedule=sch, stype=DayScheduleType.MULTIPLIER)) - - if len(translated_schedules) > 0: - return '\n'.join( - set([schedule.to_inp() for schedule in translated_schedules])) - elif len(translated_schedules) == 0: - return '\n' - - def __str__(self): - return "Model Doe2 Properties: [host: {}]".format(self.host.display_name) - - def __repr__(self): - return str(self) - - def ToString(self): - return self.__repr__() diff --git a/honeybee_doe2/properties/roof.py b/honeybee_doe2/properties/roof.py deleted file mode 100644 index cc7affb..0000000 --- a/honeybee_doe2/properties/roof.py +++ /dev/null @@ -1,47 +0,0 @@ -from ladybug_geometry.geometry3d.face import Face3D -from ..utils.doe_formatters import short_name -from .aperture import Window - - -class DoeRoof: - def __init__(self, face): - self.face = face - - def to_inp(self, space_origin): - - p_name = short_name(self.face.display_name) - - constr = self.face.properties.energy.construction.display_name - tilt = 90 - self.face.altitude - azimuth = 180 if abs(tilt) <= 0.01 else self.face.azimuth - origin_pt = self.face.geometry.lower_left_corner - space_origin - - spc = '' - obj_lines = [] - - obj_lines.append('"{}" = ROOF'.format(p_name)) - obj_lines.append('\n POLYGON = "{}"'.format(p_name+' Plg')) - obj_lines.append('\n CONSTRUCTION = "{}_c"'.format( - short_name(constr, 30))) - obj_lines.append('\n TILT = {}'.format(tilt)) - obj_lines.append('\n AZIMUTH = {}'.format(azimuth)) - obj_lines.append('\n X = {}'.format(origin_pt.x)) - obj_lines.append('\n Y = {}'.format(origin_pt.y)) - obj_lines.append('\n Z = {}\n ..\n'.format(origin_pt.z)) - temp_str = spc.join([line for line in obj_lines]) - - doe_windows = [ - Window(ap, self.face) for ap in self.face.apertures - ] - - nl = '\n' - if doe_windows is not None: - for window in doe_windows: - temp_str += window.to_inp() + nl - return temp_str - else: - return temp_str - - -def __repr__(self): - return f'DOE2 roof: {self.face.display_name}' diff --git a/honeybee_doe2/properties/room.py b/honeybee_doe2/properties/room.py deleted file mode 100644 index fe66ba7..0000000 --- a/honeybee_doe2/properties/room.py +++ /dev/null @@ -1,296 +0,0 @@ -# -*- coding: utf-8 -*- -from enum import Enum -from honeybee_energy.boundarycondition import Adiabatic - -from honeybee.boundarycondition import Ground, Outdoors, Surface -from honeybee.facetype import Wall, Floor, RoofCeiling -from honeybee.face import Face -from honeybee.room import Room -from typing import List -from uuid import uuid4 - -from ..utils.doe_formatters import short_name -from .wall import DoeWall -from .roof import DoeRoof -from .groundcontact import GroundFloor -from .exposedfloor import ExposedFloor -from .interiorfloor import InteriorFloor -from .adiabaticfloor import AdiabaticFloor -from .adiabaticroof import AdiabaticRoof -from .ceiling import DoeCeilign - - -class ZoneType(Enum): - - CONDITIONED = 'CONDITIONED' - """Space is heated and/or cooled.""" - UNCONDITIONED = 'UNCONDITIONED' - """Space is neither heated nor cooled.""" - PLENUM = 'PLENUM' - """Space is a return air plenum.""" - - -class RoomDoe2Properties(object): - """Properties for a DOE2 Space.""" - - def __init__(self, _host: Room): - self._host = _host - self._boundary = None - self._interior_wall_toggle = None - self._interior_ceiling_toggle = None - - @property - def host(self) -> Room: - return self._host - - def boundary(self, tolerance) -> Face: - if self._boundary: - return self._boundary - tol = tolerance - _boundary = self._host.horizontal_boundary(match_walls=False, tolerance=tol) - if _boundary.has_holes: - print( - f'{self.host.display_name} has {len(_boundary.holes)} holes.' - ' They will be removed.') - _boundary._holes = [] # remove holes - _boundary = _boundary.remove_colinear_vertices(tolerance=tol) - _boundary = _boundary.remove_duplicate_vertices(tolerance=tol) - boundary_face = Face(identifier=str(uuid4()), geometry=_boundary) - boundary_face.display_name = self._host.display_name - self._boundary = boundary_face - return boundary_face - - @property - def interior_wall_toggle(self): - return self._interior_wall_toggle - - @interior_wall_toggle.setter - def interior_wall_toggle(self, value): - self._interior_wall_toggle = value if value is not None else self.interior_wall_toggle - - @property - def interior_ceiling_toggle(self): - return self._interior_ceiling_toggle - - @interior_ceiling_toggle.setter - def interior_ceiling_toggle(self, value): - self._interior_ceiling_toggle = value if value is not None else self.interior_ceiling_toggle - - - - - def duplicate(self, new_host=None): - _host = new_host or self._host - new_properties_obj = RoomDoe2Properties(_host) - return new_properties_obj - - def poly(self, tolerance): - # * return self's floor's face's poly - return self.boundary(tolerance).properties.doe2.poly - - @property - def walls(self) -> List[DoeWall]: - # * Needs to return list of DoeWall objects - - walls = [ - DoeWall(face) for face in self.host.faces - if isinstance(face.type, Wall) - and isinstance(face.boundary_condition, Outdoors) - ] - return walls - - @property - def interior_walls(self): - - interior_walls = [ - DoeWall(face) for face in self.host.faces - if isinstance(face.type, Wall) - and isinstance(face.boundary_condition, (Surface, Adiabatic)) - ] - return interior_walls - - @property - def roofs(self) -> List[DoeRoof]: - roofs = [ - DoeRoof(face) for face in self.host.faces - if isinstance(face.type, RoofCeiling) - and isinstance(face.boundary_condition, Outdoors) - ] - return roofs - - @property - def ceilings(self): - ceilings = [ - DoeCeilign(face) for face in self.host.faces - if isinstance(face.type, RoofCeiling) - and isinstance(face.boundary_condition, Surface) - ] - return ceilings - - @property - def adiabatic_roofs(self): - adiabatic_roofs = [ - AdiabaticRoof(face) for face in self.host.faces - if isinstance(face.type, RoofCeiling) - and isinstance(face.boundary_condition, Adiabatic) - - ] - return adiabatic_roofs - - @property - def ground_contact_surfaces(self): - ground_contact_faces = [ - GroundFloor(face) for face in self.host.faces - if isinstance(face.type, Floor) - and isinstance(face.boundary_condition, Ground) - ] - return ground_contact_faces - - @property - def exposed_floor_surfaces(self): - exposed_floor_surfaces = [ - ExposedFloor(face) for face in self.host.faces - if isinstance(face.type, Floor) - and isinstance(face.boundary_condition, Outdoors) - ] - return exposed_floor_surfaces - - @property - def interior_floor_surfaces(self): - interior_floor_surfaces = [ - InteriorFloor(face) for face in self.host.faces - if isinstance(face.type, Floor) - and isinstance(face.boundary_condition, Surface) - ] - return interior_floor_surfaces - - @property - def adiabatic_floor_surfaces(self): - adiabatic_floor_surfaces = [ - AdiabaticFloor(face) for face in self.host.faces - if isinstance(face.type, Floor) - and isinstance(face.boundary_condition, Adiabatic) - ] - return adiabatic_floor_surfaces - - @property - def space_energy_properties(self): - return self._convert_energy_properties(self.host) - - @staticmethod - def _convert_energy_properties(host: Room) -> List[str]: - doe_energy_properties = [] - if host.properties.energy.is_conditioned: - doe_energy_properties.append( - f' ZONE-TYPE = {ZoneType.CONDITIONED.value}\n') - else: - doe_energy_properties.append( - f' ZONE-TYPE = {ZoneType.UNCONDITIONED.value}\n') - if host.properties.energy.people: - doe_energy_properties.append( - f' NUMBER-OF-PEOPLE = {host.properties.energy.people.people_per_area*host.floor_area}\n' - ) - doe_energy_properties.append( - f' PEOPLE-SCHEDULE = "{short_name(host.properties.energy.people.occupancy_schedule.display_name)}_"\n' - ) - - if host.properties.energy.lighting: - doe_energy_properties.append( - f' LIGHTING-W/AREA = {host.properties.energy.lighting.watts_per_area / 10.7639}\n' - ) - doe_energy_properties.append( - f' LIGHTING-SCHEDULE = "{short_name(host.properties.energy.lighting.schedule.display_name)}_"\n' - ) - - if host.properties.energy.electric_equipment: - - doe_energy_properties.append( - f' EQUIP-SCHEDULE = ("{short_name(host.properties.energy.electric_equipment.schedule.display_name)}_")\n' - ) - doe_energy_properties.append( - f' EQUIPMENT-W/AREA = {host.properties.energy.electric_equipment.watts_per_area / 10.7639}\n' - ) - - doe_energy_properties.append( - f' EQUIP-SENSIBLE = {1 - host.properties.energy.electric_equipment.lost_fraction - host.properties.energy.electric_equipment.latent_fraction}\n' - ) - doe_energy_properties.append( - f' EQUIP-RAD-FRAC = {host.properties.energy.electric_equipment.radiant_fraction}\n' - ) - doe_energy_properties.append( - f' EQUIP-LATENT = {host.properties.energy.electric_equipment.latent_fraction}\n' - ) - - if host.properties.energy.infiltration: - doe_energy_properties.append( - f' INF-SCHEDULE = "{short_name(host.properties.energy.infiltration.schedule.display_name)}_"\n' - ) - doe_energy_properties.append(' INF-METHOD = AIR-CHANGE\n') - doe_energy_properties.append( - f' INF-FLOW/AREA = {host.properties.energy.infiltration.flow_per_exterior_area * 196.85}\n' - ) - - return doe_energy_properties - - def space(self, floor_origin): - # chances that a space is defined by a different azimuth than 0 is very low - azimuth = 0 - # this value should be set in relation to the Floor object - if not self._boundary: - raise ValueError( - 'You must call the `poly` method to create the boundary before calling ' - 'this method.' - ) - origin = self._boundary.geometry.lower_left_corner - origin_pt = origin - floor_origin - obj_lines = [] - obj_lines.append('"{}" = SPACE\n'.format(short_name(self.host.display_name))) - obj_lines.append(' SHAPE = POLYGON\n') - obj_lines.append(' POLYGON = "{} Plg"\n'.format(self.host.display_name)) - obj_lines.append(' AZIMUTH = {}\n'.format(azimuth)) - obj_lines.append(' X = {}\n'.format(origin_pt.x)) - obj_lines.append(' Y = {}\n'.format(origin_pt.y)) - obj_lines.append(' Z = {}\n'.format(origin_pt.z)) - obj_lines.append(' VOLUME = {}\n'.format(self.host.volume)) - - if 'act_desc' not in self.host.user_data: - for prop in self.space_energy_properties: - obj_lines.append(prop) - obj_lines.append(' ..\n') - elif 'act_desc' in self.host.user_data: - obj_lines.append(' C-ACTIVITY-DESC = *{}*\n'.format(self.host.user_data['act_desc'])) - obj_lines.append(' ..\n') - - spaces = ''.join(obj_lines) - walls = '\n'.join([w.to_inp(origin) for w in self.walls]) - - - if self.interior_wall_toggle == True: - interior_walls = '\n'.join(['']) - elif self.interior_wall_toggle == False: - interior_walls = '\n'.join([w.to_inp(origin) for w in self.interior_walls]) - - if self.interior_ceiling_toggle == True: - ceilings = '\n'.join(['']) - elif self.interior_ceiling_toggle == False: - ceilings = '\n'.join([c.to_inp(origin) for c in self.ceilings]) - - - roofs = '\n'.join([r.to_inp(origin) for r in self.roofs]) - ground_floors = '\n'.join( - [g.to_inp(origin) for g in self.ground_contact_surfaces] - ) - exposed_floors = '\n'.join( - [ef.to_inp(origin) for ef in self.exposed_floor_surfaces] - ) - interior_floors = '\n'.join([inf.to_inp(origin) for inf in self.interior_floor_surfaces] - ) - adiabatic_floors = '\n'.join( - [af.to_inp(origin) for af in self.adiabatic_floor_surfaces] - ) - adiabatic_roofs = '\n'.join( - [ar.to_inp(origin) for ar in self.adiabatic_roofs] - ) - return '\n'.join( - [spaces, walls, ceilings, interior_walls, roofs, adiabatic_roofs, ground_floors, exposed_floors, - interior_floors, adiabatic_floors]) diff --git a/honeybee_doe2/properties/shades.py b/honeybee_doe2/properties/shades.py deleted file mode 100644 index 1d8246b..0000000 --- a/honeybee_doe2/properties/shades.py +++ /dev/null @@ -1,111 +0,0 @@ -from dataclasses import dataclass -from typing import List -from math import degrees, isclose -import math - -from honeybee.model import Model -from honeybee.shade import Shade -from ladybug_geometry.geometry3d.face import Face3D -from ladybug_geometry.geometry3d.pointvector import Vector3D -from ladybug_geometry.geometry3d.line import LineSegment3D -from ..utils.doe_formatters import short_name -from ..geometry.polygon import DoePolygon -from .inputils import blocks as fb - - -@dataclass -class Doe2Shade: - # TODO: will need to change things up to support rm2d.shade_params - """ DOE2 shade object. Can be either: - - 'FIXED-SHADE': azimuth is independent from the building, i.e context shade such as - buildings and terrain. Objects that are independent from the orientation of the - building during ASHRAE 90.1 baseline orientation averages. - - 'BUILDING-SHADE': azimuth is connected to the buildign azimuth, - will rotate withh the building on change of azimuth, i.e fin shades, awnings, and - other types of "on building" shading devices. - - """ - name: str - shade_type: str - x_ref: float - y_ref: float - z_ref: float - azimuth: float - tilt: float - polygon: DoePolygon - transmittance: float = 0.0 - - @classmethod - def from_shade(cls, shade: Shade): - """Generate doe2 fixed shades shades""" - name = short_name(shade.display_name) - shade_type = 'FIXED-SHADE' - tolerance = 0.001 - if abs(shade.altitude + 90) <= tolerance: - # horizontal facing down - flip the face so we can deal with them like - # upwards facing shades. It doesn't make a difference in the results - geometry = shade.geometry.flip() - shade = Shade(name, geometry=geometry, is_detached=shade.is_detached) - - llc = shade.geometry.lower_left_corner - tilt = 90 - shade.altitude - azimuth = shade.azimuth - if abs(tilt) <= tolerance: - # set the azimuth to 180 for all the horizontal shade faces - azimuth = 180 - - x_ref = llc.x - y_ref = llc.y - z_ref = llc.z - - polygon = DoePolygon.from_face(shade) - - return cls( - name=name, shade_type=shade_type, x_ref=x_ref, y_ref=y_ref, z_ref=z_ref, - azimuth=azimuth, tilt=tilt, polygon=polygon, transmittance=0.0) - - def to_inp(self): - """Returns *.inp shade object string""" - obj_lines = [self.polygon.to_inp(), '\n\n'] - - obj_lines.append(f'"{self.name}" = {self.shade_type}\n') - obj_lines.append(' SHAPE = POLYGON\n') - obj_lines.append(f' POLYGON = "{self.name} Plg"\n') - obj_lines.append(f' TRANSMITTANCE = {self.transmittance}\n') - obj_lines.append(f' X-REF = {self.x_ref}\n') - obj_lines.append(f' Y-REF = {self.y_ref}\n') - obj_lines.append(f' Z-REF = {self.z_ref}\n') - obj_lines.append(f' TILT = {self.tilt}\n') - obj_lines.append(f' AZIMUTH = {self.azimuth}\n ..\n') - - return ''.join(obj_lines) - - def __repr__(self): - return self.to_inp() - - -@dataclass -class Doe2ShadeCollection: - - doe_shades: List[Doe2Shade] - - @classmethod - def from_hb_shades(cls, hb_shades: [Shade]): - """Generate doe2 fixed shades shades""" - - doe_shades = [Doe2Shade.from_shade(shade=shade) - for shade in hb_shades] - - return cls(doe_shades=doe_shades) - - def to_inp(self): - - block = [fb.fix_bldg_shade] - shades = [shade.to_inp() for shade in self.doe_shades] - - block.append('\n\n'.join(shades)) - - return '\n'.join(block) - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/story.py b/honeybee_doe2/properties/story.py deleted file mode 100644 index 3c5812a..0000000 --- a/honeybee_doe2/properties/story.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -# -*- Python Version: 2.7 -* -""" Doe2 'Story' Object.""" -from typing import List -from honeybee.room import Room -from honeybee.face import Face - - -class Doe2Story: - """This class represents a DOE2 FLOOR object.""" - - def __init__(self, rooms: List[Room], story_number: int, tolerance: float): - self.rooms = rooms - self.story_no = story_number - self.tolerance = tolerance - self.story_poly, self.boundary_geometry = self._story_poly - - @property - def _story_poly(self): - tol = self.tolerance - - try: - boundaries = Room.grouped_horizontal_boundary( - self.rooms, tolerance=tol, floors_only=True - ) - except IndexError: - try: - # try to create the boundary with the whole volume. This is an edge case - # that can happen in a few instances including when all the floor are air - # boundaries - boundaries = Room.grouped_horizontal_boundary( - self.rooms, tolerance=tol, floors_only=False - ) - except IndexError: - raise ValueError( - f'Failed to create the floor for floor {self.story_no} that ' - f'includes {self.rooms[0]} and {self.rooms[1]}. Check the model ' - 'and ensure there are no holes in the floor.' - ) - # pick the first boundary to represent the story - vertices = boundaries[0] \ - .remove_colinear_vertices(tol) \ - .remove_duplicate_vertices(tol).boundary # use boundary to ignore holes if any - - story_geom = Face.from_vertices( - identifier="Level_{}".format(self.story_no), - vertices=vertices) - story_geom.display_name = "Level_{}".format(self.story_no) - - story_rm_geom = [] - - story_rm_geom.append(story_geom.properties.doe2.poly) - - for room in self.rooms: - story_rm_geom.append(room.properties.doe2.poly(tol)) - for face in room.faces: - story_rm_geom.append(face.properties.doe2.poly) - story_rm_geom = '\n'.join(story_rm_geom) - - return '\n'.join([story_rm_geom]), story_geom - - @property - def space_height(self): - # TODO un-hardcode this - return self.rooms[0].average_floor_height - - @property - def floor_to_floor_height(self): - rooms = self.rooms - ceil_heights = 0 - ceil_areas = 0 - for room in rooms: - for face in room.faces: - if str(face.type) == 'RoofCeiling': - ceil_heights += face.center.z * face.area - ceil_areas += face.area - - ceil_h = ceil_heights / ceil_areas - ceil_l = rooms[0].average_floor_height - return ceil_h - ceil_l - - @property - def display_name(self): - return "Level_{}".format(self.story_no) - - def to_inp(self): - origin_pt = self.boundary_geometry.geometry.lower_left_corner - azimuth = self.boundary_geometry.azimuth - room_objs = [f.properties.doe2.space(origin_pt) for f in self.rooms] - - inp_obj = '\n"{self.display_name}"= FLOOR'.format(self=self) + \ - "\n SHAPE = POLYGON" + \ - '\n POLYGON = "Level_{self.story_no} Plg"'.format(self=self) + \ - '\n AZIMUTH = {}'.format(azimuth) + \ - '\n X = {}'.format(origin_pt.x) + \ - '\n Y = {}'.format(origin_pt.y) + \ - '\n Z = {}'.format(origin_pt.z) + \ - '\n SPACE-HEIGHT = {self.floor_to_floor_height}'.format(self=self) + \ - '\n FLOOR-HEIGHT = {self.floor_to_floor_height}'.format(self=self) + \ - '\n ..\n' - nl = '\n' - - return inp_obj + nl.join(str('\n'+f) for f in room_objs) - - def __repr__(self): - return self.to_inp() diff --git a/honeybee_doe2/properties/switchstatements.py b/honeybee_doe2/properties/switchstatements.py deleted file mode 100644 index 3d30bbe..0000000 --- a/honeybee_doe2/properties/switchstatements.py +++ /dev/null @@ -1,690 +0,0 @@ -from ladybug.datatype.temperature import Temperature -from honeybee_doe2.utils.doe_formatters import short_name -from ladybug.datatype.volumeflowrate import VolumeFlowRate - -class SwitchDesignHeat: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], room.properties.energy.setpoint.heating_setpoint) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED\n') - obj_lines.append(' DESIGN-HEAT-T =\n') - obj_lines.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {Temperature().to_ip(values=[act[1]], from_unit="C")[0][0]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchHeatSchedule: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], \ - short_name(room.properties.energy.setpoint.heating_schedule.display_name)) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED\n') - obj_lines.append(' HEAT-TEMP-SCH = \n') - obj_lines.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": #SI("{act[1]}_", "SPACE", "HEAT-TEMP-SCH")\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch} \n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchDesignCool: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], room.properties.energy.setpoint.cooling_setpoint) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED\n') - obj_lines.append(' DESIGN-COOL-T =\n') - obj_lines.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {Temperature().to_ip(values=[act[1]], from_unit="C")[0][0]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchCoolSchedule: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], \ - short_name(room.properties.energy.setpoint.cooling_schedule.display_name)) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED\n') - obj_lines.append(' COOL-TEMP-SCH = \n') - obj_lines.append('{switch(#LR("SPACE","C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": #SI("{act[1]}_", "SPACE", "COOL-TEMP-SCH")\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchAreaPerson: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], room.properties.energy.people.area_per_person) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("\nSET-DEFAULT FOR SPACE\n") - obj_lines.append(' AREA/PERSON =\n') - obj_lines.append('{switch(#L("C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {act[1]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchPeopleSched: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], short_name(room.properties.energy.people.occupancy_schedule.display_name)) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR SPACE\n") - obj_lines.append(' PEOPLE-SCHEDULE = \n') - obj_lines.append('{switch(#L("C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": #SI("{act[1]}_", "SPACE", "PEOPLE-SCHEDULE")\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchOutsideAirFlow: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], \ - VolumeFlowRate()._m3_s_to_cfm(room.properties.energy.infiltration.flow_per_exterior_area)) for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED') - obj_lines.append(" OUTSIDE-AIR-FLOW = \n") - obj_lines.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {act[1]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchLightingWArea: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], room.properties.energy.lighting.watts_per_area) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("\nSET-DEFAULT FOR SPACE\n") - obj_lines.append(' LIGHTING-W/AREA =\n') - obj_lines.append('{switch(#L("C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {act[1]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchEquipmentWArea: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], room.properties.energy.electric_equipment.watts_per_area) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("\nSET-DEFAULT FOR SPACE\n") - obj_lines.append(' EQUIPMENT-W/AREA =\n') - obj_lines.append('{switch(#L("C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {act[1]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchLightingSched: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], short_name(room.properties.energy.lighting.schedule.display_name)) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR SPACE\n") - obj_lines.append(' LIGHTING-SCHEDUL = \n') - obj_lines.append('{switch(#L("C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": #SI("{act[1]}_", "SPACE", "LIGHTING-SCHEDUL")\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchEquipmentSched: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], short_name(room.properties.energy.electric_equipment.schedule.display_name)) \ - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR SPACE\n") - obj_lines.append(' EQUIP-SCHEDULE = \n') - obj_lines.append('{switch(#L("C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": #SI("{act[1]}_", "SPACE", "EQUIP-SCHEDULE")\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchFlowArea: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], \ - VolumeFlowRate()._m3_s_to_cfm(room.properties.energy.ventilation.flow_per_area)) - for room in hb_model.rooms])) - return cls(activity_description) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED\n') - obj_lines.append(' FLOW/AREA =\n') - obj_lines.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {Temperature().to_ip(values=[act[1]], from_unit="C")[0][0]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchMinFlowRatio: - def __init__(self, _activity_descriptions, _hb_model): - self._activity_descriptions = _activity_descriptions - self._hb_model = _hb_model - - @property - def hb_model(self): - return self._hb_model - - @hb_model.setter - def hb_model(self, value): - self._hb_model = value if value is not None else self.hb_model - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], room.user_data['MIN-FLOW-RATIO']) - for room in hb_model.rooms])) - return cls(activity_description, hb_model) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED\n') - obj_lines.append(' MIN-FLOW-RATIO =\n') - obj_lines.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {act[1]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - - - def __repr__(self): - return self.to_inp() - - -class SwitchAssignedFlow: - def __init__(self, _activity_descriptions, _hb_model): - self._activity_descriptions = _activity_descriptions - self._hb_model = _hb_model - - @property - def hb_model(self): - return self._hb_model - - @hb_model.setter - def hb_model(self, value): - self._hb_model = value if value is not None else self.hb_model - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], room.user_data['ASSIGNED-FLOW']) - for room in hb_model.rooms])) - return cls(activity_description, hb_model) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED\n') - obj_lines.append(' ASSIGNED-FLOW =\n') - obj_lines.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {act[1]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchHMaxFlowRatio: - def __init__(self, _activity_descriptions, _hb_model): - self._activity_descriptions = _activity_descriptions - self._hb_model = _hb_model - - @property - def hb_model(self): - return self._hb_model - - @hb_model.setter - def hb_model(self, value): - self._hb_model = value if value is not None else self.hb_model - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], room.user_data['HMAX-FLOW-RATIO']) - for room in hb_model.rooms])) - return cls(activity_description, hb_model) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED\n') - obj_lines.append(' HMAX-FLOW-RATIO =\n') - obj_lines.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {act[1]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchMinFlowArea: - def __init__(self, _activity_descriptions, _hb_model): - self._activity_descriptions = _activity_descriptions - self._hb_model = _hb_model - - @property - def hb_model(self): - return self._hb_model - - @hb_model.setter - def hb_model(self, value): - self._hb_model = value if value is not None else self.hb_model - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - activity_description = list(set([(room.user_data['act_desc'], room.user_data['MIN-FLOW/AREA']) - for room in hb_model.rooms])) - return cls(activity_description, hb_model) - - def to_inp(self): - - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR ZONE\n") - obj_lines.append('TYPE = CONDITIONED\n') - obj_lines.append(' MIN-FLOW/AREA =\n') - obj_lines.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": {act[1]}\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - - def __repr__(self): - return self.to_inp() - - -class SwitchMinFlowSched: - def __init__(self, _activity_descriptions): - self._activity_descriptions = _activity_descriptions - - @property - def activity_descriptions(self): - return self._activity_descriptions - - @activity_descriptions.setter - def activity_descriptions(self, value): - self._activity_descriptions = value if value is not None else self.activity_descriptions - - @classmethod - def from_hb_model(cls, hb_model): - switch_rooms = [] - for room in hb_model.rooms: - if room.properties.energy.ventilation is not None: - switch_rooms.append(room) - - activity_description = list(set([(room.user_data['act_desc'], short_name(room.properties.energy.ventilation.schedule.display_name)) \ - for room in switch_rooms])) - return cls(activity_description) - - def to_inp(self): - obj_lines = [] - obj_lines.append("SET-DEFAULT FOR SPACE\n") - obj_lines.append(' MIN-FLOW-SCH = \n') - obj_lines.append('{switch(#L("C-ACTIVITY-DESC"))\n') - for act in self.activity_descriptions: - obj_lines.append(f'case "{act[0]}": #SI("{act[1]}_", "SPACE", "MIN-FLOW-SCH")\n') - obj_lines.append('default: no_default\n') - obj_lines.append('endswitch}\n') - obj_lines.append('..\n') - - switch_statement = ''.join(obj_lines) - return switch_statement - print(str(self.activity_descriptions)) - - def __repr__(self): - return self.to_inp() \ No newline at end of file diff --git a/honeybee_doe2/properties/wall.py b/honeybee_doe2/properties/wall.py deleted file mode 100644 index 32bc46f..0000000 --- a/honeybee_doe2/properties/wall.py +++ /dev/null @@ -1,87 +0,0 @@ -from ..utils.doe_formatters import short_name -from .aperture import Window -from .door import Door -from honeybee.facetype import Wall, Floor, RoofCeiling -from .constructions import Construction, UvalueConstruction - -class WallBoundaryCondition: - def __init__(self, boundary_condition): - self.boundary_condition = boundary_condition - - @property - def wall_typology(self): - return self._make_wall_type(self.boundary_condition) - - @staticmethod - def _make_wall_type(b_c): - if b_c is not None: - if str(b_c) == 'Outdoors': - return 'EXTERIOR-WALL' - elif str(b_c) == 'Ground': - return 'UNDERGROUND-WALL' - elif str(b_c) == 'Surface': - return 'INTERIOR-WALL' - elif str(b_c) == 'Adiabatic': - return 'INTERIOR-WALL' - - -class DoeWall: - def __init__(self, face): - self.face = face - - def to_inp(self, space_origin): - - p_name = short_name(self.face.display_name) - wall_typology = WallBoundaryCondition(self.face.boundary_condition).wall_typology - constr = short_name(self.face.properties.energy.construction.display_name, 30) - tilt = 90 - self.face.altitude - azimuth = self.face.azimuth - origin_pt = self.face.geometry.lower_left_corner - space_origin - - spc = '' - obj_lines = [] - - obj_lines.append('"{}" = {}'.format(p_name, wall_typology)) - obj_lines.append('\n POLYGON = "{}"'.format(f'{p_name} Plg')) - obj_lines.append('\n CONSTRUCTION = "{}_c"'.format(constr)) - obj_lines.append('\n TILT = {}'.format(tilt)) - obj_lines.append('\n AZIMUTH = {}'.format(azimuth)) - obj_lines.append('\n X = {}'.format(origin_pt.x)) - obj_lines.append('\n Y = {}'.format(origin_pt.y)) - obj_lines.append('\n Z = {}'.format(origin_pt.z)) - if wall_typology == 'INTERIOR-WALL' and str( - self.face.boundary_condition) == 'Surface': - if self.face.user_data: - next_to = self.face.user_data['adjacent_room'] - obj_lines.append('\n NEXT-TO = "{}"'.format(next_to)) - else: - print( - f'{self.face.display_name} is an interior face but is missing ' - 'adjacent room info in user data.' - ) - if wall_typology == 'INTERIOR-WALL' and str( - self.face.boundary_condition) == 'Adiabatic': - obj_lines.append('\n INT-WALL-TYPE = ADIABATIC') - next_to = self.face.parent.display_name - obj_lines.append('\n NEXT-TO = "{}"'.format(next_to)) - obj_lines.append('\n ..\n') - - temp_str = spc.join([line for line in obj_lines]) - - doe_windows = [ - Window(ap, self.face).to_inp() for ap in self.face.apertures - ] - - temp_str += '\n'.join(doe_windows) - - if wall_typology == 'EXTERIOR-WALL': - # Doors can only be part of exterior walls - doe_doors = [ - Door(dr, self.face).to_inp() for dr in self.face.doors - ] - temp_str += '\n'.join(doe_doors) - - return temp_str - - def __repr__(self): - return f'DOE2 Wall: {self.face.display_name}' diff --git a/honeybee_doe2/schedule.py b/honeybee_doe2/schedule.py new file mode 100644 index 0000000..f76e466 --- /dev/null +++ b/honeybee_doe2/schedule.py @@ -0,0 +1 @@ +"""honeybee-doe2 schedule translators.""" diff --git a/honeybee_doe2/utils/__init__.py b/honeybee_doe2/utils/__init__.py deleted file mode 100644 index ef84b73..0000000 --- a/honeybee_doe2/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# random_drawer.py diff --git a/honeybee_doe2/utils/doe_formatters.py b/honeybee_doe2/utils/doe_formatters.py deleted file mode 100644 index df3ad08..0000000 --- a/honeybee_doe2/utils/doe_formatters.py +++ /dev/null @@ -1,91 +0,0 @@ -import re -import os -import shutil -import subprocess -from ladybug.datatype import UNITS as lbt_units, TYPESDICT as lbt_td - - -def short_name(name, max_length=24): - if len(name) <= max_length: - return name - - shortened_name = ''.join(re.split("[aeiouy\\-\\_/]", name)) - - shortened_name = shortened_name.replace(' ', '') if len( - shortened_name) > max_length else shortened_name - - if len(shortened_name) <= max_length: - return shortened_name - - shortened_name = ''.join(shortened_name.split()) - if len(shortened_name) > max_length: - shortened_name = ''.join(shortened_name.split()) - if len(shortened_name) > max_length: - end_length = -1 * (max_length - 17) - shortened_name = f'{shortened_name[:16]}_{shortened_name[end_length:]}' - return shortened_name - - -def lower_left_properties(room_2d): - """Get the vertices, boundary conditions and windows starting from lower left. - v2 WIP - """ - room_2d.remove_duplicate_vertices() - simple_w_con = room_2d.properties.energy.construction_set.aperture_set.window_construction.to_simple_construction() - w_const_name = short_name(simple_w_con.identifier, 32) - floor_geo = room_2d.floor_geometry - start_pt = floor_geo.boundary[0] - min_y, min_x, pt_i = start_pt.y, start_pt.x, 0 - for i, pt in enumerate(floor_geo.boundary): - if pt.y < min_y: - min_y, min_x = pt.y, pt.x - pt_i = i - elif pt.y == min_y: - if pt.x < min_x: - min_y, min_x = pt.y, pt.x - pt_i = i - verts = floor_geo.boundary[pt_i:] + floor_geo.boundary[:pt_i] - if floor_geo.has_holes: - bcs = room_2d.boundary_conditions[:len(floor_geo.boundary)] - w_par = room_2d.window_parameters[:len(floor_geo.boundary)] - else: - bcs = room_2d.boundary_conditions - w_par = room_2d.window_parameters - bcs = bcs[pt_i:] + bcs[:pt_i] - w_par = w_par[pt_i:] + w_par[:pt_i] - - return (verts, bcs, w_par, w_const_name) - - -def unit_convertor(value, to_, from_): - """Helper function to convert values from one unit to another.""" - for key in lbt_units: - if from_ in lbt_units[key]: - base_type = lbt_td[key]() - break - else: - raise ValueError(f'Invalid type: {from_}') - - value = base_type.to_unit(value, to_, from_) - return round(value[0], 3) - - -def get_key(x): - return x.split('=')[0].replace('\n', '').replace('\"', '').strip() - - -def get_value(x): - return x.split( - '=' - )[1].split('\n')[0].strip() - - -def poly_name(polystr): - sub = re.sub('\$.*\n', '', polystr) - split = sub.split('..') - trimmed = {get_key(x): get_value(x) for x in split if len(x.split('=')) > 1 - - } - # return trimmed - for k, v in trimmed.items(): - return str(k) diff --git a/honeybee_doe2/utils/vector.py b/honeybee_doe2/utils/vector.py deleted file mode 100644 index becac14..0000000 --- a/honeybee_doe2/utils/vector.py +++ /dev/null @@ -1,7 +0,0 @@ - - -def cross(a, b): - c = [a.y*b.z - a.z*b.y, - a.z*b.x - a.x*b.z, - a.x*b.y - a.y*b.x] - return c diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 35b6d6d..b2ee57b 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -1,290 +1,220 @@ -import pathlib - -from .properties.inputils import blocks as fb -from .properties.inputils.compliance import ComplianceData -from .properties.inputils.sitebldg import SiteBldgData as sbd -from .properties.inputils.run_period import RunPeriod -from .properties.inputils.title import Title -from .properties.inputils.glass_types import GlassType -from .utils.doe_formatters import short_name - -from honeybee.model import Model -from honeybee_energy.construction.window import WindowConstruction -from honeybee_energy.lib.constructionsets import generic_construction_set -from honeybee.boundarycondition import Surface -from honeybee.typing import clean_string -from typing import List - -from .properties.switchstatements import * - -def model_to_inp(hb_model, hvac_mapping='story', exclude_interior_walls:bool=False, exclude_interior_ceilings:bool=False, switch_statements:bool=False): - # type: (Model) -> str +# coding=utf-8 +"""Methods to write to inp.""" + + +def shade_mesh_to_inp(shade_mesh): + """Generate an INP string representation of a ShadeMesh. + + Args: + shade_mesh: A honeybee ShadeMesh for which an INP representation + will be returned. + + Returns: + A tuple with two elements. + + - shade_polygons: A list of text strings for the INP polygons needed + to represent the ShadeMesh. + + - shade_defs: A list of text strings for the INP definitions needed + to represent the ShadeMesh. """ - args: - hb_model: Honeybee model - hvac_mapping: accepts: 'room', 'story', 'model', 'assigned-hvac' + # TODO: write the real code + return None, None + + +def shade_to_inp(shade): + """Generate an INP string representation of a Shade. + + Args: + shade: A honeybee Shade for which an INP representation will be returned. + + Returns: + A tuple with two elements. + + - shade_polygon: Text string for the INP polygon for the Shade. + + - shade_def: Text string for the INP definition of the Shade. """ + # TODO: write the real code + return None, None + + +def door_to_inp(door): + """Generate an INP string representation of a Door. + + Doors assigned to a parent Face will use the parent Face plane in order to + determine their XY coordinates. Otherwise, the Door's own plane will be used. + + Note that the resulting string does not include full construction definitions. + Also note that shades assigned to the Door are not included in the resulting + string. To write these objects into a final string, you must loop through the + Door.shades, and call the to.inp method on each one. + + Args: + door: A honeybee Door for which an INP representation will be returned. + + Returns: + Text string for the INP definition of the Door. + """ + # TODO: write the real code + return None + + +def aperture_to_inp(aperture): + """Generate an INP string representation of a Aperture. + + Apertures assigned to a parent Face will use the parent Face plane in order to + determine their XY coordinates. Otherwise, the Aperture's own plane will be used. + + Note that the resulting string does not include full construction definitions. + Also note that shades assigned to the Aperture are not included in the resulting + string. To write these objects into a final string, you must loop through the + Aperture.shades, and call the to.inp method on each one. + + Args: + aperture: A honeybee Aperture for which an INP representation will be returned. + + Returns: + Text string for the INP definition of the Aperture. + """ + # TODO: write the real code + return None + + +def face_to_inp(face): + """Generate an INP string representation of a Face. + + Note that the resulting string does not include full construction definitions. + + Also note that this does not include any of the shades assigned to the Face + in the resulting string. Nor does it include the strings for the + apertures or doors. To write these objects into a final string, you must + loop through the Face.apertures, and Face.doors and call the to.inp method + on each one. + + Args: + face: A honeybee Face for which an INP representation will be returned. - room_list = [room for room in hb_model.rooms] - counts = {} - for i, room in enumerate(room_list): - if room.display_name in counts: - counts[room.display_name] += 1 - room_list[i].display_name = f"{short_name(clean_string(value=room.display_name))}{counts[room.display_name]}" - else: - counts[room.display_name] = 1 - - mapper_options = ['room', 'story', 'model', 'assigned-hvac'] - if hvac_mapping not in mapper_options: - raise ValueError( - f'Invalid hvac_mapping input: {hvac_mapping}\n' - f'Expected one of the following: {mapper_options}' - ) - hvac_maps = None - if hvac_mapping == 'room': - hvac_maps = hb_model.properties.doe2.hvac_sys_zones_by_room - elif hvac_mapping == 'story': - hvac_maps = hb_model.properties.doe2.hvac_sys_zones_by_story - elif hvac_mapping == 'model': - hvac_maps = hb_model.properties.doe2.hvac_sys_zones_by_model - elif hvac_mapping == 'assigned-hvac': - hvac_maps = hb_model.properties.doe2.hvac_sys_zones_by_hb_hvac - - hb_model = hb_model.duplicate() - - if hb_model.units != 'Feet': - hb_model.convert_to_units(units='Feet') - hb_model.remove_degenerate_geometry() - - if switch_statements: - - switchPeopleSched = SwitchPeopleSched.from_hb_model(hb_model).to_inp() - switchAreaPerson = SwitchAreaPerson.from_hb_model(hb_model).to_inp() - switchLightingWArea = SwitchLightingWArea.from_hb_model(hb_model).to_inp() - switchEquipmentWArea = SwitchEquipmentWArea.from_hb_model(hb_model).to_inp() - switchLightingSched = SwitchLightingSched.from_hb_model(hb_model).to_inp() - switchEquipmentSched = SwitchEquipmentSched.from_hb_model(hb_model).to_inp() - switchDesignHeat = SwitchDesignHeat.from_hb_model(hb_model).to_inp() - switchHeatSchedule = SwitchHeatSchedule.from_hb_model(hb_model).to_inp() - switchDesignCool = SwitchDesignCool.from_hb_model(hb_model).to_inp() - switchCoolSchedule = SwitchCoolSchedule.from_hb_model(hb_model).to_inp() - switchOutsideAirFlow = SwitchOutsideAirFlow.from_hb_model(hb_model).to_inp() - switchFlowArea = SwitchFlowArea.from_hb_model(hb_model).to_inp() - switchMinFlowRatio = SwitchMinFlowRatio.from_hb_model(hb_model).to_inp() - switchAssignedFlow = SwitchAssignedFlow.from_hb_model(hb_model).to_inp() - switchHMaxFlowRatio = SwitchHMaxFlowRatio.from_hb_model(hb_model).to_inp() - switchMinFlowArea = SwitchMinFlowArea.from_hb_model(hb_model).to_inp() - #switchMinFlowSch = SwitchMinFlowSched.from_hb_model(hb_model).to_inp() - else: - #switchMinFlowSch = '' - switchPeopleSched = '' - switchAreaPerson = '' - switchLightingWArea = '' - switchEquipmentWArea = '' - switchLightingSched = '' - switchEquipmentSched = '' - switchDesignHeat = '' - switchHeatSchedule = '' - switchDesignCool = '' - switchCoolSchedule = '' - switchOutsideAirFlow = '' - switchFlowArea = '' - switchMinFlowRatio = '' - switchAssignedFlow = '' - switchHMaxFlowRatio = '' - switchMinFlowArea = '' - - for room in hb_model.rooms: - room.properties.doe2.interior_wall_toggle = exclude_interior_walls + Returns: + A tuple with two elements. + + - face_polygon: Text string for the INP polygon for the Face. + + - face_def: Text string for the INP definition of the Face. + """ + # TODO: write the real code + return None, None + + +def room_to_inp(room): + """Generate an INP string representation of a Room. + + The resulting string will include all internal gain definitions for the Room + (people, lights, equipment), infiltration definitions, ventilation requirements, + and thermostat objects. However, complete schedule definitions assigned to + these objects are excluded. + + Also note that this method does not write any of of the Room's constituent + Faces Shades, or windows into the resulting string. To represent the full + Room geometry, you must loop through the Room.faces and call the to.inp + method on each one. + + Args: + room: A honeybee Room for which an INP representation will be returned. - for room in hb_model.rooms: - room.properties.doe2.interior_ceiling_toggle = exclude_interior_ceilings - - day_list = [] - for scheduleruleset in hb_model.properties.energy.schedules: - for day in scheduleruleset.day_schedules: - day.unlock() - day.display_name = short_name(day.display_name) - day_list.append(day) - counts = {} - for i, day in enumerate(day_list): - if day.display_name in counts: - counts[day.display_name] += 1 - day_list[i].display_name = f"{day.display_name[:-1]}{counts[day.display_name]}" - else: - counts[day.display_name] = 1 + Returns: + A tuple with two elements. + + - room_polygon: Text string for the INP polygon for the Room. + + - room_def: Text string for the INP SPACE definition of the Room. + """ + # TODO: write the real code + return None, None + + +def model_to_inp( + model, hvac_mapping='Story', exclude_interior_walls=False, + exclude_interior_ceilings=False +): + """Generate an INP string representation of a Model. + + The resulting string will include all geometry (Rooms, Faces, Apertures, + Doors, Shades), all fully-detailed constructions + materials, all fully-detailed + schedules, and the room properties. + + Essentially, the string includes everything needed to simulate the model + except the simulation parameters. So joining this string with the output of + SimulationParameter.to_inp() should create a simulate-able INP. + + Args: + model: A honeybee Model for which an INP representation will be returned. + hvac_mapping: Text to indicate how HVAC systems should be assigned to the + exported model. Story will assign one HVAC system for each distinct + level polygon, Model will use only one HVAC system for the whole model + and AssignedHVAC will follow how the HVAC systems have been assigned + to the Rooms.properties.energy.hvac. Choose from the options + below. (Default: Story). + + * Room + * Story + * Model + * AssignedHVAC + + exclude_interior_walls: Boolean to note whether interior wall Faces + should be excluded from the resulting string. (Default: False). + exclude_interior_ceilings: Boolean to note whether interior ceiling + Faces should be excluded from the resulting string. (Default: False). + Usage: + + .. code-block:: python + + import os + from ladybug.futil import write_to_file + from honeybee.model import Model + from honeybee.room import Room + from honeybee.config import folders + from honeybee_doe2.simulation import SimulationParameter + + # Get input Model + room = Room.from_box('Tiny House Zone', 5, 10, 3) + room.properties.energy.program_type = office_program + room.properties.energy.add_default_ideal_air() + model = Model('Tiny House', [room]) + # Get the input SimulationParameter + sim_par = SimulationParameter() + + # create the INP string for simulation parameters and model + inp_str = '\n\n'.join((sim_par.to_inp(), model.to.inp(model))) + + # write the final string into an INP + inp = os.path.join(folders.default_simulation_folder, 'test_file', 'in.inp') + write_to_file(inp, inp_str, True) + """ + # duplicate model to avoid mutating it as we edit it for energy simulation + original_model = model + model = model.duplicate() + + # scale the model if the units are not feet + if model.units != 'Feet': + model.convert_to_units('Feet') + # remove degenerate geometry within native DOE-2 tolerance of 0.1 feet try: - hb_model.rectangularize_apertures( - subdivision_distance=0.5, max_separation=0.0, merge_all=True, - resolve_adjacency=True - ) - except AssertionError: - # try without resolving adjacency that can create errors - hb_model.rectangularize_apertures( - subdivision_distance=0.5, max_separation=0.0, merge_all=True, - resolve_adjacency=False - ) - - room_names = {} - face_names = {} - for room in hb_model.rooms: - room.display_name = clean_string(room.display_name).replace('..', '_') - room.display_name = short_name(room.display_name) - if room.display_name in room_names: - original_name = room.display_name - room.display_name = f'{original_name}_{room_names[original_name]}' - room_names[original_name] += 1 - else: - room_names[room.display_name] = 1 - - for face in room.faces: - face.display_name = clean_string(face.display_name).replace('..', '_') - face.display_name = short_name(face.display_name) - if face.display_name in face_names: - original_name = face.display_name - face.display_name = f'{original_name}_{face_names[original_name]}' - face_names[original_name] += 1 - else: - face_names[face.display_name] = 1 - - for apt in face.apertures: - apt.display_name = clean_string(apt.display_name).replace('..', '_') - apt.display_name = short_name(apt.display_name) - if apt.display_name in face_names: - original_name = apt.display_name - apt.display_name = f'{original_name}_{face_names[original_name]}' - face_names[original_name] += 1 - else: - face_names[apt.display_name] = 1 - - room_mapper = { - r.identifier: r.display_name for r in hb_model.rooms - } - for i, shade in enumerate(hb_model.shades): - shade.display_name = f'shade_{i}' - for face in hb_model.faces: - if isinstance(face.boundary_condition, Surface): - adj_room_identifier = face.boundary_condition.boundary_condition_objects[1] - face.user_data = {'adjacent_room': room_mapper[adj_room_identifier]} - - window_constructions = [ - generic_construction_set.aperture_set.window_construction, - generic_construction_set.aperture_set.interior_construction, - generic_construction_set.aperture_set.operable_construction, - generic_construction_set.aperture_set.skylight_construction - ] - - for construction in hb_model.properties.energy.constructions: - if isinstance(construction, WindowConstruction): - window_constructions.append(construction) - wind_con_set = set(window_constructions) - win_con_to_inp = [GlassType.from_hb_window_constr(constr) for constr in wind_con_set] - - - rp = RunPeriod() - comp_data = ComplianceData() - sb_data = sbd() - data = [ - hb_model.properties.doe2._header, - fb.global_params, - fb.ttrpddh, - Title(title=str(hb_model.display_name)).to_inp(), - rp.to_inp(), # TODO unhardcode - fb.comply, - comp_data.to_inp(), - sb_data.to_inp(), - fb.daySch, - hb_model.properties.doe2.day_scheduels, - fb.weekSch, - hb_model.properties.doe2.week_scheduels, - fb.mats_layers, - hb_model.properties.doe2.mats_cons_layers, - fb.glzCode, - '\n'.join(gt.to_inp() for gt in win_con_to_inp), - fb.doorCode, - fb.polygons, - '\n'.join(s.story_poly for s in hb_model.properties.doe2.stories), - fb.wallParams, - hb_model.properties.doe2.fixed_shades, - fb.miscCost, - fb.perfCurve, - fb.floorNspace, - switchPeopleSched, - switchAreaPerson, - switchLightingWArea, - switchLightingSched, - switchEquipmentWArea, - switchEquipmentSched, - '\n'.join(str(story) for story in hb_model.properties.doe2.stories), - fb.elecFuelMeter, - fb.elec_meter, - fb.fuel_meter, - fb.master_meter, - fb.hvac_circ_loop, - fb.pumps, - fb.heat_exch, - fb.circ_loop, - fb.chiller_objs, - fb.boiler_objs, - fb.dwh, - fb.heat_reject, - fb.tower_free, - fb.pvmod, - fb.elecgen, - fb.thermal_store, - fb.ground_loop_hx, - fb.comp_dhw_res, - fb.steam_cld_mtr, - fb.steam_mtr, - fb.chill_meter, - fb.hvac_sys_zone, - switchDesignHeat, - switchHeatSchedule, - switchDesignCool, - switchCoolSchedule, - switchOutsideAirFlow, - switchFlowArea, - switchMinFlowRatio, - switchAssignedFlow, - switchHMaxFlowRatio, - switchMinFlowArea, - #switchMinFlowSch, - '\n'.join(hv_sys.to_inp() - for hv_sys in hvac_maps), # * change to variable - fb.misc_meter_hvac, - fb.equip_controls, - fb.load_manage, - fb.big_util_rate, - fb.ratchets, - fb.block_charge, - fb.small_util_rate, - fb.output_reporting, - fb.loads_non_hrly, - fb.sys_non_hrly, - fb.plant_non_hrly, - fb.econ_non_hrly, - fb.hourly_rep, - fb.the_end - ] - return str('\n\n'.join(data)) - - -def honeybee_model_to_inp( - model: Model, hvac_mapping: str = 'story',exclude_interior_walls=False, exclude_interior_ceilings=False, - switch_statements:bool=False, folder: str = '.', name: str = None) -> pathlib.Path: - - inp_model = model_to_inp(model, hvac_mapping=hvac_mapping, - exclude_interior_walls=exclude_interior_walls, - exclude_interior_ceilings=exclude_interior_ceilings, - switch_statements=switch_statements) - - name = name or model.display_name - if not name.lower().endswith('.inp'): - name = f'{name}.inp' - out_folder = pathlib.Path(folder) - out_folder.mkdir(parents=True, exist_ok=True) - out_file = out_folder.joinpath(name) - out_file.write_text(inp_model) - return out_file + model.remove_degenerate_geometry(0.1) + except ValueError: + error = 'Failed to remove degenerate Rooms.\nYour Model units system is: {}. ' \ + 'Is this correct?'.format(original_model.units) + raise ValueError(error) + + # TODO: split all of the Rooms with holes so that they can be translated + # convert all of the Aperture geometries to rectangles so they can be translated + model.rectangularize_apertures( + subdivision_distance=0.5, max_separation=0.0, + merge_all=True, resolve_adjacency=True + ) + + # TODO: reassign stories to the model such that each has only one polygon + + # TODO: overwrite all identifiers to obey the eQuest 32 character length \ No newline at end of file diff --git a/tests/ceiling_toggle_test.py b/tests/ceiling_toggle_test.py deleted file mode 100644 index d815352..0000000 --- a/tests/ceiling_toggle_test.py +++ /dev/null @@ -1,25 +0,0 @@ -import pathlib - -from honeybee_doe2.writer import honeybee_model_to_inp -from honeybee.model import Model - -hvac_test = './tests/assets/multi_hvac.hbjson' -standard_test = './tests/assets/2023_rac_advanced_sample_project.hbjson' -air_wall_test = './tests/assets/Air_Wall_test.hbjson' -ceiling_adj_test = './tests/assets/ceiling_adj_test.hbjson' - -def test_hbjson_translate(): - """Test translating a HBJSON file to an inp file.""" - hb_json = ceiling_adj_test - - out_inp = './tests/assets/sample_out' - out_file = pathlib.Path(out_inp, 'ceiling_test_model.inp') - # delete if exists - if out_file.exists(): - out_file.unlink() - hb_model = Model.from_file(hb_json) - honeybee_model_to_inp(hb_model, hvac_mapping='model', exclude_interior_walls=False, exclude_interior_ceilings=True, - folder=out_inp, name='ceiling_test_model.inp') - - assert out_file.exists() - out_file.unlink() \ No newline at end of file diff --git a/tests/cli_test.py b/tests/cli_test.py deleted file mode 100644 index 3eed91f..0000000 --- a/tests/cli_test.py +++ /dev/null @@ -1,25 +0,0 @@ -import os - -from click.testing import CliRunner -from ladybug.futil import nukedir - -from honeybee_doe2.cli.translate import hb_model_to_inp_file - - -def test_model_to_folder(): - runner = CliRunner() - input_hb_model = './tests/assets/shade_test.hbjson' - folder = './tests/assets/sample_out' - name = 'cli_test' - hvac_mapping = 'story' - - - - result = runner.invoke( - hb_model_to_inp_file, - [input_hb_model, '--hvac-mapping', hvac_mapping, '--exclude-interior-walls', - '--exclude-interior-ceilings', '--name', name, '--folder', folder]) - - assert result.exit_code == 0 - assert os.path.isfile(os.path.join(folder, f'{name}.inp')) - nukedir(folder, True) \ No newline at end of file diff --git a/tests/exclude_true_test.py b/tests/exclude_true_test.py deleted file mode 100644 index 022e188..0000000 --- a/tests/exclude_true_test.py +++ /dev/null @@ -1,24 +0,0 @@ -import pathlib - -from honeybee_doe2.writer import honeybee_model_to_inp -from honeybee.model import Model - -hvac_test = './tests/assets/multi_hvac.hbjson' -standard_test = './tests/assets/2023_rac_advanced_sample_project.hbjson' -air_wall_test = './tests/assets/Air_Wall_test.hbjson' - -def test_hbjson_translate(): - """Test translating a HBJSON file to an inp file.""" - hb_json = standard_test - - out_inp = './tests/assets/sample_out' - out_file = pathlib.Path(out_inp, 'wall_test_model.inp') - # delete if exists - if out_file.exists(): - out_file.unlink() - hb_model = Model.from_file(hb_json) - honeybee_model_to_inp(hb_model, hvac_mapping='model', exclude_interior_walls=True, - folder=out_inp, name='wall_test_model.inp') - - - out_file.unlink() \ No newline at end of file diff --git a/tests/programtype_test.py b/tests/programtype_test.py deleted file mode 100644 index 06df69a..0000000 --- a/tests/programtype_test.py +++ /dev/null @@ -1,18 +0,0 @@ -import pathlib - -from honeybee_doe2.writer import honeybee_model_to_inp -from honeybee.model import Model - - -def test_model_with_program_types(): - hb_json = './tests/assets/revit-sample-with-program-type.hbjson' - out_inp = './tests/assets/sample_out' - out_file = pathlib.Path(out_inp, 'test_model_w_pt.inp') - if out_file.exists(): - out_file.unlink() - hb_model = Model.from_file(hb_json) - honeybee_model_to_inp(hb_model, hvac_mapping='story', - folder=out_inp, name='test_model_w_pt.inp') - - assert out_file.exists() - out_file.unlink() diff --git a/tests/rac_test.py b/tests/rac_test.py deleted file mode 100644 index 707b758..0000000 --- a/tests/rac_test.py +++ /dev/null @@ -1,20 +0,0 @@ -import pathlib - -from honeybee_doe2.writer import honeybee_model_to_inp -from honeybee.model import Model - -def test_duplicate_names(): - """Test writiting inp with rooms having duplicate names""" - hb_json = "./tests/assets/2023_rac_advanced_sample_project.hbjson" - out_inp = './tests/assets/sample_out' - out_file = pathlib.Path(out_inp, 'rac_test_model.inp') - # delete if exists - if out_file.exists(): - out_file.unlink() - hb_model = Model.from_file(hb_json) - honeybee_model_to_inp(hb_model, hvac_mapping='model', exclude_interior_walls=False, - exclude_interior_ceilings=False,switch_statements=[], - folder=out_inp, name='rac_test_model.inp') - - assert out_file.exists() - out_file.unlink() \ No newline at end of file diff --git a/tests/switch_statement_test.py b/tests/switch_statement_test.py deleted file mode 100644 index 4f6ff01..0000000 --- a/tests/switch_statement_test.py +++ /dev/null @@ -1,52 +0,0 @@ -import pathlib - -from honeybee_doe2.writer import honeybee_model_to_inp -from honeybee.model import Model - - - -def test_switch_true(): - """Test writiting inp with switch_statements""" - hb_json = "./tests/assets/switch_with_user_data.hbjson" - out_inp = './tests/assets/sample_out' - out_file = pathlib.Path(out_inp, 'switch_true_test_model.inp') - # delete if exists - if out_file.exists(): - out_file.unlink() - hb_model = Model.from_file(hb_json) - honeybee_model_to_inp(hb_model, hvac_mapping='model', exclude_interior_walls=False, - exclude_interior_ceilings=False,switch_statements=True, - folder=out_inp, name='switch_true_test_model.inp') - - assert out_file.exists() - out_file.unlink() - -def test_switch_false(): - """Test writiting inp with switch_statements""" - hb_json = "./tests/assets/testbed_no_user_data.hbjson" - out_inp = './tests/assets/sample_out' - out_file = pathlib.Path(out_inp, 'switch_false_test_model.inp') - # delete if exists - if out_file.exists(): - out_file.unlink() - hb_model = Model.from_file(hb_json) - honeybee_model_to_inp(hb_model, hvac_mapping='model', exclude_interior_walls=False, - exclude_interior_ceilings=False, switch_statements=False, - folder=out_inp, name='switch_false_test_model.inp') - - assert out_file.exists() - out_file.unlink() - - - - - - - - - - - - - - diff --git a/tests/writer_test.py b/tests/writer_test.py index 5a78065..6458da3 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -1,43 +1,24 @@ -import pathlib +"""Tests the features that honeybee_energy adds to honeybee_core Model.""" +from ladybug_geometry.geometry3d import Point3D, Vector3D, Mesh3D -from honeybee_doe2.writer import honeybee_model_to_inp from honeybee.model import Model - -hvac_test = './tests/assets/multi_hvac.hbjson' -standard_test = './tests/assets/2023_rac_advanced_sample_project.hbjson' -air_wall_test = './tests/assets/Air_Wall_test.hbjson' -ceiling_adj_test = './tests/assets/ceiling_adj_test.hbjson' - -def test_hbjson_translate(): - """Test translating a HBJSON file to an inp file.""" - hb_json = standard_test - - out_inp = './tests/assets/sample_out' - out_file = pathlib.Path(out_inp, 'test_model.inp') - # delete if exists - if out_file.exists(): - out_file.unlink() - hb_model = Model.from_file(hb_json) - honeybee_model_to_inp(hb_model, hvac_mapping='model', exclude_interior_walls=False, - folder=out_inp, name='test_model.inp') - - assert out_file.exists() - out_file.unlink() - - -def test_hbjson_with_schedule(): - - hb_json = './tests/assets/inp_schedule.hbjson' - out_inp = './tests/assets/sample_out' - out_file = pathlib.Path(out_inp, 'schedule_test.inp') - # delete if exists - if out_file.exists(): - out_file.unlink() - hb_model = Model.from_file(hb_json) - honeybee_model_to_inp( - hb_model, hvac_mapping='model', exclude_interior_walls=False, - folder=out_inp, name='schedule_test.inp' - ) - - assert out_file.exists() - out_file.unlink() +from honeybee.room import Room +from honeybee.shademesh import ShadeMesh + + +def test_inp_writer(): + """Test the existence of the Model inp reiter.""" + room = Room.from_box('Tiny_House_Zone', 5, 10, 3) + south_face = room[3] + south_face.apertures_by_ratio(0.4, 0.01) + south_face.apertures[0].overhang(0.5, indoor=False) + south_face.apertures[0].overhang(0.5, indoor=True) + south_face.apertures[0].move_shades(Vector3D(0, 0, -0.5)) + pts = (Point3D(0, 0, 4), Point3D(0, 2, 4), Point3D(2, 2, 4), + Point3D(2, 0, 4), Point3D(4, 0, 4)) + mesh = Mesh3D(pts, [(0, 1, 2, 3), (2, 3, 4)]) + awning_1 = ShadeMesh('Awning_1', mesh) + + model = Model('Tiny_House', [room], shade_meshes=[awning_1]) + + assert hasattr(model.to, 'inp') From ce6867f615db77389a2010a5e02512424adb3112 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Mon, 22 Apr 2024 17:47:36 -0700 Subject: [PATCH 02/27] feat(writer): Add writer functions for all of the objects --- .github/workflows/ci.yaml | 2 +- honeybee_doe2/config.py | 8 + honeybee_doe2/load.py | 76 ++++++++ honeybee_doe2/writer.py | 388 ++++++++++++++++++++++++++++++++++---- requirements.txt | 2 +- 5 files changed, 441 insertions(+), 35 deletions(-) create mode 100644 honeybee_doe2/config.py create mode 100644 honeybee_doe2/load.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6fd3561..19403b9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: name: Unit tests strategy: matrix: - python-version: ['3.7', '3.10'] + python-version: ['3.10'] os: [macos-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} diff --git a/honeybee_doe2/config.py b/honeybee_doe2/config.py new file mode 100644 index 0000000..b01276e --- /dev/null +++ b/honeybee_doe2/config.py @@ -0,0 +1,8 @@ +"""Global settings and configurations governing the translation process.""" + +# global parameters that may need to be adjusted in the future +DOE2_TOLERANCE = 0.03 # current best guess for DOE-2 absolute tolerance in Feet +DOE2_ANGLE_TOL = 1.0 # current best guess for DOE-2 angle tolerance in degrees +DOE2_INTERIOR_BCS = ('Surface', 'Adiabatic', 'OtherSideTemperature') +GEO_CHARS = 24 # number of original characters used in names of geometry +RES_CHARS = 30 # number of characters used in names of resources (constructions, etc.) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py new file mode 100644 index 0000000..7890da1 --- /dev/null +++ b/honeybee_doe2/load.py @@ -0,0 +1,76 @@ +"""honeybee-doe2 load translators.""" +from ladybug.datatype.area import Area +from ladybug.datatype.energyflux import EnergyFlux +from ladybug.datatype.volumeflowrateintensity import VolumeFlowRateIntensity +from honeybee.typing import clean_doe2_string + +from .config import RES_CHARS + +# TODO: Add methods to map to SOURCE-TYPE HOT-WATER and PROCESS + + +def people_to_inp(room): + """Translate the People definition of a Room into INP (Keywords, Values).""" + people = room.properties.energy.people + ppl_den = Area().to_unit([people.area_per_person], 'ft2', 'm2')[0] + ppl_total = ppl_den * room.floor_area + ppl_sch = clean_doe2_string(people.occupancy_schedule.display_name, RES_CHARS) + ppl_sch = '"{}"'.format(ppl_sch) + ppl_kwd = ('NUMBER-OF-PEOPLE', 'PEOPLE-SCHEDULE') + ppl_val = (ppl_total, ppl_sch) + return ppl_kwd, ppl_val + + +def lighting_to_inp(room): + """Translate the Lighting definition of a Room into INP (Keywords, Values).""" + lighting = room.properties.energy.lighting + lpd = EnergyFlux().to_unit([lighting.watts_per_area], 'W/ft2', 'W/m2')[0] + lgt_sch = clean_doe2_string(lighting.schedule.display_name, RES_CHARS) + lgt_sch = '"{}"'.format(lgt_sch) + light_kwd = ('LIGHTING-W/AREA', 'LIGHTING-SCHEDULE', 'LIGHT-TO-RETURN') + light_val = (lpd, lgt_sch, lighting.return_air_fraction) + return light_kwd, light_val + + +def equipment_to_inp(room): + """Translate the Equipment definition(s) of a Room into INP (Keywords, Values).""" + # first evaluate what types of equipment we have + ele_equip = room.properties.energy.electric_equipment + gas_equip = room.properties.energy.gas_equipment + + # extract the properties from the equipment objects + if ele_equip is not None and gas_equip is not None: # write them as lists + equip_val = [[], [], [], [], []] + for equip in (ele_equip, gas_equip): + epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] + equip_val[0].append(epd) + eqp_sch = clean_doe2_string(equip.schedule.display_name, RES_CHARS) + equip_val[1].append('"{}"'.format(eqp_sch)) + equip_val[2].append(1 - equip.latent_fraction - equip.lost_fraction) + equip_val[3].append(equip.latent_fraction) + equip_val[4].append(equip.radiant_fraction) + equip_val = ['( {}, {} )'.format(v[0], v[1]) for v in equip_val] + else: # write them as a single item + equip = ele_equip if gas_equip is None else gas_equip + epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] + eqp_sch = clean_doe2_string(equip.schedule.display_name, RES_CHARS) + eqp_sch = '"{}"'.format(eqp_sch) + sens_fract = 1 - equip.latent_fraction - equip.lost_fraction + equip_val = (epd, eqp_sch, sens_fract, equip.latent_fraction, + equip.radiant_fraction) + + equip_kwd = ('EQUIPMENT-W/AREA', 'EQUIPMENT-SCHEDULE', + 'EQUIP-SENSIBLE', 'EQUIP-LATENT', 'EQUIP-RAD-FRAC') + return equip_kwd, equip_val + + +def infiltration_to_inp(room): + """Translate the Infiltration definition of a Room into INP (Keywords, Values).""" + infil = room.properties.energy.infiltration + inf_den = infil.flow_per_exterior_area + inf_den = VolumeFlowRateIntensity().to_unit([inf_den], 'cfm/ft2', 'm3/s-m2')[0] + inf_sch = clean_doe2_string(infil.schedule.display_name, RES_CHARS) + inf_sch = '"{}"'.format(inf_sch) + inf_kwd = ('INF-METHOD', 'INF-FLOW/AREA', 'INF-SCHEDULE') + inf_val = ('AIR-CHANGE', inf_den, inf_sch) + return inf_kwd, inf_val diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index b2ee57b..bd138b5 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -1,5 +1,121 @@ # coding=utf-8 """Methods to write to inp.""" +import math +from ladybug_geometry.geometry2d import Point2D +from ladybug_geometry.geometry3d import Vector3D, Point3D, Plane, Face3D +from honeybee.typing import clean_doe2_string +from honeybee.boundarycondition import Surface +from honeybee.facetype import Wall, Floor, RoofCeiling + +from .config import DOE2_TOLERANCE, DOE2_ANGLE_TOL, DOE2_INTERIOR_BCS, \ + GEO_CHARS, RES_CHARS +from load import people_to_inp, lighting_to_inp, equipment_to_inp, \ + infiltration_to_inp + + +def generate_inp_string(u_name, command, keywords, values): + """Get an INP string representation of a DOE-2 object. + + This method is written in a generic way so that it can describe practically + any element of the INP Building Description Language (BDL). + + Args: + u_name: Text for the unique, user-specified name of the object being created. + This must be 32 characters or less and not contain special or non-ASCII + characters. The clean_doe2_string method may be used to convert + strings to a format that is acceptable here. For example, a U-Name + of a space might be "Floor2W ClosedOffice5". + command: Text indicating the type of instruction that the DOE-2 object + executes. Commands are typically in capital letters and examples + include POLYGON, FLOOR, SPACE, EXTERIOR-WALL, WINDOW, CONSTRUCTION, etc. + keywords: A list of text with the same length as the values that denote + the attributes of the DOE-2 object. + values: A list of values with the same length as the keywords that describe + the values of the attributes for the object. + + Returns: + inp_str -- A DOE-2 INP string representing a single object. + """ + space_count = tuple((25 - len(str(n))) for n in keywords) + spc = tuple(s_c * ' ' if s_c > 0 else ' ' for s_c in space_count) + body_str = '\n'.join(' {}{}= {}'.format(kwd, s, val) + for kwd, s, val in zip(keywords, spc, values)) + inp_str = '"{}" = {}\n{}\n ..\n'.format(u_name, command, body_str) + return inp_str + + +def face_3d_to_inp(face_3d, parent_name='HB object', is_shade=False): + """Convert a Face3D into a DOE-2 POLYGON string and info to position it in space. + + In this operation, all holes in the Face3D are ignored since they are not + supported by DOE-2. Collapsing the boundary and holes into a single list + that winds inward to cut out the holes will cause eQuest to raise an error. + + Args: + face_3d: A ladybug-geometry Face3D object for which a INP POLYGON + string will be generated. + parent_name: The name of the parent object that will reference this + POLYGON. This will be used to generate a name for the polygon. + Note that this should ideally have 24 characters or less so that + the result complies with the strict 32 character limit of DOE-2 + identifiers. + is_shade: Boolean to note whether the location_str needs to be generated + using the conventions for FIXED-SHADE as opposed to WALL, ROOF, FLOOR. + + Returns: + A tuple with two elements. + + - polygon_str: Text string for the INP polygon. + + - position_info: A tuple of values used to locate the Polygon in 3D space. + The order of properties in the tuple is as follows: (ORIGIN, TILT, AZIMUTH). + """ + # TODO: Consider adding a workaround for the DOE-2 limit of 40 vertices + # perhaps we can just say NO-SHAPE and specify AREA, VOLUME, and HEIGHT + # get the main properties that place the geometry in 3D space + pts_3d = face_3d.lower_left_counter_clockwise_boundary + llc_origin = pts_3d[0] + tilt, azimuth = math.degrees(face_3d.tilt), math.degrees(face_3d.azimuth) + + # get the 2D vertices in the plane of the Face + if DOE2_ANGLE_TOL <= tilt <= 180 - DOE2_ANGLE_TOL: # vertical or tilted + proj_y = Vector3D(0, 0, 1).project(face_3d.normal) + proj_x = proj_y.rotate(face_3d.normal, math.pi / -2) + ref_plane = Plane(face_3d.normal, llc_origin, proj_x) + vertices = [ref_plane.xyz_to_xy(pt) for pt in pts_3d] + else: # horizontal; ensure vertices are always counterclockwise from above + llc = Point2D(llc_origin.x, llc_origin.y) + vertices = [Point2D(v[0] - llc.x, v[1] - llc.y) for v in pts_3d] + if tilt > 180 - DOE2_ANGLE_TOL: + vertices = [Point2D(v.x, -v.y) for v in vertices] + + # format the vertices into a POLYGON string + vert_template = '( %f, %f )' + verts_values = tuple(vert_template % (pt.x, pt.y) for pt in vertices) + verts_keywords = tuple('V{}'.format(i + 1) for i in range(len(verts_values))) + poly_name = '{} Plg'.format(parent_name) + polygon_str = generate_inp_string(poly_name, 'POLYGON', verts_keywords, verts_values) + position_info = (llc_origin, azimuth, tilt) + return polygon_str, position_info + + +def _energy_trans_sch_to_transmittance(shade_obj): + """Try to extract the transmittance from the shade energy properties.""" + trans = 0 + trans_sch = shade_obj.properties.energy.transmittance_schedule + if trans_sch is not None: + if trans_sch.is_constant: + try: # assume ScheduleRuleset + trans = trans_sch.default_day_schedule[0] + except AttributeError: # ScheduleFixedInterval + trans = trans_sch.values[0] + else: # not a constant schedule; use the average transmittance + try: # assume ScheduleRuleset + sch_vals = trans_sch.values() + except Exception: # ScheduleFixedInterval + sch_vals = trans_sch.values + trans = sum(sch_vals) / len(sch_vals) + return trans def shade_mesh_to_inp(shade_mesh): @@ -18,8 +134,26 @@ def shade_mesh_to_inp(shade_mesh): - shade_defs: A list of text strings for the INP definitions needed to represent the ShadeMesh. """ - # TODO: write the real code - return None, None + # TODO: Sense when the shade is a rectangle and, if so, translate it without POLYGON + # set up collector lists and properties for all shades + shade_type = 'FIXED-SHADE' if shade_mesh.is_detached else 'BUILDING-SHADE' + base_id = clean_doe2_string(shade_mesh.identifier, GEO_CHARS) + trans = _energy_trans_sch_to_transmittance(shade_mesh) + keywords = ('SHAPE', 'POLYGON', 'TRANSMITTANCE', + 'X-REF', 'Y-REF', 'Z-REF', 'TILT', 'AZIMUTH') + shade_polygons, shade_defs = [], [] + # loop through the mesh faces and create individual shade objects + for i, face in enumerate(shade_mesh.geometry.face_vertices): + f_geo = Face3D(face) + shd_geo = f_geo.geometry if f_geo.altitude > 0 else f_geo.geometry.flip() + doe2_id = '{}{}'.format(base_id, i) + shade_polygon, pos_info = face_3d_to_inp(shd_geo, doe2_id) + origin, tilt, az = pos_info + values = ('POLYGON', '"{} Plg"', trans, origin.x, origin.y, origin.z, tilt, az) + shade_def = generate_inp_string(doe2_id, shade_type, keywords, values) + shade_polygons.append(shade_polygon) + shade_defs.append(shade_def) + return shade_polygons, shade_defs def shade_to_inp(shade): @@ -35,8 +169,22 @@ def shade_to_inp(shade): - shade_def: Text string for the INP definition of the Shade. """ - # TODO: write the real code - return None, None + # TODO: Sense when the shade is a rectangle and, if so, translate it without POLYGON + # create the polygon string from the geometry + shade_type = 'FIXED-SHADE' if shade.is_detached else 'BUILDING-SHADE' + doe2_id = clean_doe2_string(shade.identifier, GEO_CHARS) + shd_geo = shade.geometry if shade.altitude > 0 else shade.geometry.flip() + clean_geo = shd_geo.remove_colinear_vertices(DOE2_TOLERANCE) + shade_polygon, pos_info = face_3d_to_inp(clean_geo, doe2_id) + origin, tilt, az = pos_info + # create the shade definition, which includes the position information + trans = _energy_trans_sch_to_transmittance(shade) + keywords = ('SHAPE', 'POLYGON', 'TRANSMITTANCE', + 'X-REF', 'Y-REF', 'Z-REF', 'TILT', 'AZIMUTH') + values = ('POLYGON', '"{} Plg"', trans, + origin.x, origin.y, origin.z, tilt, az) + shade_def = generate_inp_string(doe2_id, shade_type, keywords, values) + return shade_polygon, shade_def def door_to_inp(door): @@ -56,8 +204,37 @@ def door_to_inp(door): Returns: Text string for the INP definition of the Door. """ - # TODO: write the real code - return None + # extract the plane information from the parent geometry + if door.has_parent: + parent_llc = door.parent.geometry.lower_left_corner + rel_plane = door.parent.geometry.plane + else: + parent_llc = door.geometry.lower_left_corner + rel_plane = door.geometry.plane + # get the LLC and URC of the bounding rectangle of the door + apt_llc = door.geometry.lower_left_corner + apt_urc = door.geometry.upper_right_corner + + # determine the width and height and origin in the parent coordinate system + if DOE2_ANGLE_TOL <= door.tilt <= 180 - DOE2_ANGLE_TOL: # vertical or tilted + proj_y = Vector3D(0, 0, 1).project(rel_plane.n) + proj_x = proj_y.rotate(rel_plane.n, math.pi / -2) + else: # located within the XY plane + proj_x = Vector3D(1, 0, 0) + ref_plane = Plane(rel_plane.n, parent_llc, proj_x) + min_2d = ref_plane.xyz_to_xy(apt_llc) + max_2d = ref_plane.xyz_to_xy(apt_urc) + width = max_2d.x - min_2d.x + height = max_2d.y - min_2d.y + + # create the aperture definition + doe2_id = clean_doe2_string(door.identifier, GEO_CHARS) + constr_o_name = door.properties.energy.construction.display_name + constr = clean_doe2_string(constr_o_name, RES_CHARS) + keywords = ('X', 'Y', 'WIDTH', 'HEIGHT', 'CONSTRUCTION') + values = (min_2d.x, min_2d.y, width, height, constr) + door_def = generate_inp_string(doe2_id, 'DOOR', keywords, values) + return door_def def aperture_to_inp(aperture): @@ -77,11 +254,40 @@ def aperture_to_inp(aperture): Returns: Text string for the INP definition of the Aperture. """ - # TODO: write the real code - return None - - -def face_to_inp(face): + # extract the plane information from the parent geometry + if aperture.has_parent: + parent_llc = aperture.parent.geometry.lower_left_corner + rel_plane = aperture.parent.geometry.plane + else: + parent_llc = aperture.geometry.lower_left_corner + rel_plane = aperture.geometry.plane + # get the LLC and URC of the bounding rectangle of the aperture + apt_llc = aperture.geometry.lower_left_corner + apt_urc = aperture.geometry.upper_right_corner + + # determine the width and height and origin in the parent coordinate system + if DOE2_ANGLE_TOL <= aperture.tilt <= 180 - DOE2_ANGLE_TOL: # vertical or tilted + proj_y = Vector3D(0, 0, 1).project(rel_plane.n) + proj_x = proj_y.rotate(rel_plane.n, math.pi / -2) + else: # located within the XY plane + proj_x = Vector3D(1, 0, 0) + ref_plane = Plane(rel_plane.n, parent_llc, proj_x) + min_2d = ref_plane.xyz_to_xy(apt_llc) + max_2d = ref_plane.xyz_to_xy(apt_urc) + width = max_2d.x - min_2d.x + height = max_2d.y - min_2d.y + + # create the aperture definition + doe2_id = clean_doe2_string(aperture.identifier, GEO_CHARS) + constr_o_name = aperture.properties.energy.construction.display_name + constr = clean_doe2_string(constr_o_name, RES_CHARS) + keywords = ('X', 'Y', 'WIDTH', 'HEIGHT', 'GLASS-TYPE') + values = (min_2d.x, min_2d.y, width, height, constr) + aperture_def = generate_inp_string(doe2_id, 'WINDOW', keywords, values) + return aperture_def + + +def face_to_inp(face, space_origin=Point3D(0, 0, 0)): """Generate an INP string representation of a Face. Note that the resulting string does not include full construction definitions. @@ -94,6 +300,8 @@ def face_to_inp(face): Args: face: A honeybee Face for which an INP representation will be returned. + space_origin: A ladybug-geometry Point3D for the origin of the space + to which the Face is assigned. (Default: (0, 0, 0)). Returns: A tuple with two elements. @@ -102,35 +310,148 @@ def face_to_inp(face): - face_def: Text string for the INP definition of the Face. """ - # TODO: write the real code - return None, None - - -def room_to_inp(room): + # set up attributes based on the face type and boundary condition + f_type_str, bc_str = str(face.type), str(face.boundary_condition) + if bc_str == 'Outdoors': + doe2_type = 'EXTERIOR-WALL' # DOE2 uses walls for a lot of things + if f_type_str == 'RoofCeiling': + doe2_type = 'ROOF' + elif bc_str in DOE2_INTERIOR_BCS or f_type_str == 'AirBoundary': + doe2_type = 'INTERIOR-WALL' # DOE2 uses walls for a lot of things + else: # likely ground or some other fancy ground boundary condition + doe2_type = 'UNDERGROUND-WALL' + + # create the polygon string from the geometry + doe2_id = clean_doe2_string(face.identifier, GEO_CHARS) + f_geo = face.geometry.remove_colinear_vertices(DOE2_TOLERANCE) + face_polygon, pos_info = face_3d_to_inp(f_geo, doe2_id) + face_origin, tilt, az = pos_info + origin = face_origin - space_origin + + # create the face definition, which includes the position info + constr_o_name = face.properties.energy.construction.display_name + constr = clean_doe2_string(constr_o_name, RES_CHARS) + keywords = ['POLYGON', 'CONSTRUCTION', 'TILT', 'AZIMUTH', 'X', 'Y', 'Z'] + values = ['"{} Plg"'.format(doe2_id), constr, tilt, az, origin.x, origin.y, origin.z] + if bc_str == 'Surface': + adj_room = face.boundary_condition.boundary_condition_objects[-1] + adj_id = clean_doe2_string(adj_room, GEO_CHARS) + values.append('"{}"'.format(adj_id)) + keywords.append('NEXT-TO') + elif doe2_type == 'INTERIOR-WALL': # assume that it is adiabatic + keywords.append('INT-WALL-TYPE') + values.append('ADIABATIC') + if f_type_str == 'Floor' and doe2_type != 'INTERIOR-WALL': + keywords.append('LOCATION') + values.append('BOTTOM') + face_def = generate_inp_string(doe2_id, doe2_type, keywords, values) + + return face_polygon, face_def + + +def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=False, + exclude_interior_ceilings=False): """Generate an INP string representation of a Room. - The resulting string will include all internal gain definitions for the Room - (people, lights, equipment), infiltration definitions, ventilation requirements, - and thermostat objects. However, complete schedule definitions assigned to - these objects are excluded. + This will include the Room's constituent Faces, Apertures and Doors with + each of these elements being a separate item in the list of strings returned. + However, any shades assigned to the Room or its constituent elements are + excluded and should be written by looping through the shades on the parent model. - Also note that this method does not write any of of the Room's constituent - Faces Shades, or windows into the resulting string. To represent the full - Room geometry, you must loop through the Room.faces and call the to.inp - method on each one. + The resulting string will also include all internal gain definitions for the + Room (people, lights, equipment), infiltration definitions, ventilation + requirements, and thermostat objects. + + However, complete schedule definitions assigned to these load objects are + excluded as well as any construction or material definitions. Args: - room: A honeybee Room for which an INP representation will be returned. - + floor_origin: A ladybug-geometry Point3D for the origin of the + floor (aka. story) to which the Room is a part of. (Default: (0, 0, 0)). + exclude_interior_walls: Boolean to note whether interior wall Faces + should be excluded from the resulting string. (Default: False). + exclude_interior_ceilings: Boolean to note whether interior ceiling + Faces should be excluded from the resulting string. (Default: False). + Returns: A tuple with two elements. - - room_polygon: Text string for the INP polygon for the Room. + - room_polygons: A list of text strings for the INP polygons needed + to represent the Room and all of its constituent Faces. - - room_def: Text string for the INP SPACE definition of the Room. + - room_defs: A list of text strings for the INP definitions needed + to represent the Room and all of its constituent Faces, Apertures + and Doors. """ - # TODO: write the real code - return None, None + # TODO: Sense when a Room is an extruded floor plate and, if so, do not use + # POLYGON to describe the Room faces + + # set up attributes based on the Room's energy properties + energy_attr_keywords = ['ZONE-TYPE'] + if room.exclude_floor_area: + energy_attr_values = ['PLENUM'] + elif room.properties.energy.is_conditioned: + energy_attr_values = ['CONDITIONED'] + else: + energy_attr_values = ['UNCONDITIONED'] + if room.properties.energy.people: + ppl_kwd, ppl_val = people_to_inp(room) + energy_attr_keywords.extend(ppl_kwd) + energy_attr_values.extend(ppl_val) + if room.properties.energy.lighting: + lgt_kwd, lgt_val = lighting_to_inp(room) + energy_attr_keywords.extend(lgt_kwd) + energy_attr_values.extend(lgt_val) + if room.properties.energy.electric_equipment or room.properties.energy.gas_equipment: + eq_kwd, eq_val = equipment_to_inp(room) + energy_attr_keywords.extend(eq_kwd) + energy_attr_values.extend(eq_val) + if room.properties.energy.infiltration: + inf_kwd, inf_val = infiltration_to_inp(room) + energy_attr_keywords.extend(inf_kwd) + energy_attr_values.extend(inf_val) + + # create the polygon string from the geometry + doe2_id = clean_doe2_string(room.identifier, GEO_CHARS) + r_geo = room.horizontal_boundary(match_walls=False, tolerance=DOE2_TOLERANCE) + r_geo = r_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) + room_polygon, pos_info = face_3d_to_inp(r_geo, doe2_id) + space_origin, _, _ = pos_info + origin = space_origin - floor_origin + + # create the space definition, which includes the position info + keywords = ['SHAPE', 'POLYGON', 'AZIMUTH', 'X', 'Y', 'Z' 'VOLUME'] + values = ['POLYGON', '"{} Plg"'.format(doe2_id), 0, + origin.x, origin.y, origin.z, room.volume] + if room.multiplier != 1: + keywords.append('MULTIPLIER') + values.append(room.multiplier) + keywords.extend(energy_attr_keywords) + values.extend(energy_attr_values) + space_def = generate_inp_string(doe2_id, 'SPACE', keywords, values) + + # gather together all definitions and polygons to define the room + room_polygons = [room_polygon] + room_defs = [space_def] + for face in room.faces: + # first check if this is a face that should be excluded + if isinstance(face.boundary_condition, Surface): + if exclude_interior_walls and isinstance(face.type, Wall): + continue + elif exclude_interior_ceilings and isinstance(face.type, (Floor, RoofCeiling)): + continue + # add the face definition along with all apertures and doors + face_polygon, face_def = face_to_inp(face, space_origin) + room_polygons.append(face_polygon) + room_defs.append(face_def) + for ap in face.apertures: + ap_def = aperture_to_inp(ap) + room_defs.append(ap_def) + if not isinstance(face.boundary_condition, Surface): + for dr in face.doors: + dr_def = door_to_inp(dr) + room_defs.append(dr_def) + return room_polygons, room_defs def model_to_inp( @@ -202,7 +523,7 @@ def model_to_inp( model.convert_to_units('Feet') # remove degenerate geometry within native DOE-2 tolerance of 0.1 feet try: - model.remove_degenerate_geometry(0.1) + model.remove_degenerate_geometry(DOE2_TOLERANCE) except ValueError: error = 'Failed to remove degenerate Rooms.\nYour Model units system is: {}. ' \ 'Is this correct?'.format(original_model.units) @@ -215,6 +536,7 @@ def model_to_inp( merge_all=True, resolve_adjacency=True ) - # TODO: reassign stories to the model such that each has only one polygon + # TODO: reassign stories to the model such that each level has only one polygon - # TODO: overwrite all identifiers to obey the eQuest 32 character length \ No newline at end of file + # reset identifiers to make them unique and derived from the display names + model.reset_ids() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4b18072..c56a809 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -honeybee-energy>=1.101.8 +honeybee-energy>=1.105.45 From 01ce01c23eea486adfc92b541005d645a90e0426 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 23 Apr 2024 17:57:22 -0700 Subject: [PATCH 03/27] feat(schedule): Add functions for translating schedules --- honeybee_doe2/_extend_honeybee.py | 2 - honeybee_doe2/config.py | 4 +- honeybee_doe2/load.py | 1 - honeybee_doe2/material.py | 1 - honeybee_doe2/schedule.py | 253 ++++++++++++++++++++++++++++++ honeybee_doe2/simulation.py | 2 + honeybee_doe2/util.py | 56 +++++++ honeybee_doe2/writer.py | 201 +++++++++++++++--------- tests/schedule_test.py | 227 +++++++++++++++++++++++++++ 9 files changed, 672 insertions(+), 75 deletions(-) delete mode 100644 honeybee_doe2/material.py create mode 100644 honeybee_doe2/simulation.py create mode 100644 honeybee_doe2/util.py create mode 100644 tests/schedule_test.py diff --git a/honeybee_doe2/_extend_honeybee.py b/honeybee_doe2/_extend_honeybee.py index 017d1d5..2668ae8 100644 --- a/honeybee_doe2/_extend_honeybee.py +++ b/honeybee_doe2/_extend_honeybee.py @@ -19,5 +19,3 @@ aperture_writer.inp = aperture_to_inp door_writer.inp = door_to_inp shade_mesh_writer.inp = shade_mesh_to_inp - -# TODO: Extend classes of honeybee-energy with to_inp() methods and inp_identifier() diff --git a/honeybee_doe2/config.py b/honeybee_doe2/config.py index b01276e..b6f27a1 100644 --- a/honeybee_doe2/config.py +++ b/honeybee_doe2/config.py @@ -1,8 +1,10 @@ """Global settings and configurations governing the translation process.""" -# global parameters that may need to be adjusted in the future +# global parameters that may need to be adjusted or exposed in the future DOE2_TOLERANCE = 0.03 # current best guess for DOE-2 absolute tolerance in Feet DOE2_ANGLE_TOL = 1.0 # current best guess for DOE-2 angle tolerance in degrees +FLOOR_LEVEL_TOL = 0.1 # tolerance for grouping Rooms by floor elevations in Feet +RECT_WIN_SUBD = 0.5 # subdivision distance to rectangularize windows in Feet DOE2_INTERIOR_BCS = ('Surface', 'Adiabatic', 'OtherSideTemperature') GEO_CHARS = 24 # number of original characters used in names of geometry RES_CHARS = 30 # number of characters used in names of resources (constructions, etc.) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 7890da1..1c88ad6 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -5,7 +5,6 @@ from honeybee.typing import clean_doe2_string from .config import RES_CHARS - # TODO: Add methods to map to SOURCE-TYPE HOT-WATER and PROCESS diff --git a/honeybee_doe2/material.py b/honeybee_doe2/material.py deleted file mode 100644 index 8ef372e..0000000 --- a/honeybee_doe2/material.py +++ /dev/null @@ -1 +0,0 @@ -"""honeybee-doe2 material translators.""" diff --git a/honeybee_doe2/schedule.py b/honeybee_doe2/schedule.py index f76e466..3224bb1 100644 --- a/honeybee_doe2/schedule.py +++ b/honeybee_doe2/schedule.py @@ -1 +1,254 @@ +# coding=utf-8 """honeybee-doe2 schedule translators.""" +from ladybug.dt import Date, MONTHNAMES +from honeybee.typing import clean_doe2_string + +from .config import RES_CHARS +from .util import generate_inp_string + + +def schedule_type_limit_to_inp(type_limit): + """Get the DOE-2 type for a honeybee-energy ScheduleTypeLimit.""" + if type_limit is None: + return 'FRACTION' + elif type_limit.unit_type == 'Temperature': + return 'TEMPERATURE' + else: + if type_limit.numeric_type == 'Discrete': + return 'ON/OFF' + else: + return 'FRACTION' + + +def schedule_day_to_inp(day_schedule, type_limit=None): + """Convert a ScheduleDay into a DAY-SCHEDULE INP string.""" + # setup the DOE-2 identifier and lists for keywords and values + doe2_id = clean_doe2_string(day_schedule.identifier, RES_CHARS) + type_text = schedule_type_limit_to_inp(type_limit) + keywords, values = ['TYPE'], [type_text] + hour_values = day_schedule.values_at_timestep(1) + + # setup a function to format list of values correctly + def _format_day_values(values_to_format): + if len(values_to_format) == 1: + return'({})'.format(values_to_format[0]) + else: + return str(tuple(values_to_format)) + + # loop through the hourly values and write them in the format DOE-2 likes + prev_count, prev_hour, prev_values = 0, 1, [hour_values[0]] + for i, val in enumerate(hour_values): + if i == 0: + continue # ignore the first value already in the list + if val == prev_values[-1]: + prev_count += 1 + if len(prev_values) > 1: + keywords.append('HOURS') + if prev_hour != i - 1: + values.append('({}, {})'.format(prev_hour, i - 1)) + keywords.append('VALUES') + values.append(_format_day_values(prev_values[:-1])) + prev_values = [prev_values[-1]] + prev_hour = i + else: + values.append('({}, {})'.format(prev_hour, i)) + keywords.append('VALUES') + values.append(_format_day_values(prev_values)) + prev_values = [prev_values[-1]] + prev_hour = i + 1 + continue # wait for the value to change + if prev_count == 0: # just keep assembling the list of values + prev_values.append(val) + else: + keywords.append('HOURS') + values.append('({}, {})'.format(prev_hour, i)) + keywords.append('VALUES') + values.append(_format_day_values(prev_values)) + prev_values = [val] + prev_hour = i + 1 + prev_count = 0 + keywords.append('HOURS') + values.append('({}, {})'.format(prev_hour, 24)) + keywords.append('VALUES') + values.append(_format_day_values(prev_values)) + + # return the INP string + return generate_inp_string(doe2_id, 'DAY-SCHEDULE', keywords, values) + + +def schedule_ruleset_to_inp(schedule): + """Convert a ScheduleRuleset into a WEEK-SCHEDULE and SCHEDULE INP strings. + + Note that this method only outputs SCHEDULE and WEEK-SCHEDULE objects + However, to write the full schedule into an INP, the schedules's + day_schedules must also be written. + + Returns: + A tuple with two elements + + - year_schedule: Text string representation of the SCHEDULE + describing this schedule. + + - week_schedules: A list of WEEK-SCHEDULE test strings that are + referenced in the year_schedule. + """ + # setup the DOE-2 identifier and lists for keywords and values + doe2_id = clean_doe2_string(schedule.identifier, RES_CHARS) + type_text = schedule_type_limit_to_inp(schedule.schedule_type_limit) + day_types = ['(MON)', '(TUE)', '(WED)', '(THU)', '(FRI)', '(SAT)', '(SUN)', + '(HOL)', '(HDD)', '(CDD)'] + + def _get_week_list(schedule, rule_indices): + """Get a list of the ScheduleDay identifiers applied on each day of the week.""" + week_list = [] + for dow in range(7): + for i in rule_indices: + if schedule._schedule_rules[i].week_apply_tuple[dow]: + day_sch_id = schedule._schedule_rules[i].schedule_day.identifier + week_list.append(clean_doe2_string(day_sch_id, RES_CHARS)) + break + else: # no rule applies; use default_day_schedule. + day_sch_id = schedule.default_day_schedule.identifier + week_list.append(clean_doe2_string(day_sch_id, RES_CHARS)) + week_list.append(week_list.pop(0)) # DOE-2 starts week on Monday; not Sunday + return week_list + + def _get_extra_week_fields(schedule): + """Get schedule identifiers of extra days in Schedule:Week.""" + # add summer and winter design days + week_fields = [] + if schedule._holiday_schedule is not None: + day_sch_id = schedule._holiday_schedule.identifier + week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS)) + else: + day_sch_id = schedule._default_day_schedule.identifier + week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS)) + if schedule._winter_designday_schedule is not None: + day_sch_id = schedule._winter_designday_schedule.identifier + week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS)) + else: + day_sch_id = schedule._default_day_schedule.identifier + week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS)) + if schedule._summer_designday_schedule is not None: + day_sch_id = schedule._summer_designday_schedule.identifier + week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS)) + else: + day_sch_id = schedule._default_day_schedule.identifier + week_fields.append(clean_doe2_string(day_sch_id, RES_CHARS)) + return week_fields + + def _inp_week_schedule_from_rule_indices(schedule, rule_indices, week_index): + """Create an INP string of a week schedule from a list of rules indices.""" + week_sch_id = '{}_Week {}'.format(schedule.identifier, week_index) + week_sch_id = clean_doe2_string(week_sch_id, RES_CHARS) + week_fields = [] + # check rules that apply for the days of the week + week_fields.extend(_get_week_list(schedule, rule_indices)) + # add extra days (including summer and winter design days) + week_fields.extend(_get_extra_week_fields(schedule)) + week_keywords, week_values = ['TYPE'], [type_text] + for day_type, day_sch in zip(day_types, week_fields): + week_keywords.append('DAYS') + week_values.append(day_type) + week_keywords.append('DAY-SCHEDULES') + week_values.append('"{}"'.format(day_sch)) + week_schedule = generate_inp_string( + week_sch_id, 'WEEK-SCHEDULE', week_keywords, week_values) + return week_schedule, week_sch_id + + def _inp_week_schedule_from_week_list(schedule, week_list, week_index): + """Create an INP string of a week schedule from a list ScheduleDay identifiers. + """ + week_sch_id = '{}_Week {}'.format(schedule.identifier, week_index) + week_sch_id = clean_doe2_string(week_sch_id, RES_CHARS) + week_fields = list(week_list) + week_fields.append(week_fields.pop(0)) # DOE-2 starts week on Monday; not Sunday + week_fields.extend(_get_extra_week_fields(schedule)) + week_keywords, week_values = ['TYPE'], [type_text] + for day_type, day_sch in zip(day_types, week_fields): + week_keywords.append('DAYS') + week_values.append(day_type) + week_keywords.append('DAY-SCHEDULES') + week_values.append('"{}"'.format(day_sch)) + week_schedule = generate_inp_string( + week_sch_id, 'WEEK-SCHEDULE', week_keywords, week_values) + return week_schedule, week_sch_id + + # prepare to create a full Schedule:Year + date_comments = ['start month {}', 'start day {}', 'end month {}', 'end day {}'] + week_schedules = [] + + if schedule.is_single_week: # create the only one week schedule + wk_sch, wk_sch_id = \ + _inp_week_schedule_from_rule_indices(schedule, range(len(schedule)), 1) + week_schedules.append(wk_sch) + yr_wk_s_ids = [wk_sch_id] + yr_wk_dt_range = [[Date(1, 1), Date(12, 31)]] + else: # create a set of week schedules throughout the year + # loop through 365 days of the year to find unique combinations of rules + rules_each_day = [] + for doy in range(1, 366): + rules_on_doy = tuple(i for i, rule in enumerate(schedule._schedule_rules) + if rule.does_rule_apply_doy(doy)) + rules_each_day.append(rules_on_doy) + unique_rule_sets = set(rules_each_day) + # check if any combination yield the same week schedule and remove duplicates + week_tuples = [tuple(_get_week_list(schedule, rule_set)) + for rule_set in unique_rule_sets] + unique_week_tuples = list(set(week_tuples)) + # create the unique week schedules from the combinations of rules + week_sched_ids = [] + for i, week_list in enumerate(unique_week_tuples): + wk_schedule, wk_sch_id = \ + _inp_week_schedule_from_week_list(schedule, week_list, i + 1) + week_schedules.append(wk_schedule) + week_sched_ids.append(wk_sch_id) + # create a dictionary mapping unique rule index lists to week schedule ids + rule_set_map = {} + for rule_i, week_list in zip(unique_rule_sets, week_tuples): + unique_week_i = unique_week_tuples.index(week_list) + rule_set_map[rule_i] = week_sched_ids[unique_week_i] + # loop through all 365 days of the year to find when rules change + yr_wk_s_ids = [] + yr_wk_dt_range = [] + prev_week_sched = None + for doy in range(1, 366): + week_sched = rule_set_map[rules_each_day[doy - 1]] + if week_sched != prev_week_sched: # change to a new rule set + yr_wk_s_ids.append(week_sched) + if doy != 1: + yr_wk_dt_range[-1].append(Date.from_doy(doy - 1)) + yr_wk_dt_range.append([Date.from_doy(doy)]) + else: + yr_wk_dt_range.append([Date(1, 1)]) + prev_week_sched = week_sched + yr_wk_dt_range[-1].append(Date(12, 31)) + + # create the year fields + keywords, values = ['TYPE'], [type_text] + for i, (wk_sch_id, dt_range) in enumerate(zip(yr_wk_s_ids, yr_wk_dt_range)): + end_date = dt_range[1] + thru = 'THRU {} {}'.format(MONTHNAMES[end_date.month - 1].upper(), end_date.day) + keywords.append(thru) + values.append('"{}"'.format(wk_sch_id)) + year_schedule = generate_inp_string(doe2_id, 'SCHEDULE', keywords, values) + return year_schedule, week_schedules + + +def energy_trans_sch_to_transmittance(shade_obj): + """Try to extract the transmittance from the shade energy properties.""" + trans = 0 + trans_sch = shade_obj.properties.energy.transmittance_schedule + if trans_sch is not None: + if trans_sch.is_constant: + try: # assume ScheduleRuleset + trans = trans_sch.default_day_schedule[0] + except AttributeError: # ScheduleFixedInterval + trans = trans_sch.values[0] + else: # not a constant schedule; use the average transmittance + try: # assume ScheduleRuleset + sch_vals = trans_sch.values() + except Exception: # ScheduleFixedInterval + sch_vals = trans_sch.values + trans = sum(sch_vals) / len(sch_vals) + return trans diff --git a/honeybee_doe2/simulation.py b/honeybee_doe2/simulation.py new file mode 100644 index 0000000..8d8bb70 --- /dev/null +++ b/honeybee_doe2/simulation.py @@ -0,0 +1,2 @@ +# coding=utf-8 +"""honeybee-doe2 simulation parameters.""" diff --git a/honeybee_doe2/util.py b/honeybee_doe2/util.py new file mode 100644 index 0000000..3cb644d --- /dev/null +++ b/honeybee_doe2/util.py @@ -0,0 +1,56 @@ +# coding=utf-8 +"""Various utilities used throughout the package.""" + + +def generate_inp_string(u_name, command, keywords, values): + """Get an INP string representation of a DOE-2 object. + + This method is written in a generic way so that it can describe practically + any element of the INP Building Description Language (BDL). + + Args: + u_name: Text for the unique, user-specified name of the object being created. + This must be 32 characters or less and not contain special or non-ASCII + characters. The clean_doe2_string method may be used to convert + strings to a format that is acceptable here. For example, a U-Name + of a space might be "Floor2W ClosedOffice5". + command: Text indicating the type of instruction that the DOE-2 object + executes. Commands are typically in capital letters and examples + include POLYGON, FLOOR, SPACE, EXTERIOR-WALL, WINDOW, CONSTRUCTION, etc. + keywords: A list of text with the same length as the values that denote + the attributes of the DOE-2 object. + values: A list of values with the same length as the keywords that describe + the values of the attributes for the object. + + Returns: + inp_str -- A DOE-2 INP string representing a single object. + """ + space_count = tuple((25 - len(str(n))) for n in keywords) + spc = tuple(s_c * ' ' if s_c > 0 else ' ' for s_c in space_count) + body_str = '\n'.join(' {}{}= {}'.format(kwd, s, val) + for kwd, s, val in zip(keywords, spc, values)) + inp_str = '"{}" = {}\n{}\n ..\n'.format(u_name, command, body_str) + return inp_str + + +def header_comment_minor(header_text): + """Create a header given header_text, which can help organize the INP file contents. + """ + return \ + '$ ---------------------------------------------------------\n' \ + '$ {}\n' \ + '$ ---------------------------------------------------------\n'\ + '\n'.format(header_text) + + +def header_comment_major(header_text): + """Create a header given header_text, which can help organize the INP file contents. + """ + return \ + '$ *********************************************************\n' \ + '$ ** **\n' \ + '$ {}\n' \ + '$ ** **\n' \ + '$ *********************************************************\n'\ + '\n'.format(header_text) + diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index bd138b5..bf0ef1f 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -1,50 +1,25 @@ # coding=utf-8 """Methods to write to inp.""" import math + from ladybug_geometry.geometry2d import Point2D from ladybug_geometry.geometry3d import Vector3D, Point3D, Plane, Face3D from honeybee.typing import clean_doe2_string from honeybee.boundarycondition import Surface from honeybee.facetype import Wall, Floor, RoofCeiling - -from .config import DOE2_TOLERANCE, DOE2_ANGLE_TOL, DOE2_INTERIOR_BCS, \ - GEO_CHARS, RES_CHARS -from load import people_to_inp, lighting_to_inp, equipment_to_inp, \ +from honeybee.room import Room +from honeybee_energy.lib.constructionsets import generic_construction_set + +from .config import DOE2_TOLERANCE, DOE2_ANGLE_TOL, FLOOR_LEVEL_TOL, RECT_WIN_SUBD, \ + DOE2_INTERIOR_BCS, GEO_CHARS, RES_CHARS +from .util import generate_inp_string, header_comment_minor, \ + header_comment_major +from .schedule import energy_trans_sch_to_transmittance +from .load import people_to_inp, lighting_to_inp, equipment_to_inp, \ infiltration_to_inp -def generate_inp_string(u_name, command, keywords, values): - """Get an INP string representation of a DOE-2 object. - - This method is written in a generic way so that it can describe practically - any element of the INP Building Description Language (BDL). - - Args: - u_name: Text for the unique, user-specified name of the object being created. - This must be 32 characters or less and not contain special or non-ASCII - characters. The clean_doe2_string method may be used to convert - strings to a format that is acceptable here. For example, a U-Name - of a space might be "Floor2W ClosedOffice5". - command: Text indicating the type of instruction that the DOE-2 object - executes. Commands are typically in capital letters and examples - include POLYGON, FLOOR, SPACE, EXTERIOR-WALL, WINDOW, CONSTRUCTION, etc. - keywords: A list of text with the same length as the values that denote - the attributes of the DOE-2 object. - values: A list of values with the same length as the keywords that describe - the values of the attributes for the object. - - Returns: - inp_str -- A DOE-2 INP string representing a single object. - """ - space_count = tuple((25 - len(str(n))) for n in keywords) - spc = tuple(s_c * ' ' if s_c > 0 else ' ' for s_c in space_count) - body_str = '\n'.join(' {}{}= {}'.format(kwd, s, val) - for kwd, s, val in zip(keywords, spc, values)) - inp_str = '"{}" = {}\n{}\n ..\n'.format(u_name, command, body_str) - return inp_str - - -def face_3d_to_inp(face_3d, parent_name='HB object', is_shade=False): +def face_3d_to_inp(face_3d, parent_name='HB object'): """Convert a Face3D into a DOE-2 POLYGON string and info to position it in space. In this operation, all holes in the Face3D are ignored since they are not @@ -59,8 +34,6 @@ def face_3d_to_inp(face_3d, parent_name='HB object', is_shade=False): Note that this should ideally have 24 characters or less so that the result complies with the strict 32 character limit of DOE-2 identifiers. - is_shade: Boolean to note whether the location_str needs to be generated - using the conventions for FIXED-SHADE as opposed to WALL, ROOF, FLOOR. Returns: A tuple with two elements. @@ -99,25 +72,6 @@ def face_3d_to_inp(face_3d, parent_name='HB object', is_shade=False): return polygon_str, position_info -def _energy_trans_sch_to_transmittance(shade_obj): - """Try to extract the transmittance from the shade energy properties.""" - trans = 0 - trans_sch = shade_obj.properties.energy.transmittance_schedule - if trans_sch is not None: - if trans_sch.is_constant: - try: # assume ScheduleRuleset - trans = trans_sch.default_day_schedule[0] - except AttributeError: # ScheduleFixedInterval - trans = trans_sch.values[0] - else: # not a constant schedule; use the average transmittance - try: # assume ScheduleRuleset - sch_vals = trans_sch.values() - except Exception: # ScheduleFixedInterval - sch_vals = trans_sch.values - trans = sum(sch_vals) / len(sch_vals) - return trans - - def shade_mesh_to_inp(shade_mesh): """Generate an INP string representation of a ShadeMesh. @@ -138,7 +92,7 @@ def shade_mesh_to_inp(shade_mesh): # set up collector lists and properties for all shades shade_type = 'FIXED-SHADE' if shade_mesh.is_detached else 'BUILDING-SHADE' base_id = clean_doe2_string(shade_mesh.identifier, GEO_CHARS) - trans = _energy_trans_sch_to_transmittance(shade_mesh) + trans = energy_trans_sch_to_transmittance(shade_mesh) keywords = ('SHAPE', 'POLYGON', 'TRANSMITTANCE', 'X-REF', 'Y-REF', 'Z-REF', 'TILT', 'AZIMUTH') shade_polygons, shade_defs = [], [] @@ -178,7 +132,7 @@ def shade_to_inp(shade): shade_polygon, pos_info = face_3d_to_inp(clean_geo, doe2_id) origin, tilt, az = pos_info # create the shade definition, which includes the position information - trans = _energy_trans_sch_to_transmittance(shade) + trans = energy_trans_sch_to_transmittance(shade) keywords = ('SHAPE', 'POLYGON', 'TRANSMITTANCE', 'X-REF', 'Y-REF', 'Z-REF', 'TILT', 'AZIMUTH') values = ('POLYGON', '"{} Plg"', trans, @@ -486,7 +440,7 @@ def model_to_inp( should be excluded from the resulting string. (Default: False). exclude_interior_ceilings: Boolean to note whether interior ceiling Faces should be excluded from the resulting string. (Default: False). - + Usage: .. code-block:: python @@ -514,29 +468,136 @@ def model_to_inp( inp = os.path.join(folders.default_simulation_folder, 'test_file', 'in.inp') write_to_file(inp, inp_str, True) """ - # duplicate model to avoid mutating it as we edit it for energy simulation + # duplicate model to avoid mutating it as we edit it for INP export original_model = model model = model.duplicate() - # scale the model if the units are not feet if model.units != 'Feet': model.convert_to_units('Feet') - # remove degenerate geometry within native DOE-2 tolerance of 0.1 feet + # remove degenerate geometry within native DOE-2 tolerance try: model.remove_degenerate_geometry(DOE2_TOLERANCE) except ValueError: error = 'Failed to remove degenerate Rooms.\nYour Model units system is: {}. ' \ 'Is this correct?'.format(original_model.units) raise ValueError(error) - - # TODO: split all of the Rooms with holes so that they can be translated # convert all of the Aperture geometries to rectangles so they can be translated model.rectangularize_apertures( - subdivision_distance=0.5, max_separation=0.0, + subdivision_distance=RECT_WIN_SUBD, max_separation=0.0, merge_all=True, resolve_adjacency=True ) - - # TODO: reassign stories to the model such that each level has only one polygon - # reset identifiers to make them unique and derived from the display names - model.reset_ids() \ No newline at end of file + model.reset_ids() + + # write the simulation parameters into the string + model_str = ['INPUT ..\n\n'] + model_str.append(header_comment_minor('Abort, Diagnostics')) + model_str.append(header_comment_minor('Global Parameters')) + model_str.append(header_comment_minor('Title, Run Periods, Design Days, Holidays')) + model_str.append(header_comment_minor('Compliance Data')) + model_str.append(header_comment_minor('Site and Building Data')) + + # write all of the schedules + sched_strs = [] + used_day_sched_ids, used_day_count = {}, 1 + all_scheds = model.properties.energy.schedules + for sched in all_scheds: + try: # ScheduleRuleset + year_schedule, week_schedules = sched.to_idf() + if week_schedules is None: # ScheduleConstant + sched_strs.append(year_schedule) + else: # ScheduleYear + # check that day schedules aren't referenced by other model schedules + day_scheds = [] + for day in sched.day_schedules: + if day.identifier not in used_day_sched_ids: + day_scheds.append(day.to_idf(sched.schedule_type_limit)) + used_day_sched_ids[day.identifier] = day + elif day != used_day_sched_ids[day.identifier]: + new_day = day.duplicate() + new_day.identifier = 'Schedule Day {}'.format(used_day_count) + day_scheds.append(new_day.to_idf(sched.schedule_type_limit)) + for i, week_sch in enumerate(week_schedules): + week_schedules[i] = \ + week_sch.replace(day.identifier, new_day.identifier) + used_day_count += 1 + sched_strs.extend([year_schedule] + week_schedules + day_scheds) + except TypeError: # ScheduleFixedInterval + sched_strs.append(sched.to_idf_compact()) + model_str.append(header_comment_minor('Day Schedules')) + model_str.append(header_comment_minor('Week Schedules')) + model_str.append(header_comment_minor('Annual Schedules')) + + # write all of the materials and constructions + materials = [] + construction_strs = [] + dynamic_cons = [] + all_constrs = model.properties.energy.constructions + \ + generic_construction_set.constructions_unique + for constr in set(all_constrs): + try: + materials.extend(constr.materials) + construction_strs.append(constr.to_idf()) + if constr.has_frame: + materials.append(constr.frame) + if constr.has_shade: + if constr.window_construction in all_constrs: + construction_strs.pop(-1) # avoid duplicate specification + if constr.is_switchable_glazing: + materials.append(constr.switched_glass_material) + construction_strs.append(constr.to_shaded_idf()) + elif constr.is_dynamic: + dynamic_cons.append(constr) + except AttributeError: + try: # AirBoundaryConstruction or ShadeConstruction + construction_strs.append(constr.to_idf()) # AirBoundaryConstruction + except TypeError: + pass # ShadeConstruction; no need to write it + model_str.append(header_comment_minor('Materials / Layers / Constructions')) + model_str.append(header_comment_minor('Glass Types')) + model_str.append(header_comment_minor('Door Construction')) + + # group rooms in the model such that each level has only one polygon + grouped_rooms = Room.group_by_floor_height(model.rooms, FLOOR_LEVEL_TOL) + + model_str.append(header_comment_minor('Polygons')) + model_str.append(header_comment_minor('Wall Parameters')) + model_str.append(header_comment_minor('Fixed and Building Shades')) + model_str.append(header_comment_minor('Misc Cost Related Objects')) + model_str.append(header_comment_major('Performance Curves')) + model_str.append(header_comment_major('Floors / Spaces / Walls / Windows / Doors')) + + # assign HVAC systems given the specified hvac_mapping + model_str.append(header_comment_major('Electric & Fuel Meters')) + for meter in ('Electric Meters', 'Fuel Meters', 'Master Meters'): + model_str.append(header_comment_minor(meter)) + model_str.append(header_comment_major('HVAC Circulation Loops / Plant Equipment')) + hvac_comp_types = ( + 'Pumps', 'Heat Exchangers', 'Circulation Loops', 'Chillers', 'Boilers', + 'Domestic Water Heaters', 'Heat Rejection', 'Tower Free Cooling', + 'Photovoltaic Modules', 'Electric Generators', 'Thermal Storage', + 'Ground Loop Heat Exchangers', 'Compliance DHW (residential dwelling units)') + for comp in hvac_comp_types: + model_str.append(header_comment_minor(comp)) + model_str.append(header_comment_major('Steam & Chilled Water Meters')) + model_str.append(header_comment_minor('Steam Meters')) + model_str.append(header_comment_minor('Chilled Water Meters')) + model_str.append(header_comment_major('HVAC Systems / Zones')) + + + # provide a few last comment headers and end the file + model_str.append(header_comment_major('Metering & Misc HVAC')) + model_str.append(header_comment_minor('Equipment Controls')) + model_str.append(header_comment_minor('Load Management')) + model_str.append(header_comment_major('Utility Rates')) + for rate in ('Ratchets', 'Block Charges', 'Utility Rates'): + model_str.append(header_comment_minor(rate)) + model_str.append(header_comment_major('Output Reporting')) + report_types = ( + 'Loads Non-Hourly Reporting', 'Systems Non-Hourly Reporting', + 'Plant Non-Hourly Reporting', 'Economics Non-Hourly Reporting', + 'Hourly Reporting', 'THE END') + for report in report_types: + model_str.append(header_comment_minor(report)) + model_str = ['END ..\nCOMPUTE ..\nSTOP ..\n'] + return ''.join(model_str) diff --git a/tests/schedule_test.py b/tests/schedule_test.py new file mode 100644 index 0000000..f66d301 --- /dev/null +++ b/tests/schedule_test.py @@ -0,0 +1,227 @@ +# coding=utf-8 +from ladybug.dt import Time, Date +from honeybee_energy.schedule.day import ScheduleDay +from honeybee_energy.schedule.rule import ScheduleRule +from honeybee_energy.schedule.ruleset import ScheduleRuleset +import honeybee_energy.lib.scheduletypelimits as schedule_types + +from honeybee_doe2.schedule import schedule_day_to_inp, schedule_ruleset_to_inp + + +def test_schedule_day_to_inp(): + """Test ScheduleDay to_inp.""" + simple_office = ScheduleDay('Simple Office Occupancy', [0, 1, 0], + [Time(0, 0), Time(9, 0), Time(17, 0)]) + inp_str = schedule_day_to_inp(simple_office) + assert inp_str == \ + '"Simple Office Occupancy" = DAY-SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' HOURS = (1, 9)\n' \ + ' VALUES = (0.0)\n' \ + ' HOURS = (10, 17)\n' \ + ' VALUES = (1.0)\n' \ + ' HOURS = (18, 24)\n' \ + ' VALUES = (0.0)\n' \ + ' ..\n' + + simple_office2 = ScheduleDay( + 'Simple Office Occupancy', [1, 0.5, 1, 0.5, 1], + [Time(0, 0), Time(6, 0), Time(12, 0), Time(16, 0), Time(20, 0)]) + inp_str = schedule_day_to_inp(simple_office2) + assert inp_str == \ + '"Simple Office Occupancy" = DAY-SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' HOURS = (1, 6)\n' \ + ' VALUES = (1.0)\n' \ + ' HOURS = (7, 12)\n' \ + ' VALUES = (0.5)\n' \ + ' HOURS = (13, 16)\n' \ + ' VALUES = (1.0)\n' \ + ' HOURS = (17, 20)\n' \ + ' VALUES = (0.5)\n' \ + ' HOURS = (21, 24)\n' \ + ' VALUES = (1.0)\n' \ + ' ..\n' + + equest_sample = [0, 0, 0, 0, 0, 0, 0, 0, 0.3,0.6,0.8, + 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0] + equest_sample_sch = ScheduleDay.from_values_at_timestep( + 'eQuest Sample Sch Day', equest_sample) + inp_str = schedule_day_to_inp(equest_sample_sch) + assert inp_str == \ + '"eQuest Sample Sch Day" = DAY-SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' HOURS = (1, 8)\n' \ + ' VALUES = (0.0)\n' \ + ' HOURS = (9, 11)\n' \ + ' VALUES = (0.3, 0.6, 0.8)\n' \ + ' HOURS = (12, 18)\n' \ + ' VALUES = (1.0)\n' \ + ' HOURS = (19, 24)\n' \ + ' VALUES = (0.0)\n' \ + ' ..\n' + + +def test_schedule_day_to_inp_start_end_change(): + """Test ScheduleDay to_inp with values that change in the first and last hour.""" + hourly_vals_from_ep = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.166, 0.33, 0.5, + 0.66, 0.833, 1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, + 0.75, 1.0, 0.75, 0.5, 0.25, 0.0] + complex_office = ScheduleDay.from_values_at_timestep( + 'Complex Office Occupancy', hourly_vals_from_ep) + inp_str = schedule_day_to_inp(complex_office) + assert inp_str == \ + '"Complex Office Occupancy" = DAY-SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' HOURS = (1, 6)\n' \ + ' VALUES = (0.0)\n' \ + ' HOURS = (7, 24)\n' \ + ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833, 1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5, 0.25, 0.0)\n' \ + ' ..\n' + + hourly_vals_from_ep[-2] = 0 + complex_office = ScheduleDay.from_values_at_timestep( + 'Complex Office Occupancy', hourly_vals_from_ep) + inp_str = schedule_day_to_inp(complex_office) + assert inp_str == \ + '"Complex Office Occupancy" = DAY-SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' HOURS = (1, 6)\n' \ + ' VALUES = (0.0)\n' \ + ' HOURS = (7, 22)\n' \ + ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833, 1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5)\n' \ + ' HOURS = (23, 24)\n' \ + ' VALUES = (0.0)\n' \ + ' ..\n' + + hourly_vals_from_ep[1] = 1.0 + complex_office = ScheduleDay.from_values_at_timestep( + 'Complex Office Occupancy', hourly_vals_from_ep) + inp_str = schedule_day_to_inp(complex_office) + assert inp_str == \ + '"Complex Office Occupancy" = DAY-SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' HOURS = (1, 2)\n' \ + ' VALUES = (0.0, 1.0)\n' \ + ' HOURS = (3, 6)\n' \ + ' VALUES = (0.0)\n' \ + ' HOURS = (7, 22)\n' \ + ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833, 1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5)\n' \ + ' HOURS = (23, 24)\n' \ + ' VALUES = (0.0)\n' \ + ' ..\n' + + hourly_vals_from_ep[0] = 1.0 + hourly_vals_from_ep[1] = 0.0 + complex_office = ScheduleDay.from_values_at_timestep( + 'Complex Office Occupancy', hourly_vals_from_ep) + inp_str = schedule_day_to_inp(complex_office) + assert inp_str == \ + '"Complex Office Occupancy" = DAY-SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' HOURS = (1, 2)\n' \ + ' VALUES = (1.0, 0.0)\n' \ + ' HOURS = (3, 6)\n' \ + ' VALUES = (0.0)\n' \ + ' HOURS = (7, 22)\n' \ + ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833, 1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5)\n' \ + ' HOURS = (23, 24)\n' \ + ' VALUES = (0.0)\n' \ + ' ..\n' + + +def test_schedule_ruleset_to_inp(): + """Test the ScheduleRuleset to_inp method.""" + weekday_office = ScheduleDay('Weekday Office Occupancy', [0, 1, 0], + [Time(0, 0), Time(9, 0), Time(17, 0)]) + saturday_office = ScheduleDay('Saturday Office Occupancy', [0, 0.25, 0], + [Time(0, 0), Time(9, 0), Time(17, 0)]) + sunday_office = ScheduleDay('Sunday Office Occupancy', [0]) + sat_rule = ScheduleRule(saturday_office, apply_saturday=True) + sun_rule = ScheduleRule(sunday_office, apply_sunday=True) + summer_office = ScheduleDay('Summer Office Occupancy', [0, 1, 0.25], + [Time(0, 0), Time(6, 0), Time(22, 0)]) + winter_office = ScheduleDay('Winter Office Occupancy', [0]) + schedule = ScheduleRuleset('Office Occupancy', weekday_office, + [sat_rule, sun_rule], schedule_types.fractional, + sunday_office, summer_office, winter_office) + + inp_yr_str, inp_week_strs = schedule_ruleset_to_inp(schedule) + + assert inp_yr_str == \ + '"Office Occupancy" = SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' THRU DEC 31 = "Office Occupancy_Week 1"\n' \ + ' ..\n' + assert len(inp_week_strs) == 1 + assert inp_week_strs[0] == \ + '"Office Occupancy_Week 1" = WEEK-SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' DAYS = (MON)\n' \ + ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ + ' DAYS = (TUE)\n' \ + ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ + ' DAYS = (WED)\n' \ + ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ + ' DAYS = (THU)\n' \ + ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ + ' DAYS = (FRI)\n' \ + ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ + ' DAYS = (SAT)\n' \ + ' DAY-SCHEDULES = "Saturday Office Occupancy"\n' \ + ' DAYS = (SUN)\n' \ + ' DAY-SCHEDULES = "Sunday Office Occupancy"\n' \ + ' DAYS = (HOL)\n' \ + ' DAY-SCHEDULES = "Sunday Office Occupancy"\n' \ + ' DAYS = (HDD)\n' \ + ' DAY-SCHEDULES = "Winter Office Occupancy"\n' \ + ' DAYS = (CDD)\n' \ + ' DAY-SCHEDULES = "Summer Office Occupancy"\n' \ + ' ..\n' + + +def test_schedule_ruleset_to_inp_date_range(): + """Test the ScheduleRuleset to_inp method with schedules over a date range.""" + weekday_school = ScheduleDay('Weekday School Year', [0.1, 1, 0.1], + [Time(0, 0), Time(8, 0), Time(17, 0)]) + weekend_school = ScheduleDay('Weekend School Year', [0.1]) + weekday_summer = ScheduleDay('Weekday Summer', [0, 0.5, 0], + [Time(0, 0), Time(9, 0), Time(17, 0)]) + weekend_summer = ScheduleDay('Weekend Summer', [0]) + + summer_weekday_rule = ScheduleRule( + weekday_summer, start_date=Date(7, 1), end_date=Date(9, 1)) + summer_weekday_rule.apply_weekday = True + summer_weekend_rule = ScheduleRule( + weekend_summer, start_date=Date(7, 1), end_date=Date(9, 1)) + summer_weekend_rule.apply_weekend = True + school_weekend_rule = ScheduleRule(weekend_school) + school_weekend_rule.apply_weekend = True + + summer_design = ScheduleDay('School Summer Design', [0, 1, 0.25], + [Time(0, 0), Time(6, 0), Time(18, 0)]) + winter_design = ScheduleDay('School Winter Design', [0]) + + all_rules = [summer_weekday_rule, summer_weekend_rule, school_weekend_rule] + school_schedule = ScheduleRuleset( + 'School Occupancy', weekday_school, all_rules, schedule_types.fractional, + None, summer_design, winter_design) + + inp_yr_str, inp_week_strs = schedule_ruleset_to_inp(school_schedule) + + print(inp_yr_str) + assert inp_yr_str == \ + '"School Occupancy" = SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' THRU JUN 30 = "School Occupancy_Week 1"\n' \ + ' THRU SEP 1 = "School Occupancy_Week 2"\n' \ + ' THRU DEC 31 = "School Occupancy_Week 1"\n' \ + ' ..\n' \ + or inp_yr_str == \ + '"School Occupancy" = SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' THRU JUN 30 = "School Occupancy_Week 2"\n' \ + ' THRU SEP 1 = "School Occupancy_Week 1"\n' \ + ' THRU DEC 31 = "School Occupancy_Week 2"\n' \ + ' ..\n' + assert len(inp_week_strs) == 2 From cc07eccf8ecd114622502f167980c680d6881fc6 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Wed, 24 Apr 2024 16:53:51 -0700 Subject: [PATCH 04/27] feat(construction): Add translators for constructions and materials --- honeybee_doe2/_extend_honeybee.py | 10 ++++ honeybee_doe2/config.py | 1 + honeybee_doe2/construction.py | 96 +++++++++++++++++++++++++++++++ honeybee_doe2/schedule.py | 2 - honeybee_doe2/util.py | 43 ++++++++++++++ honeybee_doe2/writer.py | 91 +++++++++++++++-------------- 6 files changed, 197 insertions(+), 46 deletions(-) diff --git a/honeybee_doe2/_extend_honeybee.py b/honeybee_doe2/_extend_honeybee.py index 2668ae8..1147954 100644 --- a/honeybee_doe2/_extend_honeybee.py +++ b/honeybee_doe2/_extend_honeybee.py @@ -19,3 +19,13 @@ aperture_writer.inp = aperture_to_inp door_writer.inp = door_to_inp shade_mesh_writer.inp = shade_mesh_to_inp + + +# import the modules that extend honeybee-energy objects +from honeybee_energy.schedule.day import ScheduleDay +from honeybee_energy.schedule.ruleset import ScheduleRuleset +from .schedule import schedule_day_to_inp, schedule_ruleset_to_inp + +# add the methods to the honeybee-energy classes +ScheduleDay.to_inp = schedule_day_to_inp +ScheduleRuleset.to_inp = schedule_ruleset_to_inp diff --git a/honeybee_doe2/config.py b/honeybee_doe2/config.py index b6f27a1..beb2fdd 100644 --- a/honeybee_doe2/config.py +++ b/honeybee_doe2/config.py @@ -6,5 +6,6 @@ FLOOR_LEVEL_TOL = 0.1 # tolerance for grouping Rooms by floor elevations in Feet RECT_WIN_SUBD = 0.5 # subdivision distance to rectangularize windows in Feet DOE2_INTERIOR_BCS = ('Surface', 'Adiabatic', 'OtherSideTemperature') +MIN_LAYER_THICKNESS = 0.003 # the minimum thickness for a material to be valid in meters GEO_CHARS = 24 # number of original characters used in names of geometry RES_CHARS = 30 # number of characters used in names of resources (constructions, etc.) diff --git a/honeybee_doe2/construction.py b/honeybee_doe2/construction.py index 39fba67..27f832d 100644 --- a/honeybee_doe2/construction.py +++ b/honeybee_doe2/construction.py @@ -1 +1,97 @@ """honeybee-inp construction translators.""" +from __future__ import division + +from ladybug.datatype.uvalue import UValue +from ladybug.datatype.rvalue import RValue +from ladybug.datatype.distance import Distance +from honeybee.typing import clean_doe2_string +from honeybee_energy.material.opaque import EnergyMaterialNoMass + +from .config import RES_CHARS, MIN_LAYER_THICKNESS +from .util import generate_inp_string, generate_inp_string_list_format + +# dictionary to map between E+ and DOE-2 roughness types +ROUGHNESS_MAP = { + 'VeryRough': 1, 'Rough': 2, 'MediumRough': 3, + 'MediumSmooth': 4, 'Smooth': 5, 'VerySmooth': 6 +} + + +def opaque_material_to_inp(material): + """Convert an EnergyMaterial or EnergyMaterialNoMass into a MATERIAL INP string. + + Note that EnergyMaterials that are below a certain thickness will be automatically + converted to No Mass materials for compatibility with DOE-2. Also note that + this does not work for any materials that can be a part of a window constructions. + """ + doe2_id = clean_doe2_string(material.identifier, RES_CHARS) + # check if the material should be translated as a no mass material + if isinstance(material, EnergyMaterialNoMass) or \ + material.thickness < MIN_LAYER_THICKNESS: + r_val = RValue([material.r_value], 'h-ft2-F/Btu', 'm2-K/W')[0] + keywords = ('TYPE', 'RESISTANCE') + values = ('RESISTANCE', r_val) + return generate_inp_string(doe2_id, 'MATERIAL', keywords, values) + # write out detailed properties for the material + thickness = round(Distance([material.thickness], 'ft', 'm')[0], 3) + conduct = round(material.conductivity * 0.578176, 3) # convert to BTU/h-ft-F + density = round(material.density / 16.018, 3) # convert to lb/ft3 + spec_en = round(material.specific_heat * 0.0002388459, 3) # convert to BTU/lb-F + keywords = ('TYPE', 'THICKNESS', 'CONDUCTIVITY', 'DENSITY', 'SPECIFIC-HEAT') + values = ('PROPERTIES', thickness, conduct, density, spec_en) + return generate_inp_string(doe2_id, 'MATERIAL', keywords, values) + + +def opaque_construction_to_inp(construction): + """Convert an OpaqueConstruction into a CONSTRUCTION INP string. + + This will include both the LAYERS definition as well as the CONSTRUCTION but + it does NOT include the constituent MATERIAL definitions and their properties. + """ + doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) + # create the specification of material layers + layer_id = '{}_l'.format(doe2_id) + layers = [clean_doe2_string(mat, RES_CHARS) for mat in construction.layers] + layer_str = generate_inp_string_list_format( + layer_id, 'LAYERS', ['MATERIAL'], [layers]) + # create the construction specification + roughness = ROUGHNESS_MAP[construction.materials[0].roughness] + sol_absorb = 1 - construction.outside_solar_reflectance + keywords = ('TYPE', 'ABSORPTANCE', 'ROUGHNESS', 'LAYERS') + values = ('LAYERS', sol_absorb, roughness, layer_id) + constr_str = generate_inp_string(doe2_id, 'CONSTRUCTION', keywords, values) + return ''.join((layer_str, constr_str)) + + +def window_construction_to_inp(construction): + """Convert a WindowConstruction (or its variants) into a GLASS-TYPE INP string.""" + doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) + shading_coef = construction.shgc / 0.87 + glass_cond = UValue([construction.u_factor], 'Btu/h-ft2-F', 'W/m2-K')[0] + keywords = ('TYPE', 'SHADING-COEF', 'GLASS-CONDUCT') + values = ('SHADING-COEF', shading_coef, glass_cond) + return generate_inp_string(doe2_id, 'GLASS-TYPE', keywords, values) + + +def door_construction_to_inp(construction): + """Convert an OpaqueConstruction or WindowConstruction to a CONSTRUCTION INP string. + + This translation pathway always uses a NoMass U-VALUE Construction. + """ + doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) + constr_cond = UValue([construction.u_factor], 'Btu/h-ft2-F', 'W/m2-K')[0] + keywords = ('TYPE', 'U-VALUE') + values = ('U-VALUE', constr_cond) + return generate_inp_string(doe2_id, 'CONSTRUCTION', keywords, values) + + +def air_construction_to_inp(construction): + """Convert an AirBoundaryConstruction to a CONSTRUCTION INP string. + + This translation pathway always uses a NoMass U-VALUE Construction. + """ + doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) + constr_cond = UValue([6], 'Btu/h-ft2-F', 'W/m2-K')[0] + keywords = ('TYPE', 'U-VALUE') + values = ('U-VALUE', constr_cond) + return generate_inp_string(doe2_id, 'CONSTRUCTION', keywords, values) diff --git a/honeybee_doe2/schedule.py b/honeybee_doe2/schedule.py index 3224bb1..4b1bc2e 100644 --- a/honeybee_doe2/schedule.py +++ b/honeybee_doe2/schedule.py @@ -175,9 +175,7 @@ def _inp_week_schedule_from_week_list(schedule, week_list, week_index): return week_schedule, week_sch_id # prepare to create a full Schedule:Year - date_comments = ['start month {}', 'start day {}', 'end month {}', 'end day {}'] week_schedules = [] - if schedule.is_single_week: # create the only one week schedule wk_sch, wk_sch_id = \ _inp_week_schedule_from_rule_indices(schedule, range(len(schedule)), 1) diff --git a/honeybee_doe2/util.py b/honeybee_doe2/util.py index 3cb644d..1261f84 100644 --- a/honeybee_doe2/util.py +++ b/honeybee_doe2/util.py @@ -33,6 +33,49 @@ def generate_inp_string(u_name, command, keywords, values): return inp_str +def generate_inp_string_list_format(u_name, command, keywords, values): + """Get an INP string of a DOE-2 object with nicer formatting for list values. + + This method will process any values that are a list or tuple and format them + such that they are indented and more readable. This method is written in a + generic way so that it can describe practically any element of the INP Building + Description Language (BDL). + + Args: + u_name: Text for the unique, user-specified name of the object being created. + This must be 32 characters or less and not contain special or non-ASCII + characters. The clean_doe2_string method may be used to convert + strings to a format that is acceptable here. For example, a U-Name + of a space might be "Floor2W ClosedOffice5". + command: Text indicating the type of instruction that the DOE-2 object + executes. Commands are typically in capital letters and examples + include POLYGON, FLOOR, SPACE, EXTERIOR-WALL, WINDOW, CONSTRUCTION, etc. + keywords: A list of text with the same length as the values that denote + the attributes of the DOE-2 object. + values: A list of values with the same length as the keywords that describe + the values of the attributes for the object. The items in this list + can be list themselves, in which case they will be translated with + nice indented formatting. + + Returns: + inp_str -- A DOE-2 INP string representing a single object. + """ + space_count = tuple((25 - len(str(n))) for n in keywords) + spc = tuple(s_c * ' ' if s_c > 0 else ' ' for s_c in space_count) + body_strs = [] + for kwd, s, val in zip(keywords, spc, values): + if isinstance(val, (list, tuple)): + body_strs.append(' {}{}= ('.format(kwd, s, val)) + for v in val: + body_strs.append(' {},'.format(v)) + body_strs.append(' )') + else: + body_strs.append(' {}{}= {}'.format(kwd, s, val)) + body_str = '\n'.join(body_strs) + inp_str = '"{}" = {}\n{}\n ..\n'.format(u_name, command, body_str) + return inp_str + + def header_comment_minor(header_text): """Create a header given header_text, which can help organize the INP file contents. """ diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index bf0ef1f..1f6e0e7 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -8,12 +8,16 @@ from honeybee.boundarycondition import Surface from honeybee.facetype import Wall, Floor, RoofCeiling from honeybee.room import Room +from honeybee_energy.construction.opaque import OpaqueConstruction +from honeybee_energy.construction.air import AirBoundaryConstruction from honeybee_energy.lib.constructionsets import generic_construction_set from .config import DOE2_TOLERANCE, DOE2_ANGLE_TOL, FLOOR_LEVEL_TOL, RECT_WIN_SUBD, \ DOE2_INTERIOR_BCS, GEO_CHARS, RES_CHARS from .util import generate_inp_string, header_comment_minor, \ header_comment_major +from .construction import opaque_material_to_inp, opaque_construction_to_inp, \ + window_construction_to_inp, door_construction_to_inp, air_construction_to_inp from .schedule import energy_trans_sch_to_transmittance from .load import people_to_inp, lighting_to_inp, equipment_to_inp, \ infiltration_to_inp @@ -183,7 +187,7 @@ def door_to_inp(door): # create the aperture definition doe2_id = clean_doe2_string(door.identifier, GEO_CHARS) - constr_o_name = door.properties.energy.construction.display_name + constr_o_name = door.properties.energy.construction.identifier constr = clean_doe2_string(constr_o_name, RES_CHARS) keywords = ('X', 'Y', 'WIDTH', 'HEIGHT', 'CONSTRUCTION') values = (min_2d.x, min_2d.y, width, height, constr) @@ -233,7 +237,7 @@ def aperture_to_inp(aperture): # create the aperture definition doe2_id = clean_doe2_string(aperture.identifier, GEO_CHARS) - constr_o_name = aperture.properties.energy.construction.display_name + constr_o_name = aperture.properties.energy.construction.identifier constr = clean_doe2_string(constr_o_name, RES_CHARS) keywords = ('X', 'Y', 'WIDTH', 'HEIGHT', 'GLASS-TYPE') values = (min_2d.x, min_2d.y, width, height, constr) @@ -283,7 +287,7 @@ def face_to_inp(face, space_origin=Point3D(0, 0, 0)): origin = face_origin - space_origin # create the face definition, which includes the position info - constr_o_name = face.properties.energy.construction.display_name + constr_o_name = face.properties.energy.construction.identifier constr = clean_doe2_string(constr_o_name, RES_CHARS) keywords = ['POLYGON', 'CONSTRUCTION', 'TILT', 'AZIMUTH', 'X', 'Y', 'Z'] values = ['"{} Plg"'.format(doe2_id), constr, tilt, az, origin.x, origin.y, origin.z] @@ -498,64 +502,63 @@ def model_to_inp( model_str.append(header_comment_minor('Site and Building Data')) # write all of the schedules - sched_strs = [] + all_day_scheds, all_week_scheds, all_year_scheds = [], [], [] used_day_sched_ids, used_day_count = {}, 1 all_scheds = model.properties.energy.schedules for sched in all_scheds: - try: # ScheduleRuleset - year_schedule, week_schedules = sched.to_idf() - if week_schedules is None: # ScheduleConstant - sched_strs.append(year_schedule) - else: # ScheduleYear - # check that day schedules aren't referenced by other model schedules - day_scheds = [] - for day in sched.day_schedules: - if day.identifier not in used_day_sched_ids: - day_scheds.append(day.to_idf(sched.schedule_type_limit)) - used_day_sched_ids[day.identifier] = day - elif day != used_day_sched_ids[day.identifier]: - new_day = day.duplicate() - new_day.identifier = 'Schedule Day {}'.format(used_day_count) - day_scheds.append(new_day.to_idf(sched.schedule_type_limit)) - for i, week_sch in enumerate(week_schedules): - week_schedules[i] = \ - week_sch.replace(day.identifier, new_day.identifier) - used_day_count += 1 - sched_strs.extend([year_schedule] + week_schedules + day_scheds) - except TypeError: # ScheduleFixedInterval - sched_strs.append(sched.to_idf_compact()) + if sched.__class__.__name__ == 'ScheduleRuleset': + year_schedule, week_schedules = sched.to_inp() + # check that day schedules aren't referenced by other model schedules + day_scheds = [] + for day in sched.day_schedules: + if day.identifier not in used_day_sched_ids: + day_scheds.append(day.to_inp(sched.schedule_type_limit)) + used_day_sched_ids[day.identifier] = day + elif day != used_day_sched_ids[day.identifier]: + new_day = day.duplicate() + new_day.identifier = 'Schedule Day {}'.format(used_day_count) + day_scheds.append(new_day.to_inp(sched.schedule_type_limit)) + for i, week_sch in enumerate(week_schedules): + old_day_id = clean_doe2_string(day.identifier, RES_CHARS) + new_day_id = clean_doe2_string(new_day.identifier, RES_CHARS) + week_schedules[i] = week_sch.replace(old_day_id, new_day_id) + used_day_count += 1 + all_day_scheds.extend(day_scheds) + all_week_scheds.extend(week_schedules) + all_year_scheds.append(year_schedule) + else: # ScheduleFixedInterval + pass + # TODO: Add translators for ScheduleFixedInterval model_str.append(header_comment_minor('Day Schedules')) + model_str.extend(all_day_scheds) model_str.append(header_comment_minor('Week Schedules')) + model_str.extend(all_week_scheds) model_str.append(header_comment_minor('Annual Schedules')) + model_str.extend(all_year_scheds) # write all of the materials and constructions + window_constructions = model.properties.energy.aperture_constructions() + door_constructions = model.properties.energy.door_constructions() + drc_ids = set([con.identifier for con in door_constructions]) materials = [] construction_strs = [] - dynamic_cons = [] all_constrs = model.properties.energy.constructions + \ generic_construction_set.constructions_unique for constr in set(all_constrs): - try: + if isinstance(constr, OpaqueConstruction) and constr.identifier not in drc_ids: materials.extend(constr.materials) - construction_strs.append(constr.to_idf()) - if constr.has_frame: - materials.append(constr.frame) - if constr.has_shade: - if constr.window_construction in all_constrs: - construction_strs.pop(-1) # avoid duplicate specification - if constr.is_switchable_glazing: - materials.append(constr.switched_glass_material) - construction_strs.append(constr.to_shaded_idf()) - elif constr.is_dynamic: - dynamic_cons.append(constr) - except AttributeError: - try: # AirBoundaryConstruction or ShadeConstruction - construction_strs.append(constr.to_idf()) # AirBoundaryConstruction - except TypeError: - pass # ShadeConstruction; no need to write it + construction_strs.append(opaque_construction_to_inp(constr)) + elif isinstance(constr, AirBoundaryConstruction): + construction_strs.append(air_construction_to_inp(constr)) model_str.append(header_comment_minor('Materials / Layers / Constructions')) + model_str.extend([opaque_material_to_inp(mat) for mat in set(materials)]) + model_str.extend(construction_strs) model_str.append(header_comment_minor('Glass Types')) + for w_con in window_constructions: + model_str.append(window_construction_to_inp(w_con)) model_str.append(header_comment_minor('Door Construction')) + for dr_con in door_constructions: + model_str.append(door_construction_to_inp(dr_con)) # group rooms in the model such that each level has only one polygon grouped_rooms = Room.group_by_floor_height(model.rooms, FLOOR_LEVEL_TOL) From d039fc04ab456cfea7917cb25146d08f500be323 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 25 Apr 2024 15:29:41 -0700 Subject: [PATCH 05/27] feat(simulation): Add classes to handle simulation parameters --- honeybee_doe2/_extend_honeybee.py | 22 ++- honeybee_doe2/cli/translate.py | 24 ++- honeybee_doe2/construction.py | 16 +- honeybee_doe2/load.py | 2 + honeybee_doe2/schedule.py | 2 + honeybee_doe2/simulation.py | 265 ++++++++++++++++++++++++++++++ honeybee_doe2/util.py | 1 + honeybee_doe2/writer.py | 37 ++--- tests/construction_test.py | 167 +++++++++++++++++++ tests/simulation_test.py | 88 ++++++++++ 10 files changed, 582 insertions(+), 42 deletions(-) create mode 100644 tests/construction_test.py create mode 100644 tests/simulation_test.py diff --git a/honeybee_doe2/_extend_honeybee.py b/honeybee_doe2/_extend_honeybee.py index 1147954..40b4a07 100644 --- a/honeybee_doe2/_extend_honeybee.py +++ b/honeybee_doe2/_extend_honeybee.py @@ -1,5 +1,4 @@ # coding=utf-8 - # import all of the modules for writing geometry to INP import honeybee.writer.shademesh as shade_mesh_writer import honeybee.writer.door as door_writer @@ -24,8 +23,29 @@ # import the modules that extend honeybee-energy objects from honeybee_energy.schedule.day import ScheduleDay from honeybee_energy.schedule.ruleset import ScheduleRuleset +from honeybee_energy.material.opaque import EnergyMaterial, EnergyMaterialNoMass, \ + EnergyMaterialVegetation +from honeybee_energy.construction.opaque import OpaqueConstruction +from honeybee_energy.construction.window import WindowConstruction +from honeybee_energy.construction.windowshade import WindowConstructionShade +from honeybee_energy.construction.dynamic import WindowConstructionDynamic +from honeybee_energy.construction.air import AirBoundaryConstruction +from honeybee_energy.simulation.runperiod import RunPeriod + from .schedule import schedule_day_to_inp, schedule_ruleset_to_inp +from .construction import opaque_material_to_inp, opaque_construction_to_inp, \ + window_construction_to_inp, air_construction_to_inp +from .simulation import run_period_to_inp # add the methods to the honeybee-energy classes ScheduleDay.to_inp = schedule_day_to_inp ScheduleRuleset.to_inp = schedule_ruleset_to_inp +EnergyMaterial.to_inp = opaque_material_to_inp +EnergyMaterialNoMass.to_inp = opaque_material_to_inp +EnergyMaterialVegetation.to_inp = opaque_material_to_inp +OpaqueConstruction.to_inp = opaque_construction_to_inp +WindowConstruction.to_inp = window_construction_to_inp +WindowConstructionShade.to_inp = window_construction_to_inp +WindowConstructionDynamic.to_inp = window_construction_to_inp +AirBoundaryConstruction.to_inp = air_construction_to_inp +RunPeriod.to_inp = run_period_to_inp diff --git a/honeybee_doe2/cli/translate.py b/honeybee_doe2/cli/translate.py index 7a5b4ef..30747d7 100644 --- a/honeybee_doe2/cli/translate.py +++ b/honeybee_doe2/cli/translate.py @@ -8,6 +8,8 @@ from ladybug.futil import write_to_file_by_name from honeybee.model import Model +from honeybee_doe2.simulation import SimulationPar + _logger = logging.getLogger(__name__) @@ -20,7 +22,7 @@ def translate(): @click.argument('model-file', type=click.Path( exists=True, file_okay=True, dir_okay=False, resolve_path=True)) @click.option( - '--sim-par-json', '-sp', help='Full path to a honeybee-doe2 SimulationParameter ' + '--sim-par-json', '-sp', help='Full path to a honeybee-doe2 SimulationPar ' 'JSON that describes all of the settings for the simulation. If unspecified, ' 'default parameters will be generated.', default=None, show_default=True, type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True)) @@ -64,24 +66,20 @@ def model_to_inp( Args: model_file: Full path to a Honeybee Model file (HBJSON or HBpkl).""" try: - # load simulation parameters or generate default ones - #if sim_par_json is not None: - # with open(sim_par_json) as json_file: - # data = json.load(json_file) - # sim_par = SimulationParameter.from_dict(data) - #else: - # sim_par = SimulationParameter() - # sim_par_str = sim_par.to_idf() - sim_par_str = '' + # load simulation parameters if specified + sim_par = None + if sim_par_json is not None: + with open(sim_par_json) as json_file: + data = json.load(json_file) + sim_par = SimulationPar.from_dict(data) # re-serialize the Model to Python model = Model.from_file(model_file) x_int_w = not include_interior_walls x_int_c = not include_interior_ceilings - # create the strings for simulation parameters and model - model_str = model.to.inp(model, hvac_mapping, x_int_w, x_int_c) - inp_str = '\n\n'.join([sim_par_str, model_str]) + # create the strings for the model + inp_str = model.to.inp(model, sim_par, hvac_mapping, x_int_w, x_int_c) # write out the INP file if folder is not None and name is not None: diff --git a/honeybee_doe2/construction.py b/honeybee_doe2/construction.py index 27f832d..c3a7ca1 100644 --- a/honeybee_doe2/construction.py +++ b/honeybee_doe2/construction.py @@ -28,12 +28,12 @@ def opaque_material_to_inp(material): # check if the material should be translated as a no mass material if isinstance(material, EnergyMaterialNoMass) or \ material.thickness < MIN_LAYER_THICKNESS: - r_val = RValue([material.r_value], 'h-ft2-F/Btu', 'm2-K/W')[0] + r_val = RValue().to_unit([material.r_value], 'h-ft2-F/Btu', 'm2-K/W')[0] keywords = ('TYPE', 'RESISTANCE') - values = ('RESISTANCE', r_val) + values = ('RESISTANCE', round(r_val, 3)) return generate_inp_string(doe2_id, 'MATERIAL', keywords, values) # write out detailed properties for the material - thickness = round(Distance([material.thickness], 'ft', 'm')[0], 3) + thickness = round(Distance().to_unit([material.thickness], 'ft', 'm')[0], 3) conduct = round(material.conductivity * 0.578176, 3) # convert to BTU/h-ft-F density = round(material.density / 16.018, 3) # convert to lb/ft3 spec_en = round(material.specific_heat * 0.0002388459, 3) # convert to BTU/lb-F @@ -67,9 +67,9 @@ def window_construction_to_inp(construction): """Convert a WindowConstruction (or its variants) into a GLASS-TYPE INP string.""" doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) shading_coef = construction.shgc / 0.87 - glass_cond = UValue([construction.u_factor], 'Btu/h-ft2-F', 'W/m2-K')[0] + glass_cond = UValue().to_unit([construction.u_factor], 'Btu/h-ft2-F', 'W/m2-K')[0] keywords = ('TYPE', 'SHADING-COEF', 'GLASS-CONDUCT') - values = ('SHADING-COEF', shading_coef, glass_cond) + values = ('SHADING-COEF', round(shading_coef, 3), round(glass_cond, 3)) return generate_inp_string(doe2_id, 'GLASS-TYPE', keywords, values) @@ -79,9 +79,9 @@ def door_construction_to_inp(construction): This translation pathway always uses a NoMass U-VALUE Construction. """ doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) - constr_cond = UValue([construction.u_factor], 'Btu/h-ft2-F', 'W/m2-K')[0] + constr_cond = UValue().to_unit([construction.u_factor], 'Btu/h-ft2-F', 'W/m2-K')[0] keywords = ('TYPE', 'U-VALUE') - values = ('U-VALUE', constr_cond) + values = ('U-VALUE', round(constr_cond, 3)) return generate_inp_string(doe2_id, 'CONSTRUCTION', keywords, values) @@ -91,7 +91,7 @@ def air_construction_to_inp(construction): This translation pathway always uses a NoMass U-VALUE Construction. """ doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) - constr_cond = UValue([6], 'Btu/h-ft2-F', 'W/m2-K')[0] + constr_cond = 1.0 # default U-Value in Btu/h-ft2-F keywords = ('TYPE', 'U-VALUE') values = ('U-VALUE', constr_cond) return generate_inp_string(doe2_id, 'CONSTRUCTION', keywords, values) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 1c88ad6..5caa2e6 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -1,4 +1,6 @@ """honeybee-doe2 load translators.""" +from __future__ import division + from ladybug.datatype.area import Area from ladybug.datatype.energyflux import EnergyFlux from ladybug.datatype.volumeflowrateintensity import VolumeFlowRateIntensity diff --git a/honeybee_doe2/schedule.py b/honeybee_doe2/schedule.py index 4b1bc2e..2fc2eb0 100644 --- a/honeybee_doe2/schedule.py +++ b/honeybee_doe2/schedule.py @@ -1,5 +1,7 @@ # coding=utf-8 """honeybee-doe2 schedule translators.""" +from __future__ import division + from ladybug.dt import Date, MONTHNAMES from honeybee.typing import clean_doe2_string diff --git a/honeybee_doe2/simulation.py b/honeybee_doe2/simulation.py index 8d8bb70..ada0ad0 100644 --- a/honeybee_doe2/simulation.py +++ b/honeybee_doe2/simulation.py @@ -1,2 +1,267 @@ # coding=utf-8 """honeybee-doe2 simulation parameters.""" +from __future__ import division + +from honeybee.typing import clean_doe2_string, int_in_range +from honeybee_energy.simulation.runperiod import RunPeriod + +from .config import GEO_CHARS +from .util import generate_inp_string, header_comment_minor + + +class SimulationPar(object): + """Complete set of DOE-2 Simulation Settings. + + Args: + title: Text for the title of the project. (Default: *Unnamed*). + run_period: A honeybee-energy RunPeriod object to describe the time period + over which to run the simulation. (Default: Run for the whole year). + site: A SiteData object describing the site where the simulation is + run. (Default: HArtford, CT). + + Properties: + * title + * run_period + * site + """ + __slots__ = ('_title', '_run_period', '_site') + + def __init__(self, title='Unnamed', run_period=None, site=None): + """Initialize SimulationPar.""" + self.title = title + self.run_period = run_period + self.site = site + + @classmethod + def from_dict(cls, data): + """Create a SimulationPar object from a dictionary. + + Args: + data: A SimulationPar dictionary in following the format below. + + .. code-block:: python + + { + "type": "SimulationPar", + "title": 'sample_project', # Text for the title + "run_period": {}, # Honeybee RunPeriod dictionary + "site": {}, # Honeybee SiteData dictionary + } + """ + assert data['type'] == 'SimulationPar', \ + 'Expected SimulationPar dictionary. Got {}.'.format(data['type']) + title = data['title'] if 'title' in data else 'Unnamed' + run_period = None + if 'run_period' in data and data['run_period'] is not None: + run_period = RunPeriod.from_dict(data['run_period']) + site = None + if 'site' in data and data['site'] is not None: + site = SiteData.from_dict(data['site']) + return cls(title, run_period, site) + + @property + def title(self): + """Get or set text for the title of the project.""" + return self._title + + @title.setter + def title(self, value): + if value: + value = clean_doe2_string(value, GEO_CHARS) + else: + value = None + self._title = value + + @property + def run_period(self): + """Get or set a RunPeriod object for the time period to run the simulation.""" + return self._run_period + + @run_period.setter + def run_period(self, value): + if value is not None: + assert isinstance(value, RunPeriod), 'Expected RunPeriod for ' \ + 'SimulationPar run_period. Got {}.'.format(type(value)) + self._run_period = value + else: + self._run_period = RunPeriod() + + @property + def site(self): + """Get or set a SiteData object for the project site of the simulation.""" + return self._site + + @site.setter + def site(self, value): + if value is not None: + assert isinstance(value, SiteData), 'Expected SiteData for ' \ + 'SimulationPar site. Got {}.'.format(type(value)) + self._site = value + else: + self._site = SiteData() + + def to_inp(self): + """Get an DOE-2 INP string representation of the SimulationPar.""" + # add the starting headers + sp_str = [] + sp_str.append(header_comment_minor('Abort, Diagnostics')) + sp_str.append(header_comment_minor('Global Parameters')) + sp_str.append(header_comment_minor('Title, Run Periods, Design Days, Holidays')) + # add the title and run period + title_str = \ + 'TITLE\n' \ + ' LINE-1 = *{}*\n' \ + ' ..\n'.format(self.title) + sp_str.append(title_str) + sp_str.append(self.run_period.to_inp()) + # add the site and building data + sp_str.append(header_comment_minor('Compliance Data')) + sp_str.append(header_comment_minor('Site and Building Data')) + sp_str.append(self.site.to_inp()) + # add the end tag for the project data + sp_str.append('PROJECT-DATA\n ..\n') + return '\n'.join(sp_str) + + def to_dict(self): + """SimulationPar dictionary representation.""" + return { + 'type': 'SimulationPar', + 'title': self.title, + 'run_period': self.run_period.to_dict(), + 'site': self.site.to_dict(), + } + + def duplicate(self): + """Get a copy of this object.""" + return self.__copy__() + + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__() + + def __copy__(self): + return SimulationPar( + self.title, self.run_period.duplicate(), self.site.duplicate()) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.title, hash(self.run_period), hash(self.site)) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, SimulationPar) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return 'DOE-2 SimulationPar: {}'.format(self.title) + + +class SiteData(object): + """Object to describe the project site of the simulation. + + Args: + altitude: A number for the altitude of the location above sea level + in Feet. (Default: 150). + + Properties: + * altitude + """ + __slots__ = ('_altitude',) + + def __init__(self, altitude=150): + """Initialize SimulationPar.""" + self.altitude = altitude + + @classmethod + def from_dict(cls, data): + """Create a SiteData object from a dictionary. + + Args: + data: A SiteData dictionary in following the format below. + + .. code-block:: python + + { + "type": "SiteData", + "altitude": 100 # altitude above sea level (ft) + } + """ + assert data['type'] == 'SiteData', \ + 'Expected SiteData dictionary. Got {}.'.format(data['type']) + altitude = data['altitude'] if 'altitude' in data else 150 + return cls(altitude) + + @property + def altitude(self): + """A number for the altitude of the location above sea level in Feet.""" + return self._altitude + + @altitude.setter + def altitude(self, value): + self._altitude = int_in_range(value, input_name='site data altitude') + + def to_inp(self): + """Get an DOE-2 INP string representation of the SiteData.""" + # create the INP string for the site + keywords = ('ALTITUDE', 'C-STATE', 'C-WEATHER-FILE', + 'C-COUNTRY', 'C-901-LOCATION') + values = (self.altitude, '21', '*TMY2\HARTFOCT.bin*', '1', '1092') + site_str = generate_inp_string('Site Data', 'SITE-PARAMETERS', keywords, values) + # create the INP string for the building data + keywords, values = ('HOLIDAYS',), ('"Standard US Holidays"',) + bldg_str = generate_inp_string( + 'Building Data', 'BUILD-PARAMETERS', keywords, values) + return site_str + bldg_str + + def to_dict(self): + """SiteData dictionary representation.""" + return { + 'type': 'SiteData', + 'altitude': self.altitude + } + + def duplicate(self): + """Get a copy of this object.""" + return self.__copy__() + + def ToString(self): + """Overwrite .NET ToString.""" + return self.__repr__() + + def __copy__(self): + return SiteData(self.altitude) + + def __key(self): + """A tuple based on the object properties, useful for hashing.""" + return (self.altitude,) + + def __hash__(self): + return hash(self.__key()) + + def __eq__(self, other): + return isinstance(other, SiteData) and self.__key() == other.__key() + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return self.to_inp() + + +def run_period_to_inp(run_period): + """Translate a honeybee-energy RunPeriod object to a DOE-2 INP string""" + # create the string for the run period + year = 2020 if run_period.is_leap_year else 2021 + keywords = ('BEGIN-MONTH', 'BEGIN-DAY', 'BEGIN-YEAR', + 'END-MONTH', 'END-DAY', 'END-YEAR') + values = (run_period.start_date.month, run_period.start_date.day, year, + run_period.end_date.month, run_period.end_date.day, year) + rp_str = generate_inp_string('Default Run Period', 'RUN-PERIOD-PD', keywords, values) + # create the string for the holidays + keywords, values = ('LIBRARY-ENTRY',), ('"US"',) + hol_str = generate_inp_string('Standard US Holidays', 'HOLIDAYS', keywords, values) + return rp_str + hol_str diff --git a/honeybee_doe2/util.py b/honeybee_doe2/util.py index 1261f84..7f34ba6 100644 --- a/honeybee_doe2/util.py +++ b/honeybee_doe2/util.py @@ -1,5 +1,6 @@ # coding=utf-8 """Various utilities used throughout the package.""" +from __future__ import division def generate_inp_string(u_name, command, keywords, values): diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 1f6e0e7..8f3be39 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -1,5 +1,6 @@ # coding=utf-8 """Methods to write to inp.""" +from __future__ import division import math from ladybug_geometry.geometry2d import Point2D @@ -21,6 +22,7 @@ from .schedule import energy_trans_sch_to_transmittance from .load import people_to_inp, lighting_to_inp, equipment_to_inp, \ infiltration_to_inp +from .simulation import SimulationPar def face_3d_to_inp(face_3d, parent_name='HB object'): @@ -413,21 +415,23 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals def model_to_inp( - model, hvac_mapping='Story', exclude_interior_walls=False, - exclude_interior_ceilings=False + model, simulation_par=None, hvac_mapping='Story', + exclude_interior_walls=False, exclude_interior_ceilings=False ): """Generate an INP string representation of a Model. The resulting string will include all geometry (Rooms, Faces, Apertures, Doors, Shades), all fully-detailed constructions + materials, all fully-detailed - schedules, and the room properties. - - Essentially, the string includes everything needed to simulate the model - except the simulation parameters. So joining this string with the output of - SimulationParameter.to_inp() should create a simulate-able INP. + schedules, and the room properties. It will also include the simulation + parameters. Essentially, the string includes everything needed to simulate + the model. Args: model: A honeybee Model for which an INP representation will be returned. + simulation_par: A honeybee-doe2 SimulationPar object to specify how the + DOE-2 simulation should be run. If None, default simulation + parameters will be generated, which will run the simulation for the + full year. (Default: None). hvac_mapping: Text to indicate how HVAC systems should be assigned to the exported model. Story will assign one HVAC system for each distinct level polygon, Model will use only one HVAC system for the whole model @@ -454,19 +458,15 @@ def model_to_inp( from honeybee.model import Model from honeybee.room import Room from honeybee.config import folders - from honeybee_doe2.simulation import SimulationParameter - # Get input Model + # Crate an input Model room = Room.from_box('Tiny House Zone', 5, 10, 3) room.properties.energy.program_type = office_program room.properties.energy.add_default_ideal_air() model = Model('Tiny House', [room]) - # Get the input SimulationParameter - sim_par = SimulationParameter() - - # create the INP string for simulation parameters and model - inp_str = '\n\n'.join((sim_par.to_inp(), model.to.inp(model))) + # create the INP string for the model + inp_str = model.to.inp(model) # write the final string into an INP inp = os.path.join(folders.default_simulation_folder, 'test_file', 'in.inp') @@ -495,11 +495,8 @@ def model_to_inp( # write the simulation parameters into the string model_str = ['INPUT ..\n\n'] - model_str.append(header_comment_minor('Abort, Diagnostics')) - model_str.append(header_comment_minor('Global Parameters')) - model_str.append(header_comment_minor('Title, Run Periods, Design Days, Holidays')) - model_str.append(header_comment_minor('Compliance Data')) - model_str.append(header_comment_minor('Site and Building Data')) + sim_par = simulation_par if simulation_par is not None else SimulationPar() + model_str.append(sim_par.to_inp()) # write all of the schedules all_day_scheds, all_week_scheds, all_year_scheds = [], [], [] @@ -603,4 +600,4 @@ def model_to_inp( for report in report_types: model_str.append(header_comment_minor(report)) model_str = ['END ..\nCOMPUTE ..\nSTOP ..\n'] - return ''.join(model_str) + return '\n'.join(model_str) diff --git a/tests/construction_test.py b/tests/construction_test.py new file mode 100644 index 0000000..776650c --- /dev/null +++ b/tests/construction_test.py @@ -0,0 +1,167 @@ +from honeybee_energy.material.opaque import EnergyMaterial, EnergyMaterialNoMass +from honeybee_energy.material.glazing import EnergyWindowMaterialGlazing +from honeybee_energy.material.shade import EnergyWindowMaterialShade +from honeybee_energy.material.gas import EnergyWindowMaterialGas +from honeybee_energy.construction.opaque import OpaqueConstruction +from honeybee_energy.construction.window import WindowConstruction +from honeybee_energy.construction.windowshade import WindowConstructionShade +from honeybee_energy.construction.air import AirBoundaryConstruction +from honeybee_energy.schedule.ruleset import ScheduleRuleset + + +def test_material_to_inp(): + """Test the EnergyMaterial to_inp method.""" + concrete = EnergyMaterial('Concrete', 0.2, 0.5, 800, 1200, + 'MediumSmooth', 0.95, 0.75, 0.8) + inp_str = concrete.to_inp() + + assert inp_str == \ + '"Concrete" = MATERIAL\n' \ + ' TYPE = PROPERTIES\n' \ + ' THICKNESS = 0.656\n' \ + ' CONDUCTIVITY = 0.289\n' \ + ' DENSITY = 49.944\n' \ + ' SPECIFIC-HEAT = 0.287\n' \ + ' ..\n' + + +def test_material_nomass_to_inp(): + """Test the EnergyMaterialNoMass to_inp method.""" + insul_r2 = EnergyMaterialNoMass('Insulation R-2', 2, 'MediumSmooth', 0.95, 0.75, 0.8) + inp_str = insul_r2.to_inp() + + assert inp_str == \ + '"Insulation R-2" = MATERIAL\n' \ + ' TYPE = RESISTANCE\n' \ + ' RESISTANCE = 11.357\n' \ + ' ..\n' + + +def test_opaque_construction_to_inp(): + """Test the OpaqueConstruction to_inp method.""" + concrete = EnergyMaterial('Concrete', 0.15, 2.31, 2322, 832, 'MediumRough', + 0.95, 0.75, 0.8) + insulation = EnergyMaterialNoMass('Insulation R-3', 3, 'MediumSmooth') + wall_gap = EnergyMaterial('Wall Air Gap', 0.1, 0.67, 1.2925, 1006.1) + gypsum = EnergyMaterial('Gypsum', 0.0127, 0.16, 784.9, 830, 'MediumRough', + 0.93, 0.6, 0.65) + wall_constr = OpaqueConstruction( + 'Generic Wall Construction', [concrete, insulation, wall_gap, gypsum]) + inp_str = wall_constr.to_inp() + + assert inp_str == \ + '"Generic Wall Construction_l" = LAYERS\n' \ + ' MATERIAL = (\n' \ + ' Concrete,\n' \ + ' Insulation R-3,\n' \ + ' Wall Air Gap,\n' \ + ' Gypsum,\n' \ + ' )\n' \ + ' ..\n' \ + '"Generic Wall Construction" = CONSTRUCTION\n' \ + ' TYPE = LAYERS\n' \ + ' ABSORPTANCE = 0.75\n' \ + ' ROUGHNESS = 3\n' \ + ' LAYERS = Generic Wall Construction_l\n' \ + ' ..\n' + + +def test_window_construction_to_inp(): + """Test the WindowConstruction to_inp method.""" + simple_double_low_e = WindowConstruction.from_simple_parameters( + 'NECB Window Construction', 1.7, 0.4) + lowe_glass = EnergyWindowMaterialGlazing( + 'Low-e Glass', 0.00318, 0.4517, 0.359, 0.714, 0.207, + 0, 0.84, 0.046578, 1.0) + clear_glass = EnergyWindowMaterialGlazing( + 'Clear Glass', 0.005715, 0.770675, 0.07, 0.8836, 0.0804, + 0, 0.84, 0.84, 1.0) + gap = EnergyWindowMaterialGas('air gap', thickness=0.0127) + double_low_e = WindowConstruction( + 'Double Low-E Window', [lowe_glass, gap, clear_glass]) + double_clear = WindowConstruction( + 'Double Clear Window', [clear_glass, gap, clear_glass]) + triple_clear = WindowConstruction( + 'Triple Clear Window', [clear_glass, gap, clear_glass, gap, clear_glass]) + + inp_str = simple_double_low_e.to_inp() + assert inp_str == \ + '"NECB Window Construction" = GLASS-TYPE\n' \ + ' TYPE = SHADING-COEF\n' \ + ' SHADING-COEF = 0.46\n' \ + ' GLASS-CONDUCT = 0.302\n' \ + ' ..\n' + + inp_str = double_low_e.to_inp() + assert inp_str == \ + '"Double Low-E Window" = GLASS-TYPE\n' \ + ' TYPE = SHADING-COEF\n' \ + ' SHADING-COEF = 0.488\n' \ + ' GLASS-CONDUCT = 0.299\n' \ + ' ..\n' + + inp_str = double_clear.to_inp() + assert inp_str == \ + '"Double Clear Window" = GLASS-TYPE\n' \ + ' TYPE = SHADING-COEF\n' \ + ' SHADING-COEF = 0.791\n' \ + ' GLASS-CONDUCT = 0.479\n' \ + ' ..\n' + + inp_str = triple_clear.to_inp() + assert inp_str == \ + '"Triple Clear Window" = GLASS-TYPE\n' \ + ' TYPE = SHADING-COEF\n' \ + ' SHADING-COEF = 0.688\n' \ + ' GLASS-CONDUCT = 0.309\n' \ + ' ..\n' + + +def test_window_construction_shade_to_inp(): + """Test the WindowConstructionShade to_inp method.""" + lowe_glass = EnergyWindowMaterialGlazing( + 'Low-e Glass', 0.00318, 0.4517, 0.359, 0.714, 0.207, + 0, 0.84, 0.046578, 1.0) + clear_glass = EnergyWindowMaterialGlazing( + 'Clear Glass', 0.005715, 0.770675, 0.07, 0.8836, 0.0804, + 0, 0.84, 0.84, 1.0) + gap = EnergyWindowMaterialGas('air gap', thickness=0.0127) + shade_mat = EnergyWindowMaterialShade( + 'Low-e Diffusing Shade', 0.005, 0.15, 0.5, 0.25, 0.5, 0, 0.4, + 0.2, 0.1, 0.75, 0.25) + window_constr = WindowConstruction('Double Low-E', [lowe_glass, gap, clear_glass]) + double_low_e_shade = WindowConstructionShade( + 'Double Low-E with Shade', window_constr, shade_mat, 'Exterior', + 'OnIfHighSolarOnWindow', 200) + + inp_str = double_low_e_shade.to_inp() + assert inp_str == \ + '"Double Low-E with Shade" = GLASS-TYPE\n' \ + ' TYPE = SHADING-COEF\n' \ + ' SHADING-COEF = 0.488\n' \ + ' GLASS-CONDUCT = 0.299\n' \ + ' ..\n' + + +def test_air_construction_to_inp(): + """Test the AirBoundaryConstruction to_inp method.""" + default_constr = AirBoundaryConstruction('Default Air Construction') + night_flush = ScheduleRuleset.from_daily_values( + 'Night Flush', [1, 1, 1, 1, 1, 1, 1, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 1]) + night_flush_constr = AirBoundaryConstruction('Night Flush Boundary', 0.4, night_flush) + + inp_str = default_constr.to_inp() + assert inp_str == \ + '"Default Air Construction" = CONSTRUCTION\n' \ + ' TYPE = U-VALUE\n' \ + ' U-VALUE = 1.0\n' \ + ' ..\n' + + inp_str = night_flush_constr.to_inp() + assert inp_str == \ + '"Night Flush Boundary" = CONSTRUCTION\n' \ + ' TYPE = U-VALUE\n' \ + ' U-VALUE = 1.0\n' \ + ' ..\n' + diff --git a/tests/simulation_test.py b/tests/simulation_test.py new file mode 100644 index 0000000..e146d62 --- /dev/null +++ b/tests/simulation_test.py @@ -0,0 +1,88 @@ +# coding=utf-8 +from honeybee_energy.simulation.runperiod import RunPeriod + +from honeybee_doe2.simulation import SimulationPar, SiteData + +DEFAULT_RUN_PERIOD = \ + '"Default Run Period" = RUN-PERIOD-PD\n' \ + ' BEGIN-MONTH = 1\n' \ + ' BEGIN-DAY = 1\n' \ + ' BEGIN-YEAR = 2021\n' \ + ' END-MONTH = 12\n' \ + ' END-DAY = 31\n' \ + ' END-YEAR = 2021\n' \ + ' ..\n' \ + '"Standard US Holidays" = HOLIDAYS\n' \ + ' LIBRARY-ENTRY = "US"\n' \ + ' ..\n' +DEFAULT_SITE_DATA = \ + '"Site Data" = SITE-PARAMETERS\n' \ + ' ALTITUDE = 100\n' \ + ' C-STATE = 21\n' \ + ' C-WEATHER-FILE = *TMY2\\HARTFOCT.bin*\n' \ + ' C-COUNTRY = 1\n' \ + ' C-901-LOCATION = 1092\n' \ + ' ..\n' \ + '"Building Data" = BUILD-PARAMETERS\n' \ + ' HOLIDAYS = "Standard US Holidays"\n' \ + ' ..\n' + + +def test_run_period_to_inp(): + """Test the RunPeriod to_inp method.""" + run_period = RunPeriod() + inp_str = run_period.to_inp() + assert inp_str == DEFAULT_RUN_PERIOD + + +def test_site_data_to_inp(): + """Test the SiteData to_inp method.""" + site_data = SiteData(100) + inp_str = site_data.to_inp() + assert inp_str == DEFAULT_SITE_DATA + + +def test_simulation_par_to_inp(): + """Test the SimulationPar to_inp method.""" + title = 'Sample_Project' + simulation_par = SimulationPar(title) + simulation_par.site.altitude = 100 + inp_str = simulation_par.to_inp() + + assert title in inp_str + assert DEFAULT_RUN_PERIOD in inp_str + assert DEFAULT_SITE_DATA in inp_str + assert inp_str.endswith('PROJECT-DATA\n ..\n') + + +def test_simulation_par_init(): + """Test the initialization of SimulationPar and basic properties.""" + sim_par = SimulationPar() + str(sim_par) # test the string representation + + assert sim_par.title == 'Unnamed' + assert sim_par.run_period == RunPeriod() + assert sim_par.site == SiteData() + + sim_par_dup = sim_par.duplicate() + sim_par_alt = SimulationPar(title='Sample_Project') + assert sim_par is sim_par + assert sim_par is not sim_par_dup + assert sim_par == sim_par_dup + sim_par_dup.title = 'test' + assert sim_par != sim_par_dup + assert sim_par != sim_par_alt + + +def test_simulation_parameter_to_dict_methods(): + """Test the to/from dict methods.""" + sim_par = SimulationPar() + sim_par_dict = sim_par.to_dict() + + assert 'title' in sim_par_dict + assert 'run_period' in sim_par_dict + assert 'site' in sim_par_dict + + new_sim_par = SimulationPar.from_dict(sim_par_dict) + assert new_sim_par == sim_par + assert sim_par_dict == new_sim_par.to_dict() From 0e2b09261bc1730e0b691a34eb6387f87319932f Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 25 Apr 2024 17:34:04 -0700 Subject: [PATCH 06/27] fix(writer): Add method to group rooms by DOE-2 level --- honeybee_doe2/writer.py | 67 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 8f3be39..67d3229 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -374,6 +374,7 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals # create the polygon string from the geometry doe2_id = clean_doe2_string(room.identifier, GEO_CHARS) r_geo = room.horizontal_boundary(match_walls=False, tolerance=DOE2_TOLERANCE) + r_geo = r_geo if r_geo.normal.z >= 0 else r_geo.flip() r_geo = r_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) room_polygon, pos_info = face_3d_to_inp(r_geo, doe2_id) space_origin, _, _ = pos_info @@ -478,6 +479,7 @@ def model_to_inp( # scale the model if the units are not feet if model.units != 'Feet': model.convert_to_units('Feet') + tol = model.tolerance # remove degenerate geometry within native DOE-2 tolerance try: model.remove_degenerate_geometry(DOE2_TOLERANCE) @@ -557,9 +559,11 @@ def model_to_inp( for dr_con in door_constructions: model_str.append(door_construction_to_inp(dr_con)) - # group rooms in the model such that each level has only one polygon - grouped_rooms = Room.group_by_floor_height(model.rooms, FLOOR_LEVEL_TOL) - + # loop through rooms grouped by floor level and boundary to get polygons + room_polygons, bldg_geo_defs = [], [] + + + model_str.append(header_comment_minor('Polygons')) model_str.append(header_comment_minor('Wall Parameters')) model_str.append(header_comment_minor('Fixed and Building Shades')) @@ -601,3 +605,60 @@ def model_to_inp( model_str.append(header_comment_minor(report)) model_str = ['END ..\nCOMPUTE ..\nSTOP ..\n'] return '\n'.join(model_str) + + +def group_rooms_by_doe2_level(rooms, model_tolerance): + """Group Honeybee Rooms according to acceptable floor levels in DOE-2. + + This means that not only will Rooms be on separate DOE-2 levels if their floor + heights differ but also Rooms that share the same floor height but are + disconnected from one another in plan will also be separate levels. + For example, when the model is of two towers on a plinth, each tower will + get its own separate level group. + + Args: + rooms: A list of Honeybee Rooms to be grouped. + model_tolerance: The tolerance of the model that the Rooms originated from. + + Returns: + A tuple with three elements. + + - room_groups: A list of lists where each sub-list contains Honeybee + Rooms that should be on the same DOE-2 level. + + - level_geometry: A list of Face3D with the same length as the + room_groups, which contains the geometry that represents each floor + level. These geometries will always be pointing upwards so that + their vertices are counter-clockwise when viewed from above. They + will also have colinear vertices removed such that they are ready + to be translated to INP POLYGONS. + + - level_names: A list of text strings that align with the level + geometry and contain suggested names for the DOE-2 levels. + """ + # set up lists of the outputs to be populated + room_groups, level_geometry, level_names = [], [], [] + + # first group the rooms by floor height + grouped_rooms = Room.group_by_floor_height(rooms, FLOOR_LEVEL_TOL) + for fi, room_group in enumerate(grouped_rooms): + hor_bounds = Room.grouped_horizontal_boundary( + room_group, tolerance=model_tolerance, floors_only=True) + if len(hor_bounds) == 0: # possible when Rooms have no floors + hor_bounds = Room.grouped_horizontal_boundary( + room_group, tolerance=model_tolerance, floors_only=False) + # if we got lucky and everything is one contiguous polygon, we're done! + if len(hor_bounds) == 1: + flr_geo = hor_bounds[0] + flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip() + flr_geo = flr_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) + room_groups.append(room_group) + level_geometry.append(flr_geo) + level_names.append('Level_{}'.format(fi)) + else: # otherwise, we need to figure out which Room belongs to which geometry + for flr_geo in hor_bounds: + flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip() + r_geo = r_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) + + # return all of the outputs + return room_groups, level_geometry, level_names From b69615fb684c6199c2c1fd2660dd965da588edde Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Fri, 26 Apr 2024 13:05:03 -0700 Subject: [PATCH 07/27] feat(writer): Add methods to group and assign HVAC Zones --- honeybee_doe2/grouping.py | 145 ++++++++++++++++++++++++++++++++++++++ honeybee_doe2/writer.py | 145 +++++++++++++++++++++----------------- 2 files changed, 224 insertions(+), 66 deletions(-) create mode 100644 honeybee_doe2/grouping.py diff --git a/honeybee_doe2/grouping.py b/honeybee_doe2/grouping.py new file mode 100644 index 0000000..b530a16 --- /dev/null +++ b/honeybee_doe2/grouping.py @@ -0,0 +1,145 @@ +# coding=utf-8 +"""Methods for grouping rooms to comply with INP rules.""" +from __future__ import division +import math + +from ladybug_geometry.geometry2d import Point2D, Polygon2D +from ladybug_geometry.geometry3d import Vector3D +from honeybee.typing import clean_doe2_string +from honeybee.room import Room + +from .config import DOE2_TOLERANCE, FLOOR_LEVEL_TOL, RES_CHARS + + +def group_rooms_by_doe2_level(rooms, model_tolerance): + """Group Honeybee Rooms according to acceptable floor levels in DOE-2. + + This means that not only will Rooms be on separate DOE-2 levels if their floor + heights differ but also Rooms that share the same floor height but are + disconnected from one another in plan will also be separate levels. + For example, when the model is of two towers on a plinth, each tower will + get its own separate level group. + + Args: + rooms: A list of Honeybee Rooms to be grouped. + model_tolerance: The tolerance of the model that the Rooms originated from. + + Returns: + A tuple with three elements. + + - room_groups: A list of lists where each sub-list contains Honeybee + Rooms that should be on the same DOE-2 level. + + - level_geometries: A list of Face3D with the same length as the + room_groups, which contains the geometry that represents each floor + level. These geometries will always be pointing upwards so that + their vertices are counter-clockwise when viewed from above. They + will also have colinear vertices removed such that they are ready + to be translated to INP POLYGONS. + + - level_names: A list of text strings that align with the level + geometry and contain suggested names for the DOE-2 levels. + """ + # set up lists of the outputs to be populated + room_groups, level_geometries, level_names = [], [], [] + + # first group the rooms by floor height + grouped_rooms = Room.group_by_floor_height(rooms, FLOOR_LEVEL_TOL) + for fi, room_group in enumerate(grouped_rooms): + # then, group the rooms by contiguous horizontal boundary + hor_bounds = Room.grouped_horizontal_boundary( + room_group, tolerance=model_tolerance, floors_only=True) + if len(hor_bounds) == 0: # possible when Rooms have no floors + hor_bounds = Room.grouped_horizontal_boundary( + room_group, tolerance=model_tolerance, floors_only=False) + + # if we got lucky and everything is one contiguous polygon, we're done! + if len(hor_bounds) == 1: + flr_geo = hor_bounds[0] + flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip() + flr_geo = flr_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) + room_groups.append(room_group) + level_geometries.append(flr_geo) + level_names.append('Level_{}'.format(fi)) + else: # otherwise, we need to figure out which Room belongs to which geometry + # first get a set of Point2Ds that are inside each room in plan + room_pts, z_axis = [], Vector3D(0, 0, 1) + for room in rooms: + for face in room.faces: + if math.degrees(z_axis.angle(face.normal)) >= 91: + down_geo = face.geometry + break + room_pt3d = down_geo.center if down_geo.is_convex else \ + down_geo.pole_of_inaccessibility(DOE2_TOLERANCE) + room_pts.append(Point2D(room_pt3d.x, room_pt3d.y)) + # loop through floor geometries and determine all rooms associated with them + for si, flr_geo in enumerate(hor_bounds): + flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip() + flr_geo = flr_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) + flr_poly = Polygon2D([Point2D(pt.x, pt.y) for pt in flr_geo.boundary]) + flr_rooms = [] + for room, room_pt in zip(rooms, room_pts): + if flr_poly.is_point_inside_bound_rect(room_pt): + flr_rooms.append(room) + room_groups.append(flr_rooms) + level_geometries.append(flr_geo) + level_names.append('Level_{}_Section{}'.format(fi, si)) + + # return all of the outputs + return room_groups, level_geometries, level_names + + +def group_rooms_by_doe2_hvac(model, hvac_mapping): + """Group Honeybee Rooms according to HVAC logic. + + Args: + model: A Honeybee Model for which Rooms will be grouped for HVAC assignment. + hvac_mapping: Text to indicate how HVAC systems should be assigned. + Model will use only one HVAC system for the whole model and + AssignedHVAC will follow how the HVAC systems have been assigned + to the Rooms.properties.energy.hvac. Choose from the options below. + + * Room + * Model + * AssignedHVAC + + Returns: + A tuple with three elements. + + - room_groups: A list of lists where each sub-list contains Honeybee + Rooms that should ave the same HVAC system. + + - hvac_names: A list of text strings that align with the room_groups + and contain suggested names for the DOE-2 HVAC systems. + """ + # clean up the hvac_mapping text + hvac_mapping = hvac_mapping.upper().replace('-', '').replace(' ', '') + + # determine the mapping to be used + if hvac_mapping == 'MODEL': + hvac_name = clean_doe2_string('{}_Sys'.format(model.display_name), RES_CHARS) + return model.rooms, [hvac_name] + elif hvac_mapping == 'ROOM': + hvac_names = [clean_doe2_string('{}_Sys'.format(room.display_name), RES_CHARS) + for room in model.rooms] + room_groups = [[room] for room in model.rooms] + else: # assume that it is the assigned HVAC + hvac_dict = {} + for room in model.rooms: + if room.properties.energy.hvac is not None: + hvac_id = room.properties.energy.hvac.display_name + try: + hvac_dict[hvac_id].append(room) + except KeyError: # the first time that we are encountering the HVAC + hvac_dict[hvac_id] = [room] + else: + try: + hvac_dict['Unassigned'].append(room) + except KeyError: # the first time that we have an unassigned room + hvac_dict['Unassigned'] = [room] + room_groups, hvac_names = [], [] + for hvac_name, rooms in hvac_dict.items(): + room_groups.append(rooms) + hvac_names.append(clean_doe2_string(hvac_name), RES_CHARS) + + return room_groups, hvac_names diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 67d3229..98fc4b8 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -8,15 +8,15 @@ from honeybee.typing import clean_doe2_string from honeybee.boundarycondition import Surface from honeybee.facetype import Wall, Floor, RoofCeiling -from honeybee.room import Room from honeybee_energy.construction.opaque import OpaqueConstruction from honeybee_energy.construction.air import AirBoundaryConstruction from honeybee_energy.lib.constructionsets import generic_construction_set -from .config import DOE2_TOLERANCE, DOE2_ANGLE_TOL, FLOOR_LEVEL_TOL, RECT_WIN_SUBD, \ +from .config import DOE2_TOLERANCE, DOE2_ANGLE_TOL, RECT_WIN_SUBD, \ DOE2_INTERIOR_BCS, GEO_CHARS, RES_CHARS from .util import generate_inp_string, header_comment_minor, \ header_comment_major +from .grouping import group_rooms_by_doe2_level, group_rooms_by_doe2_hvac from .construction import opaque_material_to_inp, opaque_construction_to_inp, \ window_construction_to_inp, door_construction_to_inp, air_construction_to_inp from .schedule import energy_trans_sch_to_transmittance @@ -348,12 +348,7 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals # set up attributes based on the Room's energy properties energy_attr_keywords = ['ZONE-TYPE'] - if room.exclude_floor_area: - energy_attr_values = ['PLENUM'] - elif room.properties.energy.is_conditioned: - energy_attr_values = ['CONDITIONED'] - else: - energy_attr_values = ['UNCONDITIONED'] + energy_attr_values = [room_doe2_conditioning_type(room)] if room.properties.energy.people: ppl_kwd, ppl_val = people_to_inp(room) energy_attr_keywords.extend(ppl_kwd) @@ -479,7 +474,6 @@ def model_to_inp( # scale the model if the units are not feet if model.units != 'Feet': model.convert_to_units('Feet') - tol = model.tolerance # remove degenerate geometry within native DOE-2 tolerance try: model.remove_degenerate_geometry(DOE2_TOLERANCE) @@ -560,18 +554,53 @@ def model_to_inp( model_str.append(door_construction_to_inp(dr_con)) # loop through rooms grouped by floor level and boundary to get polygons - room_polygons, bldg_geo_defs = [], [] - - - + level_room_groups, level_geos, level_names = \ + group_rooms_by_doe2_level(model.rooms, model.tolerance) + bldg_polygons, bldg_geo_defs = [], [] + for flr_rooms, flr_geo, flr_name in zip(level_room_groups, level_geos, level_names): + # create the story POLYGON and definition + flr_polygon, pos_info = face_3d_to_inp(flr_geo, flr_name) + flr_origin, _, _ = pos_info + rooms_f2f = [room.max.z - room.min.z for room in flr_rooms] + flr_keys = ('SHAPE', 'POLYGON', 'AZIMUTH', 'X', 'Y', 'Z', + 'SPACE-HEIGHT', 'FLOOR-HEIGHT') + flr_vals = ('POLYGON', '"{} Plg"'.format(flr_name), 0, + flr_origin.x, flr_origin.y, flr_origin.z, + round(sum(rooms_f2f) / len(rooms_f2f), 3), round(max(rooms_f2f), 3)) + flr_def = generate_inp_string(flr_name, 'FLOOR', flr_keys, flr_vals) + bldg_polygons.append(flr_polygon) + bldg_geo_defs.append(flr_def) + # add the room and face definitions + polygons + for room in flr_rooms: + room_polygons, room_defs = room_to_inp( + room, flr_origin, exclude_interior_walls, exclude_interior_ceilings) + bldg_polygons.extend(room_polygons) + bldg_geo_defs.extend(room_defs) + + # loop through the shades and get their definitions and polygons + shade_polygons, shade_geo_defs = [], [] + for shade in model.shades: + shade_polygon, shade_def = shade_to_inp(shade) + shade_polygons.append(shade_polygon) + shade_geo_defs.append(shade_def) + for shade in model.shade_meshes: + shade_polygon, shade_def = shade_mesh_to_inp(shade) + shade_polygons.extend(shade_polygon) + shade_geo_defs.extend(shade_def) + + # write the building and shade geometry into the INP model_str.append(header_comment_minor('Polygons')) + model_str.extend(bldg_polygons) model_str.append(header_comment_minor('Wall Parameters')) model_str.append(header_comment_minor('Fixed and Building Shades')) + model_str.extend(shade_polygons) + model_str.extend(shade_geo_defs) model_str.append(header_comment_minor('Misc Cost Related Objects')) model_str.append(header_comment_major('Performance Curves')) model_str.append(header_comment_major('Floors / Spaces / Walls / Windows / Doors')) + model_str.extend(bldg_geo_defs) - # assign HVAC systems given the specified hvac_mapping + # write in placeholder headers for various HVAC components model_str.append(header_comment_major('Electric & Fuel Meters')) for meter in ('Electric Meters', 'Fuel Meters', 'Master Meters'): model_str.append(header_comment_minor(meter)) @@ -588,6 +617,33 @@ def model_to_inp( model_str.append(header_comment_minor('Chilled Water Meters')) model_str.append(header_comment_major('HVAC Systems / Zones')) + # assign HVAC systems given the specified hvac_mapping + if hvac_mapping.upper() == 'STORY': + hvac_rooms = level_room_groups + hvac_names = ['{}_Sys'.format(name) for name in level_names] + else: + hvac_rooms, hvac_names = group_rooms_by_doe2_hvac(model, hvac_mapping) + for hvac_name, rooms in zip(hvac_names, hvac_rooms): + # create the definition of the HVAC + hvac_keys = ('TYPE', 'HEAT-SOURCE', 'SYSTEM-REPORTS') + hvac_vals = ('SUM', 'NONE', 'NO') + hvac_def = generate_inp_string(hvac_name, 'SYSTEM', hvac_keys, hvac_vals) + model_str.append(hvac_def) + for room in rooms: + space_name = clean_doe2_string(room.identifier, GEO_CHARS) + zone_name = '_Zn'.format(space_name) + zone_type = room_doe2_conditioning_type(room) + heat_setpt, cool_setpt = 72, 75 + setpoint = room.properties.energy.setpoint + if setpoint is not None: + heat_setpt = round(setpoint.heating_setpoint * (9. / 5.) + 32., 2) + cool_setpt = round(setpoint.cooling_setpoint * (9. / 5.) + 32., 2) + zone_keys = ('TYPE', 'DESIGN-HEAT-T', 'DESIGN-COOL-T', + 'SIZING-OPTION', 'SPACE') + zone_vals = (zone_type, heat_setpt, cool_setpt, + 'ADJUST-LOADS', space_name) + zone_def = generate_inp_string(zone_name, 'ZONE', zone_keys, zone_vals) + model_str.append(zone_def) # provide a few last comment headers and end the file model_str.append(header_comment_major('Metering & Misc HVAC')) @@ -607,58 +663,15 @@ def model_to_inp( return '\n'.join(model_str) -def group_rooms_by_doe2_level(rooms, model_tolerance): - """Group Honeybee Rooms according to acceptable floor levels in DOE-2. - - This means that not only will Rooms be on separate DOE-2 levels if their floor - heights differ but also Rooms that share the same floor height but are - disconnected from one another in plan will also be separate levels. - For example, when the model is of two towers on a plinth, each tower will - get its own separate level group. +def room_doe2_conditioning_type(room): + """Get the DOE-2 conditioning type that should be assigned to both the Space and Zone. Args: - rooms: A list of Honeybee Rooms to be grouped. - model_tolerance: The tolerance of the model that the Rooms originated from. - - Returns: - A tuple with three elements. - - - room_groups: A list of lists where each sub-list contains Honeybee - Rooms that should be on the same DOE-2 level. - - - level_geometry: A list of Face3D with the same length as the - room_groups, which contains the geometry that represents each floor - level. These geometries will always be pointing upwards so that - their vertices are counter-clockwise when viewed from above. They - will also have colinear vertices removed such that they are ready - to be translated to INP POLYGONS. - - - level_names: A list of text strings that align with the level - geometry and contain suggested names for the DOE-2 levels. + room: A Honeybee Room for which the conditioning type will be returned. """ - # set up lists of the outputs to be populated - room_groups, level_geometry, level_names = [], [], [] - - # first group the rooms by floor height - grouped_rooms = Room.group_by_floor_height(rooms, FLOOR_LEVEL_TOL) - for fi, room_group in enumerate(grouped_rooms): - hor_bounds = Room.grouped_horizontal_boundary( - room_group, tolerance=model_tolerance, floors_only=True) - if len(hor_bounds) == 0: # possible when Rooms have no floors - hor_bounds = Room.grouped_horizontal_boundary( - room_group, tolerance=model_tolerance, floors_only=False) - # if we got lucky and everything is one contiguous polygon, we're done! - if len(hor_bounds) == 1: - flr_geo = hor_bounds[0] - flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip() - flr_geo = flr_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) - room_groups.append(room_group) - level_geometry.append(flr_geo) - level_names.append('Level_{}'.format(fi)) - else: # otherwise, we need to figure out which Room belongs to which geometry - for flr_geo in hor_bounds: - flr_geo = flr_geo if flr_geo.normal.z >= 0 else flr_geo.flip() - r_geo = r_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) - - # return all of the outputs - return room_groups, level_geometry, level_names + if room.exclude_floor_area: + return 'PLENUM' + elif room.properties.energy.is_conditioned: + return 'CONDITIONED' + else: + return 'UNCONDITIONED' From d75ffe7bc8bdf34abb758745fe795449dc149f14 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Fri, 26 Apr 2024 17:26:57 -0700 Subject: [PATCH 08/27] fix(writer): Add more tests and fix bugs --- honeybee_doe2/cli/translate.py | 6 +- honeybee_doe2/config.py | 1 + honeybee_doe2/construction.py | 7 +- honeybee_doe2/grouping.py | 6 +- honeybee_doe2/load.py | 6 +- honeybee_doe2/writer.py | 56 +++-- requirements.txt | 2 +- tests/cli_test.py | 23 ++ tests/construction_test.py | 10 +- tests/schedule_test.py | 16 +- tests/simulation_test.py | 4 +- tests/writer_test.py | 385 ++++++++++++++++++++++++++++++++- 12 files changed, 472 insertions(+), 50 deletions(-) create mode 100644 tests/cli_test.py diff --git a/honeybee_doe2/cli/translate.py b/honeybee_doe2/cli/translate.py index 30747d7..a276059 100644 --- a/honeybee_doe2/cli/translate.py +++ b/honeybee_doe2/cli/translate.py @@ -42,7 +42,7 @@ def translate(): 'note whether interior ceilings should be excluded from the export.', default=True, show_default=True) @click.option( - '--switch-statements/--verbose-properties', ' /-v', help='Flag to note whether ' + '--verbose-properties/--switch-statements', ' /-ss', help='Flag to note whether ' 'program types should be written with switch statements so that they can easily ' 'be edited in eQuest or a verbose definition of loads should be written for ' 'each Room/Space.', default=True, show_default=True) @@ -56,9 +56,9 @@ def translate(): '--output-file', '-o', help='Optional INP file path to output the INP string ' 'of the translation. By default this will be printed out to stdout.', type=click.File('w'), default='-', show_default=True) -def model_to_inp( +def model_to_inp_file( model_file, sim_par_json, hvac_mapping, include_interior_walls, - include_interior_ceilings, name, folder, output_file + include_interior_ceilings, verbose_properties, name, folder, output_file ): """Translate a Model (HBJSON) file to an INP file. diff --git a/honeybee_doe2/config.py b/honeybee_doe2/config.py index beb2fdd..ee07699 100644 --- a/honeybee_doe2/config.py +++ b/honeybee_doe2/config.py @@ -4,6 +4,7 @@ DOE2_TOLERANCE = 0.03 # current best guess for DOE-2 absolute tolerance in Feet DOE2_ANGLE_TOL = 1.0 # current best guess for DOE-2 angle tolerance in degrees FLOOR_LEVEL_TOL = 0.1 # tolerance for grouping Rooms by floor elevations in Feet +GEO_DEC_COUNT = 6 # number of decimal places that all geometry will be rounded RECT_WIN_SUBD = 0.5 # subdivision distance to rectangularize windows in Feet DOE2_INTERIOR_BCS = ('Surface', 'Adiabatic', 'OtherSideTemperature') MIN_LAYER_THICKNESS = 0.003 # the minimum thickness for a material to be valid in meters diff --git a/honeybee_doe2/construction.py b/honeybee_doe2/construction.py index c3a7ca1..249ffe1 100644 --- a/honeybee_doe2/construction.py +++ b/honeybee_doe2/construction.py @@ -51,14 +51,15 @@ def opaque_construction_to_inp(construction): doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) # create the specification of material layers layer_id = '{}_l'.format(doe2_id) - layers = [clean_doe2_string(mat, RES_CHARS) for mat in construction.layers] + layers = ['"{}"'.format(clean_doe2_string(mat, RES_CHARS)) + for mat in construction.layers] layer_str = generate_inp_string_list_format( layer_id, 'LAYERS', ['MATERIAL'], [layers]) # create the construction specification roughness = ROUGHNESS_MAP[construction.materials[0].roughness] - sol_absorb = 1 - construction.outside_solar_reflectance + sol_absorb = round(1 - construction.outside_solar_reflectance, 3) keywords = ('TYPE', 'ABSORPTANCE', 'ROUGHNESS', 'LAYERS') - values = ('LAYERS', sol_absorb, roughness, layer_id) + values = ('LAYERS', sol_absorb, roughness, '"{}"'.format(layer_id)) constr_str = generate_inp_string(doe2_id, 'CONSTRUCTION', keywords, values) return ''.join((layer_str, constr_str)) diff --git a/honeybee_doe2/grouping.py b/honeybee_doe2/grouping.py index b530a16..27b4006 100644 --- a/honeybee_doe2/grouping.py +++ b/honeybee_doe2/grouping.py @@ -44,7 +44,7 @@ def group_rooms_by_doe2_level(rooms, model_tolerance): room_groups, level_geometries, level_names = [], [], [] # first group the rooms by floor height - grouped_rooms = Room.group_by_floor_height(rooms, FLOOR_LEVEL_TOL) + grouped_rooms, _ = Room.group_by_floor_height(rooms, FLOOR_LEVEL_TOL) for fi, room_group in enumerate(grouped_rooms): # then, group the rooms by contiguous horizontal boundary hor_bounds = Room.grouped_horizontal_boundary( @@ -118,7 +118,7 @@ def group_rooms_by_doe2_hvac(model, hvac_mapping): # determine the mapping to be used if hvac_mapping == 'MODEL': hvac_name = clean_doe2_string('{}_Sys'.format(model.display_name), RES_CHARS) - return model.rooms, [hvac_name] + return [model.rooms], [hvac_name] elif hvac_mapping == 'ROOM': hvac_names = [clean_doe2_string('{}_Sys'.format(room.display_name), RES_CHARS) for room in model.rooms] @@ -140,6 +140,6 @@ def group_rooms_by_doe2_hvac(model, hvac_mapping): room_groups, hvac_names = [], [] for hvac_name, rooms in hvac_dict.items(): room_groups.append(rooms) - hvac_names.append(clean_doe2_string(hvac_name), RES_CHARS) + hvac_names.append(clean_doe2_string(hvac_name, RES_CHARS)) return room_groups, hvac_names diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 5caa2e6..dea671f 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -14,7 +14,7 @@ def people_to_inp(room): """Translate the People definition of a Room into INP (Keywords, Values).""" people = room.properties.energy.people ppl_den = Area().to_unit([people.area_per_person], 'ft2', 'm2')[0] - ppl_total = ppl_den * room.floor_area + ppl_total = round(room.floor_area / ppl_den, 3) ppl_sch = clean_doe2_string(people.occupancy_schedule.display_name, RES_CHARS) ppl_sch = '"{}"'.format(ppl_sch) ppl_kwd = ('NUMBER-OF-PEOPLE', 'PEOPLE-SCHEDULE') @@ -26,6 +26,7 @@ def lighting_to_inp(room): """Translate the Lighting definition of a Room into INP (Keywords, Values).""" lighting = room.properties.energy.lighting lpd = EnergyFlux().to_unit([lighting.watts_per_area], 'W/ft2', 'W/m2')[0] + lpd = round(lpd, 3) lgt_sch = clean_doe2_string(lighting.schedule.display_name, RES_CHARS) lgt_sch = '"{}"'.format(lgt_sch) light_kwd = ('LIGHTING-W/AREA', 'LIGHTING-SCHEDULE', 'LIGHT-TO-RETURN') @@ -44,6 +45,7 @@ def equipment_to_inp(room): equip_val = [[], [], [], [], []] for equip in (ele_equip, gas_equip): epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] + epd = round(epd, 3) equip_val[0].append(epd) eqp_sch = clean_doe2_string(equip.schedule.display_name, RES_CHARS) equip_val[1].append('"{}"'.format(eqp_sch)) @@ -54,6 +56,7 @@ def equipment_to_inp(room): else: # write them as a single item equip = ele_equip if gas_equip is None else gas_equip epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] + epd = round(epd, 3) eqp_sch = clean_doe2_string(equip.schedule.display_name, RES_CHARS) eqp_sch = '"{}"'.format(eqp_sch) sens_fract = 1 - equip.latent_fraction - equip.lost_fraction @@ -70,6 +73,7 @@ def infiltration_to_inp(room): infil = room.properties.energy.infiltration inf_den = infil.flow_per_exterior_area inf_den = VolumeFlowRateIntensity().to_unit([inf_den], 'cfm/ft2', 'm3/s-m2')[0] + inf_den = round(inf_den, 3) inf_sch = clean_doe2_string(infil.schedule.display_name, RES_CHARS) inf_sch = '"{}"'.format(inf_sch) inf_kwd = ('INF-METHOD', 'INF-FLOW/AREA', 'INF-SCHEDULE') diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 98fc4b8..150b6c0 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -12,7 +12,7 @@ from honeybee_energy.construction.air import AirBoundaryConstruction from honeybee_energy.lib.constructionsets import generic_construction_set -from .config import DOE2_TOLERANCE, DOE2_ANGLE_TOL, RECT_WIN_SUBD, \ +from .config import DOE2_TOLERANCE, DOE2_ANGLE_TOL, GEO_DEC_COUNT, RECT_WIN_SUBD, \ DOE2_INTERIOR_BCS, GEO_CHARS, RES_CHARS from .util import generate_inp_string, header_comment_minor, \ header_comment_major @@ -69,12 +69,19 @@ def face_3d_to_inp(face_3d, parent_name='HB object'): vertices = [Point2D(v.x, -v.y) for v in vertices] # format the vertices into a POLYGON string - vert_template = '( %f, %f )' - verts_values = tuple(vert_template % (pt.x, pt.y) for pt in vertices) + verts_values = [] + for pt in vertices: + x_coord = round(pt.x, GEO_DEC_COUNT) + y_coord = round(pt.y, GEO_DEC_COUNT) + if x_coord == 0 and math.copysign(1, x_coord): # avoid signed zero + x_coord = 0.0 + if y_coord == 0 and math.copysign(1, y_coord): # avoid signed zero + y_coord = 0.0 + verts_values.append('({}, {})'.format(x_coord, y_coord)) verts_keywords = tuple('V{}'.format(i + 1) for i in range(len(verts_values))) poly_name = '{} Plg'.format(parent_name) polygon_str = generate_inp_string(poly_name, 'POLYGON', verts_keywords, verts_values) - position_info = (llc_origin, azimuth, tilt) + position_info = (llc_origin, tilt, azimuth) return polygon_str, position_info @@ -105,11 +112,13 @@ def shade_mesh_to_inp(shade_mesh): # loop through the mesh faces and create individual shade objects for i, face in enumerate(shade_mesh.geometry.face_vertices): f_geo = Face3D(face) - shd_geo = f_geo.geometry if f_geo.altitude > 0 else f_geo.geometry.flip() + shd_geo = f_geo if f_geo.altitude > 0 else f_geo.flip() doe2_id = '{}{}'.format(base_id, i) shade_polygon, pos_info = face_3d_to_inp(shd_geo, doe2_id) origin, tilt, az = pos_info - values = ('POLYGON', '"{} Plg"', trans, origin.x, origin.y, origin.z, tilt, az) + values = ('POLYGON', '"{} Plg"'.format(doe2_id), trans, + round(origin.x, GEO_DEC_COUNT), round(origin.y, GEO_DEC_COUNT), + round(origin.z, GEO_DEC_COUNT), tilt, az) shade_def = generate_inp_string(doe2_id, shade_type, keywords, values) shade_polygons.append(shade_polygon) shade_defs.append(shade_def) @@ -141,8 +150,9 @@ def shade_to_inp(shade): trans = energy_trans_sch_to_transmittance(shade) keywords = ('SHAPE', 'POLYGON', 'TRANSMITTANCE', 'X-REF', 'Y-REF', 'Z-REF', 'TILT', 'AZIMUTH') - values = ('POLYGON', '"{} Plg"', trans, - origin.x, origin.y, origin.z, tilt, az) + values = ('POLYGON', '"{} Plg"'.format(doe2_id), trans, + round(origin.x, GEO_DEC_COUNT), round(origin.y, GEO_DEC_COUNT), + round(origin.z, GEO_DEC_COUNT), tilt, az) shade_def = generate_inp_string(doe2_id, shade_type, keywords, values) return shade_polygon, shade_def @@ -184,15 +194,16 @@ def door_to_inp(door): ref_plane = Plane(rel_plane.n, parent_llc, proj_x) min_2d = ref_plane.xyz_to_xy(apt_llc) max_2d = ref_plane.xyz_to_xy(apt_urc) - width = max_2d.x - min_2d.x - height = max_2d.y - min_2d.y + width = round(max_2d.x - min_2d.x, GEO_DEC_COUNT) + height = round(max_2d.y - min_2d.y, GEO_DEC_COUNT) # create the aperture definition doe2_id = clean_doe2_string(door.identifier, GEO_CHARS) constr_o_name = door.properties.energy.construction.identifier constr = clean_doe2_string(constr_o_name, RES_CHARS) keywords = ('X', 'Y', 'WIDTH', 'HEIGHT', 'CONSTRUCTION') - values = (min_2d.x, min_2d.y, width, height, constr) + values = (round(min_2d.x, GEO_DEC_COUNT), round(min_2d.y, GEO_DEC_COUNT), + width, height, '"{}"'.format(constr)) door_def = generate_inp_string(doe2_id, 'DOOR', keywords, values) return door_def @@ -234,15 +245,16 @@ def aperture_to_inp(aperture): ref_plane = Plane(rel_plane.n, parent_llc, proj_x) min_2d = ref_plane.xyz_to_xy(apt_llc) max_2d = ref_plane.xyz_to_xy(apt_urc) - width = max_2d.x - min_2d.x - height = max_2d.y - min_2d.y + width = round(max_2d.x - min_2d.x, GEO_DEC_COUNT) + height = round(max_2d.y - min_2d.y, GEO_DEC_COUNT) # create the aperture definition doe2_id = clean_doe2_string(aperture.identifier, GEO_CHARS) constr_o_name = aperture.properties.energy.construction.identifier constr = clean_doe2_string(constr_o_name, RES_CHARS) keywords = ('X', 'Y', 'WIDTH', 'HEIGHT', 'GLASS-TYPE') - values = (min_2d.x, min_2d.y, width, height, constr) + values = (round(min_2d.x, GEO_DEC_COUNT), round(min_2d.y, GEO_DEC_COUNT), + width, height, '"{}"'.format(constr)) aperture_def = generate_inp_string(doe2_id, 'WINDOW', keywords, values) return aperture_def @@ -292,7 +304,10 @@ def face_to_inp(face, space_origin=Point3D(0, 0, 0)): constr_o_name = face.properties.energy.construction.identifier constr = clean_doe2_string(constr_o_name, RES_CHARS) keywords = ['POLYGON', 'CONSTRUCTION', 'TILT', 'AZIMUTH', 'X', 'Y', 'Z'] - values = ['"{} Plg"'.format(doe2_id), constr, tilt, az, origin.x, origin.y, origin.z] + values = ['"{} Plg"'.format(doe2_id), '"{}"'.format(constr), tilt, az, + round(origin.x, GEO_DEC_COUNT), + round(origin.y, GEO_DEC_COUNT), + round(origin.z, GEO_DEC_COUNT)] if bc_str == 'Surface': adj_room = face.boundary_condition.boundary_condition_objects[-1] adj_id = clean_doe2_string(adj_room, GEO_CHARS) @@ -376,9 +391,10 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals origin = space_origin - floor_origin # create the space definition, which includes the position info - keywords = ['SHAPE', 'POLYGON', 'AZIMUTH', 'X', 'Y', 'Z' 'VOLUME'] + keywords = ['SHAPE', 'POLYGON', 'AZIMUTH', 'X', 'Y', 'Z', 'VOLUME'] values = ['POLYGON', '"{} Plg"'.format(doe2_id), 0, - origin.x, origin.y, origin.z, room.volume] + round(origin.x, GEO_DEC_COUNT), round(origin.y, GEO_DEC_COUNT), + round(origin.z, GEO_DEC_COUNT), round(room.volume, GEO_DEC_COUNT)] if room.multiplier != 1: keywords.append('MULTIPLIER') values.append(room.multiplier) @@ -484,7 +500,7 @@ def model_to_inp( # convert all of the Aperture geometries to rectangles so they can be translated model.rectangularize_apertures( subdivision_distance=RECT_WIN_SUBD, max_separation=0.0, - merge_all=True, resolve_adjacency=True + merge_all=True, resolve_adjacency=False ) # reset identifiers to make them unique and derived from the display names model.reset_ids() @@ -631,7 +647,7 @@ def model_to_inp( model_str.append(hvac_def) for room in rooms: space_name = clean_doe2_string(room.identifier, GEO_CHARS) - zone_name = '_Zn'.format(space_name) + zone_name = '{}_Zn'.format(space_name) zone_type = room_doe2_conditioning_type(room) heat_setpt, cool_setpt = 72, 75 setpoint = room.properties.energy.setpoint @@ -659,7 +675,7 @@ def model_to_inp( 'Hourly Reporting', 'THE END') for report in report_types: model_str.append(header_comment_minor(report)) - model_str = ['END ..\nCOMPUTE ..\nSTOP ..\n'] + model_str.append('END ..\nCOMPUTE ..\nSTOP ..\n') return '\n'.join(model_str) diff --git a/requirements.txt b/requirements.txt index c56a809..0271721 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -honeybee-energy>=1.105.45 +honeybee-energy>=1.105.48 diff --git a/tests/cli_test.py b/tests/cli_test.py new file mode 100644 index 0000000..75ac554 --- /dev/null +++ b/tests/cli_test.py @@ -0,0 +1,23 @@ +"""Test the CLI commands""" + +import os +from click.testing import CliRunner + +from honeybee_doe2.cli.translate import model_to_inp_file + + +def test_model_to_inp_cli(): + runner = CliRunner() + input_hb_model = './tests/assets/shade_test.hbjson' + out_file = './tests/assets/cli_test.inp' + hvac_mapping = 'Story' + + in_args = [ + input_hb_model, '--hvac-mapping', hvac_mapping, + '--exclude-interior-walls', '--exclude-interior-ceilings', + '--output-file', out_file] + result = runner.invoke(model_to_inp_file, in_args) + + assert result.exit_code == 0 + assert os.path.isfile(out_file) + os.remove(out_file) \ No newline at end of file diff --git a/tests/construction_test.py b/tests/construction_test.py index 776650c..60126ef 100644 --- a/tests/construction_test.py +++ b/tests/construction_test.py @@ -52,17 +52,17 @@ def test_opaque_construction_to_inp(): assert inp_str == \ '"Generic Wall Construction_l" = LAYERS\n' \ ' MATERIAL = (\n' \ - ' Concrete,\n' \ - ' Insulation R-3,\n' \ - ' Wall Air Gap,\n' \ - ' Gypsum,\n' \ + ' "Concrete",\n' \ + ' "Insulation R-3",\n' \ + ' "Wall Air Gap",\n' \ + ' "Gypsum",\n' \ ' )\n' \ ' ..\n' \ '"Generic Wall Construction" = CONSTRUCTION\n' \ ' TYPE = LAYERS\n' \ ' ABSORPTANCE = 0.75\n' \ ' ROUGHNESS = 3\n' \ - ' LAYERS = Generic Wall Construction_l\n' \ + ' LAYERS = "Generic Wall Construction_l"\n' \ ' ..\n' diff --git a/tests/schedule_test.py b/tests/schedule_test.py index f66d301..a58cd53 100644 --- a/tests/schedule_test.py +++ b/tests/schedule_test.py @@ -151,11 +151,11 @@ def test_schedule_ruleset_to_inp(): assert inp_yr_str == \ '"Office Occupancy" = SCHEDULE\n' \ ' TYPE = FRACTION\n' \ - ' THRU DEC 31 = "Office Occupancy_Week 1"\n' \ + ' THRU DEC 31 = "Office Occupancy Week 1"\n' \ ' ..\n' assert len(inp_week_strs) == 1 assert inp_week_strs[0] == \ - '"Office Occupancy_Week 1" = WEEK-SCHEDULE\n' \ + '"Office Occupancy Week 1" = WEEK-SCHEDULE\n' \ ' TYPE = FRACTION\n' \ ' DAYS = (MON)\n' \ ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ @@ -213,15 +213,15 @@ def test_schedule_ruleset_to_inp_date_range(): assert inp_yr_str == \ '"School Occupancy" = SCHEDULE\n' \ ' TYPE = FRACTION\n' \ - ' THRU JUN 30 = "School Occupancy_Week 1"\n' \ - ' THRU SEP 1 = "School Occupancy_Week 2"\n' \ - ' THRU DEC 31 = "School Occupancy_Week 1"\n' \ + ' THRU JUN 30 = "School Occupancy Week 1"\n' \ + ' THRU SEP 1 = "School Occupancy Week 2"\n' \ + ' THRU DEC 31 = "School Occupancy Week 1"\n' \ ' ..\n' \ or inp_yr_str == \ '"School Occupancy" = SCHEDULE\n' \ ' TYPE = FRACTION\n' \ - ' THRU JUN 30 = "School Occupancy_Week 2"\n' \ - ' THRU SEP 1 = "School Occupancy_Week 1"\n' \ - ' THRU DEC 31 = "School Occupancy_Week 2"\n' \ + ' THRU JUN 30 = "School Occupancy Week 2"\n' \ + ' THRU SEP 1 = "School Occupancy Week 1"\n' \ + ' THRU DEC 31 = "School Occupancy Week 2"\n' \ ' ..\n' assert len(inp_week_strs) == 2 diff --git a/tests/simulation_test.py b/tests/simulation_test.py index e146d62..09b7b20 100644 --- a/tests/simulation_test.py +++ b/tests/simulation_test.py @@ -44,7 +44,7 @@ def test_site_data_to_inp(): def test_simulation_par_to_inp(): """Test the SimulationPar to_inp method.""" - title = 'Sample_Project' + title = 'Sample Project' simulation_par = SimulationPar(title) simulation_par.site.altitude = 100 inp_str = simulation_par.to_inp() @@ -65,7 +65,7 @@ def test_simulation_par_init(): assert sim_par.site == SiteData() sim_par_dup = sim_par.duplicate() - sim_par_alt = SimulationPar(title='Sample_Project') + sim_par_alt = SimulationPar(title='Sample Project') assert sim_par is sim_par assert sim_par is not sim_par_dup assert sim_par == sim_par_dup diff --git a/tests/writer_test.py b/tests/writer_test.py index 6458da3..439a3c6 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -1,13 +1,346 @@ -"""Tests the features that honeybee_energy adds to honeybee_core Model.""" +"""Test the translators for geometry to INP.""" from ladybug_geometry.geometry3d import Point3D, Vector3D, Mesh3D from honeybee.model import Model from honeybee.room import Room +from honeybee.face import Face +from honeybee.aperture import Aperture +from honeybee.door import Door +from honeybee.shade import Shade from honeybee.shademesh import ShadeMesh +from honeybee_energy.construction.opaque import OpaqueConstruction +from honeybee_energy.material.opaque import EnergyMaterial +from honeybee_energy.schedule.ruleset import ScheduleRuleset +import honeybee_energy.lib.scheduletypelimits as schedule_types +from honeybee_energy.lib.programtypes import office_program -def test_inp_writer(): - """Test the existence of the Model inp reiter.""" + +def test_shade_writer(): + """Test the basic functionality of the Shade inp writer.""" + shade = Shade.from_vertices( + 'overhang', [[0, 0, 3], [1, 0, 3], [1, 1, 3], [0, 1, 3]]) + + shade_polygon, shade_def = shade.to.inp(shade) + assert shade_polygon == \ + '"overhang Plg" = POLYGON\n' \ + ' V1 = (0.0, 0.0)\n' \ + ' V2 = (1.0, 0.0)\n' \ + ' V3 = (1.0, 1.0)\n' \ + ' V4 = (0.0, 1.0)\n' \ + ' ..\n' + assert shade_def == \ + '"overhang" = BUILDING-SHADE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "overhang Plg"\n' \ + ' TRANSMITTANCE = 0\n' \ + ' X-REF = 0.0\n' \ + ' Y-REF = 0.0\n' \ + ' Z-REF = 3.0\n' \ + ' TILT = 0.0\n' \ + ' AZIMUTH = 0.0\n' \ + ' ..\n' + + fritted_glass_trans = ScheduleRuleset.from_constant_value( + 'Fritted Glass', 0.5, schedule_types.fractional) + shade.properties.energy.transmittance_schedule = fritted_glass_trans + shade_polygon, shade_def = shade.to.inp(shade) + assert shade_def == \ + '"overhang" = BUILDING-SHADE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "overhang Plg"\n' \ + ' TRANSMITTANCE = 0.5\n' \ + ' X-REF = 0.0\n' \ + ' Y-REF = 0.0\n' \ + ' Z-REF = 3.0\n' \ + ' TILT = 0.0\n' \ + ' AZIMUTH = 0.0\n' \ + ' ..\n' + + +def test_shade_mesh_writer(): + """Test the basic functionality of the ShadeMesh inp writer.""" + pts = (Point3D(0, 0, 4), Point3D(0, 2, 4), Point3D(2, 2, 4), + Point3D(2, 0, 4), Point3D(4, 0, 4)) + mesh = Mesh3D(pts, [(0, 1, 2, 3), (2, 3, 4)]) + shade = ShadeMesh('Awning_1', mesh) + + shade_polygons, shade_defs = shade.to.inp(shade) + assert len(shade_polygons) == 2 + assert len(shade_defs) == 2 + assert shade_polygons[0] == \ + '"Awning 10 Plg" = POLYGON\n' \ + ' V1 = (0.0, 0.0)\n' \ + ' V2 = (2.0, 0.0)\n' \ + ' V3 = (2.0, 2.0)\n' \ + ' V4 = (0.0, 2.0)\n' \ + ' ..\n' + assert shade_defs[0] == \ + '"Awning 10" = FIXED-SHADE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "Awning 10 Plg"\n' \ + ' TRANSMITTANCE = 0\n' \ + ' X-REF = 0.0\n' \ + ' Y-REF = 0.0\n' \ + ' Z-REF = 4.0\n' \ + ' TILT = 0.0\n' \ + ' AZIMUTH = 0.0\n' \ + ' ..\n' + + fritted_glass_trans = ScheduleRuleset.from_constant_value( + 'Fritted Glass', 0.5, schedule_types.fractional) + shade.properties.energy.transmittance_schedule = fritted_glass_trans + shade_polygons, shade_defs = shade.to.inp(shade) + assert shade_defs[0] == \ + '"Awning 10" = FIXED-SHADE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "Awning 10 Plg"\n' \ + ' TRANSMITTANCE = 0.5\n' \ + ' X-REF = 0.0\n' \ + ' Y-REF = 0.0\n' \ + ' Z-REF = 4.0\n' \ + ' TILT = 0.0\n' \ + ' AZIMUTH = 0.0\n' \ + ' ..\n' + + +def test_aperture_writer(): + """Test the basic functionality of the Aperture inp writer.""" + vertices_parent_wall = [[0, 0, 0], [0, 10, 0], [0, 10, 3], [0, 0, 3]] + vertices_parent_wall_2 = list(reversed(vertices_parent_wall)) + vertices_wall = [[0, 1, 1], [0, 3, 1], [0, 3, 2.5], [0, 1, 2.5]] + vertices_wall_2 = list(reversed(vertices_wall)) + vertices_parent_roof = [[10, 0, 3], [10, 10, 3], [0, 10, 3], [0, 0, 3]] + vertices_roof = [[4, 1, 3], [4, 4, 3], [1, 4, 3], [1, 1, 3]] + + wf = Face.from_vertices('wall_face', vertices_parent_wall) + wa = Aperture.from_vertices('wall_window', vertices_wall) + wf.add_aperture(wa) + Room('Test_Room_1', [wf]) + assert wa.properties.energy.construction.identifier == 'Generic Double Pane' + inp_str = wa.to.inp(wa) + assert inp_str == \ + '"wall window" = WINDOW\n' \ + ' X = 1.0\n' \ + ' Y = 1.0\n' \ + ' WIDTH = 2.0\n' \ + ' HEIGHT = 1.5\n' \ + ' GLASS-TYPE = "Generic Double Pane"\n' \ + ' ..\n' + + wf2 = Face.from_vertices('wall_face2', vertices_parent_wall_2) + wa2 = Aperture.from_vertices('wall_window2', vertices_wall_2) + wf2.add_aperture(wa2) + Room('Test_Room_2', [wf2]) + wa.set_adjacency(wa2) + assert wa.properties.energy.construction.identifier == 'Generic Single Pane' + inp_str = wa.to.inp(wa) + assert inp_str == \ + '"wall window" = WINDOW\n' \ + ' X = 1.0\n' \ + ' Y = 1.0\n' \ + ' WIDTH = 2.0\n' \ + ' HEIGHT = 1.5\n' \ + ' GLASS-TYPE = "Generic Single Pane"\n' \ + ' ..\n' + + rf = Face.from_vertices('roof_face', vertices_parent_roof) + ra = Aperture.from_vertices('roof_window', vertices_roof) + rf.add_aperture(ra) + Room('Test_Room_1', [rf]) + assert ra.properties.energy.construction.identifier == 'Generic Double Pane' + inp_str = ra.to.inp(ra) + assert inp_str == \ + '"roof window" = WINDOW\n' \ + ' X = 1.0\n' \ + ' Y = 1.0\n' \ + ' WIDTH = 3.0\n' \ + ' HEIGHT = 3.0\n' \ + ' GLASS-TYPE = "Generic Double Pane"\n' \ + ' ..\n' + + +def test_door_writer(): + """Test the basic functionality of the Door inp writer.""" + vertices_parent_wall = [[0, 0, 0], [0, 10, 0], [0, 10, 3], [0, 0, 3]] + vertices_wall = [[0, 1, 0.1], [0, 2, 0.1], [0, 2, 2.8], [0, 1, 2.8]] + vertices_parent_roof = [[10, 0, 3], [10, 10, 3], [0, 10, 3], [0, 0, 3]] + vertices_roof = [[4, 3, 3], [4, 4, 3], [3, 4, 3], [3, 3, 3]] + + wf = Face.from_vertices('wall_face', vertices_parent_wall) + wd = Door.from_vertices('wall_door', vertices_wall) + wf.add_door(wd) + Room('Test_Room_1', [wf]) + assert wd.properties.energy.construction.identifier == 'Generic Exterior Door' + inp_str = wd.to.inp(wd) + assert inp_str == \ + '"wall door" = DOOR\n' \ + ' X = 1.0\n' \ + ' Y = 0.1\n' \ + ' WIDTH = 1.0\n' \ + ' HEIGHT = 2.7\n' \ + ' CONSTRUCTION = "Generic Exterior Door"\n' \ + ' ..\n' + + rf = Face.from_vertices('roof_face', vertices_parent_roof) + rd = Door.from_vertices('roof_door', vertices_roof) + rf.add_door(rd) + Room('Test_Room_1', [rf]) + assert rd.properties.energy.construction.identifier == 'Generic Exterior Door' + inp_str = rd.to.inp(rd) + assert inp_str == \ + '"roof door" = DOOR\n' \ + ' X = 3.0\n' \ + ' Y = 3.0\n' \ + ' WIDTH = 1.0\n' \ + ' HEIGHT = 1.0\n' \ + ' CONSTRUCTION = "Generic Exterior Door"\n' \ + ' ..\n' + + +def test_face_writer(): + """Test the basic functionality of the Face inp writer.""" + concrete20 = EnergyMaterial('20cm Concrete', 0.2, 2.31, 2322, 832, + 'MediumRough', 0.95, 0.75, 0.8) + thick_constr = OpaqueConstruction( + 'Thick Concrete Construction', [concrete20]) + + wall_pts = [[0, 0, 0], [10, 0, 0], [10, 0, 10], [0, 0, 10]] + roof_pts = [[0, 0, 3], [10, 0, 3], [10, 10, 3], [0, 10, 3]] + floor_pts = [[0, 0, 0], [0, 10, 0], [10, 10, 0], [10, 0, 0]] + + face = Face.from_vertices('wall_face', wall_pts) + face.properties.energy.construction = thick_constr + face_polygon, face_def = face.to.inp(face) + assert face_polygon == \ + '"wall face Plg" = POLYGON\n' \ + ' V1 = (0.0, 0.0)\n' \ + ' V2 = (10.0, 0.0)\n' \ + ' V3 = (10.0, 10.0)\n' \ + ' V4 = (0.0, 10.0)\n' \ + ' ..\n' + assert face_def == \ + '"wall face" = EXTERIOR-WALL\n' \ + ' POLYGON = "wall face Plg"\n' \ + ' CONSTRUCTION = "Thick Concrete Construction"\n' \ + ' TILT = 90.0\n' \ + ' AZIMUTH = 180.0\n' \ + ' X = 0.0\n' \ + ' Y = 0.0\n' \ + ' Z = 0.0\n' \ + ' ..\n' + + face = Face.from_vertices('roof_face', roof_pts) + face.properties.energy.construction = thick_constr + face_polygon, face_def = face.to.inp(face) + assert face_polygon == \ + '"roof face Plg" = POLYGON\n' \ + ' V1 = (0.0, 0.0)\n' \ + ' V2 = (10.0, 0.0)\n' \ + ' V3 = (10.0, 10.0)\n' \ + ' V4 = (0.0, 10.0)\n' \ + ' ..\n' + assert face_def == \ + '"roof face" = ROOF\n' \ + ' POLYGON = "roof face Plg"\n' \ + ' CONSTRUCTION = "Thick Concrete Construction"\n' \ + ' TILT = 0.0\n' \ + ' AZIMUTH = 0.0\n' \ + ' X = 0.0\n' \ + ' Y = 0.0\n' \ + ' Z = 3.0\n' \ + ' ..\n' + + face = Face.from_vertices('floor_face', floor_pts) + face.properties.energy.construction = thick_constr + face_polygon, face_def = face.to.inp(face) + assert face_polygon == \ + '"floor face Plg" = POLYGON\n' \ + ' V1 = (0.0, 0.0)\n' \ + ' V2 = (-10.0, 0.0)\n' \ + ' V3 = (-10.0, -10.0)\n' \ + ' V4 = (0.0, -10.0)\n' \ + ' ..\n' + assert face_def == \ + '"floor face" = UNDERGROUND-WALL\n' \ + ' POLYGON = "floor face Plg"\n' \ + ' CONSTRUCTION = "Thick Concrete Construction"\n' \ + ' TILT = 180.0\n' \ + ' AZIMUTH = 0.0\n' \ + ' X = 10.0\n' \ + ' Y = 0.0\n' \ + ' Z = 0.0\n' \ + ' LOCATION = BOTTOM\n' \ + ' ..\n' + + +def test_room_writer(): + """Test the basic functionality of the Room inp writer.""" + room = Room.from_box('Tiny_House_Zone', 15, 30, 10) + south_face = room[3] + south_face.apertures_by_ratio(0.4, 0.01) + south_face.apertures[0].overhang(0.5, indoor=False) + south_face.apertures[0].overhang(0.5, indoor=True) + south_face.apertures[0].move_shades(Vector3D(0, 0, -0.5)) + + room_polygons, room_def = room.to.inp(room) + assert room_polygons[0] == \ + '"Tiny House Zone Plg" = POLYGON\n' \ + ' V1 = (0.0, 0.0)\n' \ + ' V2 = (15.0, 0.0)\n' \ + ' V3 = (15.0, 30.0)\n' \ + ' V4 = (0.0, 30.0)\n' \ + ' ..\n' + assert room_def[0] == \ + '"Tiny House Zone" = SPACE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "Tiny House Zone Plg"\n' \ + ' AZIMUTH = 0\n' \ + ' X = 0.0\n' \ + ' Y = 0.0\n' \ + ' Z = 0.0\n' \ + ' VOLUME = 4500\n' \ + ' ZONE-TYPE = UNCONDITIONED\n' \ + ' ..\n' + + room.properties.energy.program_type = office_program + room.properties.energy.add_default_ideal_air() + room_polygons, room_def = room.to.inp(room) + assert room_polygons[0] == \ + '"Tiny House Zone Plg" = POLYGON\n' \ + ' V1 = (0.0, 0.0)\n' \ + ' V2 = (15.0, 0.0)\n' \ + ' V3 = (15.0, 30.0)\n' \ + ' V4 = (0.0, 30.0)\n' \ + ' ..\n' + assert room_def[0] == \ + '"Tiny House Zone" = SPACE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "Tiny House Zone Plg"\n' \ + ' AZIMUTH = 0\n' \ + ' X = 0.0\n' \ + ' Y = 0.0\n' \ + ' Z = 0.0\n' \ + ' VOLUME = 4500\n' \ + ' ZONE-TYPE = CONDITIONED\n' \ + ' NUMBER-OF-PEOPLE = 2.362\n' \ + ' PEOPLE-SCHEDULE = "Generic Office Occupancy"\n' \ + ' LIGHTING-W/AREA = 0.98\n' \ + ' LIGHTING-SCHEDULE = "Generic Office Lighting"\n' \ + ' LIGHT-TO-RETURN = 0.0\n' \ + ' EQUIPMENT-W/AREA = 0.96\n' \ + ' EQUIPMENT-SCHEDULE = "Generic Office Equipment"\n' \ + ' EQUIP-SENSIBLE = 1.0\n' \ + ' EQUIP-LATENT = 0.0\n' \ + ' EQUIP-RAD-FRAC = 0.5\n' \ + ' INF-METHOD = AIR-CHANGE\n' \ + ' INF-FLOW/AREA = 0.045\n' \ + ' INF-SCHEDULE = "Generic Office Infiltration"\n' \ + ' ..\n' + + +def test_model_writer(): + """Test the basic functionality of the Model inp writer.""" room = Room.from_box('Tiny_House_Zone', 5, 10, 3) south_face = room[3] south_face.apertures_by_ratio(0.4, 0.01) @@ -21,4 +354,48 @@ def test_inp_writer(): model = Model('Tiny_House', [room], shade_meshes=[awning_1]) - assert hasattr(model.to, 'inp') + inp_str = model.to.inp(model) + assert inp_str.startswith('INPUT ..\n\n') + assert inp_str.endswith('END ..\nCOMPUTE ..\nSTOP ..\n') + + +def test_model_writer_from_standard_hbjson(): + """Test translating a HBJSON to an INP string.""" + standard_test = './tests/assets/2023_rac_advanced_sample_project.hbjson' + hb_model = Model.from_file(standard_test) + + inp_str = hb_model.to.inp(hb_model, hvac_mapping='Model') + assert inp_str.startswith('INPUT ..\n\n') + assert inp_str.endswith('END ..\nCOMPUTE ..\nSTOP ..\n') + + +def test_model_writer_from_hvac_hbjson(): + """Test translating a HBJSON to an INP string.""" + hvac_test = './tests/assets/multi_hvac.hbjson' + hb_model = Model.from_file(hvac_test) + + inp_str = hb_model.to.inp(hb_model, hvac_mapping='AssignedHVAC') + assert inp_str.startswith('INPUT ..\n\n') + assert inp_str.endswith('END ..\nCOMPUTE ..\nSTOP ..\n') + + +def test_model_writer_from_air_wall_hbjson(): + """Test translating a HBJSON to an INP string.""" + air_wall_test = './tests/assets/Air_Wall_test.hbjson' + hb_model = Model.from_file(air_wall_test) + + inp_str = hb_model.to.inp(hb_model, hvac_mapping='Room') + assert inp_str.startswith('INPUT ..\n\n') + assert inp_str.endswith('END ..\nCOMPUTE ..\nSTOP ..\n') + + +def test_model_writer_from_ceil_adj_hbjson(): + """Test translating a HBJSON to an INP string.""" + ceiling_adj_test = './tests/assets/ceiling_adj_test.hbjson' + hb_model = Model.from_file(ceiling_adj_test) + + inp_str = hb_model.to.inp(hb_model, hvac_mapping='AssignedHVAC') + assert inp_str.startswith('INPUT ..\n\n') + assert inp_str.endswith('END ..\nCOMPUTE ..\nSTOP ..\n') + + From f5a6a3e194fae6064b8d58e834c1e918eae1a19b Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Fri, 26 Apr 2024 17:45:23 -0700 Subject: [PATCH 09/27] fix(writer): Fix more bugs --- honeybee_doe2/cli/translate.py | 65 ++++++++++++++++++++++++++++++++++ honeybee_doe2/construction.py | 6 ++-- honeybee_doe2/writer.py | 5 +-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/honeybee_doe2/cli/translate.py b/honeybee_doe2/cli/translate.py index a276059..79322e9 100644 --- a/honeybee_doe2/cli/translate.py +++ b/honeybee_doe2/cli/translate.py @@ -83,6 +83,71 @@ def model_to_inp_file( # write out the INP file if folder is not None and name is not None: + if not name.lower().endswith('.inp'): + name = name + '.inp' + write_to_file_by_name(folder, name, inp_str, True) + else: + output_file.write(inp_str) + except Exception as e: + _logger.exception(f'Model translation failed:\n{e}') + sys.exit(1) + else: + sys.exit(0) + + +@translate.command('hbjson-to-inp') +@click.argument('model-file', type=click.Path( + exists=True, file_okay=True, dir_okay=False, resolve_path=True)) +@click.option( + '--hvac-mapping', '-hm', help='Text to indicate how HVAC systems should be ' + 'assigned to the exported model. Story will assign one HVAC system for each ' + 'distinct level polygon, Model will use only one HVAC system for the whole model ' + 'and AssignedHVAC will follow how the HVAC systems have been assigned to the' + 'Rooms.properties.energy.hvac. Choose from: Room, Story, Model, AssignedHVAC', + default='Story', show_default=True, type=str) +@click.option( + '--include-interior-walls/--exclude-interior-walls', ' /-xw', help='Flag to note ' + 'whether interior walls should be excluded from the export.', + default=True, show_default=True) +@click.option( + '--include-interior-ceilings/--exclude-interior-ceilings', ' /-xc', help='Flag to ' + 'note whether interior ceilings should be excluded from the export.', + default=True, show_default=True) +@click.option( + '--verbose-properties/--switch-statements', ' /-ss', help='Flag to note whether ' + 'program types should be written with switch statements so that they can easily ' + 'be edited in eQuest or a verbose definition of loads should be written for ' + 'each Room/Space.', default=True, show_default=True) +@click.option( + '--name', '-n', help='Deprecated option to set the name of the output file.', + default=None, show_default=True) +@click.option( + '--folder', '-f', help='Deprecated option to set the path to target folder.', + type=click.Path(file_okay=False, resolve_path=True, dir_okay=True), default=None) +@click.option( + '--output-file', '-o', help='Optional INP file path to output the INP string ' + 'of the translation. By default this will be printed out to stdout.', + type=click.File('w'), default='-', show_default=True) +def hbjson_to_inp_file( + model_file, hvac_mapping, include_interior_walls, + include_interior_ceilings, verbose_properties, name, folder, output_file +): + """Translate a Model (HBJSON) file to an INP file. + + \b + Args: + model_file: Full path to a Honeybee Model file (HBJSON or HBpkl).""" + try: + print('This method is deprecated and you should use model-to-inp instead.') + model = Model.from_file(model_file) + x_int_w = not include_interior_walls + x_int_c = not include_interior_ceilings + inp_str = model.to.inp( + model, hvac_mapping=hvac_mapping, exclude_interior_walls=x_int_w, + exclude_interior_ceilings=x_int_c) + if folder is not None and name is not None: + if not name.lower().endswith('.inp'): + name = name + '.inp' write_to_file_by_name(folder, name, inp_str, True) else: output_file.write(inp_str) diff --git a/honeybee_doe2/construction.py b/honeybee_doe2/construction.py index 249ffe1..4d1124a 100644 --- a/honeybee_doe2/construction.py +++ b/honeybee_doe2/construction.py @@ -30,7 +30,7 @@ def opaque_material_to_inp(material): material.thickness < MIN_LAYER_THICKNESS: r_val = RValue().to_unit([material.r_value], 'h-ft2-F/Btu', 'm2-K/W')[0] keywords = ('TYPE', 'RESISTANCE') - values = ('RESISTANCE', round(r_val, 3)) + values = ('RESISTANCE', round(r_val, 6)) return generate_inp_string(doe2_id, 'MATERIAL', keywords, values) # write out detailed properties for the material thickness = round(Distance().to_unit([material.thickness], 'ft', 'm')[0], 3) @@ -70,7 +70,7 @@ def window_construction_to_inp(construction): shading_coef = construction.shgc / 0.87 glass_cond = UValue().to_unit([construction.u_factor], 'Btu/h-ft2-F', 'W/m2-K')[0] keywords = ('TYPE', 'SHADING-COEF', 'GLASS-CONDUCT') - values = ('SHADING-COEF', round(shading_coef, 3), round(glass_cond, 3)) + values = ('SHADING-COEF', round(shading_coef, 3), round(glass_cond, 6)) return generate_inp_string(doe2_id, 'GLASS-TYPE', keywords, values) @@ -82,7 +82,7 @@ def door_construction_to_inp(construction): doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) constr_cond = UValue().to_unit([construction.u_factor], 'Btu/h-ft2-F', 'W/m2-K')[0] keywords = ('TYPE', 'U-VALUE') - values = ('U-VALUE', round(constr_cond, 3)) + values = ('U-VALUE', round(constr_cond, 6)) return generate_inp_string(doe2_id, 'CONSTRUCTION', keywords, values) diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 150b6c0..55d73df 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -63,8 +63,9 @@ def face_3d_to_inp(face_3d, parent_name='HB object'): ref_plane = Plane(face_3d.normal, llc_origin, proj_x) vertices = [ref_plane.xyz_to_xy(pt) for pt in pts_3d] else: # horizontal; ensure vertices are always counterclockwise from above + azimuth = 180 llc = Point2D(llc_origin.x, llc_origin.y) - vertices = [Point2D(v[0] - llc.x, v[1] - llc.y) for v in pts_3d] + vertices = [Point2D(v.x - llc.x, v.y - llc.y) for v in pts_3d] if tilt > 180 - DOE2_ANGLE_TOL: vertices = [Point2D(v.x, -v.y) for v in vertices] @@ -657,7 +658,7 @@ def model_to_inp( zone_keys = ('TYPE', 'DESIGN-HEAT-T', 'DESIGN-COOL-T', 'SIZING-OPTION', 'SPACE') zone_vals = (zone_type, heat_setpt, cool_setpt, - 'ADJUST-LOADS', space_name) + 'ADJUST-LOADS', '"{}"'.format(space_name)) zone_def = generate_inp_string(zone_name, 'ZONE', zone_keys, zone_vals) model_str.append(zone_def) From aa5985690269250ec657c8a44a765af5ae1a3196 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Fri, 26 Apr 2024 21:14:47 -0700 Subject: [PATCH 10/27] fix(package): Fix a list few bugs identified though eQuest importing --- honeybee_doe2/construction.py | 6 ++++++ honeybee_doe2/load.py | 4 ++-- honeybee_doe2/schedule.py | 36 ++++++++++++++++++----------------- honeybee_doe2/writer.py | 15 +++++++++------ requirements.txt | 2 +- tests/construction_test.py | 12 ++++++------ tests/schedule_test.py | 34 +++++++++++++-------------------- tests/writer_test.py | 18 +++++++++--------- 8 files changed, 65 insertions(+), 62 deletions(-) diff --git a/honeybee_doe2/construction.py b/honeybee_doe2/construction.py index 4d1124a..26902d3 100644 --- a/honeybee_doe2/construction.py +++ b/honeybee_doe2/construction.py @@ -49,6 +49,12 @@ def opaque_construction_to_inp(construction): it does NOT include the constituent MATERIAL definitions and their properties. """ doe2_id = clean_doe2_string(construction.identifier, RES_CHARS) + # if the construction has no heat capacity, simply make a U-VALUE construction + if construction.area_heat_capacity == 0: + con_cond = UValue().to_unit([construction.u_factor], 'Btu/h-ft2-F', 'W/m2-K')[0] + keywords = ('TYPE', 'U-VALUE') + values = ('U-VALUE', round(con_cond, 6)) + return generate_inp_string(doe2_id, 'CONSTRUCTION', keywords, values) # create the specification of material layers layer_id = '{}_l'.format(doe2_id) layers = ['"{}"'.format(clean_doe2_string(mat, RES_CHARS)) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index dea671f..c3e9f4d 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -58,12 +58,12 @@ def equipment_to_inp(room): epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] epd = round(epd, 3) eqp_sch = clean_doe2_string(equip.schedule.display_name, RES_CHARS) - eqp_sch = '"{}"'.format(eqp_sch) + eqp_sch = '("{}")'.format(eqp_sch) sens_fract = 1 - equip.latent_fraction - equip.lost_fraction equip_val = (epd, eqp_sch, sens_fract, equip.latent_fraction, equip.radiant_fraction) - equip_kwd = ('EQUIPMENT-W/AREA', 'EQUIPMENT-SCHEDULE', + equip_kwd = ('EQUIPMENT-W/AREA', 'EQUIP-SCHEDULE', 'EQUIP-SENSIBLE', 'EQUIP-LATENT', 'EQUIP-RAD-FRAC') return equip_kwd, equip_val diff --git a/honeybee_doe2/schedule.py b/honeybee_doe2/schedule.py index 2fc2eb0..d1f2fab 100644 --- a/honeybee_doe2/schedule.py +++ b/honeybee_doe2/schedule.py @@ -6,7 +6,7 @@ from honeybee.typing import clean_doe2_string from .config import RES_CHARS -from .util import generate_inp_string +from .util import generate_inp_string, generate_inp_string_list_format def schedule_type_limit_to_inp(type_limit): @@ -33,9 +33,9 @@ def schedule_day_to_inp(day_schedule, type_limit=None): # setup a function to format list of values correctly def _format_day_values(values_to_format): if len(values_to_format) == 1: - return'({})'.format(values_to_format[0]) + return'({})'.format(round(values_to_format[0], 3)) else: - return str(tuple(values_to_format)) + return str(tuple(round(v, 3) for v in values_to_format)) # loop through the hourly values and write them in the format DOE-2 likes prev_count, prev_hour, prev_values = 0, 1, [hour_values[0]] @@ -97,8 +97,10 @@ def schedule_ruleset_to_inp(schedule): # setup the DOE-2 identifier and lists for keywords and values doe2_id = clean_doe2_string(schedule.identifier, RES_CHARS) type_text = schedule_type_limit_to_inp(schedule.schedule_type_limit) - day_types = ['(MON)', '(TUE)', '(WED)', '(THU)', '(FRI)', '(SAT)', '(SUN)', - '(HOL)', '(HDD)', '(CDD)'] + day_types = [ + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', + 'Sunday', 'Holiday', 'Winter Design Day', 'Summer Design Day' + ] def _get_week_list(schedule, rule_indices): """Get a list of the ScheduleDay identifiers applied on each day of the week.""" @@ -149,13 +151,13 @@ def _inp_week_schedule_from_rule_indices(schedule, rule_indices, week_index): # add extra days (including summer and winter design days) week_fields.extend(_get_extra_week_fields(schedule)) week_keywords, week_values = ['TYPE'], [type_text] + day_list = [] for day_type, day_sch in zip(day_types, week_fields): - week_keywords.append('DAYS') - week_values.append(day_type) - week_keywords.append('DAY-SCHEDULES') - week_values.append('"{}"'.format(day_sch)) - week_schedule = generate_inp_string( - week_sch_id, 'WEEK-SCHEDULE', week_keywords, week_values) + day_list.append('"{}", $ {}'.format(day_sch, day_type)) + week_keywords.append('DAY-SCHEDULES') + week_values.append(day_list) + week_schedule = generate_inp_string_list_format( + week_sch_id, 'WEEK-SCHEDULE-PD', week_keywords, week_values) return week_schedule, week_sch_id def _inp_week_schedule_from_week_list(schedule, week_list, week_index): @@ -167,13 +169,13 @@ def _inp_week_schedule_from_week_list(schedule, week_list, week_index): week_fields.append(week_fields.pop(0)) # DOE-2 starts week on Monday; not Sunday week_fields.extend(_get_extra_week_fields(schedule)) week_keywords, week_values = ['TYPE'], [type_text] + day_list = [] for day_type, day_sch in zip(day_types, week_fields): - week_keywords.append('DAYS') - week_values.append(day_type) - week_keywords.append('DAY-SCHEDULES') - week_values.append('"{}"'.format(day_sch)) - week_schedule = generate_inp_string( - week_sch_id, 'WEEK-SCHEDULE', week_keywords, week_values) + day_list.append('"{}", $ {}'.format(day_sch, day_type)) + week_keywords.append('DAY-SCHEDULES') + week_values.append(day_list) + week_schedule = generate_inp_string_list_format( + week_sch_id, 'WEEK-SCHEDULE-PD', week_keywords, week_values) return week_schedule, week_sch_id # prepare to create a full Schedule:Year diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 55d73df..b010e8a 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -63,7 +63,7 @@ def face_3d_to_inp(face_3d, parent_name='HB object'): ref_plane = Plane(face_3d.normal, llc_origin, proj_x) vertices = [ref_plane.xyz_to_xy(pt) for pt in pts_3d] else: # horizontal; ensure vertices are always counterclockwise from above - azimuth = 180 + azimuth = 180.0 llc = Point2D(llc_origin.x, llc_origin.y) vertices = [Point2D(v.x - llc.x, v.y - llc.y) for v in pts_3d] if tilt > 180 - DOE2_ANGLE_TOL: @@ -104,7 +104,6 @@ def shade_mesh_to_inp(shade_mesh): """ # TODO: Sense when the shade is a rectangle and, if so, translate it without POLYGON # set up collector lists and properties for all shades - shade_type = 'FIXED-SHADE' if shade_mesh.is_detached else 'BUILDING-SHADE' base_id = clean_doe2_string(shade_mesh.identifier, GEO_CHARS) trans = energy_trans_sch_to_transmittance(shade_mesh) keywords = ('SHAPE', 'POLYGON', 'TRANSMITTANCE', @@ -120,7 +119,7 @@ def shade_mesh_to_inp(shade_mesh): values = ('POLYGON', '"{} Plg"'.format(doe2_id), trans, round(origin.x, GEO_DEC_COUNT), round(origin.y, GEO_DEC_COUNT), round(origin.z, GEO_DEC_COUNT), tilt, az) - shade_def = generate_inp_string(doe2_id, shade_type, keywords, values) + shade_def = generate_inp_string(doe2_id, 'FIXED-SHADE', keywords, values) shade_polygons.append(shade_polygon) shade_defs.append(shade_def) return shade_polygons, shade_defs @@ -141,7 +140,6 @@ def shade_to_inp(shade): """ # TODO: Sense when the shade is a rectangle and, if so, translate it without POLYGON # create the polygon string from the geometry - shade_type = 'FIXED-SHADE' if shade.is_detached else 'BUILDING-SHADE' doe2_id = clean_doe2_string(shade.identifier, GEO_CHARS) shd_geo = shade.geometry if shade.altitude > 0 else shade.geometry.flip() clean_geo = shd_geo.remove_colinear_vertices(DOE2_TOLERANCE) @@ -154,7 +152,7 @@ def shade_to_inp(shade): values = ('POLYGON', '"{} Plg"'.format(doe2_id), trans, round(origin.x, GEO_DEC_COUNT), round(origin.y, GEO_DEC_COUNT), round(origin.z, GEO_DEC_COUNT), tilt, az) - shade_def = generate_inp_string(doe2_id, shade_type, keywords, values) + shade_def = generate_inp_string(doe2_id, 'FIXED-SHADE', keywords, values) return shade_polygon, shade_def @@ -200,7 +198,9 @@ def door_to_inp(door): # create the aperture definition doe2_id = clean_doe2_string(door.identifier, GEO_CHARS) - constr_o_name = door.properties.energy.construction.identifier + dr_con = door.properties.energy.construction + constr_o_name = dr_con.identifier if isinstance(dr_con, OpaqueConstruction) \ + else dr_con.identifier + '_d' constr = clean_doe2_string(constr_o_name, RES_CHARS) keywords = ('X', 'Y', 'WIDTH', 'HEIGHT', 'CONSTRUCTION') values = (round(min_2d.x, GEO_DEC_COUNT), round(min_2d.y, GEO_DEC_COUNT), @@ -568,6 +568,9 @@ def model_to_inp( model_str.append(window_construction_to_inp(w_con)) model_str.append(header_comment_minor('Door Construction')) for dr_con in door_constructions: + if not isinstance(dr_con, OpaqueConstruction): + dr_con = dr_con.duplicate() + dr_con.identifier = dr_con.identifier + '_d' model_str.append(door_construction_to_inp(dr_con)) # loop through rooms grouped by floor level and boundary to get polygons diff --git a/requirements.txt b/requirements.txt index 0271721..cfabf46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -honeybee-energy>=1.105.48 +honeybee-energy>=1.105.50 diff --git a/tests/construction_test.py b/tests/construction_test.py index 60126ef..eb0a099 100644 --- a/tests/construction_test.py +++ b/tests/construction_test.py @@ -33,7 +33,7 @@ def test_material_nomass_to_inp(): assert inp_str == \ '"Insulation R-2" = MATERIAL\n' \ ' TYPE = RESISTANCE\n' \ - ' RESISTANCE = 11.357\n' \ + ' RESISTANCE = 11.356527\n' \ ' ..\n' @@ -89,7 +89,7 @@ def test_window_construction_to_inp(): '"NECB Window Construction" = GLASS-TYPE\n' \ ' TYPE = SHADING-COEF\n' \ ' SHADING-COEF = 0.46\n' \ - ' GLASS-CONDUCT = 0.302\n' \ + ' GLASS-CONDUCT = 0.302373\n' \ ' ..\n' inp_str = double_low_e.to_inp() @@ -97,7 +97,7 @@ def test_window_construction_to_inp(): '"Double Low-E Window" = GLASS-TYPE\n' \ ' TYPE = SHADING-COEF\n' \ ' SHADING-COEF = 0.488\n' \ - ' GLASS-CONDUCT = 0.299\n' \ + ' GLASS-CONDUCT = 0.299039\n' \ ' ..\n' inp_str = double_clear.to_inp() @@ -105,7 +105,7 @@ def test_window_construction_to_inp(): '"Double Clear Window" = GLASS-TYPE\n' \ ' TYPE = SHADING-COEF\n' \ ' SHADING-COEF = 0.791\n' \ - ' GLASS-CONDUCT = 0.479\n' \ + ' GLASS-CONDUCT = 0.479229\n' \ ' ..\n' inp_str = triple_clear.to_inp() @@ -113,7 +113,7 @@ def test_window_construction_to_inp(): '"Triple Clear Window" = GLASS-TYPE\n' \ ' TYPE = SHADING-COEF\n' \ ' SHADING-COEF = 0.688\n' \ - ' GLASS-CONDUCT = 0.309\n' \ + ' GLASS-CONDUCT = 0.309475\n' \ ' ..\n' @@ -139,7 +139,7 @@ def test_window_construction_shade_to_inp(): '"Double Low-E with Shade" = GLASS-TYPE\n' \ ' TYPE = SHADING-COEF\n' \ ' SHADING-COEF = 0.488\n' \ - ' GLASS-CONDUCT = 0.299\n' \ + ' GLASS-CONDUCT = 0.299039\n' \ ' ..\n' diff --git a/tests/schedule_test.py b/tests/schedule_test.py index a58cd53..2578a00 100644 --- a/tests/schedule_test.py +++ b/tests/schedule_test.py @@ -155,28 +155,20 @@ def test_schedule_ruleset_to_inp(): ' ..\n' assert len(inp_week_strs) == 1 assert inp_week_strs[0] == \ - '"Office Occupancy Week 1" = WEEK-SCHEDULE\n' \ + '"Office Occupancy Week 1" = WEEK-SCHEDULE-PD\n' \ ' TYPE = FRACTION\n' \ - ' DAYS = (MON)\n' \ - ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ - ' DAYS = (TUE)\n' \ - ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ - ' DAYS = (WED)\n' \ - ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ - ' DAYS = (THU)\n' \ - ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ - ' DAYS = (FRI)\n' \ - ' DAY-SCHEDULES = "Weekday Office Occupancy"\n' \ - ' DAYS = (SAT)\n' \ - ' DAY-SCHEDULES = "Saturday Office Occupancy"\n' \ - ' DAYS = (SUN)\n' \ - ' DAY-SCHEDULES = "Sunday Office Occupancy"\n' \ - ' DAYS = (HOL)\n' \ - ' DAY-SCHEDULES = "Sunday Office Occupancy"\n' \ - ' DAYS = (HDD)\n' \ - ' DAY-SCHEDULES = "Winter Office Occupancy"\n' \ - ' DAYS = (CDD)\n' \ - ' DAY-SCHEDULES = "Summer Office Occupancy"\n' \ + ' DAY-SCHEDULES = (\n' \ + ' "Weekday Office Occupancy", $ Monday,\n' \ + ' "Weekday Office Occupancy", $ Tuesday,\n' \ + ' "Weekday Office Occupancy", $ Wednesday,\n' \ + ' "Weekday Office Occupancy", $ Thursday,\n' \ + ' "Weekday Office Occupancy", $ Friday,\n' \ + ' "Saturday Office Occupancy", $ Saturday,\n' \ + ' "Sunday Office Occupancy", $ Sunday,\n' \ + ' "Sunday Office Occupancy", $ Holiday,\n' \ + ' "Winter Office Occupancy", $ Winter Design Day,\n' \ + ' "Summer Office Occupancy", $ Summer Design Day,\n' \ + ' )\n' \ ' ..\n' diff --git a/tests/writer_test.py b/tests/writer_test.py index 439a3c6..cca975b 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -30,7 +30,7 @@ def test_shade_writer(): ' V4 = (0.0, 1.0)\n' \ ' ..\n' assert shade_def == \ - '"overhang" = BUILDING-SHADE\n' \ + '"overhang" = FIXED-SHADE\n' \ ' SHAPE = POLYGON\n' \ ' POLYGON = "overhang Plg"\n' \ ' TRANSMITTANCE = 0\n' \ @@ -38,7 +38,7 @@ def test_shade_writer(): ' Y-REF = 0.0\n' \ ' Z-REF = 3.0\n' \ ' TILT = 0.0\n' \ - ' AZIMUTH = 0.0\n' \ + ' AZIMUTH = 180.0\n' \ ' ..\n' fritted_glass_trans = ScheduleRuleset.from_constant_value( @@ -46,7 +46,7 @@ def test_shade_writer(): shade.properties.energy.transmittance_schedule = fritted_glass_trans shade_polygon, shade_def = shade.to.inp(shade) assert shade_def == \ - '"overhang" = BUILDING-SHADE\n' \ + '"overhang" = FIXED-SHADE\n' \ ' SHAPE = POLYGON\n' \ ' POLYGON = "overhang Plg"\n' \ ' TRANSMITTANCE = 0.5\n' \ @@ -54,7 +54,7 @@ def test_shade_writer(): ' Y-REF = 0.0\n' \ ' Z-REF = 3.0\n' \ ' TILT = 0.0\n' \ - ' AZIMUTH = 0.0\n' \ + ' AZIMUTH = 180.0\n' \ ' ..\n' @@ -84,7 +84,7 @@ def test_shade_mesh_writer(): ' Y-REF = 0.0\n' \ ' Z-REF = 4.0\n' \ ' TILT = 0.0\n' \ - ' AZIMUTH = 0.0\n' \ + ' AZIMUTH = 180.0\n' \ ' ..\n' fritted_glass_trans = ScheduleRuleset.from_constant_value( @@ -100,7 +100,7 @@ def test_shade_mesh_writer(): ' Y-REF = 0.0\n' \ ' Z-REF = 4.0\n' \ ' TILT = 0.0\n' \ - ' AZIMUTH = 0.0\n' \ + ' AZIMUTH = 180.0\n' \ ' ..\n' @@ -245,7 +245,7 @@ def test_face_writer(): ' POLYGON = "roof face Plg"\n' \ ' CONSTRUCTION = "Thick Concrete Construction"\n' \ ' TILT = 0.0\n' \ - ' AZIMUTH = 0.0\n' \ + ' AZIMUTH = 180.0\n' \ ' X = 0.0\n' \ ' Y = 0.0\n' \ ' Z = 3.0\n' \ @@ -266,7 +266,7 @@ def test_face_writer(): ' POLYGON = "floor face Plg"\n' \ ' CONSTRUCTION = "Thick Concrete Construction"\n' \ ' TILT = 180.0\n' \ - ' AZIMUTH = 0.0\n' \ + ' AZIMUTH = 180.0\n' \ ' X = 10.0\n' \ ' Y = 0.0\n' \ ' Z = 0.0\n' \ @@ -329,7 +329,7 @@ def test_room_writer(): ' LIGHTING-SCHEDULE = "Generic Office Lighting"\n' \ ' LIGHT-TO-RETURN = 0.0\n' \ ' EQUIPMENT-W/AREA = 0.96\n' \ - ' EQUIPMENT-SCHEDULE = "Generic Office Equipment"\n' \ + ' EQUIP-SCHEDULE = ("Generic Office Equipment")\n' \ ' EQUIP-SENSIBLE = 1.0\n' \ ' EQUIP-LATENT = 0.0\n' \ ' EQUIP-RAD-FRAC = 0.5\n' \ From bc98abe3cac4f63b8d0bf3ad333773759e38516c Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 30 Apr 2024 13:43:57 -0700 Subject: [PATCH 11/27] fix(load): Make several improvements to the way loads are translated --- honeybee_doe2/cli/translate.py | 20 +-- honeybee_doe2/load.py | 225 ++++++++++++++++++++++++++------- honeybee_doe2/schedule.py | 4 + honeybee_doe2/writer.py | 56 ++++---- tests/writer_test.py | 2 +- 5 files changed, 215 insertions(+), 92 deletions(-) diff --git a/honeybee_doe2/cli/translate.py b/honeybee_doe2/cli/translate.py index 79322e9..02c49e7 100644 --- a/honeybee_doe2/cli/translate.py +++ b/honeybee_doe2/cli/translate.py @@ -41,24 +41,13 @@ def translate(): '--include-interior-ceilings/--exclude-interior-ceilings', ' /-xc', help='Flag to ' 'note whether interior ceilings should be excluded from the export.', default=True, show_default=True) -@click.option( - '--verbose-properties/--switch-statements', ' /-ss', help='Flag to note whether ' - 'program types should be written with switch statements so that they can easily ' - 'be edited in eQuest or a verbose definition of loads should be written for ' - 'each Room/Space.', default=True, show_default=True) -@click.option( - '--name', '-n', help='Deprecated option to set the name of the output file.', - default=None, show_default=True) -@click.option( - '--folder', '-f', help='Deprecated option to set the path to target folder.', - type=click.Path(file_okay=False, resolve_path=True, dir_okay=True), default=None) @click.option( '--output-file', '-o', help='Optional INP file path to output the INP string ' 'of the translation. By default this will be printed out to stdout.', type=click.File('w'), default='-', show_default=True) def model_to_inp_file( model_file, sim_par_json, hvac_mapping, include_interior_walls, - include_interior_ceilings, verbose_properties, name, folder, output_file + include_interior_ceilings, output_file ): """Translate a Model (HBJSON) file to an INP file. @@ -82,12 +71,7 @@ def model_to_inp_file( inp_str = model.to.inp(model, sim_par, hvac_mapping, x_int_w, x_int_c) # write out the INP file - if folder is not None and name is not None: - if not name.lower().endswith('.inp'): - name = name + '.inp' - write_to_file_by_name(folder, name, inp_str, True) - else: - output_file.write(inp_str) + output_file.write(inp_str) except Exception as e: _logger.exception(f'Model translation failed:\n{e}') sys.exit(1) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index c3e9f4d..362d6af 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -3,79 +3,212 @@ from ladybug.datatype.area import Area from ladybug.datatype.energyflux import EnergyFlux +from ladybug.datatype.volumeflowrate import VolumeFlowRate from ladybug.datatype.volumeflowrateintensity import VolumeFlowRateIntensity from honeybee.typing import clean_doe2_string from .config import RES_CHARS # TODO: Add methods to map to SOURCE-TYPE HOT-WATER and PROCESS +# TODO: Implement the keys that Trevor wants: +# FLOW/AREA, ASSIGNED-FLOW, MIN-FLOW-RATIO, MIN-FLOW/AREA, HMAX-FLOW-RATIO -def people_to_inp(room): - """Translate the People definition of a Room into INP (Keywords, Values).""" - people = room.properties.energy.people +def people_to_inp(people): + """Translate a People definition into INP (Keywords, Values). + + Args: + people: A honeybee-energy People definition. None is allowed. + + Returns: + A tuple with two elements. + + - keywords: A tuple of text strings for keywords related to defining + people for a Space. + + - values: A tuple of text strings that aligns with the keywords and + denotes the value for each keyword. + """ + if people is None: + return (), () ppl_den = Area().to_unit([people.area_per_person], 'ft2', 'm2')[0] - ppl_total = round(room.floor_area / ppl_den, 3) - ppl_sch = clean_doe2_string(people.occupancy_schedule.display_name, RES_CHARS) + ppl_den = round(ppl_den, 3) + ppl_sch = clean_doe2_string(people.occupancy_schedule.identifier, RES_CHARS) ppl_sch = '"{}"'.format(ppl_sch) - ppl_kwd = ('NUMBER-OF-PEOPLE', 'PEOPLE-SCHEDULE') - ppl_val = (ppl_total, ppl_sch) - return ppl_kwd, ppl_val + keywords = ('AREA/PERSON', 'PEOPLE-SCHEDULE') + values = (ppl_den, ppl_sch) + return keywords, values + + +def lighting_to_inp(lighting): + """Translate a Lighting definition into INP (Keywords, Values). + + Args: + lighting: A honeybee-energy Lighting definition. None is allowed. + Returns: + A tuple with two elements. -def lighting_to_inp(room): - """Translate the Lighting definition of a Room into INP (Keywords, Values).""" - lighting = room.properties.energy.lighting + - keywords: A tuple of text strings for keywords related to defining + lighting for a Space. + + - values: A tuple of text strings that aligns with the keywords and + denotes the value for each keyword. + """ + if lighting is None: + return (), () lpd = EnergyFlux().to_unit([lighting.watts_per_area], 'W/ft2', 'W/m2')[0] lpd = round(lpd, 3) - lgt_sch = clean_doe2_string(lighting.schedule.display_name, RES_CHARS) + lgt_sch = clean_doe2_string(lighting.schedule.identifier, RES_CHARS) lgt_sch = '"{}"'.format(lgt_sch) - light_kwd = ('LIGHTING-W/AREA', 'LIGHTING-SCHEDULE', 'LIGHT-TO-RETURN') - light_val = (lpd, lgt_sch, lighting.return_air_fraction) - return light_kwd, light_val + keywords = ('LIGHTING-W/AREA', 'LIGHTING-SCHEDULE', 'LIGHT-TO-RETURN') + values = (lpd, lgt_sch, lighting.return_air_fraction) + return keywords, values + + +def equipment_to_inp(electric_equip, gas_equip=None): + """Translate an Equipment definition(s) into INP (Keywords, Values). + Args: + electric_equip: A honeybee-energy ElectricEquipment definition. None is allowed. + gas_equip: A honeybee-energy GasEquipment definition. None is allowed. -def equipment_to_inp(room): - """Translate the Equipment definition(s) of a Room into INP (Keywords, Values).""" - # first evaluate what types of equipment we have - ele_equip = room.properties.energy.electric_equipment - gas_equip = room.properties.energy.gas_equipment + Returns: + A tuple with two elements. + - keywords: A tuple of text strings for keywords related to defining + the equipment for a Space. + + - values: A tuple of text strings that aligns with the keywords and + denotes the value for each keyword. + """ # extract the properties from the equipment objects - if ele_equip is not None and gas_equip is not None: # write them as lists - equip_val = [[], [], [], [], []] - for equip in (ele_equip, gas_equip): + if electric_equip is not None and gas_equip is not None: # write them as lists + values = [[], [], [], [], []] + for equip in (electric_equip, gas_equip): epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] epd = round(epd, 3) - equip_val[0].append(epd) - eqp_sch = clean_doe2_string(equip.schedule.display_name, RES_CHARS) - equip_val[1].append('"{}"'.format(eqp_sch)) - equip_val[2].append(1 - equip.latent_fraction - equip.lost_fraction) - equip_val[3].append(equip.latent_fraction) - equip_val[4].append(equip.radiant_fraction) - equip_val = ['( {}, {} )'.format(v[0], v[1]) for v in equip_val] - else: # write them as a single item - equip = ele_equip if gas_equip is None else gas_equip + values[0].append(epd) + eqp_sch = clean_doe2_string(equip.schedule.identifier, RES_CHARS) + values[1].append('"{}"'.format(eqp_sch)) + values[2].append(1 - equip.latent_fraction - equip.lost_fraction) + values[3].append(equip.latent_fraction) + values[4].append(equip.radiant_fraction) + values = ['( {}, {} )'.format(v[0], v[1]) for v in values] + elif electric_equip is not None or gas_equip is not None: # write as a single item + equip = electric_equip if gas_equip is None else gas_equip epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] epd = round(epd, 3) - eqp_sch = clean_doe2_string(equip.schedule.display_name, RES_CHARS) + eqp_sch = clean_doe2_string(equip.schedule.identifier, RES_CHARS) eqp_sch = '("{}")'.format(eqp_sch) sens_fract = 1 - equip.latent_fraction - equip.lost_fraction - equip_val = (epd, eqp_sch, sens_fract, equip.latent_fraction, - equip.radiant_fraction) + values = (epd, eqp_sch, sens_fract, equip.latent_fraction, + equip.radiant_fraction) + else: # no equipment assigned + return (), () + + keywords = ('EQUIPMENT-W/AREA', 'EQUIP-SCHEDULE', + 'EQUIP-SENSIBLE', 'EQUIP-LATENT', 'EQUIP-RAD-FRAC') + return keywords, values + - equip_kwd = ('EQUIPMENT-W/AREA', 'EQUIP-SCHEDULE', - 'EQUIP-SENSIBLE', 'EQUIP-LATENT', 'EQUIP-RAD-FRAC') - return equip_kwd, equip_val +def infiltration_to_inp(infiltration): + """Translate an Infiltration definition into INP (Keywords, Values). + Args: + infiltration: A honeybee-energy Infiltration definition. None is allowed. -def infiltration_to_inp(room): - """Translate the Infiltration definition of a Room into INP (Keywords, Values).""" - infil = room.properties.energy.infiltration - inf_den = infil.flow_per_exterior_area + Returns: + A tuple with two elements. + + - keywords: A tuple of text strings for keywords related to defining + infiltration for a Space. + + - values: A tuple of text strings that aligns with the keywords and + denotes the value for each keyword. + """ + if infiltration is None: + return (), () + inf_den = infiltration.flow_per_exterior_area inf_den = VolumeFlowRateIntensity().to_unit([inf_den], 'cfm/ft2', 'm3/s-m2')[0] inf_den = round(inf_den, 3) - inf_sch = clean_doe2_string(infil.schedule.display_name, RES_CHARS) + inf_sch = clean_doe2_string(infiltration.schedule.identifier, RES_CHARS) inf_sch = '"{}"'.format(inf_sch) - inf_kwd = ('INF-METHOD', 'INF-FLOW/AREA', 'INF-SCHEDULE') - inf_val = ('AIR-CHANGE', inf_den, inf_sch) - return inf_kwd, inf_val + keywords = ('INF-METHOD', 'INF-FLOW/AREA', 'INF-SCHEDULE') + values = ('AIR-CHANGE', inf_den, inf_sch) + return keywords, values + + +def setpoint_to_inp(setpoint): + """Translate a Setpoint definition into INP (Keywords, Values). + + Args: + setpoint: A honeybee-energy Setpoint definition. None is allowed. + + Returns: + A tuple with two elements. + + - keywords: A tuple of text strings for keywords related to defining + setpoints for a Zone. + + - values: A tuple of text strings that aligns with the keywords and + denotes the value for each keyword. + """ + if setpoint is None: # use some default setpoints + return ('DESIGN-HEAT-T', 'DESIGN-COOL-T'), (72, 75) + heat_setpt = round(setpoint.heating_setpoint * (9. / 5.) + 32., 2) + cool_setpt = round(setpoint.cooling_setpoint * (9. / 5.) + 32., 2) + heat_sch = clean_doe2_string(setpoint.heating_schedule.identifier, RES_CHARS) + cool_sch = clean_doe2_string(setpoint.cooling_schedule.identifier, RES_CHARS) + keywords = ('DESIGN-HEAT-T', 'DESIGN-COOL-T', 'HEAT-TEMP-SCH', 'COOL-TEMP-SCH') + values = (heat_setpt, cool_setpt, heat_sch, cool_sch) + return keywords, values + + +def ventilation_to_inp(ventilation): + """Translate a Ventilation definition into INP (Keywords, Values). + + Args: + ventilation: A honeybee-energy Ventilation definition. None is allowed. + + Returns: + A tuple with two elements. + + - keywords: A list of text strings for keywords related to defining + ventilation for a Space. + + - values: A list of text strings that aligns with the keywords and + denotes the value for each keyword. + """ + keywords, values = [], [] + if ventilation is None: + return keywords, values + # check the flow per person + ppl_den = ventilation.flow_per_person + if ppl_den != 0: + keywords.append('OA-FLOW/PER') + ppl_den = VolumeFlowRate().to_unit([ppl_den], 'cfm', 'm3/s')[0] + values.append(round(ppl_den, 3)) + # check the flow per floor area + vent_den = ventilation.flow_per_area + if vent_den != 0: + keywords.append('OA-FLOW/AREA') + vent_den = VolumeFlowRateIntensity().to_unit([vent_den], 'cfm/ft2', 'm3/s-m2')[0] + values.append(round(vent_den, 3)) + # check the air changes per hour + ach = ventilation.air_changes_per_hour + if ach != 0: + keywords.append('OA-CHANGES') + values.append(round(ach, 3)) + # check the flow per zone + total_flow = ventilation.flow_per_zone + if total_flow != 0: + keywords.append('OA-FLOW/PER') + total_flow = VolumeFlowRate().to_unit([total_flow], 'cfm', 'm3/s')[0] + values.append(round(total_flow, 3)) + # check the schedule + vent_sch = ventilation.schedule + if vent_sch is not None: + keywords.append('MIN-FLOW-SCH') + vent_sch = clean_doe2_string(vent_sch.identifier, RES_CHARS) + values.append('"{}"'.format(vent_sch)) + return keywords, values diff --git a/honeybee_doe2/schedule.py b/honeybee_doe2/schedule.py index d1f2fab..80080f4 100644 --- a/honeybee_doe2/schedule.py +++ b/honeybee_doe2/schedule.py @@ -74,6 +74,10 @@ def _format_day_values(values_to_format): keywords.append('VALUES') values.append(_format_day_values(prev_values)) + # convert temperature to fahrenheit if the type if temperature + if type_text == 'type_text': + values = [round(v.heating_setpoint * (9. / 5.) + 32., 2) for v in values] + # return the INP string return generate_inp_string(doe2_id, 'DAY-SCHEDULE', keywords, values) diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index b010e8a..e8724c1 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -8,6 +8,7 @@ from honeybee.typing import clean_doe2_string from honeybee.boundarycondition import Surface from honeybee.facetype import Wall, Floor, RoofCeiling +from honeybee_energy.schedule.ruleset import ScheduleRuleset from honeybee_energy.construction.opaque import OpaqueConstruction from honeybee_energy.construction.air import AirBoundaryConstruction from honeybee_energy.lib.constructionsets import generic_construction_set @@ -21,7 +22,7 @@ window_construction_to_inp, door_construction_to_inp, air_construction_to_inp from .schedule import energy_trans_sch_to_transmittance from .load import people_to_inp, lighting_to_inp, equipment_to_inp, \ - infiltration_to_inp + infiltration_to_inp, setpoint_to_inp, ventilation_to_inp from .simulation import SimulationPar @@ -365,22 +366,23 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals # set up attributes based on the Room's energy properties energy_attr_keywords = ['ZONE-TYPE'] energy_attr_values = [room_doe2_conditioning_type(room)] - if room.properties.energy.people: - ppl_kwd, ppl_val = people_to_inp(room) - energy_attr_keywords.extend(ppl_kwd) - energy_attr_values.extend(ppl_val) - if room.properties.energy.lighting: - lgt_kwd, lgt_val = lighting_to_inp(room) - energy_attr_keywords.extend(lgt_kwd) - energy_attr_values.extend(lgt_val) - if room.properties.energy.electric_equipment or room.properties.energy.gas_equipment: - eq_kwd, eq_val = equipment_to_inp(room) - energy_attr_keywords.extend(eq_kwd) - energy_attr_values.extend(eq_val) - if room.properties.energy.infiltration: - inf_kwd, inf_val = infiltration_to_inp(room) - energy_attr_keywords.extend(inf_kwd) - energy_attr_values.extend(inf_val) + # people + ppl_kwd, ppl_val = people_to_inp(room.properties.energy.people) + energy_attr_keywords.extend(ppl_kwd) + energy_attr_values.extend(ppl_val) + # lighting + lgt_kwd, lgt_val = lighting_to_inp(room.properties.energy.lighting) + energy_attr_keywords.extend(lgt_kwd) + energy_attr_values.extend(lgt_val) + # equipment + eq_kwd, eq_val = equipment_to_inp(room.properties.energy.electric_equipment, + room.properties.energy.gas_equipment) + energy_attr_keywords.extend(eq_kwd) + energy_attr_values.extend(eq_val) + # infiltration + inf_kwd, inf_val = infiltration_to_inp(room.properties.energy.infiltration) + energy_attr_keywords.extend(inf_kwd) + energy_attr_values.extend(inf_val) # create the polygon string from the geometry doe2_id = clean_doe2_string(room.identifier, GEO_CHARS) @@ -516,7 +518,7 @@ def model_to_inp( used_day_sched_ids, used_day_count = {}, 1 all_scheds = model.properties.energy.schedules for sched in all_scheds: - if sched.__class__.__name__ == 'ScheduleRuleset': + if isinstance(sched, ScheduleRuleset): year_schedule, week_schedules = sched.to_inp() # check that day schedules aren't referenced by other model schedules day_scheds = [] @@ -653,15 +655,15 @@ def model_to_inp( space_name = clean_doe2_string(room.identifier, GEO_CHARS) zone_name = '{}_Zn'.format(space_name) zone_type = room_doe2_conditioning_type(room) - heat_setpt, cool_setpt = 72, 75 - setpoint = room.properties.energy.setpoint - if setpoint is not None: - heat_setpt = round(setpoint.heating_setpoint * (9. / 5.) + 32., 2) - cool_setpt = round(setpoint.cooling_setpoint * (9. / 5.) + 32., 2) - zone_keys = ('TYPE', 'DESIGN-HEAT-T', 'DESIGN-COOL-T', - 'SIZING-OPTION', 'SPACE') - zone_vals = (zone_type, heat_setpt, cool_setpt, - 'ADJUST-LOADS', '"{}"'.format(space_name)) + zone_keys = ['TYPE', 'SIZING-OPTION', 'SPACE'] + zone_vals = [zone_type, 'ADJUST-LOADS', '"{}"'.format(space_name)] + if room.properties.energy.is_conditioned: + stp_kwd, stp_val = setpoint_to_inp(room.properties.energy.setpoint) + zone_keys.extend(stp_kwd) + zone_vals.extend(stp_val) + vt_kwd, vt_val = ventilation_to_inp(room.properties.energy.ventilation) + zone_keys.extend(vt_kwd) + zone_vals.extend(vt_val) zone_def = generate_inp_string(zone_name, 'ZONE', zone_keys, zone_vals) model_str.append(zone_def) diff --git a/tests/writer_test.py b/tests/writer_test.py index cca975b..80e030a 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -323,7 +323,7 @@ def test_room_writer(): ' Z = 0.0\n' \ ' VOLUME = 4500\n' \ ' ZONE-TYPE = CONDITIONED\n' \ - ' NUMBER-OF-PEOPLE = 2.362\n' \ + ' AREA/PERSON = 190.512\n' \ ' PEOPLE-SCHEDULE = "Generic Office Occupancy"\n' \ ' LIGHTING-W/AREA = 0.98\n' \ ' LIGHTING-SCHEDULE = "Generic Office Lighting"\n' \ From 50b3af70fd258840b93ac08a239f689e39607c27 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 30 Apr 2024 15:55:04 -0700 Subject: [PATCH 12/27] fix(schedules): Fix several bugs in previous version This also adds support for hot water loads --- .github/workflows/ci.yaml | 1 + honeybee_doe2/load.py | 58 +++++++++++++++++++++++++--- honeybee_doe2/schedule.py | 21 +++++++--- honeybee_doe2/writer.py | 7 +++- setup.py | 6 +++ standards-requirements.txt | 1 + tests/schedule_test.py | 20 ++++++++-- tests/writer_test.py | 79 +++++++++++++++++++++++++++++++++++++- 8 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 standards-requirements.txt diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 19403b9..a47b827 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,6 +23,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + pip install -r standards-requirements.txt pip install -r dev-requirements.txt - name: run tests run: python -m pytest tests/ diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 362d6af..4437e8a 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -2,12 +2,14 @@ from __future__ import division from ladybug.datatype.area import Area +from ladybug.datatype.power import Power from ladybug.datatype.energyflux import EnergyFlux from ladybug.datatype.volumeflowrate import VolumeFlowRate from ladybug.datatype.volumeflowrateintensity import VolumeFlowRateIntensity from honeybee.typing import clean_doe2_string from .config import RES_CHARS +# TODO: Add methods to translate daylight sensors # TODO: Add methods to map to SOURCE-TYPE HOT-WATER and PROCESS # TODO: Implement the keys that Trevor wants: # FLOW/AREA, ASSIGNED-FLOW, MIN-FLOW-RATIO, MIN-FLOW/AREA, HMAX-FLOW-RATIO @@ -86,14 +88,19 @@ def equipment_to_inp(electric_equip, gas_equip=None): values = [[], [], [], [], []] for equip in (electric_equip, gas_equip): epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] - epd = round(epd, 3) - values[0].append(epd) + values[0].append(round(epd, 3)) eqp_sch = clean_doe2_string(equip.schedule.identifier, RES_CHARS) values[1].append('"{}"'.format(eqp_sch)) - values[2].append(1 - equip.latent_fraction - equip.lost_fraction) - values[3].append(equip.latent_fraction) - values[4].append(equip.radiant_fraction) - values = ['( {}, {} )'.format(v[0], v[1]) for v in values] + values[2].append(round(1 - equip.latent_fraction - equip.lost_fraction, 3)) + values[3].append(round(equip.latent_fraction, 3)) + values[4].append(round(equip.radiant_fraction, 3)) + format_values = [] + for v in values: + if isinstance(v[0], str): # make sure the schedules do not go past 100 chars + format_values.append('({},\n{}{})'.format(v[0], ' ' * 31, v[1])) + else: + format_values.append('({}, {})'.format(v[0], v[1])) + values = format_values elif electric_equip is not None or gas_equip is not None: # write as a single item equip = electric_equip if gas_equip is None else gas_equip epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] @@ -111,6 +118,43 @@ def equipment_to_inp(electric_equip, gas_equip=None): return keywords, values +def hot_water_to_inp(hot_water, room_floor_area): + """Translate a ServiceHotWater definition into INP (Keywords, Values). + + Args: + hot_water: A honeybee-energy ServiceHotWater definition. None is allowed. + room_floor_area: The host Room floor area in square feet, which will + be used to convert the hot water flow per unit floor area to an + absolute load in BTU/h. + + Returns: + A tuple with two elements. + + - keywords: A tuple of text strings for keywords related to defining + the hot water SOURCE load for a Space. + + - values: A tuple of text strings that aligns with the keywords and + denotes the value for each keyword. + """ + if hot_water is None: + return (), () + flow_den = hot_water.flow_per_area # L/h-m2 + flr_area = Area().to_unit([room_floor_area], 'm2', 'ft2')[0] # m2 + total_flow = flow_den * flr_area # L/h + delta_t = 50 # assume the water heater must heat water from 10C to 60C + c_water = 4.186 # J/g-C, the specific heat of water + shw_heat = total_flow * c_water * delta_t # J/h using Q = m * c * deltaT + shw_heat = shw_heat / 3600. # Watts + shw_power = round(Power().to_unit([shw_heat], 'Btu/h', 'W')[0], 3) + shw_sch = clean_doe2_string(hot_water.schedule.identifier, RES_CHARS) + shw_sch = '"{}"'.format(shw_sch) + keywords = ('SOURCE-TYPE', 'SOURCE-POWER', 'SOURCE-SCHEDULE', + 'SOURCE-SENSIBLE', 'SOURCE-RAD-FRAC', 'SOURCE-LATENT') + values = ('HOT-WATER', shw_power, shw_sch, + hot_water.sensible_fraction, 0, hot_water.latent_fraction) + return keywords, values + + def infiltration_to_inp(infiltration): """Translate an Infiltration definition into INP (Keywords, Values). @@ -158,7 +202,9 @@ def setpoint_to_inp(setpoint): heat_setpt = round(setpoint.heating_setpoint * (9. / 5.) + 32., 2) cool_setpt = round(setpoint.cooling_setpoint * (9. / 5.) + 32., 2) heat_sch = clean_doe2_string(setpoint.heating_schedule.identifier, RES_CHARS) + heat_sch = '"{}"'.format(heat_sch) cool_sch = clean_doe2_string(setpoint.cooling_schedule.identifier, RES_CHARS) + cool_sch = '"{}"'.format(cool_sch) keywords = ('DESIGN-HEAT-T', 'DESIGN-COOL-T', 'HEAT-TEMP-SCH', 'COOL-TEMP-SCH') values = (heat_setpt, cool_setpt, heat_sch, cool_sch) return keywords, values diff --git a/honeybee_doe2/schedule.py b/honeybee_doe2/schedule.py index 80080f4..578fd97 100644 --- a/honeybee_doe2/schedule.py +++ b/honeybee_doe2/schedule.py @@ -30,12 +30,27 @@ def schedule_day_to_inp(day_schedule, type_limit=None): keywords, values = ['TYPE'], [type_text] hour_values = day_schedule.values_at_timestep(1) + # convert temperature to fahrenheit if the type if temperature + if type_text == 'TEMPERATURE': + hour_values = [round(v * (9. / 5.) + 32., 2) for v in hour_values] + # setup a function to format list of values correctly def _format_day_values(values_to_format): if len(values_to_format) == 1: return'({})'.format(round(values_to_format[0], 3)) - else: + elif len(values_to_format) < 5: return str(tuple(round(v, 3) for v in values_to_format)) + else: # we have to format it with multiple lines + spc = ' ' * 31 + full_str = '(' + for i, v in enumerate(values_to_format): + if i == len(values_to_format) - 1: + full_str += str(round(v, 3)) + ')' + elif (i + 1) % 5 == 0: + full_str += str(round(v, 3)) + ',\n' + spc + else: + full_str += str(round(v, 3)) + ', ' + return full_str # loop through the hourly values and write them in the format DOE-2 likes prev_count, prev_hour, prev_values = 0, 1, [hour_values[0]] @@ -74,10 +89,6 @@ def _format_day_values(values_to_format): keywords.append('VALUES') values.append(_format_day_values(prev_values)) - # convert temperature to fahrenheit if the type if temperature - if type_text == 'type_text': - values = [round(v.heating_setpoint * (9. / 5.) + 32., 2) for v in values] - # return the INP string return generate_inp_string(doe2_id, 'DAY-SCHEDULE', keywords, values) diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index e8724c1..5c6565f 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -21,7 +21,7 @@ from .construction import opaque_material_to_inp, opaque_construction_to_inp, \ window_construction_to_inp, door_construction_to_inp, air_construction_to_inp from .schedule import energy_trans_sch_to_transmittance -from .load import people_to_inp, lighting_to_inp, equipment_to_inp, \ +from .load import people_to_inp, lighting_to_inp, equipment_to_inp, hot_water_to_inp, \ infiltration_to_inp, setpoint_to_inp, ventilation_to_inp from .simulation import SimulationPar @@ -379,6 +379,11 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals room.properties.energy.gas_equipment) energy_attr_keywords.extend(eq_kwd) energy_attr_values.extend(eq_val) + # hot water usage + shw_kwd, shw_val = hot_water_to_inp(room.properties.energy.service_hot_water, + room.floor_area) + energy_attr_keywords.extend(shw_kwd) + energy_attr_values.extend(shw_val) # infiltration inf_kwd, inf_val = infiltration_to_inp(room.properties.energy.infiltration) energy_attr_keywords.extend(inf_kwd) diff --git a/setup.py b/setup.py index 1bcd9c2..3a1b081 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,9 @@ with open('requirements.txt') as f: requirements = f.read().splitlines() +with open('standards-requirements.txt') as f: + standards_requirements = f.read().splitlines() + setuptools.setup( name="honeybee-doe2", use_scm_version=True, @@ -18,6 +21,9 @@ url="https://github.com/ladybug-tools/honeybee-doe2", packages=setuptools.find_packages(exclude=["tests*", "equest_docs*"]), install_requires=requirements, + extras_require={ + 'standards': standards_requirements + }, include_package_data=True, entry_points={ "console_scripts": ["honeybee-doe2 = honeybee_doe2.cli:doe2"] diff --git a/standards-requirements.txt b/standards-requirements.txt new file mode 100644 index 0000000..0f1bcea --- /dev/null +++ b/standards-requirements.txt @@ -0,0 +1 @@ +honeybee-energy-standards==2.3.0 diff --git a/tests/schedule_test.py b/tests/schedule_test.py index 2578a00..83128ec 100644 --- a/tests/schedule_test.py +++ b/tests/schedule_test.py @@ -76,7 +76,10 @@ def test_schedule_day_to_inp_start_end_change(): ' HOURS = (1, 6)\n' \ ' VALUES = (0.0)\n' \ ' HOURS = (7, 24)\n' \ - ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833, 1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5, 0.25, 0.0)\n' \ + ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833,\n' \ + ' 1.0, 0.75, 0.5, 0.25, 0.0,\n' \ + ' 0.25, 0.5, 0.75, 1.0, 0.75,\n' \ + ' 0.5, 0.25, 0.0)\n' \ ' ..\n' hourly_vals_from_ep[-2] = 0 @@ -89,7 +92,10 @@ def test_schedule_day_to_inp_start_end_change(): ' HOURS = (1, 6)\n' \ ' VALUES = (0.0)\n' \ ' HOURS = (7, 22)\n' \ - ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833, 1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5)\n' \ + ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833,\n' \ + ' 1.0, 0.75, 0.5, 0.25, 0.0,\n' \ + ' 0.25, 0.5, 0.75, 1.0, 0.75,\n' \ + ' 0.5)\n' \ ' HOURS = (23, 24)\n' \ ' VALUES = (0.0)\n' \ ' ..\n' @@ -106,7 +112,10 @@ def test_schedule_day_to_inp_start_end_change(): ' HOURS = (3, 6)\n' \ ' VALUES = (0.0)\n' \ ' HOURS = (7, 22)\n' \ - ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833, 1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5)\n' \ + ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833,\n' \ + ' 1.0, 0.75, 0.5, 0.25, 0.0,\n' \ + ' 0.25, 0.5, 0.75, 1.0, 0.75,\n' \ + ' 0.5)\n' \ ' HOURS = (23, 24)\n' \ ' VALUES = (0.0)\n' \ ' ..\n' @@ -124,7 +133,10 @@ def test_schedule_day_to_inp_start_end_change(): ' HOURS = (3, 6)\n' \ ' VALUES = (0.0)\n' \ ' HOURS = (7, 22)\n' \ - ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833, 1.0, 0.75, 0.5, 0.25, 0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5)\n' \ + ' VALUES = (0.166, 0.33, 0.5, 0.66, 0.833,\n' \ + ' 1.0, 0.75, 0.5, 0.25, 0.0,\n' \ + ' 0.25, 0.5, 0.75, 1.0, 0.75,\n' \ + ' 0.5)\n' \ ' HOURS = (23, 24)\n' \ ' VALUES = (0.0)\n' \ ' ..\n' diff --git a/tests/writer_test.py b/tests/writer_test.py index 80e030a..62aea7a 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -13,7 +13,7 @@ from honeybee_energy.material.opaque import EnergyMaterial from honeybee_energy.schedule.ruleset import ScheduleRuleset import honeybee_energy.lib.scheduletypelimits as schedule_types -from honeybee_energy.lib.programtypes import office_program +from honeybee_energy.lib.programtypes import office_program, program_type_by_identifier def test_shade_writer(): @@ -339,6 +339,83 @@ def test_room_writer(): ' ..\n' +def test_room_writer_program(): + """Test the the Room inp writer with different types of programs.""" + room = Room.from_box('Tiny_House_Zone', 15, 30, 10) + south_face = room[3] + south_face.apertures_by_ratio(0.4, 0.01) + + apartment_prog = program_type_by_identifier('2019::MidriseApartment::Apartment') + room.properties.energy.program_type = apartment_prog + room.properties.energy.add_default_ideal_air() + _, room_def = room.to.inp(room) + assert room_def[0] == \ + '"Tiny House Zone" = SPACE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "Tiny House Zone Plg"\n' \ + ' AZIMUTH = 0\n' \ + ' X = 0.0\n' \ + ' Y = 0.0\n' \ + ' Z = 0.0\n' \ + ' VOLUME = 4500\n' \ + ' ZONE-TYPE = CONDITIONED\n' \ + ' AREA/PERSON = 380.228\n' \ + ' PEOPLE-SCHEDULE = "ApartmentMidRise OCC APT SCH"\n' \ + ' LIGHTING-W/AREA = 0.87\n' \ + ' LIGHTING-SCHEDULE = "ApartmentMidRise LTG APT SCH"\n' \ + ' LIGHT-TO-RETURN = 0.0\n' \ + ' EQUIPMENT-W/AREA = 0.62\n' \ + ' EQUIP-SCHEDULE = ("ApartmentMidRise EQP APT SCH")\n' \ + ' EQUIP-SENSIBLE = 1.0\n' \ + ' EQUIP-LATENT = 0.0\n' \ + ' EQUIP-RAD-FRAC = 0.5\n' \ + ' SOURCE-TYPE = HOT-WATER\n' \ + ' SOURCE-POWER = 1.238\n' \ + ' SOURCE-SCHEDULE = "ApartmentMidRise APT DHW SCH"\n' \ + ' SOURCE-SENSIBLE = 0.2\n' \ + ' SOURCE-RAD-FRAC = 0\n' \ + ' SOURCE-LATENT = 0.05\n' \ + ' INF-METHOD = AIR-CHANGE\n' \ + ' INF-FLOW/AREA = 0.112\n' \ + ' INF-SCHEDULE = "ApartmentMidRise INF APT SCH"\n' \ + ' ..\n' + + kitchen_prog = program_type_by_identifier('2019::FullServiceRestaurant::Kitchen') + room.properties.energy.program_type = kitchen_prog + _, room_def = room.to.inp(room) + print(room_def[0]) + assert room_def[0] == \ + '"Tiny House Zone" = SPACE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "Tiny House Zone Plg"\n' \ + ' AZIMUTH = 0\n' \ + ' X = 0.0\n' \ + ' Y = 0.0\n' \ + ' Z = 0.0\n' \ + ' VOLUME = 4500\n' \ + ' ZONE-TYPE = CONDITIONED\n' \ + ' AREA/PERSON = 200.0\n' \ + ' PEOPLE-SCHEDULE = "RestaurantSitDown BLDG OCC SCH"\n' \ + ' LIGHTING-W/AREA = 0.87\n' \ + ' LIGHTING-SCHEDULE = "RstrntStDwnBLDG_HENSCH20102013"\n' \ + ' LIGHT-TO-RETURN = 0.0\n' \ + ' EQUIPMENT-W/AREA = (37.53, 60.317)\n' \ + ' EQUIP-SCHEDULE = ("RstrntStDwn BLDG EQUIP SCH", "RstrntStDwn Rst GAS EQUIP SCH")\n' \ + ' EQUIP-SENSIBLE = (0.55, 0.2)\n' \ + ' EQUIP-LATENT = 0.0\n' \ + ' EQUIP-RAD-FRAC = 0.5\n' \ + ' SOURCE-TYPE = HOT-WATER\n' \ + ' SOURCE-POWER = 1.238\n' \ + ' SOURCE-SCHEDULE = "ApartmentMidRise APT DHW SCH"\n' \ + ' SOURCE-SENSIBLE = 0.2\n' \ + ' SOURCE-RAD-FRAC = 0\n' \ + ' SOURCE-LATENT = 0.05\n' \ + ' INF-METHOD = AIR-CHANGE\n' \ + ' INF-FLOW/AREA = 0.112\n' \ + ' INF-SCHEDULE = "ApartmentMidRise INF APT SCH"\n' \ + ' ..\n' + + def test_model_writer(): """Test the basic functionality of the Model inp writer.""" room = Room.from_box('Tiny_House_Zone', 5, 10, 3) From ae7e187716374ac6b91db597bf8d9f2213fadccd Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 30 Apr 2024 17:53:04 -0700 Subject: [PATCH 13/27] feat(shade): Write shades without POLYGON when they are RECTANGLE --- honeybee_doe2/load.py | 2 +- honeybee_doe2/writer.py | 87 +++++++++++++++++++++++++++++++++++------ tests/writer_test.py | 43 ++++++++++++++------ 3 files changed, 106 insertions(+), 26 deletions(-) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 4437e8a..4518afd 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -10,7 +10,7 @@ from .config import RES_CHARS # TODO: Add methods to translate daylight sensors -# TODO: Add methods to map to SOURCE-TYPE HOT-WATER and PROCESS +# TODO: Add methods to map honeybee_energy process loads to SOURCE-TYPE PROCESS # TODO: Implement the keys that Trevor wants: # FLOW/AREA, ASSIGNED-FLOW, MIN-FLOW-RATIO, MIN-FLOW/AREA, HMAX-FLOW-RATIO diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 5c6565f..07c8a39 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -3,7 +3,7 @@ from __future__ import division import math -from ladybug_geometry.geometry2d import Point2D +from ladybug_geometry.geometry2d import Vector2D, Point2D from ladybug_geometry.geometry3d import Vector3D, Point3D, Plane, Face3D from honeybee.typing import clean_doe2_string from honeybee.boundarycondition import Surface @@ -87,6 +87,53 @@ def face_3d_to_inp(face_3d, parent_name='HB object'): return polygon_str, position_info +def face_3d_to_inp_rectangle(face_3d): + """Convert a Face3D into parameters needed to represent it as a rectangle in INP. + + The output of this function will be None if the Face3D cannot be represented + as an INP rectangle without alteration of the geometry. + + Args: + face_3d: A ladybug-geometry Face3D object which will be tested for whether + it can be represented as a rectangle in INP. + + Returns: + Will be None if the Face3D cannot be translated to a WIDTH and HEIGHT + without alteration of the geometry. If the geometry can be successfully + translated, this will be a tuple with five elements. + + - width: A number for the width of the rectangle. + + - height: A number for the height of the rectangle. + + - llc_origin: A Point3D for the lower-left corner of the Shade + geometry origin. + + - tilt: A number for the tilt of the rectangle in degrees. + + - azimuth: A number for the azimuth of the rectangle in degrees. + """ + if face_3d.boundary_polygon2d.is_rectangle(math.radians(DOE2_ANGLE_TOL)): + # check to see at least one of the segments is horizontal + are_segs_hor = [seg.max.z - seg.min.z <= DOE2_TOLERANCE + for seg in face_3d.boundary_segments] + if True in are_segs_hor: + pts_3d = face_3d.lower_left_counter_clockwise_boundary + llc_origin = pts_3d[0] + width = llc_origin.distance_to_point(pts_3d[1]) + height = llc_origin.distance_to_point(pts_3d[-1]) + if all(is_horiz for is_horiz in are_segs_hor): # horizontal; adjust azimuth + tilt = 0.0 + hgt_vec = llc_origin - pts_3d[-1] + hgt_vec_2d = Vector2D(hgt_vec.x, hgt_vec.y) + azimuth = math.degrees(Vector2D(0, 1).angle_clockwise(hgt_vec_2d)) + else: # vertical or tilted; use Face3D azimuth + tilt = math.degrees(face_3d.tilt) + azimuth = math.degrees(face_3d.azimuth) + return width, height, llc_origin, tilt, azimuth + return None + + def shade_mesh_to_inp(shade_mesh): """Generate an INP string representation of a ShadeMesh. @@ -139,20 +186,36 @@ def shade_to_inp(shade): - shade_def: Text string for the INP definition of the Shade. """ - # TODO: Sense when the shade is a rectangle and, if so, translate it without POLYGON - # create the polygon string from the geometry + # extract the transmittance properties of the shade doe2_id = clean_doe2_string(shade.identifier, GEO_CHARS) + trans_kwd = ['TRANSMITTANCE'] + trans_vals = [energy_trans_sch_to_transmittance(shade)] + t_sch_obj = shade.properties.energy.transmittance_schedule + if t_sch_obj is not None and not t_sch_obj.is_constant: + trans_kwd.append('SHADE-SCHEDULE') + trans_vals.append(clean_doe2_string(t_sch_obj.identifier, RES_CHARS)) + + # extract the geometry properties of the shade shd_geo = shade.geometry if shade.altitude > 0 else shade.geometry.flip() clean_geo = shd_geo.remove_colinear_vertices(DOE2_TOLERANCE) - shade_polygon, pos_info = face_3d_to_inp(clean_geo, doe2_id) - origin, tilt, az = pos_info - # create the shade definition, which includes the position information - trans = energy_trans_sch_to_transmittance(shade) - keywords = ('SHAPE', 'POLYGON', 'TRANSMITTANCE', - 'X-REF', 'Y-REF', 'Z-REF', 'TILT', 'AZIMUTH') - values = ('POLYGON', '"{} Plg"'.format(doe2_id), trans, - round(origin.x, GEO_DEC_COUNT), round(origin.y, GEO_DEC_COUNT), - round(origin.z, GEO_DEC_COUNT), tilt, az) + rect_info = face_3d_to_inp_rectangle(clean_geo) + if rect_info is not None: # shade is a rectangle; translate it without POLYGON + width, height, origin, tilt, az = rect_info + geo_kwd = ['SHAPE', 'HEIGHT', 'WIDTH'] + geo_vals = ['RECTANGLE', height, width] + shade_polygon = '' + else: # otherwise, create the polygon string from the geometry + shade_polygon, pos_info = face_3d_to_inp(clean_geo, doe2_id) + origin, tilt, az = pos_info + geo_kwd = ['SHAPE', 'POLYGON'] + geo_vals = ['POLYGON', '"{} Plg"'.format(doe2_id)] + geo_kwd.extend(('X-REF', 'Y-REF', 'Z-REF', 'TILT', 'AZIMUTH')) + geo_vals.extend((round(origin.x, GEO_DEC_COUNT), round(origin.y, GEO_DEC_COUNT), + round(origin.z, GEO_DEC_COUNT), tilt, az)) + + # create the final shade definition, which includes the position information + keywords = geo_kwd + trans_kwd + values = geo_vals + trans_vals shade_def = generate_inp_string(doe2_id, 'FIXED-SHADE', keywords, values) return shade_polygon, shade_def diff --git a/tests/writer_test.py b/tests/writer_test.py index 62aea7a..0a3d9ad 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -18,27 +18,44 @@ def test_shade_writer(): """Test the basic functionality of the Shade inp writer.""" - shade = Shade.from_vertices( - 'overhang', [[0, 0, 3], [1, 0, 3], [1, 1, 3], [0, 1, 3]]) + rect_verts = [[0, 0, 3], [1, 0, 3], [1, 1, 3], [0, 1, 3]] + non_rect_verts = [[0, 0, 3], [1, 0, 3], [2, 1, 3], [0, 1, 3]] + shade = Shade.from_vertices('overhang', rect_verts) + shade_polygon, shade_def = shade.to.inp(shade) + assert shade_polygon == '' + assert shade_def == \ + '"overhang" = FIXED-SHADE\n' \ + ' SHAPE = RECTANGLE\n' \ + ' HEIGHT = 1.0\n' \ + ' WIDTH = 1.0\n' \ + ' X-REF = 0.0\n' \ + ' Y-REF = 0.0\n' \ + ' Z-REF = 3.0\n' \ + ' TILT = 0.0\n' \ + ' AZIMUTH = 180.0\n' \ + ' TRANSMITTANCE = 0\n' \ + ' ..\n' + + shade = Shade.from_vertices('overhang', non_rect_verts) shade_polygon, shade_def = shade.to.inp(shade) assert shade_polygon == \ '"overhang Plg" = POLYGON\n' \ ' V1 = (0.0, 0.0)\n' \ ' V2 = (1.0, 0.0)\n' \ - ' V3 = (1.0, 1.0)\n' \ + ' V3 = (2.0, 1.0)\n' \ ' V4 = (0.0, 1.0)\n' \ ' ..\n' assert shade_def == \ '"overhang" = FIXED-SHADE\n' \ ' SHAPE = POLYGON\n' \ ' POLYGON = "overhang Plg"\n' \ - ' TRANSMITTANCE = 0\n' \ ' X-REF = 0.0\n' \ ' Y-REF = 0.0\n' \ ' Z-REF = 3.0\n' \ ' TILT = 0.0\n' \ ' AZIMUTH = 180.0\n' \ + ' TRANSMITTANCE = 0\n' \ ' ..\n' fritted_glass_trans = ScheduleRuleset.from_constant_value( @@ -49,12 +66,12 @@ def test_shade_writer(): '"overhang" = FIXED-SHADE\n' \ ' SHAPE = POLYGON\n' \ ' POLYGON = "overhang Plg"\n' \ - ' TRANSMITTANCE = 0.5\n' \ ' X-REF = 0.0\n' \ ' Y-REF = 0.0\n' \ ' Z-REF = 3.0\n' \ ' TILT = 0.0\n' \ ' AZIMUTH = 180.0\n' \ + ' TRANSMITTANCE = 0.5\n' \ ' ..\n' @@ -383,7 +400,6 @@ def test_room_writer_program(): kitchen_prog = program_type_by_identifier('2019::FullServiceRestaurant::Kitchen') room.properties.energy.program_type = kitchen_prog _, room_def = room.to.inp(room) - print(room_def[0]) assert room_def[0] == \ '"Tiny House Zone" = SPACE\n' \ ' SHAPE = POLYGON\n' \ @@ -396,23 +412,24 @@ def test_room_writer_program(): ' ZONE-TYPE = CONDITIONED\n' \ ' AREA/PERSON = 200.0\n' \ ' PEOPLE-SCHEDULE = "RestaurantSitDown BLDG OCC SCH"\n' \ - ' LIGHTING-W/AREA = 0.87\n' \ + ' LIGHTING-W/AREA = 1.09\n' \ ' LIGHTING-SCHEDULE = "RstrntStDwnBLDG_HENSCH20102013"\n' \ ' LIGHT-TO-RETURN = 0.0\n' \ ' EQUIPMENT-W/AREA = (37.53, 60.317)\n' \ - ' EQUIP-SCHEDULE = ("RstrntStDwn BLDG EQUIP SCH", "RstrntStDwn Rst GAS EQUIP SCH")\n' \ + ' EQUIP-SCHEDULE = ("RstrntStDwn BLDG EQUIP SCH",\n' \ + ' "RstrntStDwn Rst GAS EQUIP SCH")\n' \ ' EQUIP-SENSIBLE = (0.55, 0.2)\n' \ - ' EQUIP-LATENT = 0.0\n' \ - ' EQUIP-RAD-FRAC = 0.5\n' \ + ' EQUIP-LATENT = (0.25, 0.1)\n' \ + ' EQUIP-RAD-FRAC = (0.3, 0.2)\n' \ ' SOURCE-TYPE = HOT-WATER\n' \ - ' SOURCE-POWER = 1.238\n' \ - ' SOURCE-SCHEDULE = "ApartmentMidRise APT DHW SCH"\n' \ + ' SOURCE-POWER = 29.943\n' \ + ' SOURCE-SCHEDULE = "RestaurantSitDown BLDG SWH SCH"\n' \ ' SOURCE-SENSIBLE = 0.2\n' \ ' SOURCE-RAD-FRAC = 0\n' \ ' SOURCE-LATENT = 0.05\n' \ ' INF-METHOD = AIR-CHANGE\n' \ ' INF-FLOW/AREA = 0.112\n' \ - ' INF-SCHEDULE = "ApartmentMidRise INF APT SCH"\n' \ + ' INF-SCHEDULE = "RstrntStDwn INFIL HALF ON SCH"\n' \ ' ..\n' From c8ada269899703ddc5714f051c5663dcca7f6545 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Wed, 1 May 2024 11:28:33 -0700 Subject: [PATCH 14/27] fix(writer): Ensure that ShadeMesh also uses Rectangle --- honeybee_doe2/schedule.py | 2 +- honeybee_doe2/writer.py | 49 +++++++++++++++++++++++++++----------- tests/writer_test.py | 50 +++++++++++++++++++++++++++++++-------- 3 files changed, 76 insertions(+), 25 deletions(-) diff --git a/honeybee_doe2/schedule.py b/honeybee_doe2/schedule.py index 578fd97..00fba00 100644 --- a/honeybee_doe2/schedule.py +++ b/honeybee_doe2/schedule.py @@ -267,5 +267,5 @@ def energy_trans_sch_to_transmittance(shade_obj): sch_vals = trans_sch.values() except Exception: # ScheduleFixedInterval sch_vals = trans_sch.values - trans = sum(sch_vals) / len(sch_vals) + trans = round(sum(sch_vals) / len(sch_vals), 3) return trans diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 07c8a39..8c7764a 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -150,26 +150,45 @@ def shade_mesh_to_inp(shade_mesh): - shade_defs: A list of text strings for the INP definitions needed to represent the ShadeMesh. """ - # TODO: Sense when the shade is a rectangle and, if so, translate it without POLYGON - # set up collector lists and properties for all shades + # extract the transmittance properties of the shade base_id = clean_doe2_string(shade_mesh.identifier, GEO_CHARS) - trans = energy_trans_sch_to_transmittance(shade_mesh) - keywords = ('SHAPE', 'POLYGON', 'TRANSMITTANCE', - 'X-REF', 'Y-REF', 'Z-REF', 'TILT', 'AZIMUTH') + trans_kwd = ['TRANSMITTANCE'] + trans_vals = [energy_trans_sch_to_transmittance(shade_mesh)] + t_sch_obj = shade_mesh.properties.energy.transmittance_schedule + if t_sch_obj is not None and not t_sch_obj.is_constant: + trans_kwd.append('SHADE-SCHEDULE') + t_shc_id = clean_doe2_string(t_sch_obj.identifier, RES_CHARS) + trans_vals.append('"{}"'.format(t_shc_id)) + + # set up collector lists and properties for all shades shade_polygons, shade_defs = [], [] + # loop through the mesh faces and create individual shade objects for i, face in enumerate(shade_mesh.geometry.face_vertices): + doe2_id = '{}{}'.format(base_id, i) f_geo = Face3D(face) shd_geo = f_geo if f_geo.altitude > 0 else f_geo.flip() - doe2_id = '{}{}'.format(base_id, i) - shade_polygon, pos_info = face_3d_to_inp(shd_geo, doe2_id) - origin, tilt, az = pos_info - values = ('POLYGON', '"{} Plg"'.format(doe2_id), trans, - round(origin.x, GEO_DEC_COUNT), round(origin.y, GEO_DEC_COUNT), - round(origin.z, GEO_DEC_COUNT), tilt, az) + clean_geo = shd_geo.remove_colinear_vertices(DOE2_TOLERANCE) + rect_info = face_3d_to_inp_rectangle(clean_geo) + if rect_info is not None: # shade is a rectangle; translate it without POLYGON + width, height, origin, tilt, az = rect_info + geo_kwd = ['SHAPE', 'HEIGHT', 'WIDTH'] + geo_vals = ['RECTANGLE', height, width] + else: # otherwise, create the polygon string from the geometry + shade_polygon, pos_info = face_3d_to_inp(clean_geo, doe2_id) + shade_polygons.append(shade_polygon) + origin, tilt, az = pos_info + geo_kwd = ['SHAPE', 'POLYGON'] + geo_vals = ['POLYGON', '"{} Plg"'.format(doe2_id)] + geo_kwd.extend(('X-REF', 'Y-REF', 'Z-REF', 'TILT', 'AZIMUTH')) + geo_vals.extend((round(origin.x, GEO_DEC_COUNT), round(origin.y, GEO_DEC_COUNT), + round(origin.z, GEO_DEC_COUNT), tilt, az)) + # create the final shade definition, which includes the position information + keywords = geo_kwd + trans_kwd + values = geo_vals + trans_vals shade_def = generate_inp_string(doe2_id, 'FIXED-SHADE', keywords, values) - shade_polygons.append(shade_polygon) shade_defs.append(shade_def) + return shade_polygons, shade_defs @@ -193,7 +212,8 @@ def shade_to_inp(shade): t_sch_obj = shade.properties.energy.transmittance_schedule if t_sch_obj is not None and not t_sch_obj.is_constant: trans_kwd.append('SHADE-SCHEDULE') - trans_vals.append(clean_doe2_string(t_sch_obj.identifier, RES_CHARS)) + t_shc_id = clean_doe2_string(t_sch_obj.identifier, RES_CHARS) + trans_vals.append('"{}"'.format(t_shc_id)) # extract the geometry properties of the shade shd_geo = shade.geometry if shade.altitude > 0 else shade.geometry.flip() @@ -671,7 +691,8 @@ def model_to_inp( shade_polygons, shade_geo_defs = [], [] for shade in model.shades: shade_polygon, shade_def = shade_to_inp(shade) - shade_polygons.append(shade_polygon) + if shade_polygon != '': # shade written with a RECTANGLE + shade_polygons.append(shade_polygon) shade_geo_defs.append(shade_def) for shade in model.shade_meshes: shade_polygon, shade_def = shade_mesh_to_inp(shade) diff --git a/tests/writer_test.py b/tests/writer_test.py index 0a3d9ad..8a2932b 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -74,6 +74,24 @@ def test_shade_writer(): ' TRANSMITTANCE = 0.5\n' \ ' ..\n' + move_vals = [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0] + moveable_trans = ScheduleRuleset.from_daily_values('Moveable Awning', move_vals) + shade.properties.energy.transmittance_schedule = moveable_trans + shade_polygon, shade_def = shade.to.inp(shade) + assert shade_def == \ + '"overhang" = FIXED-SHADE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "overhang Plg"\n' \ + ' X-REF = 0.0\n' \ + ' Y-REF = 0.0\n' \ + ' Z-REF = 3.0\n' \ + ' TILT = 0.0\n' \ + ' AZIMUTH = 180.0\n' \ + ' TRANSMITTANCE = 0.417\n' \ + ' SHADE-SCHEDULE = "Moveable Awning"\n' \ + ' ..\n' + def test_shade_mesh_writer(): """Test the basic functionality of the ShadeMesh inp writer.""" @@ -83,25 +101,36 @@ def test_shade_mesh_writer(): shade = ShadeMesh('Awning_1', mesh) shade_polygons, shade_defs = shade.to.inp(shade) - assert len(shade_polygons) == 2 + assert len(shade_polygons) == 1 assert len(shade_defs) == 2 assert shade_polygons[0] == \ - '"Awning 10 Plg" = POLYGON\n' \ + '"Awning 11 Plg" = POLYGON\n' \ ' V1 = (0.0, 0.0)\n' \ ' V2 = (2.0, 0.0)\n' \ - ' V3 = (2.0, 2.0)\n' \ - ' V4 = (0.0, 2.0)\n' \ + ' V3 = (0.0, 2.0)\n' \ ' ..\n' assert shade_defs[0] == \ '"Awning 10" = FIXED-SHADE\n' \ - ' SHAPE = POLYGON\n' \ - ' POLYGON = "Awning 10 Plg"\n' \ - ' TRANSMITTANCE = 0\n' \ + ' SHAPE = RECTANGLE\n' \ + ' HEIGHT = 2.0\n' \ + ' WIDTH = 2.0\n' \ ' X-REF = 0.0\n' \ ' Y-REF = 0.0\n' \ ' Z-REF = 4.0\n' \ ' TILT = 0.0\n' \ ' AZIMUTH = 180.0\n' \ + ' TRANSMITTANCE = 0\n' \ + ' ..\n' + assert shade_defs[1] == \ + '"Awning 11" = FIXED-SHADE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "Awning 11 Plg"\n' \ + ' X-REF = 2.0\n' \ + ' Y-REF = 0.0\n' \ + ' Z-REF = 4.0\n' \ + ' TILT = 0.0\n' \ + ' AZIMUTH = 180.0\n' \ + ' TRANSMITTANCE = 0\n' \ ' ..\n' fritted_glass_trans = ScheduleRuleset.from_constant_value( @@ -110,14 +139,15 @@ def test_shade_mesh_writer(): shade_polygons, shade_defs = shade.to.inp(shade) assert shade_defs[0] == \ '"Awning 10" = FIXED-SHADE\n' \ - ' SHAPE = POLYGON\n' \ - ' POLYGON = "Awning 10 Plg"\n' \ - ' TRANSMITTANCE = 0.5\n' \ + ' SHAPE = RECTANGLE\n' \ + ' HEIGHT = 2.0\n' \ + ' WIDTH = 2.0\n' \ ' X-REF = 0.0\n' \ ' Y-REF = 0.0\n' \ ' Z-REF = 4.0\n' \ ' TILT = 0.0\n' \ ' AZIMUTH = 180.0\n' \ + ' TRANSMITTANCE = 0.5\n' \ ' ..\n' From fed196456ba1c1f9d5101c4a348a0ba79035b049 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 2 May 2024 13:58:23 -0700 Subject: [PATCH 15/27] feat(room): Add support for writing extruded Rooms without polygons --- honeybee_doe2/load.py | 6 +- honeybee_doe2/writer.py | 136 ++++++++++++++++++++++++++++++++-------- 2 files changed, 114 insertions(+), 28 deletions(-) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 4518afd..9a435a2 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -9,10 +9,10 @@ from honeybee.typing import clean_doe2_string from .config import RES_CHARS -# TODO: Add methods to translate daylight sensors -# TODO: Add methods to map honeybee_energy process loads to SOURCE-TYPE PROCESS # TODO: Implement the keys that Trevor wants: # FLOW/AREA, ASSIGNED-FLOW, MIN-FLOW-RATIO, MIN-FLOW/AREA, HMAX-FLOW-RATIO +# TODO: Add methods to translate daylight sensors +# TODO: Add methods to map honeybee_energy process loads to SOURCE-TYPE PROCESS def people_to_inp(people): @@ -248,7 +248,7 @@ def ventilation_to_inp(ventilation): # check the flow per zone total_flow = ventilation.flow_per_zone if total_flow != 0: - keywords.append('OA-FLOW/PER') + keywords.append('OUTSIDE-AIR-FLOW') total_flow = VolumeFlowRate().to_unit([total_flow], 'cfm', 'm3/s')[0] values.append(round(total_flow, 3)) # check the schedule diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 8c7764a..0283378 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -344,7 +344,7 @@ def aperture_to_inp(aperture): return aperture_def -def face_to_inp(face, space_origin=Point3D(0, 0, 0)): +def face_to_inp(face, space_origin=Point3D(0, 0, 0), location=None): """Generate an INP string representation of a Face. Note that the resulting string does not include full construction definitions. @@ -359,6 +359,9 @@ def face_to_inp(face, space_origin=Point3D(0, 0, 0)): face: A honeybee Face for which an INP representation will be returned. space_origin: A ladybug-geometry Point3D for the origin of the space to which the Face is assigned. (Default: (0, 0, 0)). + location: An optional text string to note the DOE-2 LOCATION of the + Face on the parent Room. When this is specified, the Face will be + written without using a POLYGON. (Default: None). Returns: A tuple with two elements. @@ -378,21 +381,28 @@ def face_to_inp(face, space_origin=Point3D(0, 0, 0)): else: # likely ground or some other fancy ground boundary condition doe2_type = 'UNDERGROUND-WALL' - # create the polygon string from the geometry + # process the face identifier and the construction doe2_id = clean_doe2_string(face.identifier, GEO_CHARS) - f_geo = face.geometry.remove_colinear_vertices(DOE2_TOLERANCE) - face_polygon, pos_info = face_3d_to_inp(f_geo, doe2_id) - face_origin, tilt, az = pos_info - origin = face_origin - space_origin - - # create the face definition, which includes the position info constr_o_name = face.properties.energy.construction.identifier constr = clean_doe2_string(constr_o_name, RES_CHARS) - keywords = ['POLYGON', 'CONSTRUCTION', 'TILT', 'AZIMUTH', 'X', 'Y', 'Z'] - values = ['"{} Plg"'.format(doe2_id), '"{}"'.format(constr), tilt, az, - round(origin.x, GEO_DEC_COUNT), - round(origin.y, GEO_DEC_COUNT), - round(origin.z, GEO_DEC_COUNT)] + + # process the geometry + if location is not None: + keywords = ['CONSTRUCTION', 'LOCATION'] + values = ['"{}"'.format(constr), location] + face_polygon = '' + else: # create the polygon string from the geometry + f_geo = face.geometry.remove_colinear_vertices(DOE2_TOLERANCE) + face_polygon, pos_info = face_3d_to_inp(f_geo, doe2_id) + face_origin, tilt, az = pos_info + origin = face_origin - space_origin + keywords = ['POLYGON', 'CONSTRUCTION', 'TILT', 'AZIMUTH', 'X', 'Y', 'Z'] + values = ['"{} Plg"'.format(doe2_id), '"{}"'.format(constr), tilt, az, + round(origin.x, GEO_DEC_COUNT), + round(origin.y, GEO_DEC_COUNT), + round(origin.z, GEO_DEC_COUNT)] + + # add information related to the boundary condition if bc_str == 'Surface': adj_room = face.boundary_condition.boundary_condition_objects[-1] adj_id = clean_doe2_string(adj_room, GEO_CHARS) @@ -401,11 +411,12 @@ def face_to_inp(face, space_origin=Point3D(0, 0, 0)): elif doe2_type == 'INTERIOR-WALL': # assume that it is adiabatic keywords.append('INT-WALL-TYPE') values.append('ADIABATIC') - if f_type_str == 'Floor' and doe2_type != 'INTERIOR-WALL': + if location is None and f_type_str == 'Floor' and doe2_type != 'INTERIOR-WALL': keywords.append('LOCATION') values.append('BOTTOM') - face_def = generate_inp_string(doe2_id, doe2_type, keywords, values) + # create the face definition + face_def = generate_inp_string(doe2_id, doe2_type, keywords, values) return face_polygon, face_def @@ -443,8 +454,8 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals to represent the Room and all of its constituent Faces, Apertures and Doors. """ - # TODO: Sense when a Room is an extruded floor plate and, if so, do not use - # POLYGON to describe the Room faces + # process the room identifier + doe2_id = clean_doe2_string(room.identifier, GEO_CHARS) # set up attributes based on the Room's energy properties energy_attr_keywords = ['ZONE-TYPE'] @@ -472,11 +483,85 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals energy_attr_keywords.extend(inf_kwd) energy_attr_values.extend(inf_val) - # create the polygon string from the geometry - doe2_id = clean_doe2_string(room.identifier, GEO_CHARS) - r_geo = room.horizontal_boundary(match_walls=False, tolerance=DOE2_TOLERANCE) - r_geo = r_geo if r_geo.normal.z >= 0 else r_geo.flip() - r_geo = r_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) + + # sense when a Room is an extruded floor plate + def _is_room_3d_extruded(hb_room): + """Test if a Room is a pure extrusion. + + Args: + hb_room: The Honeybee Room to be tested. + + Returns: + A tuple with two elements. + + - is_extrusion: True if the geometry is an extrusion. False if not. + + - face_orientations: A list of integers that aligns with the Room.faces + and denotes whether each face is downward (-1), vertical (0) or + upward (+1). + """ + # set up the parameters for evaluating vertical or horizontal + vert_vec = Vector3D(0, 0, 1) + min_v_ang = math.radians(DOE2_ANGLE_TOL) + max_v_ang = math.pi - min_v_ang + min_h_ang = (math.pi / 2) - min_v_ang + max_h_ang = (math.pi / 2) + min_v_ang + + # loop through the Room faces and test them + face_orientations = [] + for face in hb_room.faces: + try: # first make sure that the geometry is not degenerate + clean_geo = face.geometry.remove_colinear_vertices(DOE2_TOLERANCE) + v_ang = clean_geo.normal.angle(vert_vec) + if v_ang <= min_v_ang: + face_orientations.append(1) + continue + elif v_ang >= max_v_ang: + face_orientations.append(-1) + continue + elif min_h_ang <= v_ang <= max_h_ang: + face_orientations.append(0) + continue + return False, [] + except AssertionError: # degenerate face to ignore + pass + return True, face_orientations + + # if the room is extruded, determine the locations for each face + face_locations = [] + is_extrusion, face_orientations = _is_room_3d_extruded(room) + if is_extrusion: # try to translate without using POLYGON for the Room faces + r_geo = room.horizontal_boundary(match_walls=True, tolerance=DOE2_TOLERANCE) + r_geo = r_geo if r_geo.normal.z >= 0 else r_geo.flip() + wall_count = len([orient for orient in face_orientations if orient == 0]) + if len(r_geo) == wall_count: # all walls can be represented with room vertices + rm_pts = r_geo.lower_left_counter_clockwise_boundary + ceil_count = len([orient for orient in face_orientations if orient == 1]) + floor_count = len([orient for orient in face_orientations if orient == -1]) + for face, orient in zip(room.faces, face_orientations): + if orient == 0: # wall to associate with a room vertex + f_origin = face.geometry.lower_left_corner + for i, r_pt in enumerate(rm_pts): + if f_origin.is_equivalent(r_pt, DOE2_TOLERANCE): + face_locations.append('SPACE-V{}'.format(i + 1)) + break + else: + face_locations.append(None) + elif orient == 1: + loc = 'TOP' if ceil_count == 1 else None + face_locations.append(loc) + else: + loc = 'BOTTOM' if floor_count == 1 else None + face_locations.append(loc) + + # if the room is not extruded, just use the generic horizontal boundary + if len(face_locations) == 0: + r_geo = room.horizontal_boundary(match_walls=False, tolerance=DOE2_TOLERANCE) + r_geo = r_geo if r_geo.normal.z >= 0 else r_geo.flip() + r_geo = r_geo.remove_colinear_vertices(tolerance=DOE2_TOLERANCE) + face_locations = [None] * len(room.faces) + + # create the room polygon string from the geometry room_polygon, pos_info = face_3d_to_inp(r_geo, doe2_id) space_origin, _, _ = pos_info origin = space_origin - floor_origin @@ -496,15 +581,16 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals # gather together all definitions and polygons to define the room room_polygons = [room_polygon] room_defs = [space_def] - for face in room.faces: + for face, f_loc in zip(room.faces, face_locations): # first check if this is a face that should be excluded if isinstance(face.boundary_condition, Surface): if exclude_interior_walls and isinstance(face.type, Wall): continue - elif exclude_interior_ceilings and isinstance(face.type, (Floor, RoofCeiling)): + elif exclude_interior_ceilings and \ + isinstance(face.type, (Floor, RoofCeiling)): continue # add the face definition along with all apertures and doors - face_polygon, face_def = face_to_inp(face, space_origin) + face_polygon, face_def = face_to_inp(face, space_origin, f_loc) room_polygons.append(face_polygon) room_defs.append(face_def) for ap in face.apertures: From 99e65d4de0d08e85efcc3979b7325020a3e76092 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 2 May 2024 14:17:11 -0700 Subject: [PATCH 16/27] fix(writer): Fix issues in previous commit --- honeybee_doe2/writer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 0283378..2073f40 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -533,6 +533,7 @@ def _is_room_3d_extruded(hb_room): if is_extrusion: # try to translate without using POLYGON for the Room faces r_geo = room.horizontal_boundary(match_walls=True, tolerance=DOE2_TOLERANCE) r_geo = r_geo if r_geo.normal.z >= 0 else r_geo.flip() + r_geo = r_geo.remove_duplicate_vertices(DOE2_TOLERANCE) wall_count = len([orient for orient in face_orientations if orient == 0]) if len(r_geo) == wall_count: # all walls can be represented with room vertices rm_pts = r_geo.lower_left_counter_clockwise_boundary @@ -591,7 +592,8 @@ def _is_room_3d_extruded(hb_room): continue # add the face definition along with all apertures and doors face_polygon, face_def = face_to_inp(face, space_origin, f_loc) - room_polygons.append(face_polygon) + if face_polygon != '': + room_polygons.append(face_polygon) room_defs.append(face_def) for ap in face.apertures: ap_def = aperture_to_inp(ap) From 1bd630d7bcacc5fe86fc250e132c69fb226b5e71 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 2 May 2024 15:48:50 -0700 Subject: [PATCH 17/27] fix(schedule): Add workaround for ScheduleFixedInterval to avoid errors --- honeybee_doe2/_extend_honeybee.py | 5 +- honeybee_doe2/schedule.py | 77 ++++++++++++++++++++++++++++++- honeybee_doe2/writer.py | 6 ++- tests/schedule_test.py | 70 +++++++++++++++++++++++++++- 4 files changed, 151 insertions(+), 7 deletions(-) diff --git a/honeybee_doe2/_extend_honeybee.py b/honeybee_doe2/_extend_honeybee.py index 40b4a07..a91da90 100644 --- a/honeybee_doe2/_extend_honeybee.py +++ b/honeybee_doe2/_extend_honeybee.py @@ -23,6 +23,7 @@ # import the modules that extend honeybee-energy objects from honeybee_energy.schedule.day import ScheduleDay from honeybee_energy.schedule.ruleset import ScheduleRuleset +from honeybee_energy.schedule.fixedinterval import ScheduleFixedInterval from honeybee_energy.material.opaque import EnergyMaterial, EnergyMaterialNoMass, \ EnergyMaterialVegetation from honeybee_energy.construction.opaque import OpaqueConstruction @@ -32,7 +33,8 @@ from honeybee_energy.construction.air import AirBoundaryConstruction from honeybee_energy.simulation.runperiod import RunPeriod -from .schedule import schedule_day_to_inp, schedule_ruleset_to_inp +from .schedule import schedule_day_to_inp, schedule_ruleset_to_inp, \ + schedule_fixed_interval_to_inp from .construction import opaque_material_to_inp, opaque_construction_to_inp, \ window_construction_to_inp, air_construction_to_inp from .simulation import run_period_to_inp @@ -40,6 +42,7 @@ # add the methods to the honeybee-energy classes ScheduleDay.to_inp = schedule_day_to_inp ScheduleRuleset.to_inp = schedule_ruleset_to_inp +ScheduleFixedInterval.to_inp = schedule_fixed_interval_to_inp EnergyMaterial.to_inp = opaque_material_to_inp EnergyMaterialNoMass.to_inp = opaque_material_to_inp EnergyMaterialVegetation.to_inp = opaque_material_to_inp diff --git a/honeybee_doe2/schedule.py b/honeybee_doe2/schedule.py index 00fba00..482024b 100644 --- a/honeybee_doe2/schedule.py +++ b/honeybee_doe2/schedule.py @@ -3,6 +3,7 @@ from __future__ import division from ladybug.dt import Date, MONTHNAMES +from ladybug.analysisperiod import AnalysisPeriod from honeybee.typing import clean_doe2_string from .config import RES_CHARS @@ -94,7 +95,7 @@ def _format_day_values(values_to_format): def schedule_ruleset_to_inp(schedule): - """Convert a ScheduleRuleset into a WEEK-SCHEDULE and SCHEDULE INP strings. + """Convert a ScheduleRuleset into a WEEK-SCHEDULE-PD and SCHEDULE INP strings. Note that this method only outputs SCHEDULE and WEEK-SCHEDULE objects However, to write the full schedule into an INP, the schedules's @@ -106,7 +107,7 @@ def schedule_ruleset_to_inp(schedule): - year_schedule: Text string representation of the SCHEDULE describing this schedule. - - week_schedules: A list of WEEK-SCHEDULE test strings that are + - week_schedules: A list of WEEK-SCHEDULE-PD text strings that are referenced in the year_schedule. """ # setup the DOE-2 identifier and lists for keywords and values @@ -252,6 +253,78 @@ def _inp_week_schedule_from_week_list(schedule, week_list, week_index): return year_schedule, week_schedules +def schedule_fixed_interval_to_inp(schedule): + """Convert a ScheduleFixedInterval to INP strings. + + Note that true Fixed Interval schedules are not supported by DOE-2 and there + is no way to faithfully translate them given that DOE-2 SCHEDULE objects have + a hard limit of 12 THRU statements. This method tries to write as best of + an approximation for the schedule as possible by averaging the hourly values + from each day of the fixed interval schedule. A separate day schedule will + be used for each month in an attempt to account for changes in the fixed + interval schedule over the year. + + All of this will allow the translation to succeed and gives roughly matching + behavior of the DOE-2 simulation to the EnergyPlus simulation. However, it is + recommended that users replace ScheduleFixedIntervals with ScheduleRulesets + that they know best represents the schedule. Or EnergyPlus should be used for + the simulation instead of DOE-2. + + Returns: + A tuple with two elements + + - year_schedule: Text string representation of the SCHEDULE + describing this schedule. + + - week_schedules: A list of WEEK-SCHEDULE text strings that are + referenced in the year_schedule. + + - day_schedules: A list of DAY-SCHEDULE-PD text strings that are + referenced in the week_schedules. + """ + # setup the DOE-2 identifier and lists for keywords and values + doe2_id = clean_doe2_string(schedule.identifier, RES_CHARS) + base_id = clean_doe2_string(schedule.identifier, RES_CHARS - 6) + type_text = schedule_type_limit_to_inp(schedule.schedule_type_limit) + + # loop through the months of the year and create appropriate schedules + day_schedules, week_schedules = [], [] + year_keywords, year_values = ['TYPE'], [type_text] + sch_data = schedule.data_collection + if sch_data.header.analysis_period.timestep != 1: + sch_data = sch_data.cull_to_timestep(1) + for month_i in range(1, 13): + # create the day schedules + month_name = AnalysisPeriod.MONTHNAMES[month_i] + month_days = AnalysisPeriod.NUMOFDAYSEACHMONTH[month_i - 1] + week_id = '{}{}'.format(base_id, month_name) + day_id = '{}{}'.format(week_id, 'Day') + period = AnalysisPeriod(st_month=month_i, end_month=month_i, end_day=month_days) + month_data = sch_data.filter_by_analysis_period(period) + mon_per_hr = month_data.average_monthly_per_hour() + hour_values = [round(v, 3) for v in mon_per_hr.values] + if type_text == 'TEMPERATURE': + hour_values = [round(v * (9. / 5.) + 32., 2) for v in hour_values] + day_keywords, day_values = ['TYPE', 'VALUES'], [type_text, hour_values] + day_inp_str = generate_inp_string_list_format( + day_id, 'DAY-SCHEDULE-PD', day_keywords, day_values) + day_schedules.append(day_inp_str) + # create week schedule + week_keywords = ['TYPE', 'DAYS', 'DAY-SCHEDULES'] + week_values = [type_text, '(ALL)', '("{}")'.format(day_id)] + week_sch = generate_inp_string( + week_id, 'WEEK-SCHEDULE', week_keywords, week_values) + week_schedules.append(week_sch) + # add values to the year schedules + thru = 'THRU {} {}'.format(month_name.upper(), period.end_day) + year_keywords.append(thru) + year_values.append('"{}"'.format(week_id)) + + # return all of the strings + year_schedule = generate_inp_string(doe2_id, 'SCHEDULE', year_keywords, year_values) + return year_schedule, week_schedules, day_schedules + + def energy_trans_sch_to_transmittance(shade_obj): """Try to extract the transmittance from the shade energy properties.""" trans = 0 diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 2073f40..da7adde 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -715,8 +715,10 @@ def model_to_inp( all_week_scheds.extend(week_schedules) all_year_scheds.append(year_schedule) else: # ScheduleFixedInterval - pass - # TODO: Add translators for ScheduleFixedInterval + year_schedule, week_schedules, year_schedule = sched.to_inp() + all_day_scheds.extend(day_scheds) + all_week_scheds.extend(week_schedules) + all_year_scheds.append(year_schedule) model_str.append(header_comment_minor('Day Schedules')) model_str.extend(all_day_scheds) model_str.append(header_comment_minor('Week Schedules')) diff --git a/tests/schedule_test.py b/tests/schedule_test.py index 83128ec..4d0871d 100644 --- a/tests/schedule_test.py +++ b/tests/schedule_test.py @@ -3,9 +3,11 @@ from honeybee_energy.schedule.day import ScheduleDay from honeybee_energy.schedule.rule import ScheduleRule from honeybee_energy.schedule.ruleset import ScheduleRuleset +from honeybee_energy.schedule.fixedinterval import ScheduleFixedInterval import honeybee_energy.lib.scheduletypelimits as schedule_types -from honeybee_doe2.schedule import schedule_day_to_inp, schedule_ruleset_to_inp +from honeybee_doe2.schedule import schedule_day_to_inp, schedule_ruleset_to_inp, \ + schedule_fixed_interval_to_inp def test_schedule_day_to_inp(): @@ -213,7 +215,6 @@ def test_schedule_ruleset_to_inp_date_range(): inp_yr_str, inp_week_strs = schedule_ruleset_to_inp(school_schedule) - print(inp_yr_str) assert inp_yr_str == \ '"School Occupancy" = SCHEDULE\n' \ ' TYPE = FRACTION\n' \ @@ -229,3 +230,68 @@ def test_schedule_ruleset_to_inp_date_range(): ' THRU DEC 31 = "School Occupancy Week 2"\n' \ ' ..\n' assert len(inp_week_strs) == 2 + + +def test_schedule_fixedinterval_to_inp(): + """Test the ScheduleFixedInterval to_inp method.""" + trans_sched = ScheduleFixedInterval( + 'Custom Transmittance', [x / 8760 for x in range(8760)], + schedule_types.fractional) + inp_yr_str, inp_week_strs, inp_day_strs = schedule_fixed_interval_to_inp(trans_sched) + + assert inp_yr_str == \ + '"Custom Transmittance" = SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' THRU JAN 31 = "Custom TransmittanceJan"\n' \ + ' THRU FEB 28 = "Custom TransmittanceFeb"\n' \ + ' THRU MAR 31 = "Custom TransmittanceMar"\n' \ + ' THRU APR 30 = "Custom TransmittanceApr"\n' \ + ' THRU MAY 31 = "Custom TransmittanceMay"\n' \ + ' THRU JUN 30 = "Custom TransmittanceJun"\n' \ + ' THRU JUL 31 = "Custom TransmittanceJul"\n' \ + ' THRU AUG 31 = "Custom TransmittanceAug"\n' \ + ' THRU SEP 30 = "Custom TransmittanceSep"\n' \ + ' THRU OCT 31 = "Custom TransmittanceOct"\n' \ + ' THRU NOV 30 = "Custom TransmittanceNov"\n' \ + ' THRU DEC 31 = "Custom TransmittanceDec"\n' \ + ' ..\n' + + assert len(inp_week_strs) == 12 + assert inp_week_strs[0] == \ + '"Custom TransmittanceJan" = WEEK-SCHEDULE\n' \ + ' TYPE = FRACTION\n' \ + ' DAYS = (ALL)\n' \ + ' DAY-SCHEDULES = ("Custom TransmittanceJanDay")\n' \ + ' ..\n' + + assert len(inp_day_strs) == 12 + assert inp_day_strs[0] == \ + '"Custom TransmittanceJanDay" = DAY-SCHEDULE-PD\n' \ + ' TYPE = FRACTION\n' \ + ' VALUES = (\n' \ + ' 0.041,\n' \ + ' 0.041,\n' \ + ' 0.041,\n' \ + ' 0.041,\n' \ + ' 0.042,\n' \ + ' 0.042,\n' \ + ' 0.042,\n' \ + ' 0.042,\n' \ + ' 0.042,\n' \ + ' 0.042,\n' \ + ' 0.042,\n' \ + ' 0.042,\n' \ + ' 0.042,\n' \ + ' 0.043,\n' \ + ' 0.043,\n' \ + ' 0.043,\n' \ + ' 0.043,\n' \ + ' 0.043,\n' \ + ' 0.043,\n' \ + ' 0.043,\n' \ + ' 0.043,\n' \ + ' 0.043,\n' \ + ' 0.044,\n' \ + ' 0.044,\n' \ + ' )\n' \ + ' ..\n' From e2af2ee1f910d0b5f02f313dd317993a90bbc3b7 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 2 May 2024 16:51:47 -0700 Subject: [PATCH 18/27] feat(properties): Add RoomDoe2Properties for the few extra attributes It seems there are 5 numerical properties that have no equivalent in honeybee-core or honeybee-energy since they have to do with the simplified way that DOE-2 models HVAC systems. But BR+A has confirmed that they need them and I can see that they are fairly prominent within the eQuest UI. So I think others will need to set them as well. So we're going to add some RoomDoe2Properties in order to manage them. I'll make some pathways to make these new properties compatible with the old hacky way that this was done with Room.user_data. But these properties will be the long-term solution for this. --- cli_test/unnamed.inp | 2226 -------------------------- honeybee_doe2/load.py | 2 - honeybee_doe2/properties/__init__.py | 1 + honeybee_doe2/properties/room.py | 188 +++ 4 files changed, 189 insertions(+), 2228 deletions(-) delete mode 100644 cli_test/unnamed.inp create mode 100644 honeybee_doe2/properties/__init__.py create mode 100644 honeybee_doe2/properties/room.py diff --git a/cli_test/unnamed.inp b/cli_test/unnamed.inp deleted file mode 100644 index 5307f5c..0000000 --- a/cli_test/unnamed.inp +++ /dev/null @@ -1,2226 +0,0 @@ -INPUT .. - - - - -$ --------------------------------------------------------- -$ Abort, Diagnostics -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Global Parameters -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Title, Run Periods, Design Days, Holidays -$ --------------------------------------------------------- - - - - -TITLE - LINE-1 = *unnamed* - .. - -"Entire Year" = RUN-PERIOD-PD - BEGIN-MONTH = 1 - BEGIN-DAY = 1 - BEGIN-YEAR = 2021 - END-MONTH = 12 - END-DAY = 31 - END-YEAR = 2021 - .. - -"Standard US Holidays" = HOLIDAYS - LIBRARY-ENTRY "US" - .. - -$ --------------------------------------------------------- -$ Compliance Data -$ --------------------------------------------------------- - - - - -"Compliance Data" = COMPLIANCE - C-PERMIT-SCOPE = 0 - C-PROJ-NAME = *sample_project* - C-BUILDING-TYPE = 32 - C-CONS-PHASE = 0 - C-NR-DHW-INCL = 1 - C-CODE-VERSION = 1 - C-901-NUM-FLRS = 1 - C-901-BLDG-TYPE = 32 - .. - -$--------------------------------------------------------- -$ Site and Building Data -$--------------------------------------------------------- - -"Site Data" = SITE-PARAMETERS - ALTITUDE = 150 - C-STATE = 21 - C-WEATHER-FILE = *TMY2\HARTFOCT.bin* - C-COUNTRY = 1 - C-901-LOCATION = 1092 - .. -"Building Data" = BUILD-PARAMETERS - HOLIDAYS = "Standard US Holidays" - .. - - -PROJECT-DATA - .. - - - -$ --------------------------------------------------------- -$ Day Schedules -$ --------------------------------------------------------- - - - - - - - -$ --------------------------------------------------------- -$ Week Schedules -$ --------------------------------------------------------- - - - - - - - -$ --------------------------------------------------------- -$ Materials / Layers / Constructions -$ --------------------------------------------------------- - - - - - -"Typical Insulation-R31" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 31.000000000000004 - .. - -"Typical Insulation-R7" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 7.0 - .. - -"5/8 in. Gypsum Board" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.052 - CONDUCTIVITY = 0.051 - DENSITY = 49.944 - SPECIFIC-HEAT = 0.468 - .. - -"Typical Insulation-R24" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 23.999999999999996 - .. - -"Generic Roof Membrane" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.033 - CONDUCTIVITY = 0.051 - DENSITY = 69.921 - SPECIFIC-HEAT = 0.628 - .. - -"Metal Roof Surface" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 0.00010038981005248146 - .. - -"Generic LW Concrete" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.328 - CONDUCTIVITY = 0.168 - DENSITY = 79.91 - SPECIFIC-HEAT = 0.361 - .. - -"F08 Metal surface" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 0.00010038981005248146 - .. - -"Typical Insulation-R4" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 4.0 - .. - -"Typical Insulation-R3" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 2.9999999999999996 - .. - -"8 n. Nrmlwght Cncrt Flr" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.667 - CONDUCTIVITY = 0.732 - DENSITY = 144.962 - SPECIFIC-HEAT = 0.357 - .. - -"8 n. Cncrt Blck Bsmnt Wll" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.667 - CONDUCTIVITY = 0.42 - DENSITY = 114.996 - SPECIFIC-HEAT = 0.392 - .. - -"Typical Carpet Pad" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 1.22923037424 - .. - -"Generic Ceiling Air Gap" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.328 - CONDUCTIVITY = 0.176 - DENSITY = 0.08 - SPECIFIC-HEAT = 0.43 - .. - -"Generic 50mm Insulation" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.164 - CONDUCTIVITY = 0.01 - DENSITY = 2.684 - SPECIFIC-HEAT = 0.52 - .. - -"Generic Wall Air Gap" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.328 - CONDUCTIVITY = 0.211 - DENSITY = 0.08 - SPECIFIC-HEAT = 0.43 - .. - -"25mm Stucco" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.083 - CONDUCTIVITY = 0.228 - DENSITY = 115.87 - SPECIFIC-HEAT = 0.361 - .. - -"Generic Painted Metal" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 0.00018927544456666666 - .. - -"Generic Brick" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.328 - CONDUCTIVITY = 0.285 - DENSITY = 119.865 - SPECIFIC-HEAT = 0.34 - .. - -"Typical Insulation-R17" = MATERIAL - TYPE = RESISTANCE - RESISTANCE = 17.0 - .. - -"Roof Membrane" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.031 - CONDUCTIVITY = 0.051 - DENSITY = 70.002 - SPECIFIC-HEAT = 0.627 - .. - -"Generic HW Concrete" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.656 - CONDUCTIVITY = 0.618 - DENSITY = 139.843 - SPECIFIC-HEAT = 0.387 - .. - -"Generic Gypsum Board" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.042 - CONDUCTIVITY = 0.051 - DENSITY = 49.944 - SPECIFIC-HEAT = 0.469 - .. - -"Generic Acoustic Tile" = MATERIAL - TYPE = PROPERTIES - THICKNESS = 0.066 - CONDUCTIVITY = 0.019 - DENSITY = 22.974 - SPECIFIC-HEAT = 0.254 - .. - -"Generic Exterior Wall_l" = LAYERS - MATERIAL = ( - "Generic Brick", - "Generic LW Concrete", - "Generic 50mm Insulation", - "Generic Wall Air Gap", - "Generic Gypsum Board" - ) - .. - -"Generic Exterior Wall_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.65 - ROUGHNESS = 3 - LAYERS = "Generic Exterior Wall_l" - .. - -"Generic Interior Wall_l" = LAYERS - MATERIAL = ( - "Generic Gypsum Board", - "Generic Wall Air Gap", - "Generic Gypsum Board" - ) - .. - -"Generic Interior Wall_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.5 - ROUGHNESS = 4 - LAYERS = "Generic Interior Wall_l" - .. - -"Generic Underground Wall_l" = LAYERS - MATERIAL = ( - "Generic 50mm Insulation", - "Generic HW Concrete", - "Generic Wall Air Gap", - "Generic Gypsum Board" - ) - .. - -"Generic Underground Wall_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 3 - LAYERS = "Generic Underground Wall_l" - .. - -"Generic Exposed Floor_l" = LAYERS - MATERIAL = ( - "Generic Painted Metal", - "Generic Ceiling Air Gap", - "Generic 50mm Insulation", - "Generic LW Concrete" - ) - .. - -"Generic Exposed Floor_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.5 - ROUGHNESS = 5 - LAYERS = "Generic Exposed Floor_l" - .. - -"Generic Interior Floor_l" = LAYERS - MATERIAL = ( - "Generic Acoustic Tile", - "Generic Ceiling Air Gap", - "Generic LW Concrete" - ) - .. - -"Generic Interior Floor_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.2 - ROUGHNESS = 4 - LAYERS = "Generic Interior Floor_l" - .. - -"Generic Ground Slab_l" = LAYERS - MATERIAL = ( - "Generic 50mm Insulation", - "Generic HW Concrete" - ) - .. - -"Generic Ground Slab_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 3 - LAYERS = "Generic Ground Slab_l" - .. - -"Generic Roof_l" = LAYERS - MATERIAL = ( - "Generic Roof Membrane", - "Generic 50mm Insulation", - "Generic LW Concrete", - "Generic Ceiling Air Gap", - "Generic Acoustic Tile" - ) - .. - -"Generic Roof_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.65 - ROUGHNESS = 3 - LAYERS = "Generic Roof_l" - .. - -"Generic Interior Ceiling_l" = LAYERS - MATERIAL = ( - "Generic LW Concrete", - "Generic Ceiling Air Gap", - "Generic Acoustic Tile" - ) - .. - -"Generic Interior Ceiling_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.8 - ROUGHNESS = 3 - LAYERS = "Generic Interior Ceiling_l" - .. - -"Generic Underground Roof_l" = LAYERS - MATERIAL = ( - "Generic 50mm Insulation", - "Generic HW Concrete", - "Generic Ceiling Air Gap", - "Generic Acoustic Tile" - ) - .. - -"Generic Underground Roof_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 3 - LAYERS = "Generic Underground Roof_l" - .. - -"Tpcl Insltd Bsmnt Mss WllR8_l" = LAYERS - MATERIAL = ( - "Typical Insulation-R7", - "8 n. Cncrt Blck Bsmnt Wll" - ) - .. - -"Tpcl Insltd Bsmnt Mss WllR8_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 4 - LAYERS = "Tpcl Insltd Bsmnt Mss WllR8_l" - .. - -"Tpcl Insltd Crptd 8n Slb FlrR5_l" = LAYERS - MATERIAL = ( - "Typical Insulation-R4", - "8 n. Nrmlwght Cncrt Flr", - "Typical Carpet Pad" - ) - .. - -"Tpcl Insltd Crptd 8n Slb FlrR5_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 4 - LAYERS = "Tpcl Insltd Crptd 8n Slb FlrR5_l" - .. - -"Typical Overhead Door-R4_l" = LAYERS - MATERIAL = ( - "Typical Insulation-R4" - ) - .. - -"Typical Overhead Door-R4_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 4 - LAYERS = "Typical Overhead Door-R4_l" - .. - -"Tpcl Insltd Mtl DrR3_l" = LAYERS - MATERIAL = ( - "F08 Metal surface", - "Typical Insulation-R3" - ) - .. - -"Tpcl Insltd Mtl DrR3_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 5 - LAYERS = "Tpcl Insltd Mtl DrR3_l" - .. - -"TpclInsltdStlFrmdExtrrFlrR27_l" = LAYERS - MATERIAL = ( - "25mm Stucco", - "5/8 in. Gypsum Board", - "Typical Insulation-R24", - "5/8 in. Gypsum Board", - "Typical Carpet Pad" - ) - .. - -"TpclInsltdStlFrmdExtrrFlrR27_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 5 - LAYERS = "TpclInsltdStlFrmdExtrrFlrR27_l" - .. - -"TpclInsltdStlFrmdExtrrWllR19_l" = LAYERS - MATERIAL = ( - "25mm Stucco", - "5/8 in. Gypsum Board", - "Typical Insulation-R17", - "5/8 in. Gypsum Board" - ) - .. - -"TpclInsltdStlFrmdExtrrWllR19_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 5 - LAYERS = "TpclInsltdStlFrmdExtrrWllR19_l" - .. - -"Typical IEAD Roof-R32_l" = LAYERS - MATERIAL = ( - "Roof Membrane", - "Typical Insulation-R31", - "Metal Roof Surface" - ) - .. - -"Typical IEAD Roof-R32_c" = CONSTRUCTION - TYPE = LAYERS - ABSORPTANCE = 0.7 - ROUGHNESS = 1 - LAYERS = "Typical IEAD Roof-R32_l" - .. - - -$ --------------------------------------------------------- -$ Glass Type Codes -$ --------------------------------------------------------- - - - - -"U 0.36 SHGC 0.38 Smpl Glzng Wndw" = GLASS-TYPE - TYPE = SHADING-COEF - SHADING-COEF = 0.4367816091954023 - GLASS-CONDUCT = 0.36 - .. - -"Generic Double Pane" = GLASS-TYPE - TYPE = SHADING-COEF - SHADING-COEF = 0.4874490884111821 - GLASS-CONDUCT = 0.297 - .. - -"U 0.5 SHGC 0.4 Smpl Glzng Sklght" = GLASS-TYPE - TYPE = SHADING-COEF - SHADING-COEF = 0.4597701149425288 - GLASS-CONDUCT = 0.5 - .. - -"Generic Single Pane" = GLASS-TYPE - TYPE = SHADING-COEF - SHADING-COEF = 0.9206277902735016 - GLASS-CONDUCT = 1.009 - .. - -"U0.44SHGC0.26DblRfBHClr6mm13mmAr" = GLASS-TYPE - TYPE = SHADING-COEF - SHADING-COEF = 0.318658941545642 - GLASS-CONDUCT = 0.437 - .. - - -$ --------------------------------------------------------- -$ Door Construction -$ --------------------------------------------------------- - - -"Generic Door" = CONSTRUCTION - TYPE = U-VALUE - U-VALUE = 0.5 -.. - - -$ --------------------------------------------------------- -$ Polygons -$ --------------------------------------------------------- - - - - -"Level_0 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 98.425197, -32.808399 ) - V3 = ( 98.425197, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Room1 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 98.425197, -32.808399 ) - V3 = ( 98.425197, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Face1 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 98.425197, -0.000000 ) - V4 = ( 98.425197, 13.123360 ) - .. -"Face2 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 32.808399, -0.000000 ) - V4 = ( 32.808399, 13.123360 ) - .. -"Face3 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 98.425197, -0.000000 ) - V4 = ( 98.425197, 13.123360 ) - .. -"Face4 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 32.808399, -0.000000 ) - V4 = ( 32.808399, 13.123360 ) - .. -"Face5 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 98.425197, -32.808399 ) - V3 = ( 98.425197, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Face6 Plg" = POLYGON - V1 = ( 0.000000, 32.808399 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 68.897638, 0.000000 ) - V4 = ( 68.897638, 32.808399 ) - .. -"Face6_1 Plg" = POLYGON - V1 = ( 0.000000, 32.808399 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 29.527559, 0.000000 ) - V4 = ( 29.527559, 32.808399 ) - .. -"Level_1 Plg" = POLYGON - V1 = ( 29.527559, -32.808399 ) - V2 = ( 98.425197, -32.808399 ) - V3 = ( 98.425197, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - V5 = ( 0.000000, -78.740157 ) - V6 = ( 29.527559, -78.740157 ) - .. -"Room3 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 68.897638, -32.808399 ) - V3 = ( 68.897638, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Face1_2 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 68.897638, -0.000000 ) - V4 = ( 68.897638, 13.123360 ) - .. -"Face2_2 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 32.808399, -0.000000 ) - V4 = ( 32.808399, 13.123360 ) - .. -"Face3_3 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 68.897638, -0.000000 ) - V4 = ( 68.897638, 13.123360 ) - .. -"Face4_2 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 32.808399, -0.000000 ) - V4 = ( 32.808399, 13.123360 ) - .. -"Face5_3 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 68.897638, -32.808399 ) - V3 = ( 68.897638, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Face6_4 Plg" = POLYGON - V1 = ( 0.000000, 32.808399 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 34.448819, 0.000000 ) - V4 = ( 34.448819, 32.808399 ) - .. -"Face6_5 Plg" = POLYGON - V1 = ( 0.000000, 32.808399 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 34.448819, 0.000000 ) - V4 = ( 34.448819, 32.808399 ) - .. -"Room2 Plg" = POLYGON - V1 = ( 0.000000, -78.740157 ) - V2 = ( 29.527559, -78.740157 ) - V3 = ( 29.527559, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Face1_1 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 78.740157, 0.000000 ) - V4 = ( 78.740157, 13.123360 ) - .. -"Face2_1 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 29.527559, -0.000000 ) - V4 = ( 29.527559, 13.123360 ) - .. -"Face3_1 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 45.931759, 0.000000 ) - V4 = ( 45.931759, 13.123360 ) - .. -"Face4_1 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 29.527559, -0.000000 ) - V4 = ( 29.527559, 13.123360 ) - .. -"Face5_1 Plg" = POLYGON - V1 = ( 0.000000, -45.931759 ) - V2 = ( 29.527559, -45.931759 ) - V3 = ( 29.527559, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Face6_2 Plg" = POLYGON - V1 = ( 0.000000, 45.931759 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 29.527559, 0.000000 ) - V4 = ( 29.527559, 45.931759 ) - .. -"Face5_2 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 29.527559, -32.808399 ) - V3 = ( 29.527559, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Face3_2 Plg" = POLYGON - V1 = ( 0.000000, 13.123360 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 32.808399, -0.000000 ) - V4 = ( 32.808399, 13.123360 ) - .. -"Face6_3 Plg" = POLYGON - V1 = ( 0.000000, 32.808399 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 29.527559, 0.000000 ) - V4 = ( 29.527559, 32.808399 ) - .. -"Level_2 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 63.976378, -32.808399 ) - V3 = ( 63.976378, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Room4 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 63.976378, -32.808399 ) - V3 = ( 63.976378, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Face1_3 Plg" = POLYGON - V1 = ( 0.000000, 9.842520 ) - V2 = ( -0.000000, 0.000000 ) - V3 = ( 63.976378, -0.000000 ) - V4 = ( 63.976378, 9.842520 ) - .. -"Face2_3 Plg" = POLYGON - V1 = ( 0.000000, 9.842520 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 32.808399, 0.000000 ) - V4 = ( 32.808399, 9.842520 ) - .. -"Face3_4 Plg" = POLYGON - V1 = ( 0.000000, 9.842520 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 63.976378, -0.000000 ) - V4 = ( 63.976378, 9.842520 ) - .. -"Face4_3 Plg" = POLYGON - V1 = ( 0.000000, 9.842520 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 32.808399, 0.000000 ) - V4 = ( 32.808399, 9.842520 ) - .. -"Face5_4 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 34.448819, -32.808399 ) - V3 = ( 34.448819, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. -"Face6_6 Plg" = POLYGON - V1 = ( 0.000000, 32.808399 ) - V2 = ( 0.000000, 0.000000 ) - V3 = ( 63.976378, 0.000000 ) - V4 = ( 63.976378, 32.808399 ) - .. -"Face5_5 Plg" = POLYGON - V1 = ( 0.000000, -32.808399 ) - V2 = ( 29.527559, -32.808399 ) - V3 = ( 29.527559, 0.000000 ) - V4 = ( 0.000000, 0.000000 ) - .. - - - -$ --------------------------------------------------------- -$ Wall Parameters -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Fixed and Building Shades -$ --------------------------------------------------------- - - - - - -$ --------------------------------------------------------- -$ Misc Cost Related Objects -$ --------------------------------------------------------- - - - - -$ ********************************************************* -$ ** ** -$ ** Performance Curves ** -$ ** ** -$ ********************************************************* - - - - -$ ********************************************************* -$ ** ** -$ ** Floors / Spaces / Walls / Windows / Doors ** -$ ** ** -$ ********************************************************* - - - - - -"Level_0"= FLOOR - SHAPE = POLYGON - POLYGON = "Level_0 Plg" - AZIMUTH = 0.0 - X = 0.0 - Y = 32.80839895013123 - Z = 0.0 - SPACE-HEIGHT = 13.123359580052492 - FLOOR-HEIGHT = 13.123359580052492 - .. - -"Room1" = SPACE - SHAPE = POLYGON - POLYGON = "Room1 Plg" - AZIMUTH = 0 - X = 0.0 - Y = 0.0 - Z = 0.0 - VOLUME = 42377.60006578629 - ZONE-TYPE = UNCONDITIONED - .. - -"Face1" = EXTERIOR-WALL - POLYGON = "Face1 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 180.0 - X = 2.9582283945787943e-31 - Y = -32.80839895013123 - Z = -5.329070518200751e-15 - .. -"Rmc4f0fFc4cb800d2Glz00" = WINDOW - - X = 0.9842519685039394 - Y = 2.624671916010498 - WIDTH = 96.45669291338581 - HEIGHT = 6.695591622475768 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face2" = EXTERIOR-WALL - POLYGON = "Face2 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 90.0 - X = 98.42519685039369 - Y = -32.80839895013123 - Z = -1.7763568394002505e-15 - .. -"Rmc4f0fFcb878607Glz00" = WINDOW - - X = 0.3280839895013113 - Y = 2.6246719160104988 - WIDTH = 32.15223097112861 - HEIGHT = 6.695591622475764 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face3" = EXTERIOR-WALL - POLYGON = "Face3 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 0.0 - X = 98.42519685039369 - Y = 0.0 - Z = -5.329070518200751e-15 - .. -"Rmc4f0fFc39c880Glz00" = WINDOW - - X = 0.9842519685039407 - Y = 2.624671916010498 - WIDTH = 96.45669291338581 - HEIGHT = 6.695591622475768 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face4" = EXTERIOR-WALL - POLYGON = "Face4 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 270.0 - X = 0.0 - Y = 0.0 - Z = -1.7763568394002505e-15 - .. -"Rmc4f0fFc26fc7Glz00" = WINDOW - - X = 0.32808398950130896 - Y = 2.6246719160104988 - WIDTH = 32.15223097112861 - HEIGHT = 6.695591622475764 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - - -"Face6" = ROOF - POLYGON = "Face6 Plg" - CONSTRUCTION = "Generic Interior Ceiling_c" - TILT = 0.0 - AZIMUTH = 180 - X = 29.527559055118108 - Y = -32.80839895013123 - Z = 13.123359580052492 - .. - -"Face6_1" = ROOF - POLYGON = "Face6_1 Plg" - CONSTRUCTION = "Generic Interior Ceiling_c" - TILT = 0.0 - AZIMUTH = 180 - X = 0.0 - Y = -32.80839895013123 - Z = 13.123359580052492 - .. - - -"Face5_ug Plg" = POLYGON - V1 = ( 98.425197, -0.000000 ) - V2 = ( 98.425197, 32.808399 ) - V3 = ( 0.000000, 32.808399 ) - V4 = ( 0.000000, -0.000000 ) - .. -"Face5" = UNDERGROUND-WALL - CONSTRUCTION = "TpclInsltdCrptd8_lbFlrR5_c" - LOCATION = BOTTOM - POLYGON = "Face5_ug Plg" - AZIMUTH = 180 - X = 0.0 - Y = 0.0 - Z = 0.0 - .. - - - - - -"Level_1"= FLOOR - SHAPE = POLYGON - POLYGON = "Level_1 Plg" - AZIMUTH = 0.0 - X = 0.0 - Y = 32.80839895013123 - Z = 13.123359580052492 - SPACE-HEIGHT = 13.123359580052492 - FLOOR-HEIGHT = 13.123359580052492 - .. - -"Room3" = SPACE - SHAPE = POLYGON - POLYGON = "Room3 Plg" - AZIMUTH = 0 - X = 29.527559055118104 - Y = 0.0 - Z = 0.0 - VOLUME = 29664.3200460504 - ZONE-TYPE = UNCONDITIONED - .. - -"Face1_2" = EXTERIOR-WALL - POLYGON = "Face1_2 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 180.0 - X = 3.552713678800501e-15 - Y = -32.80839895013123 - Z = -3.552713678800501e-15 - .. -"Rm3bd51c8Fccb71168Glz00" = WINDOW - - X = 0.6889763779527592 - Y = 2.624671916010497 - WIDTH = 67.51968503937009 - HEIGHT = 6.695591622475767 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face2_2" = EXTERIOR-WALL - POLYGON = "Face2_2 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 90.0 - X = 68.89763779527559 - Y = -32.80839895013123 - Z = -1.7763568394002505e-15 - .. -"Rm3bd51c8Fc63f7077Glz00" = WINDOW - - X = 0.3280839895013113 - Y = 2.624671916010497 - WIDTH = 32.15223097112861 - HEIGHT = 6.695591622475767 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face3_3" = EXTERIOR-WALL - POLYGON = "Face3_3 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 0.0 - X = 68.89763779527559 - Y = 0.0 - Z = -3.552713678800501e-15 - .. -"Rm3bd51c8Fc61b6b287Glz00" = WINDOW - - X = 0.6889763779527557 - Y = 2.624671916010497 - WIDTH = 67.51968503937007 - HEIGHT = 6.695591622475767 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face4_2" = INTERIOR-WALL - POLYGON = "Face4_2 Plg" - CONSTRUCTION = "Generic Interior Wall_c" - TILT = 90.0 - AZIMUTH = 270.0 - X = 3.552713678800501e-15 - Y = 0.0 - Z = -1.7763568394002505e-15 - NEXT-TO = "Room2" - .. - -"Face6_4" = ROOF - POLYGON = "Face6_4 Plg" - CONSTRUCTION = "Typical IEAD Roof-R32_c" - TILT = 0.0 - AZIMUTH = 180 - X = 34.44881889763779 - Y = -32.80839895013123 - Z = 13.123359580052492 - .. - -"Face6_5" = ROOF - POLYGON = "Face6_5 Plg" - CONSTRUCTION = "Generic Interior Ceiling_c" - TILT = 0.0 - AZIMUTH = 180 - X = 3.552713678800501e-15 - Y = -32.80839895013123 - Z = 13.123359580052492 - .. - - - - -"Face5_3_ef Plg" = POLYGON - V1 = ( 68.897638, -0.000000 ) - V2 = ( 68.897638, 32.808399 ) - V3 = ( 0.000000, 32.808399 ) - V4 = ( 0.000000, -0.000000 ) - .. -"Face5_3" = INTERIOR-WALL - POLYGON = "Face5_3_ef Plg" - CONSTRUCTION = "Generic Interior Floor_c" - NEXT-TO = "Room1" - TILT = 180.0 - AZIMUTH = 180 - X = 0.0 - Y = 0.0 - Z = 0.0 - .. - - - -"Room2" = SPACE - SHAPE = POLYGON - POLYGON = "Room2 Plg" - AZIMUTH = 0 - X = 0.0 - Y = 0.0 - Z = 0.0 - VOLUME = 30511.872047366134 - ZONE-TYPE = UNCONDITIONED - .. - -"Face1_1" = EXTERIOR-WALL - POLYGON = "Face1_1 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 270.0 - X = 0.0 - Y = 0.0 - Z = -5.329070518200751e-15 - .. -"Rm583913Fc93020d1Glz00" = WINDOW - - X = 0.7874015748031483 - Y = 2.6246719160104988 - WIDTH = 77.16535433070864 - HEIGHT = 6.6955916224757654 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face2_1" = EXTERIOR-WALL - POLYGON = "Face2_1 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 180.0 - X = 9.860761315262648e-32 - Y = -78.74015748031495 - Z = -1.7763568394002505e-15 - .. -"Rm583913Fc4170b63Glz00" = WINDOW - - X = 0.29527559055118074 - Y = 2.624671916010497 - WIDTH = 28.937007874015745 - HEIGHT = 6.695591622475764 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face3_1" = EXTERIOR-WALL - POLYGON = "Face3_1 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 90.0 - X = 29.527559055118108 - Y = -78.74015748031495 - Z = -3.552713678800501e-15 - .. -"Rm583913Fcc46b34Glz00" = WINDOW - - X = 0.4593175853018396 - Y = 2.624671916010497 - WIDTH = 45.01312335958004 - HEIGHT = 6.695591622475767 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face4_1" = EXTERIOR-WALL - POLYGON = "Face4_1 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 0.0 - X = 29.527559055118108 - Y = 0.0 - Z = -1.7763568394002505e-15 - .. -"Rm583913Fcfd0d77f2Glz00" = WINDOW - - X = 0.29527559055118163 - Y = 2.624671916010497 - WIDTH = 28.937007874015745 - HEIGHT = 6.695591622475764 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face3_2" = INTERIOR-WALL - POLYGON = "Face3_2 Plg" - CONSTRUCTION = "Generic Interior Wall_c" - TILT = 90.0 - AZIMUTH = 90.0 - X = 29.527559055118108 - Y = -32.80839895013123 - Z = -1.7763568394002505e-15 - NEXT-TO = "Room3" - .. - -"Face6_2" = ROOF - POLYGON = "Face6_2 Plg" - CONSTRUCTION = "Typical IEAD Roof-R32_c" - TILT = 0.0 - AZIMUTH = 180 - X = 0.0 - Y = -78.74015748031495 - Z = 13.123359580052492 - .. - -"Face6_3" = ROOF - POLYGON = "Face6_3 Plg" - CONSTRUCTION = "Generic Interior Ceiling_c" - TILT = 0.0 - AZIMUTH = 180 - X = 0.0 - Y = -32.80839895013123 - Z = 13.123359580052492 - .. - - - -"Face5_1_ef Plg" = POLYGON - V1 = ( 29.527559, -0.000000 ) - V2 = ( 29.527559, 45.931759 ) - V3 = ( 0.000000, 45.931759 ) - V4 = ( 0.000000, -0.000000 ) - .. -"Face5_1" = EXTERIOR-WALL - CONSTRUCTION = "TpclInsltdStlFrmdExtrrFlrR27_c" - LOCATION = BOTTOM - POLYGON = "Face5_1_ef Plg" - TILT = 180.0 - AZIMUTH = 180 - X = 0.0 - Y = -32.80839895013123 - Z = 0.0 - .. - -"Face5_2_ef Plg" = POLYGON - V1 = ( 29.527559, -0.000000 ) - V2 = ( 29.527559, 32.808399 ) - V3 = ( 0.000000, 32.808399 ) - V4 = ( 0.000000, -0.000000 ) - .. -"Face5_2" = INTERIOR-WALL - POLYGON = "Face5_2_ef Plg" - CONSTRUCTION = "Generic Interior Floor_c" - NEXT-TO = "Room1" - TILT = 180.0 - AZIMUTH = 180 - X = 0.0 - Y = 0.0 - Z = 0.0 - .. - - - -"Level_2"= FLOOR - SHAPE = POLYGON - POLYGON = "Level_2 Plg" - AZIMUTH = 0.0 - X = 0.0 - Y = 32.80839895013123 - Z = 26.246719160104984 - SPACE-HEIGHT = 9.842519685039367 - FLOOR-HEIGHT = 9.842519685039367 - .. - -"Room4" = SPACE - SHAPE = POLYGON - POLYGON = "Room4 Plg" - AZIMUTH = 0 - X = 0.0 - Y = 0.0 - Z = 0.0 - VOLUME = 20659.08003207082 - ZONE-TYPE = UNCONDITIONED - .. - -"Face1_3" = EXTERIOR-WALL - POLYGON = "Face1_3 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 180.0 - X = 2.9582283945787943e-31 - Y = -32.80839895013123 - Z = -3.552713678800501e-15 - .. -"Rmcd7f84c1Fc3bdd_4Glz110" = WINDOW - - X = 35.58686023622047 - Y = 2.6246719160105 - WIDTH = 2.399114173228348 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_f4Glz21" = WINDOW - - X = 6.797490157480316 - Y = 2.6246719160105054 - WIDTH = 2.399114173228347 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_f4Glz02" = WINDOW - - X = 0.39985236220472475 - Y = 2.624671916010506 - WIDTH = 2.3991141732283463 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_f4Glz13" = WINDOW - - X = 3.5986712598425203 - Y = 2.624671916010506 - WIDTH = 2.399114173228347 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_f4Glz34" = WINDOW - - X = 9.996309055118111 - Y = 2.6246719160105054 - WIDTH = 2.3991141732283445 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_f4Glz45" = WINDOW - - X = 13.195127952755906 - Y = 2.624671916010505 - WIDTH = 2.3991141732283445 - HEIGHT = 6.561679790026246 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_f4Glz66" = WINDOW - - X = 19.592765748031493 - Y = 2.6246719160105045 - WIDTH = 2.3991141732283445 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_f4Glz77" = WINDOW - - X = 22.791584645669285 - Y = 2.6246719160105045 - WIDTH = 2.3991141732283445 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_f4Glz88" = WINDOW - - X = 25.99040354330708 - Y = 2.624671916010504 - WIDTH = 2.3991141732283445 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_f4Glz99" = WINDOW - - X = 29.189222440944878 - Y = 2.624671916010504 - WIDTH = 2.3991141732283445 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_Glz1010" = WINDOW - - X = 32.38804133858267 - Y = 2.624671916010504 - WIDTH = 2.399114173228341 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_Glz1211" = WINDOW - - X = 38.78567913385827 - Y = 2.6246719160105 - WIDTH = 2.399114173228348 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_Glz1312" = WINDOW - - X = 41.98449803149607 - Y = 2.6246719160104997 - WIDTH = 2.399114173228348 - HEIGHT = 6.56167979002625 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_Glz1513" = WINDOW - - X = 48.38213582677166 - Y = 2.624671916010499 - WIDTH = 2.399114173228348 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_Glz1614" = WINDOW - - X = 51.58095472440945 - Y = 2.624671916010499 - WIDTH = 2.399114173228348 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_Glz1715" = WINDOW - - X = 54.779773622047244 - Y = 2.6246719160104988 - WIDTH = 2.399114173228348 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_4Glz516" = WINDOW - - X = 16.3939468503937 - Y = 2.624671916010505 - WIDTH = 2.3991141732283445 - HEIGHT = 6.561679790026246 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_Glz1417" = WINDOW - - X = 45.183316929133866 - Y = 2.6246719160104997 - WIDTH = 2.399114173228341 - HEIGHT = 6.56167979002625 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_Glz1818" = WINDOW - - X = 57.978592519685044 - Y = 2.6246719160104988 - WIDTH = 2.399114173228341 - HEIGHT = 6.561679790026258 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc3bdd_Glz1919" = WINDOW - - X = 61.177411417322844 - Y = 2.624671916010506 - WIDTH = 2.399114173228334 - HEIGHT = 6.561679790026243 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face2_3" = EXTERIOR-WALL - POLYGON = "Face2_3 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 90.0 - X = 63.9763779527559 - Y = -32.80839895013123 - Z = -3.552713678800501e-15 - .. -"Rmcd7f84c1Fc649b_bcGlz20" = WINDOW - - X = 6.9717847769028864 - Y = 2.6246719160105054 - WIDTH = 2.4606299212598444 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc649b_bcGlz61" = WINDOW - - X = 20.095144356955377 - Y = 2.6246719160105045 - WIDTH = 2.4606299212598444 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc649b_bcGlz82" = WINDOW - - X = 26.656824146981624 - Y = 2.624671916010504 - WIDTH = 2.4606299212598444 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc649b_bcGlz43" = WINDOW - - X = 13.533464566929132 - Y = 2.624671916010505 - WIDTH = 2.4606299212598426 - HEIGHT = 6.561679790026246 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc649b_bcGlz14" = WINDOW - - X = 3.6909448818897634 - Y = 2.6246719160105054 - WIDTH = 2.460629921259843 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc649b_bcGlz05" = WINDOW - - X = 0.41010498687664054 - Y = 2.624671916010506 - WIDTH = 2.4606299212598426 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc649b_bcGlz36" = WINDOW - - X = 10.252624671916012 - Y = 2.6246719160105054 - WIDTH = 2.460629921259841 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc649b_bcGlz57" = WINDOW - - X = 16.814304461942257 - Y = 2.624671916010505 - WIDTH = 2.460629921259841 - HEIGHT = 6.561679790026246 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc649b_bcGlz78" = WINDOW - - X = 23.3759842519685 - Y = 2.6246719160105045 - WIDTH = 2.460629921259841 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fc649b_bcGlz99" = WINDOW - - X = 29.937664041994747 - Y = 2.624671916010504 - WIDTH = 2.4606299212598337 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face3_4" = EXTERIOR-WALL - POLYGON = "Face3_4 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 0.0 - X = 63.9763779527559 - Y = 0.0 - Z = -3.552713678800501e-15 - .. -"Rmcd7f84c1Fcc78d_8Glz120" = WINDOW - - X = 38.78567913385826 - Y = 2.6246719160105 - WIDTH = 2.399114173228355 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_8Glz181" = WINDOW - - X = 57.978592519685044 - Y = 2.6246719160104988 - WIDTH = 2.399114173228348 - HEIGHT = 6.561679790026258 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_28Glz32" = WINDOW - - X = 9.996309055118104 - Y = 2.6246719160105054 - WIDTH = 2.399114173228348 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_28Glz43" = WINDOW - - X = 13.195127952755904 - Y = 2.624671916010505 - WIDTH = 2.399114173228348 - HEIGHT = 6.561679790026246 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_28Glz94" = WINDOW - - X = 29.189222440944874 - Y = 2.624671916010504 - WIDTH = 2.399114173228348 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_28Glz05" = WINDOW - - X = 0.3998523622047189 - Y = 2.624671916010506 - WIDTH = 2.3991141732283485 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_28Glz26" = WINDOW - - X = 6.797490157480304 - Y = 2.6246719160105054 - WIDTH = 2.399114173228348 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_8Glz107" = WINDOW - - X = 32.38804133858267 - Y = 2.624671916010504 - WIDTH = 2.399114173228348 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_8Glz158" = WINDOW - - X = 48.38213582677165 - Y = 2.624671916010499 - WIDTH = 2.399114173228348 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_8Glz169" = WINDOW - - X = 51.58095472440945 - Y = 2.624671916010499 - WIDTH = 2.399114173228348 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_Glz1710" = WINDOW - - X = 54.77977362204725 - Y = 2.6246719160104988 - WIDTH = 2.399114173228341 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_8Glz611" = WINDOW - - X = 19.592765748031496 - Y = 2.6246719160105045 - WIDTH = 2.399114173228341 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_8Glz512" = WINDOW - - X = 16.393946850393704 - Y = 2.624671916010505 - WIDTH = 2.399114173228341 - HEIGHT = 6.561679790026246 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_8Glz713" = WINDOW - - X = 22.79158464566929 - Y = 2.6246719160105045 - WIDTH = 2.399114173228341 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_8Glz814" = WINDOW - - X = 25.99040354330708 - Y = 2.624671916010504 - WIDTH = 2.399114173228341 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_Glz1115" = WINDOW - - X = 35.58686023622047 - Y = 2.6246719160105 - WIDTH = 2.399114173228341 - HEIGHT = 6.5616797900262505 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_Glz1316" = WINDOW - - X = 41.98449803149607 - Y = 2.6246719160104997 - WIDTH = 2.399114173228341 - HEIGHT = 6.56167979002625 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_Glz1417" = WINDOW - - X = 45.18331692913386 - Y = 2.6246719160104997 - WIDTH = 2.399114173228341 - HEIGHT = 6.56167979002625 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_8Glz118" = WINDOW - - X = 3.5986712598425186 - Y = 2.624671916010506 - WIDTH = 2.3991141732283348 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fcc78d_Glz1919" = WINDOW - - X = 61.177411417322844 - Y = 2.624671916010506 - WIDTH = 2.3991141732283268 - HEIGHT = 6.561679790026243 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Face4_3" = EXTERIOR-WALL - POLYGON = "Face4_3 Plg" - CONSTRUCTION = "TpclInsltdStlFrmdExtrrWllR19_c" - TILT = 90.0 - AZIMUTH = 270.0 - X = 0.0 - Y = 0.0 - Z = -3.552713678800501e-15 - .. -"Rmcd7f84c1Fccbf0f69Glz20" = WINDOW - - X = 6.971784776902883 - Y = 2.6246719160105054 - WIDTH = 2.4606299212598444 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fccbf0f69Glz81" = WINDOW - - X = 26.65682414698162 - Y = 2.624671916010504 - WIDTH = 2.4606299212598444 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fccbf0f69Glz12" = WINDOW - - X = 3.6909448818897594 - Y = 2.6246719160105054 - WIDTH = 2.4606299212598453 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fccbf0f69Glz33" = WINDOW - - X = 10.252624671916006 - Y = 2.6246719160105054 - WIDTH = 2.4606299212598444 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fccbf0f69Glz04" = WINDOW - - X = 0.4101049868766397 - Y = 2.624671916010506 - WIDTH = 2.4606299212598413 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fccbf0f69Glz45" = WINDOW - - X = 13.53346456692913 - Y = 2.624671916010505 - WIDTH = 2.460629921259841 - HEIGHT = 6.561679790026246 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fccbf0f69Glz56" = WINDOW - - X = 16.814304461942253 - Y = 2.624671916010505 - WIDTH = 2.460629921259841 - HEIGHT = 6.561679790026246 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fccbf0f69Glz67" = WINDOW - - X = 20.095144356955373 - Y = 2.6246719160105045 - WIDTH = 2.4606299212598444 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fccbf0f69Glz78" = WINDOW - - X = 23.375984251968497 - Y = 2.6246719160105045 - WIDTH = 2.460629921259841 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - -"Rmcd7f84c1Fccbf0f69Glz99" = WINDOW - - X = 29.937664041994744 - Y = 2.624671916010504 - WIDTH = 2.4606299212598373 - HEIGHT = 6.561679790026247 - GLASS-TYPE = "U 0.36 SHGC 0.38 Smpl Glzng Wndw" - .. - - -"Face6_6" = ROOF - POLYGON = "Face6_6 Plg" - CONSTRUCTION = "Typical IEAD Roof-R32_c" - TILT = 0.0 - AZIMUTH = 180 - X = 0.0 - Y = -32.80839895013123 - Z = 9.842519685039367 - .. - - - - -"Face5_4_ef Plg" = POLYGON - V1 = ( 34.448819, -0.000000 ) - V2 = ( 34.448819, 32.808399 ) - V3 = ( 0.000000, 32.808399 ) - V4 = ( 0.000000, -0.000000 ) - .. -"Face5_4" = INTERIOR-WALL - POLYGON = "Face5_4_ef Plg" - CONSTRUCTION = "Generic Interior Floor_c" - NEXT-TO = "Room3" - TILT = 180.0 - AZIMUTH = 180 - X = 29.527559055118104 - Y = 0.0 - Z = 0.0 - .. - -"Face5_5_ef Plg" = POLYGON - V1 = ( 29.527559, -0.000000 ) - V2 = ( 29.527559, 32.808399 ) - V3 = ( 0.000000, 32.808399 ) - V4 = ( 0.000000, -0.000000 ) - .. -"Face5_5" = INTERIOR-WALL - POLYGON = "Face5_5_ef Plg" - CONSTRUCTION = "Generic Interior Floor_c" - NEXT-TO = "Room2" - TILT = 180.0 - AZIMUTH = 180 - X = 0.0 - Y = 0.0 - Z = 0.0 - .. - - - - - -$ ********************************************************* -$ ** ** -$ ** Electric & Fuel Meters ** -$ ** ** -$ ********************************************************* - - - - -$ --------------------------------------------------------- -$ Electric Meters -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Fuel Meters -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Master Meters -$ --------------------------------------------------------- - - - - -$ ********************************************************* -$ ** ** -$ ** HVAC Circulation Loops / Plant Equipment ** -$ ** ** -$ ********************************************************* - - - - -$ --------------------------------------------------------- -$ Pumps -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Heat Exchangers -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Circulation Loops -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Chillers -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Boilers -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Domestic Water Heaters -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Heat Rejection -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Tower Free Cooling -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Photovoltaic Modules -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Electric Generators -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Thermal Storage -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Ground Loop Heat Exchangers -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Compliance DHW (residential dwelling units) -$ --------------------------------------------------------- - - - - -$ ********************************************************* -$ ** ** -$ ** Steam & Chilled Water Meters ** -$ ** ** -$ ********************************************************* - - - - -$ --------------------------------------------------------- -$ Steam Meters -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Chilled Water Meters -$ --------------------------------------------------------- - - - - -$ ********************************************************* -$ ** ** -$ ** HVAC Systems / Zones ** -$ ** ** -$ ********************************************************* - - - - -"Level_0_Sys (SUM)" = SYSTEM - TYPE = SUM - HEAT-SOURCE = NONE - SYSTEM-REPORTS = NO - .. - -"Room1 Zn" = ZONE - TYPE = UNCONDITIONED - DESIGN-HEAT-T = 72 - DESIGN-COOL-T = 75 - SIZING-OPTION = ADJUST-LOADS - SPACE = "Room1" - .. - -"Level_1_Sys (SUM)" = SYSTEM - TYPE = SUM - HEAT-SOURCE = NONE - SYSTEM-REPORTS = NO - .. - -"Room2 Zn" = ZONE - TYPE = UNCONDITIONED - DESIGN-HEAT-T = 72 - DESIGN-COOL-T = 75 - SIZING-OPTION = ADJUST-LOADS - SPACE = "Room2" - .. - -"Room3 Zn" = ZONE - TYPE = UNCONDITIONED - DESIGN-HEAT-T = 72 - DESIGN-COOL-T = 75 - SIZING-OPTION = ADJUST-LOADS - SPACE = "Room3" - .. - -"Level_2_Sys (SUM)" = SYSTEM - TYPE = SUM - HEAT-SOURCE = NONE - SYSTEM-REPORTS = NO - .. - -"Room4 Zn" = ZONE - TYPE = UNCONDITIONED - DESIGN-HEAT-T = 72 - DESIGN-COOL-T = 75 - SIZING-OPTION = ADJUST-LOADS - SPACE = "Room4" - .. - - -$ ********************************************************* -$ ** ** -$ ** Metering & Misc HVAC ** -$ ** ** -$ ********************************************************* - - - - -$ --------------------------------------------------------- -$ Equipment Controls -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Load Management -$ --------------------------------------------------------- - - - - -$ ********************************************************* -$ ** ** -$ ** Utility Rates ** -$ ** ** -$ ********************************************************* - - - - -$ --------------------------------------------------------- -$ Ratchets -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Block Charges -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Utility Rates -$ --------------------------------------------------------- - - - - -$ ********************************************************* -$ ** ** -$ ** Output Reporting ** -$ ** ** -$ ********************************************************* - - - - -$ --------------------------------------------------------- -$ Loads Non-Hourly Reporting -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Systems Non-Hourly Reporting -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Plant Non-Hourly Reporting -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Economics Non-Hourly Reporting -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ Hourly Reporting -$ --------------------------------------------------------- - - - - -$ --------------------------------------------------------- -$ THE END -$ --------------------------------------------------------- - -END .. -COMPUTE .. -STOP .. diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 9a435a2..e477a98 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -9,8 +9,6 @@ from honeybee.typing import clean_doe2_string from .config import RES_CHARS -# TODO: Implement the keys that Trevor wants: -# FLOW/AREA, ASSIGNED-FLOW, MIN-FLOW-RATIO, MIN-FLOW/AREA, HMAX-FLOW-RATIO # TODO: Add methods to translate daylight sensors # TODO: Add methods to map honeybee_energy process loads to SOURCE-TYPE PROCESS diff --git a/honeybee_doe2/properties/__init__.py b/honeybee_doe2/properties/__init__.py new file mode 100644 index 0000000..3edc67a --- /dev/null +++ b/honeybee_doe2/properties/__init__.py @@ -0,0 +1 @@ +"""honeybee-doe2 properties.""" diff --git a/honeybee_doe2/properties/room.py b/honeybee_doe2/properties/room.py new file mode 100644 index 0000000..b75d2a7 --- /dev/null +++ b/honeybee_doe2/properties/room.py @@ -0,0 +1,188 @@ +# coding=utf-8 +"""Room DOE-2 Properties.""" +from honeybee.typing import float_in_range, float_positive +# ASSIGNED-FLOW, FLOW/AREA, MIN-FLOW-RATIO, MIN-FLOW/AREA, HMAX-FLOW-RATIO + + +class RoomDoe2Properties(object): + """DOE-2 Properties for Honeybee Room. + + Args: + host: A honeybee_core Room object that hosts these properties. + assigned_flow: A number for the design supply air flow rate for the zone + the Room is assigned to (cfm). This establishes the minimum allowed + design air flow. Note that the actual design flow may be larger. If + None, this parameter will not be written into the INP. (Default: None). + flow_per_area: A number for the design supply air flow rate to + the zone per unit floor area (cfm/ft2). If None, this parameter + will not be written into the INP. (Default: None). + min_flow_ratio: A number between 0 and 1 for the minimum allowable zone + air supply flow rate, expressed as a fraction of design flow rate. + Applicable to variable-volume type systems only. If None, this parameter + will not be written into the INP. (Default: None). + min_flow_per_area: A number for the minimum air flow per square foot of + floor area (cfm/ft2). This is an alternative way of specifying the + min_flow_ratio. If None, this parameter will not be written into + the INP. (Default: None). + hmax_flow_ratio: A number between 0 and 1 for the ratio of the maximum + (or fixed) heating airflow to the cooling airflow. The specific + meaning varies according to the type of zone terminal. If None, this + parameter will not be written into the INP. (Default: None). + + Properties: + * host + * assigned_flow + * flow_per_area + * min_flow_ratio + * min_flow_per_area + * hmax_flow_ratio + """ + + __slots__ = ( + '_host', '_assigned_flow', '_flow_per_area', '_min_flow_ratio', + '_min_flow_per_area', '_hmax_flow_ratio' + ) + + def __init__(self, host, assigned_flow=None, flow_per_area=None, min_flow_ratio=None, + min_flow_per_area=None, hmax_flow_ratio=None): + """Initialize Room DOE-2 properties.""" + # set the main properties of the Room + self._host = host + self.assigned_flow = assigned_flow + self.flow_per_area = flow_per_area + self.min_flow_ratio = min_flow_ratio + self.min_flow_per_area = min_flow_per_area + self.hmax_flow_ratio = hmax_flow_ratio + + @property + def host(self): + """Get the Room object hosting these properties.""" + return self._host + + @property + def assigned_flow(self): + """Get or set the design supply air flow rate for the zone (cfm).""" + return self._assigned_flow + + @assigned_flow.setter + def assigned_flow(self, value): + if value is not None: + value = float_positive(value, 'zone assigned flow') + self._assigned_flow = value + + @property + def flow_per_area(self): + """Get or set the design supply air flow rate per unit floor area (cfm/ft2). + """ + return self._flow_per_area + + @flow_per_area.setter + def flow_per_area(self, value): + if value is not None: + value = float_positive(value, 'zone flow per area') + self._flow_per_area = value + + @property + def min_flow_ratio(self): + """Get or set the the min supply airflow rate as a fraction of design flow rate. + """ + return self._min_flow_ratio + + @min_flow_ratio.setter + def min_flow_ratio(self, value): + if value is not None: + value = float_in_range(value, 0.0, 1.0, 'zone min flow ratio') + self._min_flow_ratio = value + + @property + def min_flow_per_area(self): + """Get or set the minimum air flow per square foot of floor area (cfm/ft2).""" + return self._min_flow_per_area + + @min_flow_per_area.setter + def min_flow_per_area(self, value): + if value is not None: + value = float_positive(value, 'zone min flow per area') + self._min_flow_per_area = value + + @property + def hmax_flow_ratio(self): + """Get or set the ratio of the maximum heating airflow to the cooling airflow. + """ + return self._hmax_flow_ratio + + @hmax_flow_ratio.setter + def hmax_flow_ratio(self, value): + if value is not None: + value = float_in_range(value, 0.0, 1.0, 'zone heating max flow ratio') + self._hmax_flow_ratio = value + + @classmethod + def from_dict(cls, data, host): + """Create RoomDoe2Properties from a dictionary. + + Args: + data: A dictionary representation of RoomDoe2Properties with the + format below. + host: A Room object that hosts these properties. + + .. code-block:: python + + { + "type": 'RoomDoe2Properties', + "assigned_flow": 100, # number in cfm + "flow_per_area": 1, # number in cfm/ft2 + "min_flow_ratio": 0.3, # number between 0 and 1 + "min_flow_per_area": 0.3, # number in cfm/ft2 + "hmax_flow_ratio": 0.3 # number between 0 and 1 + } + """ + assert data['type'] == 'RoomDoe2Properties', \ + 'Expected RoomDoe2Properties. Got {}.'.format(data['type']) + new_prop = cls(host) + if 'assigned_flow' in data: + new_prop.assigned_flow = data['assigned_flow'] + if 'flow_per_area' in data: + new_prop.flow_per_area = data['flow_per_area'] + if 'min_flow_ratio' in data: + new_prop.min_flow_ratio = data['min_flow_ratio'] + if 'min_flow_per_area' in data: + new_prop.min_flow_per_area = data['min_flow_per_area'] + if 'hmax_flow_ratio' in data: + new_prop.hmax_flow_ratio = data['hmax_flow_ratio'] + return new_prop + + def to_dict(self): + """Return Room Doe2 properties as a dictionary.""" + base = {'doe2': {}} + base['doe2']['type'] = 'RoomDoe2Properties' + if self.assigned_flow is not None: + base['assigned_flow'] = self.assigned_flow + if self.flow_per_area is not None: + base['flow_per_area'] = self.flow_per_area + if self.min_flow_ratio is not None: + base['min_flow_ratio'] = self.min_flow_ratio + if self.min_flow_per_area is not None: + base['min_flow_per_area'] = self.min_flow_per_area + if self.hmax_flow_ratio is not None: + base['hmax_flow_ratio'] = self.hmax_flow_ratio + return base + + def duplicate(self, new_host=None): + """Get a copy of this object. + + Args: + new_host: A new Room object that hosts these properties. + If None, the properties will be duplicated with the same host. + """ + _host = new_host or self._host + new_room = RoomDoe2Properties( + _host, self.assigned_flow, self.flow_per_area, self.min_flow_ratio, + self.min_flow_per_area, self.hmax_flow_ratio) + return new_room + + def ToString(self): + return self.__repr__() + + def __repr__(self): + return 'Room DOE2 Properties: [host: {}]'.format(self.host.display_name) From 4101ae0f87df25cb97b22a8377114bb449d95b44 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Thu, 2 May 2024 17:51:57 -0700 Subject: [PATCH 19/27] fix(writer): Improve the check for when we forego POLYGON --- honeybee_doe2/writer.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index da7adde..421e8eb 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -534,26 +534,31 @@ def _is_room_3d_extruded(hb_room): r_geo = room.horizontal_boundary(match_walls=True, tolerance=DOE2_TOLERANCE) r_geo = r_geo if r_geo.normal.z >= 0 else r_geo.flip() r_geo = r_geo.remove_duplicate_vertices(DOE2_TOLERANCE) - wall_count = len([orient for orient in face_orientations if orient == 0]) - if len(r_geo) == wall_count: # all walls can be represented with room vertices - rm_pts = r_geo.lower_left_counter_clockwise_boundary - ceil_count = len([orient for orient in face_orientations if orient == 1]) - floor_count = len([orient for orient in face_orientations if orient == -1]) - for face, orient in zip(room.faces, face_orientations): - if orient == 0: # wall to associate with a room vertex + rm_pts = r_geo.lower_left_counter_clockwise_boundary + rm_height = room.max.z - room.min.z + ceil_count = len([orient for orient in face_orientations if orient == 1]) + floor_count = len([orient for orient in face_orientations if orient == -1]) + for face, orient in zip(room.faces, face_orientations): + if orient == 0: # wall to associate with a room vertex + clean_geo = face.geometry.remove_colinear_vertices(DOE2_TOLERANCE) + face_height = face.max.z - face.min.z + if clean_geo.boundary_polygon2d.is_rectangle(DOE2_ANGLE_TOL) and \ + abs(rm_height - face_height) <= DOE2_TOLERANCE: f_origin = face.geometry.lower_left_corner for i, r_pt in enumerate(rm_pts): if f_origin.is_equivalent(r_pt, DOE2_TOLERANCE): face_locations.append('SPACE-V{}'.format(i + 1)) break - else: + else: # not associated with any Room vertex face_locations.append(None) - elif orient == 1: - loc = 'TOP' if ceil_count == 1 else None - face_locations.append(loc) - else: - loc = 'BOTTOM' if floor_count == 1 else None - face_locations.append(loc) + else: # not a rectangular geometry + face_locations.append(None) + elif orient == 1: + loc = 'TOP' if ceil_count == 1 else None + face_locations.append(loc) + else: + loc = 'BOTTOM' if floor_count == 1 else None + face_locations.append(loc) # if the room is not extruded, just use the generic horizontal boundary if len(face_locations) == 0: From a3f15e3b5b18a299c96b1eef718e7ddacec25167 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Fri, 3 May 2024 16:56:24 -0700 Subject: [PATCH 20/27] feat(room): Finish adding RoomDoe2Properties to assign HVAC parameters --- honeybee_doe2/_extend_honeybee.py | 25 +++++ honeybee_doe2/properties/model.py | 52 ++++++++++ honeybee_doe2/properties/room.py | 97 ++++++++++++++++--- honeybee_doe2/writer.py | 6 ++ tests/model_extend_test.py | 35 +++++++ tests/room_extend_test.py | 155 ++++++++++++++++++++++++++++++ 6 files changed, 357 insertions(+), 13 deletions(-) create mode 100644 honeybee_doe2/properties/model.py create mode 100644 tests/model_extend_test.py create mode 100644 tests/room_extend_test.py diff --git a/honeybee_doe2/_extend_honeybee.py b/honeybee_doe2/_extend_honeybee.py index a91da90..471f359 100644 --- a/honeybee_doe2/_extend_honeybee.py +++ b/honeybee_doe2/_extend_honeybee.py @@ -1,5 +1,6 @@ # coding=utf-8 # import all of the modules for writing geometry to INP +from honeybee.properties import ModelProperties, RoomProperties import honeybee.writer.shademesh as shade_mesh_writer import honeybee.writer.door as door_writer import honeybee.writer.aperture as aperture_writer @@ -7,9 +8,33 @@ import honeybee.writer.face as face_writer import honeybee.writer.room as room_writer import honeybee.writer.model as model_writer + +from .properties.model import ModelDoe2Properties +from .properties.room import RoomDoe2Properties from .writer import model_to_inp, room_to_inp, face_to_inp, shade_to_inp, \ aperture_to_inp, door_to_inp, shade_mesh_to_inp +# set a hidden doe2 attribute on each core geometry Property class to None +# define methods to produce doe2 property instances on each Property instance +ModelProperties._doe2 = None +RoomProperties._doe2 = None + +def model_doe2_properties(self): + if self._doe2 is None: + self._doe2 = ModelDoe2Properties(self.host) + return self._doe2 + + +def room_doe2_properties(self): + if self._doe2 is None: + self._doe2 = RoomDoe2Properties(self.host) + return self._doe2 + +# add doe2 property methods to the Properties classes +ModelProperties.doe2 = property(model_doe2_properties) +RoomProperties.doe2 = property(room_doe2_properties) + + # add writers to the honeybee-core modules model_writer.inp = model_to_inp room_writer.inp = room_to_inp diff --git a/honeybee_doe2/properties/model.py b/honeybee_doe2/properties/model.py new file mode 100644 index 0000000..39acc77 --- /dev/null +++ b/honeybee_doe2/properties/model.py @@ -0,0 +1,52 @@ +# coding=utf-8 +"""Model DOE-2 Properties.""" + + +class ModelDoe2Properties(object): + """DOE-2 Properties for Honeybee Model. + + Args: + host: A honeybee_core Model object that hosts these properties. + + Properties: + * host + """ + + def __init__(self, host): + """Initialize ModelDoe2Properties.""" + self._host = host + + @property + def host(self): + """Get the Model object hosting these properties.""" + return self._host + + def to_dict(self): + """Return Model DOE-2 properties as a dictionary.""" + return {'doe2': {'type': 'ModelDoe2Properties'}} + + def apply_properties_from_dict(self, data): + """Apply the energy properties of a dictionary to the host Model of this object. + + Args: + data: A dictionary representation of an entire honeybee-core Model. + Note that this dictionary must have ModelEnergyProperties in order + for this method to successfully apply the energy properties. + """ + assert 'doe2' in data['properties'], \ + 'Dictionary possesses no ModelDoe2Properties.' + room_doe2_dicts = [] + for room_dict in data['rooms']: + try: + room_doe2_dicts.append(room_dict['properties']['doe2']) + except KeyError: + room_doe2_dicts.append(None) + for room, r_dict in zip(self.host.rooms, room_doe2_dicts): + if r_dict is not None: + room.properties.doe2.apply_properties_from_dict(r_dict) + + def ToString(self): + return self.__repr__() + + def __repr__(self): + return 'Model DOE2 Properties: [host: {}]'.format(self.host.display_name) diff --git a/honeybee_doe2/properties/room.py b/honeybee_doe2/properties/room.py index b75d2a7..e40849e 100644 --- a/honeybee_doe2/properties/room.py +++ b/honeybee_doe2/properties/room.py @@ -1,7 +1,7 @@ # coding=utf-8 """Room DOE-2 Properties.""" from honeybee.typing import float_in_range, float_positive -# ASSIGNED-FLOW, FLOW/AREA, MIN-FLOW-RATIO, MIN-FLOW/AREA, HMAX-FLOW-RATIO +from honeybee.altnumber import autocalculate class RoomDoe2Properties(object): @@ -37,11 +37,12 @@ class RoomDoe2Properties(object): * min_flow_per_area * hmax_flow_ratio """ - __slots__ = ( '_host', '_assigned_flow', '_flow_per_area', '_min_flow_ratio', '_min_flow_per_area', '_hmax_flow_ratio' ) + INP_ATTR = ('ASSIGNED-FLOW', 'FLOW/AREA', 'MIN-FLOW-RATIO', + 'MIN-FLOW/AREA', 'HMAX-FLOW-RATIO') def __init__(self, host, assigned_flow=None, flow_per_area=None, min_flow_ratio=None, min_flow_per_area=None, hmax_flow_ratio=None): @@ -140,34 +141,104 @@ def from_dict(cls, data, host): assert data['type'] == 'RoomDoe2Properties', \ 'Expected RoomDoe2Properties. Got {}.'.format(data['type']) new_prop = cls(host) - if 'assigned_flow' in data: + auto_dict = autocalculate.to_dict() + if 'assigned_flow' in data and data['assigned_flow'] != auto_dict: new_prop.assigned_flow = data['assigned_flow'] - if 'flow_per_area' in data: + if 'flow_per_area' in data and data['flow_per_area'] != auto_dict: new_prop.flow_per_area = data['flow_per_area'] - if 'min_flow_ratio' in data: + if 'min_flow_ratio' in data and data['min_flow_ratio'] != auto_dict: new_prop.min_flow_ratio = data['min_flow_ratio'] - if 'min_flow_per_area' in data: + if 'min_flow_per_area' in data and data['min_flow_per_area'] != auto_dict: new_prop.min_flow_per_area = data['min_flow_per_area'] - if 'hmax_flow_ratio' in data: + if 'hmax_flow_ratio' in data and data['hmax_flow_ratio'] != auto_dict: new_prop.hmax_flow_ratio = data['hmax_flow_ratio'] return new_prop - def to_dict(self): + def apply_properties_from_dict(self, data): + """Apply properties from a RoomDoe2Properties dictionary. + + Args: + data: A RoomDoe2Properties dictionary (typically coming from a Model). + """ + auto_dict = autocalculate.to_dict() + if 'assigned_flow' in data and data['assigned_flow'] != auto_dict: + self.assigned_flow = data['assigned_flow'] + if 'flow_per_area' in data and data['flow_per_area'] != auto_dict: + self.flow_per_area = data['flow_per_area'] + if 'min_flow_ratio' in data and data['min_flow_ratio'] != auto_dict: + self.min_flow_ratio = data['min_flow_ratio'] + if 'min_flow_per_area' in data and data['min_flow_per_area'] != auto_dict: + self.min_flow_per_area = data['min_flow_per_area'] + if 'hmax_flow_ratio' in data and data['hmax_flow_ratio'] != auto_dict: + self.hmax_flow_ratio = data['hmax_flow_ratio'] + + def apply_properties_from_user_data(self): + """Apply properties from a the user_data assigned to the host room. + + For this method to successfully assign properties from user_data, the + properties on this object must currently be None and the keys in + user_data must use the INP convention for each of the attributes, + which must be CAPITALIZED like the following: + + .. code-block:: python + + { + "ASSIGNED-FLOW": 100, # number in cfm + "FLOW/AREA": 1, # number in cfm/ft2 + "MIN-FLOW-RATIO": 0.3, # number between 0 and 1 + "MIN-FLOW/AREA": 0.3, # number in cfm/ft2 + "HMAX-FLOW-RATIO": 0.3 # number between 0 and 1 + } + """ + attrs = ('assigned_flow', 'flow_per_area', 'min_flow_ratio', + 'min_flow_per_area', 'hmax_flow_ratio') + data = self.host.user_data + if data is not None: + for key, attr in zip(self.INP_ATTR, attrs): + if key in data and getattr(self, attr) is None: + try: + setattr(self, attr, data[key]) + except Exception: + pass # it's user_data; users are allowed to make mistakes + + def to_dict(self, abridged=False): """Return Room Doe2 properties as a dictionary.""" base = {'doe2': {}} base['doe2']['type'] = 'RoomDoe2Properties' if self.assigned_flow is not None: - base['assigned_flow'] = self.assigned_flow + base['doe2']['assigned_flow'] = self.assigned_flow if self.flow_per_area is not None: - base['flow_per_area'] = self.flow_per_area + base['doe2']['flow_per_area'] = self.flow_per_area if self.min_flow_ratio is not None: - base['min_flow_ratio'] = self.min_flow_ratio + base['doe2']['min_flow_ratio'] = self.min_flow_ratio if self.min_flow_per_area is not None: - base['min_flow_per_area'] = self.min_flow_per_area + base['doe2']['min_flow_per_area'] = self.min_flow_per_area if self.hmax_flow_ratio is not None: - base['hmax_flow_ratio'] = self.hmax_flow_ratio + base['doe2']['hmax_flow_ratio'] = self.hmax_flow_ratio return base + def to_inp(self): + """Get RoomDoe2Properties as INP (Keywords, Values). + + Returns: + A tuple with two elements. + + - keywords: A list of text strings for keywords to assign to the room. + + - values: A list of text strings that aligns with the keywords and + denotes the value for each keyword. + """ + keywords = [] + values = [] + attrs = ('assigned_flow', 'flow_per_area', 'min_flow_ratio', + 'min_flow_per_area', 'hmax_flow_ratio') + for key, attr in zip(self.INP_ATTR, attrs): + attr_value = getattr(self, attr) + if attr_value is not None: + keywords.append(key) + values.append(attr_value) + return keywords, values + def duplicate(self, new_host=None): """Get a copy of this object. diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 421e8eb..c41907c 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -688,6 +688,9 @@ def model_to_inp( ) # reset identifiers to make them unique and derived from the display names model.reset_ids() + # assign attributes from user_data + for room in model.rooms: + room.properties.doe2.apply_properties_from_user_data() # write the simulation parameters into the string model_str = ['INPUT ..\n\n'] @@ -848,6 +851,9 @@ def model_to_inp( vt_kwd, vt_val = ventilation_to_inp(room.properties.energy.ventilation) zone_keys.extend(vt_kwd) zone_vals.extend(vt_val) + hvac_kwd, hvac_val = room.properties.doe2.to_inp() + zone_keys.extend(hvac_kwd) + zone_vals.extend(hvac_val) zone_def = generate_inp_string(zone_name, 'ZONE', zone_keys, zone_vals) model_str.append(zone_def) diff --git a/tests/model_extend_test.py b/tests/model_extend_test.py new file mode 100644 index 0000000..2cba164 --- /dev/null +++ b/tests/model_extend_test.py @@ -0,0 +1,35 @@ +"""Tests the features that honeybee_doe2 adds to honeybee_core Model.""" +from honeybee.room import Room +from honeybee.model import Model +from honeybee_energy.lib.programtypes import office_program + + +def test_from_dict(): + """Test the Room from_dict method with doe2 properties.""" + room = Room.from_box('ShoeBox', 5, 10, 3) + room.properties.energy.program_type = office_program + room.properties.energy.add_default_ideal_air() + south_face = room[3] + south_face.apertures_by_ratio(0.4, 0.01) + south_face.apertures[0].overhang(0.5, indoor=False) + south_face.apertures[0].overhang(0.5, indoor=True) + + room.properties.doe2.assigned_flow = 100 + room.properties.doe2.flow_per_area = 1 + room.properties.doe2.min_flow_ratio = 0.3 + room.properties.doe2.min_flow_per_area = 0.35 + room.properties.doe2.hmax_flow_ratio = 0.5 + + model = Model('Tiny_House', [room]) + + + model_dict = model.to_dict() + new_model = Model.from_dict(model_dict) + assert new_model.to_dict() == model_dict + + new_room = new_model.rooms[0] + assert new_room.properties.doe2.assigned_flow == 100 + assert new_room.properties.doe2.flow_per_area == 1 + assert new_room.properties.doe2.min_flow_ratio == 0.3 + assert new_room.properties.doe2.min_flow_per_area == 0.35 + assert new_room.properties.doe2.hmax_flow_ratio == 0.5 diff --git a/tests/room_extend_test.py b/tests/room_extend_test.py new file mode 100644 index 0000000..3a39379 --- /dev/null +++ b/tests/room_extend_test.py @@ -0,0 +1,155 @@ +"""Tests the features that honeybee_doe2 adds to honeybee_core Room.""" +from ladybug_geometry.geometry3d.pointvector import Point3D +from honeybee.room import Room +from honeybee_energy.lib.programtypes import office_program + +from honeybee_doe2.properties.room import RoomDoe2Properties + + +def test_doe2_properties(): + """Test the existence of the Room energy properties.""" + room = Room.from_box('ShoeBox', 5, 10, 3, 90, Point3D(0, 0, 3)) + room.properties.energy.program_type = office_program + room.properties.energy.add_default_ideal_air() + + assert hasattr(room.properties, 'doe2') + assert isinstance(room.properties.doe2, RoomDoe2Properties) + + assert room.properties.doe2.assigned_flow is None + room.properties.doe2.assigned_flow = 100 + assert room.properties.doe2.assigned_flow == 100 + + assert room.properties.doe2.flow_per_area is None + room.properties.doe2.flow_per_area = 1 + assert room.properties.doe2.flow_per_area == 1 + + assert room.properties.doe2.min_flow_ratio is None + room.properties.doe2.min_flow_ratio = 0.3 + assert room.properties.doe2.min_flow_ratio == 0.3 + + assert room.properties.doe2.min_flow_per_area is None + room.properties.doe2.min_flow_per_area = 0.35 + assert room.properties.doe2.min_flow_per_area == 0.35 + + assert room.properties.doe2.hmax_flow_ratio is None + room.properties.doe2.hmax_flow_ratio = 0.5 + assert room.properties.doe2.hmax_flow_ratio == 0.5 + + +def test_duplicate(): + """Test what happens to doe2 properties when duplicating a Room.""" + room_original = Room.from_box('ShoeBox', 5, 10, 3) + room_original.properties.energy.program_type = office_program + room_original.properties.energy.add_default_ideal_air() + + room_dup_1 = room_original.duplicate() + + assert room_original.properties.doe2.assigned_flow == \ + room_dup_1.properties.doe2.assigned_flow + assert room_original.properties.doe2.flow_per_area == \ + room_dup_1.properties.doe2.flow_per_area + assert room_original.properties.doe2.min_flow_ratio == \ + room_dup_1.properties.doe2.min_flow_ratio + assert room_original.properties.doe2.min_flow_per_area == \ + room_dup_1.properties.doe2.min_flow_per_area + assert room_original.properties.doe2.hmax_flow_ratio == \ + room_dup_1.properties.doe2.hmax_flow_ratio + + room_original.properties.doe2.assigned_flow = 100 + room_original.properties.doe2.flow_per_area = 1 + room_original.properties.doe2.min_flow_ratio = 0.3 + room_original.properties.doe2.min_flow_per_area = 0.35 + room_original.properties.doe2.hmax_flow_ratio = 0.5 + + assert room_original.properties.doe2.assigned_flow != \ + room_dup_1.properties.doe2.assigned_flow + assert room_original.properties.doe2.flow_per_area != \ + room_dup_1.properties.doe2.flow_per_area + assert room_original.properties.doe2.min_flow_ratio != \ + room_dup_1.properties.doe2.min_flow_ratio + assert room_original.properties.doe2.min_flow_per_area != \ + room_dup_1.properties.doe2.min_flow_per_area + assert room_original.properties.doe2.hmax_flow_ratio != \ + room_dup_1.properties.doe2.hmax_flow_ratio + + +def test_to_dict(): + """Test the Room to_dict method with doe2 properties.""" + room = Room.from_box('ShoeBox', 5, 10, 3) + room.properties.energy.program_type = office_program + room.properties.energy.add_default_ideal_air() + room.properties.doe2.assigned_flow = 100 + room.properties.doe2.flow_per_area = 1 + room.properties.doe2.min_flow_ratio = 0.3 + room.properties.doe2.min_flow_per_area = 0.35 + room.properties.doe2.hmax_flow_ratio = 0.5 + + rd = room.to_dict() + assert 'properties' in rd + assert rd['properties']['type'] == 'RoomProperties' + assert 'doe2' in rd['properties'] + assert rd['properties']['doe2']['type'] == 'RoomDoe2Properties' + assert rd['properties']['doe2']['assigned_flow'] == 100 + assert rd['properties']['doe2']['flow_per_area'] == 1 + assert rd['properties']['doe2']['min_flow_ratio'] == 0.3 + assert rd['properties']['doe2']['min_flow_per_area'] == 0.35 + assert rd['properties']['doe2']['hmax_flow_ratio'] == 0.5 + + +def test_from_dict(): + """Test the Room from_dict method with doe2 properties.""" + room = Room.from_box('ShoeBox', 5, 10, 3) + room.properties.energy.program_type = office_program + room.properties.energy.add_default_ideal_air() + room.properties.doe2.assigned_flow = 100 + room.properties.doe2.flow_per_area = 1 + room.properties.doe2.min_flow_ratio = 0.3 + room.properties.doe2.min_flow_per_area = 0.35 + room.properties.doe2.hmax_flow_ratio = 0.5 + + rd = room.to_dict() + new_room = Room.from_dict(rd) + assert new_room.to_dict() == rd + + + +def test_to_inp(): + """Test the Room to_inp method with doe2 properties.""" + room = Room.from_box('ShoeBox', 5, 10, 3) + room.properties.energy.program_type = office_program + room.properties.energy.add_default_ideal_air() + + hvac_kwd, hvac_val = room.properties.doe2.to_inp() + assert hvac_kwd == [] + assert hvac_val == [] + + room.properties.doe2.assigned_flow = 100.0 + room.properties.doe2.flow_per_area = 1.0 + room.properties.doe2.min_flow_ratio = 0.3 + room.properties.doe2.min_flow_per_area = 0.35 + room.properties.doe2.hmax_flow_ratio = 0.5 + hvac_kwd, hvac_val = room.properties.doe2.to_inp() + assert hvac_kwd == ['ASSIGNED-FLOW', 'FLOW/AREA', 'MIN-FLOW-RATIO', + 'MIN-FLOW/AREA', 'HMAX-FLOW-RATIO'] + assert hvac_val == [100.0, 1.0, 0.3, 0.35, 0.5] + + +def test_apply_properties_from_user_data(): + """Test the Room apply_properties_from_user_data method with doe2 properties.""" + room = Room.from_box('ShoeBox', 5, 10, 3) + room.properties.energy.program_type = office_program + room.properties.energy.add_default_ideal_air() + room.user_data = { + "ASSIGNED-FLOW": 100.0, + "FLOW/AREA": 1.0, + "MIN-FLOW-RATIO": 0.3, + "MIN-FLOW/AREA": 0.35, + "HMAX-FLOW-RATIO": 0.5 + } + + room.properties.doe2.apply_properties_from_user_data() + assert room.properties.doe2.assigned_flow == 100 + assert room.properties.doe2.flow_per_area == 1 + assert room.properties.doe2.min_flow_ratio == 0.3 + assert room.properties.doe2.min_flow_per_area == 0.35 + assert room.properties.doe2.hmax_flow_ratio == 0.5 From 2ba92e55e5f201bb3c5ef893ec016c7938f0f66c Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 7 May 2024 07:32:05 -0700 Subject: [PATCH 21/27] feat(programtype): Support for translating programs to switch statements --- honeybee_doe2/load.py | 43 ++++++---- honeybee_doe2/programtype.py | 159 +++++++++++++++++++++++++++++++++++ honeybee_doe2/writer.py | 31 +++++-- tests/writer_test.py | 38 ++++++++- 4 files changed, 243 insertions(+), 28 deletions(-) create mode 100644 honeybee_doe2/programtype.py diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index e477a98..8b63c15 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -9,6 +9,25 @@ from honeybee.typing import clean_doe2_string from .config import RES_CHARS + +# list of all keywords associated with different load types +PEOPLE_KEYS = ('AREA/PERSON', 'PEOPLE-SCHEDULE') +LIGHTING_KEYS = ('LIGHTING-W/AREA', 'LIGHTING-SCHEDULE', 'LIGHT-TO-RETURN') +EQUIP_KEYS = ('EQUIPMENT-W/AREA', 'EQUIP-SCHEDULE', + 'EQUIP-SENSIBLE', 'EQUIP-LATENT', 'EQUIP-RAD-FRAC') +HOT_WATER_KEYS = ('SOURCE-TYPE', 'SOURCE-POWER', 'SOURCE-SCHEDULE', + 'SOURCE-SENSIBLE', 'SOURCE-RAD-FRAC', 'SOURCE-LATENT') +INFILTRATION_KEYS = ('INF-METHOD', 'INF-FLOW/AREA', 'INF-SCHEDULE') +SETPOINT_KEYS = ('DESIGN-HEAT-T', 'DESIGN-COOL-T', 'HEAT-TEMP-SCH', 'COOL-TEMP-SCH') +VENTILATION_KEYS = ('OA-FLOW/PER', 'OA-FLOW/AREA', 'OA-CHANGES', 'OUTSIDE-AIR-FLOW', + 'MIN-FLOW-SCH') +SPACE_KEYS = PEOPLE_KEYS + LIGHTING_KEYS + EQUIP_KEYS + INFILTRATION_KEYS +ZONE_KEYS = SETPOINT_KEYS + VENTILATION_KEYS +SCHEDULE_KEYS = ( + 'PEOPLE-SCHEDULE', 'LIGHTING-SCHEDULE', 'EQUIP-SCHEDULE', 'SOURCE-SCHEDULE', + 'INF-SCHEDULE', 'HEAT-TEMP-SCH', 'COOL-TEMP-SCH', 'MIN-FLOW-SCH') + + # TODO: Add methods to translate daylight sensors # TODO: Add methods to map honeybee_energy process loads to SOURCE-TYPE PROCESS @@ -34,9 +53,7 @@ def people_to_inp(people): ppl_den = round(ppl_den, 3) ppl_sch = clean_doe2_string(people.occupancy_schedule.identifier, RES_CHARS) ppl_sch = '"{}"'.format(ppl_sch) - keywords = ('AREA/PERSON', 'PEOPLE-SCHEDULE') - values = (ppl_den, ppl_sch) - return keywords, values + return PEOPLE_KEYS, (ppl_den, ppl_sch) def lighting_to_inp(lighting): @@ -60,9 +77,7 @@ def lighting_to_inp(lighting): lpd = round(lpd, 3) lgt_sch = clean_doe2_string(lighting.schedule.identifier, RES_CHARS) lgt_sch = '"{}"'.format(lgt_sch) - keywords = ('LIGHTING-W/AREA', 'LIGHTING-SCHEDULE', 'LIGHT-TO-RETURN') - values = (lpd, lgt_sch, lighting.return_air_fraction) - return keywords, values + return LIGHTING_KEYS, (lpd, lgt_sch, lighting.return_air_fraction) def equipment_to_inp(electric_equip, gas_equip=None): @@ -111,9 +126,7 @@ def equipment_to_inp(electric_equip, gas_equip=None): else: # no equipment assigned return (), () - keywords = ('EQUIPMENT-W/AREA', 'EQUIP-SCHEDULE', - 'EQUIP-SENSIBLE', 'EQUIP-LATENT', 'EQUIP-RAD-FRAC') - return keywords, values + return EQUIP_KEYS, values def hot_water_to_inp(hot_water, room_floor_area): @@ -146,11 +159,9 @@ def hot_water_to_inp(hot_water, room_floor_area): shw_power = round(Power().to_unit([shw_heat], 'Btu/h', 'W')[0], 3) shw_sch = clean_doe2_string(hot_water.schedule.identifier, RES_CHARS) shw_sch = '"{}"'.format(shw_sch) - keywords = ('SOURCE-TYPE', 'SOURCE-POWER', 'SOURCE-SCHEDULE', - 'SOURCE-SENSIBLE', 'SOURCE-RAD-FRAC', 'SOURCE-LATENT') values = ('HOT-WATER', shw_power, shw_sch, hot_water.sensible_fraction, 0, hot_water.latent_fraction) - return keywords, values + return HOT_WATER_KEYS, values def infiltration_to_inp(infiltration): @@ -175,9 +186,7 @@ def infiltration_to_inp(infiltration): inf_den = round(inf_den, 3) inf_sch = clean_doe2_string(infiltration.schedule.identifier, RES_CHARS) inf_sch = '"{}"'.format(inf_sch) - keywords = ('INF-METHOD', 'INF-FLOW/AREA', 'INF-SCHEDULE') - values = ('AIR-CHANGE', inf_den, inf_sch) - return keywords, values + return INFILTRATION_KEYS, ('AIR-CHANGE', inf_den, inf_sch) def setpoint_to_inp(setpoint): @@ -203,9 +212,7 @@ def setpoint_to_inp(setpoint): heat_sch = '"{}"'.format(heat_sch) cool_sch = clean_doe2_string(setpoint.cooling_schedule.identifier, RES_CHARS) cool_sch = '"{}"'.format(cool_sch) - keywords = ('DESIGN-HEAT-T', 'DESIGN-COOL-T', 'HEAT-TEMP-SCH', 'COOL-TEMP-SCH') - values = (heat_setpt, cool_setpt, heat_sch, cool_sch) - return keywords, values + return SETPOINT_KEYS, (heat_setpt, cool_setpt, heat_sch, cool_sch) def ventilation_to_inp(ventilation): diff --git a/honeybee_doe2/programtype.py b/honeybee_doe2/programtype.py new file mode 100644 index 0000000..e00f075 --- /dev/null +++ b/honeybee_doe2/programtype.py @@ -0,0 +1,159 @@ +"""honeybee-doe2 program type translators.""" +from __future__ import division + +from honeybee.typing import clean_doe2_string + +from .config import RES_CHARS +from .load import people_to_inp, lighting_to_inp, equipment_to_inp, \ + infiltration_to_inp, setpoint_to_inp, ventilation_to_inp, \ + SPACE_KEYS, ZONE_KEYS, SCHEDULE_KEYS +SCH_KEY_SET = set(SCHEDULE_KEYS) + + +def program_type_to_inp(program_type, switch_dict=None): + """Translate a ProgramType into a dictionary used to write INP switch statements. + + Args: + program_type: A honeybee-energy ProgramType definition. + switch_dict: An optional dictionary with INP keywords as keys (such as + PEOPLE-SCHEDULE or AREA/PERSON or LIGHTING-W/AREA). The values of the + dictionary should be lists of switch statement text strings, such as + 'case "conf": #SI("Small Off Occ", "SPACE", "PEOPLE-SCHEDULE")'. + Specifying an input dictionary here can be used to build up switch + statements for all program types across a model. + + Returns: + An dictionary with INP keywords as keys (such as PEOPLE-SCHEDULE or AREA/PERSON). + The values of the dictionary are lists of switch statement text strings, + such as 'case "conf": #SI("Small Off Occ", "SPACE", "PEOPLE-SCHEDULE")'. + """ + # set up the switch statement dictionary to be filled + switch_dict = switch_dict if switch_dict is not None else {} + prog_uid = clean_doe2_string(program_type.identifier, RES_CHARS) + prog_uid = prog_uid.replace(' ', '_') + base_switch = ' case "{}": '.format(prog_uid) + + def _format_schedule(sch_key, sch_uid, obj_type='SPACE'): + """Format schedules in the way they are written into switch statements.""" + return '{}#SI({}, "{}", "{}")'.format(base_switch, sch_uid, obj_type, sch_key) + + def _add_to_switch_dict(keyword, value): + """Add a key: value pair to the switch dictionary with a check.""" + try: + switch_dict[keyword].append(value) + except KeyError: # the first time this key was encountered in the dict + switch_dict[keyword] = [value] + + # write the people into the dictionary + ppl_kwd, ppl_val = people_to_inp(program_type.people) + for key, val in zip(ppl_kwd, ppl_val): + if key in SCH_KEY_SET: + _add_to_switch_dict(key, _format_schedule(key, val, 'SPACE')) + else: + _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) + + + # write the lighting into the dictionary + lgt_kwd, lgt_val = lighting_to_inp(program_type.lighting) + for key, val in zip(lgt_kwd, lgt_val): + if key in SCH_KEY_SET: + _add_to_switch_dict(key, _format_schedule(key, val, 'SPACE')) + else: + _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) + + # write the equipment into the dictionary + eq_kwd, eq_val = equipment_to_inp(program_type.electric_equipment, + program_type.gas_equipment) + for key, val in zip(eq_kwd, eq_val): + if key in SCH_KEY_SET: + _add_to_switch_dict(key, _format_schedule(key, val, 'SPACE')) + else: + _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) + + # write the infiltration into the dictionary + inf_kwd, inf_val = infiltration_to_inp(program_type.infiltration) + for key, val in zip(inf_kwd, inf_val): + if key in SCH_KEY_SET: + _add_to_switch_dict(key, _format_schedule(key, val, 'SPACE')) + else: + _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) + + # write the setpoint into the dictionary + stp_kwd, stp_val = setpoint_to_inp(program_type.setpoint) + for key, val in zip(stp_kwd, stp_val): + if key in SCH_KEY_SET: + _add_to_switch_dict(key, _format_schedule(key, val, 'ZONE')) + else: + _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) + + # write the ventilation into the dictionary + vt_kwd, vt_val = ventilation_to_inp(program_type.ventilation) + for key, val in zip(vt_kwd, vt_val): + if key in SCH_KEY_SET: + _add_to_switch_dict(key, _format_schedule(key, val, 'ZONE')) + else: + _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) + + return switch_dict + + +def switch_dict_to_space_inp(switch_dict): + """Translate a switch statement dictionary into INP strings for the SPACE. + + Args: + switch_dict: An dictionary with INP keywords as keys (such as + PEOPLE-SCHEDULE or AREA/PERSON or LIGHTING-W/AREA). The values of the + dictionary should be lists of switch statement text strings, such as + 'case "conf": #SI("Small Off Occ", "SPACE", "PEOPLE-SCHEDULE")'. + + Returns: + A text string to be written into an INP file. This should go at the top + of the description of Floors / Spaces. + """ + # loop through the space keys and build a list of all switch statement keys + all_switch_strs = [] + for s_key in SPACE_KEYS: + try: + switch_progs = switch_dict[s_key] + switch_strs = ['SET-DEFAULT FOR SPACE'] + switch_strs.append(' {} ='.format(s_key)) + switch_strs.append(' {switch(#L("C-ACTIVITY-DESC"))') + switch_strs.extend(switch_progs) + switch_strs.append(' default: no_default') + switch_strs.append(' endswitch}') + switch_strs.append(' ..\n') + all_switch_strs.append('\n'.join(switch_strs)) + except KeyError: + pass # none of the programs types have this space key + return '\n'.join(all_switch_strs) + + +def switch_dict_to_zone_inp(switch_dict): + """Translate a switch statement dictionary into INP strings for the ZONE. + + Args: + switch_dict: An dictionary with INP keywords as keys (such as + HEAT-TEMP-SCH or DESIGN-HEAT-T or OUTSIDE-AIR-FLOW). The values of the + dictionary should be lists of switch statement text strings, such as + 'case "conf": #SI("Small Off HtgStp", "SPACE", "HEAT-TEMP-SCH")'. + + Returns: + A text string to be written into an INP file. This should go at the top + of the description of HVAC Systems / Zones. + """ + # loop through the space keys and build a list of all switch statement keys + all_switch_strs = [] + for s_key in ZONE_KEYS: + try: + switch_progs = switch_dict[s_key] + switch_strs = ['SET-DEFAULT FOR ZONE', ' TYPE = CONDITIONED'] + switch_strs.append(' {} ='.format(s_key)) + switch_strs.append(' {switch(#LR("SPACE", "C-ACTIVITY-DESC"))') + switch_strs.extend(switch_progs) + switch_strs.append(' default: no_default') + switch_strs.append(' endswitch}') + switch_strs.append(' ..\n') + all_switch_strs.append('\n'.join(switch_strs)) + except KeyError: + pass # none of the programs types have this space key + return '\n'.join(all_switch_strs) diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index c41907c..ac1f509 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -23,6 +23,8 @@ from .schedule import energy_trans_sch_to_transmittance from .load import people_to_inp, lighting_to_inp, equipment_to_inp, hot_water_to_inp, \ infiltration_to_inp, setpoint_to_inp, ventilation_to_inp +from .programtype import program_type_to_inp, switch_dict_to_space_inp, \ + switch_dict_to_zone_inp from .simulation import SimulationPar @@ -460,17 +462,23 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals # set up attributes based on the Room's energy properties energy_attr_keywords = ['ZONE-TYPE'] energy_attr_values = [room_doe2_conditioning_type(room)] + if room.properties.energy._program_type is not None: + energy_attr_keywords.append('C-ACTIVITY-DESC') + prog_uid = clean_doe2_string( + room.properties.energy.program_type.identifier, RES_CHARS) + prog_uid = prog_uid.replace(' ', '_') + energy_attr_values.append('*{}*'.format(prog_uid)) # people - ppl_kwd, ppl_val = people_to_inp(room.properties.energy.people) + ppl_kwd, ppl_val = people_to_inp(room.properties.energy._people) energy_attr_keywords.extend(ppl_kwd) energy_attr_values.extend(ppl_val) # lighting - lgt_kwd, lgt_val = lighting_to_inp(room.properties.energy.lighting) + lgt_kwd, lgt_val = lighting_to_inp(room.properties.energy._lighting) energy_attr_keywords.extend(lgt_kwd) energy_attr_values.extend(lgt_val) # equipment - eq_kwd, eq_val = equipment_to_inp(room.properties.energy.electric_equipment, - room.properties.energy.gas_equipment) + eq_kwd, eq_val = equipment_to_inp(room.properties.energy._electric_equipment, + room.properties.energy._gas_equipment) energy_attr_keywords.extend(eq_kwd) energy_attr_values.extend(eq_val) # hot water usage @@ -479,7 +487,7 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals energy_attr_keywords.extend(shw_kwd) energy_attr_values.extend(shw_val) # infiltration - inf_kwd, inf_val = infiltration_to_inp(room.properties.energy.infiltration) + inf_kwd, inf_val = infiltration_to_inp(room.properties.energy._infiltration) energy_attr_keywords.extend(inf_kwd) energy_attr_values.extend(inf_val) @@ -688,7 +696,7 @@ def model_to_inp( ) # reset identifiers to make them unique and derived from the display names model.reset_ids() - # assign attributes from user_data + # assign any doe2 properties previously supported through user_data for room in model.rooms: room.properties.doe2.apply_properties_from_user_data() @@ -761,6 +769,11 @@ def model_to_inp( dr_con.identifier = dr_con.identifier + '_d' model_str.append(door_construction_to_inp(dr_con)) + # gather together all of the program types in a dictionary for switch statements + switch_dict = {} + for program in model.properties.energy.program_types: + program_type_to_inp(program, switch_dict) + # loop through rooms grouped by floor level and boundary to get polygons level_room_groups, level_geos, level_names = \ group_rooms_by_doe2_level(model.rooms, model.tolerance) @@ -807,6 +820,7 @@ def model_to_inp( model_str.append(header_comment_minor('Misc Cost Related Objects')) model_str.append(header_comment_major('Performance Curves')) model_str.append(header_comment_major('Floors / Spaces / Walls / Windows / Doors')) + model_str.append(switch_dict_to_space_inp(switch_dict)) model_str.extend(bldg_geo_defs) # write in placeholder headers for various HVAC components @@ -825,6 +839,7 @@ def model_to_inp( model_str.append(header_comment_minor('Steam Meters')) model_str.append(header_comment_minor('Chilled Water Meters')) model_str.append(header_comment_major('HVAC Systems / Zones')) + model_str.append(switch_dict_to_zone_inp(switch_dict)) # assign HVAC systems given the specified hvac_mapping if hvac_mapping.upper() == 'STORY': @@ -845,10 +860,10 @@ def model_to_inp( zone_keys = ['TYPE', 'SIZING-OPTION', 'SPACE'] zone_vals = [zone_type, 'ADJUST-LOADS', '"{}"'.format(space_name)] if room.properties.energy.is_conditioned: - stp_kwd, stp_val = setpoint_to_inp(room.properties.energy.setpoint) + stp_kwd, stp_val = setpoint_to_inp(room.properties.energy._setpoint) zone_keys.extend(stp_kwd) zone_vals.extend(stp_val) - vt_kwd, vt_val = ventilation_to_inp(room.properties.energy.ventilation) + vt_kwd, vt_val = ventilation_to_inp(room.properties.energy._ventilation) zone_keys.extend(vt_kwd) zone_vals.extend(vt_val) hvac_kwd, hvac_val = room.properties.doe2.to_inp() diff --git a/tests/writer_test.py b/tests/writer_test.py index 8a2932b..f8c9b4c 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -360,6 +360,27 @@ def test_room_writer(): ' V3 = (15.0, 30.0)\n' \ ' V4 = (0.0, 30.0)\n' \ ' ..\n' + assert room_def[0] == \ + '"Tiny House Zone" = SPACE\n' \ + ' SHAPE = POLYGON\n' \ + ' POLYGON = "Tiny House Zone Plg"\n' \ + ' AZIMUTH = 0\n' \ + ' X = 0.0\n' \ + ' Y = 0.0\n' \ + ' Z = 0.0\n' \ + ' VOLUME = 4500\n' \ + ' ZONE-TYPE = CONDITIONED\n' \ + ' C-ACTIVITY-DESC = *Generic_Office_Program*\n' \ + ' ..\n' + + room.properties.energy.program_type = None + room.properties.energy.people = office_program.people + room.properties.energy.lighting = office_program.lighting + room.properties.energy.electric_equipment = office_program.electric_equipment + room.properties.energy.infiltration = office_program.infiltration + room.properties.energy.ventilation = office_program.ventilation + room.properties.energy.setpoint = office_program.setpoint + room_polygons, room_def = room.to.inp(room) assert room_def[0] == \ '"Tiny House Zone" = SPACE\n' \ ' SHAPE = POLYGON\n' \ @@ -393,8 +414,14 @@ def test_room_writer_program(): south_face.apertures_by_ratio(0.4, 0.01) apartment_prog = program_type_by_identifier('2019::MidriseApartment::Apartment') - room.properties.energy.program_type = apartment_prog room.properties.energy.add_default_ideal_air() + room.properties.energy.people = apartment_prog.people + room.properties.energy.lighting = apartment_prog.lighting + room.properties.energy.electric_equipment = apartment_prog.electric_equipment + room.properties.energy.service_hot_water = apartment_prog.service_hot_water + room.properties.energy.infiltration = apartment_prog.infiltration + room.properties.energy.ventilation = apartment_prog.ventilation + room.properties.energy.setpoint = apartment_prog.setpoint _, room_def = room.to.inp(room) assert room_def[0] == \ '"Tiny House Zone" = SPACE\n' \ @@ -428,7 +455,14 @@ def test_room_writer_program(): ' ..\n' kitchen_prog = program_type_by_identifier('2019::FullServiceRestaurant::Kitchen') - room.properties.energy.program_type = kitchen_prog + room.properties.energy.people = kitchen_prog.people + room.properties.energy.lighting = kitchen_prog.lighting + room.properties.energy.electric_equipment = kitchen_prog.electric_equipment + room.properties.energy.gas_equipment = kitchen_prog.gas_equipment + room.properties.energy.service_hot_water = kitchen_prog.service_hot_water + room.properties.energy.infiltration = kitchen_prog.infiltration + room.properties.energy.ventilation = kitchen_prog.ventilation + room.properties.energy.setpoint = kitchen_prog.setpoint _, room_def = room.to.inp(room) assert room_def[0] == \ '"Tiny House Zone" = SPACE\n' \ From 0d4013e415f06e39cafb13171c86e93bdd0c28c3 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 7 May 2024 11:25:25 -0700 Subject: [PATCH 22/27] fix(programtype): Fix several issues uncovered through eQuest testing --- honeybee_doe2/programtype.py | 32 +++++++++++++++-------------- honeybee_doe2/util.py | 19 +++++++++++++++++ honeybee_doe2/writer.py | 13 ++++++------ tests/program_type_test.py | 40 ++++++++++++++++++++++++++++++++++++ tests/writer_test.py | 2 +- 5 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 tests/program_type_test.py diff --git a/honeybee_doe2/programtype.py b/honeybee_doe2/programtype.py index e00f075..5a9ea42 100644 --- a/honeybee_doe2/programtype.py +++ b/honeybee_doe2/programtype.py @@ -1,9 +1,7 @@ """honeybee-doe2 program type translators.""" from __future__ import division -from honeybee.typing import clean_doe2_string - -from .config import RES_CHARS +from .util import switch_statement_id from .load import people_to_inp, lighting_to_inp, equipment_to_inp, \ infiltration_to_inp, setpoint_to_inp, ventilation_to_inp, \ SPACE_KEYS, ZONE_KEYS, SCHEDULE_KEYS @@ -29,9 +27,8 @@ def program_type_to_inp(program_type, switch_dict=None): """ # set up the switch statement dictionary to be filled switch_dict = switch_dict if switch_dict is not None else {} - prog_uid = clean_doe2_string(program_type.identifier, RES_CHARS) - prog_uid = prog_uid.replace(' ', '_') - base_switch = ' case "{}": '.format(prog_uid) + prog_uid = switch_statement_id(program_type.identifier) + base_switch = 'case "{}": '.format(prog_uid) def _format_schedule(sch_key, sch_uid, obj_type='SPACE'): """Format schedules in the way they are written into switch statements.""" @@ -56,7 +53,8 @@ def _add_to_switch_dict(keyword, value): # write the lighting into the dictionary lgt_kwd, lgt_val = lighting_to_inp(program_type.lighting) for key, val in zip(lgt_kwd, lgt_val): - if key in SCH_KEY_SET: + if key == 'LIGHTING-SCHEDULE': + key = 'LIGHTING-SCHEDUL' # there's a typo in DOE-2 that was never fixed _add_to_switch_dict(key, _format_schedule(key, val, 'SPACE')) else: _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) @@ -75,6 +73,8 @@ def _add_to_switch_dict(keyword, value): for key, val in zip(inf_kwd, inf_val): if key in SCH_KEY_SET: _add_to_switch_dict(key, _format_schedule(key, val, 'SPACE')) + elif key == 'INF-METHOD': + continue # DOE-2 does not like when we define this key else: _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) @@ -114,14 +114,16 @@ def switch_dict_to_space_inp(switch_dict): all_switch_strs = [] for s_key in SPACE_KEYS: try: + if s_key == 'LIGHTING-SCHEDULE': + s_key = 'LIGHTING-SCHEDUL' switch_progs = switch_dict[s_key] switch_strs = ['SET-DEFAULT FOR SPACE'] switch_strs.append(' {} ='.format(s_key)) - switch_strs.append(' {switch(#L("C-ACTIVITY-DESC"))') + switch_strs.append('{switch(#L("C-ACTIVITY-DESC"))') switch_strs.extend(switch_progs) - switch_strs.append(' default: no_default') - switch_strs.append(' endswitch}') - switch_strs.append(' ..\n') + switch_strs.append('default: no_default') + switch_strs.append('endswitch}') + switch_strs.append('..\n') all_switch_strs.append('\n'.join(switch_strs)) except KeyError: pass # none of the programs types have this space key @@ -148,11 +150,11 @@ def switch_dict_to_zone_inp(switch_dict): switch_progs = switch_dict[s_key] switch_strs = ['SET-DEFAULT FOR ZONE', ' TYPE = CONDITIONED'] switch_strs.append(' {} ='.format(s_key)) - switch_strs.append(' {switch(#LR("SPACE", "C-ACTIVITY-DESC"))') + switch_strs.append('{switch(#LR("SPACE", "C-ACTIVITY-DESC"))') switch_strs.extend(switch_progs) - switch_strs.append(' default: no_default') - switch_strs.append(' endswitch}') - switch_strs.append(' ..\n') + switch_strs.append('default: no_default') + switch_strs.append('endswitch}') + switch_strs.append('..\n') all_switch_strs.append('\n'.join(switch_strs)) except KeyError: pass # none of the programs types have this space key diff --git a/honeybee_doe2/util.py b/honeybee_doe2/util.py index 7f34ba6..e513071 100644 --- a/honeybee_doe2/util.py +++ b/honeybee_doe2/util.py @@ -2,6 +2,9 @@ """Various utilities used throughout the package.""" from __future__ import division +import re +import uuid + def generate_inp_string(u_name, command, keywords, values): """Get an INP string representation of a DOE-2 object. @@ -98,3 +101,19 @@ def header_comment_major(header_text): '$ *********************************************************\n'\ '\n'.format(header_text) + +def switch_statement_id(value): + """Convert a string into a 4-character ID that can be used for switch statements. + + This is needed to deal with the major limitations that DOE-2 places on + switch statement IDs, where every ID must be 4 characters + """ + val = ''.join(i for i in value if ord(i) < 128) # strip out non-ascii + val = re.sub(r'["\(\)\[\]\,\=\n\t]', '', val) # remove DOE-2 special characters + val = val.replace(' ', '').replace('_', '').replace(':', '') # remove spaces and colons + if len(val) == 4: # the user has formatted it for switch statements + return val + val = re.sub(r'[aeiouy_\-]', '', val) # remove lower-case vowels for readability + if len(val) >= 4: + return val[-4:] + return str(uuid.uuid4())[:4] # no hope of getting a good ID diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index ac1f509..977c111 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -16,7 +16,7 @@ from .config import DOE2_TOLERANCE, DOE2_ANGLE_TOL, GEO_DEC_COUNT, RECT_WIN_SUBD, \ DOE2_INTERIOR_BCS, GEO_CHARS, RES_CHARS from .util import generate_inp_string, header_comment_minor, \ - header_comment_major + header_comment_major, switch_statement_id from .grouping import group_rooms_by_doe2_level, group_rooms_by_doe2_hvac from .construction import opaque_material_to_inp, opaque_construction_to_inp, \ window_construction_to_inp, door_construction_to_inp, air_construction_to_inp @@ -464,9 +464,7 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals energy_attr_values = [room_doe2_conditioning_type(room)] if room.properties.energy._program_type is not None: energy_attr_keywords.append('C-ACTIVITY-DESC') - prog_uid = clean_doe2_string( - room.properties.energy.program_type.identifier, RES_CHARS) - prog_uid = prog_uid.replace(' ', '_') + prog_uid = switch_statement_id(room.properties.energy.program_type.identifier) energy_attr_values.append('*{}*'.format(prog_uid)) # people ppl_kwd, ppl_val = people_to_inp(room.properties.energy._people) @@ -860,9 +858,10 @@ def model_to_inp( zone_keys = ['TYPE', 'SIZING-OPTION', 'SPACE'] zone_vals = [zone_type, 'ADJUST-LOADS', '"{}"'.format(space_name)] if room.properties.energy.is_conditioned: - stp_kwd, stp_val = setpoint_to_inp(room.properties.energy._setpoint) - zone_keys.extend(stp_kwd) - zone_vals.extend(stp_val) + if room.properties.energy._setpoint is not None: + stp_kwd, stp_val = setpoint_to_inp(room.properties.energy._setpoint) + zone_keys.extend(stp_kwd) + zone_vals.extend(stp_val) vt_kwd, vt_val = ventilation_to_inp(room.properties.energy._ventilation) zone_keys.extend(vt_kwd) zone_vals.extend(vt_val) diff --git a/tests/program_type_test.py b/tests/program_type_test.py new file mode 100644 index 0000000..0c74396 --- /dev/null +++ b/tests/program_type_test.py @@ -0,0 +1,40 @@ +"""Test the translators for ProgramType to INP.""" +from honeybee_energy.lib.programtypes import office_program, program_type_by_identifier + +from honeybee_doe2.load import SPACE_KEYS, SETPOINT_KEYS +from honeybee_doe2.programtype import program_type_to_inp, switch_dict_to_space_inp, \ + switch_dict_to_zone_inp + + +def test_program_type_to_inp(): + """Test the basic functionality of the ProgramType inp writer.""" + switch_dict = program_type_to_inp(office_program) + + for key in SPACE_KEYS: + if key not in ('LIGHTING-SCHEDULE', 'INF-METHOD'): + assert key in switch_dict + for key in SETPOINT_KEYS: + assert key in switch_dict + + inp_space_switch = switch_dict_to_space_inp(switch_dict) + st_str = \ + 'SET-DEFAULT FOR SPACE\n' \ + ' AREA/PERSON =\n' \ + '{switch(#L("C-ACTIVITY-DESC"))\n' \ + 'case "rgrm": 190.512\n' \ + 'default: no_default\n' \ + 'endswitch}\n' \ + '..\n' + assert inp_space_switch.startswith(st_str) + + inp_zone_switch = switch_dict_to_zone_inp(switch_dict) + st_str = \ + 'SET-DEFAULT FOR ZONE\n' \ + ' TYPE = CONDITIONED\n' \ + ' DESIGN-HEAT-T =\n' \ + '{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n' \ + 'case "rgrm": 69.8\n' \ + 'default: no_default\n' \ + 'endswitch}\n' \ + '..' + assert inp_zone_switch.startswith(st_str) diff --git a/tests/writer_test.py b/tests/writer_test.py index f8c9b4c..1179aa1 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -370,7 +370,7 @@ def test_room_writer(): ' Z = 0.0\n' \ ' VOLUME = 4500\n' \ ' ZONE-TYPE = CONDITIONED\n' \ - ' C-ACTIVITY-DESC = *Generic_Office_Program*\n' \ + ' C-ACTIVITY-DESC = *rgrm*\n' \ ' ..\n' room.properties.energy.program_type = None From 6dd31b4a68299d2a4464a54ce05ae31043412526 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 7 May 2024 13:27:11 -0700 Subject: [PATCH 23/27] fix(load): Handle the case that both gas equipment and SHW are specified --- honeybee_doe2/load.py | 117 +++++++++++++++++++---------------- honeybee_doe2/programtype.py | 5 +- honeybee_doe2/writer.py | 19 +++--- tests/writer_test.py | 23 ++++--- 4 files changed, 87 insertions(+), 77 deletions(-) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 8b63c15..04e34f8 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -15,8 +15,8 @@ LIGHTING_KEYS = ('LIGHTING-W/AREA', 'LIGHTING-SCHEDULE', 'LIGHT-TO-RETURN') EQUIP_KEYS = ('EQUIPMENT-W/AREA', 'EQUIP-SCHEDULE', 'EQUIP-SENSIBLE', 'EQUIP-LATENT', 'EQUIP-RAD-FRAC') -HOT_WATER_KEYS = ('SOURCE-TYPE', 'SOURCE-POWER', 'SOURCE-SCHEDULE', - 'SOURCE-SENSIBLE', 'SOURCE-RAD-FRAC', 'SOURCE-LATENT') +SOURCE_KEYS = ('SOURCE-TYPE', 'SOURCE-POWER', 'SOURCE-SCHEDULE', + 'SOURCE-SENSIBLE', 'SOURCE-RAD-FRAC', 'SOURCE-LATENT') INFILTRATION_KEYS = ('INF-METHOD', 'INF-FLOW/AREA', 'INF-SCHEDULE') SETPOINT_KEYS = ('DESIGN-HEAT-T', 'DESIGN-COOL-T', 'HEAT-TEMP-SCH', 'COOL-TEMP-SCH') VENTILATION_KEYS = ('OA-FLOW/PER', 'OA-FLOW/AREA', 'OA-CHANGES', 'OUTSIDE-AIR-FLOW', @@ -27,9 +27,8 @@ 'PEOPLE-SCHEDULE', 'LIGHTING-SCHEDULE', 'EQUIP-SCHEDULE', 'SOURCE-SCHEDULE', 'INF-SCHEDULE', 'HEAT-TEMP-SCH', 'COOL-TEMP-SCH', 'MIN-FLOW-SCH') - -# TODO: Add methods to translate daylight sensors # TODO: Add methods to map honeybee_energy process loads to SOURCE-TYPE PROCESS +# TODO: Add methods to translate daylight sensors def people_to_inp(people): @@ -80,12 +79,11 @@ def lighting_to_inp(lighting): return LIGHTING_KEYS, (lpd, lgt_sch, lighting.return_air_fraction) -def equipment_to_inp(electric_equip, gas_equip=None): - """Translate an Equipment definition(s) into INP (Keywords, Values). +def electric_equipment_to_inp(electric_equip): + """Translate an ElectricEquipment into INP (Keywords, Values). Args: - electric_equip: A honeybee-energy ElectricEquipment definition. None is allowed. - gas_equip: A honeybee-energy GasEquipment definition. None is allowed. + electric_equip: A honeybee-energy ElectricEquipment definition. Returns: A tuple with two elements. @@ -96,44 +94,26 @@ def equipment_to_inp(electric_equip, gas_equip=None): - values: A tuple of text strings that aligns with the keywords and denotes the value for each keyword. """ - # extract the properties from the equipment objects - if electric_equip is not None and gas_equip is not None: # write them as lists - values = [[], [], [], [], []] - for equip in (electric_equip, gas_equip): - epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] - values[0].append(round(epd, 3)) - eqp_sch = clean_doe2_string(equip.schedule.identifier, RES_CHARS) - values[1].append('"{}"'.format(eqp_sch)) - values[2].append(round(1 - equip.latent_fraction - equip.lost_fraction, 3)) - values[3].append(round(equip.latent_fraction, 3)) - values[4].append(round(equip.radiant_fraction, 3)) - format_values = [] - for v in values: - if isinstance(v[0], str): # make sure the schedules do not go past 100 chars - format_values.append('({},\n{}{})'.format(v[0], ' ' * 31, v[1])) - else: - format_values.append('({}, {})'.format(v[0], v[1])) - values = format_values - elif electric_equip is not None or gas_equip is not None: # write as a single item - equip = electric_equip if gas_equip is None else gas_equip - epd = EnergyFlux().to_unit([equip.watts_per_area], 'W/ft2', 'W/m2')[0] - epd = round(epd, 3) - eqp_sch = clean_doe2_string(equip.schedule.identifier, RES_CHARS) - eqp_sch = '("{}")'.format(eqp_sch) - sens_fract = 1 - equip.latent_fraction - equip.lost_fraction - values = (epd, eqp_sch, sens_fract, equip.latent_fraction, - equip.radiant_fraction) - else: # no equipment assigned + if electric_equip is None: return (), () - + epd = EnergyFlux().to_unit([electric_equip.watts_per_area], 'W/ft2', 'W/m2')[0] + epd = round(epd, 3) + eqp_sch = clean_doe2_string(electric_equip.schedule.identifier, RES_CHARS) + eqp_sch = '("{}")'.format(eqp_sch) + sens_fract = 1 - electric_equip.latent_fraction - electric_equip.lost_fraction + values = (epd, eqp_sch, sens_fract, electric_equip.latent_fraction, + electric_equip.radiant_fraction) return EQUIP_KEYS, values -def hot_water_to_inp(hot_water, room_floor_area): - """Translate a ServiceHotWater definition into INP (Keywords, Values). +def hot_water_and_gas_to_inp(hot_water, gas_equip, room_floor_area): + """Translate a ServiceHotWater and/or GasEquipment into INP (Keywords, Values). Args: hot_water: A honeybee-energy ServiceHotWater definition. None is allowed. + None is allowed. + gas_equip: gas_equip: A honeybee-energy GasEquipment definition. + None is allowed. room_floor_area: The host Room floor area in square feet, which will be used to convert the hot water flow per unit floor area to an absolute load in BTU/h. @@ -147,21 +127,52 @@ def hot_water_to_inp(hot_water, room_floor_area): - values: A tuple of text strings that aligns with the keywords and denotes the value for each keyword. """ - if hot_water is None: + # first check whether anything is assigned + if hot_water is None and gas_equip is None: return (), () - flow_den = hot_water.flow_per_area # L/h-m2 - flr_area = Area().to_unit([room_floor_area], 'm2', 'ft2')[0] # m2 - total_flow = flow_den * flr_area # L/h - delta_t = 50 # assume the water heater must heat water from 10C to 60C - c_water = 4.186 # J/g-C, the specific heat of water - shw_heat = total_flow * c_water * delta_t # J/h using Q = m * c * deltaT - shw_heat = shw_heat / 3600. # Watts - shw_power = round(Power().to_unit([shw_heat], 'Btu/h', 'W')[0], 3) - shw_sch = clean_doe2_string(hot_water.schedule.identifier, RES_CHARS) - shw_sch = '"{}"'.format(shw_sch) - values = ('HOT-WATER', shw_power, shw_sch, - hot_water.sensible_fraction, 0, hot_water.latent_fraction) - return HOT_WATER_KEYS, values + + # process the hot water and gas into absolute values in Btu/h + shw_values, gas_values = None, None + if hot_water is not None: + flow_den = hot_water.flow_per_area # L/h-m2 + flr_area = Area().to_unit([room_floor_area], 'm2', 'ft2')[0] # m2 + total_flow = flow_den * flr_area # L/h + delta_t = 50 # assume the water heater must heat water from 10C to 60C + c_water = 4.186 # J/g-C, the specific heat of water + shw_heat = total_flow * c_water * delta_t # J/h using Q = m * c * deltaT + shw_heat = shw_heat / 3600. # Watts + shw_power = round(Power().to_unit([shw_heat], 'Btu/h', 'W')[0], 3) + shw_sch = clean_doe2_string(hot_water.schedule.identifier, RES_CHARS) + shw_sch = '"{}"'.format(shw_sch) + sens_fract = round(hot_water.sensible_fraction, 3) + lat_fract = round(hot_water.latent_fraction, 3) + shw_values = ('HOT-WATER', shw_power, shw_sch, sens_fract, 0.0, lat_fract) + if gas_equip is not None: + epd = EnergyFlux().to_unit([gas_equip.watts_per_area], 'Btu/h-ft2', 'W/m2')[0] + total_power = round(epd * flr_area, 3) # Btu/h + eqp_sch = clean_doe2_string(gas_equip.schedule.identifier, RES_CHARS) + eqp_sch = '"{}"'.format(eqp_sch) + sens_fract = round(1 - gas_equip.latent_fraction - gas_equip.lost_fraction, 3) + rad_fract = round(gas_equip.radiant_fraction, 3) + lat_fract = round(gas_equip.latent_fraction, 3) + gas_values = ('GAS', total_power, eqp_sch, sens_fract, rad_fract, lat_fract) + + # if both were specified, format them into a single set of numbers + if shw_values is not None and gas_values is not None: + total_load = round(shw_values[1] + gas_values[1], 3) + shw_weight = shw_values[1] / total_load + gas_weight = gas_values[1] / total_load + if gas_weight > shw_weight: + values = ['GAS', total_load, gas_values[2]] + else: + values = ['HOT-WATER', total_load, shw_values[2]] + for shw_v, gas_v in zip(shw_values[3:], gas_values[3:]): + new_v = (shw_v * shw_weight) + (gas_v * gas_weight) + values.append(round(new_v, 3)) + else: + values = shw_values if shw_values is not None else gas_values + + return SOURCE_KEYS, values def infiltration_to_inp(infiltration): diff --git a/honeybee_doe2/programtype.py b/honeybee_doe2/programtype.py index 5a9ea42..17adaad 100644 --- a/honeybee_doe2/programtype.py +++ b/honeybee_doe2/programtype.py @@ -2,7 +2,7 @@ from __future__ import division from .util import switch_statement_id -from .load import people_to_inp, lighting_to_inp, equipment_to_inp, \ +from .load import people_to_inp, lighting_to_inp, electric_equipment_to_inp, \ infiltration_to_inp, setpoint_to_inp, ventilation_to_inp, \ SPACE_KEYS, ZONE_KEYS, SCHEDULE_KEYS SCH_KEY_SET = set(SCHEDULE_KEYS) @@ -60,8 +60,7 @@ def _add_to_switch_dict(keyword, value): _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) # write the equipment into the dictionary - eq_kwd, eq_val = equipment_to_inp(program_type.electric_equipment, - program_type.gas_equipment) + eq_kwd, eq_val = electric_equipment_to_inp(program_type.electric_equipment) for key, val in zip(eq_kwd, eq_val): if key in SCH_KEY_SET: _add_to_switch_dict(key, _format_schedule(key, val, 'SPACE')) diff --git a/honeybee_doe2/writer.py b/honeybee_doe2/writer.py index 977c111..dc2cee6 100644 --- a/honeybee_doe2/writer.py +++ b/honeybee_doe2/writer.py @@ -21,8 +21,8 @@ from .construction import opaque_material_to_inp, opaque_construction_to_inp, \ window_construction_to_inp, door_construction_to_inp, air_construction_to_inp from .schedule import energy_trans_sch_to_transmittance -from .load import people_to_inp, lighting_to_inp, equipment_to_inp, hot_water_to_inp, \ - infiltration_to_inp, setpoint_to_inp, ventilation_to_inp +from .load import people_to_inp, lighting_to_inp, electric_equipment_to_inp, \ + hot_water_and_gas_to_inp, infiltration_to_inp, setpoint_to_inp, ventilation_to_inp from .programtype import program_type_to_inp, switch_dict_to_space_inp, \ switch_dict_to_zone_inp from .simulation import SimulationPar @@ -475,15 +475,16 @@ def room_to_inp(room, floor_origin=Point3D(0, 0, 0), exclude_interior_walls=Fals energy_attr_keywords.extend(lgt_kwd) energy_attr_values.extend(lgt_val) # equipment - eq_kwd, eq_val = equipment_to_inp(room.properties.energy._electric_equipment, - room.properties.energy._gas_equipment) + eq_kwd, eq_val = electric_equipment_to_inp( + room.properties.energy._electric_equipment) energy_attr_keywords.extend(eq_kwd) energy_attr_values.extend(eq_val) - # hot water usage - shw_kwd, shw_val = hot_water_to_inp(room.properties.energy.service_hot_water, - room.floor_area) - energy_attr_keywords.extend(shw_kwd) - energy_attr_values.extend(shw_val) + # hot water and gas usage + shw_gas_kwd, shw_gas_val = hot_water_and_gas_to_inp( + room.properties.energy.service_hot_water, + room.properties.energy.gas_equipment, room.floor_area) + energy_attr_keywords.extend(shw_gas_kwd) + energy_attr_values.extend(shw_gas_val) # infiltration inf_kwd, inf_val = infiltration_to_inp(room.properties.energy._infiltration) energy_attr_keywords.extend(inf_kwd) diff --git a/tests/writer_test.py b/tests/writer_test.py index 1179aa1..b8e2a01 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -447,7 +447,7 @@ def test_room_writer_program(): ' SOURCE-POWER = 1.238\n' \ ' SOURCE-SCHEDULE = "ApartmentMidRise APT DHW SCH"\n' \ ' SOURCE-SENSIBLE = 0.2\n' \ - ' SOURCE-RAD-FRAC = 0\n' \ + ' SOURCE-RAD-FRAC = 0.0\n' \ ' SOURCE-LATENT = 0.05\n' \ ' INF-METHOD = AIR-CHANGE\n' \ ' INF-FLOW/AREA = 0.112\n' \ @@ -479,18 +479,17 @@ def test_room_writer_program(): ' LIGHTING-W/AREA = 1.09\n' \ ' LIGHTING-SCHEDULE = "RstrntStDwnBLDG_HENSCH20102013"\n' \ ' LIGHT-TO-RETURN = 0.0\n' \ - ' EQUIPMENT-W/AREA = (37.53, 60.317)\n' \ - ' EQUIP-SCHEDULE = ("RstrntStDwn BLDG EQUIP SCH",\n' \ - ' "RstrntStDwn Rst GAS EQUIP SCH")\n' \ - ' EQUIP-SENSIBLE = (0.55, 0.2)\n' \ - ' EQUIP-LATENT = (0.25, 0.1)\n' \ - ' EQUIP-RAD-FRAC = (0.3, 0.2)\n' \ - ' SOURCE-TYPE = HOT-WATER\n' \ - ' SOURCE-POWER = 29.943\n' \ - ' SOURCE-SCHEDULE = "RestaurantSitDown BLDG SWH SCH"\n' \ + ' EQUIPMENT-W/AREA = 37.53\n' \ + ' EQUIP-SCHEDULE = ("RstrntStDwn BLDG EQUIP SCH")\n' \ + ' EQUIP-SENSIBLE = 0.55\n' \ + ' EQUIP-LATENT = 0.25\n' \ + ' EQUIP-RAD-FRAC = 0.3\n' \ + ' SOURCE-TYPE = GAS\n' \ + ' SOURCE-POWER = 8634.118\n' \ + ' SOURCE-SCHEDULE = "RstrntStDwn Rst GAS EQUIP SCH"\n' \ ' SOURCE-SENSIBLE = 0.2\n' \ - ' SOURCE-RAD-FRAC = 0\n' \ - ' SOURCE-LATENT = 0.05\n' \ + ' SOURCE-RAD-FRAC = 0.199\n' \ + ' SOURCE-LATENT = 0.1\n' \ ' INF-METHOD = AIR-CHANGE\n' \ ' INF-FLOW/AREA = 0.112\n' \ ' INF-SCHEDULE = "RstrntStDwn INFIL HALF ON SCH"\n' \ From 63059d04a44dce64048f953553e5fed9f478b574 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 7 May 2024 13:55:43 -0700 Subject: [PATCH 24/27] fix(util): Use better way to process program IDs in switch statements --- honeybee_doe2/util.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/honeybee_doe2/util.py b/honeybee_doe2/util.py index e513071..98f1198 100644 --- a/honeybee_doe2/util.py +++ b/honeybee_doe2/util.py @@ -3,7 +3,6 @@ from __future__ import division import re -import uuid def generate_inp_string(u_name, command, keywords, values): @@ -108,12 +107,23 @@ def switch_statement_id(value): This is needed to deal with the major limitations that DOE-2 places on switch statement IDs, where every ID must be 4 characters """ - val = ''.join(i for i in value if ord(i) < 128) # strip out non-ascii - val = re.sub(r'["\(\)\[\]\,\=\n\t]', '', val) # remove DOE-2 special characters - val = val.replace(' ', '').replace('_', '').replace(':', '') # remove spaces and colons - if len(val) == 4: # the user has formatted it for switch statements + # first remove dangerous characters + val = re.sub(r'[^.A-Za-z0-9:]', '', value) # remove all unusable characters + val = val.replace(' ', '').replace('_', '') # remove spaces and underscores + + # the user has formatted their program id specifically for switch statements + if len(val) <= 4: return val - val = re.sub(r'[aeiouy_\-]', '', val) # remove lower-case vowels for readability + + # remove lower-case vowels for readability + val = re.sub(r'[aeiouy_\-]', '', val) + if '::' in val: # program id originating from openstudio-standards + val = val.split('::')[-1] + if len(val) >= 4: + return val[:4] + + # some special user-generated program id + val = val.replace(':', '') if len(val) >= 4: return val[-4:] - return str(uuid.uuid4())[:4] # no hope of getting a good ID + return val From fbe0efa5c054b46072a03a567b9d0b1d236eb278 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 7 May 2024 14:53:40 -0700 Subject: [PATCH 25/27] fix(load): Further improvements to loads --- honeybee_doe2/load.py | 7 +++++-- honeybee_doe2/programtype.py | 9 +++++++-- tests/writer_test.py | 3 +++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 04e34f8..21d0908 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -12,7 +12,8 @@ # list of all keywords associated with different load types PEOPLE_KEYS = ('AREA/PERSON', 'PEOPLE-SCHEDULE') -LIGHTING_KEYS = ('LIGHTING-W/AREA', 'LIGHTING-SCHEDULE', 'LIGHT-TO-RETURN') +LIGHTING_KEYS = ('LIGHTING-W/AREA', 'LIGHTING-SCHEDULE', 'LIGHT-TO-RETURN', + 'LIGHT-RAD-FRAC') EQUIP_KEYS = ('EQUIPMENT-W/AREA', 'EQUIP-SCHEDULE', 'EQUIP-SENSIBLE', 'EQUIP-LATENT', 'EQUIP-RAD-FRAC') SOURCE_KEYS = ('SOURCE-TYPE', 'SOURCE-POWER', 'SOURCE-SCHEDULE', @@ -76,7 +77,9 @@ def lighting_to_inp(lighting): lpd = round(lpd, 3) lgt_sch = clean_doe2_string(lighting.schedule.identifier, RES_CHARS) lgt_sch = '"{}"'.format(lgt_sch) - return LIGHTING_KEYS, (lpd, lgt_sch, lighting.return_air_fraction) + ret_fract = round(lighting.return_air_fraction, 3) + rad_fract = round(lighting.radiant_fraction, 3) + return LIGHTING_KEYS, (lpd, lgt_sch, ret_fract, rad_fract) def electric_equipment_to_inp(electric_equip): diff --git a/honeybee_doe2/programtype.py b/honeybee_doe2/programtype.py index 17adaad..4e39c68 100644 --- a/honeybee_doe2/programtype.py +++ b/honeybee_doe2/programtype.py @@ -113,8 +113,8 @@ def switch_dict_to_space_inp(switch_dict): all_switch_strs = [] for s_key in SPACE_KEYS: try: - if s_key == 'LIGHTING-SCHEDULE': - s_key = 'LIGHTING-SCHEDUL' + if len(s_key) > 16: # switch statements limit characters to 16 + s_key = s_key[:16] switch_progs = switch_dict[s_key] switch_strs = ['SET-DEFAULT FOR SPACE'] switch_strs.append(' {} ='.format(s_key)) @@ -126,6 +126,11 @@ def switch_dict_to_space_inp(switch_dict): all_switch_strs.append('\n'.join(switch_strs)) except KeyError: pass # none of the programs types have this space key + # add something to set the INF-METHOD for all spaces + inf_str = 'SET-DEFAULT FOR SPACE\n' \ + ' INF-METHOD = AIR-CHANGE\n' \ + '..\n' + all_switch_strs.append(inf_str) return '\n'.join(all_switch_strs) diff --git a/tests/writer_test.py b/tests/writer_test.py index b8e2a01..2f928d3 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -396,6 +396,7 @@ def test_room_writer(): ' LIGHTING-W/AREA = 0.98\n' \ ' LIGHTING-SCHEDULE = "Generic Office Lighting"\n' \ ' LIGHT-TO-RETURN = 0.0\n' \ + ' LIGHT-RAD-FRAC = 0.7\n' \ ' EQUIPMENT-W/AREA = 0.96\n' \ ' EQUIP-SCHEDULE = ("Generic Office Equipment")\n' \ ' EQUIP-SENSIBLE = 1.0\n' \ @@ -438,6 +439,7 @@ def test_room_writer_program(): ' LIGHTING-W/AREA = 0.87\n' \ ' LIGHTING-SCHEDULE = "ApartmentMidRise LTG APT SCH"\n' \ ' LIGHT-TO-RETURN = 0.0\n' \ + ' LIGHT-RAD-FRAC = 0.6\n' \ ' EQUIPMENT-W/AREA = 0.62\n' \ ' EQUIP-SCHEDULE = ("ApartmentMidRise EQP APT SCH")\n' \ ' EQUIP-SENSIBLE = 1.0\n' \ @@ -479,6 +481,7 @@ def test_room_writer_program(): ' LIGHTING-W/AREA = 1.09\n' \ ' LIGHTING-SCHEDULE = "RstrntStDwnBLDG_HENSCH20102013"\n' \ ' LIGHT-TO-RETURN = 0.0\n' \ + ' LIGHT-RAD-FRAC = 0.7\n' \ ' EQUIPMENT-W/AREA = 37.53\n' \ ' EQUIP-SCHEDULE = ("RstrntStDwn BLDG EQUIP SCH")\n' \ ' EQUIP-SENSIBLE = 0.55\n' \ From 38ceecd3b83a2b941ce7af9365527b41128f9cd8 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 7 May 2024 15:21:14 -0700 Subject: [PATCH 26/27] feat(program): Add support for assigning mech airflow via program type --- honeybee_doe2/load.py | 4 +- honeybee_doe2/programtype.py | 9 ++++- honeybee_doe2/properties/room.py | 8 ++-- tests/program_type_test.py | 67 +++++++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/honeybee_doe2/load.py b/honeybee_doe2/load.py index 21d0908..f2e1c17 100644 --- a/honeybee_doe2/load.py +++ b/honeybee_doe2/load.py @@ -22,8 +22,10 @@ SETPOINT_KEYS = ('DESIGN-HEAT-T', 'DESIGN-COOL-T', 'HEAT-TEMP-SCH', 'COOL-TEMP-SCH') VENTILATION_KEYS = ('OA-FLOW/PER', 'OA-FLOW/AREA', 'OA-CHANGES', 'OUTSIDE-AIR-FLOW', 'MIN-FLOW-SCH') +MECH_AIRFLOW_KEYS = ('ASSIGNED-FLOW', 'FLOW/AREA', 'MIN-FLOW-RATIO', 'MIN-FLOW/AREA', + 'HMAX-FLOW-RATIO') SPACE_KEYS = PEOPLE_KEYS + LIGHTING_KEYS + EQUIP_KEYS + INFILTRATION_KEYS -ZONE_KEYS = SETPOINT_KEYS + VENTILATION_KEYS +ZONE_KEYS = SETPOINT_KEYS + VENTILATION_KEYS + MECH_AIRFLOW_KEYS SCHEDULE_KEYS = ( 'PEOPLE-SCHEDULE', 'LIGHTING-SCHEDULE', 'EQUIP-SCHEDULE', 'SOURCE-SCHEDULE', 'INF-SCHEDULE', 'HEAT-TEMP-SCH', 'COOL-TEMP-SCH', 'MIN-FLOW-SCH') diff --git a/honeybee_doe2/programtype.py b/honeybee_doe2/programtype.py index 4e39c68..5a0230e 100644 --- a/honeybee_doe2/programtype.py +++ b/honeybee_doe2/programtype.py @@ -4,7 +4,7 @@ from .util import switch_statement_id from .load import people_to_inp, lighting_to_inp, electric_equipment_to_inp, \ infiltration_to_inp, setpoint_to_inp, ventilation_to_inp, \ - SPACE_KEYS, ZONE_KEYS, SCHEDULE_KEYS + SPACE_KEYS, ZONE_KEYS, SCHEDULE_KEYS, MECH_AIRFLOW_KEYS SCH_KEY_SET = set(SCHEDULE_KEYS) @@ -93,6 +93,13 @@ def _add_to_switch_dict(keyword, value): else: _add_to_switch_dict(key, '{}{}'.format(base_switch, val)) + # if the user_data of the ProgramType has Mech AirFlow keys, add them + if program_type.user_data is not None: + for air_key in MECH_AIRFLOW_KEYS: + if air_key in program_type.user_data: + val = program_type.user_data[air_key] + _add_to_switch_dict(air_key, '{}{}'.format(base_switch, val)) + return switch_dict diff --git a/honeybee_doe2/properties/room.py b/honeybee_doe2/properties/room.py index e40849e..a0890ed 100644 --- a/honeybee_doe2/properties/room.py +++ b/honeybee_doe2/properties/room.py @@ -3,6 +3,8 @@ from honeybee.typing import float_in_range, float_positive from honeybee.altnumber import autocalculate +from ..load import MECH_AIRFLOW_KEYS + class RoomDoe2Properties(object): """DOE-2 Properties for Honeybee Room. @@ -41,8 +43,6 @@ class RoomDoe2Properties(object): '_host', '_assigned_flow', '_flow_per_area', '_min_flow_ratio', '_min_flow_per_area', '_hmax_flow_ratio' ) - INP_ATTR = ('ASSIGNED-FLOW', 'FLOW/AREA', 'MIN-FLOW-RATIO', - 'MIN-FLOW/AREA', 'HMAX-FLOW-RATIO') def __init__(self, host, assigned_flow=None, flow_per_area=None, min_flow_ratio=None, min_flow_per_area=None, hmax_flow_ratio=None): @@ -194,7 +194,7 @@ def apply_properties_from_user_data(self): 'min_flow_per_area', 'hmax_flow_ratio') data = self.host.user_data if data is not None: - for key, attr in zip(self.INP_ATTR, attrs): + for key, attr in zip(MECH_AIRFLOW_KEYS, attrs): if key in data and getattr(self, attr) is None: try: setattr(self, attr, data[key]) @@ -232,7 +232,7 @@ def to_inp(self): values = [] attrs = ('assigned_flow', 'flow_per_area', 'min_flow_ratio', 'min_flow_per_area', 'hmax_flow_ratio') - for key, attr in zip(self.INP_ATTR, attrs): + for key, attr in zip(MECH_AIRFLOW_KEYS, attrs): attr_value = getattr(self, attr) if attr_value is not None: keywords.append(key) diff --git a/tests/program_type_test.py b/tests/program_type_test.py index 0c74396..9857f42 100644 --- a/tests/program_type_test.py +++ b/tests/program_type_test.py @@ -1,7 +1,7 @@ """Test the translators for ProgramType to INP.""" -from honeybee_energy.lib.programtypes import office_program, program_type_by_identifier +from honeybee_energy.lib.programtypes import office_program -from honeybee_doe2.load import SPACE_KEYS, SETPOINT_KEYS +from honeybee_doe2.load import SPACE_KEYS, SETPOINT_KEYS, MECH_AIRFLOW_KEYS from honeybee_doe2.programtype import program_type_to_inp, switch_dict_to_space_inp, \ switch_dict_to_zone_inp @@ -38,3 +38,66 @@ def test_program_type_to_inp(): 'endswitch}\n' \ '..' assert inp_zone_switch.startswith(st_str) + + +def test_program_type_to_inp_mech_airflow(): + """Test the ProgramType inp writer with mechanical airflow values in user_data.""" + office_with_airflow = office_program.duplicate() + airflow_data = { + "ASSIGNED-FLOW": 10.0, # number in cfm + "FLOW/AREA": 1.0, # number in cfm/ft2 + "MIN-FLOW-RATIO": 0.3, # number between 0 and 1 + "MIN-FLOW/AREA": 0.3, # number in cfm/ft2 + "HMAX-FLOW-RATIO": 0.3 # number between 0 and 1 + } + office_with_airflow.user_data = airflow_data + + switch_dict = program_type_to_inp(office_with_airflow) + for key in MECH_AIRFLOW_KEYS: + assert key in switch_dict + + inp_zone_switch = switch_dict_to_zone_inp(switch_dict) + + flow_per_area_str = \ + 'SET-DEFAULT FOR ZONE\n' \ + ' TYPE = CONDITIONED\n' \ + ' FLOW/AREA =\n' \ + '{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n' \ + 'case "rgrm": 1.0\n' \ + 'default: no_default\n' \ + 'endswitch}\n' \ + '..' + assert flow_per_area_str in inp_zone_switch + + min_flow_ratio_str = \ + 'SET-DEFAULT FOR ZONE\n' \ + ' TYPE = CONDITIONED\n' \ + ' MIN-FLOW-RATIO =\n' \ + '{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n' \ + 'case "rgrm": 0.3\n' \ + 'default: no_default\n' \ + 'endswitch}\n' \ + '..' + assert min_flow_ratio_str in inp_zone_switch + + min_flow_per_area_str = \ + 'SET-DEFAULT FOR ZONE\n' \ + ' TYPE = CONDITIONED\n' \ + ' MIN-FLOW/AREA =\n' \ + '{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n' \ + 'case "rgrm": 0.3\n' \ + 'default: no_default\n' \ + 'endswitch}\n' \ + '..' + assert min_flow_per_area_str in inp_zone_switch + + hmax_flow_ratio_str = \ + 'SET-DEFAULT FOR ZONE\n' \ + ' TYPE = CONDITIONED\n' \ + ' HMAX-FLOW-RATIO =\n' \ + '{switch(#LR("SPACE", "C-ACTIVITY-DESC"))\n' \ + 'case "rgrm": 0.3\n' \ + 'default: no_default\n' \ + 'endswitch}\n' \ + '..' + assert hmax_flow_ratio_str in inp_zone_switch From c202fab0f5be933fb71083fec6945829410b9e56 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Tue, 7 May 2024 15:41:40 -0700 Subject: [PATCH 27/27] fix(extend): Extend ProgramType with to_inp() method --- honeybee_doe2/_extend_honeybee.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/honeybee_doe2/_extend_honeybee.py b/honeybee_doe2/_extend_honeybee.py index 471f359..339b8bd 100644 --- a/honeybee_doe2/_extend_honeybee.py +++ b/honeybee_doe2/_extend_honeybee.py @@ -49,6 +49,7 @@ def room_doe2_properties(self): from honeybee_energy.schedule.day import ScheduleDay from honeybee_energy.schedule.ruleset import ScheduleRuleset from honeybee_energy.schedule.fixedinterval import ScheduleFixedInterval +from honeybee_energy.programtype import ProgramType from honeybee_energy.material.opaque import EnergyMaterial, EnergyMaterialNoMass, \ EnergyMaterialVegetation from honeybee_energy.construction.opaque import OpaqueConstruction @@ -62,12 +63,14 @@ def room_doe2_properties(self): schedule_fixed_interval_to_inp from .construction import opaque_material_to_inp, opaque_construction_to_inp, \ window_construction_to_inp, air_construction_to_inp +from .programtype import program_type_to_inp from .simulation import run_period_to_inp # add the methods to the honeybee-energy classes ScheduleDay.to_inp = schedule_day_to_inp ScheduleRuleset.to_inp = schedule_ruleset_to_inp ScheduleFixedInterval.to_inp = schedule_fixed_interval_to_inp +ProgramType.to_inp = program_type_to_inp EnergyMaterial.to_inp = opaque_material_to_inp EnergyMaterialNoMass.to_inp = opaque_material_to_inp EnergyMaterialVegetation.to_inp = opaque_material_to_inp