diff --git a/.gitignore b/.gitignore index 3095c5dd..8b27bffa 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,7 @@ venv/ ENV/ env.bak/ venv.bak/ +testenv/ # Spyder project settings .spyderproject diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dcda03b1..9d6c0873 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,4 +14,3 @@ repos: entry: python -m black types: [python] args: [--line-length=88, --safe] - python_version: python3.6 diff --git a/ditto/consistency/check_transformer_phase_path.py b/ditto/consistency/check_transformer_phase_path.py index dc423392..626a86e5 100644 --- a/ditto/consistency/check_transformer_phase_path.py +++ b/ditto/consistency/check_transformer_phase_path.py @@ -54,6 +54,8 @@ def check_transformer_phase_path(model,needs_transformers=False,verbose=True): source_name = source.connecting_element all_paths = nx.single_source_shortest_path(ditto_graph.graph,source_name) break_load = False + msgs = [] + for load in all_loads: load_connection = load.connecting_element if load_connection in all_paths: @@ -75,7 +77,7 @@ def check_transformer_phase_path(model,needs_transformers=False,verbose=True): transformer_low_side = path[i-1] ### Check that the low side of the transformer is connected to a line that leads to a load - if num_transformers ==1: + if num_transformers == 1: load_transformer_map[load.name] = element['name'] if model[transformer_names[0]].to_element != transformer_low_side: if verbose: @@ -121,18 +123,24 @@ def check_transformer_phase_path(model,needs_transformers=False,verbose=True): if element['equipment'] == 'PowerTransformer' and not element['is_substation']: break if element['equipment'] == 'Line': - line_phases = [wire.phase for wire in element['wires'] if wire.phase != 'N'] #Neutral phases not included in the transformer - if not set(high_phases).issubset(set(line_phases)): #MV phase line phase must be able to support transformer phase - if verbose: - print('Load '+load.name+ ' has incorrect phases '+str(line_phases)+' '+str(high_phases)+' on high side of transformer for line '+element['name']) - result = False - break - if len(line_phases) > len(prev_line_phases): - if verbose: - print('Number of phases increases along line '+element['name'] +' from '+str(len(prev_line_phases))+' to '+str(len(line_phases))) - result = False - break - prev_line_phases = line_phases + if not "wires" in element.keys(): + msg = f"Warning: Line {element['equipment_name']} has no wires!" + if not msg in msgs: + print(msg) + msgs.append(msg) + else: + line_phases = [wire.phase for wire in element['wires'] if wire.phase != 'N'] #Neutral phases not included in the transformer + if not set(high_phases).issubset(set(line_phases)): #MV phase line phase must be able to support transformer phase + if verbose: + print('Load '+load.name+ ' has incorrect phases '+str(line_phases)+' '+str(high_phases)+' on high side of transformer for line '+element['name']) + result = False + break + if len(line_phases) > len(prev_line_phases): + if verbose: + print('Number of phases increases along line '+element['name'] +' from '+str(len(prev_line_phases))+' to '+str(len(line_phases))) + result = False + break + prev_line_phases = line_phases elif element['equipment'] != 'Regulator': print('Warning: element of type '+element['equipment'] +' found on path to load '+load.name) @@ -143,13 +151,21 @@ def check_transformer_phase_path(model,needs_transformers=False,verbose=True): for i in range(len(path)-1): element = ditto_graph.graph[path[i]][path[i+1]] if element['equipment'] == 'Line': - line_phases = [wire.phase for wire in element['wires'] if wire.phase != 'N'] #Neutral phases not included in the transformer - if len(line_phases) > len(prev_line_phases): - if verbose: - print('Number of phases increases along line '+element['name'] +' from '+str(len(prev_line_phases))+' to '+str(len(line_phases))) - result = False - break - prev_line_phases = line_phases + if not "wires" in element.keys(): + msg = f"Warning: Line {element['equipment_name']} has no wires!" + if not msg in msgs: + print(msg) + msgs.append(msg) + else: + line_phases = [wire.phase for wire in element['wires'] if wire.phase != 'N'] #Neutral phases not included in the transformer + if len(line_phases) > len(prev_line_phases): + msg = 'Number of phases increases along line '+element['name'] +' from '+str(len(prev_line_phases))+' to '+str(len(line_phases)) + if verbose and not msg in msgs: + print(msg) + msgs.append(msg) + result = False + break + prev_line_phases = line_phases elif element['equipment'] != 'Regulator' and element['equipment'] != 'PowerTransformer': print('Warning: element of type '+element['equipment'] +' found on path to load '+load.name) return result diff --git a/ditto/metrics/network_analysis.py b/ditto/metrics/network_analysis.py index b446b24c..f5dc1d6f 100644 --- a/ditto/metrics/network_analysis.py +++ b/ditto/metrics/network_analysis.py @@ -157,7 +157,7 @@ def __init__(self, model, compute_network=True, *args): self.edge_equipment_name = None modifier = system_structure_modifier(self.model, source) - modifier.set_nominal_voltages() + modifier.set_nominal_voltages_recur() # IMPORTANT: the following two parameters define what is LV and what is MV. # - Object is LV if object.nominal_voltage<=LV_threshold # - Object is MV if MV_threshold>=object.nominal_voltage>LV_threshold @@ -205,12 +205,10 @@ def add_feeder_information( """ Use this function to add the feeder information if available. - :param feeder_names: List of the feeder names - :type feeder_names: List(str) - :param feeder_nodes: List of lists containing feeder nodes - :type feeder_nodes: List of Lists of strings - :param feeder_types: List of feeder types. - :type feeder_types: List or string if all feeders have the same type + :param feeder_names: List(str) of the feeder names + :param feeder_nodes: List of lists of strings containing feeder nodes + :param substations: List(str) of the substations names + :param feeder_types: List(str) of feeder types or string if all feeders have the same type """ if len(feeder_names) != len(feeder_nodes): raise ValueError( @@ -465,7 +463,7 @@ def tag_objects(self): hasattr(prev_obj, "feeder_name") and hasattr(prev_obj, "name") and prev_obj.feeder_name is not None - and prev_obj.feeder_name is not "" + and prev_obj.feeder_name != "" and prev_obj.name in self.node_feeder_mapping # In case a default value has been set for all feeder_name values ): @@ -498,7 +496,7 @@ def tag_objects(self): hasattr(prev_obj, "feeder_name") and hasattr(prev_obj, "name") and prev_obj.feeder_name is not None - and prev_obj.feeder_name is not "" + and prev_obj.feeder_name != "" and prev_obj.name in self.node_feeder_mapping # In case a default value has been set for all feeder_name values ): @@ -525,7 +523,7 @@ def tag_objects(self): hasattr(prev_obj, "feeder_name") and hasattr(prev_obj, "name") and prev_obj.feeder_name is not None - and prev_obj.feeder_name is not "" + and prev_obj.feeder_name != "" and prev_obj.name in self.node_feeder_mapping # In case a default value has been set for all feeder_name values ): diff --git a/ditto/models/base.py b/ditto/models/base.py index 568d2ade..3fa37574 100644 --- a/ditto/models/base.py +++ b/ditto/models/base.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, division, print_function from builtins import super, range, zip, round, map -import warnings from traitlets.traitlets import ( ObserveHandler, _deprecated_method, @@ -29,9 +28,7 @@ def set_name(self, model): try: name = self.name if name in model.model_names: - warnings.warn("Duplicate name %s being set. Object overwritten." % name) - logger.debug("Duplicate name %s being set. Object overwritten." % name) - logger.debug(model.model_names[name], self) + logger.warn("Duplicate name %s being set. Object overwritten." % name) model.model_names[name] = self except AttributeError: pass diff --git a/ditto/models/capacitor.py b/ditto/models/capacitor.py index 3761d2e5..560317e9 100644 --- a/ditto/models/capacitor.py +++ b/ditto/models/capacitor.py @@ -122,6 +122,14 @@ class Capacitor(DiTToHasTraits): help="""Flag that indicates wheter the element is inside a substation or not.""", default_value=False, ) + vmax = Float( + help="""Maximum voltage for control override OFF""", + default_value=None + ) + vmin = Float( + help="""Minimum voltage for control override ON""", + default_value=None + ) def build(self, model): self._model = model 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/modify/system_structure.py b/ditto/modify/system_structure.py index b607c28a..86c7d894 100644 --- a/ditto/modify/system_structure.py +++ b/ditto/modify/system_structure.py @@ -213,6 +213,7 @@ def set_feeder_headnodes(self): def set_nominal_voltages_recur(self, *args): """This function sets the nominal voltage of the elements in the network. + This function is only used in the CYME Reader. This is currently the fastest implementation available as of early January 2018. It uses a kind os message passing algorithm. A node passes its nominal voltage to its succesors but modify this value if there is a voltage transformation. @@ -224,9 +225,10 @@ def set_nominal_voltages_recur(self, *args): previous = self.source else: node, voltage, previous = args - if (previous, node) in self.edge_equipment and self.edge_equipment[ - (previous, node) - ] == "PowerTransformer": + if ( + (previous, node) in self.edge_equipment and + self.edge_equipment[(previous, node)] == "PowerTransformer" + ): trans_name = self.edge_equipment_name[(previous, node)] new_value = min( [ @@ -235,9 +237,10 @@ def set_nominal_voltages_recur(self, *args): if w.nominal_voltage is not None ] ) - elif (node, previous) in self.edge_equipment and self.edge_equipment[ - (node, previous) - ] == "PowerTransformer": + elif ( + (node, previous) in self.edge_equipment and + self.edge_equipment[(node, previous)] == "PowerTransformer" + ): trans_name = self.edge_equipment_name[(node, previous)] new_value = min( [ @@ -248,8 +251,12 @@ def set_nominal_voltages_recur(self, *args): ) else: new_value = voltage + if hasattr(self.model[node], "nominal_voltage"): - self.model[node].nominal_voltage = new_value + if new_value != self.model[node].nominal_voltage: + if self.model[node].nominal_voltage is not None: + logger.warn(f"Setting node {node} nominal voltage to {new_value} from {self.model[node].nominal_voltage}") + self.model[node].nominal_voltage = new_value for child in self.G.digraph.successors(node): self.set_nominal_voltages_recur(child, new_value, node) @@ -520,205 +527,6 @@ def test_feeder_cut(self): ) logger.debug(intersection) - def set_nominal_voltages(self): - """This function does the exact same thing as _set_nominal_voltages. - The implementation is less obvious but should be much faster. - - **Algorithm:** - - - Find all edges modeling transformers in the network - - Disconnect these edges (which should disconnect the network) - - Compute the connected components - - Group the nodes according to these connected components - - Re-connect the network by adding back the removed edges - - For every group of nodes: - - Find the nominal voltage of one node (look at secondary voltage of the upstream transformer) - - All nodes in this group get the same nominal voltage - - For every line: - - Set nominal voltage as the nominal voltage of one of the end-points (that we have thanks to the previous loop...) - - .. note:: This should be faster than _set_nominal_voltages since we only look upstream once for every group instead of doing it once for every node. - - .. warning:: Use set_nominal_voltages_recur instead. - - .. TODO:: Find out why the results of this and set_nominal_voltages_recur don't match... - """ - self.model.set_names() - - # We will remove all edges representing transformers - edges_to_remove = [ - edge - for edge in self.G.graph.edges(data=True) - if "equipment" in edge[2] and edge[2]["equipment"] == "PowerTransformer" - ] - - # Do it!! - self.G.graph.remove_edges_from(edges_to_remove) - - # Get the connected components - cc = nx.connected_components(self.G.graph) - - # Extract the groups of nodes with same nominal voltage - node_mapping = [component for component in cc] - - # Restaure the network by addind back the edges previously removed - self.G.graph.add_edges_from(edges_to_remove) - - # Graph should be connected, otherwise we broke it... - assert nx.is_connected(self.G.graph) - - # Instanciate the list of nominal voltages (one value for each group) - nominal_voltage_group = [None for _ in node_mapping] - upstream_transformer_name_group = [None for _ in node_mapping] - - # For every group... - for idx, group in enumerate(node_mapping): - - # ...first node is volonteered to be searched - volonteer = group.pop() - while not isinstance(self.model[volonteer], Node): - volonteer = group.pop() - - # Get the name of the upstream transformer - upstream_transformer_name = self.G.get_upstream_transformer( - self.model, volonteer - ) - - # If we got None, there is nothing we can do. Otherwise... - if upstream_transformer_name is not None: - - # ...get the transformer object - upstream_transformer_object = self.model[upstream_transformer_name] - upstream_transformer_name_group[idx] = upstream_transformer_name - - # Get the nominal voltage of the secondary - if ( - hasattr(upstream_transformer_object, "windings") - and upstream_transformer_object.windings is not None - ): - - volts = [] - for winding in upstream_transformer_object.windings: - if ( - hasattr(winding, "nominal_voltage") - and winding.nominal_voltage is not None - ): - volts.append(winding.nominal_voltage) - - secondary_voltage = min(volts) - # And assign this value as the nominal voltage for the group of nodes - nominal_voltage_group[idx] = secondary_voltage - - # Now, we simply loop over all the groups - for idx, group in enumerate(node_mapping): - - # And all the nodes inside of the groups - for n in group: - - # And set the nominal voltage as the group value - self.model[n].nominal_voltage = nominal_voltage_group[idx] - if isinstance(self.model[n], Load): - self.model[ - n - ].upstream_transformer_name = upstream_transformer_name_group[idx] - - # Now we take care of the Lines. - # Since we should have the nominal voltage for every node (in a perfect world), - # We just have to grab the nominal voltage of one of the end-points. - for obj in self.model.models: - # If we get a line - if isinstance(obj, Line) and obj.nominal_voltage is None: - # Get the from node - if hasattr(obj, "from_element") and obj.from_element is not None: - node_from_object = self.model[obj.from_element] - - # If the from node has a known nominal voltage, then use this value - if ( - hasattr(node_from_object, "nominal_voltage") - and node_from_object.nominal_voltage is not None - ): - obj.nominal_voltage = node_from_object.nominal_voltage - - def _set_nominal_voltages(self): - """This function looks for all nodes and lines which have empty nominal_voltage fields. - The function goes upstream in the network representation to find a transformer. - The nominal voltage of the secondary of this transformer is then used to fill the empty nominal_voltage fields. - - .. warning:: DO NOT USE. Use set_nominal_voltages instead - - .. TODO:: Remove this once everything is stable. - """ - self.model.set_names() - # Loop over the objects - for obj in self.model.models: - - # If we get a Node with an empty nominal_voltage field - if isinstance(obj, Node) and obj.nominal_voltage is None: - # Get the upstream transformer name - try: - upstream_transformer_name = self.G.get_upstream_transformer( - self.model, obj.name - ) - except: - continue - if upstream_transformer_name is not None: - # Get the corresponding object - upstream_transformer_object = self.model[upstream_transformer_name] - # If possible, get all the winding voltages and select the minimum as the secondary voltage - if ( - hasattr(upstream_transformer_object, "windings") - and upstream_transformer_object.windings is not None - ): - volts = [] - for winding in upstream_transformer_object.windings: - if ( - hasattr(winding, "nominal_voltage") - and winding.nominal_voltage is not None - ): - volts.append(winding.nominal_voltage) - secondary_voltage = min(volts) - # Finally, assign this value to the object's nominal voltage - obj.nominal_voltage = secondary_voltage - - # If we get a line - if isinstance(obj, Line) and obj.nominal_voltage is None: - # Get the from node - if hasattr(obj, "from_element") and obj.from_element is not None: - node_from_object = self.model[obj.from_element] - - # If the from node has a known nominal voltage, then use this value - if ( - hasattr(node_from_object, "nominal_voltage") - and node_from_object.nominal_voltage is not None - ): - obj.nominal_voltage = node_from_object.nominal_voltage - - # Otherwise, do as before with the from node - # - # TODO: Put the following code into a function - # - else: - upstream_transformer_name = self.G.get_upstream_transformer( - self.model, node_from_object.name - ) - if upstream_transformer_name is not None: - upstream_transformer_object = self.model[ - upstream_transformer_name - ] - if ( - hasattr(upstream_transformer_object, "windings") - and upstream_transformer_object.windings is not None - ): - volts = [] - for winding in upstream_transformer_object.windings: - if ( - hasattr(winding, "nominal_voltage") - and winding.nominal_voltage is not None - ): - volts.append(winding.nominal_voltage) - secondary_voltage = min(volts) - obj.nominal_voltage = secondary_voltage - def center_tap_load_preprocessing(self): """Performs the center tap load pre-processing step. This function is responsible for setting the correct phase of center-tap loads. diff --git a/ditto/network/network.py b/ditto/network/network.py index 8133b572..7bdd27ca 100644 --- a/ditto/network/network.py +++ b/ditto/network/network.py @@ -15,17 +15,11 @@ class Network: def __init__(self): - self.graph = None - self.digraph = None # Doesn't contain attributes, just topology - self.class_map = ( - {} - ) # Map the networkx names to the object type (not included in attributes) - self.is_built = ( - False # Flag that indicates whether the Network has been built or not. - ) - self.attributes_set = ( - False # Flag that indicates whether the attributes have been set or not. - ) + self.graph = None # becomes nx.Graph() in self.build + self.digraph = None # becomes nx.DiGraph() in self.build. Doesn't contain attributes, just topology + self.class_map = {} # Map the networkx names to the object type (not included in attributes) + self.is_built = False # Flag that indicates whether the Network has been built or not. + self.attributes_set = False # Flag that indicates whether the attributes have been set or not. def provide_graphs(self, graph, digraph): """ @@ -346,7 +340,7 @@ def remove_open_switches(self, model): if self.digraph.has_edge(m.to_element, m.from_element): self.digraph.remove_edge(m.to_element, m.from_element) - def get_upstream_transformer(self, model, node): + def get_upstream_transformer(self, node): curr_node = node curr = list(self.digraph.predecessors(node)) @@ -369,21 +363,19 @@ def get_all_elements_downstream(self, model, source): model.set_names() # Checking that the network is already built - # TODO: Log instead of printing... if not self.is_built: - logger.debug( + logger.warn( "Warning. Trying to use Network model without building the network." ) - logger.debug("Calling build() with source={}".format(source)) + logger.warn("Calling build() with source={}".format(source)) self.build(model, source=source) # Checking that the attributes have been set - # TODO: Log instead of printing... if not self.attributes_set: - logger.debug( + logger.warn( "Warning. Trying to use Network model without setting the attributes first." ) - logger.debug("Setting the attributes...") + logger.warn("Setting the attributes...") self.set_attributes(model) # Run the dfs or die trying... diff --git a/ditto/readers/README.md b/ditto/readers/README.md new file mode 100644 index 00000000..25b0ca88 --- /dev/null +++ b/ditto/readers/README.md @@ -0,0 +1,26 @@ +# Readers design pattern + +## 1. Inherit from `AbstractReader` +The `AbstractReader.parse` method can optionally be overridden. The base class `parse` method calls: +1. `parse_nodes` +2. `parse_lines` +3. `parse_transformers` +4. `parse_loads` +5. `parse_regulators` +6. `parse_capacitors` +7. `parse_dg` +8. `if hasattr(self, "DSS_file_names")` then `parse_default_values` + +Each of these parse methods takes a single input argument, an instance of `Store`, commonly +called `model`. + +The `model` stores instances of the DiTTo power system objects defined in ditto/models/. + +## 2. Each `parse_x` method: +1. Define maps of header names to integer column location +2. Loop through network.txt/equipment.txt/load.txt + - use `parser_helper` for each line and each CYME object to get dictionaries of object values + - returns a dictionary of dictionaries, where each sub-dictionary contains the values of the desired attributes of a CYME object. + - TODO example output + - these dictionaries are stored in `self.settings`, which is emptied at the beginning of each `parse_x` method. +3. Loop through the parsed objects to create DiTTo model components \ No newline at end of file diff --git a/ditto/readers/cyme/read.py b/ditto/readers/cyme/read.py index 43012000..40e29cf5 100644 --- a/ditto/readers/cyme/read.py +++ b/ditto/readers/cyme/read.py @@ -2,7 +2,6 @@ import logging import math -import cmath import os from functools import reduce from six import string_types @@ -29,6 +28,7 @@ from ditto.models.photovoltaic import Photovoltaic from ditto.models.storage import Storage from ditto.models.phase_storage import PhaseStorage +from ditto.readers.cyme.utils import get_transformer_xhl_Rpercent, add_two_windings from ditto.models.base import Unicode from ditto.modify.system_structure import system_structure_modifier @@ -113,6 +113,8 @@ class Reader(AbstractReader): +-------------------------------------------+--------------------------------------------+ | 'transformer_settings' | '[TRANSFORMER SETTING]' | +-------------------------------------------+--------------------------------------------+ + | 'transformer_byphase_settings' | '[TRANSFORMER BYPHASE SETTING]' | + +-------------------------------------------+--------------------------------------------+ | 'auto_transformer' | '[AUTO TRANSFORMER]' | +-------------------------------------------+--------------------------------------------+ | 'grounding_transformer' | '[GROUNDING TRANSFORMER]' | @@ -160,6 +162,48 @@ class Reader(AbstractReader): +-------------------------------------------+--------------------------------------------+ | 'network_equivalent_setting' | '[NETWORK EQUIVALENT SETTING]' | +-------------------------------------------+--------------------------------------------+ + + (nlaws fall 2022) + The CYME import/export manual is essentially a list of the section headers and the column names + for each section. Most of the following has been deducted from actual CYME models and their + ASCII exports. + + An attempt at describing the CYME modeling concepts in CYME 9.0: + + The highest level ID (abstract object) appears to be a StructureID, + which can be found in: + - [HEADNODES], [STRUCTUREUDD], [UNCONNECTED NODES] + However, there can probably be more than one structure in a network, so + perhaps the NetworkID is the highest level ID. + It appears that a NetworkID is one to one with a substation or feeder. + + [HEADNODES] + - two mandatory fields: NodeID, NetworkID + - it appears that the NodeID in HEADNODES serve as the source nodes for each network + - source nodes should correspond to a substation or feeder (just like NetworkID) + + [SOURCE (EQUIVALENT)] + - I do not have an example of CYME export with a SOURCE section + - so using the SOURCE EQUIVALENT SECTION, the only mandatory field is NodeID + - in the examples that I have the [HEADNODES].NodeID are 1:1 with [SOURCE EQUIVALENT].NodeID + + [SECTION] + - the rest of the network connections are specified in the [SECTION] block + - the [SECTION] block is peculiar compared to the other object blocks because it can contain + more than one "TOPO", where a TOPO line starts with FORMAT_OBJNAME, for example: + - FORMAT_SECTION=SectionID,FromNodeID,FromNodeIndex,ToNodeID,ToNodeIndex,Phase,ZoneID,SubNetworkId + - FORMAT_FEEDER=NetworkID,HeadNodeID,CoordSet,Year,Description,Color,LoadFactor,LossLoadFactorK,Group1,Group2,Group3,Group4,Group5,Group6,North,South,East,West,AreaFilter,TagText,TagProperties,TagDeltaX,TagDeltaY,TagAngle,TagAlignment,TagBorder,TagBackground,TagTextColor,TagBorderColor,TagBackgroundColor,TagLocation,TagFont,TagTextSize,TagOffset,Version + - FORMAT_SUBSTATION=NetworkID,HeadNodeID,CoordSet,Year,Description,Color,LoadFactor,LossLoadFactorK,Group1,Group2,Group3,Group4,Group5,Group6,North,South,East,West,AreaFilter,TagText,TagProperties,TagDeltaX,TagDeltaY,TagAngle,TagAlignment,TagBorder,TagBackground,TagTextColor,TagBorderColor,TagBackgroundColor,TagLocation,TagFont,TagTextSize,TagOffset,Version + - with the exception of FORMAT_SECTION, any data line that uses FORMAT_OBJNAME must start with OBJNAME= + - for example, a feeder specification will look like: + - FEEDER=1234,SOURCE_1234,1, ... + - SectionID's tend to correspond to line specifications, but could also be a load or any piece of equipment (I think) + - perhaps the biggest gotcha in CYME is that more than one object can be in a section (i.e. between two nodes) + - for example, a node-transformer-line-node specification looks like: + - in [OVERHEADLINE SETTING] SectionID=sec1, DeviceNumber=dev1, LineCableID=lin1 + - in [TRANSFORMER SETTING] SectionID=sec1, FromNodeID=nd1 + - in [SECTION] SectionID=sec1, FromNodeID=nd1, ToNodeID=nd2 + - these issues are fixed in fix_section_overlaps """ register_names = ["cyme", "Cyme", "CYME"] @@ -198,6 +242,11 @@ def __init__(self, **kwargs): else: self.load_filename = "load.txt" + # optional load_model_id will parse only the loads with the corresponding LoadModelID (value in [CUSTOMER LOADS]) + self.load_model_id = None + if "load_model_id" in kwargs.keys(): + self.load_model_id = int(kwargs["load_model_id"]) + # Set the Network Type to be None. This is set in the parse_sections() function self.network_type = None @@ -254,6 +303,7 @@ def __init__(self, **kwargs): "[THREE WINDING TRANSFORMER SETTING]" ], "transformer_settings": ["[TRANSFORMER SETTING]"], + "transformer_byphase_settings": ["[TRANSFORMER BYPHASE SETTING]"], "phase_shifter_transformer_settings": [ "[PHASE SHIFTER TRANSFORMER SETTING]" ], @@ -328,9 +378,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 @@ -356,7 +406,8 @@ def get_file_content(self, filename): self.content = iter(content_) - def phase_mapping(self, CYME_value): + @staticmethod + def phase_mapping(CYME_value): """ Maps the CYME phase value format to a list of ABC phases: @@ -403,7 +454,8 @@ def phase_mapping(self, CYME_value): else: return list(CYME_value) - def phase_to_num(self, phase): + @staticmethod + def phase_to_num(phase): """ Maps phase in 'A', 'B', 'C' format in 1, 2, 3 format. @@ -428,7 +480,8 @@ def phase_to_num(self, phase): else: return phase - def load_value_type_mapping(self, load_type, value1, value2): + @staticmethod + def load_value_type_mapping(load_type, value1, value2): """ CYME customer loads provide two values v1 and v2 as well as a load value type: This function takes these as inputs and outputs P and Q of the load. @@ -503,7 +556,8 @@ def load_value_type_mapping(self, load_type, value1, value2): ) ) - def capacitors_connection_mapping(self, conn): + @staticmethod + def capacitors_connection_mapping(conn): """ Maps the capacitors connection in CYME (CAP_CONN) to DiTTo connection_type. @@ -540,7 +594,8 @@ def capacitors_connection_mapping(self, conn): else: return conn - def connection_configuration_mapping(self, value): + @staticmethod + def connection_configuration_mapping(value): """ Map the connection configuration from CYME to DiTTo. @@ -614,112 +669,7 @@ def connection_configuration_mapping(self, value): ) ) - def transformer_connection_configuration_mapping(self, value, winding): - """ - Map the connection configuration for transformer (2 windings) objects from CYME to DiTTo. - - :param value: CYME value (either string or id) - :type value: int or str - :param winding: Number of the winding (0 or 1) - :type winding: int - :returns: DiTTo connection configuration for the requested winding - :rtype: str - - **Mapping:** - - +----------+----------------+------------+ - | Value | CYME | DiTTo | - +----------+----------------+-----+------+ - | | | 1st | 2nd | - +==========+================+=====+======+ - | 0 or '0' | 'Y_Y' | 'Y' | 'Y' | - +----------+----------------+-----+------+ - | 1 or '1' | 'D_Y' | 'D' | 'Y' | - +----------+----------------+-----+------+ - | 2 or '2' | 'Y_D' | 'Y' | 'D' | - +----------+----------------+-----+------+ - | 3 or '3' | 'YNG_YNG' | 'Y' | 'Y' | - +----------+----------------+-----+------+ - | 4 or '4' | 'D_D' | 'D' | 'D' | - +----------+----------------+-----+------+ - | 5 or '5' | 'DO_DO' | 'D' | 'D' | - +----------+----------------+-----+------+ - | 6 or '6' | 'YO_DO' | 'Y' | 'D' | - +----------+----------------+-----+------+ - | 7 or '7' | 'D_YNG' | 'D' | 'Y' | - +----------+----------------+-----+------+ - | 8 or '8' | 'YNG_D' | 'Y' | 'D' | - +----------+----------------+-----+------+ - | 9 or '9' | 'Y_YNG' | 'Y' | 'Y' | - +----------+----------------+-----+------+ - |10 or '10'| 'YNG_Y' | 'Y' | 'Y' | - +----------+----------------+-----+------+ - |11 or '11'| 'Yg_Zg' | 'Y' | 'Z' | - +----------+----------------+-----+------+ - |12 or '12'| 'D_Zg' | 'D' | 'Z' | - +----------+----------------+-----+------+ - """ - if winding not in [0, 1]: - raise ValueError( - "transformer_connection_configuration_mapping expects an integer 0 or 1 for winding arg. {} was provided.".format( - winding - ) - ) - - res = (None, None) - - if isinstance(value, int): - if value == 0 or value == 3 or value == 9 or value == 10: - res = ("Y", "Y") - if value == 1 or value == 7: - res = ("D", "Y") - if value == 2 or value == 6 or value == 8: - res = ("Y", "D") - if value == 4 or value == 5: - res = ("D", "D") - if value == 11: - res = ("Y", "Z") - if value == 12: - res = ("D", "Z") - - elif isinstance(value, string_types): - if value == "0" or value.lower() == "y_y": - res = ("Y", "Y") - if value == "1" or value.lower() == "d_y": - res = ("D", "Y") - if value == "2" or value.lower() == "y_d": - res = ("Y", "D") - if value == "3" or value.lower() == "yng_yng": - res = ("Y", "Y") - if value == "4" or value.lower() == "d_d": - res = ("D", "D") - if value == "5" or value.lower() == "do_do": - res = ("D", "D") - if value == "6" or value.lower() == "yo_do": - res = ("Y", "D") - if value == "7" or value.lower() == "d_yng": - res = ("D", "Y") - if value == "8" or value.lower() == "yng_d": - res = ("Y", "D") - if value == "9" or value.lower() == "y_yng": - res = ("Y", "Y") - if value == "10" or value.lower() == "yng_y": - res = ("Y", "Y") - if value == "11" or value.lower() == "yg_zg": - res = ("Y", "Z") - if value == "12" or value.lower() == "d_zg": - res = ("D", "Z") - - else: - raise ValueError( - "transformer_connection_configuration_mapping expects an integer or a string. {} was provided.".format( - type(value) - ) - ) - - 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,22 +704,72 @@ 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: str, 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 line after `obj` is found. + The function returns a dictionary of dictionaries, where each sub-dictionary contains the values of the desired attributes of a CYME object. + + The first and only top-level key in the returned dictionary is set to the first comma separated value in the line + after the "format" line. For example, given + + - line="[TRANSFORMER]" + - obj="transformer" + - attribute_list=["kva", "kvllprim", "kvllsec"] + - args={"type": "transformer"} + + the following (truncated) example lines in network.txt: + + [TRANSFORMER] + FORMAT_TRANSFORMER=ID,Type,WindingType,KVA,VoltageUnit,KVLLprim,KVLLsec, ... + 1P_7.2KV/120/240V_25KVA,1,1,25.000000,0,12.470000,0.240000, ... + 1P_7.2KV/240/120V_50KVA,1,1,50.000000,0,12.470000,0.240000, ... + + will result in a returned dictionary like: + + { + "type": "transformer", + + "1P_7.2KV/120/240V_25KVA": { + "kva": 25.000000, + "kvllprim": 12.470000, + "kvllsec": 0.240000 + }, + + "1P_7.2KV/240/120V_50KVA": { + "kva": 50.000000, + "kvllprim": 12.470000, + "kvllsec": 0.240000 + }, + } + + Note that the `mapping` argument is not used in the above example because there is a "FORMAT_TRANSFORMER=" + line after the section header, which is used to define the `mapping`. + + + TODO is this case handled?!: (i.e. when the format is only temporary?) + [SECTION] + FORMAT_SECTION=SectionID,FromNodeID,FromNodeIndex,ToNodeID,ToNodeIndex,Phase,ZoneID,SubNetworkId + FORMAT_FEEDER=NetworkID,HeadNodeID,CoordSet,Year,Description,Color,LoadFactor,LossLoadFactorK,Group1,Group2,Group3,Group4,Group5,Group6,North,South,East,West,AreaFilter,TagText,TagProperties,TagDeltaX,TagDeltaY,TagAngle,TagAlignment,TagBorder,TagBackground,TagTextColor,TagBorderColor,TagBackgroundColor,TagLocation,TagFont,TagTextSize,TagOffset,Version + FEEDER=60803,SOURCE_60803,1,1662563223,,0,1.000000,0.150000,Lancaster,UNK,,,,,225423.609616,194367.254874,2452036.118000,2423405.968000,217563134,NULL,,,,,,,,,,,,,,,-1 + 43044S21249-L,2430449.596_212244.46,0,43044S21249-L,0,C,, + 43936S21443-L,2439387.172_214373.799,0,43936S21443-L,0,A,, + ... + + """ if isinstance(attribute_list, list): - attribute_list = np.array(attribute_list) + attribute_list = np.array(attribute_list) # why do we need an numpy array? if not isinstance(attribute_list, np.ndarray): raise ValueError("Could not cast attribute list to Numpy array.") if args and isinstance(args[0], dict): - additional_information = args[0] + additional_information = args[0] # typically {"type": "same-string-as-obj-arg"}, which gets added to the returned dict + # TODO make this update explicit if this is all that it is used for else: additional_information = {} @@ -783,11 +783,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 +806,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 +821,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 +859,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) @@ -878,50 +872,44 @@ def parse(self, model, **kwargs): """ Parse the CYME model to DiTTo. - :param model: DiTTo model - :type model: DiTTo model - :param verbose: Set the verbose mode. Optional. Default=True + :param model: DiTTo Store instance + :param verbose: Set the verbose mode. Optional. Default=False :type verbose: bool """ + self.verbose = False if "verbose" in kwargs and isinstance(kwargs["verbose"], bool): self.verbose = kwargs["verbose"] - else: - self.verbose = False - if self.verbose: - logger.info("Parsing the header...") + if self.verbose: logger.info("Parsing the header...") self.parse_header() + + if self.verbose: logger.info("Parsing the Headnodes...") + self.parse_head_nodes(model) - logger.info("Parsing the sections...") + if self.verbose: logger.info("Parsing the sections...") self.parse_sections(model) - logger.info("Parsing the sources...") - self.parse_sources(model) - # Call parse method of abtract reader super(Reader, self).parse(model, **kwargs) - logger.info("Parsing the network equivalents...") - self.parse_network_equivalent(model) + if self.verbose: logger.info("Parsing the subnetwork connections...") + self.parse_subnetwork_connections(model) - # The variable self.network_type is set in the parse_sections() function. - # i.e. parse_sections - if self.network_type == "substation": - logger.info("Parsing the subnetwork connections...") - self.parse_subnetwork_connections(model) - else: - logger.info("Parsing the Headnodes...") - self.parse_head_nodes(model) + if self.verbose: logger.info("Parsing the sources...") + self.parse_sources(model) - self.fix_section_overlaps(model) + if self.verbose: logger.info("Parsing the network equivalents...") + self.parse_network_equivalent(model) + self.fix_section_overlaps(model) + # overwriting 7_reg after this? yes. what is the point of set_names() ? it's called a bunch of times model.set_names() + if self.verbose: logger.info("Setting node and line nominal voltages...") modifier = system_structure_modifier(model) modifier.set_nominal_voltages_recur() modifier.set_nominal_voltages_recur_line() - def parse_header(self): """ Parse the information available in the header. @@ -966,9 +954,7 @@ def parse_header(self): self.cyme_version = cyme_version if self.use_SI is None: - raise ValueError( - "Could not find [SI] or [IMPERIAL] unit system information. Unable to parse." - ) + raise ValueError("Could not find [SI] or [IMPERIAL] unit system information. Unable to parse.") def parse_subnetwork_connections(self, model): """Parse the subnetwork connections. @@ -982,19 +968,21 @@ def parse_subnetwork_connections(self, model): self.subnetwork_connections.update( self.parser_helper( line, - ["subnetwork_connections"], + "subnetwork_connections", ["nodeid"], mapp_subnetwork_connections, ) ) for key in self.subnetwork_connections: - model[ - self.subnetwork_connections[key]["nodeid"] - ].is_substation_connection = True + model[self.subnetwork_connections[key]["nodeid"]].is_substation_connection = True def parse_head_nodes(self, model): - """ This parses the [HEADNODES] objects and is used to build Feeder_metadata DiTTo objects which define the feeder names and feeder headnodes""" + """ + This parses the [HEADNODES] objects and is used to build Feeder_metadata DiTTo objects which define the feeder names and feeder headnodes + HEADNODES maps NodeID <-> NetworkID + and NetworkID is used to identify SUBSTATION and FEEDER in [SECTION] + """ # Open the network file self.get_file_content("network") mapp = { @@ -1004,13 +992,15 @@ 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) ) + self.headnodes = headnodes - for sid, headnode in headnodes.items(): - feeder_metadata = Feeder_metadata(model) - feeder_metadata.name = headnode["networkid"].strip().lower() - feeder_metadata.headnode = headnode["nodeid"].strip().lower() + # for sid, headnode in headnodes.items(): + # feeder_metadata = Feeder_metadata(model) + # feeder_metadata.name = headnode["networkid"].strip().lower() + # feeder_metadata.headnode = headnode["nodeid"].strip().lower() + def parse_sources(self, model): """Parse the sources.""" @@ -1036,11 +1026,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 +1038,7 @@ def parse_sources(self, model): source_equivalents.update( self.parser_helper( line, - ["source_equivalent"], + "source_equivalent", [ "nodeid", "voltage", @@ -1078,7 +1068,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: @@ -1098,11 +1088,8 @@ def parse_sources(self, model): _from = v["tonodeid"] _to = v["fromnodeid"] phases = list(v["phase"]) - try: - api_source = PowerSource(model) - except: - pass + api_source = PowerSource(model) api_source.name = _from + "_src" try: @@ -1116,8 +1103,18 @@ def parse_sources(self, model): api_source.phases = phases except: pass - - api_source.is_sourcebus = True + + # If there is a substation then it should be the source bus not a feeder + # Or if there is only one source + # TODO multiple substations are multiple feeders without a substation? + if len(source_equivalent_data) == 1: + api_source.is_sourcebus = True + else: + if _from in self.headnodes.keys(): + if "type" in self.headnodes[_from]: + if self.headnodes[_from]["type"] == "substation": + api_source.is_sourcebus = True + try: api_source.rated_power = 10 ** 3 * float( @@ -1371,7 +1368,7 @@ def parse_nodes(self, model): nodes.update( self.parser_helper( line, - ["node"], + "node", ["nodeid", "coordx", "coordy", "ratedvoltage"], mapp, **kwargs @@ -1381,7 +1378,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,8 +1553,8 @@ def parse_sections(self, model): ... [SECTION] - FORMAT_section=sectionid,fromnodeid,tonodeid,phase - FORMAT_Feeder=networkid,headnodeid + 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 ... @@ -1565,8 +1562,11 @@ def parse_sections(self, model): Feeder=feeder_2,head_feeder_2 section_1_feeder_2,node_1,node_2,ABC ... + FORMAT_SUBSTATION=NetworkID,HeadNodeID + SUBSTATION=substation_name,substation_node_id, ... + **What is done in this function:** - We need to create a clear and fast mapping between feeders and sectionids @@ -1652,8 +1652,12 @@ def parse_sections(self, model): or line[:15].lower() == "generalnetwork=" ): self.network_type = "feeder" + network_type = "feeder" if line[:11].lower() == "substation=": self.network_type = "substation" + network_type = "substation" + # all sections parsed after this line are in a substation with the NetworkID + # from SUBSTATION=THE_NTWK_ID (until a new FORMAT is found) # We should have a format for sections and feeders, # otherwise, raise an error... @@ -1700,7 +1704,7 @@ def parse_sections(self, model): self.network_data[_netID][key] = value # Then, we create a new entry in feeder_section_mapping - self.feeder_section_mapping[_netID] = [] + self.feeder_section_mapping[_netID] = [] # never filled in ? # Otherwise, we should have a new section... else: @@ -1734,9 +1738,14 @@ def parse_sections(self, model): # Populate this new entry for key, value in zip(format_section, section_data): self.section_phase_mapping[_sectionID][key] = value + if key == "fromnodeid": + if value in self.headnodes.keys(): + self.headnodes[value]["type"] = network_type # And finally, add a new entry to section_feeder_mapping self.section_feeder_mapping[_sectionID] = _netID + # section_feeder_mapping should be section_network_mapping + # b/c network can be feeder or substation # Finally, move on to next line line = next(self.content) @@ -1947,7 +1956,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 +1973,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 +1990,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["overhead_byphase_settings"], + "overhead_byphase_settings", [ "sectionid", "devicenumber", @@ -2011,7 +2020,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 +2037,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 +2054,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 +2071,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 +2088,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 +2105,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 +2122,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 +2139,7 @@ def parse_lines(self, model): self.settings, self.parser_helper( line, - ["section"], + "section", ["sectionid", "fromnodeid", "tonodeid", "phase"], mapp_section, ), @@ -2157,7 +2166,7 @@ def parse_lines(self, model): self.balanced_lines.update( self.parser_helper( line, - ["line"], + "line", [ "id", "phasecondid", @@ -2185,7 +2194,7 @@ def parse_lines(self, model): self.unbalanced_lines.update( self.parser_helper( line, - ["unbalanced_line"], + "unbalanced_line", [ "id", "condid_a", @@ -2225,7 +2234,7 @@ def parse_lines(self, model): self.spacings.update( self.parser_helper( line, - ["spacing_table"], + "spacing_table", [ "id", "posofcond1_x", @@ -2252,7 +2261,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 +2276,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 +2300,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 +2314,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 +2327,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 +2342,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 +2357,7 @@ def parse_lines(self, model): self.sectionalizers.update( self.parser_helper( line, - ["sectionalizer"], + "sectionalizer", ["id", "amps", "kvll", "interruptingrating"], mapp_sectionalizers, ) @@ -2363,7 +2372,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 +2387,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 +3033,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" @@ -3848,13 +3856,33 @@ def parse_capacitors(self, model): "switchedkvara": 13, "switchedkvarb": 14, "switchedkvarc": 15, + "voltageoverride": 20, + "voltageoverrideon": 21, + "voltageoverrideoff": 22, "kv": 24, + "control": 25, + "onvaluea": 26, + "onvalueb": 27, + "onvaluec": 28, + "offvaluea": 29, + "offvalueb": 30, + "offvaluec": 31, "controllingphase": 35, } mapp_serie_capacitor = {"id": 0, "reactance": 6} mapp_shunt_capacitor = {"id": 0, "kvar": 1, "kv": 2, "type": 6} self.settings = {} self.capacitors = {} + cap_control_map = { # cyme names in comments, None's don't have an equivalent in Ditto model + 0: None, # "Manual Control", + 1: "voltage", # "Voltage Control" + 2: "currentFlow", # "Current Control" + 3: None, # "Reactive Current Control", + 4: "activePower", # "Power Factor Control", + 5: None, # "Temperature Control", + 6: "timeScheduled", # "Time Control", + 7: "reactivePower", # "KVAR Control", + } ##################################################### # # @@ -3877,7 +3905,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 +3921,7 @@ def parse_capacitors(self, model): self.settings.update( self.parser_helper( line, - ["shunt_capacitor_settings"], + "shunt_capacitor_settings", [ "sectionid", "shuntcapacitorid", @@ -3906,6 +3934,16 @@ def parse_capacitors(self, model): "switchedkvarc", "kv", "controllingphase", + "onvaluea", + "onvalueb", + "onvaluec", + "offvaluea", + "offvalueb", + "offvaluec", + "control", + "voltageoverride", + "voltageoverrideon", + "voltageoverrideoff", ], mapp_shunt_capacitor_settings, {"type": "shunt"}, @@ -3932,7 +3970,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 +3983,7 @@ def parse_capacitors(self, model): self.capacitors.update( self.parser_helper( line, - ["shunt_capacitor"], + "shunt_capacitor", ["id", "kvar", "kv", "type"], mapp_shunt_capacitor, ) @@ -3956,24 +3994,14 @@ def parse_capacitors(self, model): sectionID = sectionID.strip("*").lower() # Instanciate Capacitor DiTTo objects - try: - api_capacitor = Capacitor(model) - except: - raise ValueError( - "Unable to instanciate capacitor {id}".format(id=scap["sectionid"]) - ) + api_capacitor = Capacitor(model) # Set the name - try: - api_capacitor.name = "Cap_" + sectionID - except: - pass + api_capacitor.name = "Cap_" + sectionID # Set the connecting element (info is in the section) try: - api_capacitor.connecting_element = self.section_phase_mapping[ - sectionID - ]["fromnodeid"] + api_capacitor.connecting_element = self.section_phase_mapping[sectionID]["fromnodeid"] except: pass @@ -4008,12 +4036,11 @@ def parse_capacitors(self, model): pass # Get the device number + dev_num = None if "eqid" in settings: dev_num = settings["eqid"] elif "shuntcapacitorid" in settings: dev_num = settings["shuntcapacitorid"] - else: - dev_num = None capacitor_data = None if dev_num is not None: @@ -4066,95 +4093,98 @@ def parse_capacitors(self, model): "Capacitor {name} is monitoring phase {p} which is not in the section {id} phase list {lis}.".format( name=api_capacitor.name, p=api_capacitor.pt_phase, - id=scap["sectionid"], + id=sectionID, lis=phases, ) ) - # For each phase... + if "voltageoverride" in settings.keys() and settings["voltageoverride"]: + if "voltageoverrideon" in settings.keys(): + try: api_capacitor.vmin = float(settings["voltageoverrideon"]) + except: logger.warn(f"Could not set vmin for Capacitor {api_capacitor.name}") + if "voltageoverrideoff" in settings.keys(): + try: api_capacitor.vmax = float(settings["voltageoverrideoff"]) + except: logger.warn(f"Could not set vmax for Capacitor {api_capacitor.name}") + + # For each phase make a PhaseCapacitor and set its rating + on_values = [] + off_values = [] + control_mode_ints = [] for p in phases: - - # Instanciate a PhaseCapacitor DiTTo object - try: - api_phaseCapacitor = PhaseCapacitor(model) - except: - raise ValueError( - "Unable to instanciate PhaseCapacitor DiTTo object." - ) - - # Set the phase - try: - api_phaseCapacitor.phase = p - except: - pass + api_phaseCapacitor = PhaseCapacitor(model) + api_phaseCapacitor.phase = p # Set var value + fixed_key = "fixedkvar" + p.lower() # p is one of "A", "B", "C" + switched_key = "switchedkvar" + p.lower() if ( - "fixedkvara" in settings - and "fixedkvarb" in settings - and "fixedkvarc" in settings - and max( - float(settings["fixedkvara"]), - max( - float(settings["fixedkvarb"]), float(settings["fixedkvarc"]) - ), - ) - > 0 + fixed_key in settings.keys() and settings[fixed_key] is not None and + float(settings[fixed_key]) > 0 ): - try: - if p == "A": - api_phaseCapacitor.var = ( - float(settings["fixedkvara"]) * 10 ** 3 - ) # Ditto in var - if p == "B": - api_phaseCapacitor.var = ( - float(settings["fixedkvarb"]) * 10 ** 3 - ) # Ditto in var - if p == "C": - api_phaseCapacitor.var = ( - float(settings["fixedkvarc"]) * 10 ** 3 - ) # Ditto in var - except: - pass + api_phaseCapacitor.var = ( + float(settings[fixed_key]) * 10 ** 3 + ) # Ditto in var elif ( - "switchedkvara" in settings - and "switchedkvarb" in settings - and "switchedkvarc" in settings - and max( - float(settings["switchedkvara"]), - max( - float(settings["switchedkvarb"]), - float(settings["switchedkvarc"]), - ), - ) - > 0 + switched_key in settings.keys() and settings[switched_key] is not None and + float(settings[switched_key]) > 0 ): - try: - if p == "A": - api_phaseCapacitor.var = ( - float(settings["switchedkvara"]) * 10 ** 3 - ) # Ditto in var - if p == "B": - api_phaseCapacitor.var = ( - float(settings["switchedkvarb"]) * 10 ** 3 - ) # Ditto in var - if p == "C": - api_phaseCapacitor.var = ( - float(settings["switchedkvarc"]) * 10 ** 3 - ) # Ditto in var - except: - pass - - elif capacitor_data is not None: - try: + api_phaseCapacitor.var = ( + float(settings[switched_key]) * 10 ** 3 + ) # Ditto in var + api_phaseCapacitor.switch = True + elif capacitor_data is not None and "kvar" in capacitor_data.keys(): + if capacitor_data["kvar"] is not None: api_phaseCapacitor.var = ( float(capacitor_data["kvar"]) * 10 ** 3 ) # DiTTo in var - except: - pass + else: + continue # can't make a capacitor without a rating + + # check for on/off settings + on_key = "onvalue" + p.lower() + off_key = "offvalue" + p.lower() + + if on_key in settings.keys() and settings[on_key] is not None: + try: + if float(settings[on_key]) > 0: + on_values.append(float(settings[on_key])) + except ValueError: pass # empty string + + if off_key in settings.keys() and settings[off_key] is not None: + try: + if float(settings[off_key]) > 0: + off_values.append(float(settings[off_key])) + except ValueError: pass # empty string + + if "control" in settings.keys() and settings["control"] is not None: + try: + control_mode_ints.append(int(settings["control"])) + except ValueError: pass # empty string # Append the phase capacitor object to the capacitor + api_capacitor.measuring_element = sectionID # the line for control measurements api_capacitor.phase_capacitors.append(api_phaseCapacitor) + + if len(on_values) > 1: + if not all(x == on_values[0] for x in on_values[1:]): + logger.warn(f"Not all of the on values for capacitor in section {sectionID} are the same. Using the lowest value.") + api_capacitor.low = min(on_values) + elif len(on_values) == 1: + api_capacitor.low = on_values[0] + + if len(control_mode_ints) > 1: + if not all(x == control_mode_ints[0] for x in control_mode_ints[1:]): + logger.warn(f"Not all of the control modes for capacitor in section {sectionID} are the same. Using the first value.") + api_capacitor.mode = cap_control_map[control_mode_ints[0]] + elif len(control_mode_ints) == 1: + api_capacitor.mode = cap_control_map[control_mode_ints[0]] + + if len(off_values) > 1: + if not all(x == off_values[0] for x in off_values[1:]): + logger.warn(f"Not all of the off values for capacitor in section {sectionID} are the same. Using the highest value.") + api_capacitor.high = max(off_values) + elif len(off_values) == 1: + api_capacitor.high = off_values[0] self._capacitors.append(api_capacitor) if not sectionID in self.section_duplicates: @@ -4164,10 +4194,59 @@ def parse_capacitors(self, model): return 1 def parse_transformers(self, model): - """Parse the transformers from CYME to DiTTo. Since substation transformer can have LTCs attached, when parsing a transformer, we may also create a regulator. LTCs are represented as regulators.""" + """ + Parse the transformers from CYME to DiTTo. + Since substation transformer can have LTCs attached, when parsing a transformer, + we also create a regulator if "isltc" column is true. (LTCs are represented as regulators.) + CYME equipment that have "isltc" column include: + - TRANSFORMER + - AUTOTRANSFORMER + + The [TRANSFORMER] section specifies generic transformers that can be place in the + network via other section specifications. For example, + [TRANSFORMER BYPHASE SETTING] contains columns for + PhaseTransformerID1, PhaseTransformerID2, and PhaseTransformerID3 + which must have at least one (string) value that corresponds to [TRANSFORMER] ID + + List of sections parsed: + + # network.txt + NOTE: When parsing the network values the "type" key is set to the corresponding + equipment.txt name by passing {"type": "grounding_transformer"} for example + to `parser_helper` when parsing "grounding_transformer_settings". + + NOTE: seems like the SETTING values have required SectionID and EqID fields but + it may not be consistent (for example PHOTOVOLTAIC SETTINGS has a required + EquipmentID field and a rewquired DeviceNumber field) + + NOTE: all of these SETTING sections start with SectionID (but there are exceptions + for other SETTING sections such as [CONVERTER CONTROL SETTING] that starts with DeviceNumber) + + - [AUTO TRANSFORMER SETTING] + - [GROUNDINGTRANSFORMER SETTINGS] + - [THREE WINDING AUTO TRANSFORMER SETTING] + - [THREE WINDING TRANSFORMER SETTING] + - [TRANSFORMER SETTING] + - [TRANSFORMER BYPHASE SETTING] + - [PHASE SHIFTER TRANSFORMER SETTING] + + # equipment.txt + NOTE: all equipment sections start with ID (not SectionID) + - [AUTO TRANSFORMER] + - [GROUNDING TRANSFORMER] + - [THREE WINDING AUTO TRANSFORMER] + - [THREE WINDING TRANSFORMER] + - [TRANSFORMER] + - TODO add [PHASE SHIFTER TRANSFORMER] + + NOTE: it appears that all BYPHASE values are only SETTINGs (no BYPHASE equipment) and so + TRANSFORMER BYPHASE SETTING defines values for a TRANSFORMER (there are also + REGULATOR BYHPASE SETTING and OVERHEAD BYPHASE SETTING) + """ # Instanciate the list in which we store the DiTTo transformer objects self._transformers = [] + # maps of object column names to indices mapp_auto_transformer_settings = { "sectionid": 0, "eqid": 2, @@ -4283,7 +4362,15 @@ def parse_transformers(self, model): "coordx": 10, "coordy": 11, } + mapp_transformer_byphase_settings = { + "sectionid": 0, + "phasetransformerid1": 8, # maps to TRANSFORMER ID + "phasetransformerid2": 9, + "phasetransformerid3": 10, + "feedingnode": 16 + } + # empty dicts to fill from txt files self.auto_transformers = {} self.grounding_transformers = {} self.three_winding_auto_transformers = {} @@ -4300,19 +4387,19 @@ def parse_transformers(self, model): # Open the network file self.get_file_content("network") - # Loop over the network file + # Loop over the network file to get the SETTINGS for line in self.content: ######################################### # # - # AUTO TRANSFORMER # + # AUTO TRANSFORMER SETTINGS # # # ######################################### # self.settings.update( self.parser_helper( line, - ["auto_transformer_settings"], + "auto_transformer_settings", [ "sectionid", "eqid", @@ -4328,30 +4415,30 @@ def parse_transformers(self, model): ######################################### # # - # GROUNDING TRANSFORMER # + # GROUNDING TRANSFORMER SETTINGS # # # ######################################### # self.settings.update( self.parser_helper( line, - ["grounding_transformer_settings"], + "grounding_transformer_settings", ["sectionid", "equipmentid", "connectionconfiguration", "phase"], mapp_grounding_transformer_settings, {"type": "grounding_transformer"}, ) ) - ######################################### - # # - # THREE WINDING AUTO TRANSFORMER # - # # - ######################################### + ########################################### + # # + # THREE WINDING AUTO TRANSFORMER SETTINGS # + # # + ########################################### # self.settings.update( self.parser_helper( line, - ["three_winding_auto_transformer_settings"], + "three_winding_auto_transformer_settings", [ "sectionid", "eqid", @@ -4371,14 +4458,14 @@ def parse_transformers(self, model): ######################################### # # - # THREE WINDING TRANSFORMER # + # THREE WINDING TRANSFORMER SETTINGS # # # ######################################### # self.settings.update( self.parser_helper( line, - ["three_winding_transformer_settings"], + "three_winding_transformer_settings", [ "sectionid", "eqid", @@ -4398,25 +4485,23 @@ def parse_transformers(self, model): ######################################### # # - # TRANSFORMER # + # TRANSFORMER SETTINGS # # # ######################################### # self.settings.update( self.parser_helper( line, - ["transformer_settings"], + "transformer_settings", [ "sectionid", "eqid", "coordx", "coordy", - "primaryfixedtapsetting", - "secondaryfixedtapsetting", - "tertiaryfixedtapsetting", + "primtap", + "secondarytap", "primarybasevoltage", "secondarybasevoltage", - "tertiarybasevoltage", "setpoint", "maxbuck", "maxboost", @@ -4430,14 +4515,30 @@ def parse_transformers(self, model): ######################################### # # - # PHASE SHIFTER TRANSFORMER # + # TRANSFORMER BYPHASE SETTING # + # # + ######################################### + + self.settings.update( # why self.settings? (and not a local dict?) + self.parser_helper( + line, + "transformer_byphase_settings", + list(mapp_transformer_byphase_settings.keys()), # why a list of keys to parse separate from the mapp dict? + mapp_transformer_byphase_settings, + {"type": "transformer_byphase"}, + ) + ) + + ######################################### + # # + # PHASE SHIFTER TRANSFORMER SETTINGS # # # ######################################### # 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 +4566,7 @@ def parse_transformers(self, model): self.auto_transformers.update( self.parser_helper( line, - ["auto_transformer"], + "auto_transformer", [ "id", "kva", @@ -4489,7 +4590,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 +4606,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 +4631,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 +4655,7 @@ def parse_transformers(self, model): self.transformers.update( self.parser_helper( line, - ["transformer"], + "transformer", [ "id", "type", @@ -4577,6 +4678,7 @@ def parse_transformers(self, model): ) ) + # for each line in a SETTING section define a PowerTransformer for sectionID, settings in self.settings.items(): sectionID = sectionID.strip("*").lower() @@ -4597,6 +4699,7 @@ def parse_transformers(self, model): try: phases = self.section_phase_mapping[sectionID]["phase"] + # phases is a string of length 1-3 with any combination of "A", "B", "C" except: raise ValueError("Empty phases for transformer {}.".format(sectionID)) @@ -4739,54 +4842,14 @@ def parse_transformers(self, model): else: transformer_data = self.transformers["DEFAULT"] - # Resistance - # - # Note: Imported from Julietta's code - # - Z1 = float(transformer_data["z1"]) - Z0 = float(transformer_data["z0"]) - XR = float(transformer_data["xr"]) - XR0 = float(transformer_data["xr0"]) - if XR == 0: - R1 = 0 - X1 = 0 - else: - R1 = Z1 / math.sqrt(1 + XR * XR) - X1 = Z1 / math.sqrt(1 + 1 / (XR * XR)) - if XR0 == 0: - R0 = 0 - X0 = 0 - else: - R0 = Z0 / math.sqrt(1 + XR0 * XR0) - X0 = Z0 / math.sqrt(1 + 1 / (XR0 * XR0)) - complex0 = complex(R0, X0) - complex1 = complex(R1, X1) - matrix = np.array( - [[complex0, 0, 0], [0, complex1, 0], [0, 0, complex1]] - ) - a = 1 * cmath.exp(2 * math.pi * 1j / 3) - T = np.array([[1.0, 1.0, 1.0], [1.0, a * a, a], [1.0, a, a * a]]) - T_inv = np.linalg.inv(T) - Zabc = T * matrix * T_inv - Z_perc = Zabc.item((0, 0)) - R_perc = Z_perc.real / 2.0 - xhl = Z_perc.imag + xhl, R_perc = get_transformer_xhl_Rpercent(transformer_data) # Check if it's an LTC # if "isltc" in transformer_data and transformer_data["isltc"]: # Instanciate a Regulator DiTTo object - try: - api_regulator = Regulator(model) - except: - raise ValueError( - "Unable to instanciate Regulator DiTTo object." - ) - - try: - api_regulator.name = "Reg_" + settings["sectionid"] - except: - pass + api_regulator = Regulator(model) + api_regulator.name = "Reg_" + settings["sectionid"] api_regulator.feeder_name = self.section_feeder_mapping[sectionID] try: @@ -4823,77 +4886,8 @@ def parse_transformers(self, model): pass # Here we know that we have two windings... - for w in range(2): - - # Instanciate a Winding DiTTo object - try: - api_winding = Winding(model) - except: - raise ValueError("Unable to instanciate Winding DiTTo object.") - - # Set the rated power - try: - if w == 0: - api_winding.rated_power = ( - float(transformer_data["kva"]) * 10 ** 3 - ) # DiTTo in volt ampere - if w == 1: - api_winding.rated_power = ( - float(transformer_data["kva"]) * 10 ** 3 - ) # DiTTo in volt ampere - except: - pass - - # Set the nominal voltage - try: - if w == 0: - api_winding.nominal_voltage = ( - float(transformer_data["kvllprim"]) * 10 ** 3 - ) # DiTTo in volt - if w == 1: - api_winding.nominal_voltage = ( - float(transformer_data["kvllsec"]) * 10 ** 3 - ) # DiTTo in volt - except: - pass - - # Connection configuration - try: - api_winding.connection_type = self.transformer_connection_configuration_mapping( - transformer_data["conn"], w - ) - except: - pass - - # Resistance - try: - api_winding.resistance = R_perc - except: - pass - - # For each phase... - for p in phases: - - # Instanciate a PhaseWinding DiTTo object - try: - api_phase_winding = PhaseWinding(model) - except: - raise ValueError( - "Unable to instanciate PhaseWinding DiTTo object." - ) - - # Set the phase - try: - api_phase_winding.phase = p - except: - pass - - # Add the phase winding object to the winding - api_winding.phase_windings.append(api_phase_winding) - - # Add the winding object to the transformer - api_transformer.windings.append(api_winding) - + add_two_windings(api_transformer, transformer_data, settings, model, phases, R_perc) + # Handle Grounding transformers if settings["type"] == "grounding_transformer": @@ -4915,14 +4909,9 @@ def parse_transformers(self, model): # Set the rated power try: - if w == 0: - api_winding.rated_power = ( - float(transformer_data["ratedcapacity"]) * 10 ** 3 - ) # DiTTo in volt ampere - if w == 1: - api_winding.rated_power = ( - float(transformer_data["ratedcapacity"]) * 10 ** 3 - ) # DiTTo in volt ampere + api_winding.rated_power = ( + float(transformer_data["ratedcapacity"]) * 10 ** 3 + ) # DiTTo in volt ampere except: pass @@ -4970,6 +4959,27 @@ def parse_transformers(self, model): # Add the winding object to the transformer api_transformer.windings.append(api_winding) + if settings["type"] == "transformer_byphase": + # at least one PhaseTransformerID is required, which corresponds + # with a self.transformers key + + trfx_id = None + for phs in [1,2,3]: + k = "phasetransformerid"+str(phs) + if ( + k in settings.keys() and + len(settings[k]) > 1 + ): + trfx_id = settings[k] + break + # TODO is it possible for TRANSFORMER BYPHASE SETTING to have more than one PhaseTransformerID? + + if trfx_id is not None and trfx_id in self.transformers.keys(): + transformer_data = self.transformers[trfx_id] + xhl, R_perc = get_transformer_xhl_Rpercent(transformer_data) + api_transformer.reactances = [float(xhl)] + add_two_windings(api_transformer, transformer_data, settings, model, phases, R_perc) + # Add the transformer object to the list of transformers self._transformers.append(api_transformer) if not sectionID in self.section_duplicates: @@ -5012,6 +5022,7 @@ def parse_regulators(self, model): "phaseon": 9, "ct": 12, "pt": 13, + "settingoption": 14, # TODO map, "T" = terminal, i.e. control voltage at regulator terminal (secondary winding in OpenDSS, use bus= in RegControl) "vseta": 16, "vsetb": 17, "vsetc": 18, @@ -5042,7 +5053,7 @@ def parse_regulators(self, model): self.settings.update( self.parser_helper( line, - ["regulator_settings"], + "regulator_settings", [ "sectionid", "eqid", @@ -5081,7 +5092,7 @@ def parse_regulators(self, model): self.regulators.update( self.parser_helper( line, - ["regulator"], + "regulator", [ "id", "type", @@ -5242,10 +5253,7 @@ def parse_regulators(self, model): for w in range(2): # Instanciate a Winding DiTTo object - try: - api_winding = Winding(model) - except: - raise ValueError("Unable to instanciate Winding DiTTo object.") + api_winding = Winding(model) # Set the rated power try: @@ -5265,7 +5273,7 @@ def parse_regulators(self, model): # Set the nominal voltage try: - api_winding.nominal_voltage = float(regulator_data["kvln"]) + api_winding.nominal_voltage = float(regulator_data["kvln"]) * 10 ** 3 except: pass @@ -5359,7 +5367,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 +5385,7 @@ def parse_network_equivalent(self, model): self.settings, self.parser_helper( line, - ["section"], + "section", ["sectionid", "fromnodeid", "tonodeid", "phase"], mapp_section, ), @@ -5557,9 +5565,6 @@ def parse_network_equivalent(self, model): api_load_to.phase_loads.append(api_phase_load_to) - - - def parse_loads(self, model): """Parse the loads from CYME to DiTTo.""" # Instanciate the list in which we store the DiTTo load objects @@ -5621,7 +5626,7 @@ def parse_loads(self, model): self.loads.update( self.parser_helper( line, - ["loads"], + "loads", ["sectionid", "devicenumber", "loadtype", "connection"], mapp_loads, ) @@ -5636,7 +5641,7 @@ def parse_loads(self, model): self.customer_loads.update( self.parser_helper( line, - ["customer_loads"], + "customer_loads", [ "sectionid", "devicenumber", @@ -5664,7 +5669,7 @@ def parse_loads(self, model): self.customer_class.update( self.parser_helper( line, - ["customer_class"], + "customer_class", [ "id", "constantpower", @@ -5682,28 +5687,43 @@ def parse_loads(self, model): ) ) - duplicate_loads = set() + section_ids_with_more_than_one_load = set() for sectionID in self.customer_loads.keys(): if sectionID.endswith("*"): - duplicate_loads.add(sectionID.lower().strip("*")) + section_ids_with_more_than_one_load.add(sectionID.lower().strip("*")) + for sectionID, settings in self.customer_loads.items(): + """ + if more than one loadmodelid is in Loads.txt then all of the loadmodels get added together in one Load object. + for example, four differnt loadmodelids on a phase Cresults in 4 loads on busName.3.3.3.3 added together :/ + However, if the Reader is instantiated with a load_model_id then we only parse the loads with that load_model_id. + """ + if self.load_model_id is not None and "loadmodelid" in settings: + # we only want to create a load if it has the matching LoadModelID + try: + if self.load_model_id != int(settings["loadmodelid"]): + continue + except ValueError: + logger.warn(f"Cannot convert LoadModelID {settings['loadmodelid']} for {sectionID} to integer.") + sectionID = sectionID.strip("*").lower() if sectionID in self.loads: - load_data = self.loads[sectionID] + # FORMAT_LOADS=SectionID,DeviceNumber,DeviceStage,Flags,LoadType,Connection,Location + load_data = self.loads[sectionID] else: load_data = {} + connectedkva = None if "connectedkva" in settings: connectedkva = float(settings["connectedkva"]) - else: - connectedkva = None + value_type = None if "valuetype" in settings: value_type = int(settings["valuetype"]) - if "value1" in settings and "value2" in settings: + if "value1" in settings and "value2" in settings and value_type is not None: if ( float(settings["value1"]) == 0.0 and float(settings["value2"]) == 0.0 @@ -5714,9 +5734,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,48 +5747,45 @@ 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 - if p >= 0 or q >= 0: + if p >= 0 or q >= 0: # then a Load is created + phases = [] if "loadphase" in settings: phases = settings["loadphase"] - else: - phases = [] - fused = False - if sectionID in duplicate_loads: + if sectionID in section_ids_with_more_than_one_load: fusion = True - if sectionID in self._loads: + if sectionID in self._loads: # already created a Load for this sectionID api_load = self._loads[sectionID] fused = True - elif p != 0: + elif p != 0: # have not created a load yet api_load = Load(model) else: fusion = False @@ -5784,9 +5799,8 @@ def parse_loads(self, model): try: if fusion and sectionID in self._loads: - api_load.name += "_" + reduce( - lambda x, y: x + "_" + y, phases - ) + # add phases to name with underscores + api_load.name += "_" + reduce(lambda x, y: x + "_" + y, phases) else: api_load.name = ( "Load_" @@ -5800,18 +5814,12 @@ def parse_loads(self, model): try: if not (fusion and sectionID in self._loads): if connectedkva is not None: - api_load.transformer_connected_kva = ( - connectedkva * 10 ** 3 - ) # DiTTo in var + api_load.transformer_connected_kva = (connectedkva * 10 ** 3) # DiTTo in var elif connectedkva is not None: if api_load.transformer_connected_kva is None: - api_load.transformer_connected_kva = ( - connectedkva * 10 ** 3 - ) # DiTTo in var + api_load.transformer_connected_kva = (connectedkva * 10 ** 3) # DiTTo in var else: - api_load.transformer_connected_kva += ( - connectedkva * 10 ** 3 - ) # DiTTo in var + api_load.transformer_connected_kva += (connectedkva * 10 ** 3) # DiTTo in var except: pass @@ -5845,12 +5853,7 @@ def parse_loads(self, model): api_load.num_users = float(settings["numberofcustomer"]) for ph in phases: - try: - api_phase_load = PhaseLoad(model) - except: - raise ValueError( - "Unable to instanciate PhaseLoad DiTTo object." - ) + api_phase_load = PhaseLoad(model) try: api_phase_load.phase = ph @@ -5896,7 +5899,6 @@ def parse_loads(self, model): # if api_phase_load.p!=0 or api_phase_load.q!=0: api_load.phase_loads.append(api_phase_load) - self._loads[sectionID] = api_load if not sectionID in self.section_duplicates: self.section_duplicates[sectionID] = [] @@ -6025,7 +6027,7 @@ def parse_dg(self, model): self.converter.update( self.parser_helper( line, - ["converter"], + "converter", [ "devicenumber", "devicetype", @@ -6051,7 +6053,7 @@ def parse_dg(self, model): self.converter_settings.update( self.parser_helper( line, - ["converter_control_settings"], + "converter_control_settings", [ "devicenumber", "devicetype", @@ -6076,7 +6078,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 +6094,7 @@ def parse_dg(self, model): self.bess_settings.update( self.parser_helper( line, - ["bess_settings"], + "bess_settings", [ "sectionid", "devicenumber", @@ -6116,7 +6118,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 +6139,7 @@ def parse_dg(self, model): self.dg_generation.update( self.parser_helper( line, - ["dggenerationmodel"], + "dggenerationmodel", [ "devicenumber", "devicetype", @@ -6171,7 +6173,7 @@ def parse_dg(self, model): self.bess.update( self.parser_helper( line, - ["bess"], + "bess", [ "id", "ratedstorageenergy", @@ -6464,9 +6466,9 @@ def fix_section_overlaps(self, model, **kwargs): for i,j in self.section_duplicates.items(): if len(j)>1: multiple_elements[i] = j + # multiple_elements has keys of sectionID and values as list of DiTTo objects for sectionID in multiple_elements: - connectors = [] regulators = [] transformers = [] lines = [] #Warning - if multiple lines are used the names will be the same @@ -6508,6 +6510,11 @@ def fix_section_overlaps(self, model, **kwargs): if from_element is None or to_element is None: # i.e. just loads, pvs and caps so no problem continue + # if all we have is (single phase) regulators between nodes no need to fix it + # (looks like parse_regulators creates a Regulator for each phase) + if all(len(el) == 0 for el in [transformers, lines, loads, bess, capacitors]): + continue + original_from_element = from_element original_from_node = model[from_element] intermediate_count = 0 @@ -6517,11 +6524,11 @@ def fix_section_overlaps(self, model, **kwargs): element.from_element = from_element # Regulators go between the same two nodes - if isinstance(element,Regulator): + if isinstance(element, Regulator): from_element = original_from_element+'_reg' else: from_element = original_from_element+'_sec_'+str(intermediate_count) - intermediate_count +=1 + intermediate_count += 1 if intermediate_count != connector_count: element.to_element = from_element api_node = Node(model) diff --git a/ditto/readers/cyme/utils.py b/ditto/readers/cyme/utils.py new file mode 100644 index 00000000..87175b34 --- /dev/null +++ b/ditto/readers/cyme/utils.py @@ -0,0 +1,256 @@ +import numpy as np +import math +import cmath +from ditto.store import Store +from ditto.models.winding import Winding +from ditto.models.phase_winding import PhaseWinding +from ditto.models.powertransformer import PowerTransformer +from six import string_types + + +def get_transformer_xhl_Rpercent(trfx: dict): + # Resistance + # Note: Imported from Julietta's code + Z1 = float(trfx["z1"]) # TODO default values + Z0 = float(trfx["z0"]) + XR = float(trfx["xr"]) + XR0 = float(trfx["xr0"]) + if XR == 0: + R1 = 0 + X1 = 0 + else: + R1 = Z1 / math.sqrt(1 + XR * XR) + X1 = Z1 / math.sqrt(1 + 1 / (XR * XR)) + if XR0 == 0: + R0 = 0 + X0 = 0 + else: + R0 = Z0 / math.sqrt(1 + XR0 * XR0) + X0 = Z0 / math.sqrt(1 + 1 / (XR0 * XR0)) + complex0 = complex(R0, X0) + complex1 = complex(R1, X1) + matrix = np.array( + [[complex0, 0, 0], [0, complex1, 0], [0, 0, complex1]] + ) + a = 1 * cmath.exp(2 * math.pi * 1j / 3) + T = np.array([[1.0, 1.0, 1.0], [1.0, a * a, a], [1.0, a, a * a]]) + T_inv = np.linalg.inv(T) + Zabc = T * matrix * T_inv + Z_perc = Zabc.item((0, 0)) + R_perc = Z_perc.real / 2.0 + xhl = Z_perc.imag + return xhl, R_perc + + +def transformer_connection_configuration_mapping(value, winding): + """ + Map the connection configuration for transformer (2 windings) objects from CYME to DiTTo. + + :param value: CYME value (either string or id) + :type value: int or str + :param winding: Number of the winding (0 or 1) + :type winding: int + :returns: DiTTo connection configuration for the requested winding + :rtype: str + + **Mapping:** + + +----------+----------------+------------+ + | Value | CYME | DiTTo | + +----------+----------------+-----+------+ + | | | 1st | 2nd | + +==========+================+=====+======+ + | 0 or '0' | 'Y_Y' | 'Y' | 'Y' | + +----------+----------------+-----+------+ + | 1 or '1' | 'D_Y' | 'D' | 'Y' | + +----------+----------------+-----+------+ + | 2 or '2' | 'Y_D' | 'Y' | 'D' | + +----------+----------------+-----+------+ + | 3 or '3' | 'YNG_YNG' | 'Y' | 'Y' | + +----------+----------------+-----+------+ + | 4 or '4' | 'D_D' | 'D' | 'D' | + +----------+----------------+-----+------+ + | 5 or '5' | 'DO_DO' | 'D' | 'D' | + +----------+----------------+-----+------+ + | 6 or '6' | 'YO_DO' | 'Y' | 'D' | + +----------+----------------+-----+------+ + | 7 or '7' | 'D_YNG' | 'D' | 'Y' | + +----------+----------------+-----+------+ + | 8 or '8' | 'YNG_D' | 'Y' | 'D' | + +----------+----------------+-----+------+ + | 9 or '9' | 'Y_YNG' | 'Y' | 'Y' | + +----------+----------------+-----+------+ + |10 or '10'| 'YNG_Y' | 'Y' | 'Y' | + +----------+----------------+-----+------+ + |11 or '11'| 'Yg_Zg' | 'Y' | 'Z' | + +----------+----------------+-----+------+ + |12 or '12'| 'D_Zg' | 'D' | 'Z' | + +----------+----------------+-----+------+ + """ + if winding not in [0, 1]: + raise ValueError( + "transformer_connection_configuration_mapping expects an integer 0 or 1 for winding arg. {} was provided.".format( + winding + ) + ) + + res = (None, None) + + if isinstance(value, int): + if value == 0 or value == 3 or value == 9 or value == 10: + res = ("Y", "Y") + if value == 1 or value == 7: + res = ("D", "Y") + if value == 2 or value == 6 or value == 8: + res = ("Y", "D") + if value == 4 or value == 5: + res = ("D", "D") + if value == 11: + res = ("Y", "Z") + if value == 12: + res = ("D", "Z") + + elif isinstance(value, string_types): + if value == "0" or value.lower() == "y_y": + res = ("Y", "Y") + if value == "1" or value.lower() == "d_y": + res = ("D", "Y") + if value == "2" or value.lower() == "y_d": + res = ("Y", "D") + if value == "3" or value.lower() == "yng_yng": + res = ("Y", "Y") + if value == "4" or value.lower() == "d_d": + res = ("D", "D") + if value == "5" or value.lower() == "do_do": + res = ("D", "D") + if value == "6" or value.lower() == "yo_do": + res = ("Y", "D") + if value == "7" or value.lower() == "d_yng": + res = ("D", "Y") + if value == "8" or value.lower() == "yng_d": + res = ("Y", "D") + if value == "9" or value.lower() == "y_yng": + res = ("Y", "Y") + if value == "10" or value.lower() == "yng_y": + res = ("Y", "Y") + if value == "11" or value.lower() == "yg_zg": + res = ("Y", "Z") + if value == "12" or value.lower() == "d_zg": + res = ("D", "Z") + + else: + raise ValueError( + "transformer_connection_configuration_mapping expects an integer or a string. {} was provided.".format( + type(value) + ) + ) + + return res[winding] + + +def add_two_windings( + api_transformer: PowerTransformer, + trfx_data: dict, + settings: dict, + model: Store, + phases: str, + R_perc: float + ) -> None: + """ + Add Winding and PhaseWinding objects to PowerTransformer + using `trfx_data` from the [TRANSFORMER] section + and `settings` from either [TRANFORMER SETTINGS] or [TRANSFORMER BYPHASE SETTINGS] + """ + + # Here we know that we have two windings... + for w in range(2): + + # Instanciate a Winding DiTTo object + try: + api_winding = Winding(model) + except: + raise ValueError("Unable to instanciate Winding DiTTo object.") + + # Set the rated power + try: + api_winding.rated_power = ( + float(trfx_data["kva"]) * 10 ** 3 + ) # DiTTo in volt ampere + except: + pass + + # Set the nominal voltage + try: + scaler = 1 + if len(phases) == 1: + scaler = 1/math.sqrt(3) + if w == 0: + api_winding.nominal_voltage = ( + float(trfx_data["kvllprim"]) * 10 ** 3 * scaler + ) # DiTTo in volt + if w == 1: + api_winding.nominal_voltage = ( + float(trfx_data["kvllsec"]) * 10 ** 3 * scaler + ) # DiTTo in volt + except: + pass + + # Set base voltage + try: + if w == 0: + api_winding.base_voltage = ( + float(settings["primarybasevoltage"]) * 10 ** 3 + ) # DiTTo in volt + if w == 1: + api_winding.base_voltage = ( + float(settings["secondarybasevoltage"]) * 10 ** 3 + ) # DiTTo in volt + except: + pass + + + # Connection configuration + try: + api_winding.connection_type = transformer_connection_configuration_mapping( + trfx_data["conn"], w + ) + except: + pass + + # Resistance + try: + api_winding.resistance = R_perc + except: + pass + + # For each phase... + for p in phases: + + # Instanciate a PhaseWinding DiTTo object + try: + api_phase_winding = PhaseWinding(model) + except: + raise ValueError( + "Unable to instanciate PhaseWinding DiTTo object." + ) + # Set the phase + try: + api_phase_winding.phase = p + except: + pass + + # set tap + tapkey = "primtap" if w == 0 else "secondarytap" + if ( + tapkey in settings.keys() and + settings[tapkey] is not None + ): + api_phase_winding.tap_position = float(settings[tapkey]) / 100 + + # Add the phase winding object to the winding + api_winding.phase_windings.append(api_phase_winding) + + # Add the winding object to the transformer + api_transformer.windings.append(api_winding) + + return None diff --git a/ditto/store.py b/ditto/store.py index e6f53b7f..47663ec2 100644 --- a/ditto/store.py +++ b/ditto/store.py @@ -187,9 +187,7 @@ def set_node_voltages(self): self.set_names() for i in self.models: if isinstance(i, Node) and hasattr(i, "name") and i.name is not None: - upstream_transformer = self._network.get_upstream_transformer( - self, i.name - ) + upstream_transformer = self._network.get_upstream_transformer(i.name) try: upstream_voltage = ( self[upstream_transformer].windings[-1].nominal_voltage diff --git a/ditto/writers/opendss/write.py b/ditto/writers/opendss/write.py index 119422f4..443ecb5b 100644 --- a/ditto/writers/opendss/write.py +++ b/ditto/writers/opendss/write.py @@ -132,7 +132,7 @@ def __init__(self, **kwargs): # Call super super(Writer, self).__init__(**kwargs) - self._baseKV_ = set() + self._baseKV_ = set() # should only be LL? self._baseKV_feeders_ = {} logger.info("DiTTo--->OpenDSS writer successfuly instanciated.") @@ -423,528 +423,510 @@ def write_transformers(self, model): feeder_text_map = {} # Loop over the DiTTo objects - for i in model.models: - # If we get a transformer object... - if isinstance(i, PowerTransformer): - # Write the data in the file - # Name - if ( - self.separate_feeders - and hasattr(i, "feeder_name") - and i.feeder_name is not None - ): - feeder_name = i.feeder_name - else: - feeder_name = "DEFAULT" - if ( - self.separate_substations - and hasattr(i, "substation_name") - and i.substation_name is not None - ): - substation_name = i.substation_name - else: - substation_name = "DEFAULT" - - if not substation_name in substation_text_map: - substation_text_map[substation_name] = set([feeder_name]) - else: - substation_text_map[substation_name].add(feeder_name) - txt = "" - if substation_name + "_" + feeder_name in feeder_text_map: - txt = feeder_text_map[substation_name + "_" + feeder_name] + for i in model.iter_models(PowerTransformer): + # Write the data in the file + # Name + feeder_name = "DEFAULT" + if ( + self.separate_feeders + and hasattr(i, "feeder_name") + and i.feeder_name is not None + ): + feeder_name = i.feeder_name + + substation_name = "DEFAULT" + if ( + self.separate_substations + and hasattr(i, "substation_name") + and i.substation_name is not None + ): + substation_name = i.substation_name - if hasattr(i, "name") and i.name is not None: - txt += "New Transformer." + i.name - else: - # If we do not have a valid name, do not even try - # to write anything for this transformer.... - logger.error("Tansformer {obj} is misssing name.".format(obj=i)) - - # Number of phases and windings - if hasattr(i, "windings") and i.windings is not None: - N_phases = [] - for winding in i.windings: - if ( - hasattr(winding, "phase_windings") - and winding.phase_windings is not None - ): - N_phases.append(len(winding.phase_windings)) + if not substation_name in substation_text_map: + substation_text_map[substation_name] = set([feeder_name]) + else: + substation_text_map[substation_name].add(feeder_name) - try: - # phase-phase delta connection defined with phase=1 even though it's two phase. - # This is because there's no implicit neutral - if ( - len(i.windings) == 3 - and i.windings[0].connection_type is not None - and i.windings[0].connection_type == "D" - ): - txt += " phases={Np}".format(Np=N_phases[0] - 1) - else: - txt += " phases={Np}".format(Np=N_phases[0]) - txt += " windings={N}".format(N=len(i.windings)) - except: - logger.error( - "Could not write the number of phases for transformer {name}".format( - name=i.name - ) - ) + txt = "" + sub_fdr_key = substation_name + "_" + feeder_name + if sub_fdr_key in feeder_text_map: + txt = feeder_text_map[sub_fdr_key] - # Connection - if hasattr(i, "from_element") and i.from_element is not None: - bus1 = i.from_element - else: - loggger.error("Missing a from_element connection in {name}".format(i.name)) - if hasattr(i, "to_element") and i.to_element is not None: - bus2 = i.to_element - else: - loggger.error("Missing a to_element connection in {name}".format(name=i.name)) + if hasattr(i, "name") and i.name is not None: + txt += "New Transformer." + i.name + else: + # If we do not have a valid name, do not even try + # to write anything for this transformer.... + logger.error("Tansformer {obj} is misssing name.".format(obj=i)) + + # Number of phases and windings + if hasattr(i, "windings") and i.windings is not None: + N_phases = [] + for winding in i.windings: + if ( + hasattr(winding, "phase_windings") + and winding.phase_windings is not None + ): + N_phases.append(len(winding.phase_windings)) + try: + # phase-phase connection defined with phase=1 even though it's two phase. + if ( + len(i.windings) == 3 + and i.windings[0].connection_type is not None + and i.windings[0].connection_type == "D" + ): + txt += " phases={Np}".format(Np=N_phases[0] - 1) + else: + txt += " phases={Np}".format(Np=N_phases[0]) + txt += " windings={N}".format(N=len(i.windings)) + except: + logger.error(f"Could not write the number of phases for transformer {i.name}") + + # Connection + bus1 = None + if hasattr(i, "from_element") and i.from_element is not None: + bus1 = i.from_element + + bus2 = None + if hasattr(i, "to_element") and i.to_element is not None: + bus2 = i.to_element + + buses = None + if bus1 is not None and bus2 is not None: buses = [bus1, bus2] - # Loadloss - if hasattr(i, "loadloss") and i.loadloss is not None: - txt += " %loadloss=" + str(i.loadloss) # OpenDSS in kWatts - - # install type (Not mapped) - - # noload_loss - if hasattr(i, "noload_loss") and i.noload_loss is not None: - txt += " %Noloadloss=" + str(i.noload_loss) - - # noload_loss - if hasattr(i, "normhkva") and i.normhkva is not None: - txt += " normhkva=" + str(i.normhkva) - - # phase shift (Not mapped) - - # Assume that we only have two or three windings. Three are used for center-tap transformers. Other single or three phase transformers use 2 windings - # For banked 3-phase transformers, separate single phase transformers are used - if hasattr(i, "windings") and i.windings is not None: - - for cnt, winding in enumerate(i.windings): - if ( - hasattr(winding, "phase_windings") - and winding.phase_windings is not None - ): - - for phase_winding in winding.phase_windings: - if ( - hasattr(phase_winding, "compensator_r") - and phase_winding.compensator_r is not None - ): - if not i.name in self.compensator: - self.compensator[i.name] = {} - self.compensator[i.name]["R"] = set( - [phase_winding.compensator_r] - ) - elif "R" in self.compensator[i.name]: - self.compensator[i.name]["R"].add( - phase_winding.compensator_r - ) - else: - self.compensator[i.name]["R"] = set( - [phase_winding.compensator_r] - ) - - if ( - hasattr(phase_winding, "compensator_x") - and phase_winding.compensator_x is not None - ): - if not i.name in self.compensator: - self.compensator[i.name] = {} - self.compensator[i.name]["X"] = set( - [phase_winding.compensator_x] - ) - elif "X" in self.compensator[i.name]: - self.compensator[i.name]["X"].add( - phase_winding.compensator_x - ) - else: - self.compensator[i.name]["X"] = set( - [phase_winding.compensator_x] - ) - - if len(i.windings) == 2: + # Rated power + # if hasattr(i, 'rated_power') and i.rated_power is not None: + # fp.write(' kva='+str(i.rated_power*10**-3)) #OpenDSS in kWatts - for cnt, winding in enumerate(i.windings): + # Emergency power + # Emergency_power removed from powerTransformers and added to windings by Tarek + # if hasattr(i, 'emergency_power') and i.emergency_power is not None: + # fp.write(' EmergHKVA='+str(i.emergency_power*10**-3)) #OpenDSS in kWatts + # phase shift (Not mapped) - txt += " wdg={N}".format(N=cnt + 1) + # Assume that we only have two or three windings. Three are used for center-tap transformers. Other single or three phase transformers use 2 windings + # For banked 3-phase transformers, separate single phase transformers are used + nphases = 3 + if hasattr(i, "windings") and i.windings is not None: - # Connection type + # set compensator_r and compensator_x + for cnt, winding in enumerate(i.windings): + if ( + hasattr(winding, "phase_windings") + and winding.phase_windings is not None + ): + nphases = 0 + for phase_winding in winding.phase_windings: if ( - hasattr(winding, "connection_type") - and winding.connection_type is not None + hasattr(phase_winding, "compensator_r") + and phase_winding.compensator_r is not None ): - if winding.connection_type == "Y": - txt += " conn=wye" - elif winding.connection_type == "D": - txt += " conn=delta" + nphases += 1 + if not i.name in self.compensator: + self.compensator[i.name] = {} + self.compensator[i.name]["R"] = set( + [phase_winding.compensator_r] + ) + elif "R" in self.compensator[i.name]: + self.compensator[i.name]["R"].add( + phase_winding.compensator_r + ) else: - logger.error( - "Unsupported type of connection {conn} for transformer {name}".format( - conn=winding.connection_type, name=i.name - ) + self.compensator[i.name]["R"] = set( + [phase_winding.compensator_r] ) - # Voltage type (Not mapped) - - # volage basis if ( - hasattr(winding, "nominal_voltage") - and winding.nominal_voltage is not None - and winding.nominal_voltage > 0 + hasattr(phase_winding, "compensator_x") + and phase_winding.compensator_x is not None ): - self._baseKV_.add(winding.nominal_voltage * 10 ** -3) - - # Nominal voltage - if ( - hasattr(winding, "nominal_voltage") - and winding.nominal_voltage is not None - ): - txt += " Kv={kv}".format( - kv=winding.nominal_voltage * 10 ** -3 - ) # OpenDSS in kvolts - if ( - not substation_name + "_" + feeder_name - in self._baseKV_feeders_ - ): - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ] = set() - - if ( - winding.nominal_voltage < 300 - ): # Line-Neutral voltage for 120 V - self._baseKV_.add( - winding.nominal_voltage - * math.sqrt(3) - * 10 ** -3 + if not i.name in self.compensator: + self.compensator[i.name] = {} + self.compensator[i.name]["X"] = set( + [phase_winding.compensator_x] ) - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ].add( - winding.nominal_voltage - * math.sqrt(3) - * 10 ** -3 + elif "X" in self.compensator[i.name]: + self.compensator[i.name]["X"].add( + phase_winding.compensator_x ) else: - self._baseKV_.add( - winding.nominal_voltage * 10 ** -3 + self.compensator[i.name]["X"] = set( + [phase_winding.compensator_x] ) - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ].add(winding.nominal_voltage * 10 ** -3) - # rated power - if ( - hasattr(winding, "rated_power") - and winding.rated_power is not None - ): - txt += " kva={kva}".format( - kva=winding.rated_power * 10 ** -3 - ) - - # emergency_power - if ( - hasattr(winding, "emergency_power") - and winding.emergency_power is not None - ): - txt += " EmergHKVA={}".format( - winding.emergency_power * 10 ** -3 - ) # OpenDSS in kWatts - - # resistance - if ( - hasattr(winding, "resistance") - and winding.resistance is not None - ): - txt += " %R={R}".format(R=winding.resistance) - - # Voltage limit (Not mapped) - - # Reverse resistance (Not mapped) - - # Check if winding is grounded - # this check is done here so that it happens only for 2 windings + if len(i.windings) == 2: + emergency_power = None # openDSS expects one EmergHKVA value, but DiTTo has emergency_power for each phase_winding + for cnt, winding in enumerate(i.windings): - # Phase windings - if ( - hasattr(winding, "phase_windings") - and winding.phase_windings is not None - ): + txt += " wdg={N}".format(N=cnt + 1) - if buses is not None: - bus = buses[cnt] - txt += " bus={bus}".format(bus=re.sub('[^0-9a-zA-Z]+', '_', str(bus))) + # bus + if ( + hasattr(winding, "phase_windings") + and winding.phase_windings is not None + ): - if len(winding.phase_windings) != 3: + if buses is not None: + bus = buses[cnt] + txt += " bus={bus}".format(bus=re.sub('[^0-9a-zA-Z]+', '_', str(bus))) - for j, phase_winding in enumerate( - winding.phase_windings - ): + if len(winding.phase_windings) != 3: - # Connection - if ( - hasattr(phase_winding, "phase") - and phase_winding.phase is not None - ): - txt += "." + str( - self.phase_mapping(phase_winding.phase) - ) + for phase_winding in winding.phase_windings: + # Connection if ( - winding.connection_type == "D" - and len(winding.phase_windings) == 1 + hasattr(phase_winding, "phase") + and phase_winding.phase is not None ): - logger.warning( - "Warning - only one phase specified for a delta system - adding another connection" - ) - if self.phase_mapping(phase_winding.phase) == 1: - txt += ".2" - if self.phase_mapping(phase_winding.phase) == 2: - txt += ".3" - if self.phase_mapping(phase_winding.phase) == 3: - txt += ".1" - - if winding.is_grounded: - txt += ".0" + txt += "." + str(self.phase_mapping(phase_winding.phase)) - if ( - hasattr(winding, "phase_windings") - and winding.phase_windings is not None - ): - # Tap position - # THIS CAN CAUSE PROBLEMS - # Use write_taps boolean to write this information or not if ( - self.write_taps - and hasattr( - winding.phase_windings[0], "tap_position" - ) - and winding.phase_windings[0].tap_position - is not None + winding.connection_type == "D" + and len(winding.phase_windings) == 1 ): - txt += " Tap={tap}".format( - tap=winding.phase_windings[0].tap_position + logger.warn( + "Warning - only one phase specified for a delta system - adding another connection" ) + if self.phase_mapping(phase_winding.phase) == 1: + txt += ".2" + if self.phase_mapping(phase_winding.phase) == 2: + txt += ".3" + if self.phase_mapping(phase_winding.phase) == 3: + txt += ".1" - if hasattr(i, "reactances") and i.reactances is not None: - # Since we are in the case of 2 windings, we should only have one reactance - if isinstance(i.reactances, list): - if len(i.reactances) != 1: - logger.error( - "Number of reactances incorrect for transformer {name}. Expected 1, got {N}".format( - name=i.name, N=len(i.reactances) - ) - ) - else: - txt += " XHL={reac}".format(reac=i.reactances[0]) - # If it is not a list, maybe it was entered as a scalar, but should not be that way.... - elif isinstance(i.reactances, (int, float)): - txt += " XHL={reac}".format(reac=i.reactances) + if winding.is_grounded: + txt += ".0" + + # Connection type + if ( + hasattr(winding, "connection_type") + and winding.connection_type is not None + ): + if winding.connection_type == "Y": + txt += " conn=wye" + elif winding.connection_type == "D": + txt += " conn=delta" else: logger.error( - "Reactances not understood for transformer {name}.".format( - name=i.name + "Unsupported type of connection {conn} for transformer {name}".format( + conn=winding.connection_type, name=i.name ) ) - # This is used to represent center-tap transformers - # As described in the documentation, if the R and X values are not known, the values described by default_r and default_x should be used - # TODO: represent three winding transformers that are not center tap. - if len(i.windings) == 3 and i.is_center_tap: - default_r = [0.6, 1.2, 1.2] - default_x = [2.04, 2.04, 1.36] - - for cnt, winding in enumerate(i.windings): + # Voltage type (Not mapped) - txt += f" wdg={cnt+1}" + # Nominal voltage + if ( + hasattr(winding, "nominal_voltage") + and winding.nominal_voltage is not None + ): + txt += " Kv={kv}".format( + kv=winding.nominal_voltage * 10 ** -3 + ) # OpenDSS in kvolts + if (not sub_fdr_key in self._baseKV_feeders_): + self._baseKV_feeders_[sub_fdr_key] = set() # conn = wye or delta if ( - hasattr(winding, "connection_type") - and winding.connection_type is not None + winding.nominal_voltage < 300 + or nphases == 1 + ): # Line-Neutral voltage for 120 V + kVLL = round(winding.nominal_voltage * math.sqrt(3) * 10 ** -3, 3) + else: + kVLL = round(winding.nominal_voltage * 10 ** -3, 3) + + self._baseKV_.add(kVLL) + self._baseKV_feeders_[sub_fdr_key].add(kVLL) + + # rated power + if ( + hasattr(winding, "rated_power") + and winding.rated_power is not None + ): + txt += " kva={kva}".format( + kva=winding.rated_power * 10 ** -3 + ) + + if ( + hasattr(winding, "phase_windings") + and winding.phase_windings is not None + ): + # Tap position + # THIS CAN CAUSE PROBLEMS + # Use write_taps boolean to write this information or not + if ( + self.write_taps + and hasattr( + winding.phase_windings[0], "tap_position" + ) + and winding.phase_windings[0].tap_position + is not None ): - if winding.connection_type == "Y": - txt += " conn=wye" - elif winding.connection_type == "D": - txt += " conn=delta" - else: - logger.error( - "Unsupported type of connection {conn} for transformer {name}".format( - conn=winding.connection_type, name=i.name - ) - ) + txt += " Tap={tap}".format( + tap=winding.phase_windings[0].tap_position + ) - # Connection - if buses is not None: + # resistance + if ( + hasattr(winding, "resistance") + and winding.resistance is not None + ): + txt += " %R={R}".format(R=winding.resistance) - if cnt == 0 or cnt == 1: - txt += " bus={b}".format(b=re.sub('[^0-9a-zA-Z]+', '_', buses[cnt])) - elif cnt == 2: - txt += " bus={b}".format(b=re.sub('[^0-9a-zA-Z]+', '_', buses[cnt - 1])) + # emergency_power + # Was added to windings by Tarek + if ( + hasattr(winding, "emergency_power") + and winding.emergency_power is not None + and emergency_power is None + ): + emergency_power = " EmergHKVA={}".format( + winding.emergency_power * 10 ** -3 + ) # OpenDSS in kWatts - # These are the configurations for center tap transformers - if cnt == 0: - txt += ".{}".format( - self.phase_mapping( - winding.phase_windings[ - 0 - ].phase # Should be only one phase if it's a Wye transformer - ) + # Voltage limit (Not mapped) + + # Reverse resistance (Not mapped) + if hasattr(i, "reactances") and i.reactances is not None: + # Since we are in the case of 2 windings, we should only have one reactance + if isinstance(i.reactances, list): + if len(i.reactances) != 1: + logger.error( + "Number of reactances incorrect for transformer {name}. Expected 1, got {N}".format( + name=i.name, N=len(i.reactances) ) - if ( - len(winding.phase_windings) > 1 - and winding.connection_type == "Y" - ): - logger.warning( - "Warning - Wye center-tap transformer with more than one phase connection. Only using first one" - ) - if ( - winding.connection_type == "D" - and len(winding.phase_windings) >= 2 - ): - txt += ".{}".format( - self.phase_mapping( - winding.phase_windings[1].phase - ) - ) - if len(winding.phase_windings) > 2: - logger.warning( - "Warning - Delta center-tap transformer with more than two phase connection. Only using first two" - ) - if ( - winding.connection_type == "D" - and len(winding.phase_windings) == 1 - ): - logger.warning( - "Warning - only one phase specified for a delta system - adding another connection" - ) - if self.phase_mapping(phase_winding.phase) == 1: - txt += ".2" - if self.phase_mapping(phase_winding.phase) == 2: - txt += ".3" - if self.phase_mapping(phase_winding.phase) == 3: - txt += ".1" + ) + else: + txt += " XHL={reac}".format(reac=i.reactances[0]) + # If it is not a list, maybe it was entered as a scalar, but should not be that way.... + elif isinstance(i.reactances, (int, float)): + txt += " XHL={reac}".format(reac=i.reactances) + else: + logger.error( + "Reactances not understood for transformer {name}.".format( + name=i.name + ) + ) + if emergency_power is not None: + txt += emergency_power - if cnt == 1: - txt += ".1.0" - if cnt == 2: - txt += ".0.2" + # This is used to represent center-tap transformers + # As described in the documentation, if the R and X values are not known, the values described by default_r and default_x should be used + if len(i.windings) == 3: + default_r = [0.6, 1.2, 1.2] + default_x = [2.04, 2.04, 1.36] - # Voltage type (Not mapped) + for cnt, winding in enumerate(i.windings): - # Nominal voltage - if ( - hasattr(winding, "nominal_voltage") - and winding.nominal_voltage is not None - ): - txt += " Kv={kv}".format( - kv=winding.nominal_voltage * 10 ** -3 - ) # OpenDSS in kvolts + txt += f" wdg={cnt+1}" + + # conn = wye or delta + if ( + hasattr(winding, "connection_type") + and winding.connection_type is not None + ): + if winding.connection_type == "Y": + txt += " conn=wye" + elif winding.connection_type == "D": + txt += " conn=delta" + else: + logger.error( + "Unsupported type of connection {conn} for transformer {name}".format( + conn=winding.connection_type, name=i.name + ) + ) + + # Connection + if buses is not None: + + if cnt == 0 or cnt == 1: + txt += " bus={b}".format(b=re.sub('[^0-9a-zA-Z]+', '_', buses[cnt])) + elif cnt == 2: + txt += " bus={b}".format(b=re.sub('[^0-9a-zA-Z]+', '_', buses[cnt - 1])) + + # These are the configurations for center tap transformers + if cnt == 0: + txt += ".{}".format( + self.phase_mapping( + winding.phase_windings[ + 0 + ].phase # Should beOnly one phase if it's a Wye transformer + ) + ) if ( - not substation_name + "_" + feeder_name - in self._baseKV_feeders_ + len(winding.phase_windings) > 1 + and winding.connection_type == "Y" ): - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ] = set() - if ( - winding.nominal_voltage < 300 - ): # Line-Neutral voltage for 120 V - self._baseKV_.add( - winding.nominal_voltage - * math.sqrt(3) - * 10 ** -3 + print( + "Warning - Wye center-tap transformer with more than one phase connection. Only using first one" ) - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ].add( - winding.nominal_voltage - * math.sqrt(3) - * 10 ** -3 + if ( + winding.connection_type == "D" + and len(winding.phase_windings) >= 2 + ): + txt += ".{}".format( + self.phase_mapping( + winding.phase_windings[1].phase + ) ) - else: - self._baseKV_.add( - winding.nominal_voltage * 10 ** -3 + if len(winding.phase_windings) > 2: + print( + "Warning - Delta center-tap transformer with more than two phase connection. Only using first two" + ) + if ( + winding.connection_type == "D" + and len(winding.phase_windings) == 1 + ): + print( + "Warning - only one phase specified for a delta system - adding another connection" ) - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ].add(winding.nominal_voltage * 10 ** -3) + if self.phase_mapping(phase_winding.phase) == 1: + txt += ".2" + if self.phase_mapping(phase_winding.phase) == 2: + txt += ".3" + if self.phase_mapping(phase_winding.phase) == 3: + txt += ".1" - # rated power - if ( - hasattr(winding, "rated_power") - and winding.rated_power is not None - ): - txt += " kva={kva}".format( - kva=winding.rated_power * 10 ** -3 - ) + if cnt == 1: + txt += ".1.0" + if cnt == 2: + txt += ".0.2" - # emergency_power - # Was added to windings by Tarek - if ( - hasattr(winding, "emergency_power") - and winding.emergency_power is not None - ): - txt += " EmergHKVA={}".format( - winding.emergency_power * 10 ** -3 - ) # OpenDSS in kWatts + # Voltage type (Not mapped) - # Tap position + # Nominal voltage + if ( + hasattr(winding, "nominal_voltage") + and winding.nominal_voltage is not None + ): + txt += " Kv={kv}".format( + kv=winding.nominal_voltage * 10 ** -3 + ) # OpenDSS in kvolts if ( - self.write_taps - and hasattr(winding, "phase_windings") - and winding.phase_windings is not None - and hasattr(winding.phase_windings[0], "tap_position") - and winding.phase_windings[0].tap_position is not None + not sub_fdr_key + in self._baseKV_feeders_ ): - txt += " Tap={tap}".format( - tap=winding.phase_windings[0].tap_position + self._baseKV_feeders_[ + sub_fdr_key + ] = set() + if ( + winding.nominal_voltage < 300 + ): # Line-Neutral voltage for 120 V + self._baseKV_.add( + winding.nominal_voltage + * math.sqrt(3) + * 10 ** -3 + ) + self._baseKV_feeders_[ + sub_fdr_key + ].add( + winding.nominal_voltage + * math.sqrt(3) + * 10 ** -3 ) + else: + self._baseKV_.add( + winding.nominal_voltage * 10 ** -3 + ) + self._baseKV_feeders_[ + sub_fdr_key + ].add(winding.nominal_voltage * 10 ** -3) - # Voltage limit (Not mapped) + # rated power + if ( + hasattr(winding, "rated_power") + and winding.rated_power is not None + ): + txt += " kva={kva}".format( + kva=winding.rated_power * 10 ** -3 + ) - # Reverse resistance (Not mapped) + # emergency_power + # Was added to windings by Tarek + if ( + hasattr(winding, "emergency_power") + and winding.emergency_power is not None + ): + txt += " EmergHKVA={}".format( + winding.emergency_power * 10 ** -3 + ) # OpenDSS in kWatts - if ( - hasattr(winding, "resistance") - and winding.resistance is not None - ): - txt += " %R={R}".format(R=winding.resistance) - else: - txt += " %R={R}".format(R=default_r[cnt - 1]) + # Tap position + if ( + self.write_taps + and hasattr(winding, "phase_windings") + and winding.phase_windings is not None + and hasattr(winding.phase_windings[0], "tap_position") + and winding.phase_windings[0].tap_position is not None + ): + txt += " Tap={tap}".format( + tap=winding.phase_windings[0].tap_position + ) - if hasattr(i, "reactances") and i.reactances is not None: - # Here, we should have 3 reactances - if ( - isinstance(i.reactances, list) - and len(i.reactances) == 3 - ): - txt += " XHL={XHL} XLT={XLT} XHT={XHT}".format( - XHL=i.reactances[0], - XLT=i.reactances[1], - XHT=i.reactances[2], - ) - else: - logger.error( - "Wrong number of reactances for transformer {name}".format( - name=i.name - ) - ) + # Voltage limit (Not mapped) + + # resistance + # if hasattr(winding, 'resistance') and winding.resistance is not None: + # fp.write(' %R={R}'.format(R=winding.resistance)) + + # Reverse resistance (Not mapped) + + if ( + hasattr(winding, "resistance") + and winding.resistance is not None + ): + txt += " %r={R}".format(R=winding.resistance) else: - txt += " XHL=%f XHT=%f XLT=%f" % ( - default_x[0], - default_x[1], - default_x[2], + txt += " %r={R}".format(R=default_r[cnt - 1]) + + if hasattr(i, "reactances") and i.reactances is not None: + # Here, we should have 3 reactances + if ( + isinstance(i.reactances, list) + and len(i.reactances) == 3 + ): + txt += " XHL={XHL} XLT={XLT} XHT={XHT}".format( + XHL=i.reactances[0], + XLT=i.reactances[1], + XHT=i.reactances[2], ) + else: + logger.error( + "Wrong number of reactances for transformer {name}".format( + name=i.name + ) + ) + else: + txt += " XHL=%f XHT=%f XLT=%f" % ( + default_x[0], + default_x[1], + default_x[2], + ) - txt += "\n\n" - feeder_text_map[substation_name + "_" + feeder_name] = txt + + # Loadloss + if hasattr(i, "loadloss") and i.loadloss is not None: + txt += " %loadloss=" + str(i.loadloss) # OpenDSS in kWatts + + # install type (Not mapped) + + # noload_loss + if hasattr(i, "noload_loss") and i.noload_loss is not None: + txt += " %Noloadloss=" + str(i.noload_loss) + + # noload_loss + if hasattr(i, "normhkva") and i.normhkva is not None: + txt += " normhkva=" + str(i.normhkva) + + txt += "\n\n" + feeder_text_map[sub_fdr_key] = txt for substation_name in substation_text_map: for feeder_name in substation_text_map[substation_name]: - txt = feeder_text_map[substation_name + "_" + feeder_name] + txt = feeder_text_map[sub_fdr_key] feeder_name = re.sub('[^0-9a-zA-Z]+', '_', feeder_name.lower()) substation_name = re.sub('[^0-9a-zA-Z]+', '_', substation_name.lower()) if txt != "": @@ -1243,22 +1225,22 @@ def write_PVs(self, model): for i in model.models: if isinstance(i, Photovoltaic): # If is_sourcebus is set to 1, then the object represents a source and not a PV system + + feeder_name = "DEFAULT" if ( self.separate_feeders and hasattr(i, "feeder_name") and i.feeder_name is not None ): feeder_name = i.feeder_name - else: - feeder_name = "DEFAULT" + + substation_name = "DEFAULT" if ( self.separate_substations and hasattr(i, "substation_name") and i.substation_name is not None ): substation_name = i.substation_name - else: - substation_name = "DEFAULT" if not substation_name in substation_text_map: substation_text_map[substation_name] = set([feeder_name]) @@ -1268,23 +1250,27 @@ def write_PVs(self, model): voltvar_nodes = set() voltwatt_nodes = set() voltwatt_voltvar_nodes = set() - if substation_name + "_" + feeder_name in feeder_text_map: - txt = feeder_text_map[substation_name + "_" + feeder_name] + sub_fdr_key = substation_name + "_" + feeder_name - if substation_name + "_" + feeder_name in feeder_voltvar_map: - voltvar_nodes = feeder_voltvar_map[ - substation_name + "_" + feeder_name - ] + if sub_fdr_key in feeder_text_map: + txt = feeder_text_map[sub_fdr_key] - if substation_name + "_" + feeder_name in feeder_voltwatt_map: - voltwatt_nodes = feeder_voltwatt_map[ - substation_name + "_" + feeder_name - ] + if sub_fdr_key in feeder_voltvar_map: + voltvar_nodes = feeder_voltvar_map[sub_fdr_key] + + if sub_fdr_key in feeder_voltwatt_map: + voltwatt_nodes = feeder_voltwatt_map[sub_fdr_key] # Name if hasattr(i, "name") and i.name is not None: txt += "New PVSystem.{name}".format(name=i.name) + # Phases + n_phases = 3 + if hasattr(i, "phases") and i.phases is not None: + n_phases = len(i.phases) + txt += f" phases={n_phases}" + # connecting element if ( hasattr(i, "connecting_element") @@ -1311,20 +1297,18 @@ def write_PVs(self, model): txt += " kV={kV}".format( kV=i.nominal_voltage * 10 ** -3 ) # DiTTo in volts - if not substation_name + "_" + feeder_name in self._baseKV_feeders_: - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ] = set() + if not sub_fdr_key in self._baseKV_feeders_: + self._baseKV_feeders_[sub_fdr_key] = set() if ( i.nominal_voltage < 300 ): # Line-Neutral voltage for 120 V (i.e. 240V) self._baseKV_.add(i.nominal_voltage * 10 ** -3) - self._baseKV_feeders_[substation_name + "_" + feeder_name].add( + self._baseKV_feeders_[sub_fdr_key].add( i.nominal_voltage * 2 * 10 ** -3 ) else: self._baseKV_.add(i.nominal_voltage * 10 ** -3) - self._baseKV_feeders_[substation_name + "_" + feeder_name].add( + self._baseKV_feeders_[sub_fdr_key].add( i.nominal_voltage * 10 ** -3 ) else: @@ -1346,26 +1330,20 @@ def write_PVs(self, model): kV=parent.nominal_voltage * 10 ** -3 ) # DiTTo in volts if ( - not substation_name + "_" + feeder_name + not sub_fdr_key in self._baseKV_feeders_ ): - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ] = set() + self._baseKV_feeders_[sub_fdr_key] = set() if ( parent.nominal_voltage < 300 ): # Line-Line voltage for 120 V (i.e. 240V) self._baseKV_.add( parent.nominal_voltage * 10 ** -3 ) - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ].add(parent.nominal_voltage * 2 * 10 ** -3) + self._baseKV_feeders_[sub_fdr_key].add(parent.nominal_voltage * 2 * 10 ** -3) else: self._baseKV_.add(parent.nominal_voltage * 10 ** -3) - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ].add(parent.nominal_voltage * 10 ** -3) + self._baseKV_feeders_[sub_fdr_key].add(parent.nominal_voltage * 10 ** -3) if hasattr(i, "active_rating") and i.active_rating is not None: pf_local = 1.0 @@ -1480,7 +1458,7 @@ def write_PVs(self, model): and os.path.isfile(os.path.join(self.output_path,ts.data_location)) ): filename = self.timeseries_datasets[ - substation_name + "_" + feeder_name + sub_fdr_key ][ts.data_location] if self.remove_loadshapes: optional_comment = '!' @@ -1496,10 +1474,10 @@ def write_PVs(self, model): # TODO: manage the data correctly when it is only in memory txt += "\n" - feeder_text_map[substation_name + "_" + feeder_name] = txt - feeder_voltvar_map[substation_name + "_" + feeder_name] = voltvar_nodes + feeder_text_map[sub_fdr_key] = txt + feeder_voltvar_map[sub_fdr_key] = voltvar_nodes feeder_voltwatt_map[ - substation_name + "_" + feeder_name + sub_fdr_key ] = voltwatt_nodes feeder_voltwatt_voltvar_map[ substation_name + "_" + feeder_name @@ -1839,69 +1817,138 @@ def write_loads(self, model): :returns: 1 for success, -1 for failure :rtype: int """ - substation_text_map = {} feeder_text_map = {} - for i in model.models: - if isinstance(i, Load): + + def append_time_series(i, txt): + for ts in i.timeseries: + substation = "DEFAULT" + feeder = "DEFAULT" + if ts.feeder_name is not None: + feeder = ts.feeder_name + if ts.substation_name is not None: + substation = ts.substation_name if ( - self.separate_feeders - and hasattr(i, "feeder_name") - and i.feeder_name is not None + hasattr(ts, "data_location") + and ts.data_label is not None + and ts.data_location is not None ): - feeder_name = i.feeder_name - else: - feeder_name = "DEFAULT" - if ( - self.separate_substations - and hasattr(i, "substation_name") - and i.substation_name is not None - ): - substation_name = i.substation_name - else: - substation_name = "DEFAULT" + filename = self.timeseries_datasets[ + substation + "_" + feeder + ][ts.data_location] + txt += " {ts_format}={filename}".format( + ts_format=self.timeseries_format[filename], + filename=filename, + ) + # TODO: manage the data correctly when it is only in memory + return txt - if not substation_name in substation_text_map: - substation_text_map[substation_name] = set([feeder_name]) - else: - substation_text_map[substation_name].add(feeder_name) + for i in model.iter_models(Load): - txt = "" - if substation_name + "_" + feeder_name in feeder_text_map: - txt = feeder_text_map[substation_name + "_" + feeder_name] + if not (hasattr(i, "name") and i.name is not None): + continue # gotta have a name - # Name - if hasattr(i, "name") and i.name is not None: - txt += "New Load." + i.name - else: - logger.error("Name missing for Load") - continue + if not (hasattr(i, "phase_loads") and i.phase_loads is not None): + continue # no loads - # Connection type - if hasattr(i, "connection_type") and i.connection_type is not None: - if i.connection_type == "Y": - txt += " conn=wye" - elif i.connection_type == "D": - txt += " conn=delta" + if not (hasattr(i, "connecting_element") and i.connecting_element is not None): + continue # no Bus1 to place load - # Connecting element - if ( - hasattr(i, "connecting_element") - and i.connecting_element is not None - ): - txt += " bus1={bus}".format(bus=re.sub('[^0-9a-zA-Z]+', '_', i.connecting_element)) - if hasattr(i, "phase_loads") and i.phase_loads is not None: - for phase_load in i.phase_loads: - if ( - hasattr(phase_load, "phase") - and phase_load.phase is not None - ): - txt += ".{p}".format( - p=self.phase_mapping(phase_load.phase) - ) + feeder_name = "DEFAULT" + if ( + self.separate_feeders + and hasattr(i, "feeder_name") + and i.feeder_name is not None + ): + feeder_name = i.feeder_name + + substation_name = "DEFAULT" + if ( + self.separate_substations + and hasattr(i, "substation_name") + and i.substation_name is not None + ): + substation_name = i.substation_name - if i.connection_type == "D" and len(i.phase_loads) == 1: - logger.warning( "Warning - only one phase specified for a delta system - adding another connection") + if not substation_name in substation_text_map: + substation_text_map[substation_name] = set([feeder_name]) + else: + substation_text_map[substation_name].add(feeder_name) + + txt = "" # first Load in the loop for the substation and feeder + sub_fdr_key = substation_name + "_" + feeder_name + if sub_fdr_key in feeder_text_map: + txt = feeder_text_map[sub_fdr_key] + # we are appending to the text to be written (last entry should be \n\n) + + kws = [] + kvars = [] + for phase_load in i.phase_loads: + if hasattr(phase_load, "p") and phase_load.p is not None: + kws.append(phase_load.p * 10 ** -3) + if hasattr(phase_load, "q") and phase_load.q is not None: + kvars.append(phase_load.q * 10 ** -3) + + if len(kws) == 0 and len(kvars) == 0: + continue + + balanced_load = True + + if len(kws) > 1 and len(kvars) > 1: + p1, q1 = kws[0], kvars[0] + if any(p1 != p for p in kws[1:]): + balanced_load = False + if any(q1 != q for q in kvars[1:]): + balanced_load = False + + # define shared values + # Connection type + conn_type_txt = "" + if hasattr(i, "connection_type") and i.connection_type is not None: + if i.connection_type == "Y": + conn_type_txt = " conn=wye" + elif i.connection_type == "D": + conn_type_txt = " conn=delta" + + bus1_txt = " bus1={bus}".format(bus=re.sub('[^0-9a-zA-Z]+', '_', i.connecting_element)) + + nomimal_voltage_txt = "" + nominal_voltage = 0.0 + if hasattr(i, "nominal_voltage") and i.nominal_voltage is not None: + + if i.nominal_voltage < 300: # Line-Neutral voltage for 120 V + nominal_voltage = i.nominal_voltage * math.sqrt(3) * 10 ** -3 + nomimal_voltage_txt = f" kV={nominal_voltage}" + else: + nominal_voltage = i.nominal_voltage * 10 ** -3 + nomimal_voltage_txt = f" kV={nominal_voltage}" + + if not sub_fdr_key in self._baseKV_feeders_: + self._baseKV_feeders_[sub_fdr_key] = set() + + self._baseKV_.add(nominal_voltage) + self._baseKV_feeders_[sub_fdr_key].add(nominal_voltage) + + vmin_txt = "" + if hasattr(i, "vmin") and i.vmin is not None: + vmin_txt = " Vminpu={vmin}".format(vmin=i.vmin) + + vmax_txt = "" + if hasattr(i, "vmax") and i.vmax is not None: + vmax_txt = " Vmaxpu={vmax}".format(vmax=i.vmax) + + if balanced_load: # only need to define one New Load regardless of number of phases + + txt += "New Load." + i.name + txt += bus1_txt + # balanced_load case: + for phase_load in i.phase_loads: # add the .phs1.phs2 etc + # add all phases onto Bus1 + if hasattr(phase_load, "phase") and phase_load.phase is not None: + txt += f".{self.phase_mapping(phase_load.phase)}" + + # handle Delta connections + if i.connection_type == "D" and len(i.phase_loads) == 1: if self.phase_mapping(i.phase_loads[0].phase) == 1: txt += ".2" if self.phase_mapping(i.phase_loads[0].phase) == 2: @@ -1909,161 +1956,139 @@ def write_loads(self, model): if self.phase_mapping(i.phase_loads[0].phase) == 3: txt += ".1" - # nominal voltage - if hasattr(i, "nominal_voltage") and i.nominal_voltage is not None: - if i.nominal_voltage < 300: - txt += " kV={volt}".format( - volt=i.nominal_voltage * 10 ** -3 - ) - else: - txt += " kV={volt}".format(volt=i.nominal_voltage * 10 ** -3) - if not substation_name + "_" + feeder_name in self._baseKV_feeders_: - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ] = set() - if i.nominal_voltage < 300: # Line-Neutral voltage for 120 V - self._baseKV_.add(i.nominal_voltage * math.sqrt(3) * 10 ** -3) - self._baseKV_feeders_[substation_name + "_" + feeder_name].add( - i.nominal_voltage * math.sqrt(3) * 10 ** -3 - ) - else: - self._baseKV_.add(i.nominal_voltage * 10 ** -3) - self._baseKV_feeders_[substation_name + "_" + feeder_name].add( - i.nominal_voltage * 10 ** -3 - ) + # if i.connection_type=='Y': + txt += " Phases={N}".format(N=len(i.phase_loads)) + # elif i.connection_type=='D' and len(i.phase_loads)==3: + # fp.write(' Phases=3') + # elif i.connection_type=='D' and len(i.phase_loads)==2: + # fp.write(' Phases=1') + + txt += nomimal_voltage_txt + + txt += " kW={P}".format(P=sum(kws)) + + txt += " model={N}".format(N=i.phase_loads[0].model) + + txt += conn_type_txt + + txt += " kvar={Q}".format(Q=sum(kvars)) + + txt += vmin_txt + + txt += vmax_txt - # Vmin - if hasattr(i, "vmin") and i.vmin is not None: - txt += " Vminpu={vmin}".format(vmin=i.vmin) + # positions (Not mapped) + + for phase_load in i.phase_loads: - # Vmax - if hasattr(i, "vmax") and i.vmax is not None: - txt += " Vmaxpu={vmax}".format(vmax=i.vmax) + # ZIP load model + if ( + hasattr(phase_load, "use_zip") + and phase_load.use_zip is not None + ): + if phase_load.use_zip: + + # Get the coefficients + if ( + ( + hasattr(i, "ppercentimpedance") + and i.ppercentimpedance is not None + ) + and ( + hasattr(i, "qpercentimpedance") + and i.qpercentimpedance is not None + ) + and ( + hasattr(i, "ppercentcurrent") + and i.ppercentcurrent is not None + ) + and ( + hasattr(i, "qpercentcurrent") + and i.qpercentcurrent is not None + ) + and ( + hasattr(i, "ppercentpower") + and i.ppercentpower is not None + ) + and ( + hasattr(i, "qpercentpower") + and i.qpercentpower is not None + ) + ): + + txt += ( + " model=8 ZIPV=[%.3f, %.3f, %.3f, %.3f, %.3f, %.3f, %.3f]" + % ( + i.ppercentimpedance, + i.ppercentcurrent, + i.ppercentpower, + i.qpercentimpedance, + i.qpercentcurrent, + i.qpercentpower, + ) + ) + + # timeseries object + if hasattr(i, "timeseries") and i.timeseries is not None: + txt = append_time_series(i, txt) + + txt += "\n\n" + + else: # unbalanced load, need to define more than one New Load + for (n, phase_load) in enumerate(i.phase_loads): + txt += "New Load." + i.name + "_" + str(n+1) # need unique names + txt += bus1_txt + if hasattr(phase_load, "phase") and phase_load.phase is not None: + dss_phase = self.phase_mapping(phase_load.phase) + txt += f".{dss_phase}" + + # handle Delta connections + if i.connection_type == "D": + if dss_phase == 1: + txt += ".2" + if dss_phase == 2: + txt += ".3" + if dss_phase == 3: + txt += ".1" - # positions (Not mapped) + txt += " Phases=1" - # KW - total_P = 0 - if hasattr(i, "phase_loads") and i.phase_loads: - txt += " model={N}".format(N=i.phase_loads[0].model) - - for phase_load in i.phase_loads: - if hasattr(phase_load, "p") and phase_load.p is not None: - total_P += phase_load.p - txt += " kW={P}".format(P=total_P * 10 ** -3) - - # Kva - total_Q = 0 - if hasattr(i, "phase_loads") and i.phase_loads: - for phase_load in i.phase_loads: - if hasattr(phase_load, "q") and phase_load.q is not None: - total_Q += phase_load.q - txt += " kvar={Q}".format(Q=total_Q * 10 ** -3) - - # phase_loads - if hasattr(i, "phase_loads") and i.phase_loads is not None: - - # if i.connection_type=='Y': - if i.nominal_voltage is not None and i.nominal_voltage < 300: - txt += " Phases=1" - else: - txt += " Phases={N}".format(N=len(i.phase_loads)) + nomimal_voltage_txt = "" + if hasattr(i, "nominal_voltage") and i.nominal_voltage is not None: + nominal_voltage = i.nominal_voltage * 10 ** -3 + if not (i.connection_type == "D" or nominal_voltage < 300): + # single phase connections in openDSS should specify LN voltage + # if not delta connection or not already LN + nominal_voltage = round(nominal_voltage / math.sqrt(3), 4) + nomimal_voltage_txt = f" kV={nominal_voltage}" - for phase_load in i.phase_loads: + txt += nomimal_voltage_txt - # P - # if hasattr(phase_load, 'p') and phase_load.p is not None: - # fp.write(' kW={P}'.format(P=phase_load.p*10**-3)) + txt += " kW={P}".format(P=kws[n]) - # Q - # if hasattr(phase_load, 'q') and phase_load.q is not None: - # fp.write(' kva={Q}'.format(Q=phase_load.q*10**-3)) + txt += " model={N}".format(N=phase_load.model) - # ZIP load model - if ( - hasattr(phase_load, "use_zip") - and phase_load.use_zip is not None - ): - if phase_load.use_zip: + txt += conn_type_txt - # Get the coefficients - if ( - ( - hasattr(i, "ppercentimpedance") - and i.ppercentimpedance is not None - ) - and ( - hasattr(i, "qpercentimpedance") - and i.qpercentimpedance is not None - ) - and ( - hasattr(i, "ppercentcurrent") - and i.ppercentcurrent is not None - ) - and ( - hasattr(i, "qpercentcurrent") - and i.qpercentcurrent is not None - ) - and ( - hasattr(i, "ppercentpower") - and i.ppercentpower is not None - ) - and ( - hasattr(i, "qpercentpower") - and i.qpercentpower is not None - ) - ): + txt += " kvar={Q}".format(Q=kvars[n]) + + txt += vmin_txt + txt += vmax_txt + # positions (Not mapped) - txt += ( - " model=8 ZIPV=[%.3f, %.3f, %.3f, %.3f, %.3f, %.3f, %.3f]" - % ( - i.ppercentimpedance, - i.ppercentcurrent, - i.ppercentpower, - i.qpercentimpedance, - i.qpercentcurrent, - i.qpercentpower, - ) - ) + # timeseries object + if hasattr(i, "timeseries") and i.timeseries is not None: + txt = append_time_series(i, txt) - # timeseries object - if hasattr(i, "timeseries") and i.timeseries is not None: - for ts in i.timeseries: - substation = "DEFAULT" - feeder = "DEFAULT" - if ts.feeder_name is not None: - feeder = ts.feeder_name - if ts.substation_name is not None: - substation = ts.substation_name - if ( - hasattr(ts, "data_location") - and ts.data_label is not None - and ts.data_location is not None - ): - filename = self.timeseries_datasets[ - substation + "_" + feeder - ][ts.data_location] - if self.remove_loadshapes: - optional_comment = '!' - else: - optional_comment = '' - txt += " {optional_comment}{ts_format}={filename}".format( - optional_comment = optional_comment, - ts_format=self.timeseries_format[filename], - filename=filename, - ) - else: - pass - # TODO: manage the data correctly when it is only in memory + txt += "\n\n" - txt += "\n\n" - feeder_text_map[substation_name + "_" + feeder_name] = txt + feeder_text_map[sub_fdr_key] = txt for substation_name in substation_text_map: for feeder_name in substation_text_map[substation_name]: - txt = feeder_text_map[substation_name + "_" + feeder_name] - feeder_name = re.sub('[^0-9a-zA-Z]+', '_', feeder_name.lower()) - substation_name = re.sub('[^0-9a-zA-Z]+', '_', substation_name.lower()) + txt = feeder_text_map[sub_fdr_key] + feeder_name = feeder_name.replace(">", "-") + substation_name = substation_name.replace(">", "-") if txt != "": output_folder = None output_redirect = None @@ -2129,329 +2154,314 @@ def write_regulators(self, model): # At the end, we simply loop over the list to write all strings to transformers.dss transfo_creation_string_map = {} - for i in model.models: - if isinstance(i, Regulator): - - if ( - self.separate_feeders - and hasattr(i, "feeder_name") - and i.feeder_name is not None - ): - feeder_name = i.feeder_name - else: - feeder_name = "DEFAULT" - if ( - self.separate_substations - and hasattr(i, "substation_name") - and i.substation_name is not None - ): - substation_name = i.substation_name - else: - substation_name = "DEFAULT" - - if not substation_name in substation_text_map: - substation_text_map[substation_name] = set([feeder_name]) - else: - substation_text_map[substation_name].add(feeder_name) - txt = "" - transfo_creation_string = "" - if substation_name + "_" + feeder_name in feeder_text_map: - txt = feeder_text_map[substation_name + "_" + feeder_name] - transfo_creation_string = transfo_creation_string_map[ - substation_name + "_" + feeder_name - ] + for i in model.iter_models(Regulator): - if hasattr(i, "name") and i.name is not None: - txt += "New RegControl.{name}".format(name=i.name) - else: - continue + if ( + self.separate_feeders + and hasattr(i, "feeder_name") + and i.feeder_name is not None + ): + feeder_name = i.feeder_name + else: + feeder_name = "DEFAULT" + if ( + self.separate_substations + and hasattr(i, "substation_name") + and i.substation_name is not None + ): + substation_name = i.substation_name + else: + substation_name = "DEFAULT" - # Connected transformer - if hasattr(i, "connected_transformer"): + if not substation_name in substation_text_map: + substation_text_map[substation_name] = set([feeder_name]) + else: + substation_text_map[substation_name].add(feeder_name) + txt = "" + transfo_creation_string = "" + if substation_name + "_" + feeder_name in feeder_text_map: + txt = feeder_text_map[substation_name + "_" + feeder_name] + transfo_creation_string = transfo_creation_string_map[ + substation_name + "_" + feeder_name + ] - # If we have a valid connected_transformer then job's easy... - if i.connected_transformer is not None: # not setting the connected_transformer in reader parse_regulators - txt += " transformer={trans}".format( - trans=i.connected_transformer - ) + if hasattr(i, "name") and i.name is not None: + txt += "New RegControl.{name}".format(name=i.name) + else: + continue - # Otherwise, we have to create a new transformer and write it to the transformers file - else: + # Connected transformer + if hasattr(i, "connected_transformer"): - # Initialize the string: - transfo_creation_string += "New Transformer." + # If we have a valid connected_transformer then job's easy... + if i.connected_transformer is not None: # not setting the connected_transformer in reader parse_regulators + txt += " transformer={trans}".format( + trans=i.connected_transformer + ) - # Name: - transfo_name = "trans_{}".format( - i.name - ) # Maybe not the best naming convention.... - transfo_creation_string += transfo_name + # Otherwise, we have to create a new transformer and write it to the transformers file + else: - # Number of Phases - if hasattr(i, "windings") and i.windings is not None: - if ( - hasattr(i.windings[0], "phase_windings") - and i.windings[0].phase_windings is not None - ): - try: - transfo_creation_string += " phases={}".format( - len(i.windings[0].phase_windings) - ) - except: - pass - phases = [ - self.phase_mapping(x.phase) - for x in i.windings[0].phase_windings - ] - phase_string = reduce( - lambda x, y: str(x) + "." + str(y), phases - ) + # Initialize the string: + transfo_creation_string += "New Transformer." - # Number of windings - if hasattr(i, "windings") and i.windings is not None: - try: - transfo_creation_string += " windings={}".format( - len(i.windings) - ) - except: - pass + # Name: + transfo_name = "trans_{}".format( + i.name + ) # Maybe not the best naming convention.... + transfo_creation_string += transfo_name - # buses: + # Number of Phases + nphases = 0 + if hasattr(i, "windings") and i.windings is not None: if ( - hasattr(i, "from_element") - and i.from_element is not None - and hasattr(i, "to_element") - and i.to_element is not None + hasattr(i.windings[0], "phase_windings") + and i.windings[0].phase_windings is not None ): - transfo_creation_string += " buses=({b1}.{p},{b2}.{p})".format( - b1=re.sub('[^0-9a-zA-Z]+', '_', i.from_element), b2=re.sub('[^0-9a-zA-Z]+', '_', i.to_element), p=phase_string + nphases = len(i.windings[0].phase_windings) + transfo_creation_string += f" phases={nphases}" + phases = [ + self.phase_mapping(x.phase) + for x in i.windings[0].phase_windings + ] + phase_string = reduce( + lambda x, y: str(x) + "." + str(y), phases ) - # Conns - if hasattr(i, "windings") and i.windings is not None: - conns = " conns=(" - for w, winding in enumerate(i.windings): - if hasattr( - i.windings[w], "connection_type" - ) and i.windings[w].connection_type in ["Y", "D", "Z"]: - mapp = {"Y": "Wye", "D": "Delta", "Z": "Zigzag"} - conns += mapp[i.windings[w].connection_type] + ", " - conns = conns[:-2] - conns += ")" - if conns == " conns)": - conns = "" - transfo_creation_string += conns - - # kvs - if hasattr(i, "windings") and i.windings is not None: - kvs = " kvs=(" - for w, winding in enumerate(i.windings): - if hasattr(i.windings[w], "nominal_voltage"): - kvs += ( - str(i.windings[w].nominal_voltage * 10 ** -3) - + ", " - ) - if ( - not substation_name + "_" + feeder_name - in self._baseKV_feeders_ - ): - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ] = set() - if ( - i.windings[w].nominal_voltage < 300 - ): # Line-Neutral voltage for 120 V - self._baseKV_.add( - i.windings[w].nominal_voltage - * math.sqrt(3) - * 10 ** -3 - ) - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ].add( - winding.nominal_voltage - * math.sqrt(3) - * 10 ** -3 - ) - else: - self._baseKV_.add( - i.windings[w].nominal_voltage * 10 ** -3 - ) - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ].add(winding.nominal_voltage * 10 ** -3) - - kvs = kvs[:-2] - kvs += ")" - transfo_creation_string += kvs - - # kvas - if hasattr(i, "windings") and i.windings is not None: - kvas = " kvas=(" - for w, winding in enumerate(i.windings): + # Number of windings + if hasattr(i, "windings") and i.windings is not None: + try: + transfo_creation_string += " windings={}".format( + len(i.windings) + ) + except: + pass + + # buses: + if ( + hasattr(i, "from_element") + and i.from_element is not None + and hasattr(i, "to_element") + and i.to_element is not None + ): + transfo_creation_string += " buses=({b1}.{p},{b2}.{p})".format( + b1=re.sub('[^0-9a-zA-Z]+', '_', i.from_element), + b2=re.sub('[^0-9a-zA-Z]+', '_', i.to_element), p=phase_string + ) + + # Conns + if hasattr(i, "windings") and i.windings is not None: + conns = " conns=(" + for w, winding in enumerate(i.windings): + if hasattr( + i.windings[w], "connection_type" + ) and i.windings[w].connection_type in ["Y", "D", "Z"]: + mapp = {"Y": "Wye", "D": "Delta", "Z": "Zigzag"} + conns += mapp[i.windings[w].connection_type] + ", " + conns = conns[:-2] + conns += ")" + if conns == " conns)": + conns = "" + transfo_creation_string += conns + + # kvs + if hasattr(i, "windings") and i.windings is not None: + kvs = " kvs=(" + for winding in i.windings: + if hasattr(winding, "nominal_voltage"): + kvs += ( + str(winding.nominal_voltage * 10 ** -3) + + ", " + ) if ( - hasattr(i.windings[w], "rated_power") - and i.windings[w].rated_power is not None + not substation_name + "_" + feeder_name + in self._baseKV_feeders_ ): - kvas += ( - str(i.windings[w].rated_power * 10 ** -3) + ", " - ) - kvas = kvas[:-2] - kvas += ")" - transfo_creation_string += kvas + self._baseKV_feeders_[ + substation_name + "_" + feeder_name + ] = set() + if ( + winding.nominal_voltage < 300 + or nphases == 1 + ): # Line-Neutral voltage for 120 V or single phase trfo + kVLL = round(winding.nominal_voltage * math.sqrt(3) * 10 ** -3, 3) + else: + kVLL = round(winding.nominal_voltage * 10 ** -3, 3) - # emergency_power - if hasattr(i, "windings") and i.windings is not None: + self._baseKV_.add(kVLL) + self._baseKV_feeders_[substation_name + "_" + feeder_name].add(kVLL) + + kvs = kvs[:-2] # drop the last ", " + kvs += ")" + transfo_creation_string += kvs + + # kvas + if hasattr(i, "windings") and i.windings is not None: + kvas = " kvas=(" + for w, winding in enumerate(i.windings): if ( - hasattr(i.windings[0], "emergency_power") - and i.windings[0].emergency_power is not None + hasattr(i.windings[w], "rated_power") + and i.windings[w].rated_power is not None ): - transfo_creation_string += " EmerghKVA={}".format( - i.windings[0].emergency_power + kvas += ( + str(i.windings[w].rated_power * 10 ** -3) + ", " ) + kvas = kvas[:-2] + kvas += ")" + transfo_creation_string += kvas - # reactances: - if hasattr(i, "reactances") and i.reactances is not None: - # XHL: - try: - if isinstance(i.reactances[0], (int, float)): - transfo_creation_string += " XHL={}".format( - i.reactances[0] - ) - except: - logger.warning( - "Could not extract XHL from regulator {name}".format( - name=i.name - ) + # emergency_power + if hasattr(i, "windings") and i.windings is not None: + if ( + hasattr(i.windings[0], "emergency_power") + and i.windings[0].emergency_power is not None + ): + transfo_creation_string += " EmerghKVA={}".format( + i.windings[0].emergency_power + ) + + # reactances: + if hasattr(i, "reactances") and i.reactances is not None: + # XHL: + try: + if isinstance(i.reactances[0], (int, float)): + transfo_creation_string += " XHL={}".format( + i.reactances[0] ) - pass - # XLT: - try: # probably an index error b/c cyme reader only has api_transformer.reactances = [float(xhl)] - if isinstance(i.reactances[1], (int, float)): - transfo_creation_string += " XLT={}".format( - i.reactances[1] - ) - except: - logger.warning( - "Could not extract XLT from regulator {name}".format( - name=i.name - ) + except: + logger.warning( + "Could not extract XHL from regulator {name}. Setting XHL=1".format( + name=i.name ) - pass - # XHT: - try: # probably an index error b/c cyme reader only has api_transformer.reactances = [float(xhl)] - if isinstance(i.reactances[2], (int, float)): - transfo_creation_string += " XHT={}".format( - i.reactances[2] - ) - except: - logger.warning( - "Could not extract XHT from regulator {name}".format( - name=i.name - ) + ) + transfo_creation_string += " XHL=1" + # XLT: + try: # probably an index error b/c cyme reader only has api_transformer.reactances = [float(xhl)] + if isinstance(i.reactances[1], (int, float)): + transfo_creation_string += " XLT={}".format( + i.reactances[1] ) - pass - - txt += " transformer={trans}".format(trans=transfo_name) - - # Winding - if hasattr(i, "winding") and i.winding is not None: - txt += " winding={w}".format(w=i.winding) - else: - txt += " winding=2" - - # CTprim - if hasattr(i, "ct_prim") and i.ct_prim is not None: - txt += " CTprim={CT}".format(CT=i.ct_prim) - - # noload_loss - if hasattr(i, "noload_loss") and i.noload_loss is not None: - txt += " %noLoadLoss={nL}".format(NL=i.noload_loss) - - # Delay - if hasattr(i, "delay") and i.delay is not None: - txt += " delay={d}".format(d=i.delay) - - # highstep - # if hasattr(i, "highstep") and i.highstep is not None: - # txt += " maxtapchange={high}".format(high=i.highstep) - - # lowstep (Not mapped) - - # pt ratio - if hasattr(i, "pt_ratio") and i.pt_ratio is not None: - txt += " ptratio={PT}".format(PT=i.pt_ratio) + except: + logger.warning( + "Could not extract XLT from regulator {name}".format( + name=i.name + ) + ) + pass + # XHT: + try: # probably an index error b/c cyme reader only has api_transformer.reactances = [float(xhl)] + if isinstance(i.reactances[2], (int, float)): + transfo_creation_string += " XHT={}".format( + i.reactances[2] + ) + except: + logger.warning( + "Could not extract XHT from regulator {name}".format( + name=i.name + ) + ) + pass - # ct ratio (Not mapped) + txt += " transformer={trans}".format(trans=transfo_name) + + # Winding + txt += " winding=2" + if hasattr(i, "winding") and i.winding is not None: + txt += " winding={w}".format(w=i.winding) + + # band center + if hasattr(i, "bandcenter") and i.bandcenter is not None: + txt += " vreg={vreg}".format(vreg=i.bandcenter) + + # TODO WHAT IS GOING ON HERE? 1. overwrites vreg; 2. spaces not allowd in parameter setting + # if hasattr(i, "setpoint") and i.setpoint is not None: + # txt += " vreg = {setp}".format(setp=i.setpoint / 100.0 * 120) + + # bandwidth + if hasattr(i, "bandwidth") and i.bandwidth is not None: + txt += " band={b}".format( + b=i.bandwidth + ) # The bandwidth is operated at 120 V + + # Delay + if hasattr(i, "delay") and i.delay is not None: + txt += " delay={d}".format(d=i.delay) + + # pt ratio + if hasattr(i, "pt_ratio") and i.pt_ratio is not None: + txt += " ptratio={PT}".format(PT=i.pt_ratio) + + # CTprim + if hasattr(i, "ct_prim") and i.ct_prim is not None: + txt += " CTprim={CT}".format(CT=i.ct_prim) + + # R (Store in the Phase Windings of the transformer) + if i.name in self.compensator: + if "R" in self.compensator[i.name]: + if len(self.compensator[i.name]["R"]) == 1: + txt += " R={r}".format( + r=list(self.compensator[i.name]["R"])[0] + ) + else: + logger.warning( + """Compensator_r not the same for all windings of transformer {name}. + Using the first value for regControl {name2}.""".format( + name=i.connected_transformer, name2=i.name + ) + ) + txt += " R={r}".format( + r=list(self.compensator[i.name]["R"])[0] + ) - # phase shift (Not mapped) + # X (Store in the Phase Windings of the transformer) + if i.name in self.compensator: + if "X" in self.compensator[i.name]: + if len(self.compensator[i.name]["X"]) == 1: + txt += " X={x}".format( + x=list(self.compensator[i.name]["X"])[0] + ) + else: + logger.warning( + """Compensator_x not the same for all windings of transformer {name}. + Using the first value for regControl {name2}.""".format( + name=i.connected_transformer, name2=i.name + ) + ) + txt += " X={x}".format( + x=list(self.compensator[i.name]["X"])[0] + ) - # ltc (Not mapped) + # Pt phase + if hasattr(i, "pt_phase") and i.pt_phase is not None: + txt += " Ptphase={PT}".format(PT=self.phase_mapping(i.pt_phase)) - # bandwidth - if hasattr(i, "bandwidth") and i.bandwidth is not None: - txt += " band={b}".format( - b=i.bandwidth * 1.2 - ) # The bandwidth is operated at 120 V + # noload_loss + if hasattr(i, "noload_loss") and i.noload_loss is not None: + txt += " %noLoadLoss={nL}".format(NL=i.noload_loss) - # band center - if hasattr(i, "bandcenter") and i.bandcenter is not None: - txt += " vreg={vreg}".format(vreg=i.bandcenter) + # highstep + # if hasattr(i, "highstep") and i.highstep is not None: + # txt += " maxtapchange={high}".format(high=i.highstep) - # Pt phase - if hasattr(i, "pt_phase") and i.pt_phase is not None: - txt += " Ptphase={PT}".format(PT=self.phase_mapping(i.pt_phase)) + # lowstep (Not mapped) - # Voltage limit - if hasattr(i, "voltage_limit") and i.voltage_limit is not None: - txt += " vlimit={vlim}".format(vlim=i.voltage_limit) + # ct ratio (Not mapped) - if hasattr(i, "setpoint") and i.setpoint is not None: - txt += " vreg = {setp}".format(setp=i.setpoint / 100.0 * 120) + # phase shift (Not mapped) - # X (Store in the Phase Windings of the transformer) - if i.name in self.compensator: - if "X" in self.compensator[i.name]: - if len(self.compensator[i.name]["X"]) == 1: - txt += " X={x}".format( - x=list(self.compensator[i.name]["X"])[0] - ) - else: - logger.warning( - """Compensator_x not the same for all windings of transformer {name}. - Using the first value for regControl {name2}.""".format( - name=i.connected_transformer, name2=i.name - ) - ) - txt += " X={x}".format( - x=list(self.compensator[i.name]["X"])[0] - ) + # ltc (Not mapped) - # R (Store in the Phase Windings of the transformer) - if i.name in self.compensator: - if "R" in self.compensator[i.name]: - if len(self.compensator[i.name]["R"]) == 1: - txt += " R={r}".format( - r=list(self.compensator[i.name]["R"])[0] - ) - else: - logger.warning( - """Compensator_r not the same for all windings of transformer {name}. - Using the first value for regControl {name2}.""".format( - name=i.connected_transformer, name2=i.name - ) - ) - txt += " R={r}".format( - r=list(self.compensator[i.name]["R"])[0] - ) + # Voltage limit + if hasattr(i, "voltage_limit") and i.voltage_limit is not None: + txt += " vlimit={vlim}".format(vlim=i.voltage_limit) - txt += "\n\n" - if len(transfo_creation_string) > 0: - transfo_creation_string += "\n\n" - feeder_text_map[substation_name + "_" + feeder_name] = txt - transfo_creation_string_map[ - substation_name + "_" + feeder_name - ] = transfo_creation_string + txt += "\n\n" + if len(transfo_creation_string) > 0: + transfo_creation_string += "\n\n" + feeder_text_map[substation_name + "_" + feeder_name] = txt + transfo_creation_string_map[ + substation_name + "_" + feeder_name + ] = transfo_creation_string for substation_name in substation_text_map: for feeder_name in substation_text_map[substation_name]: @@ -2544,30 +2554,30 @@ def write_capacitors(self, model): if isinstance(i, Capacitor): + feeder_name = "DEFAULT" if ( self.separate_feeders and hasattr(i, "feeder_name") and i.feeder_name is not None ): feeder_name = i.feeder_name - else: - feeder_name = "DEFAULT" + + substation_name = "DEFAULT" if ( self.separate_substations and hasattr(i, "substation_name") and i.substation_name is not None ): substation_name = i.substation_name - else: - substation_name = "DEFAULT" if not substation_name in substation_text_map: substation_text_map[substation_name] = set([feeder_name]) else: substation_text_map[substation_name].add(feeder_name) txt = "" - if substation_name + "_" + feeder_name in feeder_text_map: - txt = feeder_text_map[substation_name + "_" + feeder_name] + sub_fdr_key = substation_name + "_" + feeder_name + if sub_fdr_key in feeder_text_map: + txt = feeder_text_map[sub_fdr_key] # Name if hasattr(i, "name") and i.name is not None: @@ -2611,18 +2621,16 @@ def write_capacitors(self, model): txt += " Kv={volt}".format( volt=i.nominal_voltage * 10 ** -3 ) # OpenDSS in Kvolts - if not substation_name + "_" + feeder_name in self._baseKV_feeders_: - self._baseKV_feeders_[ - substation_name + "_" + feeder_name - ] = set() + if not sub_fdr_key in self._baseKV_feeders_: + self._baseKV_feeders_[sub_fdr_key] = set() if i.nominal_voltage < 300: # Line-Neutral voltage for 120 V self._baseKV_.add(i.nominal_voltage * 10 ** -3) - self._baseKV_feeders_[substation_name + "_" + feeder_name].add( + self._baseKV_feeders_[sub_fdr_key].add( i.nominal_voltage * math.sqrt(3) * 10 ** -3 ) else: self._baseKV_.add(i.nominal_voltage * 10 ** -3) - self._baseKV_feeders_[substation_name + "_" + feeder_name].add( + self._baseKV_feeders_[sub_fdr_key].add( i.nominal_voltage * 10 ** -3 ) @@ -2640,8 +2648,8 @@ def write_capacitors(self, model): ) # Rated kvar - # In DiTTo, this is splitted accross phase_capacitors - # We thus have to sum them up + # In DiTTo, this is split accross phase_capacitors + # We thus have to sum them up (3 phase kVaR is split equally by openDSS) total_var = 0 if hasattr(i, "phase_capacitors") and i.phase_capacitors is not None: for phase_capacitor in i.phase_capacitors: @@ -2654,75 +2662,118 @@ def write_capacitors(self, model): except: logger.error( "Cannot compute Var of capacitor {name}".format( - name=name + name=i.name ) ) pass total_var *= 10 ** -3 # OpenDSS in Kvar txt += " Kvar={kvar}".format(kvar=total_var) - # We create a CapControl if we have valid input - # values that indicate that we should - create_capcontrol = False - if (hasattr(i, "name") and i.name is not None) and ( - (hasattr(i, "delay") and i.delay is not None) - or (hasattr(i, "mode") and i.mode is not None) - or (hasattr(i, "low") and i.low is not None) - or (hasattr(i, "high") and i.high is not None) - or (hasattr(i, "pt_ratio") and i.pt_ratio is not None) - or (hasattr(i, "ct_ratio") and i.ct_ratio is not None) - or (hasattr(i, "pt_phase") and i.pt_phase is not None) + """ CapControl Object + openDSS (DiTTo) values in order (some documentation copied here from June 2021 manual): + - Element (Capacitor.measuring_element) REQUIRED + - Capacitor (Capacitor.name) REQUIRED + - Type (Capacitor.mode) {Current | voltage | kvar | PF | time } REQUIRED Control type + - CTPhase + - Number of the phase being monitored for CURRENT control or one of {AVG | MAX | MIN} for all phases. Default=1. If delta or L-L connection, enter the first or the two phases being monitored [1-2, 2-3, 3-1]. Must be less than the number of phases. Does not apply to kvar control which uses all phases by default. + - CTratio + - Ratio of the CT from line amps to control ampere setting for current and kvar control type + - DeadTime + - time after capacitor is turned OFF before it can be turned back ON. default 300 seconds + - Delay (Capacitor.delay) + - time delay, in seconds, from when the control is armed before it sends out the switching command to turn ON. default 15 seconds + - DelayOFF (Capacitor.delay) + - default 15 seconds + - EventLog + - {Yes/True* | No/False} Default is YES for CapControl. Log control actions to Eventlog. + - NOTE event logging can significantly slow down solve times (esp. for time series) + - OFFsetting (Capacitor.low) + - Value at which the control arms to switch the capacitor OFF. + - ONsetting (Capacitor.high) + - Value at which the control arms to switch the capacitor ON (or ratchet up a step). Type of Control: + - Current: Line Amps / CTratio + - Voltage: Line-Neutral (or Line-Line for delta) Volts / PTratio + - kvar: Total kvar, all phases (3-phase for pos seq model). This is directional. + PF: Power Factor, Total power in monitored terminal. Negative for Leading. + Time: Hrs from Midnight as a floating point number (decimal). 7:30am would be entered as 7.5. + - PTPhase (Capacitor.pt_phase) + - Number of the phase being monitored for VOLTAGE control or one of {AVG | MAX | MIN} for all phases. Default=1. If delta or L-L connection, enter the first or the two phases being monitored [1-2, 2-3, 3-1]. Must be less than the number of phases. Does not apply to kvar control which uses all phases by default. + - PTratio (Capacitor.pt_ratio) + - Ratio of the PT that converts the monitored voltage to the control voltage. Default is 60. If the capacitor is Wye, the 1st phase line-to-neutral voltage is monitored. Else, the line-to-line voltage (1st - 2nd phase) is monitored. + - terminal + - Number of the terminal of the circuit element to which the CapControl is connected. 1 or 2, typically. Default is 1. + - VBus + - Name of bus to use for voltage override function. Default is bus at monitored terminal. Sometimes it is useful to monitor a bus in another location to emulate various DMS control algorithms. + - Vmax + - Maximum voltage, in volts. If the voltage across the capacitor divided by the PTRATIO is greater than this voltage, the capacitor will switch OFF regardless of other control settings. Default is 126 (goes with a PT ratio of 60 for 12.47 kV system). + - Vmin + - Minimum voltage, in volts. If the voltage across the capacitor divided by the PTRATIO is less than this voltage, the capacitor will switch ON regardless of other control settings. Default is 115 (goes with a PT ratio of 60 for 12.47 kV system). + - VoltOverride + - {Yes | No} Default is No. Switch to indicate whether VOLTAGE OVERRIDE is to be considered. Vmax and Vmin must be set to reasonable values if this property is Yes. + """ + if ( + i.name is not None and + i.measuring_element is not None and + i.mode is not None and + i.low is not None and + i.high is not None ): - create_capcontrol = True + txt += f"\n\nNew CapControl.{i.name} Element=Line.{i.measuring_element} Capacitor={i.name}" - # Create CapControl - if create_capcontrol: - txt += "\n\nNew CapControl.{name} Capacitor={name}".format( - name=i.name - ) + # mode (CONTROL) + txt += f" Type={self.mode_mapping(i.mode)}" - # Element (CONTROL) - if ( - hasattr(i, "measuring_element") - and i.measuring_element is not None - ): - txt += " Element=Line.{elt} Terminal=1".format( - elt=i.measuring_element - ) + # CTPhase not in Capacitor model + + # CTRatio (CONTROL) + if hasattr(i, "ct_ratio") and i.ct_ratio is not None: + txt += " Ctratio={CT}".format(CT=i.ct_ratio) + + # DeadTime not in Capacitor model # Delay (CONTROL) if hasattr(i, "delay") and i.delay is not None: txt += " delay={d}".format(d=i.delay) - # mode (CONTROL) - if hasattr(i, "mode") and i.mode is not None: - txt += " Type={m}".format(m=self.mode_mapping(i.mode)) + # DelayOFF not in Capacitor model + # EventLog not in Capacitor model - # Low (CONTROL) - if hasattr(i, "low") and i.low is not None: - txt += " Vmin={vmin}".format(vmin=i.low) + # OFFsetting + if i.high is not None: + txt += f" OFFsetting={i.high}" - # high (CONTROL) - if hasattr(i, "high") and i.high is not None: - txt += " Vmax={vmax}".format(vmax=i.high) + # ONsetting + if i.low is not None: + txt += f" ONsetting={i.low}" + + # Pt phase (CONTROL) + if hasattr(i, "pt_phase") and i.pt_phase is not None: + txt += " PTPhase={PT}".format(PT=self.phase_mapping(i.pt_phase)) # Pt ratio (CONTROL) if hasattr(i, "pt_ratio") and i.pt_ratio is not None: txt += " Ptratio={PT}".format(PT=i.pt_ratio) - # Ct ratio (CONTROL) - if hasattr(i, "ct_ratio") and i.ct_ratio is not None: - txt += " Ctratio={CT}".format(CT=i.ct_ratio) + # terminal not in Capacitor model + # VBus not in Capacitor model - # Pt phase (CONTROL) - if hasattr(i, "pt_phase") and i.pt_phase is not None: - txt += " PTPhase={PT}".format(PT=self.phase_mapping(i.pt_phase)) + # vmax override (CONTROL) + if i.vmax is not None: + txt += f" Vmax={i.vmax}" + + # vmin override (CONTROL) + if i.vmin is not None: + txt += f" Vmin={i.vmin}" + if i.vmax is not None or i.vmin is not None: + txt += " VoltOverride=Yes" + txt += "\n\n" - feeder_text_map[substation_name + "_" + feeder_name] = txt + feeder_text_map[sub_fdr_key] = txt + for substation_name in substation_text_map: for feeder_name in substation_text_map[substation_name]: - txt = feeder_text_map[substation_name + "_" + feeder_name] + txt = feeder_text_map[sub_fdr_key] feeder_name = re.sub('[^0-9a-zA-Z]+', '_', feeder_name.lower()) substation_name = re.sub('[^0-9a-zA-Z]+', '_', substation_name.lower()) if txt != "": @@ -2904,6 +2955,7 @@ def write_lines(self, model): else: continue + """ if hasattr(i,'positions') and i.positions is not None and len(i.positions) > 0: intermediate_txt += i.name for position in i.positions: @@ -2926,6 +2978,42 @@ def write_lines(self, model): # line type (Not mapped) + order for opendss is: + (order matters b/c o.w. causes seg faults c.f https://sourceforge.net/p/electricdss/discussion/861977/thread/e29ef76db9/) + bus1 + bus2 + Linecode + Length + Phases + R1 + X1 + R0 + X0 + C1 + C0 + B1 + B0 + Normamps + Emergamps + Faultrate + Pctperm + Repair + BaseFreq + Rmatrix + Xmatrix + Cmatrix + Switch + Rg + Xg + Rho + Geometry + EarthModel + Units + Seasons + Ratings + LineType + Like + """ # from_element if hasattr(i, "from_element") and i.from_element is not None: txt += " bus1={from_el}".format(from_el=re.sub('[^0-9a-zA-Z]+', '_', i.from_element)) @@ -2950,6 +3038,27 @@ def write_lines(self, model): ): txt += ".{p}".format(p=self.phase_mapping(wire.phase)) + geometry = "" + if i in lines_to_geometrify: + geometry = " geometry={g}".format(g=i.nameclass) # added later in the correct order + elif i in lines_to_linecodify: + txt += " Linecode={c}".format(c=re.sub('[^0-9a-zA-Z]+', '_', i.nameclass)) + + # Length + if hasattr(i, "length") and i.length is not None: + txt += " Length={length}".format( + length=max( + 0.001, self.convert_from_meters(np.real(i.length), u"km") + ) + ) + + # Set the units in miles for comparison (IEEE 13 nodes feeder) + # TODO: Let the user specify the export units + txt += " Units=km" + + # nominal_voltage (Not mapped) + + # line type (Not mapped) # is_switch or is_breaker if (hasattr(i, "is_switch") and i.is_switch == 1) or ( hasattr(i, "is_breaker") and i.is_breaker == 1 @@ -2972,6 +3081,10 @@ def write_lines(self, model): txt += " enabled=n" else: txt += " enabled=y" + # enabled is not in opendss manual for line objects? + + if len(geometry) > 1: + txt += geometry # is_fuse if hasattr(i, "is_fuse") and i.is_fuse == 1: @@ -3435,10 +3548,9 @@ def write_linecodes(self, list_of_lines, feeder_name=None, substation_name=None) ) if i.nameclass is not None: + n_phases = "" if "nphases" in parsed_line: n_phases = str(parsed_line["nphases"]) - else: - n_phases = "" nameclass_phase = i.nameclass + "_" + n_phases if nameclass_phase not in self.all_linecodes: self.all_linecodes[nameclass_phase] = parsed_line @@ -3561,7 +3673,7 @@ def write_linecodes(self, list_of_lines, feeder_name=None, substation_name=None) ) as fp: for linecode_name, linecode_data in txt.items(): fp.write("New Linecode.{name}".format(name=re.sub('[^0-9a-zA-Z]+', '_', linecode_name))) - for k, v in linecode_data.items(): + for k, v in linecode_data.items(): # needs an order fp.write(" {k}={v}".format(k=k, v=v)) fp.write("\n\n") diff --git a/setup.py b/setup.py index d2831b03..f2d19a13 100644 --- a/setup.py +++ b/setup.py @@ -27,28 +27,35 @@ version = version.splitlines()[1].split()[2].strip('"').strip("'") test_requires = [ - "backports.tempfile~=1.0", - "pytest~=7.0.1", - "pytest-cov~=4.0.0", - "sphinx-rtd-theme~=1.1.1", - "nbsphinx~=0.8.10", - "sphinxcontrib-napoleon~=0.7", - "ghp-import~=2.1.0", + "backports.tempfile>=1.0", + "pytest>=7.0.1", + "pytest-cov>=4.0.0", + "sphinx-rtd-theme>=1.1.1", + "nbsphinx>=0.8.10", + "sphinxcontrib-napoleon>=0.7", + "ghp-import>=2.1.0", ] -numpy_dependency = "numpy~=1.20.1" +numpy_dependency = "numpy>=1.20.0" -extras_requires = ["lxml~=4.9.1", "pandas~=1.3.5", "scipy~=1.7.3", numpy_dependency, "XlsxWriter~=3.0.3"] +extras_requires = [ + "lxml>=4.9.1", + "pandas", + "scipy>=1.7.3", + numpy_dependency, + "XlsxWriter>=3.0.3", +] -opendss_requires = ["OpenDSSDirect.py~=0.7.0", "pandas~=1.3.5", numpy_dependency] -dew_requires = [numpy_dependency, "xlrd~=2.0.1"] -gridlabd_requires = ["croniter~=1.3.7", numpy_dependency] +opendss_requires = ["OpenDSSDirect.py>=0.7.0", "pandas", numpy_dependency] +dew_requires = [numpy_dependency, "xlrd>=2.0.1"] +gridlabd_requires = ["croniter>=1.3.7", numpy_dependency] cyme_requires = [numpy_dependency] -ephasor_requires = [numpy_dependency, "pandas~=1.3.5"] +ephasor_requires = [numpy_dependency, "pandas"] synergi_requires = [ numpy_dependency, - "pandas_access~=0.0.1", + "pandas_access>=0.0.1", ] # Need pandas_access to convert the MDB tables to Pandas DataFrame +windmil_requires = ["bokeh", "xmltodict"] class PostDevelopCommand(develop): @@ -109,7 +116,16 @@ def run(self): "Programming Language :: Python :: 3.6", ], test_suite="tests", - install_requires=["click~=8.0.4", "future~=0.18.2", "networkx~=2.5.1", "six~=1.16.0", "traitlets~=5.1.1", "json_tricks~=3.16.1", "pandas~=1.3.5", numpy_dependency], + install_requires=[ + "click>=8.0.4", + "future>=0.18.2", + "networkx>=2.5.1", + "six>=1.16.0", + "traitlets>=5.1.1", + "json_tricks>=3.16.1", + "pandas", + numpy_dependency, + ], extras_require={ "all": extras_requires + opendss_requires @@ -117,7 +133,8 @@ def run(self): + gridlabd_requires + ephasor_requires + cyme_requires - + synergi_requires, + + synergi_requires + + windmil_requires, "extras": extras_requires, "cyme": cyme_requires, "dew": dew_requires, @@ -125,6 +142,7 @@ def run(self): "synergi": synergi_requires, "gridlabd": gridlabd_requires, "opendss": opendss_requires, + "windmil": windmil_requires, "test": test_requires, "dev": test_requires + ["pypandoc", "black", "pre-commit"], }, 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(): """