Skip to content

Commit

Permalink
feat(properties): Add ModelIESProperties with a specific check for IES
Browse files Browse the repository at this point in the history
This change also refactors the CLI to be call-able as standalone functions
  • Loading branch information
chriswmackey committed Dec 3, 2024
1 parent 2e20f84 commit ccb8d89
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 28 deletions.
23 changes: 18 additions & 5 deletions honeybee_ies/_extend_honeybee.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
from .writer import _shade_to_ies, room_to_ies, model_to_ies
from honeybee.model import Shade, Room, Model
# coding=utf-8
# import all of the modules for writing geometry to IES
from honeybee.properties import ModelProperties

Shade.to_gem = _shade_to_ies
Room.to_gem = room_to_ies
Model.to_gem = model_to_ies
from .properties.model import ModelIESProperties

# 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._ies = None


def model_ies_properties(self):
if self._ies is None:
self._ies = ModelIESProperties(self.host)
return self._ies


# add ies property methods to the Properties classes
ModelProperties.ies = property(model_ies_properties)
73 changes: 57 additions & 16 deletions honeybee_ies/cli/translate.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
"""honeybee ies translation commands."""
import click
import sys
import pathlib
import os
import logging
import json

from ladybug.futil import write_to_file_by_name
from honeybee.model import Model
from honeybee_ies.writer import model_to_gem
from honeybee_ies.writer import model_to_gem as model_to_gem_str
from honeybee_ies.reader import model_from_ies

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -37,29 +36,43 @@ def translate():
'--output-file', '-o', help='Optional GEM file path to output the GEM string '
'of the translation. By default this will be printed out to stdout.',
type=click.File('w'), default='-', show_default=True)
def model_to_gem_file(model_file, shade_thickness, name, folder, output_file):
def model_to_gem_cli(model_file, shade_thickness, name, folder, output_file):
"""Translate a Model JSON file to an IES GEM file.
\b
Args:
model_file: Full path to a Model JSON file (HBJSON) or a Model pkl (HBpkl) file.
"""
try:
model = Model.from_file(model_file)
gem_str = model_to_gem(model, shade_thickness=shade_thickness)
if folder is not None and name is not None:
if not name.lower().endswith('.gem'):
name = name + '.gem'
write_to_file_by_name(folder, name, gem_str, True)
else:
output_file.write(gem_str)
output_file = os.path.join(folder, name)
model_to_gem(model_file, shade_thickness, output_file)
except Exception as e:
_logger.exception('Model translation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


def model_to_gem(model_file, shade_thickness=0, output_file=None):
"""Translate a Model JSON file to an IES GEM file.
Args:
model_file: Full path to a Honeybee Model file (HBJSON or HBpkl).
shade_thickness: Optional value for shade thickness in meters. This value
will be used to extrude shades with no group id. IES does not consider
the effect of shades with no thickness in SunCalc. This function
extrudes the geometry to create a closed volume for the shade. (Default: 0).
output_file: Optional GEM file path to output the GEM string of the
translation. If None, the string will be returned from this function.
"""
model = Model.from_file(model_file)
gem_str = model_to_gem_str(model, shade_thickness=shade_thickness)
return _process_content_to_output(gem_str, output_file)


@translate.command('gem-to-model')
@click.argument('gem-file', type=click.Path(
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
Expand All @@ -73,23 +86,51 @@ def model_to_gem_file(model_file, shade_thickness, name, folder, output_file):
'--output-file', '-o', help='Optional HBJSON file path to output the HBJSON string '
'of the translation. By default this will be printed out to stdout.',
type=click.File('w'), default='-', show_default=True)
def gem_to_model_file(gem_file, name, folder, output_file):
def gem_to_model_cli(gem_file, name, folder, output_file):
"""Translate an IES GEM file to a HBJSON model.
\b
Args:
gem-file: Full path to an IES VE GEM file.
"""
try:
model = model_from_ies(gem_file)
if folder is not None and name is not None:
folder = pathlib.Path(folder)
folder.mkdir(parents=True, exist_ok=True)
model.to_hbjson(name=name, folder=folder.as_posix())
else:
output_file.write(json.dumps(model.to_dict()))
low_name = name.lower()
if not low_name.endswith('.hbjson') and not low_name.endswith('.json'):
name = name + '.hbjson'
output_file = os.path.join(folder, name)
gem_to_model(gem_file, output_file)
except Exception as e:
_logger.exception('Model translation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


def gem_to_model(gem_file, output_file=None):
"""Translate an IES GEM file to a HBJSON model.
Args:
gem_file: Full path to an IES VE GEM file.
output_file: Optional HBJSON file path to output the JSON string of the
translation. If None, the string will be returned from this function.
"""
model = model_from_ies(gem_file)
content_str = json.dumps(model.to_dict())
return _process_content_to_output(content_str, output_file)


def _process_content_to_output(content_str, output_file):
"""Process content strings for various types of output_files."""
if output_file is None:
return content_str
elif isinstance(output_file, str):
if not os.path.isdir(os.path.dirname(output_file)):
os.makedirs(os.path.dirname(output_file))
with open(output_file, 'w') as of:
of.write(content_str)
else:
if 'stdout' not in str(output_file):
if not os.path.isdir(os.path.dirname(output_file.name)):
os.makedirs(os.path.dirname(output_file.name))
output_file.write(content_str)
1 change: 1 addition & 0 deletions honeybee_ies/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_ies/properties/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# coding=utf-8
"""Model IES Properties."""


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

def __init__(self, host):
"""Initialize ModelIESProperties."""
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 IES simulation.
This process includes all relevant honeybee-core checks as well as checks
that apply only for IES.
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_rooms_solid(tol, ang_tol, False, detailed))

# perform checks related to adjacency relationships
msgs.append(self.host.check_room_volume_collisions(tol, False, detailed))
msgs.append(self.host.check_all_air_boundaries_adjacent(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 IES properties as a dictionary."""
return {'ies': {'type': 'ModelIESProperties'}}

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

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

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

from ladybug.futil import nukedir

from honeybee_ies.cli.translate import model_to_gem_file
from honeybee_ies.cli.translate import model_to_gem_cli


def test_model_to_gem():
Expand All @@ -14,8 +14,9 @@ def test_model_to_gem():
name = 'cli_test_45'

result = runner.invoke(
model_to_gem_file, [input_hb_model, '--folder', folder, '--name', name]
model_to_gem_cli, [input_hb_model, '--folder', folder, '--name', name]
)
print(result.output)
assert result.exit_code == 0
assert os.path.isfile(os.path.join(folder, f'{name}.gem'))
nukedir(folder, True)
10 changes: 5 additions & 5 deletions tests/extend_test.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import pathlib
from honeybee.model import Model
from honeybee_ies.writer import model_to_gem


def test_model():
# serialize the model
in_file = './tests/assets/sample_model_45.hbjson'
out_folder = pathlib.Path('./tests/assets/temp')
out_folder.mkdir(parents=True, exist_ok=True)
model = Model.from_hbjson(in_file)
outf = model.to_gem(out_folder.as_posix(), name='sample_model_45')
assert outf.exists()
# check that the model can be translated to GEM
gem_str = model_to_gem(model)
assert isinstance(gem_str, str)

0 comments on commit ccb8d89

Please sign in to comment.