def apply(cls, stack, model, *args, **kwargs):
        model.set_names()

        if "feeder_name" in kwargs:
            feeder_name = kwargs["feeder_name"]
        else:
            feeder_name = "sce_feeder"

        if "output_filename_xlsx" in kwargs:
            output_filename_xlsx = kwargs["output_filename_xlsx"]
        else:
            output_filename_xlsx = '{}.xlsx'.format(feeder_name)

        if "output_filename_json" in kwargs:
            output_filename_json = kwargs["output_filename_json"]
        else:
            output_filename_json = '{}.json'.format(feeder_name)

        #Create the modifier object
        modifier = system_structure_modifier(model)

        #Set the nominal voltages
        modifier.set_nominal_voltages_recur()
        modifier.set_nominal_voltages_recur_line()

        #Create the network analyzer
        network_analyst = network_analyzer(modifier.model)
        network_analyst.model.set_names()
        network_analyst.compute_all_metrics(feeder_name)

        #Export the metrics
        network_analyst.export(output_filename_xlsx)
        network_analyst.export_json(output_filename_json)

        return model
    def apply(cls, stack, model, *args, **kwargs):

        #Create the modifier object
        modifier = system_structure_modifier(model, 'st_mat')

        # Set missing coordinates
        modifier.set_missing_coords_recur()

        return model
def test_metric_extraction():
    """
        This test reads all small OpenDSS test cases, set the nominal voltages using a
        system_structure_modifier object and compute all metrics using a network analyzer object.
        Finally, it exports the metrics to excel and Json formats.
    """
    from ditto.readers.opendss.read import Reader
    from ditto.store import Store
    from ditto.modify.system_structure import system_structure_modifier
    from ditto.metrics.network_analysis import NetworkAnalyzer as network_analyzer

    opendss_models = [
        f for f in os.listdir(
            os.path.join(current_directory, "data/small_cases/opendss/"))
        if not f.startswith(".")
    ]
    opendss_models.remove("storage_test")

    for model in opendss_models:
        m = Store()
        r = Reader(
            master_file=os.path.join(
                current_directory,
                "data/small_cases/opendss/{model}/master.dss".format(
                    model=model),
            ),
            buscoordinates_file=os.path.join(
                current_directory,
                "data/small_cases/opendss/{model}/buscoord.dss".format(
                    model=model),
            ),
        )
        r.parse(m)
        m.set_names()

        # Create a modifier object
        modifier = system_structure_modifier(m)

        # And set the nominal voltages of the elements since we don't have it from OpenDSS
        modifier.set_nominal_voltages_recur()
        modifier.set_nominal_voltages_recur_line()

        # 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()

        output_path = tempfile.gettempdir()

        # Export them to excel
        net.export(os.path.join(output_path, "metrics.xlsx"))

        # Export them to JSON
        net.export_json(os.path.join(output_path, "metrics.json"))
    def apply(cls, stack, model, *args, **kwargs):
        path_to_feeder_file = None
        path_to_switching_devices_file = None
        center_tap_postprocess = False
        switch_to_recloser = False
        if 'path_to_feeder_file' in kwargs:
            path_to_feeder_file = kwargs['path_to_feeder_file']

        if 'path_to_switching_devices_file' in kwargs:
            path_to_switching_devices_file = kwargs[
                'path_to_switching_devices_file']

        if 'switch_to_recloser' in kwargs:
            switch_to_recloser = kwargs['switch_to_recloser']

        if 'center_tap_postprocess' in kwargs:
            center_tap_postprocess = kwargs['center_tap_postprocess']

        #Make sure the voltage of st_mat is set
        model['st_mat'].nominal_voltage = 230000.0

        #Create the modifier object
        modifier = system_structure_modifier(model, 'st_mat')

        #Center-tap loads
        if center_tap_postprocess:
            modifier.center_tap_load_preprocessing()

        #Set node nominal voltages
        modifier.set_nominal_voltages_recur()

        #Set line nominal voltages
        modifier.set_nominal_voltages_recur_line()

        #Create Feeder_metadata objects for each feeder

        #Open and read feeder.txt
        with open(path_to_feeder_file, 'r') as f:
            lines = f.readlines()

        all_feeder_data = set()
        for line in lines[1:]:
            #Parse the line
            node, sub, feed, sub_trans = map(lambda x: x.strip().lower(),
                                             line.split(' '))
            sub_trans = sub_trans.replace('ctrafo', 'tr')
            all_feeder_data.add((feed, sub, sub_trans))

        for feeder in all_feeder_data:
            modifier.set_feeder_metadata(feeder[0], feeder[1], feeder[2])

        for i in modifier.model.models:
            if isinstance(i, PowerSource) and hasattr(
                    i, 'is_sourcebus') and i.is_sourcebus == 1:
                i.positive_sequence_impedance = complex(1.1208, 3.5169)
                i.zero_sequence_impedance = complex(1.1208, 3.5169)

        #Set headnodes for each feeder
        modifier.set_feeder_headnodes()

        #Replace all switchin devices ampacity 3000.0 default values with nans
        #such that we can call the modifier method on it
        for obj in modifier.model.models:
            if isinstance(obj, Line):
                if obj.is_switch == 1 or obj.is_breaker == 1 or obj.is_sectionalizer == 1 or obj.is_fuse == 1 or obj.is_recloser == 1:
                    for w in obj.wires:
                        if w.ampacity == 3000.0:
                            w.ampacity = np.nan

        #Do the actual mdifications
        modifier.set_switching_devices_ampacity()

        #Open the switches which need to be opened.
        modifier.open_close_switches(path_to_switching_devices_file)

        tmp_net = Network()
        tmp_net.build(modifier.model, 'st_mat')
        tmp_net.set_attributes(modifier.model)
        tmp_net.remove_open_switches(modifier.model)
        print(len(nx.cycle_basis(tmp_net.graph)))
        print(nx.cycle_basis(tmp_net.graph))

        #Create the sub-transmission Feeder_metadata
        if not 'subtransmission' in modifier.model.model_names.keys():
            api_feeder_metadata = Feeder_metadata(modifier.model)
            api_feeder_metadata.name = 'subtransmission'
            api_feeder_metadata.substation = modifier.source
            api_feeder_metadata.headnode = modifier.source
            api_feeder_metadata.transformer = modifier.source

        #switch to recloser post-processing


