Skip to content

Commit

Permalink
feat(properties): Add ModelIDAICEProperties with validation for IDAICE
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey committed Dec 3, 2024
1 parent 6f32b68 commit 0e81434
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 39 deletions.
21 changes: 18 additions & 3 deletions honeybee_idaice/_extend_honeybee.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
from .writer import model_to_idm
from honeybee.model import Model
# coding=utf-8
# import all of the modules for writing geometry to IDAICE
from honeybee.properties import ModelProperties

Model.to_idm = model_to_idm
from .properties.model import ModelIDAICEProperties

# set a hidden ies attribute on each core geometry Property class to None
# define methods to produce ies property instances on each Property instance
ModelProperties._idaice = None


def model_idaice_properties(self):
if self._idaice is None:
self._idaice = ModelIDAICEProperties(self.host)
return self._idaice


# add IDAICE property methods to the Properties classes
ModelProperties.idaice = property(model_idaice_properties)
106 changes: 74 additions & 32 deletions honeybee_idaice/cli/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
import click
import sys
import os
import pathlib
import logging
import base64
import tempfile
import uuid

from honeybee.model import Model
from honeybee.units import parse_distance_string
from honeybee_idaice.writer import model_to_idm as model_to_idm_file

_logger = logging.getLogger(__name__)

Expand All @@ -20,7 +20,7 @@ def translate():


