# -*- coding: utf-8 -*-
# -*- Python Version: 2.7 -*-
"""Passive House properties for honeybee_energy.material.opaque.EnergyMaterial Objects"""
from collections import defaultdict
try:
from typing import Any, Iterable, List, NoReturn, Optional
except ImportError:
pass # IronPython 2.7
try:
from honeybee_energy.material import opaque
except ImportError as e:
raise ImportError("\nFailed to import honeybee_energy:\n\t{}".format(e))
try:
from honeybee_ph_utils.color import PhColor
except ImportError as e:
raise ImportError("\nFailed to import honeybee_ph_utils:\n\t{}".format(e))
[docs]
class CellPositionError(Exception):
"""An error for when a cell position is out of range."""
def __init__(self, msg):
super(CellPositionError, self).__init__(msg)
[docs]
class PhDivisionCell(object):
"""A single cell in a PhLayerDivisionGrid."""
def __init__(self, _row, _column, _hbe_material):
# type: (int, int, opaque.EnergyMaterial) -> None
self.row = _row
self.column = _column
self.material = _hbe_material
[docs]
def to_dict(self):
# type: () -> dict[str, Any]
d = {}
d["row"] = self.row
d["column"] = self.column
d["material"] = self.material.to_dict()
return d
[docs]
@classmethod
def from_dict(cls, _input_dict):
# type: (dict[str, Any]) -> PhDivisionCell
new_cell = cls(
_input_dict["row"], _input_dict["column"], opaque.EnergyMaterial.from_dict(_input_dict["material"])
)
return new_cell
[docs]
def duplicate(self):
# type: () -> PhDivisionCell
"""Duplicate the cell."""
return PhDivisionCell(self.row, self.column, self.material.duplicate())
def __copy__(self):
# type: () -> PhDivisionCell
return self.duplicate()
def __str__(self):
return "{}(row={}, column={}, material={})".format(
self.__class__.__name__,
self.row,
self.column,
self.material.display_name,
)
def __repr__(self):
return str(self)
[docs]
def ToString(self):
return str(self)
[docs]
class PhDivisionGrid(object):
"""A grid of PhDivisionCell to support 'mixed' materials.
The Cell grid is ordered from top-left to bottom-right:
| | C0 | C1 | C2 | ...
|:---|:---:|:---:|:---:|:---:
| R0 | 0,0 | 1,0 | 2,0 | ...
| R1 | 0,1 | 1,1 | 2,1 | ...
| R2 | 0,2 | 1,2 | 2,2 | ...
"""
def __init__(self):
# type: () -> None
self._row_heights = [] # type: List[float]
self._column_widths = [] # type: List[float]
self._cells = [] # type: List[PhDivisionCell]
self.steel_stud_spacing_mm = None # type: float | None
@property
def column_widths(self):
# type: () -> List[float]
"""Return the list of column widths."""
return self._column_widths
@property
def column_count(self):
# type: () -> int
"""Return the number of columns in the grid."""
return len(self._column_widths)
@property
def row_heights(self):
# type: () -> List[float]
"""Return the list of row heights."""
return self._row_heights
@property
def row_count(self):
# type: () -> int
"""Return the number of rows in the grid."""
return len(self._row_heights)
@property
def cell_count(self):
# type: () -> int
"""Return the total number of cells in the grid."""
return self.row_count * self.column_count
@property
def cells(self):
# type: () -> List[PhDivisionCell]
"""Return the list of all the cells in the grid, sorted by row/column."""
return sorted(self._cells, key=lambda x: (x.row, x.column))
@property
def is_a_steel_stud_cavity(self):
# type: () -> bool
"""Check if the grid is a steel stud cavity by checking if the spacing is set."""
return self.steel_stud_spacing_mm is not None and self.steel_stud_spacing_mm > 0
[docs]
def set_column_widths(self, _column_widths):
# type: (Iterable[float]) -> None
"""Set the column widths of the grid."""
self._column_widths = []
for width in _column_widths:
self.add_new_column(width)
[docs]
def add_new_column(self, _column_width):
# type: (float) -> None
"""Add a new COLUMN to the grid with the given width."""
if _column_width is None:
return
if _column_width > 0:
self._column_widths.append(float(_column_width))
[docs]
def set_row_heights(self, _row_heights):
# type: (Iterable[float]) -> None
"""Set the row heights of the grid."""
self._row_heights = []
for height in _row_heights:
self.add_new_row(height)
[docs]
def add_new_row(self, _row_height):
# type: (float) -> None
"""Add a new ROW to the grid with the given height."""
if _row_height is None:
return
if _row_height > 0:
self._row_heights.append(float(_row_height))
[docs]
def get_cell(self, _column, _row):
# type: (int, int) -> Optional[PhDivisionCell]
"""Get the PhDivisionCell at the given column and row position."""
for cell in self._cells:
if cell.column == _column and cell.row == _row:
return cell
return None
[docs]
def set_cell_material(self, _column_num, _row_num, _hbe_material):
# type: (int, int, opaque.EnergyMaterial) -> None
"""Set the EnergyMaterial for a specific cell in the grid by its column/row position.
Cells are indexed by their column and row position stating from top-left:
| | C0 | C1 | C2 | ...
|---|------|-----|-----|----
|R0 | 0,0 | 1,0 | 2,0 | ...
|R1 | 0,1 | 1,1 | 2,1 | ...
|R2 | 0,2 | 1,2 | 2,2 | ...
"""
if _column_num >= self.column_count:
msg = (
"Error setting Material '{}' to column '{}'. The specified column is out of range. "
"Please setup all the column-widths before assigning any materials. And "
"remember that the columns start counting from '0', not '1'.".format(
_hbe_material.display_name, _column_num
)
)
raise CellPositionError(msg)
if _row_num >= self.row_count:
msg = (
"Error setting Material '{}' to row '{}'. The specified row is out of range. "
"Please setup all the row-heights before assigning any materials. And "
"remember that the rows start counting from '0', not '1'.".format(_hbe_material.display_name, _row_num)
)
raise CellPositionError(msg)
# -- See if the cell already exists, if so reset its material
# -- if it does not exist, create a new cell.
existing_cell = self.get_cell(_column_num, _row_num)
if existing_cell:
existing_cell.material = _hbe_material
else:
new_cell = PhDivisionCell(_row=_row_num, _column=_column_num, _hbe_material=_hbe_material)
self._cells.append(new_cell)
[docs]
def get_cell_material(self, _column_num, _row_num):
# type: (int, int) -> Optional[opaque.EnergyMaterial]
"""Get the PhxMaterial for a specific cell in the grid by its column/row position."""
for cell in self._cells:
if cell.row == _row_num and cell.column == _column_num:
return cell.material
return None
[docs]
def get_cell_area(self, _column_num, _row_num):
# type: (int, int) -> float
"""Get the area of a specific cell in the grid by its column/row position."""
if _column_num >= self.column_count:
return 0.0
if _row_num >= self.row_count:
return 0.0
return self._column_widths[_column_num] * self._row_heights[_row_num]
[docs]
def get_cell_width_m(self, _cell):
# type: (PhDivisionCell) -> float
"""Get the width (mm) of a specific cell."""
try:
return self._column_widths[_cell.column]
except IndexError:
return 0.0
[docs]
def get_cell_height_m(self, _cell):
# type: (PhDivisionCell) -> float
"""Get the height (mm) of a specific cell."""
try:
return self._row_heights[_cell.row]
except IndexError:
return 0.0
[docs]
def get_base_material(self):
# type: () -> Optional[opaque.EnergyMaterial]
"""Returns the 'base' material (the most common material in the grid, by area)."""
if not self._cells:
return None
# -- Collect all the cell areas
material_areas = defaultdict(float, default=0.0)
for cell in self.cells:
cell_area = self.get_cell_area(cell.column, cell.row)
material_areas[cell.material.identifier] += cell_area
# -- Find the material with the largest area. This is the 'base' material
base_material_id = sorted(material_areas.items(), key=lambda x: x[1])[-1][0]
for cell in self.cells:
if cell.material.identifier == base_material_id:
return cell.material
return None
[docs]
def get_equivalent_conductivity(self):
# type: () -> float
"""Return an area-weighted average of the conductivities of all materials in the grid."""
total_area = 0.0
total_conductivity = 0.0
for cell in self.cells:
cell_area = self.get_cell_area(cell.column, cell.row)
total_area += cell_area
total_conductivity += cell_area * cell.material.conductivity
if total_area > 0:
return total_conductivity / total_area
return 0.0
[docs]
def to_dict(self):
# type: () -> dict[str, Any]
d = {}
d["column_widths"] = self.column_widths
d["row_heights"] = self.row_heights
d["cells"] = []
for cell in self._cells:
d["cells"].append(cell.to_dict())
d["steel_stud_spacing_mm"] = self.steel_stud_spacing_mm
return d
[docs]
@classmethod
def from_dict(cls, _input_dict):
# type: (dict[str, Any]) -> PhDivisionGrid
new_grid = cls()
new_grid.set_column_widths(_input_dict.get("column_widths", []))
new_grid.set_row_heights(_input_dict.get("row_heights", []))
for cell_dict in _input_dict.get("cells", []):
new_grid.set_cell_material(
cell_dict["column"],
cell_dict["row"],
opaque.EnergyMaterial.from_dict(cell_dict["material"]),
)
new_grid.steel_stud_spacing_mm = _input_dict.get("steel_stud_spacing_mm", None)
return new_grid
[docs]
def duplicate(self):
# type: () -> PhDivisionGrid
"""Duplicate the grid."""
new_grid = PhDivisionGrid()
new_grid.set_column_widths(self.column_widths)
new_grid.set_row_heights(self.row_heights)
for cell in self._cells:
new_grid.set_cell_material(cell.column, cell.row, cell.material.duplicate())
new_grid.steel_stud_spacing_mm = self.steel_stud_spacing_mm
return new_grid
def __copy__(self):
# type: () -> PhDivisionGrid
return self.duplicate()
def __str__(self):
return "{}(columns={}, rows={})".format(
self.__class__.__name__,
self.column_count,
self.row_count,
)
def __repr__(self):
return str(self)
[docs]
def ToString(self):
return str(self)
[docs]
class EnergyMaterialPhProperties(object):
"""Passive House properties for EnergyMaterial objects."""
def __init__(self, _host=None):
# type: (Optional[opaque.EnergyMaterial]) -> None
self._host = _host
self.id_num = 0
self._ph_color = None # type: Optional[PhColor]
self.user_data = {}
self.divisions = PhDivisionGrid()
# -------------------------------------------------------------------------
# ------------------ Deprecated [April 4, 2024] ---------------------------
@property
def percentage_of_assembly(self):
# type: () -> NoReturn
raise DeprecationWarning(
"The 'percentage_of_assembly' property is deprecated. Please use the 'divisions' for mixed materials."
)
@percentage_of_assembly.setter
def percentage_of_assembly(self, _percentage):
# type: (Any) -> NoReturn
raise DeprecationWarning(
"The 'percentage_of_assembly' property is deprecated. Please use the 'divisions' for mixed materials."
)
@property
def base_material(self):
raise DeprecationWarning(
"The 'base_material' property is deprecated. Please use the 'divisions' for mixed materials."
)
@base_material.setter
def base_material(self, _material):
# type: (Any) -> NoReturn
raise DeprecationWarning(
"The 'base_material' property is deprecated. Please use the 'divisions' for mixed materials."
)
@property
def base_materials(self):
# type: () -> NoReturn
raise DeprecationWarning(
"The 'base_materials' property is deprecated. Please use the 'divisions' for mixed materials."
)
@base_materials.setter
def base_materials(self, _materials):
# type: (Any) -> NoReturn
raise DeprecationWarning(
"The 'base_materials' property is deprecated. Please use the 'divisions' for mixed materials."
)
[docs]
def add_base_material(self, _hb_material):
# type: (Any) -> NoReturn
raise DeprecationWarning(
"The 'add_base_material' function is deprecated. Please use the 'divisions' for mixed materials."
)
[docs]
def clear_base_materials(self):
# type: (Any) -> NoReturn
raise DeprecationWarning(
"The 'clear_base_materials' function is deprecated. Please use the 'divisions' for mixed materials."
)
# -------------------------------------------------------------------------
# -------------------------------------------------------------------------
@property
def ph_color(self):
# type: () -> Optional[PhColor]
return self._ph_color
@ph_color.setter
def ph_color(self, _input_color):
# type: (Optional[PhColor]) -> None
self._ph_color = _input_color
[docs]
def to_dict(self, abridged=False):
# type: (bool) -> dict[str, dict]
d = {}
d["id_num"] = self.id_num
d["divisions"] = self.divisions.to_dict()
d["user_data"] = self.user_data
if self.ph_color:
d["ph_color"] = self.ph_color.to_dict()
return {"ph": d}
[docs]
@classmethod
def from_dict(cls, _input_dict, _host):
# type: (dict, Optional[opaque.EnergyMaterial]) -> EnergyMaterialPhProperties
new_prop = cls(_host)
new_prop.id_num = _input_dict["id_num"]
new_prop._ph_color = PhColor.from_dict(_input_dict.get("ph_color", None))
new_prop.user_data = _input_dict.get("user_data", {})
new_prop.divisions = PhDivisionGrid.from_dict(_input_dict.get("divisions", {}))
return new_prop
[docs]
def apply_properties_from_dict(self, abridged_data):
# type: (Any) -> None
return None
def __copy__(self, new_host=None):
# type: (Optional[opaque.EnergyMaterial]) -> EnergyMaterialPhProperties
_host = new_host or self._host
new_obj = EnergyMaterialPhProperties(_host)
new_obj.id_num = self.id_num
new_obj.divisions = self.divisions.duplicate()
new_obj.user_data = self.user_data.copy()
if self.ph_color:
new_obj.ph_color = self.ph_color.duplicate()
return new_obj
[docs]
def duplicate(self, new_host=None):
# type: (Optional[opaque.EnergyMaterial]) -> EnergyMaterialPhProperties
return self.__copy__(new_host)
def __str__(self):
return "{}(id_num={!r}, ph_color={!r}, is_a_steel_stud_cavity={})".format(
self.__class__.__name__,
self.id_num,
self.ph_color,
)
def __repr__(self):
return str(self)
[docs]
def ToString(self):
return str(self)
[docs]
class EnergyMaterialNoMassPhProperties(object):
def __init__(self, _host=None):
# type: (Optional[opaque.EnergyMaterialNoMass]) -> None
self.host = _host
self.id_num = 0
self._ph_color = None # type: Optional[PhColor]
self.user_data = {}
self.divisions = None
@property
def ph_color(self):
# type: () -> Optional[PhColor]
return self._ph_color
@ph_color.setter
def ph_color(self, _input_color):
# type: (Optional[PhColor]) -> None
self._ph_color = _input_color
[docs]
def to_dict(self, abridged=False):
# type: (bool) -> dict[str, dict]
d = {}
d["id_num"] = self.id_num
d["user_data"] = self.user_data
d["divisions"] = self.divisions
if self.ph_color:
d["ph_color"] = self.ph_color.to_dict()
return {"ph": d}
[docs]
@classmethod
def from_dict(cls, _input_dict, _host):
# type: (dict, Optional[opaque.EnergyMaterialNoMass]) -> EnergyMaterialNoMassPhProperties
new_prop = cls(_host)
new_prop.id_num = _input_dict["id_num"]
new_prop._ph_color = PhColor.from_dict(_input_dict.get("ph_color", None))
new_prop.user_data = _input_dict.get("user_data", {})
new_prop.divisions = _input_dict["divisions"]
return new_prop
[docs]
def apply_properties_from_dict(self, abridged_data):
# type: (dict) -> None
return
def __copy__(self, new_host=None):
# type: (Optional[opaque.EnergyMaterialNoMass]) -> EnergyMaterialNoMassPhProperties
_host = new_host or self.host
new_obj = EnergyMaterialNoMassPhProperties(_host)
new_obj.id_num = self.id_num
new_obj.user_data = self.user_data.copy()
new_obj.divisions = self.divisions
if self.ph_color:
new_obj.ph_color = self.ph_color.duplicate()
return new_obj
[docs]
def duplicate(self, new_host=None):
# type: (Optional[opaque.EnergyMaterialNoMass]) -> EnergyMaterialNoMassPhProperties
return self.__copy__(new_host)
def __str__(self):
return "{}(id_num={!r}, ph_color={!r})".format(self.__class__.__name__, self.id_num, self.ph_color)
def __repr__(self):
return str(self)
[docs]
def ToString(self):
return str(self)
[docs]
class EnergyMaterialVegetationPhProperties(object):
def __init__(self, _host=None):
# type: (Optional[opaque.EnergyMaterialVegetation]) -> None
self.host = _host
self.id_num = 0
self._ph_color = None # type: Optional[PhColor]
self.user_data = {}
self.divisions = None
@property
def ph_color(self):
# type: () -> Optional[PhColor]
return self._ph_color
@ph_color.setter
def ph_color(self, _input_color):
# type: (Optional[PhColor]) -> None
self._ph_color = _input_color
[docs]
def to_dict(self, abridged=False):
# type: (bool) -> dict[str, dict]
d = {}
d["id_num"] = self.id_num
d["user_data"] = self.user_data
d["divisions"] = self.divisions
if self.ph_color:
d["ph_color"] = self.ph_color.to_dict()
return {"ph": d}
[docs]
@classmethod
def from_dict(cls, _input_dict, _host):
# type: (dict[str, Any], Optional[opaque.EnergyMaterialVegetation]) -> EnergyMaterialVegetationPhProperties
new_prop = cls(_host)
new_prop.id_num = _input_dict["id_num"]
new_prop._ph_color = PhColor.from_dict(_input_dict.get("ph_color", None))
new_prop.user_data = _input_dict.get("user_data", {})
new_prop.divisions = _input_dict["divisions"]
return new_prop
[docs]
def apply_properties_from_dict(self, abridged_data):
return
def __copy__(self, new_host=None):
# type: (Optional[opaque.EnergyMaterialVegetation]) -> EnergyMaterialVegetationPhProperties
_host = new_host or self.host
new_obj = EnergyMaterialVegetationPhProperties(_host)
new_obj.id_num = self.id_num
new_obj.user_data = self.user_data.copy()
new_obj.divisions = self.divisions
if self.ph_color:
new_obj.ph_color = self.ph_color.duplicate()
return new_obj
[docs]
def duplicate(self, new_host=None):
# type: (Optional[opaque.EnergyMaterialVegetation]) -> EnergyMaterialVegetationPhProperties
return self.__copy__(new_host)
def __str__(self):
return "{}(id_num={!r}, ph_color={!r})".format(self.__class__.__name__, self.id_num, self.ph_color)
def __repr__(self):
return str(self)
[docs]
def ToString(self):
return str(self)