#        if switch_to_recloser:
#            modifier.replace_first_switch_with_recloser()

#Return the modified model
        return modifier.model
    def apply(cls,
              stack,
              model,
              feeder_file,
              output_substation_folder,
              base_dir=None,
              readme_file=None,
              substation_folder=None):
        # Format is max number of feeders, list of substation numbers
        subs_4kv = [(4, [5]), (8, [13]), (12, [14])]
        subs_25kv = [(6, [11]), (12, [15])]
        subs_1247kv = [(1, [1]), (2, [8]), (3, [2]), (4, [4, 10]), (6, [9]),
                       (8, [3, 7]), (12, [6]), (16, [12])]
        sub_list = None
        logger.debug("Starting add_cyme_substations")
        if base_dir and (not os.path.exists(feeder_file)):
            feeder_file = os.path.join(base_dir, feeder_file)

        if substation_folder == None:
            substation_folder = os.path.join(os.path.dirname(__file__),
                                             'resources')
        elif base_dir and (not os.path.exists(output_substation_folder)):
            output_substation_folder = os.path.join(base_dir,
                                                    output_substation_folder)

        transformer_config = 'Y'  #Default is Delta-Wye. If specified we set it to be wye-wye
        if readme_file is not None and os.path.exists(readme_file):
            f = open(readme_file, 'r')
            info = f.readline().split()
            suffix = info[1][:-2]
            config = info[5]
            if config == 'delta-delta':
                transformer_config = 'D'
            kv = int(float(suffix) * 1000)
            if suffix == '12.47':
                suffix = '1247'
            suffix = '_' + suffix
            if kv == 4000:
                sub_list = subs_4kv
            elif kv == 25000:
                sub_list = subs_25kv
            else:
                sub_list = subs_1247kv
        else:
            kv = 12470
            suffix = '_1247'
            sub_list = subs_1247kv

        from_cyme_layer_dir = None
        # first look in stack
        for layer in stack:
            if layer.name == 'From CYME':
                from_cyme_layer_dir = layer.layer_dir
                break
        # then try this layer's library directory
        if from_cyme_layer_dir is None:
            from_cyme_layer_dir = os.path.join(
                os.path.dirname(os.path.dirname(__file__)), 'from_cyme')
        if not os.path.exists(from_cyme_layer_dir):
            msg = "Cannot find the 'From CYME' layer."
            logger.error(msg)
            raise Exception(msg)

        logger.debug("Building the model network")

        srcs = []
        for obj in model.models:
            if isinstance(obj, PowerSource) and obj.is_sourcebus == 1:
                srcs.append(obj.name)
        srcs = np.unique(srcs)
        if len(srcs) == 0:
            raise ValueError('No PowerSource object found in the model.')
        elif len(srcs) > 1:
            raise ValueError(
                'Mupltiple sourcebus found: {srcs}'.format(srcs=srcs))
        else:
            source = srcs[0]
        logger.debug("Identifying the Source Bus as {src}".format(src=source))
        ''' 
           Substation nodes have the form ***_1247. (or _4 or _25 for 4kv and 25kv systems) 
           The boundary of the substation is ***_69 on the high side.
           The boundary of the substation in a node with no x on the low side
           A feeder defines these boundaries.
           e.g. IHS0_1247->IDT706 is a substation of IHS0_1247 with a boundary of IDT706
           We remove everything between the high and low boundaries when updating a substation
        '''

        model.build_networkx(
            source)  # Used to remove internal edges in substation
        df = pd.read_csv(
            feeder_file, ' '
        )  #The RNM file specifying which feeders belong to which substation
        substation_boundaries_low = {}
        substation_boundaries_high = {}
        substation_transformers = {}
        substation_names = {}
        for index, row in df.iterrows():
            substation = row.iloc[1].lower()
            feeder = row.iloc[2].lower()
            transformer = row.iloc[3].lower()
            transformer = 'tr' + transformer[
                6:]  #The prefix is slightly different with the feeder.txt file
            buses = feeder.split('->')
            bus2 = buses[1]
            bus1 = buses[0]
            if substation in substation_boundaries_low:
                substation_boundaries_low[substation].add(
                    bus2 + '-' + bus1 + 'x'
                )  #Set the first node to be the location with one x sign as sometimes there isn't one with xx
                substation_names[bus2 + '-' + bus1 + 'x'] = substation
            else:
                substation_boundaries_low[substation] = set(
                    [bus2 + '-' + bus1 + 'x'])
                substation_names[bus2 + '-' + bus1 + 'x'] = substation

            if substation not in substation_boundaries_high:
                substation_boundaries_high[substation] = set([
                    substation.replace(suffix, '_69')
                ])  # All substations are from 69kV

            if substation not in substation_transformers:
                substation_transformers[substation] = set([transformer])

        print(substation_names)
        for sub in substation_boundaries_low.keys(
        ):  #sub is the name of the substation and substation_boundaries_low[sub] is a set of all the connected feeders
            logger.debug("Building to_delete and modifier")
            to_delete = Store()
            modifier = Modifier()
            logger.info(
                "Processing substation {}. There are {} in total.".format(
                    sub, len(substation_boundaries_low)))

            all_boundaries = substation_boundaries_low[sub].union(
                substation_boundaries_high[sub])

            # TODO: do a search from the high boundary to the low boundary and include everything inside
            #get_internal_nodes(substation_boundaries_high[sub],substation_boundaries_low[sub])

            feeder_names = list(
                substation_boundaries_low[sub]
            )  # Feeder point of connection to the substation
            internal_nodes = [
                i for i in model.model_names
                if sub in i and isinstance(model[i], Node)
            ]
            internal_edges = [
                i for i in model.model_names
                if sub in i and (isinstance(model[i], Line)
                                 or isinstance(model[i], PowerTransformer))
            ]
            tmp_include_edges = []
            for i in internal_edges:
                if model[i].from_element in feeder_names:
                    internal_nodes.append(model[i].from_element)
                if model[i].to_element in feeder_names:
                    internal_nodes.append(model[i].to_element)
                if 'mv' in model[i].to_element and 'mv' in model[
                        i].from_element:
                    tmp_include_edges.append(
                        i
                    )  #Used to address MV loads attached directly to the substation
            for i in tmp_include_edges:
                internal_edges.remove(i)

            high_boundary = list(substation_boundaries_high[sub])[
                0]  #Should only be one high side boundary point in the set
            internal_nodes.append(high_boundary)
            """
            #import pdb;pdb.set_trace()
            all_nodes.append(sub+'-'+high_boundary+'xxx')
            all_nodes.append(sub+'-'+high_boundary+'xx')
            all_nodes.append(sub+'-'+high_boundary+'x')
            all_nodes.append(sub)
            for low_val in substation_boundaries_low[sub]:
                all_nodes.append(low_val)  # End at these nodes. No reclosers after these.
                last_node = low_val+'-'+sub+'x'
                last_node_rev = sub+'-'+low_val+'x'
                if last_node in model.model_names:
                    all_nodes.append(last_node) #The connection point
                if last_node_rev in model.model_names:
                    all_nodes.append(last_node_rev) #The connection point if the name is backwards. Dypically with UMV nodes
                if last_node+'x' in model.model_names:
                    all_nodes.append(last_node+'x') #This extra node often exists 
                if last_node+'_p' in model.model_names:
                    all_nodes.append(last_node+'_p') #This extra node sometimes exists 
                if last_node+'xx' in model.model_names:
                    all_nodes.append(last_node+'xx') #This extra node sometimes exists too
            """
            all_nodes_set = set(internal_nodes)
            #internal_edges = model.get_internal_edges(all_nodes_set)
            #import pdb;pdb.set_trace()

            # These attribuetes will be used to inform which substation is selected
            rated_power = None
            emergency_power = None
            loadloss = None
            noloadloss = None
            reactance = None
            lat = None
            long = None
            transformer = list(substation_transformers[sub])[
                0]  # Assume only one transformer per substation in RNM

            # Currently using CYME reader which has different names for the transformers to the feeders.txt file
            #            reactance = model[transformer].reactances[0] # Assume the same for all windings in a substation
            #            loadloss = model[transformer].loadloss
            #            noloadloss = model[transformer].noload_loss
            #            rated_power = model[transformer].windings[0].rated_power # Assume the same for all windings in a substation
            #            emergency_power = model[transformer].windings[0].emergency_power # Assume the same for all windings in a substation
            try:
                lat = model[sub].positions[0].lat
                long = model[sub].positions[0].long
            except:
                raise ValueError('{} missing position elements'.format(
                    model[sub].name))

        # import pdb;pdb.set_trace()
        # Mark the internals of the substation for deletion
            for n in all_nodes_set:
                if not n in model.model_names:
                    continue
