Skip to content

Commit

Permalink
feat(mixed-materials): Mixed Material from WUFI
Browse files Browse the repository at this point in the history
- Add support for reading mixed-materials (exchange-materials) from WUFI-XML
- Update Tests
  • Loading branch information
ed-p-may committed Jan 1, 2025
1 parent 3872154 commit 5931a84
Show file tree
Hide file tree
Showing 23 changed files with 11,825 additions and 28,441 deletions.
56 changes: 38 additions & 18 deletions PHX/from_HBJSON/create_assemblies.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

"""Functions used to create Project elements from the Honeybee-Model"""

import logging
from typing import List, Optional, Tuple, Union

from honeybee import model
Expand All @@ -21,6 +22,8 @@

from PHX.model import constructions, project, shades

logger = logging.getLogger(__name__)


def _conductivity_from_r_value(_r_value: float, _thickness: float) -> float:
"""Returns a material conductivity value (W/mk), given a known r-value (M2K/W) and thickness (M).
Expand Down Expand Up @@ -81,7 +84,7 @@ def build_phx_material_from_hb_EnergyMaterial(
new_mat.density = _hb_material.density
new_mat.heat_capacity = _hb_material.specific_heat

_prop_ph = _hb_material.properties.ph # type: EnergyMaterialPhProperties # type: ignore
_prop_ph: EnergyMaterialPhProperties = getattr(_hb_material.properties, "ph")

try:
hbph_color = _prop_ph.ph_color
Expand Down Expand Up @@ -172,34 +175,51 @@ def build_phx_division_grid_from_hb_division_grid(_hb_div_grid: PhDivisionGrid)
return new_div_grid


def build_layer_from_hb_material(_hb_material: Union[EnergyMaterial, EnergyMaterialNoMass]) -> constructions.PhxLayer:
def build_layer_from_hb_material(
_hb_material: Union[EnergyMaterial, EnergyMaterialNoMass], _no_mass_thickness_m: float = 0.1
) -> constructions.PhxLayer:
"""Returns a new PHX-Layer with attributes based on a Honeybee-Material.
Arguments:
----------
*_hb_material (EnergyMaterial | EnergyMaterialNoMass): The Honeybee Material.
* _hb_material (EnergyMaterial | EnergyMaterialNoMass): The Honeybee Material.
* _no_mass_thickness_m (float): Default=0.1m (4in) The thickness to use for EnergyMaterialNoMass.
Returns:
--------
* constructions.Layer: The new PHX-Layer object.
* (PhxLayer): The new PHX-Layer object.
"""
logger.debug(
f"build_layer_from_hb_material(_hb_material={_hb_material.identifier}, _no_mass_thickness_m={_no_mass_thickness_m})"
)
new_layer = constructions.PhxLayer()

if isinstance(_hb_material, EnergyMaterial):
new_layer.thickness_m = _hb_material.thickness
new_phx_material = build_phx_material_from_hb_EnergyMaterial(_hb_material)
new_layer.set_material(new_phx_material)
# -- Build the division grid first, so we can check for the 'base' material
hbph_props: EnergyMaterialPhProperties = getattr(_hb_material.properties, "ph")
div_grid = build_phx_division_grid_from_hb_division_grid(hbph_props.divisions)
if mat := div_grid.get_base_material():
source_material = mat
else:
source_material = _hb_material

# --- Add in any 'mixed' material elements
div_grid = build_phx_division_grid_from_hb_division_grid(_hb_material.properties.ph.divisions) # type: ignore
# -- Set the new PhxLayer attributes
if isinstance(source_material, EnergyMaterial):
new_layer.thickness_m = source_material.thickness
new_phx_material = build_phx_material_from_hb_EnergyMaterial(source_material)
new_layer.set_material(new_phx_material)
new_layer.divisions = div_grid

elif isinstance(_hb_material, EnergyMaterialNoMass):
new_layer.thickness_m = 0.1 # 0.1m = 4". Use as default since No-Mass has no thickness
new_layer.set_material(build_phx_material_from_hb_EnergyMaterialNoMass(_hb_material, new_layer.thickness_m))
elif isinstance(source_material, EnergyMaterialNoMass):
new_layer.thickness_m = _no_mass_thickness_m
new_layer.set_material(build_phx_material_from_hb_EnergyMaterialNoMass(source_material, new_layer.thickness_m))

elif isinstance(source_material, constructions.PhxMaterial):
new_layer.thickness_m = _hb_material.thickness
new_layer.set_material(source_material)
new_layer.divisions = div_grid

else:
raise TypeError(f"Error: PHX does not support the Material type: '{type(_hb_material)}'.")
raise TypeError(f"Error: PHX does not support the Material type: '{type(source_material)}'.")

return new_layer

Expand All @@ -223,17 +243,17 @@ def build_opaque_assemblies_from_HB_model(_project: project.PhxProject, _hb_mode

for room in _hb_model.rooms:
for face in room.faces:
face_prop_energy = getattr(face.properties, "energy") # type: FaceEnergyProperties
hb_const = face_prop_energy.construction # type: OpaqueConstruction | AirBoundaryConstruction
face_prop_energy: FaceEnergyProperties = getattr(face.properties, "energy")
hb_const: OpaqueConstruction | AirBoundaryConstruction = face_prop_energy.construction

# -- If is an AirBoundary, use the default material
materials = getattr(hb_const, "materials", DEFAULT_MATERIALS)
materials: list[EnergyMaterial] = getattr(hb_const, "materials", DEFAULT_MATERIALS)
if not hb_const.identifier in _project.assembly_types:
# -- Create a new Assembly with Layers from the Honeybee-Construction
new_assembly = constructions.PhxConstructionOpaque()
new_assembly.id_num = constructions.PhxConstructionOpaque._count
new_assembly.display_name = hb_const.display_name
new_assembly.layers = [build_layer_from_hb_material(layer) for layer in materials]
new_assembly.layers = [build_layer_from_hb_material(mat) for mat in materials]

# -- Add the assembly to the Project
_project.add_assembly_type(new_assembly, hb_const.identifier)
Expand Down
49 changes: 46 additions & 3 deletions PHX/from_WUFI_XML/phx_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
PhxLayer,
PhxMaterial,
PhxWindowFrameElement,
PhxLayerDivisionGrid,
PhxLayerDivisionCell,
)
from PHX.model.elec_equip import (
PhxDeviceClothesDryer,
Expand Down Expand Up @@ -304,17 +306,48 @@ def _PhxConstructionOpaque(_data: wufi_xml.WufiAssembly) -> PhxConstructionOpaqu
phx_obj.display_name = _data.Name
phx_obj.layer_order = _data.Order_Layers
phx_obj.grid_kind = _data.Grid_Kind

# -- First, create any exchange-materials
exchange_materials: dict[int, PhxMaterial] = {}
for exchange_mat in _data.ExchangeMaterials or []:
new_exchange_mat: PhxMaterial = as_phx_obj(exchange_mat, "PhxExchangeMaterial")
exchange_materials[exchange_mat.IdentNr] = new_exchange_mat

for layer in _data.Layers:
new_layer = as_phx_obj(layer, "PhxLayer")
new_layer = as_phx_obj(layer, "PhxLayer", _exchange_materials=exchange_materials)
phx_obj.layers.append(new_layer)

return phx_obj


def _PhxLayer(_data: wufi_xml.WufiLayer) -> PhxLayer:
def _PhxLayer(_data: wufi_xml.WufiLayer, _exchange_materials: dict[int, PhxMaterial] = {}) -> PhxLayer:
phx_obj = PhxLayer()
phx_obj.thickness_m = _data.Thickness
new_mat = as_phx_obj(_data.Material, "PhxMaterial")
new_mat: PhxMaterial = as_phx_obj(_data.Material, "PhxMaterial")

# -- Build up any sub-divisions (mixed-material layers)
if _data.ExchangeMaterialIdentNrs:
div_grid = PhxLayerDivisionGrid()
div_grid.set_column_widths([div.Distance for div in _data.ExchangeDivisionHorizontal or []])
div_grid.set_row_heights([div.Distance for div in _data.ExchangeDivisionVertical or []])
div_grid.populate_defaults()

# -- I *think* the WUFI XML order just goes by column, top-to-bottom
# -- So split the list into groups , sized by column-count
if div_grid.column_count > 0:
ids_by_column = [
_data.ExchangeMaterialIdentNrs[i : i + div_grid.row_count]
for i in range(0, len(_data.ExchangeMaterialIdentNrs), div_grid.row_count)
]
for col_num, wufi_mat_id_nums in enumerate(ids_by_column):
for row_num, wufi_mat_id_num in enumerate(wufi_mat_id_nums):
div_grid.set_cell_material(
_column_num=col_num,
_row_num=row_num,
_phx_material=_exchange_materials.get(wufi_mat_id_num.IdentNr_Object, new_mat),
)
phx_obj.divisions = div_grid

phx_obj.set_material(new_mat)

return phx_obj
Expand All @@ -332,6 +365,16 @@ def _PhxMaterial(_data: wufi_xml.WufiMaterial) -> PhxMaterial:
return phx_obj


def _PhxExchangeMaterial(_data: wufi_xml.WufiExchangeMaterial) -> PhxMaterial:
phx_obj = PhxMaterial()
phx_obj.id_num = _data.IdentNr
phx_obj.display_name = _data.Name
phx_obj.conductivity = _data.ThermalConductivity
phx_obj.density = _data.BulkDensity
phx_obj.heat_capacity = _data.HeatCapacity
return phx_obj


def _PhxWindowShade(_data: wufi_xml.WufiSolarProtectionType) -> PhxWindowShade:
phx_obj = PhxWindowShade()
phx_obj.id_num = _data.IdentNr
Expand Down
76 changes: 74 additions & 2 deletions PHX/model/constructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ class PhxMaterial:
display_name: str = ""
conductivity: float = 0.0
density: float = 0.0
porosity: float = 0.0
porosity: float = 0.95
heat_capacity: float = 0.0
water_vapor_resistance: float = 0.0
water_vapor_resistance: float = 1.0
reference_water: float = 0.0
percentage_of_assembly: float = 1.0
argb_color: PhxColor = field(default_factory=PhxColor)
Expand All @@ -45,6 +45,21 @@ def __post_init__(self) -> None:
def __eq__(self, other: PhxMaterial) -> bool:
return self.id_num == other.id_num

def equivalent(self, other: PhxMaterial) -> bool:
"""Check if two materials are equivalent except for their ID-Number."""
return all(
[
self.display_name == other.display_name,
self.conductivity == other.conductivity,
self.density == other.density,
self.porosity == other.porosity,
self.heat_capacity == other.heat_capacity,
self.water_vapor_resistance == other.water_vapor_resistance,
self.reference_water == other.reference_water,
self.argb_color == other.argb_color,
]
)

def __hash__(self) -> int:
return hash(self.id_num)

Expand All @@ -62,6 +77,16 @@ class PhxLayerDivisionCell:
material: PhxMaterial
expanding_contracting: int = 2 # 2="Exp./Contr."

def __eq__(self, other: PhxLayerDivisionCell) -> bool:
return all(
[
self.row == other.row,
self.column == other.column,
self.material.equivalent(other.material),
self.expanding_contracting == other.expanding_contracting,
]
)


@dataclass
class PhxLayerDivisionGrid:
Expand Down Expand Up @@ -105,6 +130,11 @@ def cell_count(self) -> int:
"""Return the total number of cells in the grid."""
return self.row_count * self.column_count

@property
def cells(self) -> list[PhxLayerDivisionCell]:
"""Return a list of all the PhxLayerDivisionCells in the grid, ordered by row then column"""
return sorted(self._cells, key=lambda x: (x.row, x.column))

def set_column_widths(self, _column_widths: Iterable[float]) -> None:
"""Set the column widths of the grid."""
self._column_widths = []
Expand Down Expand Up @@ -177,6 +207,38 @@ def get_cell_area(self, _column_num: int, _row_num) -> float:
row_height = self.row_heights[_row_num]
return col_width * row_height

def get_base_material(self) -> PhxMaterial | None:
"""Get the 'base' material of the grid (the most common material in the layer, by cell-area)."""
if not self._cells:
return None

material_areas = {}
for cell in self._cells:
cell_area = self.get_cell_area(cell.column, cell.row)
if id(cell.material) not in material_areas:
record = {"material": cell.material, "area": cell_area}
material_areas[id(cell.material)] = record
else:
material_areas[id(cell.material)]["area"] += cell_area

return max(material_areas.values(), key=lambda x: x["area"])["material"]

def populate_defaults(self) -> None:
"""Populate the grid with default values. Ensure that there is at least one row or column."""
if self.column_count > 0 and self.row_count == 0:
self.add_new_row(1.0)
elif self.row_count > 0 and self.column_count == 0:
self.add_new_column(1.0)

def __eq__(self, other: PhxLayerDivisionGrid) -> bool:
return all(
[
self.row_heights == other.row_heights,
self.column_widths == other.column_widths,
self.cells == other.cells,
]
)


@dataclass
class PhxLayer:
Expand Down Expand Up @@ -322,6 +384,16 @@ def division_material_id_numbers(self) -> List[int]:
id_numbers_.append(-1)
return id_numbers_

def equivalent(self, other: PhxLayer) -> bool:
"""Check if two layers are equivalent."""
return all(
[
self.thickness_m == other.thickness_m,
self.material.equivalent(other.material),
self.divisions == other.divisions,
]
)


# -----------------------------------------------------------------------------
# Construction
Expand Down
5 changes: 0 additions & 5 deletions output.txt

This file was deleted.

8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
honeybee-core>=1.58.60
honeybee-energy>=1.109.16
honeybee-ph>=1.28.18
honeybee-core>=1.61.1
honeybee-energy>=1.111.0
honeybee-ph>=1.28.20
pydantic<2.0
PH-units>=1.5.15
PH-units>=1.5.17
rich
xlwings
lxml
Loading

0 comments on commit 5931a84

Please sign in to comment.