Skip to content

Commit

Permalink
feat(wufi): Add new merge-spaces option
Browse files Browse the repository at this point in the history
- Adds option to merge spaces on export to WUFI-Passive
- format `isort`, `black`
- update tests
  • Loading branch information
ed-p-may committed Jun 5, 2024
1 parent 0ffbc84 commit 45765b9
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 15 deletions.
2 changes: 2 additions & 0 deletions PHX/from_HBJSON/create_building.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ def create_zones_from_hb_room(
_vent_sched_collection: UtilizationPatternCollection_Ventilation,
_occ_sched_collection: UtilizationPatternCollection_Occupancy,
_lighting_sched_collection: UtilizationPatternCollection_Lighting,
_merge_spaces_by_erv: bool = False,
) -> building.PhxZone:
"""Create a new PHX-Zone based on a honeybee-Room.
Expand Down Expand Up @@ -319,6 +320,7 @@ def create_zones_from_hb_room(
new_zone.weighted_net_floor_area = sum((rm.weighted_floor_area for rm in new_zone.spaces))
new_zone.volume_net = sum((rm.net_volume for rm in new_zone.spaces))
new_zone.specific_heat_capacity = SpecificHeatCapacity(room_prop_ph.specific_heat_capacity.number)
new_zone.merge_spaces_by_erv = _merge_spaces_by_erv

# -- Set the Zone's Occupancy based on the merged HB room
new_zone = set_zone_occupancy(_hb_room, new_zone)
Expand Down
10 changes: 8 additions & 2 deletions PHX/from_HBJSON/create_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def convert_hb_model_to_PhxProject(
_hb_model: model.Model,
_group_components: bool = True,
_merge_faces: Union[bool, float] = False,
_merge_spaces_by_erv: bool = False,
) -> PhxProject:
"""Return a complete WUFI Project object with values based on the HB Model
Expand All @@ -94,6 +95,10 @@ def convert_hb_model_to_PhxProject(
group together co-planar faces in the output room using the HB model tolerance.
If a number is given, it will be used as the tolerance for merging faces.
* _merge_spaces_by_erv (bool): default=False. Set to true to have the converter
merge all the spaces by ERV zones. This is sometimes required by Phius for
large buildings with multiple ERV zones.
Returns:
--------
* (PhxProject): The new WUFI Project object.
Expand Down Expand Up @@ -128,7 +133,8 @@ def convert_hb_model_to_PhxProject(
_vent_sched_collection=phx_project.utilization_patterns_ventilation,
_occ_sched_collection=phx_project.utilization_patterns_occupancy,
_lighting_sched_collection=phx_project.utilization_patterns_lighting,
group_components=_group_components,
_group_components=_group_components,
_merge_spaces_by_erv=_merge_spaces_by_erv,
_tolerance=_hb_model.tolerance,
)

Expand All @@ -137,7 +143,7 @@ def convert_hb_model_to_PhxProject(
create_shades.add_hb_model_shades_to_variant(
new_variant,
_hb_model,
_merge_faces=_merge_faces,
_merge_faces=merge_faces,
_tolerance=_hb_model.tolerance,
_angle_tolerance_degrees=_hb_model.angle_tolerance,
)
Expand Down
17 changes: 12 additions & 5 deletions PHX/from_HBJSON/create_variant.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def add_building_from_hb_room(
_vent_sched_collection: UtilizationPatternCollection_Ventilation,
_occ_sched_collection: UtilizationPatternCollection_Occupancy,
_lighting_sched_collection: UtilizationPatternCollection_Lighting,
group_components: bool = False,
_group_components: bool = False,
_merge_spaces_by_erv: bool = False,
_tolerance: float = 0.001,
) -> None:
"""Create the PHX-Building with all Components and Zones based on a Honeybee-Room.
Expand Down Expand Up @@ -82,10 +83,11 @@ def add_building_from_hb_room(
_vent_sched_collection,
_occ_sched_collection,
_lighting_sched_collection,
_merge_spaces_by_erv,
)
)

if group_components:
if _group_components:
_variant.building.merge_opaque_components_by_assembly()
_variant.building.merge_aperture_components_by_assembly()
_variant.building.merge_thermal_bridges()
Expand Down Expand Up @@ -805,7 +807,8 @@ def from_hb_room(
_vent_sched_collection: UtilizationPatternCollection_Ventilation,
_occ_sched_collection: UtilizationPatternCollection_Occupancy,
_lighting_sched_collection: UtilizationPatternCollection_Lighting,
group_components: bool = False,
_group_components: bool = False,
_merge_spaces_by_erv: bool = False,
_tolerance: float = 0.001,
) -> project.PhxVariant:
"""Create a new PHX-Variant based on a single PH/Honeybee Room.
Expand All @@ -815,8 +818,11 @@ def from_hb_room(
* _hb_room (honeybee.room.Room): The honeybee room to base the Variant on.
* _assembly_dict (Dict[str, constructions.PhxConstructionOpaque]): The Assembly Type dict.
* _window_type_dict (Dict[str, constructions.PhxConstructionWindow]): The Window Type dict.
* group_components (bool): default=False. Set to true to have the converter
* _group_components (bool): default=False. Set to true to have the converter
group the components by assembly-type.
* _merge_spaces_by_erv (bool): default=False. Set to true to merge spaces that
have the same ERV system.
* _tolerance (float): The tolerance to use when merging spaces.
Returns:
--------
Expand Down Expand Up @@ -846,7 +852,8 @@ def from_hb_room(
_vent_sched_collection=_vent_sched_collection,
_occ_sched_collection=_occ_sched_collection,
_lighting_sched_collection=_lighting_sched_collection,
group_components=group_components,
_group_components=_group_components,
_merge_spaces_by_erv=_merge_spaces_by_erv,
_tolerance=_tolerance,
)
# -- Ventilation-Exhaust Equip must come AFTER zones are instantiated, since these
Expand Down
24 changes: 21 additions & 3 deletions PHX/hbjson_to_wufi_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def resolve_paths(_args: List[str]) -> Tuple[pathlib.Path, pathlib.Path]:


def group_components(_args: List[str]) -> bool:
"""Return the group_components boolean from the sys.args Tuple.
"""Return the 'group_components' boolean from the sys.args Tuple.
Arguments:
----------
Expand All @@ -68,7 +68,7 @@ def group_components(_args: List[str]) -> bool:


def merge_faces(_args: List[str]) -> Union[bool, float]:
"""Return the merge_faces as bool | float from the sys.args Tuple.
"""Return the 'merge_faces' as bool | float from the sys.args Tuple.
Arguments:
----------
Expand All @@ -87,6 +87,21 @@ def merge_faces(_args: List[str]) -> Union[bool, float]:
return float(_args[5])


def merge_spaces_by_erv(_args: List[str]) -> bool:
"""Return the 'merge_spaces_by_erv' boolean from the sys.args Tuple.
Arguments:
----------
* _args (Tuple): sys.args Tuple of inputs.
- [6] (str): "True" or "False" string.
Returns:
--------
* bool: True if the user wants to group components.
"""
return _args[6].lower() == "true"


def log_level(_args: List[str]) -> int:
"""Return the log_level from the sys.args Tuple.
Expand All @@ -100,7 +115,7 @@ def log_level(_args: List[str]) -> int:
* int: The logging level.
"""
try:
return int(_args[6])
return int(_args[7])
except Exception:
return 0

Expand Down Expand Up @@ -199,13 +214,15 @@ def startup_logging(_log_level: int) -> logging.Logger:
SOURCE_FILE, TARGET_FILE_XML = resolve_paths(sys.argv)
GROUP_COMPONENTS = group_components(sys.argv)
MERGE_FACES = merge_faces(sys.argv)
MERGE_SPACES_BY_ERV = merge_spaces_by_erv(sys.argv)
LOG_LEVEL = log_level(sys.argv)

## -- Setup the logging
logger = startup_logging(LOG_LEVEL)
logger.info(f"Logging with log-level: {LOG_LEVEL}")
logger.info(f"Group Components: {GROUP_COMPONENTS}")
logger.info(f"Merging Faces: {MERGE_FACES}")
logger.info(f"Merging Spaces by ERV: {MERGE_SPACES_BY_ERV}")

# --- Read in the existing HB_JSON and re-build the HB Objects
# -------------------------------------------------------------------------
Expand All @@ -219,6 +236,7 @@ def startup_logging(_log_level: int) -> logging.Logger:
hb_model,
_group_components=GROUP_COMPONENTS,
_merge_faces=MERGE_FACES,
_merge_spaces_by_erv=MERGE_SPACES_BY_ERV,
)

# --- Apply the WUFI-Passive Cooling Bug fix (200 KW limit)
Expand Down
34 changes: 33 additions & 1 deletion PHX/model/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ class PhxZone:
tfa_override: Optional[float] = None
icfa_override: Optional[float] = None

# -- This flag gets stored here since we don't really have any other way to
# -- pass these kinds of 'settings' down to the XML writer.
merge_spaces_by_erv: bool = False

def __post_init__(self) -> None:
self.__class__._count += 1
self.id_num = self.__class__._count
Expand Down Expand Up @@ -92,10 +96,33 @@ def thermal_bridges(self) -> ValuesView[PhxComponentThermalBridge]:
"""Return all of the PhxComponentThermalBridge objects in the PhxZone."""
return self._thermal_bridges.values()

@property
def spaces_grouped_by_erv(self) -> list[list[spaces.PhxSpace]]:
"""Return a dictionary of spaces grouped by their ERV ID."""
# -- Get all the spaces, sorted by ERV-id-number
grouped_spaces = defaultdict(list)
for s in self.spaces:
grouped_spaces[s.vent_unit_id_num].append(s)

# -- Return the spaces as a list of lists, sorted by the ERV-id-number
spaces_ = []
for k in sorted(grouped_spaces.keys()):
spaces_.append(grouped_spaces[k])
return spaces_

@property
def spaces_with_ventilation(self) -> List[spaces.PhxSpace]:
"""Return a list of all the spaces in the PhxZone which hav ventilation airflow."""
return [s for s in self.spaces if s.has_ventilation_airflow]
spaces_with_ventilation = [s for s in self.spaces if s.has_ventilation_airflow]
if self.merge_spaces_by_erv == False:
# -- Return all the spaces in the zone with ventilation airflow
return spaces_with_ventilation
else:
# -- Merge the Spaces together by their ERV ID
merged_spaces_ = []
for space_group in self.spaces_grouped_by_erv:
merged_spaces_.append(reduce(lambda a, b: a + b, space_group))
return merged_spaces_

def merge_thermal_bridges(self) -> None:
"""Merge together all the Thermal Bridges in the Zone if they have the same 'unique_key' attribute."""
Expand Down Expand Up @@ -380,6 +407,11 @@ def polygons(self) -> List[geometry.PhxPolygon]:
"""Returns a list of all the Polygons of all the Components in the building."""
return [poly for component in self.all_components for poly in component.polygons]

@property
def all_spaces(self) -> List[spaces.PhxSpace]:
"""Return a list of all the Spaces in the Building."""
return [s for z in self.zones for s in z.spaces]

def __bool__(self) -> bool:
return bool(self.opaque_components) or bool(self.zones)

Expand Down
8 changes: 8 additions & 0 deletions PHX/model/loads/ventilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,11 @@ class PhxLoadVentilation:
def total_airflow(self) -> float:
"""Returns the total airflow for the ventilation load."""
return self.flow_supply + self.flow_extract + self.flow_transfer

def __add__(self, other: PhxLoadVentilation) -> PhxLoadVentilation:
"""Combine two PhxLoadVentilation objects."""
new_load = PhxLoadVentilation()
new_load.flow_supply = self.flow_supply + other.flow_supply
new_load.flow_extract = self.flow_extract + other.flow_extract
new_load.flow_transfer = self.flow_transfer + other.flow_transfer
return new_load
16 changes: 15 additions & 1 deletion PHX/model/schedules/ventilation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import uuid
from dataclasses import dataclass, field
from typing import Any, ClassVar, Union
from typing import ClassVar, Union


@dataclass
Expand All @@ -23,6 +23,12 @@ def __eq__(self, other: Vent_OperatingPeriod) -> bool:
else:
return True

def __add__(self, other: Vent_OperatingPeriod) -> Vent_OperatingPeriod:
new_period = Vent_OperatingPeriod()
new_period.period_operating_hours = (self.period_operating_hours + other.period_operating_hours) / 2
new_period.period_operation_speed = (self.period_operation_speed + other.period_operation_speed) / 2
return new_period


@dataclass
class Vent_UtilPeriods:
Expand All @@ -43,6 +49,14 @@ def __eq__(self, other: Vent_UtilPeriods) -> bool:
else:
return True

def __add__(self, other: Vent_UtilPeriods) -> Vent_UtilPeriods:
new_periods = Vent_UtilPeriods()
new_periods.high = self.high + other.high
new_periods.standard = self.standard + other.standard
new_periods.basic = self.basic + other.basic
new_periods.minimum = self.minimum + other.minimum
return new_periods


@dataclass
class PhxScheduleVentilation:
Expand Down
54 changes: 52 additions & 2 deletions PHX/model/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,34 @@
from dataclasses import dataclass, field
from typing import ClassVar

from PHX.model.programs import occupancy as occupancy_prog
from PHX.model.programs.lighting import PhxProgramLighting
from PHX.model.programs.occupancy import PhxProgramOccupancy
from PHX.model.programs.ventilation import PhxProgramVentilation


def area_weighted_clear_height(space_a: PhxSpace, space_b: PhxSpace) -> float:
"""Returns the area-weighted clear-height of two spaces."""
try:
weighted_height_a = space_a.clear_height * space_a.floor_area
weighted_height_b = space_b.clear_height * space_b.floor_area
total_floor_area = space_a.floor_area + space_b.floor_area
return (weighted_height_a + weighted_height_b) / total_floor_area
except ZeroDivisionError:
return 0.0


def spaces_are_not_addable(space_a: PhxSpace, space_v: PhxSpace) -> bool:
"""Returns True if space_a can safely be added to space_b."""
return any(
(
space_a.wufi_type != space_v.wufi_type,
space_a.vent_unit_id_num != space_v.vent_unit_id_num,
space_a.occupancy != space_v.occupancy,
space_a.lighting != space_v.lighting,
)
)


@dataclass
class PhxSpace:
_count: ClassVar[int] = 0
Expand All @@ -31,7 +54,7 @@ class PhxSpace:

# -- Programs
ventilation: PhxProgramVentilation = field(default_factory=PhxProgramVentilation)
occupancy: occupancy_prog.PhxProgramOccupancy = field(default_factory=occupancy_prog.PhxProgramOccupancy)
occupancy: PhxProgramOccupancy = field(default_factory=PhxProgramOccupancy)
lighting: PhxProgramLighting = field(default_factory=PhxProgramLighting)

electric_equipment = None
Expand All @@ -57,3 +80,30 @@ def peak_occupancy(self, value: float) -> None:
def has_ventilation_airflow(self) -> bool:
"""Returns True if the space has ventilation airflow."""
return self.ventilation.has_ventilation_airflow

def __add__(self, other: PhxSpace) -> PhxSpace:
"""Adds two spaces together.
This is used to report out 'grouped' spaces for Phius. In almost all
other cases, spaces should be kept separated. Note that the occupancy,
lighting, and ventilation schedules are NOT merged, however the
ventilation loads ARE added together.
"""
if spaces_are_not_addable(self, other):
raise ValueError(f"Space {self.display_name} cannot be added to space {other.display_name}.")

new_space = PhxSpace(
display_name=f"ERV: {self.vent_unit_id_num}",
wufi_type=self.wufi_type,
quantity=1,
floor_area=self.floor_area + other.floor_area,
weighted_floor_area=self.weighted_floor_area + other.weighted_floor_area,
net_volume=self.net_volume + other.net_volume,
clear_height=area_weighted_clear_height(self, other),
vent_unit_id_num=self.vent_unit_id_num,
ventilation=self.ventilation,
occupancy=self.occupancy,
lighting=self.lighting,
)
new_space.ventilation.load = self.ventilation.load + other.ventilation.load
return new_space
5 changes: 4 additions & 1 deletion PHX/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,12 @@ def convert_hbjson_to_WUFI_XML(
_save_folder,
_group_components=True,
_merge_faces=False,
_merge_spaces_by_erv=False,
_log_level=0,
*args,
**kwargs
):
# type: (str, str, str, bool, Union[bool, float], int, List, Dict) -> tuple[str, str, str, str]
# type: (str, str, str, bool, Union[bool, float], bool, int, List, Dict) -> tuple[str, str, str, str]
"""Read in an hbjson file and output a new WUFI XML file in the designated location.
Arguments:
Expand All @@ -149,6 +150,7 @@ def convert_hbjson_to_WUFI_XML(
* _group_components (bool): Group components by construction? Default=True
* _merge_faces (bool | float): Merge together faces of the same type and touching? If
a number is provided, it will be used as the tolerance when merging faces. Default=False
* _merge_spaces_by_erv (bool): Merge spaces that are connected by ERVs? Default=False
* _log_level (int): Set the logging level for the subprocess. Default=0
* args (List): Additional arguments to pass to the subprocess.
* kwargs (Dict): Additional keyword arguments to pass to the subprocess.
Expand Down Expand Up @@ -181,6 +183,7 @@ def convert_hbjson_to_WUFI_XML(
_save_folder,
str(_group_components),
str(_merge_faces),
str(_merge_spaces_by_erv),
str(_log_level),
]
stdout, stderr = _run_subprocess(commands)
Expand Down
Loading

0 comments on commit 45765b9

Please sign in to comment.