From 266cdef59fbb2df7913aaaebc0d6bbb23c82595b Mon Sep 17 00:00:00 2001 From: Daniel Thom Date: Wed, 11 Nov 2020 18:36:46 -0700 Subject: [PATCH] Refactor element containers in Store --- ditto/metric_computer.py | 1 - ditto/models/base.py | 27 +- ditto/models/capacitor.py | 3 - ditto/models/feeder_metadata.py | 4 - ditto/models/line.py | 28 +- ditto/models/load.py | 3 - ditto/models/load_layer.py | 3 - ditto/models/meter.py | 3 - ditto/models/node.py | 5 +- ditto/models/phase_capacitor.py | 3 - ditto/models/phase_load.py | 3 - ditto/models/phase_reactor.py | 3 - ditto/models/phase_storage.py | 6 - ditto/models/phase_winding.py | 3 - ditto/models/photovoltaic.py | 3 - ditto/models/position.py | 3 - ditto/models/power_source.py | 3 - ditto/models/powertransformer.py | 7 - ditto/models/reactor.py | 3 - ditto/models/regulator.py | 3 - ditto/models/storage.py | 6 - ditto/models/timeseries.py | 3 - ditto/models/weather_layer.py | 3 - ditto/models/winding.py | 3 - ditto/models/wire.py | 6 +- ditto/modify/system_structure.py | 770 +++++++++--------- ditto/network/network.py | 2 - ditto/readers/abstract_reader.py | 1 - ditto/readers/csv/read.py | 1 - ditto/readers/cyme/read.py | 2 - ditto/readers/opendss/read.py | 54 +- ditto/store.py | 242 ++++-- ditto/writers/cyme/write.py | 1 - docs/metrics.md | 3 - tests/default_values/test_default_values.py | 1 - .../test_remove_opendss_default_values.py | 1 - tests/persistence/test_persistence.py | 1 - tests/persistence/update_json.py | 1 - .../Capacitors/test_capacitor_connectivity.py | 1 - .../opendss/Capacitors/test_capacitor_kvar.py | 1 - tests/readers/opendss/Lines/test_fuses.py | 1 - .../opendss/Lines/test_line_connectivity.py | 1 - .../readers/opendss/Lines/test_line_length.py | 1 - tests/readers/opendss/Lines/test_linecodes.py | 1 - .../opendss/Lines/test_linegeometries.py | 1 - tests/readers/opendss/Lines/test_switches.py | 1 - .../opendss/Loads/test_load_p_and_q.py | 1 - tests/readers/opendss/Loads/test_loads.py | 1 - tests/readers/opendss/Nodes/test_nodes.py | 1 - .../opendss/Powersource/test_powersource.py | 31 +- .../opendss/Regulators/test_regulators.py | 1 - .../Transformers/test_transformer_kv.py | 1 - tests/test_demo_to_gridlabd.py | 1 - tests/test_gridlabd_to_ephasor.py | 1 - tests/test_json_parsers.py | 4 - tests/test_metric_extraction.py | 2 - tests/test_opendss_to_cyme.py | 1 - tests/test_opendss_to_ephasor.py | 1 - tests/test_opendss_to_gridlabd.py | 1 - tests/test_opendss_transformer.py | 2 - tests/test_store.py | 64 ++ tests/test_writer.py | 3 - .../writers/opendss/Lines/test_lines_write.py | 2 - 63 files changed, 695 insertions(+), 649 deletions(-) create mode 100644 tests/test_store.py diff --git a/ditto/metric_computer.py b/ditto/metric_computer.py index 1df1e32d..cb48dc55 100644 --- a/ditto/metric_computer.py +++ b/ditto/metric_computer.py @@ -52,7 +52,6 @@ def compute(self): self.reader.parse(self.m) self.net = network_analyzer(self.m) - self.net.model.set_names() # If we compute the metrics per feeder, we need to have the objects taged with their feeder_names if self.by_feeder: # Split the network into feeders (assumes objects have been taged) diff --git a/ditto/models/base.py b/ditto/models/base.py index 568d2ade..d08bef4f 100644 --- a/ditto/models/base.py +++ b/ditto/models/base.py @@ -20,26 +20,19 @@ class DiTToHasTraits(T.HasTraits): response = T.Any(allow_none=True, help="default trait for managing return values") - def __init__(self, model, *args, **kwargs): - model.model_store.append(self) - self.build(model) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - 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) - model.model_names[name] = self - except AttributeError: - pass - def build(self, model): - raise NotImplementedError( - "Build function must be implemented by derived classes" - ) + """Optional function to perform post-init construction after an element + has been added to a model. + + Parameters + ---------- + model : Store + + """ + self._model = model def notify_access(self, bunch): if not isinstance(bunch, T.Bunch): diff --git a/ditto/models/capacitor.py b/ditto/models/capacitor.py index 3761d2e5..14868454 100644 --- a/ditto/models/capacitor.py +++ b/ditto/models/capacitor.py @@ -122,6 +122,3 @@ class Capacitor(DiTToHasTraits): help="""Flag that indicates wheter the element is inside a substation or not.""", default_value=False, ) - - def build(self, model): - self._model = model diff --git a/ditto/models/feeder_metadata.py b/ditto/models/feeder_metadata.py index 8efec7b4..af3e3e7a 100644 --- a/ditto/models/feeder_metadata.py +++ b/ditto/models/feeder_metadata.py @@ -61,7 +61,3 @@ class Feeder_metadata(DiTToHasTraits): help="""Negative sequence reactance for the source equivalent.""", default_value=None, ) - - def build(self, model, Asset=None, ConnectivityNode=None, Location=None): - - self._model = model diff --git a/ditto/models/line.py b/ditto/models/line.py index 3a517518..36366528 100644 --- a/ditto/models/line.py +++ b/ditto/models/line.py @@ -141,21 +141,21 @@ class Line(DiTToHasTraits): default_value=None, ) - def build( - self, - model, - Asset=None, - Line=None, - ACLineSegment=None, - PSRType=None, - baseVoltage=None, - wireSpacingInfo=None, - Location=None, - Terminal1=None, - Terminal2=None, - ): + #def build( + # self, + # model, + # Asset=None, + # Line=None, + # ACLineSegment=None, + # PSRType=None, + # baseVoltage=None, + # wireSpacingInfo=None, + # Location=None, + # Terminal1=None, + # Terminal2=None, + #): - pass + # pass # diff --git a/ditto/models/load.py b/ditto/models/load.py index 8dfae3c6..6abe8775 100644 --- a/ditto/models/load.py +++ b/ditto/models/load.py @@ -140,6 +140,3 @@ class Load(DiTToHasTraits): help="""Percentage of the load between active 1 and active 2. Should be a float between 0 and 1.""", default_value=None, ) - - def build(self, model): - self._model = model diff --git a/ditto/models/load_layer.py b/ditto/models/load_layer.py index 912d8df3..164f21d6 100644 --- a/ditto/models/load_layer.py +++ b/ditto/models/load_layer.py @@ -19,6 +19,3 @@ class LoadLayer(DiTToHasTraits): Instance(Position), help="""This parameter is a list of positional points describing the load data. The positions are objects containing elements of long, lat and elevation (See Position object documentation).""", ) - - def build(self, model): - self._model = model diff --git a/ditto/models/meter.py b/ditto/models/meter.py index 6538bcbc..39ab722d 100644 --- a/ditto/models/meter.py +++ b/ditto/models/meter.py @@ -20,6 +20,3 @@ class Meter(DiTToHasTraits): Instance(Unicode), help="""This parameter is a list of all the phases at the node. The Phases are Strings of 'A', 'B', 'C', 'N', 's1' or 's2' (for secondaries).""", ) - - def build(self, model): - self._model = model diff --git a/ditto/models/node.py b/ditto/models/node.py index 8353d6e8..e1f57fd1 100644 --- a/ditto/models/node.py +++ b/ditto/models/node.py @@ -68,10 +68,7 @@ class Node(DiTToHasTraits): default_value=None, ) - def build(self, model, Asset=None, ConnectivityNode=None, Location=None): - - self._model = model - + #def build(self, model, Asset=None, ConnectivityNode=None, Location=None): # if ConnectivityNode is None: # self._cn = self._model.env.ConnectivityNode() diff --git a/ditto/models/phase_capacitor.py b/ditto/models/phase_capacitor.py index c569f8e6..acc7dd49 100644 --- a/ditto/models/phase_capacitor.py +++ b/ditto/models/phase_capacitor.py @@ -25,6 +25,3 @@ class PhaseCapacitor(DiTToHasTraits): help="""The normal number of sections connected to this phase""", default_value=None, ) - - def build(self, model): - self._model = model diff --git a/ditto/models/phase_load.py b/ditto/models/phase_load.py index d4cbfd14..dc849a25 100644 --- a/ditto/models/phase_load.py +++ b/ditto/models/phase_load.py @@ -72,6 +72,3 @@ class PhaseLoad(DiTToHasTraits): help="""This is the portion of reactive power load modeled as constant impedance. Reactive portions of current, power and impedance should all add to 1. Used for ZIP models.""", default_value=None, ) - - def build(self, model): - self._model = model diff --git a/ditto/models/phase_reactor.py b/ditto/models/phase_reactor.py index 72246538..fc68c013 100644 --- a/ditto/models/phase_reactor.py +++ b/ditto/models/phase_reactor.py @@ -42,6 +42,3 @@ class PhaseReactor(DiTToHasTraits): reactance = Float( help="""The total reactance of the phase reactor.""", default_value=None ) - - def build(self, model): - self._model = model diff --git a/ditto/models/phase_storage.py b/ditto/models/phase_storage.py index 389cfa7d..5d8280db 100644 --- a/ditto/models/phase_storage.py +++ b/ditto/models/phase_storage.py @@ -11,9 +11,3 @@ class PhaseStorage(DiTToHasTraits): default_value=None, ) q = Float(help="""Present var value. In vars.""", default_value=None) - - def build(self, model): - """ - TODO... - """ - self._model = model diff --git a/ditto/models/phase_winding.py b/ditto/models/phase_winding.py index 17103087..c6d7931a 100644 --- a/ditto/models/phase_winding.py +++ b/ditto/models/phase_winding.py @@ -21,6 +21,3 @@ class PhaseWinding(DiTToHasTraits): compensator_x = Float( help="""The compensator reactance value for the phase""", default_value=None ) - - def build(self, model): - self._model = model diff --git a/ditto/models/photovoltaic.py b/ditto/models/photovoltaic.py index ad941e30..f19fcc62 100644 --- a/ditto/models/photovoltaic.py +++ b/ditto/models/photovoltaic.py @@ -122,6 +122,3 @@ class Photovoltaic(DiTToHasTraits): feeder_name = Unicode( help="""The name of the feeder the object is on.""", ).tag(default=None) - - def build(self, model): - self._model = model diff --git a/ditto/models/position.py b/ditto/models/position.py index 3efc17ec..65a4efce 100644 --- a/ditto/models/position.py +++ b/ditto/models/position.py @@ -8,6 +8,3 @@ class Position(DiTToHasTraits): long = Float(help="""Decimal Longitude""") lat = Float(help="""Decimal Latitude""") elevation = Float(help="""Decimal elevation (meters)""") - - def build(self, model): - self._model = model diff --git a/ditto/models/power_source.py b/ditto/models/power_source.py index ebb353c3..7fa8ae64 100644 --- a/ditto/models/power_source.py +++ b/ditto/models/power_source.py @@ -96,6 +96,3 @@ class PowerSource(DiTToHasTraits): zero_sequence_impedance = Complex( help="""Zero-sequence impedance of the source.""", default_value=None ) - - def build(self, model): - self._model = model diff --git a/ditto/models/powertransformer.py b/ditto/models/powertransformer.py index ff95ca36..ef33ac6a 100644 --- a/ditto/models/powertransformer.py +++ b/ditto/models/powertransformer.py @@ -94,10 +94,3 @@ class PowerTransformer(DiTToHasTraits): help="""The name of the substation to which the object is connected.""", ) feeder_name = Unicode(help="""The name of the feeder the object is on.""",) - - def build(self, model): - """ - The high and low properties are used to creat windings which are added to the windings list - Winding data (e.g. high_ground_reactance) should be referenced thorugh the windings list - """ - self._model = model diff --git a/ditto/models/reactor.py b/ditto/models/reactor.py index 16cf5da3..2b742516 100644 --- a/ditto/models/reactor.py +++ b/ditto/models/reactor.py @@ -71,6 +71,3 @@ class Reactor(DiTToHasTraits): # # impedance_matrix = List(List(Complex),help='''This provides the matrix representation of the reactor impedance in complex form. Computed from the values of GMR and distances of individual wires. Kron reduction is applied to make this a 3x3 matrix.''') # capacitance_matrix = List(List(Complex),help='''This provides the matrix representation of the reactor capacitance in complex form. Computed from the values of diameters and distances of individual wires. Kron reduction is applied to make this a 3x3 matrix.''') - - def build(self, model): - self._model = model diff --git a/ditto/models/regulator.py b/ditto/models/regulator.py index d5e8950a..ca70539a 100644 --- a/ditto/models/regulator.py +++ b/ditto/models/regulator.py @@ -129,6 +129,3 @@ class Regulator(DiTToHasTraits): help="""Flag that indicates wheter the element is inside a substation or not.""", default_value=False, ) - - def build(self, model): - self._model = model diff --git a/ditto/models/storage.py b/ditto/models/storage.py index 584b37bb..6e4ebbd6 100644 --- a/ditto/models/storage.py +++ b/ditto/models/storage.py @@ -116,9 +116,3 @@ class Storage(DiTToHasTraits): help="""Flag that indicates wheter the element is inside a substation or not.""", default_value=False, ) - - def build(self, model): - """ - TODO... - """ - self._model = model diff --git a/ditto/models/timeseries.py b/ditto/models/timeseries.py index 354d91f2..f47b4855 100644 --- a/ditto/models/timeseries.py +++ b/ditto/models/timeseries.py @@ -44,6 +44,3 @@ class Timeseries(DiTToHasTraits): feeder_name = Unicode( help="""The name of the feeder the object is on.""", ).tag(default=None) - - def build(self, model): - self._model = model diff --git a/ditto/models/weather_layer.py b/ditto/models/weather_layer.py index 15f9fb41..ecddeb85 100644 --- a/ditto/models/weather_layer.py +++ b/ditto/models/weather_layer.py @@ -20,6 +20,3 @@ class WeatherLayer(DiTToHasTraits): Instance(Position), help="""This parameter is a list of positional points describing the weather data. The positions are objects containing elements of long, lat and elevation (See Position object documentation).""", ) - - def build(self, model): - self._model = model diff --git a/ditto/models/winding.py b/ditto/models/winding.py index 90ed573d..6dc0eecb 100644 --- a/ditto/models/winding.py +++ b/ditto/models/winding.py @@ -57,6 +57,3 @@ class Winding(DiTToHasTraits): emergency_power = Float( help="""The emergency power of the winding""", default_value=None ) - - def build(self, model): - self._model = model diff --git a/ditto/models/wire.py b/ditto/models/wire.py index e39707b7..275cb1ff 100644 --- a/ditto/models/wire.py +++ b/ditto/models/wire.py @@ -136,9 +136,9 @@ class Wire(DiTToHasTraits): default_value=None, ) - def build(self, model): - self._model = model - pass +# def build(self, model): +# self._model = model +# pass # self._wp = self._model.env.WirePosition() diff --git a/ditto/modify/system_structure.py b/ditto/modify/system_structure.py index b607c28a..b4f1b276 100644 --- a/ditto/modify/system_structure.py +++ b/ditto/modify/system_structure.py @@ -13,6 +13,8 @@ import numpy as np +from ditto.store import ElementNotFoundError +from ditto.models.capacitor import Capacitor from ditto.models.powertransformer import PowerTransformer from ditto.models.load import Load from ditto.models.node import Node @@ -57,11 +59,7 @@ def __init__(self, model, *args): if len(args) == 1: source = args[0] else: - srcs = [] - for obj in self.model.models: - if isinstance(obj, PowerSource) and obj.is_sourcebus == 1: - srcs.append(obj.name) - srcs = np.unique(srcs) + srcs = {x.name for x in self.model.iter_elements(PowerSource, lambda x: x.is_sourcebus == 1)} if len(srcs) == 0: raise ValueError("No PowerSource object found in the model.") elif len(srcs) > 1: @@ -74,9 +72,8 @@ def __init__(self, model, *args): # TODO: Get the source voltage properly... # - for x in self.model.models: + for x in self.model.iter_elements(PowerSource): if ( - isinstance(x, PowerSource) and hasattr(x, "nominal_voltage") and x.nominal_voltage is not None and x.is_sourcebus @@ -93,12 +90,9 @@ def __init__(self, model, *args): self.G = Network() self.G.build(self.model, source=self.source) - self.model.set_names() # Set the attributes in the graph self.G.set_attributes(self.model) - self.model.set_names() - # Equipment types and names on the edges self.edge_equipment = nx.get_edge_attributes(self.G.graph, "equipment") self.edge_equipment_name = nx.get_edge_attributes( @@ -110,7 +104,7 @@ def set_missing_coords_recur(self): If no adjacent nodes have positional values continue to compute recursively (via while loop) """ recur_nodes = [] - for i in self.model.models: + for i in self.model.iter_elements(): if ( hasattr(i, "positions") and ( @@ -155,7 +149,7 @@ def set_missing_coords_recur(self): num += 1 av_lat = av_lat / float(num) av_long = av_long / float(num) - computed_pos = Position(self.model) + computed_pos = Position() computed_pos.lat = av_lat computed_pos.long = av_long self.model[i].positions = [computed_pos] @@ -170,7 +164,7 @@ def set_feeder_metadata(self, feeder_name=None, substation=None, transformer=Non """This function sets the feeder metada and adds it to the model This can be used when parsing a file with feeder information to add feeder data to the model """ - feeder_metadata = Feeder_metadata(self.model) + feeder_metadata = Feeder_metadata() feeder_metadata.name = feeder_name feeder_metadata.transformer = transformer feeder_metadata.substation = substation @@ -182,34 +176,33 @@ def set_feeder_headnodes(self): So the goal of this function is to loop over the feeder_metadata, find the buses directly downstream of the substation (i.e. the headnodes), and put a value for the headnode attribute. If this convention changes, this function might need to be updated... """ - for obj in self.model.models: - if isinstance(obj, Feeder_metadata): - name_cleaned = obj.name.replace(".", "").lower().replace("_src", "") - headnodes = list(self.G.digraph.successors(obj.substation)) + for obj in self.model.iter_elements(Feeder_metadata): + name_cleaned = obj.name.replace(".", "").lower().replace("_src", "") + headnodes = list(self.G.digraph.successors(obj.substation)) - if name_cleaned in headnodes: - obj.headnode = name_cleaned # This should not be the case because of name conflicts - else: - cleaned_headnodes = [h.strip("x") for h in headnodes] + if name_cleaned in headnodes: + obj.headnode = name_cleaned # This should not be the case because of name conflicts + else: + cleaned_headnodes = [h.strip("x") for h in headnodes] - if name_cleaned in cleaned_headnodes: - obj.headnode = headnodes[cleaned_headnodes.index(name_cleaned)] - else: - reverse_headnodes = [] - for headnode in cleaned_headnodes: - if ">" in headnode: - a, b = headnode.split(">") - reverse_headnodes.append(b + "->" + a) - else: - reverse_headnodes.append(headnode) - if name_cleaned in reverse_headnodes: - obj.headnode = headnodes[ - reverse_headnodes.index(name_cleaned) - ] - obj.nominal_voltage = self.model[ - obj.headnode - ].nominal_voltage - obj.operating_voltage = obj.nominal_voltage + if name_cleaned in cleaned_headnodes: + obj.headnode = headnodes[cleaned_headnodes.index(name_cleaned)] + else: + reverse_headnodes = [] + for headnode in cleaned_headnodes: + if ">" in headnode: + a, b = headnode.split(">") + reverse_headnodes.append(b + "->" + a) + else: + reverse_headnodes.append(headnode) + if name_cleaned in reverse_headnodes: + obj.headnode = headnodes[ + reverse_headnodes.index(name_cleaned) + ] + obj.nominal_voltage = self.model[ + obj.headnode + ].nominal_voltage + obj.operating_voltage = obj.nominal_voltage def set_nominal_voltages_recur(self, *args): """This function sets the nominal voltage of the elements in the network. @@ -228,10 +221,11 @@ def set_nominal_voltages_recur(self, *args): (previous, node) ] == "PowerTransformer": trans_name = self.edge_equipment_name[(previous, node)] + trans = self.model.get_element(PowerTransformer, trans_name) new_value = min( [ w.nominal_voltage - for w in self.model[trans_name].windings + for w in trans.windings if w.nominal_voltage is not None ] ) @@ -239,17 +233,27 @@ def set_nominal_voltages_recur(self, *args): (node, previous) ] == "PowerTransformer": trans_name = self.edge_equipment_name[(node, previous)] + trans = self.model.get_element(PowerTransformer, trans_name) new_value = min( [ w.nominal_voltage - for w in self.model[trans_name].windings + for w in trans.windings if w.nominal_voltage is not None ] ) else: new_value = voltage - if hasattr(self.model[node], "nominal_voltage"): - self.model[node].nominal_voltage = new_value + + node_model = None + for model_type in (Node, Load, PowerSource, Capacitor): + try: + node_model = self.model.get_element(model_type, node) + except ElementNotFoundError: + continue + assert node_model is not None, f"did not find {node}" + + if hasattr(node_model, "nominal_voltage"): + node_model.nominal_voltage = new_value for child in self.G.digraph.successors(node): self.set_nominal_voltages_recur(child, new_value, node) @@ -258,12 +262,11 @@ def set_nominal_voltages_recur_line(self): .. warning:: Have to be called after set_nominal_voltages_recur. """ - for obj in self.model.models: - # If we get a line - if isinstance(obj, Line) and obj.nominal_voltage is None: + for obj in self.model.iter_elements(Line, lambda x: x.nominal_voltage is None): + if 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] + node_from_object = self.model.get_element(Node, obj.from_element) # If the from node has a known nominal voltage, then use this value if ( @@ -313,32 +316,36 @@ def set_load_coordinates(self, **kwargs): else: delta_elevation = 0 - for obj in self.model.models: - if isinstance(obj, Load) and obj.positions is None: - if ( - hasattr(obj, "connecting_element") - and obj.connecting_element is not None - ): - try: - if ( - hasattr(self.model[obj.connecting_element], "position") - and self.model[obj.connecting_element].position is not None - ): - position_obj = self.model[obj.connecting_element].positions - obj.positions = [] - for po in position_obj: - _long = po.long - _lat = po.lat - _elev = po.elevation - - load_position = Position() - load_position.long = _long + delta_longitude - load_position.lat = _lat + delta_latitude - load_position.elevation = _elev + delta_elevation - - obj.positions.append(load_position) - except: - pass + filter_func = lambda x: ( + x.positions is None + and hasattr(x, "connecting_element") + and x.connecting_element is not None + ) + for obj in self.model.iter_elements(Load, filter_func): + if ( + hasattr(obj, "connecting_element") + and obj.connecting_element is not None + ): + try: + if ( + hasattr(self.model[obj.connecting_element], "position") + and self.model[obj.connecting_element].position is not None + ): + position_obj = self.model[obj.connecting_element].positions + obj.positions = [] + for po in position_obj: + _long = po.long + _lat = po.lat + _elev = po.elevation + + load_position = Position() + load_position.long = _long + delta_longitude + load_position.lat = _lat + delta_latitude + load_position.elevation = _elev + delta_elevation + + obj.positions.append(load_position) + except: + pass def feeder_preprocessing(self): """Performs the feeder cut pre-processing step. @@ -364,81 +371,79 @@ def feeder_preprocessing(self): self._list_of_feeder_objects = [] # First step: Find all the transformer objects in the models - for elt in self.model.models: - if isinstance(elt, PowerTransformer): + for elt in self.model.iter_elements(PowerTransformer): + # If we have a substation... + if ( + hasattr(elt, "is_substation") + and elt.is_substation == 1 + and hasattr(elt, "name") + ): - # If we have a substation... - if ( - hasattr(elt, "is_substation") - and elt.is_substation == 1 - and hasattr(elt, "name") - ): + # Step 2: Find all elements downstream of this substation + downstream_elts = self.G.get_all_elements_downstream( + self.model, elt.to_element + ) - # Step 2: Find all elements downstream of this substation - downstream_elts = self.G.get_all_elements_downstream( - self.model, elt.to_element - ) + # Now, we might have substations in these elements. + # In this case we simply do nothing since these lower substations will be consider later in the outer loop. + # + # TODO:: Find a more clever way to do that without looping for nothing... + # + skip = False + for down_elt in downstream_elts: + if ( + hasattr(down_elt, "is_substation") + and down_elt.is_substation == 1 + ): + logger.debug( + "Info: substation {a} found downstream of substation {b}".format( + b=elt.name, a=down_elt.name + ) + ) + skip = True + break + # If no substation was found downstream, then set the substation_name and feeder_name attributes of the objects + if not skip: + self._list_of_feeder_objects.append(downstream_elts) - # Now, we might have substations in these elements. - # In this case we simply do nothing since these lower substations will be consider later in the outer loop. - # - # TODO:: Find a more clever way to do that without looping for nothing... - # - skip = False for down_elt in downstream_elts: if ( - hasattr(down_elt, "is_substation") - and down_elt.is_substation == 1 + down_elt.substation_name is not None + and len(down_elt.substation_name) != 0 ): - logger.debug( - "Info: substation {a} found downstream of substation {b}".format( - b=elt.name, a=down_elt.name + raise ValueError( + "Substation name for element {name} was already set at {_previous}. Trying to overwrite with {_next}".format( + name=down_elt.name, + _previous=down_elt.substation_name, + _next=elt.name, ) ) - skip = True - break - # If no substation was found downstream, then set the substation_name and feeder_name attributes of the objects - if not skip: - self._list_of_feeder_objects.append(downstream_elts) - - for down_elt in downstream_elts: - if ( - down_elt.substation_name is not None - and len(down_elt.substation_name) != 0 - ): - raise ValueError( - "Substation name for element {name} was already set at {_previous}. Trying to overwrite with {_next}".format( - name=down_elt.name, - _previous=down_elt.substation_name, - _next=elt.name, - ) - ) - else: - down_elt.substation_name = elt.name + else: + down_elt.substation_name = elt.name - if ( - down_elt.feeder_name is not None - and len(down_elt.feeder_name) != 0 - ): - raise ValueError( - "Feeder name for element {name} was already set at {_previous}. Trying to overwrite with {_next}".format( - name=down_elt.name, - _previous=down_elt.feeder_name, - _next="Feeder_" + elt.name, - ) + if ( + down_elt.feeder_name is not None + and len(down_elt.feeder_name) != 0 + ): + raise ValueError( + "Feeder name for element {name} was already set at {_previous}. Trying to overwrite with {_next}".format( + name=down_elt.name, + _previous=down_elt.feeder_name, + _next="Feeder_" + elt.name, ) - else: - down_elt.feeder_name = ( - "Feeder_" + elt.name - ) # Change the feeder naming convention here... + ) + else: + down_elt.feeder_name = ( + "Feeder_" + elt.name + ) # Change the feeder naming convention here... def replace_kth_switch_with_recloser(self): """ For every feeder, replace a switch downstream of the feeder head with a recloser. The depth of the switch is chosen at random with the distribution [0.7,0.25, 0.05] """ np.random.seed(0) - # Loop over the objects - for elt in self.model.models: + # TODO: Tarek, can we specify a type? + for elt in self.model.iter_elements(): # If we get a substation connection if ( hasattr(elt, "is_substation_connection") @@ -543,8 +548,6 @@ def set_nominal_voltages(self): .. 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 @@ -625,19 +628,17 @@ def set_nominal_voltages(self): # 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] + for obj in self.model.iter_elements(Line, lambda x: x.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.get_element(Node, 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 + # 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. @@ -648,37 +649,33 @@ def _set_nominal_voltages(self): .. 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 + for obj in self.model.iter_elements(element_type=Node, + filter_func=lambda x: x.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: @@ -731,18 +728,8 @@ def center_tap_load_preprocessing(self): - If we need to delete elments like phase loads or wires, we set the drop flag of the corresponding element to 1 such that they won't be outputed in the writer. This is much faster than deleting the elements for now (delete is looping which is time consumming). """ - # Set the names in the model. - # Required if we wish to access objects by names directly instead of looping - self.model.set_names() - # Build a list of all the loads in the DiTTo model - # - # TODO: More clever way to do this??? - # - load_list = [] - for _obj in self.model.models: - if isinstance(_obj, Load): - load_list.append(_obj) + load_list = self.model.list_elements(Load) # Get the connecting elements of the loads. # These will be the starting points of the upstream walks in the graph @@ -838,8 +825,6 @@ def center_tap_load_preprocessing(self): # For every transformer name... for t_name in transformer_names: # Try to get the corresponding DiTTo object by name - # Note: This should work if set_names() has been called before... - # If it fails, raise an error... try: t_obj = self.model[t_name] # Get the phases and clean @@ -983,77 +968,72 @@ def terminals_to_phases(self): - This function is using the network module to find upstream elements in an efficient way. """ - # Set the names in the model. - # Required if we wish to access objects by names directly instead of looping - self.model.set_names() - - for _obj in self.model.models: - if isinstance(_obj, Load): - connecting_element = _obj.connecting_element - load_name = _obj.name - load_phases = [] - # Get the phases of the load + for _obj in self.model.iter_elements(Load): + connecting_element = _obj.connecting_element + load_name = _obj.name + load_phases = [] + # Get the phases of the load + try: + l_obj = self.model[load_name] + for phase_load in l_obj.phase_loads: + load_phases.append(phase_load.phase) + except: + raise ValueError( + "Unable to retrieve DiTTo object with name {}".format(load_name) + ) + + continu = True + # Find the upstream transformer by walking the graph upstream + end_node = connecting_element + transformer_name = None + while continu: + # Get predecessor node of current node in the DAG try: - l_obj = self.model[load_name] - for phase_load in l_obj.phase_loads: - load_phases.append(phase_load.phase) + from_node = next(self.G.digraph.predecessors(end_node)) except: - raise ValueError( - "Unable to retrieve DiTTo object with name {}".format(load_name) - ) - - continu = True - # Find the upstream transformer by walking the graph upstream - end_node = connecting_element - transformer_name = None - while continu: - # Get predecessor node of current node in the DAG - try: - from_node = next(self.G.digraph.predecessors(end_node)) - except: - break - # Look for the type of equipment that makes the connection between from_node and to_node - _type = None - if (from_node, end_node) in self.edge_equipment: - _type = self.edge_equipment[(from_node, end_node)] - elif (end_node, from_node) in self.edge_equipment: - _type = self.edge_equipment[(end_node, from_node)] + break + # Look for the type of equipment that makes the connection between from_node and to_node + _type = None + if (from_node, end_node) in self.edge_equipment: + _type = self.edge_equipment[(from_node, end_node)] + elif (end_node, from_node) in self.edge_equipment: + _type = self.edge_equipment[(end_node, from_node)] - # It could be a Line, a Transformer... - # If it is a transformer, then we have found the upstream transformer... - if _type == "PowerTransformer": + # It could be a Line, a Transformer... + # If it is a transformer, then we have found the upstream transformer... + if _type == "PowerTransformer": - # ...we can then stop the loop... - continu = False + # ...we can then stop the loop... + continu = False - # ...and grab the transformer name to retrieve the data from the DiTTo object - if (from_node, end_node) in self.edge_equipment_name: - transformer_name = self.edge_equipment_name[ - (from_node, end_node) - ] - self.model[ - load_name - ].upstream_transformer_name = self.edge_equipment_name[ - (from_node, end_node) - ] - elif (end_node, from_node) in self.edge_equipment_name: - transformer_name = self.edge_equipment_name[ - (end_node, from_node) - ] - self.model[ - load_name - ].upstream_transformer_name = self.edge_equipment_name[ - (end_node, from_node) - ] - # If we cannot find the object, raise an error because it sould not be the case... - else: - raise ValueError( - "Unable to find equipment between {_from} and {_to}".format( - _from=from_node, _to=end_node - ) + # ...and grab the transformer name to retrieve the data from the DiTTo object + if (from_node, end_node) in self.edge_equipment_name: + transformer_name = self.edge_equipment_name[ + (from_node, end_node) + ] + self.model[ + load_name + ].upstream_transformer_name = self.edge_equipment_name[ + (from_node, end_node) + ] + elif (end_node, from_node) in self.edge_equipment_name: + transformer_name = self.edge_equipment_name[ + (end_node, from_node) + ] + self.model[ + load_name + ].upstream_transformer_name = self.edge_equipment_name[ + (end_node, from_node) + ] + # If we cannot find the object, raise an error because it sould not be the case... + else: + raise ValueError( + "Unable to find equipment between {_from} and {_to}".format( + _from=from_node, _to=end_node ) - # Go upstream... - end_node = from_node + ) + # Go upstream... + end_node = from_node # Number of windings is 1; we ignore it as it will have the phase of the primary transformer # Number of windings is 2; we will make sure the phases of the secondary winding of a transformer are the same as the phases of loads @@ -1082,7 +1062,6 @@ def open_close_switches(self, path_to_dss_file): - If the switch has attribute R1 equals to 1e11, then the switch is open - Otherwise it is closed. """ - self.model.set_names() with open(path_to_dss_file, "r") as f: lines = f.readlines() @@ -1119,162 +1098,161 @@ def set_switching_devices_ampacity(self): Look at the neighboring lines and use their ampacity as value. """ # Loop over the ditto objects and find the switches, breakers, sectionalizers, fuses, and reclosers - for obj in self.model.models: - if isinstance(obj, Line): - if ( - obj.is_switch is True - or obj.is_breaker is True - or obj.is_sectionalizer is True - or obj.is_fuse is True - or obj.is_recloser is True - ): + for obj in self.model.iter_elements(Line): + if ( + obj.is_switch is True + or obj.is_breaker is True + or obj.is_sectionalizer is True + or obj.is_fuse is True + or obj.is_recloser is True + ): - # Store the ampacities of the device's wires - amps = np.array([wire.ampacity for wire in obj.wires]) + # Store the ampacities of the device's wires + amps = np.array([wire.ampacity for wire in obj.wires]) - # and check if there are some nan values - if np.any(np.isnan(amps)): + # and check if there are some nan values + if np.any(np.isnan(amps)): - # 2 possibilities here: - # case 1: Not all ampacity ratings are nans - if not np.all(np.isnan(amps)): + # 2 possibilities here: + # case 1: Not all ampacity ratings are nans + if not np.all(np.isnan(amps)): - # This means that at least one of the device wires has a valid rating - valid_amps = amps[np.logical_not(np.isnan(amps))] + # This means that at least one of the device wires has a valid rating + valid_amps = amps[np.logical_not(np.isnan(amps))] - # Find it and use it for the other wires - if len(valid_amps) == 1: - amps_value = valid_amps[0] - else: - amps_value = np.max( - valid_amps - ) # we have different ratings accross wires. Heuristic: use the maximum. + # Find it and use it for the other wires + if len(valid_amps) == 1: + amps_value = valid_amps[0] + else: + amps_value = np.max( + valid_amps + ) # we have different ratings accross wires. Heuristic: use the maximum. - for wire in obj.wires: - wire.ampacity = amps_value + for wire in obj.wires: + wire.ampacity = amps_value - # Case 2: All ampacity ratings are nans - else: - # Here we look for a neighboring line object with a valid ampacity rating - if ( - obj.from_element is not None - and obj.to_element is not None - ): + # Case 2: All ampacity ratings are nans + else: + # Here we look for a neighboring line object with a valid ampacity rating + if ( + obj.from_element is not None + and obj.to_element is not None + ): - should_continue = True - from_node = obj.from_element - to_node = obj.to_element + should_continue = True + from_node = obj.from_element + to_node = obj.to_element - while should_continue: - if self.G.graph.has_node( - from_node - ) and self.G.graph.has_node(to_node): + while should_continue: + if self.G.graph.has_node( + from_node + ) and self.G.graph.has_node(to_node): - # Get the neighbors on the from side - neighbors_from = [ - n - for n in nx.neighbors( - self.G.graph, from_node - ) - if n != to_node - ] - - # Get the neighbors on the to side - neighbors_to = [ - n - for n in nx.neighbors(self.G.graph, to_node) - if n != from_node - ] - - amps_value = None - if len(neighbors_from) > 0: - - # To avoid infinite loops where we always consider the same objects, select neighbors randomly - idx = random.randint( - 0, - min( - len(neighbors_from), - len(neighbors_to), - ) - - 1, + # Get the neighbors on the from side + neighbors_from = [ + n + for n in nx.neighbors( + self.G.graph, from_node + ) + if n != to_node + ] + + # Get the neighbors on the to side + neighbors_to = [ + n + for n in nx.neighbors(self.G.graph, to_node) + if n != from_node + ] + + amps_value = None + if len(neighbors_from) > 0: + + # To avoid infinite loops where we always consider the same objects, select neighbors randomly + idx = random.randint( + 0, + min( + len(neighbors_from), + len(neighbors_to), ) + - 1, + ) - # we only have the from and to nodes. We need to find the name of the corresponding ditto object - if ( - neighbors_from[idx], - obj.from_element, - ) in self.edge_equipment_name: - - try: - # Try to get the object with its name - neighboring_line_obj = self.model[ - self.edge_equipment_name[ - ( - neighbors_from[idx], - from_node, - ) - ] + # we only have the from and to nodes. We need to find the name of the corresponding ditto object + if ( + neighbors_from[idx], + obj.from_element, + ) in self.edge_equipment_name: + + try: + # Try to get the object with its name + neighboring_line_obj = self.model[ + self.edge_equipment_name[ + ( + neighbors_from[idx], + from_node, + ) ] + ] - # If we have a valid ampacity rating, then use this value and exit the loop - if neighboring_line_obj.wires[ + # If we have a valid ampacity rating, then use this value and exit the loop + if neighboring_line_obj.wires[ + 0 + ].ampacity is not None and not np.isnan( + neighboring_line_obj.wires[ + 0 + ].ampacity + ): + amps_value = neighboring_line_obj.wires[ 0 - ].ampacity is not None and not np.isnan( - neighboring_line_obj.wires[ - 0 - ].ampacity - ): - amps_value = neighboring_line_obj.wires[ - 0 - ].ampacity - should_continue = False - - # If we failed for some reason, try on the to side - except: - amps_value = None - - # If we still haven't found a value and we have sone neighbors on the to side - if amps_value is None and len(neighbors_to) > 0: - - # Try to find the name of the object - if ( - to_node, - neighbors_to[idx], - ) in self.edge_equipment_name: - - try: - neighboring_line_obj = self.model[ - self.edge_equipment_name[ - (to_node, neighbors_to[idx]) - ] + ].ampacity + should_continue = False + + # If we failed for some reason, try on the to side + except: + amps_value = None + + # If we still haven't found a value and we have sone neighbors on the to side + if amps_value is None and len(neighbors_to) > 0: + + # Try to find the name of the object + if ( + to_node, + neighbors_to[idx], + ) in self.edge_equipment_name: + + try: + neighboring_line_obj = self.model[ + self.edge_equipment_name[ + (to_node, neighbors_to[idx]) ] - if neighboring_line_obj.wires[ + ] + if neighboring_line_obj.wires[ + 0 + ].ampacity is not None and not np.isnan( + neighboring_line_obj.wires[ 0 - ].ampacity is not None and not np.isnan( - neighboring_line_obj.wires[ - 0 - ].ampacity - ): - amps_value = neighboring_line_obj.wires[ - 0 - ].ampacity - should_continue = False - except: - amps_value = None - - # At this point, if we still haven't found a value, update the from and to node to - # continue the search further away from the initial object - if should_continue: - to_node = neighbors_to[idx] - from_node = to_node - - else: - raise ValueError( - "Missing nodes {n1} and/or {n2} in network".format( - n1=from_node, n2=to_node - ) + ].ampacity + ): + amps_value = neighboring_line_obj.wires[ + 0 + ].ampacity + should_continue = False + except: + amps_value = None + + # At this point, if we still haven't found a value, update the from and to node to + # continue the search further away from the initial object + if should_continue: + to_node = neighbors_to[idx] + from_node = to_node + + else: + raise ValueError( + "Missing nodes {n1} and/or {n2} in network".format( + n1=from_node, n2=to_node ) + ) - if amps_value is not None: - for wire in obj.wires: - wire.ampacity = amps_value + if amps_value is not None: + for wire in obj.wires: + wire.ampacity = amps_value diff --git a/ditto/network/network.py b/ditto/network/network.py index 8133b572..600beaff 100644 --- a/ditto/network/network.py +++ b/ditto/network/network.py @@ -366,7 +366,6 @@ def get_all_elements_downstream(self, model, source): This might be handy when trying to find all the objects below a substation such that the network can be properly seperated in different feeders for analysis. """ _elts = set() - model.set_names() # Checking that the network is already built # TODO: Log instead of printing... @@ -411,7 +410,6 @@ def get_all_elements_downstream(self, model, source): _elts.add(edge_equipment_name[(destination, source)]) # Get the corresponding DiTTo objects - # Warning: This will fail if set_names() has not been called before. _obj = [] for x in _elts: try: diff --git a/ditto/readers/abstract_reader.py b/ditto/readers/abstract_reader.py index cfcf1325..1d0ef14b 100644 --- a/ditto/readers/abstract_reader.py +++ b/ditto/readers/abstract_reader.py @@ -921,7 +921,6 @@ def set_default_values(self, obj, attr, value, *args): setattr(obj, attr, value) def parse_default_values(self, model): - model.set_names() parsed_values = {} parsed_values.setdefault("Line", {}) if self.DSS_file_names["default_values_file"]: diff --git a/ditto/readers/csv/read.py b/ditto/readers/csv/read.py index e1d13c90..633f1768 100644 --- a/ditto/readers/csv/read.py +++ b/ditto/readers/csv/read.py @@ -138,4 +138,3 @@ def parse(self, model, input_file): parent_obj, layers[-1], dataframe.iloc[row_idx][column] ) # The attribute is always the last element of layers - model.set_names() diff --git a/ditto/readers/cyme/read.py b/ditto/readers/cyme/read.py index f2d48727..edc31713 100644 --- a/ditto/readers/cyme/read.py +++ b/ditto/readers/cyme/read.py @@ -901,7 +901,6 @@ def parse(self, model, **kwargs): else: logger.info("Parsing the Headnodes...") self.parse_head_nodes(model) - model.set_names() modifier = system_structure_modifier(model) modifier.set_nominal_voltages_recur() modifier.set_nominal_voltages_recur_line() @@ -958,7 +957,6 @@ def parse_subnetwork_connections(self, model): """Parse the subnetwork connections. These specify the interconnection points for a substation """ - model.set_names() self.get_file_content("network") mapp_subnetwork_connections = {"nodeid": 1} self.subnetwork_connections = {} diff --git a/ditto/readers/opendss/read.py b/ditto/readers/opendss/read.py index d88b66dd..e3ca2486 100644 --- a/ditto/readers/opendss/read.py +++ b/ditto/readers/opendss/read.py @@ -260,7 +260,6 @@ def set_nominal_voltages(self, model): Then loop over the objects and set the kv base using the connecting element. .. warning: This has to be called last in parse. """ - model.set_names() AllBusNames = dss.Circuit.AllBusNames() for bus_name in AllBusNames: # Set the active bus @@ -272,9 +271,8 @@ def set_nominal_voltages(self, model): ) # DiTTo in volts except: print("Could not set nominal voltage for bus {b}".format(b=bus_name)) - pass - for obj in model.models: + for obj in model.iter_elements(): if hasattr(obj, "nominal_voltage") and obj.nominal_voltage is None: # If the object has a connecting_element attribute if hasattr(obj, "connecting_element"): @@ -325,12 +323,13 @@ def parse_feeder_metadata(self, model): substation_transformers[feed] = sub.lower().replace(".", "") for f_name, f_data in feeders.items(): - api_feeder_metadata = Feeder_metadata(model) + api_feeder_metadata = Feeder_metadata() api_feeder_metadata.name = f_name if f_name in substation_transformers: api_feeder_metadata.transformer = substation_transformers[f_name] if f_name in substations: api_feeder_metadata.substation = substations[f_name] + model.add_element(api_feeder_metadata.name) @timeit def parse_power_source(self, model, **kwargs): @@ -351,7 +350,7 @@ def parse_power_source(self, model, **kwargs): # Instanciate DiTTo PowerSource object try: - api_power_source = PowerSource(model) + api_power_source = PowerSource() except: continue @@ -492,7 +491,7 @@ def parse_power_source(self, model, **kwargs): ) pass - powersource_pos = Position(model) + powersource_pos = Position() powersource_pos.long = X powersource_pos.lat = Y api_power_source.positions.append(powersource_pos) @@ -505,11 +504,13 @@ def parse_power_source(self, model, **kwargs): else: api_power_source.connecting_element = source_data["bus1"] self.source_name = api_power_source.connecting_element + "_src" - api_feeder_metadata = Feeder_metadata(model) + model.add_element(api_power_source) + api_feeder_metadata = Feeder_metadata() api_feeder_metadata.name = self.source_name api_feeder_metadata.headnode = api_power_source.connecting_element api_feeder_metadata.substation = api_power_source.connecting_element api_feeder_metadata.nominal_voltage = api_power_source.nominal_voltage + model.add_element(api_feeder_metadata) except: pass @@ -729,7 +730,7 @@ def parse_nodes(self, model, **kwargs): # Loop over the dictionary of nodes and create the DiTTo Node objects for name, data in buses.items(): - api_node = Node(model) + api_node = Node() try: api_node.name = name @@ -737,7 +738,7 @@ def parse_nodes(self, model, **kwargs): pass try: - node_pos = Position(model) + node_pos = Position() node_pos.long = data["positions"][0] node_pos.lat = data["positions"][1] api_node.positions.append(node_pos) @@ -754,6 +755,7 @@ def parse_nodes(self, model, **kwargs): pass self._nodes.append(api_node) + model.add_element(api_node) return 1 @@ -801,7 +803,7 @@ def parse_lines(self, model): if not data["Switch"] and not data["enabled"]: continue - api_line = Line(model) + api_line = Line() api_line.feeder_name = self.source_name # Name @@ -1107,7 +1109,7 @@ def parse_lines(self, model): # Loop over the wires and create the Wire DiTTo objects one by one. for p in range(number_of_conductors): - wires.append(Wire(model)) + wires.append(Wire()) # Initialize the wire nameclass with the linecode name # This is just a best effort to get some information @@ -1527,6 +1529,7 @@ def parse_lines(self, model): api_line.wires = wires self._lines.append(api_line) + model.add_element(api_line) end = time.time() logger.debug("rest= {}".format(end - middle)) @@ -1551,7 +1554,7 @@ def parse_transformers(self, model): if not data["enabled"]: continue - api_transformer = PowerTransformer(model) + api_transformer = PowerTransformer() api_transformer.feeder_name = self.source_name # Name @@ -1715,7 +1718,7 @@ def parse_transformers(self, model): for w in range(N_windings): - windings.append(Winding(model)) + windings.append(Winding()) # connection type try: @@ -1769,7 +1772,7 @@ def parse_transformers(self, model): # need to use info from the bus since N_phases may not match number of connections for p in range(len(b1_phases)): - phase_windings.append(PhaseWinding(model)) + phase_windings.append(PhaseWinding()) # tap position if "taps" in data: @@ -1832,6 +1835,7 @@ def parse_transformers(self, model): for ww in windings: api_transformer.windings.append(ww) self._transformers.append(api_transformer) + model.add_element(api_transformer) return 1 @timeit @@ -1853,7 +1857,7 @@ def parse_regulators(self, model): if not data["enabled"]: continue - api_regulator = Regulator(model) + api_regulator = Regulator() api_regulator.feeder_name = self.source_name @@ -1919,7 +1923,7 @@ def parse_regulators(self, model): api_regulator.phase_shift = -30 # Initialize the list of Windings - api_regulator.windings = [Winding(model) for _ in range(N_windings)] + api_regulator.windings = [Winding() for _ in range(N_windings)] # Connection type for w in range(N_windings): @@ -1996,7 +2000,7 @@ def parse_regulators(self, model): phases = ["1", "2", "3"] api_regulator.windings[w].phase_windings = [ - PhaseWinding(model) for _ in phases + PhaseWinding() for _ in phases ] for p, phase in enumerate(phases): @@ -2155,6 +2159,7 @@ def parse_regulators(self, model): pass self._regulators.append(api_regulator) + model.add_element(api_regulator) return 1 @@ -2176,7 +2181,7 @@ def parse_capacitors(self, model): if not data["enabled"]: continue - api_capacitor = Capacitor(model) + api_capacitor = Capacitor() api_capacitor.feeder_name = self.source_name @@ -2326,7 +2331,7 @@ def parse_capacitors(self, model): # Phase capacitors phase_capacitors = [] for p, pha in enumerate(phases): - phase_capacitors.append(PhaseCapacitor(model)) + phase_capacitors.append(PhaseCapacitor()) # phase if ( @@ -2364,6 +2369,7 @@ def parse_capacitors(self, model): api_capacitor.phase_capacitors = phase_capacitors self._capacitors.append(api_capacitor) + model.add_element(api_capacitor) return 1 @@ -2385,7 +2391,7 @@ def parse_loads(self, model): if not data["enabled"]: continue - api_load = Load(model) + api_load = Load() api_load.feeder_name = self.source_name # Name @@ -2530,7 +2536,7 @@ def parse_loads(self, model): for i, p in enumerate(phases): - _phase_loads.append(PhaseLoad(model)) + _phase_loads.append(PhaseLoad()) _phase_loads[i].phase = self.phase_mapping(p) # Case one: KW and pf @@ -2600,6 +2606,7 @@ def parse_loads(self, model): api_load.phase_loads = _phase_loads self._loads.append(api_load) + model.add_element(api_load) return 1 @@ -2613,7 +2620,7 @@ def parse_storage(self, model): if not data["enabled"]: continue - api_storage = Storage(model) + api_storage = Storage() api_storage.feeder_name = self.source_name # Name @@ -2744,7 +2751,7 @@ def parse_storage(self, model): for phase in range(N_phases): try: - api_phase_storage = PhaseStorage(model) + api_phase_storage = PhaseStorage() except: pass @@ -2759,6 +2766,7 @@ def parse_storage(self, model): pass api_storage.phase_storages.append(api_phase_storage) + model.add_element(api_storage) def _dss_class_to_dict(class_name): diff --git a/ditto/store.py b/ditto/store.py index e6f53b7f..6474210e 100644 --- a/ditto/store.py +++ b/ditto/store.py @@ -23,17 +23,21 @@ import uuid import logging import types +import warnings +from collections import defaultdict, namedtuple from functools import partial from .network.network import Network from .core import DiTToBase, DiTToTypeError from .modify.modify import Modifier +from .models.base import DiTToHasTraits from .models.node import Node logger = logging.getLogger(__name__) class Store(object): + """The Store class holds all functions supported in the transformation. The Store stores all the instances of objects of different classes in a list @@ -44,80 +48,199 @@ class Store(object): >>> M = ditto.Store() >>> M - + """ - - __store_factory = dict - def __init__(self): - self._cim_store = self.__store_factory() - self._model_store = list() - self._model_names = {} + # Two-level dictionary where each elements of the same type are stored + # together. + # Names within each element type must be unique. + # {element_type: {name: element}} + self._elements = defaultdict(dict) + + self._duplicate_element_names = set() self._network = Network() def __repr__(self): - return "<%s.%s(elements=%s, models=%s) object at %s>" % ( + return "<{}.{}(elements={}) object at {}>".format( self.__class__.__module__, self.__class__.__name__, - len(self.elements), - len(self.models), + len(self._elements), hex(id(self)), ) - def __getitem__(self, k): - return self._model_names[k] + def __getitem__(self, name): + warnings.warn( + "Store[name] is deprecated. Use Store.get_element(element_type, name) instead", + DeprecationWarning + ) + element = None + for element_type, elements in self._elements.items(): + if name in elements: + if element is not None: + raise DuplicateNameError( + f"Store[name] is not supported when the name is duplicate across element types" + ) + element = elements[name] + + if element is None: + raise ElementNotFoundError + + return element def __setitem__(self, k, v): - self._model_names[k] = v + warnings.warn( + "Store[name] = element is deprecated. Use Store.add_element(element) instead", + DeprecationWarning + ) + + if v.name != k: + raise Exception(f"key={k} must be the element name") - def iter_elements(self, type=DiTToBase): + self.add_element(v) - if type == None: - type = DiTToBase + def _raise_if_not_found(self, element_type, name): + if element_type not in self._elements: + raise ElementNotFoundError(f"{element_type} is not stored") - if not issubclass(type, DiTToBase): - raise AttributeError("Unable to find {} in ditto.environment".format(type)) + if name not in self._elements[element_type]: + raise ElementNotFoundError(f"{element_type}.{name} is not stored") - for e in self.elements: - if isinstance(e, type): - yield e + def add_element(self, element): + """Add an element to the store. - def iter_models(self, type=None): + Raises + ------ + DuplicateNameError + Raised if the element name is already stored. - if type == None: - type = object + """ + if not isinstance(element, DiTToHasTraits): + raise InvalidElementType(f"type={type(element)} cannot be added") + if not hasattr(element, "name"): + raise InvalidElementType( + f"type={type(element)} cannot be added. Must define 'name' attribute." + ) - for m in self.models: - if isinstance(m, type): - yield m + element_type = type(element) + elements_by_type = self._elements[element_type] + if element.name in elements_by_type: + raise DuplicateNameError(f"{element.name} is already stored") - @property - def elements(self): - return list(self.cim_store[k] for k in self.cim_store) + elements_by_type[element.name] = element + element.build(self) + logger.debug("Added %s.%s to store", element_type, element.name) - @property - def models(self): - return tuple(m for m in self.model_store) + def clear_elements(self): + """Clear all stored elements.""" + self._elements.clear() + logger.debug("Cleared all elements") - def remove_element(self, element): - self._model_store.remove(element) + def get_element(self, element_type, name): + """Return the model. - def add_element(self, element): - if not isinstance(element, DiTToBase): - raise DiTToTypeError( - "element must be of type DiTToBase. Please check the documentation" - ) + Parameters + ---------- + element_type : class + class for the requested model, such as Load + name : str + element name + + Returns + ------- + DiTToHasTraits + + Raises + ------ + ElementNotFoundError + Raised if the element_type is not stored. + + """ + self._raise_if_not_found(element_type, name) + return self._elements[element_type][name] + + def iter_elements(self, element_type=None, filter_func=None): + """Iterate over all elements. + + Parameters + ---------- + element_type : class + If None, iterate over all elements. Otherwise, iterate over that + type. + filter_func : callable + If specified, call on element and only return elements that return + true. + + Yields + ------ + DiTToHasTraits + + Raises + ------ + ElementNotFoundError + Raised if the element_type is not stored. + + """ + if element_type is not None: + if element_type not in self._elements: + raise ElementNotFoundError(f"{element_type} is not stored") + elements_containers = [self._elements[element_type]] else: - element.link_model = self - self.cim_store[element.UUID] = element + elements_containers = self._elements.values() + + for elements in elements_containers: + for element in elements.values(): + if filter_func is not None and not filter_func(element): + logger.debug("skip %s.%s", type(element), element.name) + continue + yield element + + def list_elements(self, element_type=None, filter_func=None): + """Return a list of elements. + + Parameters + ---------- + element_type : class + If None, return all elements. Otherwise, return only that type. + filter_func : callable + If specified, call on element and only return elements that return + true. + + Returns + ------- + list + list of DiTToHasTraits + + Raises + ------ + ElementNotFoundError + Raised if the element_type is not stored. + + """ + return list(self.iter_elements(element_type=element_type, filter_func=filter_func)) + + def remove_element(self, element): + """Remove the element from the store. + + Parameters + ---------- + element : DiTToHasTraits + + Raises + ------ + ElementNotFoundError + Raised if the element is not stored. + + """ + element_type = type(element) + self._raise_if_not_found(element_type, element.name) + self._elements[element_type].pop(element.name) + logger.debug("Removed %s.%s from store", element_type, element.name) - def set_names(self): - """ All objects with a name field included in a dictionary which maps the name to the object. Set in set_name() on the object itself if the object has a name. The dictionary is reset to empty first""" - self._model_names = {} - for m in self.models: - m.set_name(self) + if not self._elements[element_type]: + self._elements.pop(element_type) + logger.debug("Removed %s from store", element_type) def build_networkx(self, source=None): if source is not None: @@ -184,7 +307,6 @@ def delete_disconnected_nodes(self): self.build_networkx() # Should be redundant since the networkx graph is only build on connected elements 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( @@ -201,18 +323,6 @@ def set_node_voltages(self): def get_internal_edges(self, nodeset): return self._network.find_internal_edges(nodeset) - @property - def cim_store(self): - return self._cim_store - - @property - def model_store(self): - return self._model_store - - @property - def model_names(self): - return self._model_names - class EnvAttributeIntercepter(object): def __init__(self, model): @@ -281,3 +391,15 @@ def set_callback(self, name, value): def del_callback(self, name, obj): pass + + +class DuplicateNameError(Exception): + """Raised when a duplicate name is detected.""" + + +class ElementNotFoundError(Exception): + """Raised when an element is not stored.""" + + +class InvalidElementType(Exception): + """Raised when an invalid type is used.""" diff --git a/ditto/writers/cyme/write.py b/ditto/writers/cyme/write.py index 172f3676..f27caa25 100644 --- a/ditto/writers/cyme/write.py +++ b/ditto/writers/cyme/write.py @@ -274,7 +274,6 @@ def write_network_file(self, model, **kwargs): This must be called before write_equipment_file since the linecodes dictionary is built here and is needed for the equipment file. """ - model.set_names() # Output network file output_file = self.output_path + "/network.txt" diff --git a/docs/metrics.md b/docs/metrics.md index 94a49e5a..c87067e3 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -326,9 +326,6 @@ network_analyst.split_network_into_feeders() #Loop over the DiTTo objects and set the feeder_name attribute network_analyst.tag_objects() -#Set the names -network_analyst.model.set_names() - #Compute the metrics per feeders network_analyst.compute_all_metrics_per_feeder() ``` diff --git a/tests/default_values/test_default_values.py b/tests/default_values/test_default_values.py index 609274b8..c9ba4865 100644 --- a/tests/default_values/test_default_values.py +++ b/tests/default_values/test_default_values.py @@ -24,7 +24,6 @@ def test_default_values(): default_values_file=os.path.join(current_directory, "test_default_values.json"), ) r.parse(m) - m.set_names() assert m["line1"].faultrate == 0.2 diff --git a/tests/default_values/test_remove_opendss_default_values.py b/tests/default_values/test_remove_opendss_default_values.py index b5c53af2..ca71817a 100644 --- a/tests/default_values/test_remove_opendss_default_values.py +++ b/tests/default_values/test_remove_opendss_default_values.py @@ -24,7 +24,6 @@ def test_remove_opendss_default_values(): remove_opendss_default_values_flag=True, ) r.parse(m) - m.set_names() assert m["line1"].faultrate == None assert m["line1"].impedance_matrix == None diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 341b1e16..6d4dd2c5 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -27,7 +27,6 @@ #Update with other tests if they get added to the persistence tests continue reader.parse(m) - m.set_names() output_path = tempfile.TemporaryDirectory() w = Writer(output_path=output_path.name, log_path=output_path) w.write(m) diff --git a/tests/persistence/update_json.py b/tests/persistence/update_json.py index 6d8cf8e6..11d16753 100644 --- a/tests/persistence/update_json.py +++ b/tests/persistence/update_json.py @@ -18,7 +18,6 @@ def update_persistence_jsons(): #Update with other tests if they get added to the persistence tests continue reader.parse(m) - m.set_names() print("Writing "+dirpath) w = Writer(output_path=dirpath, log_path=dirpath) w.write(m) diff --git a/tests/readers/opendss/Capacitors/test_capacitor_connectivity.py b/tests/readers/opendss/Capacitors/test_capacitor_connectivity.py index 2db54759..30492698 100644 --- a/tests/readers/opendss/Capacitors/test_capacitor_connectivity.py +++ b/tests/readers/opendss/Capacitors/test_capacitor_connectivity.py @@ -25,7 +25,6 @@ def test_capacitor_connectivity(): master_file=os.path.join(current_directory, "test_capacitor_connectivity.dss") ) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/readers/opendss/Capacitors/test_capacitor_kvar.py b/tests/readers/opendss/Capacitors/test_capacitor_kvar.py index 8b26f7b9..92a3f4b5 100644 --- a/tests/readers/opendss/Capacitors/test_capacitor_kvar.py +++ b/tests/readers/opendss/Capacitors/test_capacitor_kvar.py @@ -22,7 +22,6 @@ def test_capacitor_kvar(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_capacitor_kvar.dss")) r.parse(m) - m.set_names() assert len(m["cap1"].phase_capacitors) == 3 # Cap1 is a three phase capacitor assert sum( diff --git a/tests/readers/opendss/Lines/test_fuses.py b/tests/readers/opendss/Lines/test_fuses.py index 6acc4adb..2cb7c3f2 100644 --- a/tests/readers/opendss/Lines/test_fuses.py +++ b/tests/readers/opendss/Lines/test_fuses.py @@ -30,7 +30,6 @@ def test_fuses(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_fuses.dss")) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/readers/opendss/Lines/test_line_connectivity.py b/tests/readers/opendss/Lines/test_line_connectivity.py index 56e3d35b..90b28138 100644 --- a/tests/readers/opendss/Lines/test_line_connectivity.py +++ b/tests/readers/opendss/Lines/test_line_connectivity.py @@ -31,7 +31,6 @@ def test_line_connectivity(): master_file=os.path.join(current_directory, "test_line_connectivity.dss") ) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/readers/opendss/Lines/test_line_length.py b/tests/readers/opendss/Lines/test_line_length.py index ac68adb6..2188cb66 100644 --- a/tests/readers/opendss/Lines/test_line_length.py +++ b/tests/readers/opendss/Lines/test_line_length.py @@ -29,7 +29,6 @@ def test_line_length(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_line_length.dss")) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/readers/opendss/Lines/test_linecodes.py b/tests/readers/opendss/Lines/test_linecodes.py index 9dfff9d5..628966d9 100644 --- a/tests/readers/opendss/Lines/test_linecodes.py +++ b/tests/readers/opendss/Lines/test_linecodes.py @@ -30,7 +30,6 @@ def test_linecodes(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_linecodes.dss")) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/readers/opendss/Lines/test_linegeometries.py b/tests/readers/opendss/Lines/test_linegeometries.py index bfc9af3b..f098814f 100644 --- a/tests/readers/opendss/Lines/test_linegeometries.py +++ b/tests/readers/opendss/Lines/test_linegeometries.py @@ -23,7 +23,6 @@ def test_linegeometries(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_linegeometries.dss")) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/readers/opendss/Lines/test_switches.py b/tests/readers/opendss/Lines/test_switches.py index 747f8f69..efca62fa 100644 --- a/tests/readers/opendss/Lines/test_switches.py +++ b/tests/readers/opendss/Lines/test_switches.py @@ -28,7 +28,6 @@ def test_switches(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_switches.dss")) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/readers/opendss/Loads/test_load_p_and_q.py b/tests/readers/opendss/Loads/test_load_p_and_q.py index a53f4309..da34f33b 100644 --- a/tests/readers/opendss/Loads/test_load_p_and_q.py +++ b/tests/readers/opendss/Loads/test_load_p_and_q.py @@ -22,7 +22,6 @@ def test_load_p_and_q(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_load_p_and_q.dss")) r.parse(m) - m.set_names() # P and Q values should be equally divided accross phase loads # Here we sum P and Q and check that the obtained values match the values in the DSS file diff --git a/tests/readers/opendss/Loads/test_loads.py b/tests/readers/opendss/Loads/test_loads.py index f032d869..cf34c3ee 100644 --- a/tests/readers/opendss/Loads/test_loads.py +++ b/tests/readers/opendss/Loads/test_loads.py @@ -23,7 +23,6 @@ def test_loads(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_loads.dss")) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/readers/opendss/Nodes/test_nodes.py b/tests/readers/opendss/Nodes/test_nodes.py index 055b5cc9..5bcffee2 100644 --- a/tests/readers/opendss/Nodes/test_nodes.py +++ b/tests/readers/opendss/Nodes/test_nodes.py @@ -30,7 +30,6 @@ def test_nodes(): buscoordinates_file=os.path.join(current_directory, "buscoord.dss"), ) r.parse(m) - m.set_names() assert (m["bus1"].name) == "bus1" assert (m["bus1"].nominal_voltage) == float(12.47) * 10 ** 3 diff --git a/tests/readers/opendss/Powersource/test_powersource.py b/tests/readers/opendss/Powersource/test_powersource.py index ffcf5c98..e9bc5092 100644 --- a/tests/readers/opendss/Powersource/test_powersource.py +++ b/tests/readers/opendss/Powersource/test_powersource.py @@ -12,6 +12,7 @@ import pytest import numpy as np +from ditto.models.power_source import PowerSource from ditto.store import Store from ditto.readers.opendss.read import Reader @@ -25,21 +26,21 @@ def test_powersource(): buscoordinates_file=os.path.join(current_directory, "buscoord.dss"), ) r.parse(m) - m.set_names() - assert m["Vsource.source"].name == "Vsource.source" - assert m["Vsource.source"].nominal_voltage == 230.0 * 10 ** 3 - assert m["Vsource.source"].per_unit == 0.99 - assert m["Vsource.source"].is_sourcebus == 1 - assert m["Vsource.source"].rated_power == 150000000.0 + element = m.get_element(PowerSource, "Vsource.source") + assert element.name == "Vsource.source" + assert element.nominal_voltage == 230.0 * 10 ** 3 + assert element.per_unit == 0.99 + assert element.is_sourcebus == 1 + assert element.rated_power == 150000000.0 # MVASc3 = baseKVA^2 / Z1 ; Z1 = sqrt( r1^2 + x1^2) emerg_power = int((230.0) ** 2 / math.sqrt(1.1208 ** 2 + 3.5169 ** 2)) * 10 ** 6 - assert m["Vsource.source"].emergency_power == emerg_power - assert m["Vsource.source"].zero_sequence_impedance == 1.1208 + 3.5169j - assert m["Vsource.source"].positive_sequence_impedance == 1.1208 + 3.5169j - assert m["Vsource.source"].connecting_element == "sourcebus" - assert m["Vsource.source"].phases[0].default_value == "A" - assert m["Vsource.source"].phases[1].default_value == "B" - assert m["Vsource.source"].phases[2].default_value == "C" - assert (m["Vsource.source"].positions[0].long) == float(200) - assert (m["Vsource.source"].positions[0].lat) == float(400) + assert element.emergency_power == emerg_power + assert element.zero_sequence_impedance == 1.1208 + 3.5169j + assert element.positive_sequence_impedance == 1.1208 + 3.5169j + assert element.connecting_element == "sourcebus" + assert element.phases[0].default_value == "A" + assert element.phases[1].default_value == "B" + assert element.phases[2].default_value == "C" + assert element.positions[0].long == 200.0 + assert element.positions[0].lat == 400.0 diff --git a/tests/readers/opendss/Regulators/test_regulators.py b/tests/readers/opendss/Regulators/test_regulators.py index a0f68480..0aca1c37 100644 --- a/tests/readers/opendss/Regulators/test_regulators.py +++ b/tests/readers/opendss/Regulators/test_regulators.py @@ -23,7 +23,6 @@ def test_regulators(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_regulators.dss")) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/readers/opendss/Transformers/test_transformer_kv.py b/tests/readers/opendss/Transformers/test_transformer_kv.py index 12b7d560..9d3257dd 100644 --- a/tests/readers/opendss/Transformers/test_transformer_kv.py +++ b/tests/readers/opendss/Transformers/test_transformer_kv.py @@ -23,7 +23,6 @@ def test_transformer_kv(): m = Store() r = Reader(master_file=os.path.join(current_directory, "test_transformer_kv.dss")) r.parse(m) - m.set_names() # Reading OpenDSS default values d_v = Default_Values( diff --git a/tests/test_demo_to_gridlabd.py b/tests/test_demo_to_gridlabd.py index 121bf620..5e0a4a64 100644 --- a/tests/test_demo_to_gridlabd.py +++ b/tests/test_demo_to_gridlabd.py @@ -26,7 +26,6 @@ def test_demo_to_gridlabd(): m = Store() r = Reader(input_file=os.path.join(current_directory,'data/small_cases/demo',model)) r.parse(m) - m.set_names() print('>Demo model {model} read...'.format(model=os.path.join(current_directory,'data/small_cases/demo',model))) output_path = tempfile.TemporaryDirectory() w = Writer(output_path=output_path.name, log_path=output_path) diff --git a/tests/test_gridlabd_to_ephasor.py b/tests/test_gridlabd_to_ephasor.py index b9b55f90..f75a01f3 100644 --- a/tests/test_gridlabd_to_ephasor.py +++ b/tests/test_gridlabd_to_ephasor.py @@ -28,7 +28,6 @@ def test_gridlabd_to_ephasor(): m = Store() r = Reader() r.parse(m) - m.set_names() #TODO: Log properly print('>Gridlab-D model {model} read...'.format(model=model)) output_path = tempfile.TemporaryDirectory() diff --git a/tests/test_json_parsers.py b/tests/test_json_parsers.py index 1dd34e26..76079aaf 100644 --- a/tests/test_json_parsers.py +++ b/tests/test_json_parsers.py @@ -45,7 +45,6 @@ def test_opendss_to_json(): ), ) r.parse(m) - m.set_names() output_path = tempfile.TemporaryDirectory() w = Writer(output_path=output_path.name) w.write(m) @@ -70,7 +69,6 @@ def test_cyme_to_json(): ) ) r.parse(m) - m.set_names() output_path = tempfile.TemporaryDirectory() w = Writer(output_path=output_path.name) w.write(m) @@ -110,12 +108,10 @@ def test_json_serialize_deserialize(): ), ) r.parse(m) - m.set_names() w = Writer(output_path="./") w.write(m) jr = json_reader(input_file="./Model.json") jr.parse(m) - jr.model.set_names() for obj in m.models: if hasattr(obj, "name"): diff --git a/tests/test_metric_extraction.py b/tests/test_metric_extraction.py index 340ef767..1be7d393 100644 --- a/tests/test_metric_extraction.py +++ b/tests/test_metric_extraction.py @@ -53,7 +53,6 @@ def test_metric_extraction(): ), ) r.parse(m) - m.set_names() # Create a modifier object modifier = system_structure_modifier(m) @@ -64,7 +63,6 @@ def test_metric_extraction(): # Create a Network analyszer object with the modified model net = network_analyzer(modifier.model, True, "sourcebus") - net.model.set_names() # Compute all the available metrics net.compute_all_metrics() diff --git a/tests/test_opendss_to_cyme.py b/tests/test_opendss_to_cyme.py index fe2314f8..0a3c39f9 100644 --- a/tests/test_opendss_to_cyme.py +++ b/tests/test_opendss_to_cyme.py @@ -35,7 +35,6 @@ def test_opendss_to_cyme(): buscoordinates_file=os.path.join(current_directory, 'data/small_cases/opendss/{model}/buscoord.dss'.format(model=model)) ) r.parse(m) - m.set_names() #TODO: Log properly print('>OpenDSS model {model} read...'.format(model=model)) output_path = tempfile.TemporaryDirectory() diff --git a/tests/test_opendss_to_ephasor.py b/tests/test_opendss_to_ephasor.py index a7bfaeb1..0b217ad0 100644 --- a/tests/test_opendss_to_ephasor.py +++ b/tests/test_opendss_to_ephasor.py @@ -35,7 +35,6 @@ def test_opendss_to_ephasor(): buscoordinates_file=os.path.join(current_directory, 'data/small_cases/opendss/{model}/buscoord.dss'.format(model=model)) ) r.parse(m) - m.set_names() m.build_networkx() m.direct_from_source() m.set_node_voltages() diff --git a/tests/test_opendss_to_gridlabd.py b/tests/test_opendss_to_gridlabd.py index e74ff8b1..6eadcddf 100644 --- a/tests/test_opendss_to_gridlabd.py +++ b/tests/test_opendss_to_gridlabd.py @@ -34,7 +34,6 @@ def test_opendss_to_gridlabd(): buscoordinates_file=os.path.join(current_directory, 'data/small_cases/opendss/{model}/buscoord.dss'.format(model=model)) ) r.parse(m) - m.set_names() #TODO: Log properly print('>OpenDSS model {model} red...'.format(model=model)) t = tempfile.TemporaryDirectory() diff --git a/tests/test_opendss_transformer.py b/tests/test_opendss_transformer.py index 368dc8df..29db35c1 100644 --- a/tests/test_opendss_transformer.py +++ b/tests/test_opendss_transformer.py @@ -37,7 +37,6 @@ def test_opendss_center_transformer(): m = Store() r = Reader(master_file=master_file) r.parse(m) - m.set_names() for t in m.iter_models(type=PowerTransformer): assert len(t.windings) == 3 @@ -84,7 +83,6 @@ def test_opendss_transformer(): m = Store() r = Reader(master_file=master_file,) r.parse(m) - m.set_names() for t in m.iter_models(type=PowerTransformer): assert len(t.windings) == 2 diff --git a/tests/test_store.py b/tests/test_store.py new file mode 100644 index 00000000..480b3261 --- /dev/null +++ b/tests/test_store.py @@ -0,0 +1,64 @@ + +import os + +import pytest + +from ditto.models.line import Line +from ditto.models.position import Position +from ditto.readers.opendss.read import Reader +from ditto.store import Store, DuplicateNameError, ElementNotFoundError, \ + InvalidElementType + + +MASTER = "tests/data/small_cases/opendss/ieee_4node/master.dss" +BUS_COORDS = "tests/data/small_cases/opendss/ieee_13node/buscoord.dss" + + +def test_store(): + m = Store() + r = Reader(master_file=MASTER, buscoordinates_file=BUS_COORDS) + r.parse(m) + + # Test list/iteration variations. + lines = m.list_elements(Line) + assert len(lines) == 2 + assert isinstance(lines[0], Line) + line = lines[0] + name = line.name + + found = False + for ln in m.iter_elements(Line, lambda x: x.name == name): + assert ln.name == name + + elements = m.list_elements() + # There should be more than just lines. + assert len(elements) > 2 + + # Test forms of get. + assert m.get_element(Line, name) is line + assert m[name] is line + + with pytest.raises(DuplicateNameError): + m.add_element(line) + + with pytest.raises(InvalidElementType): + m.add_element("invalid") + + # Position doesn't have a name. + with pytest.raises(InvalidElementType): + m.add_element(Position()) + + m.remove_element(line) + with pytest.raises(ElementNotFoundError): + m.get_element(Line, name) + + # Test forms of set. + m.add_element(line) + assert m.get_element(Line, name) is line + m.remove_element(line) + m[name] = line + assert m.get_element(Line, name) is line + + # Test clear. + m.clear_elements() + assert not m.list_elements() diff --git a/tests/test_writer.py b/tests/test_writer.py index 46c36d2b..11251f8c 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -92,7 +92,6 @@ def test_cyme_writer(): transformer1.reactances.append(6) # reg1 = Regulator(m, name='t1_reg', connected_transformer='t1', connected_winding=2, pt_ratio=60, delay=2) # cap1 = Capacitor(m, name='cap1', connecting_element='n2', num_phases=3, nominal_voltage=7.2, var=300, connection_type='Y') - m.set_names() t = tempfile.TemporaryDirectory() writer = Writer(output_path=t.name, log_path="./") writer.write(m) @@ -270,7 +269,6 @@ def test_gridlabd_writer(): transformer1.reactances.append(6) # reg1 = Regulator(m, name='t1_reg', connected_transformer='t1', connected_winding=2, pt_ratio=60, delay=2) # cap1 = Capacitor(m, name='cap1', connecting_element='n2', num_phases=3, nominal_voltage=7.2, var=300, connection_type='Y') - m.set_names() t = tempfile.TemporaryDirectory() writer = Writer(output_path=t.name, log_path="./") writer.write(m) @@ -348,7 +346,6 @@ def test_ephasor_writer(): transformer1.reactances.append(6) # reg1 = Regulator(m, name='t1_reg', connected_transformer='t1', connected_winding=2, pt_ratio=60, delay=2) # cap1 = Capacitor(m, name='cap1', connecting_element='n2', num_phases=3, nominal_voltage=7.2, var=300, connection_type='Y') - m.set_names() t = tempfile.TemporaryDirectory() writer = Writer(output_path=t.name, log_path="./") writer.write(m) diff --git a/tests/writers/opendss/Lines/test_lines_write.py b/tests/writers/opendss/Lines/test_lines_write.py index 2d41641d..71d707c8 100644 --- a/tests/writers/opendss/Lines/test_lines_write.py +++ b/tests/writers/opendss/Lines/test_lines_write.py @@ -31,7 +31,6 @@ def test_lines_write(): ) ) r.parse(m) - m.set_names() output_path = tempfile.gettempdir() jw = Json_Writer(output_path=output_path) @@ -45,7 +44,6 @@ def test_lines_write(): r_w = Reader(master_file=os.path.join(output_path, "Master.dss")) r_w.parse(m) - m.set_names() jw = Json_Writer(output_path="./") jw.write(m)