@translate.command('model-to-idm')
@click.argument('model-json', type=click.Path(
@click.argument('model-file', type=click.Path(
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
@click.option(
'--wall-thickness', '-t', help='Maximum thickness of the interior walls. This '
Expand Down Expand Up @@ -59,45 +59,87 @@ def translate():
'--output-file', '-o', help='Optional IDM file path to output the IDM bytes '
'of the translation. By default this will be printed out to stdout.',
type=click.File('w'), default='-', show_default=True)
def model_to_idm(
model_json, wall_thickness, adjacency_distance, name, folder, output_file
def model_to_idm_cli(
model_file, wall_thickness, adjacency_distance, name, folder, output_file
):
"""Translate a Model JSON file to an IDA-ICE IDM file.
"""Translate a Honeybee Model file to an IDA-ICE IDM file.
\b
Args:
model_json: Full path to a Model JSON file (HBJSON) or a Model pkl (HBpkl) file.
model_file: Full path to a Honeybee Model file (HBJSON or HBpkl).
"""
try:
# convert distance strings to floats
wall_thickness = parse_distance_string(str(wall_thickness), 'Meters')
adjacency_distance = parse_distance_string(str(adjacency_distance), 'Meters')

# translate the Model to IDM
model = Model.from_file(model_json)
if folder is not None and name is not None:
folder = pathlib.Path(folder)
folder.mkdir(parents=True, exist_ok=True)
model.to_idm(folder.as_posix(), name=name, debug=False,
max_int_wall_thickness=wall_thickness,
max_adjacent_sub_face_dist=adjacency_distance)
else:
if output_file.name == '<stdout>': # get a temporary file
out_file = str(uuid.uuid4())[:6]
out_folder = tempfile.gettempdir()
else:
out_folder, out_file = os.path.split(output_file.name)
idm_file = model.to_idm(out_folder, name=out_file, debug=False,
max_int_wall_thickness=wall_thickness,
max_adjacent_sub_face_dist=adjacency_distance)
if output_file.name == '<stdout>': # load file contents to stdout
with open(idm_file, 'rb') as of: # IDM can only be read as binary
f_contents = of.read()
b = base64.b64encode(f_contents)
base64_string = b.decode('utf-8')
output_file.write(base64_string)
if not name.lower().endswith('.gem'):
name = name + '.gem'
output_file = os.path.join(folder, name)
model_to_idm(model_file, wall_thickness, adjacency_distance, output_file)
except Exception as e:
_logger.exception('Model translation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


def model_to_idm(model_file, wall_thickness='0.4m', adjacency_distance='0.4m',
output_file=None):
"""Translate a Honeybee Model file to an IDA-ICE IDM file.
Args:
model_file: Full path to a Honeybee Model file (HBJSON or HBpkl).
wall_thickness: Maximum thickness of the interior walls. This can include
the units of the distance (eg. 1.5ft) or, if no units are provided,
the value will be assumed to be in meters (the native units of IDA-ICE).
This value will be used to generate the IDA-ICE building body, which dictates
which Room Faces are exterior vs. interior. This is necessary because IDA-ICE
expects the input model to have gaps between the rooms that represent
the wall thickness. This value input here must be smaller than the smallest
Room that is expected in resulting IDA-ICE model and it should never be
greater than 0.5m in order to avoid creating invalid building bodies for
IDA-ICE. For models where the walls are touching each other, use a
value of 0.
adjacency_distance: Maximum distance between interior Apertures and Doors at
which they are considered adjacent. This can include the units of the
distance (eg. 1.5ft) or, if no units are provided, the value will be
assumed to be in meters (the native units of IDA-ICE). This is used to ensure
that only one interior Aperture of an adjacent pair is written into the
IDM. This value should typically be around the --wall-thickness and should
ideally not be thicker than 0.5m. But it may be undesirable to set this to
zero (like some cases of --wall-thickness), particularly when the adjacent
interior geometries are not perfectly matching one another.
output_file: Optional IDM file path to output the IDM string of the
translation. If None, the string will be returned from this function.
"""
# convert distance strings to floats
wall_thickness = parse_distance_string(str(wall_thickness), 'Meters')
adjacency_distance = parse_distance_string(str(adjacency_distance), 'Meters')

# translate the Model to IDM
model = Model.from_file(model_file)
if isinstance(output_file, str):
folder, name = os.path.dirname(output_file), os.path.basename(output_file)
if not os.path.isdir(folder):
os.makedirs(folder)
model_to_idm_file(
model, folder, name=name, debug=False,
max_int_wall_thickness=wall_thickness,
max_adjacent_sub_face_dist=adjacency_distance)
else:
if output_file is None or output_file.name == '<stdout>': # get a temporary file
out_file = str(uuid.uuid4())[:6]
out_folder = tempfile.gettempdir()
else:
out_folder, out_file = os.path.split(output_file.name)
idm_file = model_to_idm_file(
model, out_folder, name=out_file, debug=False,
max_int_wall_thickness=wall_thickness,
max_adjacent_sub_face_dist=adjacency_distance)
if output_file is None or output_file.name == '<stdout>': # load file contents
with open(idm_file, 'rb') as of: # IDM can only be read as binary
f_contents = of.read()
b = base64.b64encode(f_contents)
base64_string = b.decode('utf-8')
if output_file is None:
return base64_string
else:
output_file.write(base64_string)
1 change: 1 addition & 0 deletions honeybee_idaice/properties/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""honeybee-ies properties."""
92 changes: 92 additions & 0 deletions honeybee_idaice/properties/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# coding=utf-8
"""Model IDAICE Properties."""


class ModelIDAICEProperties(object):
"""IDAICE Properties for Honeybee Model.
Args:
host: A honeybee_core Model object that hosts these properties.
Properties:
* host
"""

def __init__(self, host):
"""Initialize ModelIDAICEProperties."""
self._host = host

@property
def host(self):
"""Get the Model object hosting these properties."""
return self._host

def check_for_extension(self, raise_exception=True, detailed=False):
"""Check that the Model is valid for IDAICE simulation.
This process includes all relevant honeybee-core checks as well as checks
that apply only for IDAICE.
Args:
raise_exception: Boolean to note whether a ValueError should be raised
if any errors are found. If False, this method will simply
return a text string with all errors that were found. (Default: True).
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A text string with all errors that were found or a list if detailed is True.
This string (or list) will be empty if no errors were found.
"""
# set up defaults to ensure the method runs correctly
detailed = False if raise_exception else detailed
msgs = []
tol = self.host.tolerance
ang_tol = self.host.angle_tolerance

# perform checks for duplicate identifiers, which might mess with other checks
msgs.append(self.host.check_all_duplicate_identifiers(False, detailed))

# perform several checks for the Honeybee schema geometry rules
msgs.append(self.host.check_planar(tol, False, detailed))
msgs.append(self.host.check_self_intersecting(tol, False, detailed))
msgs.append(self.host.check_degenerate_rooms(tol, False, detailed))

# perform geometry checks related to parent-child relationships
msgs.append(self.host.check_sub_faces_valid(tol, ang_tol, False, detailed))
msgs.append(self.host.check_sub_faces_overlapping(tol, False, detailed))
msgs.append(self.host.check_upside_down_faces(ang_tol, False, detailed))
msgs.append(self.host.check_rooms_solid(tol, ang_tol, False, detailed))

# perform checks related to adjacency relationships
msgs.append(self.host.check_room_volume_collisions(tol, False, detailed))

# output a final report of errors or raise an exception
full_msgs = [msg for msg in msgs if msg]
if detailed:
return [m for msg in full_msgs for m in msg]
full_msg = '\n'.join(full_msgs)
if raise_exception and len(full_msgs) != 0:
raise ValueError(full_msg)
return full_msg

def to_dict(self):
"""Return Model IDAICE properties as a dictionary."""
return {'ies': {'type': 'ModelIDAICEProperties'}}

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 ModelIDAICEProperties in order
for this method to successfully apply the IDAICE properties.
"""
assert 'idaice' in data['properties'], \
'Dictionary possesses no ModelIDAICEProperties.'

def ToString(self):
return self.__repr__()

def __repr__(self):
return 'Model IDAICE Properties: [host: {}]'.format(self.host.display_name)
4 changes: 2 additions & 2 deletions tests/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import pathlib

from honeybee_idaice.cli.translate import model_to_idm
from honeybee_idaice.cli.translate import model_to_idm_cli


def test_model_to_idm():
Expand All @@ -16,7 +16,7 @@ def test_model_to_idm():
input_hb_model, '--name', out_file, '--wall-thickness', 0.35,
'--folder', out_folder.as_posix()
]
result = runner.invoke(model_to_idm, in_args)
result = runner.invoke(model_to_idm_cli, in_args)
assert result.exit_code == 0

out_path = os.path.join(out_folder.as_posix(), out_file)
Expand Down
7 changes: 5 additions & 2 deletions tests/extend_test.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import pathlib
from honeybee.model import Model
from honeybee_idaice.writer import model_to_idm


def test_model():
in_file = './tests/assets/revit_sample_model_wall_finish.hbjson'
out_folder = pathlib.Path('./tests/assets/temp')
out_folder.mkdir(parents=True, exist_ok=True)
out_folder.mkdir(parents=True, exist_ok=True)
model = Model.from_hbjson(in_file)
outf = model.to_idm(out_folder.as_posix(), name='revit_sample_model_wall_finish')
outf = model_to_idm(
model, out_folder.as_posix(),
name='revit_sample_model_wall_finish')
assert outf.exists()

0 comments on commit 0e81434

Please sign in to comment.