Source code for honeybee_doe2.util

# coding=utf-8
"""Various utilities used throughout the package."""
from __future__ import division

import os
import re


[docs] def generate_inp_string(u_name, command, keywords, values): """Get an INP string representation of a DOE-2 object. This method is written in a generic way so that it can describe practically any element of the INP Building Description Language (BDL). Args: u_name: Text for the unique, user-specified name of the object being created. This must be 32 characters or less and not contain special or non-ASCII characters. The clean_doe2_string method may be used to convert strings to a format that is acceptable here. For example, a U-Name of a space might be "Floor2W ClosedOffice5". command: Text indicating the type of instruction that the DOE-2 object executes. Commands are typically in capital letters and examples include POLYGON, FLOOR, SPACE, EXTERIOR-WALL, WINDOW, CONSTRUCTION, etc. keywords: A list of text with the same length as the values that denote the attributes of the DOE-2 object. values: A list of values with the same length as the keywords that describe the values of the attributes for the object. Returns: inp_str -- A DOE-2 INP string representing a single object. """ space_count = tuple((25 - len(str(n))) for n in keywords) spc = tuple(s_c * ' ' if s_c > 0 else ' ' for s_c in space_count) body_str = '\n'.join(' {}{}= {}'.format(kwd, s, val) for kwd, s, val in zip(keywords, spc, values)) inp_str = '"{}" = {}\n{}\n ..\n'.format(u_name, command, body_str) return inp_str
[docs] def generate_inp_string_list_format(u_name, command, keywords, values): """Get an INP string of a DOE-2 object with nicer formatting for list values. This method will process any values that are a list or tuple and format them such that they are indented and more readable. This method is written in a generic way so that it can describe practically any element of the INP Building Description Language (BDL). Args: u_name: Text for the unique, user-specified name of the object being created. This must be 32 characters or less and not contain special or non-ASCII characters. The clean_doe2_string method may be used to convert strings to a format that is acceptable here. For example, a U-Name of a space might be "Floor2W ClosedOffice5". command: Text indicating the type of instruction that the DOE-2 object executes. Commands are typically in capital letters and examples include POLYGON, FLOOR, SPACE, EXTERIOR-WALL, WINDOW, CONSTRUCTION, etc. keywords: A list of text with the same length as the values that denote the attributes of the DOE-2 object. values: A list of values with the same length as the keywords that describe the values of the attributes for the object. The items in this list can be list themselves, in which case they will be translated with nice indented formatting. Returns: inp_str -- A DOE-2 INP string representing a single object. """ space_count = tuple((25 - len(str(n))) for n in keywords) spc = tuple(s_c * ' ' if s_c > 0 else ' ' for s_c in space_count) body_strs = [] for kwd, s, val in zip(keywords, spc, values): if isinstance(val, (list, tuple)): body_strs.append(' {}{}= ('.format(kwd, s)) for v in val: body_strs.append(' {},'.format(v)) body_strs.append(' )') else: body_strs.append(' {}{}= {}'.format(kwd, s, val)) body_str = '\n'.join(body_strs) inp_str = '"{}" = {}\n{}\n ..\n'.format(u_name, command, body_str) return inp_str
[docs] def parse_inp_string(inp_string): """Parse an INP string of a single DOE-2 object into a list of values. Note that this method is only equipped to parse DOE-2 test strings that originate from eQuest or from this package. It has not yet been generalized to parse all formats of text formats that can appear in a DOE-2 file. Args: inp_string: An INP string for a single DOE-2 object. Returns: A tuple with four elements. - u_name: Text for the unique name of the object. Will be None if the object is a Parameter. - command: Text for the type of instruction that the DOE-2 object executes. - keywords: A list of text with the same length as the values that denote the attributes of the DOE-2 object. - values: A list of values with the same length as the keywords that describe the values of the attributes for the object. """ inp_string = inp_string.strip() inp_strings = inp_string.split('..') assert len(inp_strings) > 1, 'Input inp_string is not an INP object.' assert len(inp_strings) == 2, 'Received more than one object in inp_string.' inp_string = re.sub(r'\$.*\n', '\n', inp_strings[0]) # remove all comments if inp_string.startswith("PARAMETER"): lines = inp_string.splitlines() if len(lines) < 2: raise ValueError('Invalid parameter block: {}'.format(inp_string)) param_line = lines[1].strip() if '=' in param_line: key, val = [s.strip().replace('"', '') for s in param_line.split('=', 1)] return None, "PARAMETER", [key], [val] else: raise ValueError( 'Global parameter missing "=" assignment: {}'.format(inp_string)) doe2_fields = [e_str.strip() for e_str in inp_string.split('=')] u_name = doe2_fields.pop(0).replace('"', '') split_field_1 = doe2_fields[0].split('\n') command = split_field_1[0].strip() if len(split_field_1) == 1: # Occurs when the object does not have any keywords return u_name, command, None, None keywords = [split_field_1[1].strip()] values = [] for field in doe2_fields[1:]: split_field = [f.strip() for f in field.split('\n')] if len(split_field) == 1: values.append(split_field[0]) elif len(split_field) == 2 and not split_field[0].endswith(','): values.append(split_field[0]) keywords.append(split_field[1]) else: v_lines, end_val = [], False for row in split_field: if row.endswith(',') or row.endswith('('): v_lines.append(row) elif not end_val: v_lines.append(row) end_val = True else: keywords.append(row) values.append(' '.join(v_lines)) return u_name, command, keywords, values
[docs] def calculate_value_with_global_parameter(global_parameters, input_expr): """Evaluate an INP Global Parameter expression with #PA("..."). Note that this method will only return a float value if the expression is strictly an inline math expression. Args: global_parameters: A dict with parameter names as keys and floats as values. input_expr: A string of an expression to be evaluated. See an example below. Returns: Evaluated float result or None is not an inline math expression. .. code-block:: python '{0.66 * #PA("...")}' or '{0.66 * #PA("...") + #PA("...")} """ # TODO: Add the ability to evaluate SWITCH and IF statements using GlobaL Parameters try: # Remove curly braces expr = input_expr.strip() if expr.startswith('{') and expr.endswith('}'): expr = expr[1:-1].strip() matches = re.findall(r'#PA\(\s*"([^"]+)"\s*\)', expr) # replace each match in case of multiple #PA expressions for param_name in matches: if param_name in global_parameters: param_val = str(global_parameters[param_name]) expr = expr.replace('#PA("{}")'.format(param_name), param_val) return eval(expr, {"__builtins__": None}, {}) except Exception: return None
[docs] def doe2_object_blocks(inp_file_contents): """Get the object blocks of a DOE-2 INP file. Args: inp_file_contents: A string of the INP file to parse. Returns: A list of strings, where each string is a complete block of the INP file. """ blocks, buffer = [], [] ignore_blocks = ['INPUT', 'TITLE', 'END', 'COMPUTE', 'STOP'] for line in inp_file_contents.splitlines(): buffer.append(line) if line.strip().endswith(".."): if not any(buffer[0].strip().startswith(b) for b in ignore_blocks): blocks.append('\n'.join(buffer)) buffer = [] else: buffer = [] continue if buffer: blocks.append('\n'.join(buffer)) return blocks
[docs] def clean_inp_file_contents(inp_file_contents): """Clean the contents of an INP file by removing comment lines. Global parameter lines are also solved if they are inline math expressions. Args: inp_file_contents: The file contents of an INP file as a text string. Returns: The input text string cleaned of comments and with global parameters solved. """ # split the lines of the file lines = inp_file_contents.splitlines(keepends=True) global_parameters = {} file_lines = [] # loop through the lines and pull out any global parameters i = 0 while i < len(lines): line = lines[i].strip() if line == "PARAMETER": param_block = [lines[i]] i += 1 while i < len(lines): param_block.append(lines[i]) if lines[i].strip().endswith(".."): break i += 1 full_param = ''.join(param_block) _, _, keys, vals = parse_inp_string(full_param) # Save the parameter to solve the lines later global_parameters[keys[0]] = vals[0] file_lines.extend(param_block) i += 1 continue # try and replace/remove the global parameters expressions if '#PA' in line: matches = re.finditer(r'{([^}]*#PA\(".*?"\)[^}]*)}', line) for m in matches: expr = m.group(1) replacement = calculate_value_with_global_parameter(global_parameters, expr) if replacement is not None: line = line.replace(expr, str(replacement)) # skip comment lines if not line.startswith('$'): file_lines.append(lines[i]) i += 1 return ''.join(file_lines)
[docs] def header_comment_minor(header_text): """Create a header given header_text, which can help organize the INP file contents. """ return \ '$ ---------------------------------------------------------\n' \ '$ {}\n' \ '$ ---------------------------------------------------------\n'\ '\n'.format(header_text)
[docs] def header_comment_major(header_text): """Create a header given header_text, which can help organize the INP file contents. """ return \ '$ *********************************************************\n' \ '$ ** **\n' \ '$ {}\n' \ '$ ** **\n' \ '$ *********************************************************\n'\ '\n'.format(header_text)
[docs] def switch_statement_id(value): """Convert a string into a 4-character ID that can be used for switch statements. This is needed to deal with the major limitations that DOE-2 places on switch statement IDs, where every ID must be 4 characters """ # first remove dangerous characters val = re.sub(r'[^.A-Za-z0-9:]', '', value) # remove all unusable characters val = val.replace(' ', '').replace('_', '') # remove spaces and underscores # the user has formatted their program id specifically for switch statements if len(val) <= 4: return val # remove lower-case vowels for readability val = re.sub(r'[aeiouy_\-]', '', val) if '::' in val: # program id originating from openstudio-standards val = val.split('::')[-1] if len(val) >= 4: return val[:4] # some special user-generated program id val = val.replace(':', '') if len(val) >= 4: return val[-4:] return val
[docs] def inp_path_from_folder(folder_path=None, filename=None): """Get the path to an INP file from a specified folder or the current directory. Args: folder_path: Path to the folder containing the INP file. If None, the current directory will be used. filename: Name of the INP file. If None, the first file found in the folder with an .inp extension will be returned Returns: Path to the INP file. Raises: FileNotFoundError: If no INP file is found. """ # process the folder path input if folder_path is None: folder_path = os.getcwd() else: assert os.path.isdir(folder_path), \ 'No directory was found at: {}'.format(folder_path) # process the filename if filename is None: # Look for any .inp file in the folder found_files = [] for f_name in os.listdir(folder_path): if f_name.lower().endswith('*.inp'): found_files.append(os.path.join(folder_path, f_name)) if len(found_files) == 0: raise FileNotFoundError('No .inp files found in {}'.format(folder_path)) elif len(found_files) > 1: # If multiple INP files, use the first one (you might want to be more specific) print('Multiple .inp files found, using: {}'.format(found_files[0])) return found_files[0] else: # Use the specified filename inp_path = os.path.join(folder_path, filename) assert os.path.isfile(inp_path), \ 'No INP file was found at: {}'.format(inp_path) return inp_path