diff --git a/examples/RS0002/Unitary-Constant-Efficiency.RS0002.a205.json b/examples/RS0002/Unitary-Constant-Efficiency.RS0002.a205.json index 09f44ba..e0938ad 100644 --- a/examples/RS0002/Unitary-Constant-Efficiency.RS0002.a205.json +++ b/examples/RS0002/Unitary-Constant-Efficiency.RS0002.a205.json @@ -43,8 +43,9 @@ } }, "performance": { + "performance_capabilities": ["COOLING"], "compressor_speed_control_type": "DISCRETE", - "cycling_degradation_coefficient": 0.25, + "cooling_cycling_degradation_coefficient": 0.25, "performance_map_cooling": { "grid_variables": { "outdoor_coil_entering_dry_bulb_temperature": [ diff --git a/examples/RS0002/residential-unitary.RS0002.json b/examples/RS0002/residential-unitary.RS0002.json index fdaab7e..395d3eb 100644 --- a/examples/RS0002/residential-unitary.RS0002.json +++ b/examples/RS0002/residential-unitary.RS0002.json @@ -296,6 +296,7 @@ "disclaimer": "This data is synthetic and does not represent any physical products." }, "performance": { + "performance_capabilities": ["COOLING"], "compressor_speed_control_type": "DISCRETE", "cycling_degradation_coefficient": 0.1, "performance_map_cooling": { @@ -8555,4 +8556,4 @@ } } } -} \ No newline at end of file +} diff --git a/examples/RS0004/DX-Constant-Efficiency.RS0004.a205.json b/examples/RS0004/DX-Constant-Efficiency.RS0004.a205.json index 6a09efa..b445078 100644 --- a/examples/RS0004/DX-Constant-Efficiency.RS0004.a205.json +++ b/examples/RS0004/DX-Constant-Efficiency.RS0004.a205.json @@ -21,8 +21,9 @@ } }, "performance": { + "performance_capabilities": ["COOLING"], "compressor_speed_control_type": "DISCRETE", - "cycling_degradation_coefficient": 0.25, + "cooling_cycling_degradation_coefficient": 0.25, "performance_map_cooling": { "grid_variables": { "outdoor_coil_entering_dry_bulb_temperature": [ diff --git a/examples/RS0004/HPDM.RS0004.a205.json b/examples/RS0004/HPDM.RS0004.a205.json index 244bc3b..a03f4ca 100644 --- a/examples/RS0004/HPDM.RS0004.a205.json +++ b/examples/RS0004/HPDM.RS0004.a205.json @@ -22,8 +22,9 @@ } }, "performance": { + "performance_capabilities": ["COOLING"], "compressor_speed_control_type": "DISCRETE", - "cycling_degradation_coefficient": 0.1, + "cooling_cycling_degradation_coefficient": 0.1, "performance_map_cooling": { "grid_variables": { "outdoor_coil_entering_dry_bulb_temperature": [ diff --git a/examples/RS0004/residential-dx.RS0004.json b/examples/RS0004/residential-dx.RS0004.json index 8c341c0..231d5e3 100644 --- a/examples/RS0004/residential-dx.RS0004.json +++ b/examples/RS0004/residential-dx.RS0004.json @@ -11,6 +11,7 @@ "disclaimer": "This data is synthetic and does not represent any physical products." }, "performance": { + "performance_capabilities": ["COOLING"], "compressor_speed_control_type": "DISCRETE", "cycling_degradation_coefficient": 0.1, "performance_map_cooling": { diff --git a/meta-schema/meta.schema.json b/meta-schema/meta.schema.json index 355880c..3aec5f8 100644 --- a/meta-schema/meta.schema.json +++ b/meta-schema/meta.schema.json @@ -121,7 +121,7 @@ }, "ConditionallyRequiredPattern": { "type": "string", - "pattern": "if (([a-z]+)(_([a-z]|[0-9])+)*)(!?=((([-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?))|(\".*\")|(([A-Z]([A-Z]|[0-9])*)(_([A-Z]|[0-9])+)*)|(True|False)))?" + "pattern": "if (\\.\\.)?(([a-z]+)(_([a-z]|[0-9])+)*)(!?=((([-+]?[0-9]*\\.?[0-9]+([eE][-+]?[0-9]+)?))|(\".*\")|(([A-Z]([A-Z]|[0-9])*)(_([A-Z]|[0-9])+)*)|(True|False)))?" }, "Required": { "oneOf": [ @@ -273,7 +273,7 @@ }, "Scalable": { "$ref": "meta.schema.json#/definitions/Scalable" - }, + }, "Notes": { "$ref": "meta.schema.json#/definitions/Notes" }, @@ -340,7 +340,7 @@ }, "Scalable": { "$ref": "meta.schema.json#/definitions/Scalable" - }, + }, "Notes": { "$ref": "meta.schema.json#/definitions/Notes" } @@ -379,7 +379,7 @@ }, "Scalable": { "$ref": "meta.schema.json#/definitions/Scalable" - }, + }, "Notes": { "$ref": "meta.schema.json#/definitions/Notes" } @@ -618,4 +618,4 @@ } }, "additionalProperties": false -} \ No newline at end of file +} diff --git a/schema-source/ASHRAE205.schema.yaml b/schema-source/ASHRAE205.schema.yaml index c4f7a1a..45e9751 100644 --- a/schema-source/ASHRAE205.schema.yaml +++ b/schema-source/ASHRAE205.schema.yaml @@ -192,6 +192,24 @@ OperationState: Description: "Indicates that the equipment is in standby operating state" Display Text: "Standby" +PerformanceCapabilities: + Object Type: "Enumeration" + Enumerators: + COOLING: + Description: "Indicates that the equipment provides explicitly controlled cooling and the representation contains cooling-related performance data" + Display Text: "Cooling" + HEATING: + Description: "Indicates that the equipment provides explicitly controlled heating and the representation contains heating-related performance data" + Display Text: "Heating" + # TODO: Reserved for future use + # DEHUMIDIFICATION: + # Description: "Indicates that the equipment provides explicitly controlled dehumidification and the representation contains dehumidification-related performance data" + # Display Text: "Dehumidification" + # HUMIDIFICATION: + # Description: "Indicates that the equipment provides explicitly controlled humidification and the representation contains humidification-related performance data" + # Display Text: "Humidification" + + #Common Data Groups Metadata: Object Type: "Data Group" diff --git a/schema-source/RS0004.schema.yaml b/schema-source/RS0004.schema.yaml index 1956ede..af493d1 100644 --- a/schema-source/RS0004.schema.yaml +++ b/schema-source/RS0004.schema.yaml @@ -60,18 +60,29 @@ ProductInformation: Performance: Object Type: "Data Group" Data Elements: + performance_capabilities: + Description: "An array of unique operating modes that indicate the capabilities of the equipment" + Data Type: "[]" + # TODO: Constraints: unique https://json-schema.org/understanding-json-schema/reference/array#uniqueItems + Required: True compressor_speed_control_type: Description: "Method used to control different speeds of the compressor" Data Type: "" Required: True - cycling_degradation_coefficient: - Description: "Cycling degradation coefficient (C~D~) as described in AHRI 210/240" + cooling_cycling_degradation_coefficient: + Description: "Cooling cycling degradation coefficient (C~D~^c^)" Data Type: "Numeric" Units: "-" - Constraints: [">=0.0","<1.0"] - Required: True - Notes: ["Used for the lowest stage when the unit cycles to meet load", - "***Informative note:*** 340/360 specifies a fixed cycling degradation coefficient of approximately 0.12"] + Constraints: [">=0.0", "<1.0"] + Required: "if performance_capabilities contains(COOLING)" + Notes: ["Used for the lowest stage when the unit cycles to meet load"] + heating_cycling_degradation_coefficient: + Description: "Heating cycling degradation coefficient (C~D~^h^)" + Data Type: "Numeric" + Units: "-" + Constraints: [">=0.0", "<1.0"] + Required: "if performance_capabilities contains(HEATING)" + Notes: ["Used for the lowest stage when the unit cycles to meet load"] scaling: Description: "Specifies the range the performance data can be scaled to represent different capacity equipment" Data Type: "{Scaling}" @@ -79,7 +90,15 @@ Performance: performance_map_cooling: Description: "Data group describing cooling performance over a range of conditions" Data Type: "{PerformanceMapCooling}" - Required: True + Required: "if performance_capabilities contains(COOLING)" + performance_map_heating: + Description: "Data group describing heating performance over a range of conditions" + Data Type: "{PerformanceMapHeating}" + Required: "if performance_capabilities contains(HEATING)" + performance_map_defrost_correction: + Description: "Data group describing the impact on heating performance due to frost formation, defrost operation, or both" + Data Type: "{PerformanceMapDefrostCorrection}" + Required: "if performance_capabilities contains(HEATING)" performance_map_standby: Description: "Data group describing standby performance" Data Type: "{PerformanceMapStandby}" @@ -109,7 +128,7 @@ GridVariablesCooling: indoor_coil_entering_relative_humidity: Description: "Relative humidity of the air entering the indoor coil" Data Type: "[Numeric][1..]" - Constraints: [">=0.0","<=1.0"] + Constraints: [">=0.0", "<=1.0"] Units: "-" Notes: "As measured immediately before entering the coil (i.e., after the fan in a blow-through configuration)" Required: True @@ -123,7 +142,7 @@ GridVariablesCooling: indoor_coil_air_mass_flow_rate: Description: "Mass flow rate of air entering the indoor coil" Data Type: "[Numeric][1..]" - Constraints: ">0.0" + Constraints: ">=0.0" Units: "kg/s" Required: True Scalable: True @@ -132,8 +151,11 @@ GridVariablesCooling: Data Type: "[Integer][1..]" Constraints: ">=1" Units: "-" - Notes: ["If `compressor_speed_control_type` is `DISCRETE`, sequence numbers shall be provided for each discrete stage of the compressor(s)", - "If `compressor_speed_control_type` is `CONTINUOUS`, sufficient sequence numbers shall be provided to capture the continuous operation of the compressor(s)"] + Notes: + [ + "If `compressor_speed_control_type` is `DISCRETE`, sequence numbers shall be provided for each discrete stage of the compressor(s)", + "If `compressor_speed_control_type` is `CONTINUOUS`, sufficient sequence numbers shall be provided to capture the continuous operation of the compressor(s)", + ] Required: True ambient_absolute_air_pressure: Description: "Ambient absolute air pressure" @@ -164,18 +186,159 @@ LookupVariablesCooling: gross_power: Description: "Gross power draw (of the outdoor unit)" Data Type: "[Numeric][1..]" - Constraints: ">0.0" + Constraints: ">=0.0" Units: "W" Required: True Scalable: True - Notes: ["Includes compressor, outdoor fan, and any auxiliary power used by the unit's controls and any sump heater", - "Shall not include power drawn by the indoor fan"] + Notes: + [ + "Includes compressor, outdoor fan, and any auxiliary power used by the unit's controls and any crankcase heater", + "Shall not include power drawn by the indoor fan", + ] operation_state: Description: "The operation state at the operating conditions" Data Type: "[]" Units: "-" Required: True +PerformanceMapHeating: + Object Type: "Performance Map" + Data Elements: + grid_variables: + Description: "Data group defining the grid variables for heating performance" + Data Type: "{GridVariablesHeating}" + Required: True + lookup_variables: + Description: "Data group defining the lookup variables for heating performance" + Data Type: "{LookupVariablesHeating}" + Required: True + +GridVariablesHeating: + Object Type: "Grid Variables" + Data Elements: + outdoor_coil_entering_dry_bulb_temperature: + Description: "Dry bulb temperature of the air entering the outdoor coil" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0" + Units: "K" + Required: True + indoor_coil_entering_dry_bulb_temperature: + Description: "Dry bulb temperature of the air entering the indoor coil" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0" + Units: "K" + Required: True + Notes: "As measured immediately before entering the coil (i.e., after the fan in a blow-through configuration)" + indoor_coil_air_mass_flow_rate: + Description: "Mass flow rate of air entering the indoor coil" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0" + Units: "kg/s" + Required: True + Scalable: True + compressor_sequence_number: + Description: "Index indicating the relative capacity order of the compressor speed/stage expressed in order from lowest capacity (starting at 1) to highest capacity" + Data Type: "[Integer][1..]" + Constraints: ">=1" + Units: "-" + Notes: + [ + "If `compressor_speed_control_type` is `DISCRETE`, sequence numbers shall be provided for each discrete stage of the compressor(s)", + "If `compressor_speed_control_type` is `CONTINUOUS`, sufficient sequence numbers shall be provided to capture the continuous operation of the compressor(s)", + ] + Required: True + +LookupVariablesHeating: + Object Type: "Lookup Variables" + Data Elements: + gross_frost_free_capacity: + Description: "Rate of heat added by the indoor coil under steady state conditions with no frost present on the outdoor coil" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0" + Units: "W" + Required: True + Scalable: True + Notes: + - '***Informative note:*** Sometimes also referred to as "instantaneous" or "steady state" capacity' + - "Does not account for heat added by the fan" + + gross_frost_free_power: + Description: "Gross power draw of the outdoor unit under steady state conditions with no frost present on the outdoor coil" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0" + Units: "W" + Required: True + Scalable: True + Notes: + - '***Informative note:*** Sometimes also referred to as "instantaneous" or "steady state" power' + - "Does not include power drawn by the indoor fan" + - "Includes compressor, outdoor fan, and any auxiliary power used by the unit's controls and any crankcase heater" + operation_state: + Description: "The operation state at the operating conditions" + Data Type: "[]" + Units: "-" + Required: True + +PerformanceMapDefrostCorrection: + Object Type: "Performance Map" + Data Elements: + grid_variables: + Description: "Data group defining the grid variables for the impact on heating performance due to the effects of frost formation, defrost operation, or both" + Data Type: "{GridVariablesDefrostCorrection}" + Required: True + lookup_variables: + Description: "Data group defining the lookup variables for the impact on heating performance due to the effects of frost formation, defrost operation, or both" + Data Type: "{LookupVariablesDefrostCorrection}" + Required: True + +GridVariablesDefrostCorrection: + Object Type: "Grid Variables" + Data Elements: + outdoor_coil_entering_dry_bulb_temperature: + Description: "Dry bulb temperature of the air entering the outdoor coil" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0" + Units: "K" + Required: True + outdoor_coil_entering_relative_humidity: + Description: "Relative humidity of the air entering the outdoor coil" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0, <=1.0" + Units: "-" + Required: True + compressor_sequence_number: + Description: "Index indicating the relative capacity order of the compressor speed/stage expressed in order from lowest capacity (starting at 1) to highest capacity" + Data Type: "[Integer][1..]" + Constraints: ">=1" + Units: "-" + Notes: "Shall be consistent with the values of `compressor_sequence_number` in `GridVariablesHeating`" + Required: True + +LookupVariablesDefrostCorrection: + Object Type: "Lookup Variables" + Data Elements: + capacity_correction_factor: + Description: "Factor representing the correction to the gross frost-free heating capacity to account for impacts of frost formation, defrost operation, or both over the integrated time period" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0, <=1.0" + Units: "-" + Notes: "A value of 1.0 indicates no frost formation or defrost operation" + Required: True + power_correction_factor: + Description: "Factor representing the correction to the gross frost-free power to account for impacts of frost formation, defrost operation, or both over the integrated time period" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0" + Units: "-" + Required: True + Notes: "A factor greater than 1.0 indicates electric resistance strip heat might be applied to defrost the outdoor coil" + defrost_time_fraction: + Description: "Fraction of integrated time period when defrost operation is active" + Data Type: "[Numeric][1..]" + Constraints: ">=0.0, <=1.0" + Units: "-" + Required: True + Notes: "***Informative note:*** This value is used to inform application software when it may need to control supplemental heating during defrost" + PerformanceMapStandby: Object Type: "Performance Map" Data Elements: @@ -204,8 +367,8 @@ LookupVariablesStandby: gross_power: Description: "Gross power draw (of the outdoor unit)" Data Type: "[Numeric][1..]" - Constraints: ">0.0" + Constraints: ">=0.0" Units: "W" Required: True Scalable: True - Notes: "Includes any auxiliary power used by the unit's controls and any sump heater" + Notes: "Includes any auxiliary power used by the unit's controls and any crankcase heater" diff --git a/schema205/json_translate.py b/schema205/json_translate.py index 9108338..88140a3 100644 --- a/schema205/json_translate.py +++ b/schema205/json_translate.py @@ -4,35 +4,36 @@ from collections import OrderedDict import re + def get_extension(file): return os.path.splitext(file)[1] def load(input_file_path): - ''' Load schema file based on extension and return resulting dictionary. ''' + """Load schema file based on extension and return resulting dictionary.""" ext = get_extension(input_file_path).lower() - if (ext == '.json'): - with open(input_file_path, 'r') as input_file: + if ext == ".json": + with open(input_file_path, "r") as input_file: return json.load(input_file) - elif (ext == '.yaml') or (ext == '.yml'): - with open(input_file_path, 'r') as input_file: + elif (ext == ".yaml") or (ext == ".yml"): + with open(input_file_path, "r") as input_file: return yaml.load(input_file, Loader=yaml.FullLoader) else: - raise Exception(f"Unsupported input \"{ext}\".") + raise Exception(f'Unsupported input "{ext}".') def dump(content, output_file_path): - ''' Save schema file of dictionary content. ''' + """Save schema file of dictionary content.""" ext = get_extension(output_file_path).lower() - if (ext == '.json'): - with open(output_file_path,'w') as output_file: + if ext == ".json": + with open(output_file_path, "w") as output_file: json.dump(content, output_file, indent=4) - elif (ext == '.yaml') or (ext == '.yml'): - with open(output_file_path, 'w') as out_file: + elif (ext == ".yaml") or (ext == ".yml"): + with open(output_file_path, "w") as out_file: yaml.dump(content, out_file, sort_keys=False) else: - raise Exception(f"Unsupported output \"{ext}\".") + raise Exception(f'Unsupported output "{ext}".') def compare_dicts(original, modified, error_list): @@ -42,8 +43,16 @@ def compare_dicts(original, modified, error_list): # https://stackoverflow.com/questions/4527942/comparing-two-dictionaries-and-checking-how-many-key-value-pairs-are-equal -def dict_compare(d1, d2, errors, level=0, lineage=None, hide_value_mismatches=False, hide_key_mismatches=False): - ''' Compare two order-independent dictionaries, labeling added or deleted keys or mismatched values. ''' +def dict_compare( + d1, + d2, + errors, + level=0, + lineage=None, + hide_value_mismatches=False, + hide_key_mismatches=False, +): + """Compare two order-independent dictionaries, labeling added or deleted keys or mismatched values.""" if not lineage: lineage = list() if d1 == d2: @@ -55,17 +64,25 @@ def dict_compare(d1, d2, errors, level=0, lineage=None, hide_value_mismatches=Fa if d1_keys != d2_keys: added = [k for k in d2_keys if k not in d1_keys] removed = [k for k in d1_keys if k not in d2_keys] - err = '' + err = "" if added and not hide_key_mismatches: - errors.append(f'Keys added to second dictionary at level {level}, lineage {lineage}: {added}') + errors.append(f"Keys added to second dictionary at level {level}, lineage {lineage}: {added}") if removed and not hide_key_mismatches: - errors.append(f'Keys removed from first dictionary at level {level}, lineage {lineage}: {removed}.') + errors.append(f"Keys removed from first dictionary at level {level}, lineage {lineage}: {removed}.") return False else: - # Enter this part of the code if both dictionaries have all keys shared at this level + # Enter this part of the code if both dictionaries have all keys shared at this level shared_keys = d1_keys for k in shared_keys: - dict_compare(d1[k], d2[k], errors, level+1, lineage+[k], hide_value_mismatches, hide_key_mismatches) + dict_compare( + d1[k], + d2[k], + errors, + level + 1, + lineage + [k], + hide_value_mismatches, + hide_key_mismatches, + ) elif d1 != d2: # Here, we could use the util.objects_near_equal to compare objects. Currently, d1 and # d2 may have any type, i.e. float 1.0 will be considered equal to int 1. @@ -77,161 +94,165 @@ def dict_compare(d1, d2, errors, level=0, lineage=None, hide_value_mismatches=Fa # ------------------------------------------------------------------------------------------------- class DataGroup: - def __init__(self, name, type_list, ref_list=None): self._name = name self._types = type_list self._refs = ref_list - def add_data_group(self, group_name, group_subdict): - ''' + """ Process Data Group from the source schema into a properties node in json. :param group_name: Data Group name; this will become a schema definition key :param group_subdict: Dictionary of Data Elements where each element is a key - ''' - elements = {'type': 'object', - 'properties' : dict()} + """ + elements = {"type": "object", "properties": dict()} required = list() dependencies = dict() for e in group_subdict: element = group_subdict[e] - if 'Description' in element: - elements['properties'][e] = {'description' : element['Description']} - if 'Data Type' in element: + if "Description" in element: + elements["properties"][e] = {"description": element["Description"]} + if "Data Type" in element: self._create_type_entry(group_subdict[e], elements, e) - if 'Units' in element: - elements['properties'][e]['units'] = element['Units'] - if 'Scalable' in element: - elements['properties'][e]['scalable'] = element['Scalable'] - if 'Notes' in element: - elements['properties'][e]['notes'] = element['Notes'] - if 'Required' in element: - req = element['Required'] + if "Units" in element: + elements["properties"][e]["units"] = element["Units"] + if "Scalable" in element: + elements["properties"][e]["scalable"] = element["Scalable"] + if "Notes" in element: + elements["properties"][e]["notes"] = element["Notes"] + if "Required" in element: + req = element["Required"] if isinstance(req, bool): if req == True: required.append(e) - elif req.startswith('if'): + elif req.startswith("if"): self._construct_requirement_if_then(elements, dependencies, req[3:], e) # Include required text (even if it is translated into enforceable JSON schema syntax) - elements['properties'][e]['requiredText'] = str(element['Required']) - if 'Constraints' in element: + elements["properties"][e]["requiredText"] = str(element["Required"]) + if "Constraints" in element: # Include constraints text (even if it is translated into enforceable JSON schema syntax) - elements['properties'][e]['constraintsText'] = element['Constraints'] + elements["properties"][e]["constraintsText"] = element["Constraints"] if required: - elements['required'] = required + elements["required"] = required if dependencies: - elements['dependencies'] = dependencies - elements['additionalProperties'] = False - return {group_name : elements} - - - def _construct_requirement_if_then(self, - conditionals_list : dict, - dependencies_list : dict, - requirement_str : str, - requirement : str): - ''' + elements["dependencies"] = dependencies + elements["additionalProperties"] = False + return {group_name: elements} + + def _construct_requirement_if_then( + self, + conditionals_list: dict, + dependencies_list: dict, + requirement_str: str, + requirement: str, + ) -> None: + """ Construct paired if-then json entries for conditional requirements. :param conditionals_list: :param dependencies_list: :param requirement_str: Raw requirement string using A205 syntax :param requirement: requirement is present if requirement_str indicates it - ''' - separator = r'\sand\s' - selector_dict = {'properties' : {}} + """ + separator = r"\sand\s" + selector_dict = {"properties": {}} requirement_list = re.split(separator, requirement_str) - dependent_req = r'(?P[0-9a-zA-Z_]*)((?P!?=)(?P[0-9a-zA-Z_]*))?' + dependent_req = r"(?P[0-9a-zA-Z_]*)((?P!?=)(?P[0-9a-zA-Z_]*))?" for req in requirement_list: m = re.match(dependent_req, req) if m: - selector = m.group('selector') - if m.group('is_equal'): - is_equal = False if '!' in m.group('is_equal') else True - selector_state = m.group('selector_state') - if 'true' in selector_state.lower(): + if "contains" in req: + break # TODO: Skip for now. Implement in lattice. + selector = m.group("selector") + if m.group("is_equal"): + is_equal = False if "!" in m.group("is_equal") else True + selector_state = m.group("selector_state") + if "true" in selector_state.lower(): selector_state = True - elif 'false' in selector_state.lower(): + elif "false" in selector_state.lower(): selector_state = False - selector_dict['properties'][selector] = {'const' : selector_state} if is_equal else {'not' : {'const' : selector_state} } - else: # prerequisite type + selector_dict["properties"][selector] = ( + {"const": selector_state} if is_equal else {"not": {"const": selector_state}} + ) + else: # prerequisite type if dependencies_list.get(selector): dependencies_list[selector].append(requirement) else: dependencies_list[selector] = [requirement] - if selector_dict['properties'].keys(): + if selector_dict["properties"].keys(): # Conditional requirements are each a member of a list - if conditionals_list.get('allOf') == None: - conditionals_list['allOf'] = list() + if conditionals_list.get("allOf") == None: + conditionals_list["allOf"] = list() - for conditional_req in conditionals_list['allOf']: - if conditional_req.get('if') == selector_dict: # condition already exists - conditional_req['then']['required'].append(requirement) + for conditional_req in conditionals_list["allOf"]: + if conditional_req.get("if") == selector_dict: # condition already exists + conditional_req["then"]["required"].append(requirement) return - conditionals_list['allOf'].append(dict()) - conditionals_list['allOf'][-1]['if'] = selector_dict - conditionals_list['allOf'][-1]['then'] = {'required' : [requirement]} - + conditionals_list["allOf"].append(dict()) + conditionals_list["allOf"][-1]["if"] = selector_dict + conditionals_list["allOf"][-1]["then"] = {"required": [requirement]} def _create_type_entry(self, parent_dict, target_dict, entry_name): - ''' + """ Create json type node and its nested nodes if necessary. :param parent_dict: A Data Element's subdictionary, from source schema :param target_dict: The json definition node that will be populated :param entry_name: Data Element name - ''' + """ try: # If the type is an array, extract the surrounding [] first (using non-greedy qualifier "?") - m = re.findall(r'\[(.*?)\]', parent_dict['Data Type']) - target_property_entry = target_dict['properties'][entry_name] + m = re.findall(r"\[(.*?)\]", parent_dict["Data Type"]) + target_property_entry = target_dict["properties"][entry_name] if m: # 1. 'type' entry - target_property_entry['type'] = 'array' + target_property_entry["type"] = "array" # 2. 'm[in/ax]Items' entry if len(m) > 1: # Parse ellipsis range-notation e.g., '[1..]' - mnmx = re.match(r'([0-9]*)(\.*\.*)([0-9]*)', m[1]) - target_property_entry['minItems'] = int(mnmx.group(1)) - if (mnmx.group(2) and mnmx.group(3)): - target_property_entry['maxItems'] = int(mnmx.group(3)) + mnmx = re.match(r"([0-9]*)(\.*\.*)([0-9]*)", m[1]) + target_property_entry["minItems"] = int(mnmx.group(1)) + if mnmx.group(2) and mnmx.group(3): + target_property_entry["maxItems"] = int(mnmx.group(3)) elif not mnmx.group(2): - target_property_entry['maxItems'] = int(mnmx.group(1)) + target_property_entry["maxItems"] = int(mnmx.group(1)) # 3. 'items' entry - target_property_entry['items'] = dict() - self._get_simple_type(m[0], target_property_entry['items']) - #target_property_entry['items'][k] = v - if 'Constraints' in parent_dict: - self._get_simple_constraints(parent_dict['Constraints'], target_dict['items']) + target_property_entry["items"] = dict() + self._get_simple_type(m[0], target_property_entry["items"]) + # target_property_entry['items'][k] = v + if "Constraints" in parent_dict: + self._get_simple_constraints(parent_dict["Constraints"], target_dict["items"]) else: # If the type is oneOf a set - m = re.match(r'\((.*)\)', parent_dict['Data Type']) + m = re.match(r"\((.*)\)", parent_dict["Data Type"]) if m: - types = [t.strip() for t in m.group(1).split(',')] - selection_key, selections = parent_dict['Constraints'].split('(') - if target_dict.get('allOf') == None: - target_dict['allOf'] = list() - #target_dict['allOf'] = list() - for s, t in zip(selections.split(','), types): - #c = c.strip() - target_dict['allOf'].append(dict()) - self._construct_selection_if_then(target_dict['allOf'][-1], selection_key, s, entry_name) - self._get_simple_type(t, target_dict['allOf'][-1]['then']['properties'][entry_name]) + types = [t.strip() for t in m.group(1).split(",")] + selection_key, selections = parent_dict["Constraints"].split("(") + if target_dict.get("allOf") == None: + target_dict["allOf"] = list() + # target_dict['allOf'] = list() + for s, t in zip(selections.split(","), types): + # c = c.strip() + target_dict["allOf"].append(dict()) + self._construct_selection_if_then(target_dict["allOf"][-1], selection_key, s, entry_name) + self._get_simple_type( + t, + target_dict["allOf"][-1]["then"]["properties"][entry_name], + ) else: # 1. 'type' entry - self._get_simple_type(parent_dict['Data Type'], target_property_entry) + self._get_simple_type(parent_dict["Data Type"], target_property_entry) # 2. 'm[in/ax]imum' entry - self._get_simple_constraints(parent_dict['Constraints'], target_property_entry) + if "Constraints" in parent_dict: + self._get_simple_constraints(parent_dict["Constraints"], target_property_entry) except KeyError as ke: - #print('KeyError; no key exists called', ke) + # print('KeyError; no key exists called', ke) pass - def _construct_selection_if_then(self, target_dict_to_append, selector, selection, entry_name): - ''' + """ Construct paired if-then json entries for allOf collections translated from source-schema "choice" Constraints. @@ -241,30 +262,31 @@ def _construct_selection_if_then(self, target_dict_to_append, selector, selectio :param selection: Item from constraints values list. :param entry_name: Data Element for which the Data Type must match the Constraint - ''' - target_dict_to_append['if'] = {'properties' : {selector : {'const' : ''.join(ch for ch in selection if ch.isalnum())} } } - target_dict_to_append['then'] = {'properties' : {entry_name : dict()}} - + """ + target_dict_to_append["if"] = { + "properties": {selector: {"const": "".join(ch for ch in selection if ch.isalnum())}} + } + target_dict_to_append["then"] = {"properties": {entry_name: dict()}} def _get_simple_type(self, type_str, target_dict_to_append): - ''' Return the internal type described by type_str, along with its json-appropriate key. - First, attempt to capture enum, definition, or special string type as references; - then default to fundamental types with simple key "type". - - :param type_str: Input string from source schema's Data Type key - :param target_dict_to_append: The json "items" node - ''' - enum_or_def = r'(\{|\<)(.*)(\}|\>)' + """Return the internal type described by type_str, along with its json-appropriate key. + First, attempt to capture enum, definition, or special string type as references; + then default to fundamental types with simple key "type". + + :param type_str: Input string from source schema's Data Type key + :param target_dict_to_append: The json "items" node + """ + enum_or_def = r"(\{|\<)(.*)(\}|\>)" internal_type = None nested_type = None m = re.match(enum_or_def, type_str) if m: # Find the internal type. It might be inside nested-type syntax, but more likely # is a simple definition or enumeration. - m_nested = re.match(r'.*?\((.*)\)', m.group(2)) + m_nested = re.match(r".*?\((.*)\)", m.group(2)) if m_nested: # Rare case of a nested specification e.g., 'ASHRAE205(rs_id=RS0005)' - internal_type = m.group(2).split('(')[0] + internal_type = m.group(2).split("(")[0] nested_type = m_nested.group(1) else: internal_type = m.group(2) @@ -273,53 +295,56 @@ def _get_simple_type(self, type_str, target_dict_to_append): # Look through the references to assign a source to the type for key in self._refs: if internal_type in self._refs[key]: - internal_type = key + '.schema.json#/definitions/' + internal_type - target_dict_to_append['$ref'] = internal_type + internal_type = key + ".schema.json#/definitions/" + internal_type + target_dict_to_append["$ref"] = internal_type if nested_type: # Always in the form 'rs_id=RSXXXX' - target_dict_to_append['rs_id'] = nested_type.split('=')[1] + target_dict_to_append["rs_id"] = nested_type.split("=")[1] return try: - if '/' in type_str: + if "/" in type_str: # e.g., "Numeric/Null" becomes a list of 'type's - #return ('type', [self._types[t] for t in type_str.split('/')]) - target_dict_to_append['type'] = [self._types[t] for t in type_str.split('/')] + # return ('type', [self._types[t] for t in type_str.split('/')]) + target_dict_to_append["type"] = [self._types[t] for t in type_str.split("/")] else: - target_dict_to_append['type'] = self._types[type_str] + target_dict_to_append["type"] = self._types[type_str] except KeyError: - print('Type not processed:', type_str) + print("Type not processed:", type_str) return - def _get_simple_constraints(self, constraints_str, target_dict): - ''' + """ Process numeric Constraints into fields. :param constraints_str: Raw numerical limits and/or multiple information :param target_dict: json property node - ''' + """ if constraints_str is not None: constraints = constraints_str if isinstance(constraints_str, list) else [constraints_str] - minimum=None - maximum=None + minimum = None + maximum = None for c in constraints: - if 'string' in target_dict['type']: # String pattern match - target_dict['pattern'] = c.replace('"','') # TODO: Find better way to remove quotes. + if "string" in target_dict["type"]: # String pattern match + target_dict["pattern"] = c.replace('"', "") # TODO: Find better way to remove quotes. else: try: # TODO: any exotic constraint type with numerals in it, such as schmea=RS0001, will be processed here - numerical_value = re.findall(r'[+-]?[0-9]*\.?[0-9]+|[0-9]+', c)[0] - if '>' in c: - minimum = (float(numerical_value) if 'number' in target_dict['type'] else int(numerical_value)) - mn = 'exclusiveMinimum' if '=' not in c else 'minimum' + numerical_value = re.findall(r"[+-]?[0-9]*\.?[0-9]+|[0-9]+", c)[0] + if ">" in c: + minimum = ( + float(numerical_value) if "number" in target_dict["type"] else int(numerical_value) + ) + mn = "exclusiveMinimum" if "=" not in c else "minimum" target_dict[mn] = minimum - elif '<' in c: - maximum = (float(numerical_value) if 'number' in target_dict['type'] else int(numerical_value)) - mx = 'exclusiveMaximum' if '=' not in c else 'maximum' + elif "<" in c: + maximum = ( + float(numerical_value) if "number" in target_dict["type"] else int(numerical_value) + ) + mx = "exclusiveMaximum" if "=" not in c else "maximum" target_dict[mx] = maximum - elif '%' in c: - target_dict['multipleOf'] = int(numerical_value) + elif "%" in c: + target_dict["multipleOf"] = int(numerical_value) except IndexError: # Constraint was non-numeric pass @@ -332,33 +357,31 @@ def _get_simple_constraints(self, constraints_str, target_dict): # ------------------------------------------------------------------------------------------------- class Enumeration: - def __init__(self, name, description=None): self._name = name - self._enumerants = list() # list of tuple:[value, description, display_text, notes] + self._enumerants = list() # list of tuple:[value, description, display_text, notes] self.entry = dict() self.entry[self._name] = dict() if description: - self.entry[self._name]['description'] = description + self.entry[self._name]["description"] = description def add_enumerator(self, value, description=None, display_text=None, notes=None): - '''Store information grouped as a tuple per enumerant.''' + """Store information grouped as a tuple per enumerant.""" self._enumerants.append((value, description, display_text, notes)) def create_dictionary_entry(self): - ''' + """ Convert information currently grouped per enumerant, into json groups for the whole enumeration. - ''' + """ z = list(zip(*self._enumerants)) - enums = {'type': 'string', - 'enum' : z[0]} + enums = {"type": "string", "enum": z[0]} if any(z[2]): - enums['enum_text'] = z[2] + enums["enum_text"] = z[2] if any(z[1]): - enums['descriptions'] = z[1] + enums["descriptions"] = z[1] if any(z[3]): - enums['notes'] = z[3] + enums["notes"] = z[3] self.entry[self._name] = {**self.entry[self._name], **enums} return self.entry @@ -369,13 +392,14 @@ def __init__(self): self._references = dict() self._fundamental_data_types = dict() - def load_common_schema(self, input_file_path): - '''Load and process a yaml schema into its json schema equivalent.''' - self._schema = {'$schema': 'http://json-schema.org/draft-07/schema#', - 'title': None, - 'description': None, - 'definitions' : dict()} + """Load and process a yaml schema into its json schema equivalent.""" + self._schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": None, + "description": None, + "definitions": dict(), + } self._references.clear() self._source_dir = os.path.dirname(os.path.abspath(input_file_path)) self._schema_name = os.path.splitext(os.path.splitext(os.path.basename(input_file_path))[0])[0] @@ -384,117 +408,156 @@ def load_common_schema(self, input_file_path): sch = dict() # Iterate through the dictionary, looking for known types for base_level_tag in self._contents: - if 'Object Type' in self._contents[base_level_tag]: - obj_type = self._contents[base_level_tag]['Object Type'] - if obj_type == 'Meta': + if "Object Type" in self._contents[base_level_tag]: + obj_type = self._contents[base_level_tag]["Object Type"] + if obj_type == "Meta": self._load_meta_info(self._contents[base_level_tag]) - if obj_type == 'String Type': - if 'Is Regex' in self._contents[base_level_tag]: - sch = {**sch, **({base_level_tag : {"type":"string", "regex":True}})} + if obj_type == "String Type": + if "Is Regex" in self._contents[base_level_tag]: + sch = { + **sch, + **({base_level_tag: {"type": "string", "regex": True}}), + } else: - sch = {**sch, **({base_level_tag : {"type":"string", "pattern":self._contents[base_level_tag]['JSON Schema Pattern']}})} - if obj_type == 'Enumeration': + sch = { + **sch, + **( + { + base_level_tag: { + "type": "string", + "pattern": self._contents[base_level_tag]["JSON Schema Pattern"], + } + } + ), + } + if obj_type == "Enumeration": sch = {**sch, **(self._process_enumeration(base_level_tag))} - if obj_type in ['Data Group', - 'Performance Map', - 'Grid Variables', - 'Lookup Variables', - 'Rating Data Group']: + if obj_type in [ + "Data Group", + "Performance Map", + "Grid Variables", + "Lookup Variables", + "Rating Data Group", + ]: dg = DataGroup(base_level_tag, self._fundamental_data_types, self._references) - sch = {**sch, **(dg.add_data_group(base_level_tag, - self._contents[base_level_tag]['Data Elements']))} - self._schema['definitions'] = sch + sch = { + **sch, + **( + dg.add_data_group( + base_level_tag, + self._contents[base_level_tag]["Data Elements"], + ) + ), + } + self._schema["definitions"] = sch return self._schema - def _load_meta_info(self, schema_section): - '''Store the global/common types and the types defined by any named references.''' - self._schema['title'] = schema_section['Title'] - self._schema['description'] = schema_section['Description'] - if 'Version' in schema_section: - self._schema['version'] = schema_section['Version'] - if 'Root Data Group' in schema_section: - self._schema['$ref'] = self._schema_name + '.schema.json#/definitions/' + schema_section['Root Data Group'] + """Store the global/common types and the types defined by any named references.""" + self._schema["title"] = schema_section["Title"] + self._schema["description"] = schema_section["Description"] + if "Version" in schema_section: + self._schema["version"] = schema_section["Version"] + if "Root Data Group" in schema_section: + self._schema["$ref"] = self._schema_name + ".schema.json#/definitions/" + schema_section["Root Data Group"] # Create a dictionary of available external objects for reference refs = [self._schema_name] - if 'References' in schema_section: - refs += schema_section['References'] + if "References" in schema_section: + refs += schema_section["References"] for ref_file in refs: - ext_dict = load(os.path.join(self._source_dir, ref_file + '.schema.yaml')) + ext_dict = load(os.path.join(self._source_dir, ref_file + ".schema.yaml")) external_objects = list() - for base_item in [name for name in ext_dict if ext_dict[name]['Object Type'] in ( - ['Enumeration', - 'Data Group', - 'String Type', - 'Map Variables', - 'Rating Data Group', - 'Performance Map', - 'Grid Variables', - 'Lookup Variables'])]: + for base_item in [ + name + for name in ext_dict + if ext_dict[name]["Object Type"] + in ( + [ + "Enumeration", + "Data Group", + "String Type", + "Map Variables", + "Rating Data Group", + "Performance Map", + "Grid Variables", + "Lookup Variables", + ] + ) + ]: external_objects.append(base_item) self._references[ref_file] = external_objects - for base_item in [name for name in ext_dict if ext_dict[name]['Object Type'] == 'Data Type']: - self._fundamental_data_types[base_item] = ext_dict[base_item]['JSON Schema Type'] - + for base_item in [name for name in ext_dict if ext_dict[name]["Object Type"] == "Data Type"]: + self._fundamental_data_types[base_item] = ext_dict[base_item]["JSON Schema Type"] def _process_enumeration(self, name_key): - ''' Collect all Enumerators in an Enumeration block. ''' - enums = self._contents[name_key]['Enumerators'] - description = self._contents[name_key].get('Description') + """Collect all Enumerators in an Enumeration block.""" + enums = self._contents[name_key]["Enumerators"] + description = self._contents[name_key].get("Description") definition = Enumeration(name_key, description) for key in enums: try: - descr = enums[key]['Description'] if 'Description' in enums[key] else None - displ = enums[key]['Display Text'] if 'Display Text' in enums[key] else None - notes = enums[key]['Notes'] if 'Notes' in enums[key] else None + descr = enums[key]["Description"] if "Description" in enums[key] else None + displ = enums[key]["Display Text"] if "Display Text" in enums[key] else None + notes = enums[key]["Notes"] if "Notes" in enums[key] else None definition.add_enumerator(key, descr, displ, notes) - except TypeError: # key's value is None + except TypeError: # key's value is None definition.add_enumerator(key) return definition.create_dictionary_entry() # ------------------------------------------------------------------------------------------------- def print_comparison(original_dir, generated_dir, file_name_root, err): - '''Compare generated dictionary to original; send results to stdout.''' - same = compare_dicts(os.path.join(original_dir, file_name_root + '.schema.json'), - os.path.join(generated_dir, file_name_root + '.schema.json'), - err) + """Compare generated dictionary to original; send results to stdout.""" + same = compare_dicts( + os.path.join(original_dir, file_name_root + ".schema.json"), + os.path.join(generated_dir, file_name_root + ".schema.json"), + err, + ) if not same: - print(f'\nError(s) while matching {file_name_root}: Original(1) vs Generated(2)') + print(f"\nError(s) while matching {file_name_root}: Original(1) vs Generated(2)") for e in err: print(e) else: print(f"Translation of {file_name_root} successful.") + # ------------------------------------------------------------------------------------------------- + def translate_file(input_file_path, output_file_path): j = JSON_translator() schema_instance = j.load_common_schema(input_file_path) dump(schema_instance, output_file_path) + def translate_dir(input_dir_path, output_dir_path): j = JSON_translator() for file_name in sorted(os.listdir(input_dir_path)): - if '.schema.yaml' in file_name: + if ".schema.yaml" in file_name: file_name_root = os.path.splitext(os.path.splitext(file_name)[0])[0] - schema_instance = j.load_common_schema(os.path.join(input_dir_path,file_name)) - dump(schema_instance, os.path.join(output_dir_path, file_name_root + '.schema.json')) + schema_instance = j.load_common_schema(os.path.join(input_dir_path, file_name)) + dump( + schema_instance, + os.path.join(output_dir_path, file_name_root + ".schema.json"), + ) -if __name__ == '__main__': +if __name__ == "__main__": import sys - source_dir_path = os.path.join(os.path.dirname(__file__),'..','schema-source') - build_dir_path = os.path.join(os.path.dirname(__file__),'..','build') + source_dir_path = os.path.join(os.path.dirname(__file__), "..", "schema-source") + build_dir_path = os.path.join(os.path.dirname(__file__), "..", "build") if not os.path.exists(build_dir_path): os.mkdir(build_dir_path) - schema_dir_path = os.path.join(build_dir_path,'schema') + schema_dir_path = os.path.join(build_dir_path, "schema") if not os.path.exists(schema_dir_path): os.mkdir(schema_dir_path) if len(sys.argv) == 2: file_name_root = sys.argv[1] - translate_file(os.path.join(source_dir_path, f'{file_name_root}.schema.yaml'), os.path.join(schema_dir_path, file_name_root + '.schema.json')) + translate_file( + os.path.join(source_dir_path, f"{file_name_root}.schema.yaml"), + os.path.join(schema_dir_path, file_name_root + ".schema.json"), + ) else: translate_dir(source_dir_path, schema_dir_path)