#                is_endpoint = False
#                for key in substation_boundaries_low:
#                    if n in substation_boundaries_low[key]: #Don't delete the boundaries of the substation
#                        is_endpoint = True
#                        break
#                if is_endpoint:
#                    continue
                obj_name = type(model[n]).__name__
                base_obj = globals()[obj_name](to_delete)
                base_obj.name = n
            for e in internal_edges:
                if not e in model.model_names:
                    continue
                if model[
                        e].from_element in feeder_names:  #Don't remove edge from bus2-bus1-x to the first distribution transformer
                    continue
                obj_name = type(model[e]).__name__
                base_obj = globals()[obj_name](to_delete)
                base_obj.name = e

            num_model_feeders = len(feeder_names)
            not_allocated = True

            low_voltage = kv
            high_voltage = 69000

            #import pdb;pdb.set_trace()
            # Read the CYME models
            random.seed(0)
            for element in sub_list:  # Insert extra logic here to determine which one to use
                if num_model_feeders > element[0]:
                    continue

                sub_file = 'sb' + str(element[1][random.randint(
                    0,
                    len(element[1]) - 1)])
                print(sub_file)
                sub_model = Store()
                reader = CymeReader(data_folder_path=os.path.join(
                    os.path.dirname(__file__), 'resources', sub_file))
                reader.parse(sub_model)
                sub_model.set_names()

                # Set the nominal voltages within the substation using the cyme model transformer and source voltage
                modifier = system_structure_modifier(sub_model)
                modifier.set_nominal_voltages_recur()

                # Determine which nodes in the CYME model connect feeders/HV
                all_substation_connections = []
                available_feeders = 0
                for i in sub_model.models:
                    if isinstance(i, Node) and hasattr(
                            i, 'is_substation_connection'
                    ) and i.is_substation_connection == 1:
                        all_substation_connections.append(i)
                        i.setpoint = 1.03
                        try:
                            if i.nominal_voltage == low_voltage:
                                available_feeders += 1
                        except:
                            raise ValueError(
                                "Nominal Voltages not set correctly in substation"
                            )

                logger.info(
                    "Processing substation {}. There are {} feeders available and {} feeders in RNM"
                    .format(sub, available_feeders, num_model_feeders))
                if available_feeders >= num_model_feeders:
                    boundry_map = {}
                    feeder_cnt = 0
                    ref_lat = 0
                    ref_long = 0
                    for i in sub_model.models:
                        # Remove the source node. This is no longer needed
                        if isinstance(i, PowerSource):
                            sub_model.remove_element(i)

                        # Set a random node as the reference node
                        if isinstance(i, Node) and hasattr(
                                i, 'positions'
                        ) and len(i.positions) > 0 and hasattr(
                                i.positions[0], 'lat'
                        ) and i.positions[0].lat is not None and hasattr(
                                i.positions[0],
                                'long') and i.positions[0].long is not None:
                            ref_lat = i.positions[0].lat
                            ref_long = i.positions[0].long

                    substation_name = substation_names[feeder_names[0].lower()]
                    for i in sub_model.models:

                        # Remove feeder name from substation elements. This is normally set automatically when reading from CYME
                        if hasattr(i, 'feeder_name'):
                            i.feeder_name = ''
                        if hasattr(i, 'substation_name'):
                            i.substation_name = substation_name
                        if hasattr(i, 'is_substation'):
                            i.is_substation = True

                        if isinstance(
                                i, PowerTransformer
                        ) and transformer_config == 'D' and i.windings is not None and len(
                                i.windings) == 2 and i.windings[1] is not None:
                            i.windings[1].connection_type = 'D'

                        # Assign feeder names to the endpoints of the substation
                        if isinstance(i, Node) and hasattr(
                                i, 'is_substation_connection'
                        ) and i.is_substation_connection == 1:
                            if hasattr(
                                    i, 'nominal_voltage'
                            ) and i.nominal_voltage is not None and i.nominal_voltage >= high_voltage:

                                #########  USE no_feeders.txt to inform how many connection points there should be. Skip this substation if the number isn't correct-->>>>
                                #### Need to discuss with Carlos
                                boundry_map[i.name] = high_boundary
                                i.setpoint = 1.0
                                i.name = high_boundary  #TODO: issue of multiple high voltage inputs needs to be addressed
                                #i.feeder_name = 'subtransmission'
                                i.substation_name = substation_name
                                i.is_substation = False
                            elif hasattr(
                                    i, 'nominal_voltage'
                            ) and i.nominal_voltage is not None and i.nominal_voltage < high_voltage:
                                feeder_cnt += 1
                                if feeder_cnt <= num_model_feeders:
                                    endpoint = feeder_names[feeder_cnt -
                                                            1].split('-')[0]
                                    if 'mv' in endpoint:  #The node names for these are reversed for some reason
                                        boundry_map[
                                            i.
                                            name] = substation_name + '-' + endpoint + 'x'
                                        i.name = substation_name + '-' + endpoint + 'x'
                                        if i.name in all_nodes_set:
                                            all_nodes_set.remove(i.name)
                                    else:
                                        boundry_map[i.name] = feeder_names[
                                            feeder_cnt - 1]
                                        i.name = feeder_names[feeder_cnt -
                                                              1].lower()
                                    i.substation_name = substation_name
                                    i.is_substation = False
                                    i.feeder_name = i.substation_name + '->' + endpoint
                                    #import pdb;pdb.set_trace()
                                    if i.substation_name + '->' + endpoint in model.model_names:  # Set the Feedermetadata headnode to be the correct name.
                                        model[i.substation_name + '->' +
                                              endpoint].headnode = i.name
                                else:
                                    i.name = str(
                                        sub_file + '_' + sub + '_' + i.name
                                    ).lower(
                                    )  #ie. No feeders assigned to this berth so using the substation identifiers
                            else:
                                raise ValueError(
                                    "Nominal Voltages not set correctly in substation"
                                )

                        elif hasattr(i, 'name') and (not isinstance(
                                i, Feeder_metadata)):
                            i.name = str(sub_file + '_' + sub + '_' +
                                         i.name).lower()
                        if isinstance(i, Regulator) and hasattr(
                                i, 'connected_transformer'
                        ) and i.connected_transformer is not None:
                            i.connected_transformer = str(
                                sub_file + '_' + sub + '_' +
                                i.connected_transformer).lower()

                        if hasattr(
                                i,
                                'from_element') and i.from_element is not None:
                            if i.from_element in boundry_map:
                                i.from_element = boundry_map[i.from_element]
                            else:
                                i.from_element = str(sub_file + '_' + sub +
                                                     '_' +
                                                     i.from_element).lower()
                        if hasattr(i,
                                   'to_element') and i.to_element is not None:
                            if i.to_element in boundry_map:
                                i.to_element = boundry_map[i.to_element]
                            else:
                                i.to_element = str(sub_file + '_' + sub + '_' +
                                                   i.to_element).lower()

                        if hasattr(
                                i, 'positions'
                        ) and i.positions is not None and len(i.positions) > 0:
                            # import pdb;pdb.set_trace()
                            if ref_long == 0 and ref_lat == 0:
                                logger.warning(
                                    "Warning: Reference co-ords are (0,0)")
                            scale_factor = 1
                            if element[
                                    0] >= 12:  # The larger substations were created with a strange scale factor
                                scale_factor = 1 / 50.0
                            i.positions[0].lat = scale_factor * 7 * (
                                i.positions[0].lat - ref_lat) + lat
                            i.positions[0].long = scale_factor * 10 * (
                                i.positions[0].long - ref_long) + long
                            if len(i.positions) > 1:
                                for k in range(1, len(i.positions)):
                                    i.positions[k].lat = scale_factor * 7 * (
                                        i.positions[k].lat - ref_lat) + lat
                                    i.positions[k].long = scale_factor * 10 * (
                                        i.positions[k].long - ref_long) + long


#import pdb;pdb.set_trace()
                    not_allocated = False
                    sub_model.set_names()
                    to_delete.set_names()
                    reduced_model = modifier.delete(model, to_delete)
                    logger.info("Adding model from {} to model".format(
                        substation_folder))
                    #import pdb;pdb.set_trace()
                    model = modifier.add(
                        reduced_model, sub_model
                    )  #Is it a problem to be modifying the model directly?
                    break
            if not_allocated:
                raise ValueError(
                    'Substation too small. {num} feeders needed.  Exiting...'.
                    format(num=num_model_feeders))

        model.set_names()
        logger.debug("Returning {!r}".format(model))
        return model