diff --git a/ditto/models/winding.py b/ditto/models/winding.py index 90ed573d..7d0b422f 100644 --- a/ditto/models/winding.py +++ b/ditto/models/winding.py @@ -30,6 +30,9 @@ class Winding(DiTToHasTraits): nominal_voltage = Float( help="""The nominal voltage of the transformer winding""", default_value=None ) + base_voltage = Float( + help="""The voltage basis for per unit calculations""", default_value=None + ) voltage_limit = Float( help="""The maximum voltage allowed on the PT secondary.""", default_value=None ) diff --git a/ditto/readers/cyme/read.py b/ditto/readers/cyme/read.py index 43012000..38033866 100644 --- a/ditto/readers/cyme/read.py +++ b/ditto/readers/cyme/read.py @@ -328,9 +328,9 @@ def update_header_mapping(self, update): # Replace the old mapping by the new one self.header_mapping = new_mapping - def get_file_content(self, filename): + def get_file_content(self, filename: str) -> None: """ - Open the requested file and returns the content. + Open the requested file and set self.content to iter(file_pointer.readlines()) For convinience, filename can be either the full file path or: -'network': Will get the content of the network file given in the constructor @@ -719,7 +719,7 @@ def transformer_connection_configuration_mapping(self, value, winding): return res[winding] - def check_object_in_line(self, line, obj): + def check_object_in_line(self, line: str, obj: str) -> bool: """ Check if the header corresponding to object is in the given line. @@ -754,13 +754,14 @@ def check_object_in_line(self, line, obj): return np.any([x in line for x in self.header_mapping[obj]]) - def parser_helper(self, line, obj_list, attribute_list, mapping, *args, **kwargs): + def parser_helper(self, line, obj, attribute_list, mapping: dict, *args, **kwargs) -> dict: """ .. warning:: This is a helper function for the parsers. Do not use directly. - Takes as input the list of objects we want to parse as well as the list of attributes we want to extract. - Also takes the default positions of the attributes (mapping). - The function returns a list of dictionaries, where each dictionary contains the values of the desired attributes of a CYME object. + Takes as input the object we want to parse (eg. "section" maps to "[SECTION]") + as well as the list of attributes we want to extract (eg. ["sectionid", "fromnodeid", "tonodeid", "phase"]). + Also takes the default positions of the attributes (mapping), which is overwritten if "format" is found in the lines. + The function returns a dictionary of dictionaries, where each sub-dictionary contains the values of the desired attributes of a CYME object. """ if isinstance(attribute_list, list): attribute_list = np.array(attribute_list) @@ -783,11 +784,8 @@ def parser_helper(self, line, obj_list, attribute_list, mapping, *args, **kwargs result = {} - # Check the presence of headers in the given line - checks = [self.check_object_in_line(line, obj) for obj in obj_list] - - # If we have a least one - if any(checks): + # If header in line + if self.check_object_in_line(line, obj): # Get the next line next_line = next(self.content) @@ -809,13 +807,13 @@ def parser_helper(self, line, obj_list, attribute_list, mapping, *args, **kwargs idx2 = temp[0] mapping[attribute_list[idx2]] = idx except: - pass + logger.warn(f"Unable to parse CYME line FORMAT for {obj} in {next_line}") next_line = next(self.content) # At this point, we should have the mapping for the parameters of interest # while next_line[0] not in ['[','',' ','\n','\r\n']: - while len(next_line) > 2: + while len(next_line) > 2: # blank lines separate objects in CYME .txt files if "=" not in next_line.lower(): data = next_line.split(",") @@ -824,7 +822,7 @@ def parser_helper(self, line, obj_list, attribute_list, mapping, *args, **kwargs if len(data) > 1: - while ID in result: + while ID in result: # redundant keys get *'s ID += "*" result[ID] = {} @@ -862,10 +860,7 @@ def parser_helper(self, line, obj_list, attribute_list, mapping, *args, **kwargs attribute_list = additional_attributes additional_attributes = [] except: - logger.warning( - "Attempted to apply additional attributes but failed" - ) - pass + logger.warn(f"Unable to parse additional CYME line FORMAT for {obj} in {next_line}") try: next_line = next(self.content) @@ -982,7 +977,7 @@ def parse_subnetwork_connections(self, model): self.subnetwork_connections.update( self.parser_helper( line, - ["subnetwork_connections"], + "subnetwork_connections", ["nodeid"], mapp_subnetwork_connections, ) @@ -1004,7 +999,7 @@ def parse_head_nodes(self, model): headnodes = {} for line in self.content: headnodes.update( - self.parser_helper(line, ["headnodes"], ["nodeid", "networkid"], mapp) + self.parser_helper(line, "headnodes", ["nodeid", "networkid"], mapp) ) for sid, headnode in headnodes.items(): @@ -1036,11 +1031,11 @@ def parse_sources(self, model): subs = {} source_equivalents = {} - for line in self.content: + for line in self.content: # parser_helper will only loop over lines in self.content if it finds obj in the `line`; o.w. returns empty dict sources.update( self.parser_helper( line, - ["source"], + "source", ["sourceid", "nodeid", "networkid", "desiredvoltage"], mapp, ) @@ -1048,7 +1043,7 @@ def parse_sources(self, model): source_equivalents.update( self.parser_helper( line, - ["source_equivalent"], + "source_equivalent", [ "nodeid", "voltage", @@ -1078,7 +1073,7 @@ def parse_sources(self, model): for line in self.content: subs.update( self.parser_helper( - line, ["substation"], ["id", "mva", "kvll", "conn"], mapp_sub + line, "substation", ["id", "mva", "kvll", "conn"], mapp_sub ) ) if len(sources.items()) == 0: @@ -1371,7 +1366,7 @@ def parse_nodes(self, model): nodes.update( self.parser_helper( line, - ["node"], + "node", ["nodeid", "coordx", "coordy", "ratedvoltage"], mapp, **kwargs @@ -1381,7 +1376,7 @@ def parse_nodes(self, model): for line in self.content: node_connectors.update( self.parser_helper( - line, ["node_connector"], ["nodeid", "coordx", "coordy"], mapp + line, "node_connector", ["nodeid", "coordx", "coordy"], mapp ) ) @@ -1556,7 +1551,7 @@ def parse_sections(self, model): ... [SECTION] - FORMAT_section=sectionid,fromnodeid,tonodeid,phase + FORMAT_section=sectionid,fromnodeid,tonodeid,phase FORMAT_Feeder=networkid,headnodeid Feeder=feeder_1,head_feeder_1 section_1_feeder_1,node_1,node_2,ABC @@ -1947,7 +1942,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["overhead_unbalanced_line_settings"], + "overhead_unbalanced_line_settings", ["sectionid", "coordx", "coordy", "linecableid", "length"], mapp_overhead, {"type": "overhead_unbalanced"}, @@ -1964,7 +1959,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["overhead_line_settings"], + "overhead_line_settings", ["sectionid", "coordx", "coordy", "linecableid", "length"], mapp_overhead, {"type": "overhead_balanced"}, @@ -1981,7 +1976,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["overhead_byphase_settings"], + "overhead_byphase_settings", [ "sectionid", "devicenumber", @@ -2011,7 +2006,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["underground_line_settings"], + "underground_line_settings", ["sectionid", "coordx", "coordy", "linecableid", "length", "amps"], mapp_underground, {"type": "underground"}, @@ -2028,7 +2023,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["switch_settings"], + "switch_settings", ["sectionid", "coordx", "coordy", "eqid", "closedphase"], mapp_switch, {"type": "switch"}, @@ -2045,7 +2040,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["sectionalizer_settings"], + "sectionalizer_settings", ["sectionid", "coordx", "coordy", "eqid", "closedphase"], mapp_sectionalizer, {"type": "sectionalizer"}, @@ -2062,7 +2057,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["fuse_settings"], + "fuse_settings", ["sectionid", "coordx", "coordy", "eqid"], mapp_switch, # Same as switches {"type": "fuse"}, @@ -2079,7 +2074,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["recloser_settings"], + "recloser_settings", ["sectionid", "coordx", "coordy", "eqid"], mapp_switch, # Same as switches {"type": "recloser"}, @@ -2096,7 +2091,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["breaker_settings"], + "breaker_settings", ["sectionid", "coordx", "coordy", "eqid", "closedphase"], mapp_switch, # Same as switches {"type": "breaker"}, @@ -2113,7 +2108,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["network_protector_settings"], + "network_protector_settings", ["sectionid", "coordx", "coordy", "eqid", "closedphase"], mapp_switch, # Same as switches {"type": "network_protector"}, @@ -2130,7 +2125,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["section"], + "section", ["sectionid", "fromnodeid", "tonodeid", "phase"], mapp_section, ), @@ -2157,7 +2152,7 @@ def parse_lines(self, model): self.balanced_lines.update( self.parser_helper( line, - ["line"], + "line", [ "id", "phasecondid", @@ -2185,7 +2180,7 @@ def parse_lines(self, model): self.unbalanced_lines.update( self.parser_helper( line, - ["unbalanced_line"], + "unbalanced_line", [ "id", "condid_a", @@ -2225,7 +2220,7 @@ def parse_lines(self, model): self.spacings.update( self.parser_helper( line, - ["spacing_table"], + "spacing_table", [ "id", "posofcond1_x", @@ -2252,7 +2247,7 @@ def parse_lines(self, model): self.conductors.update( self.parser_helper( line, - ["conductor"], + "conductor", ["id", "diameter", "gmr", "r25", "amps", "withstandrating"], mapp_conductor, ) @@ -2267,7 +2262,7 @@ def parse_lines(self, model): self.concentric_neutral_cable.update( self.parser_helper( line, - ["concentric_neutral_cable"], + "concentric_neutral_cable", [ "id", "r1", @@ -2291,7 +2286,7 @@ def parse_lines(self, model): self.cables.update( self.parser_helper( line, - ["cable"], + "cable", ["id", "r1", "r0", "x1", "x0", "amps"], mapp_concentric_neutral_cable, ) @@ -2305,7 +2300,7 @@ def parse_lines(self, model): # self.switches.update( self.parser_helper( - line, ["switch"], ["id", "amps", "kvll"], mapp_switch_eq + line, "switch", ["id", "amps", "kvll"], mapp_switch_eq ) ) @@ -2318,7 +2313,7 @@ def parse_lines(self, model): self.fuses.update( self.parser_helper( line, - ["fuse"], + "fuse", ["id", "amps", "kvll", "interruptingrating"], mapp_network_protectors, # Same as network protectors ) @@ -2333,7 +2328,7 @@ def parse_lines(self, model): self.reclosers.update( self.parser_helper( line, - ["recloser"], + "recloser", ["id", "amps", "kvll", "interruptingrating"], mapp_network_protectors, # Same as network protectors ) @@ -2348,7 +2343,7 @@ def parse_lines(self, model): self.sectionalizers.update( self.parser_helper( line, - ["sectionalizer"], + "sectionalizer", ["id", "amps", "kvll", "interruptingrating"], mapp_sectionalizers, ) @@ -2363,7 +2358,7 @@ def parse_lines(self, model): self.breakers.update( self.parser_helper( line, - ["breaker"], + "breaker", ["id", "amps", "kvll", "interruptingrating"], mapp_network_protectors, # Same as network protectors ) @@ -2378,7 +2373,7 @@ def parse_lines(self, model): self.network_protectors.update( self.parser_helper( line, - ["network_protector"], + "network_protector", ["id", "amps", "kvll", "interruptingrating"], mapp_network_protectors, ) @@ -3024,7 +3019,6 @@ def parse_lines(self, model): line_data = self.concentric_neutral_cable[settings["linecableid"]] line_data["type"] = "balanced_line" if settings["linecableid"] in self.cables: - logger.debug("cables {}".format(sectionID)) line_data = self.cables[settings["linecableid"]] line_data["type"] = "balanced_line" @@ -3877,7 +3871,7 @@ def parse_capacitors(self, model): self.settings.update( self.parser_helper( line, - ["serie_capacitor_settings"], + "serie_capacitor_settings", ["sectionid", "eqid", "coordx", "coordy"], mapp_serie_capacitor_settings, {"type": "serie"}, @@ -3893,7 +3887,7 @@ def parse_capacitors(self, model): self.settings.update( self.parser_helper( line, - ["shunt_capacitor_settings"], + "shunt_capacitor_settings", [ "sectionid", "shuntcapacitorid", @@ -3932,7 +3926,7 @@ def parse_capacitors(self, model): # self.capacitors.update( self.parser_helper( - line, ["serie_capacitor"], ["id", "reactance"], mapp_serie_capacitor + line, "serie_capacitor", ["id", "reactance"], mapp_serie_capacitor ) ) @@ -3945,7 +3939,7 @@ def parse_capacitors(self, model): self.capacitors.update( self.parser_helper( line, - ["shunt_capacitor"], + "shunt_capacitor", ["id", "kvar", "kv", "type"], mapp_shunt_capacitor, ) @@ -4312,7 +4306,7 @@ def parse_transformers(self, model): self.settings.update( self.parser_helper( line, - ["auto_transformer_settings"], + "auto_transformer_settings", [ "sectionid", "eqid", @@ -4335,7 +4329,7 @@ def parse_transformers(self, model): self.settings.update( self.parser_helper( line, - ["grounding_transformer_settings"], + "grounding_transformer_settings", ["sectionid", "equipmentid", "connectionconfiguration", "phase"], mapp_grounding_transformer_settings, {"type": "grounding_transformer"}, @@ -4351,7 +4345,7 @@ def parse_transformers(self, model): self.settings.update( self.parser_helper( line, - ["three_winding_auto_transformer_settings"], + "three_winding_auto_transformer_settings", [ "sectionid", "eqid", @@ -4378,7 +4372,7 @@ def parse_transformers(self, model): self.settings.update( self.parser_helper( line, - ["three_winding_transformer_settings"], + "three_winding_transformer_settings", [ "sectionid", "eqid", @@ -4405,7 +4399,7 @@ def parse_transformers(self, model): self.settings.update( self.parser_helper( line, - ["transformer_settings"], + "transformer_settings", [ "sectionid", "eqid", @@ -4437,7 +4431,7 @@ def parse_transformers(self, model): self.settings.update( self.parser_helper( line, - ["phase_shifter_transformer_settings"], + "phase_shifter_transformer_settings", ["sectionid", "eqid", "coordx", "coordy"], mapp_phase_shifter_transformer_settings, {"type": "phase_shifter_transformer"}, @@ -4465,7 +4459,7 @@ def parse_transformers(self, model): self.auto_transformers.update( self.parser_helper( line, - ["auto_transformer"], + "auto_transformer", [ "id", "kva", @@ -4489,7 +4483,7 @@ def parse_transformers(self, model): self.grounding_transformers.update( self.parser_helper( line, - ["grounding_transformer"], + "grounding_transformer", ["id", "ratedcapacity", "ratedvoltage", "connection_configuration"], mapp_grounding_transformer, ) @@ -4505,7 +4499,7 @@ def parse_transformers(self, model): self.three_winding_auto_transformers.update( self.parser_helper( line, - ["three_winding_auto_transformer"], + "three_winding_auto_transformer", [ "id", "primaryratedcapacity", @@ -4530,7 +4524,7 @@ def parse_transformers(self, model): self.three_winding_transformers.update( self.parser_helper( line, - ["three_winding_transformer"], + "three_winding_transformer", [ "id", "primaryratedcapacity", @@ -4554,7 +4548,7 @@ def parse_transformers(self, model): self.transformers.update( self.parser_helper( line, - ["transformer"], + "transformer", [ "id", "type", @@ -5042,7 +5036,7 @@ def parse_regulators(self, model): self.settings.update( self.parser_helper( line, - ["regulator_settings"], + "regulator_settings", [ "sectionid", "eqid", @@ -5081,7 +5075,7 @@ def parse_regulators(self, model): self.regulators.update( self.parser_helper( line, - ["regulator"], + "regulator", [ "id", "type", @@ -5359,7 +5353,7 @@ def parse_network_equivalent(self, model): self.settings, self.parser_helper( line, - ["network_equivalent_setting"], + "network_equivalent_setting", ['sectionid', 'devicenumber', 'coordx', 'coordy', 'zraa', 'zrab', 'zrac', 'zrba', 'zrbb', 'zrbc', 'zrca', 'zrcb', 'zrcc', 'zxaa', 'zxab', 'zxac', 'zxba', 'zxbb', 'zxbc', 'zxca', 'zxcb', 'zxcc', 'loadfromkwa', 'loadfromkwb', 'loadfromkwc', 'loadfromkvara', 'loadfromkvarb', 'loadfromkvarc', 'loadtokwa', 'loadtokwb', 'loadtokwc', 'loadtokvara', 'loadtokvarb', 'loadtokvarc', 'totallengtha', 'totallengthb', 'totallengthc'], mapp_network_equivalents, @@ -5377,7 +5371,7 @@ def parse_network_equivalent(self, model): self.settings, self.parser_helper( line, - ["section"], + "section", ["sectionid", "fromnodeid", "tonodeid", "phase"], mapp_section, ), @@ -5621,7 +5615,7 @@ def parse_loads(self, model): self.loads.update( self.parser_helper( line, - ["loads"], + "loads", ["sectionid", "devicenumber", "loadtype", "connection"], mapp_loads, ) @@ -5636,7 +5630,7 @@ def parse_loads(self, model): self.customer_loads.update( self.parser_helper( line, - ["customer_loads"], + "customer_loads", [ "sectionid", "devicenumber", @@ -5664,7 +5658,7 @@ def parse_loads(self, model): self.customer_class.update( self.parser_helper( line, - ["customer_class"], + "customer_class", [ "id", "constantpower", @@ -5714,9 +5708,7 @@ def parse_loads(self, model): try: p, q = float(settings["value1"]), float(settings["value2"]) except: - logger.warning( - "WARNING:: Skipping load on section {}".format(sectionID) - ) + logger.warning(f"Problem with load on section {sectionID} with value_type=0 (P,Q). value1={settings['value1']} value2={settings['value2']}") continue elif value_type == 1: # KVA and PF are given try: @@ -5729,30 +5721,29 @@ def parse_loads(self, model): p = kva * PF q = math.sqrt(kva ** 2 - p ** 2) except: - logger.warning( - "WARNING:: Skipping load on section {}".format(sectionID) - ) + logger.warning(f"Problem with load on section {sectionID} with value_type=1 (kVa, PF). value1={settings['value1']} value2={settings['value2']}") continue elif value_type == 2: # P and PF are given try: p, PF = float(settings["value1"]), float(settings["value2"]) - if 0 <= PF <= 1: + if PF == 0: + q = 0 + elif 0 < abs(PF) <= 1: q = p * math.sqrt((1 - PF ** 2) / PF ** 2) - elif 1 < PF <= 100: + elif 1 < abs(PF) <= 100: PF /= 100.0 q = p * math.sqrt((1 - PF ** 2) / PF ** 2) else: - logger.warning("problem with PF") - logger.warning(PF) + logger.warning(f"Problem with load on section {sectionID} with value_type=2 (P, PF). value1={settings['value1']} value2={settings['value2']}") except: - logger.warning("Skipping load on section {}".format(sectionID)) + logger.warning(f"Problem with load on section {sectionID} with value_type=2 (P, PF). value1={settings['value1']} value2={settings['value2']}") continue elif value_type == 3: # AMP and PF are given # TODO logger.warning( - "WARNING:: Skipping load on section {}".format(sectionID) + "WARNING:: Skipping load on section {} because value_type=3 (AMP and PF)".format(sectionID) ) continue @@ -6025,7 +6016,7 @@ def parse_dg(self, model): self.converter.update( self.parser_helper( line, - ["converter"], + "converter", [ "devicenumber", "devicetype", @@ -6051,7 +6042,7 @@ def parse_dg(self, model): self.converter_settings.update( self.parser_helper( line, - ["converter_control_settings"], + "converter_control_settings", [ "devicenumber", "devicetype", @@ -6076,7 +6067,7 @@ def parse_dg(self, model): self.photovoltaic_settings.update( self.parser_helper( line, - ["photovoltaic_settings"], + "photovoltaic_settings", ["sectionid", "devicenumber", "eqphase", "ambienttemperature"], mapp_photovoltaic_settings, {"type": "photovoltaic_settings"}, @@ -6092,7 +6083,7 @@ def parse_dg(self, model): self.bess_settings.update( self.parser_helper( line, - ["bess_settings"], + "bess_settings", [ "sectionid", "devicenumber", @@ -6116,7 +6107,7 @@ def parse_dg(self, model): self.long_term_dynamics.update( self.parser_helper( line, - ["long_term_dynamics_curve_ext"], + "long_term_dynamics_curve_ext", [ "devicenumber", "devicetype", @@ -6137,7 +6128,7 @@ def parse_dg(self, model): self.dg_generation.update( self.parser_helper( line, - ["dggenerationmodel"], + "dggenerationmodel", [ "devicenumber", "devicetype", @@ -6171,7 +6162,7 @@ def parse_dg(self, model): self.bess.update( self.parser_helper( line, - ["bess"], + "bess", [ "id", "ratedstorageenergy", diff --git a/tests/test_reader.py b/tests/test_reader.py index 32bddf24..424fe468 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -9,6 +9,7 @@ import os import pytest as pt from ditto.store import Store +from ditto.models.load import Load current_directory = os.path.realpath(os.path.dirname(__file__)) @@ -44,6 +45,28 @@ def test_cyme_reader(): # TODO: Log properly print(">Cyme model {model} parsed.\n".format(model=model)) + if model == "ieee_13node": + """ + test load base voltage values + the load nominal voltages are set in the system_structure_modifier.set_nominal_voltages_recur + (which is only used in the cyme reader) + the test values are what is expected from the IEEE13 openDSS model + issues: + - perhaps the default load.connection_type should by "Y"? + - all connection_types are currently set to None in this test + - currently the nominal voltages are set to the nominal_voltage of the upline transformer, + but the upline transformer nominal_voltage is (usu.) the phase to phase voltage + which is not the correct voltage to use for single phase load connected between + phase and ground. + - can we use Load and PhaseLoad attributes to correctly define the load.nominal_voltage? + """ + for load in m.iter_models(Load): + nphases = len(load.phase_loads) + if load.connection_type in ("Y", None) and nphases == 1: + assert round(load.nominal_voltage, 3) in (2400, 277) + if load.connection_type == "D": + assert round(load.nominal_voltage, 3) == 4160 + # @pt.mark.skip("Segfault occurs") def test_opendss_reader(): @@ -69,6 +92,17 @@ def test_opendss_reader(): # TODO: Log properly print(">OpenDSS model {model} parsed.\n".format(model=model)) + if model == "ieee_13node": + """ + test load base voltage values match expected values + """ + for load in m.iter_models(Load): + nphases = len(load.phase_loads) + if load.connection_type in ("Y", None) and nphases == 1: + assert round(load.nominal_voltage, 3) in (2400, 277) + if load.connection_type == "D": + assert round(load.nominal_voltage, 3) == 4160 + def test_dew_reader(): """