diff --git a/honeybee/cli/validate.py b/honeybee/cli/validate.py index c1a5798f..e00f4533 100644 --- a/honeybee/cli/validate.py +++ b/honeybee/cli/validate.py @@ -1,11 +1,9 @@ """honeybee validation commands.""" import sys import logging -import json as py_json import click from honeybee.model import Model -from honeybee.config import folders _logger = logging.getLogger(__name__) @@ -19,9 +17,13 @@ def validate(): @click.argument('model-file', type=click.Path( exists=True, file_okay=True, dir_okay=False, resolve_path=True)) @click.option( - '--check-all/--room-overlaps', ' /-ro', help='Flag to note whether the output ' - 'validation report should validate all possible issues with the model or only ' - 'the Room collisions should be checked.', default=True, show_default=True) + '--extension', '-e', help='Text for the name of the extension to be checked. ' + 'The value input is case-insensitive such that "radiance" and "Radiance" will ' + 'both result in the model being checked for validity with honeybee-radiance. ' + 'This value can also be set to "All" in order to run checks for all installed ' + 'extensions. Some common honeybee extension names that can be input here include: ' + 'Radiance, EnergyPlus, DOE2, IES, IDAICE', + type=str, default='All', show_default=True) @click.option( '--plain-text/--json', ' /-j', help='Flag to note whether the output validation ' 'report should be formatted as a JSON object instead of plain text. If set to JSON, ' @@ -37,7 +39,7 @@ def validate(): '--output-file', '-f', help='Optional file to output the full report ' 'of the validation. By default it will be printed out to stdout', type=click.File('w'), default='-') -def validate_model_cli(model_file, check_all, plain_text, output_file): +def validate_model_cli(model_file, extension, plain_text, output_file): """Validate all properties of a Model file against Honeybee schema. This includes checking basic compliance with the 5 rules of honeybee geometry @@ -62,8 +64,7 @@ def validate_model_cli(model_file, check_all, plain_text, output_file): """ try: json = not plain_text - room_overlaps = not check_all - validate_model(model_file, room_overlaps, json, output_file) + validate_model(model_file, extension, json, output_file) except Exception as e: _logger.exception('Model validation failed.\n{}'.format(e)) sys.exit(1) @@ -71,8 +72,8 @@ def validate_model_cli(model_file, check_all, plain_text, output_file): sys.exit(0) -def validate_model(model_file, room_overlaps=False, json=False, output_file=None, - check_all=True, plain_text=True): +def validate_model(model_file, extension='All', json=False, output_file=None, + plain_text=True): """Validate all properties of a Model file against the Honeybee schema. This includes checking basic compliance with the 5 rules of honeybee geometry @@ -80,86 +81,122 @@ def validate_model(model_file, room_overlaps=False, json=False, output_file=None Args: model_file: Full path to a Honeybee Model file. - room_overlaps: Boolean to note whether the output validation report - should only validate the Room collisions (True) or all possible - issues with the model should be checked (False). (Default: False). + extension_name: Text for the name of the extension to be checked. + The value input here is case-insensitive such that "radiance" + and "Radiance" will both result in the model being checked for + validity with honeybee-radiance. This value can also be set to + "All" in order to run checks for all installed extensions. Some + common honeybee extension names that can be input here if they + are installed include: + + * Radiance + * EnergyPlus + * DOE2 + * IES + * IDAICE + json: Boolean to note whether the output validation report should be - formatted as a JSON object instead of plain text. - output_file: Optional file to output the string of the visualization - file contents. If None, the string will simply be returned from - this method. - """ - if not json: - # re-serialize the Model to make sure no errors are found - c_ver = folders.honeybee_core_version_str - s_ver = folders.honeybee_schema_version_str - ver_msg = 'Validating Model using honeybee-core=={} and ' \ - 'honeybee-schema=={}'.format(c_ver, s_ver) - print(ver_msg) - parsed_model = Model.from_file(model_file) - print('Re-serialization passed.') - # perform several other checks for geometry rules and others - if not room_overlaps: - report = parsed_model.check_all(raise_exception=False, detailed=False) - else: - report = parsed_model.check_room_volume_collisions(raise_exception=False) - print('Model checks completed.') - # check the report and write the summary of errors - if report == '': - full_msg = ver_msg + '\nCongratulations! Your Model is valid!' - else: - full_msg = ver_msg + \ - '\nYour Model is invalid for the following reasons:\n' + report - if output_file is None: - return full_msg - else: - output_file.write(full_msg) + formatted as a JSON object instead of plain text. (Default: False). + output_file: Optional file to output the full report of the validation. + If None, the string will simply be returned from this method. + """ + report = Model.validate(model_file, 'check_for_extension', [extension], json) + if output_file is None: + return report else: - out_dict = { - 'type': 'ValidationReport', - 'app_name': 'Honeybee', - 'app_version': folders.honeybee_core_version_str, - 'schema_version': folders.honeybee_schema_version_str - } - try: - parsed_model = Model.from_file(model_file) - out_dict['fatal_error'] = '' - if not room_overlaps: - errors = parsed_model.check_all(raise_exception=False, detailed=True) - else: - errors = parsed_model.check_room_volume_collisions( - raise_exception=False, detailed=True) - out_dict['errors'] = errors - out_dict['valid'] = True if len(out_dict['errors']) == 0 else False - except Exception as e: - out_dict['fatal_error'] = str(e) - out_dict['errors'] = [] - out_dict['valid'] = False - if output_file is None: - return py_json.dumps(out_dict, indent=4) - else: - output_file.write(py_json.dumps(out_dict, indent=4)) - - -@validate.command('room-volumes') + output_file.write(report) + + +@validate.command('rooms-solid') @click.argument('model-file', type=click.Path( exists=True, file_okay=True, dir_okay=False, resolve_path=True)) @click.option( - '--output-file', '-f', help='Optional file to output the JSON strings of ' - 'ladybug_geometry LineSegment3Ds that represent naked and non-manifold edges. ' - 'By default it will be printed out to stdout', type=click.File('w'), default='-') -def validate_room_volumes_cli(model_file, output_file): + '--plain-text/--json', ' /-j', help='Flag to note whether the output validation ' + 'report should be formatted as a JSON object instead of plain text. If set to JSON, ' + 'the output object will contain several attributes. An attribute called ' + '"fatal_error" is a text string containing an exception if the Model failed to ' + 'serialize and will be an empty string if serialization was successful. An ' + 'attribute called "errors" will contain a list of JSON objects for each ' + 'invalid issue. A boolean attribute called "valid" will note whether the Model ' + 'is valid or not.', + default=True, show_default=True) +@click.option( + '--output-file', '-f', help='Optional file to output the full report ' + 'of the validation. By default it will be printed out to stdout.', + type=click.File('w'), default='-') +def validate_rooms_solid_cli(model_file, plain_text, output_file): + """Validate whether all Room volumes in a model are solid. + + The returned result can include a list of all naked and non-manifold edges + preventing closed room volumes when --json is used. This is helpful for visually + identifying issues in geometry that are preventing the room volume from + validating as closed. + + \b + Args: + model_file: Full path to a Honeybee Model file. + """ + try: + json = not plain_text + validate_rooms_solid(model_file, json, output_file) + except Exception as e: + _logger.exception('Model room volume validation failed.\n{}'.format(e)) + sys.exit(1) + else: + sys.exit(0) + + +def validate_rooms_solid(model_file, json=False, output_file=None, plain_text=True): """Get a list of all naked and non-manifold edges preventing closed room volumes. This is helpful for visually identifying issues in geometry that are preventing the room volume from reading as closed. + Args: + model_file: Full path to a Honeybee Model file. + json: Boolean to note whether the output validation report should be + formatted as a JSON object instead of plain text. (Default: False). + output_file: Optional file to output the full report of the validation. + If None, the string will simply be returned from this method. + """ + report = Model.validate(model_file, 'check_rooms_solid', json_output=json) + if output_file is None: + return report + else: + output_file.write(report) + + +@validate.command('room-collisions') +@click.argument('model-file', type=click.Path( + exists=True, file_okay=True, dir_okay=False, resolve_path=True)) +@click.option( + '--plain-text/--json', ' /-j', help='Flag to note whether the output validation ' + 'report should be formatted as a JSON object instead of plain text. If set to JSON, ' + 'the output object will contain several attributes. An attribute called ' + '"fatal_error" is a text string containing an exception if the Model failed to ' + 'serialize and will be an empty string if serialization was successful. An ' + 'attribute called "errors" will contain a list of JSON objects for each ' + 'invalid issue. A boolean attribute called "valid" will note whether the Model ' + 'is valid or not.', default=True, show_default=True) +@click.option( + '--output-file', '-f', help='Optional file to output the full report ' + 'of the validation. By default it will be printed out to stdout.', + type=click.File('w'), default='-') +def validate_room_collisions_cli(model_file, plain_text, output_file): + """Validate whether all Room volumes in a model are solid. + + The returned result can include a list of all naked and non-manifold edges + preventing closed room volumes when --json is used. This is helpful for visually + identifying issues in geometry that are preventing the room volume from + validating as closed. + \b Args: model_file: Full path to a Honeybee Model file. """ try: - validate_room_volumes(model_file, output_file) + json = not plain_text + validate_room_collisions(model_file, json, output_file) except Exception as e: _logger.exception('Model room volume validation failed.\n{}'.format(e)) sys.exit(1) @@ -167,7 +204,7 @@ def validate_room_volumes_cli(model_file, output_file): sys.exit(0) -def validate_room_volumes(model_file, output_file=None): +def validate_room_collisions(model_file, json=False, output_file=None, plain_text=True): """Get a list of all naked and non-manifold edges preventing closed room volumes. This is helpful for visually identifying issues in geometry that are preventing @@ -175,20 +212,13 @@ def validate_room_volumes(model_file, output_file=None): Args: model_file: Full path to a Honeybee Model file. - output_file: Optional file to output the string of the visualization - file contents. If None, the string will simply be returned from - this method. + json: Boolean to note whether the output validation report should be + formatted as a JSON object instead of plain text. (Default: False). + output_file: Optional file to output the full report of the validation. + If None, the string will simply be returned from this method. """ - # re-serialize the Model and collect all naked and non-manifold edges - parsed_model = Model.from_file(model_file) - problem_edges = [] - for room in parsed_model.rooms: - if not room.geometry.is_solid: - problem_edges.extend(room.geometry.naked_edges) - problem_edges.extend(room.geometry.non_manifold_edges) - # write the new model out to the file or stdout - prob_array = [lin.to_dict() for lin in problem_edges] + report = Model.validate(model_file, 'check_room_volume_collisions', json_output=json) if output_file is None: - return py_json.dumps(prob_array) + return report else: - output_file.write(py_json.dumps(prob_array)) + output_file.write(report) diff --git a/honeybee/model.py b/honeybee/model.py index dbc632b5..bdc8f84c 100644 --- a/honeybee/model.py +++ b/honeybee/model.py @@ -3396,6 +3396,88 @@ def _all_objects(self): return self._rooms + self._orphaned_faces + self._orphaned_shades + \ self._orphaned_apertures + self._orphaned_doors + self._shade_meshes + @staticmethod + def validate(model, check_function='check_for_extension', check_args=None, + json_output=False): + """Get a string of a validation report given a specific check_function. + + Args: + model: A Honeybee Model object for which validation will be performed. + This can also be the file path to a HBJSON or a JSON string + representation of a Honeybee Model. These latter two options may + be useful if the type of validation issue with the Model is + one that prevents serialization. + check_function: Text for the name of a check function on this Model + that will be used to generate the validation report. For example, + check_all or check_rooms_solid. (Default: check_for_extension), + check_args: An optional list of arguments to be passed to the + check_function. If None, all default values for the arguments + will be used. (Default: None). + json_output: Boolean to note whether the output validation report + should be formatted as a JSON object instead of plain text. + """ + # first get the function to call on this class + check_func = getattr(Model, check_function, None) + assert check_func is not None, \ + 'Honeybee Model class has no method {}'.format(check_function) + + # process the input model if it's not already serialized + report = '' + if isinstance(model, str): + try: + if model.startswith('{'): + model = Model.from_dict(json.loads(model)) + elif os.path.isfile(model): + model = Model.from_file(model) + else: + report = 'Input Model for validation is not a Model object, ' \ + 'file path to a Model or a Model HBJSON string.' + except Exception as e: + report = str(e) + elif not isinstance(model, Model): + report = 'Input Model for validation is not a Model object, ' \ + 'file path to a Model or a Model HBJSON string.' + # process the arguments and options + args = [model] if check_args is None else [model] + list(check_args) + kwargs = {'raise_exception': False} + + # create the report + if not json_output: # create a plain text report + # add the versions of things into the validation message + c_ver = folders.honeybee_core_version_str + s_ver = folders.honeybee_schema_version_str + ver_msg = 'Validating Model using honeybee-core=={} and ' \ + 'honeybee-schema=={}'.format(c_ver, s_ver) + # run the check function + if isinstance(args[0], Model): + kwargs['detailed'] = False + report = check_func(*args, **kwargs) + # format the results of the check + if report == '': + full_msg = ver_msg + '\nCongratulations! Your Model is valid!' + else: + full_msg = ver_msg + \ + '\nYour Model is invalid for the following reasons:\n' + report + return full_msg + else: + # add the versions of things into the validation message + out_dict = { + 'type': 'ValidationReport', + 'app_name': 'Honeybee', + 'app_version': folders.honeybee_core_version_str, + 'schema_version': folders.honeybee_schema_version_str, + 'fatal_error': report + } + if report == '': + kwargs['detailed'] = True + errors = check_func(*args, **kwargs) + out_dict['errors'] = errors + out_dict['valid'] = True if len(out_dict['errors']) == 0 else False + else: + out_dict['errors'] = [] + out_dict['valid'] = False + return json.dumps(out_dict, indent=4) + @staticmethod def conversion_factor_to_meters(units): """Get the conversion factor to meters based on input units. diff --git a/honeybee/properties.py b/honeybee/properties.py index aca69d49..bc6b6a55 100644 --- a/honeybee/properties.py +++ b/honeybee/properties.py @@ -410,29 +410,30 @@ def _check_for_extension(self, extension_name, detailed=False): """ msgs = [] for atr in self._extension_attributes: - check_msg = None - try: - var = getattr(self, atr) - except AttributeError as e: - raise ImportError( - 'Extension for {} is not installed or has not been set up ' - 'for model validation.\n{}'.format(var, e)) - if not hasattr(var, 'check_for_extension'): - raise NotImplementedError( - 'Extension for {} does not have validation routines.'.format(var)) - try: - check_msg = var.check_for_extension( - raise_exception=False, detailed=detailed) - if detailed and check_msg is not None: - msgs.append(check_msg) - elif check_msg != '': - f_msg = 'Attributes for {} are invalid.\n{}'.format(atr, check_msg) - msgs.append(f_msg) - except Exception as e: - import traceback - traceback.print_exc() - raise Exception('Failed to check_for_extension ' - 'for {}: {}'.format(var, e)) + if extension_name == atr: + check_msg = None + try: + var = getattr(self, atr) + except AttributeError as e: + raise ImportError( + 'Extension for {} is not installed or has not been set up ' + 'for model validation.\n{}'.format(var, e)) + if not hasattr(var, 'check_for_extension'): + raise NotImplementedError( + 'Extension for {} does not have validation routines.'.format(var)) + try: + check_msg = var.check_for_extension( + raise_exception=False, detailed=detailed) + if detailed and check_msg is not None: + msgs.append(check_msg) + elif check_msg != '': + f_msg = 'Attributes for {} are invalid.\n{}'.format(atr, check_msg) + msgs.append(f_msg) + except Exception as e: + import traceback + traceback.print_exc() + raise Exception('Failed to check_for_extension ' + 'for {}: {}'.format(var, e)) return msgs def _check_all_extension_attr(self, detailed=False):