Source code for PyDSS.pyPostprocessor.PostprocessScripts.AutomatedVoltageUpgrade

#**Authors:**
# Akshay Kumar Jain; Akshay.Jain@nrel.gov

import os
import matplotlib.pyplot as plt
import opendssdirect as dss
import networkx as nx
import time
import numpy as np
import seaborn as sns
import re
from sklearn.cluster import AgglomerativeClustering
import json
import math

from PyDSS.common import SimulationType
from PyDSS.exceptions import InvalidParameter, OpenDssConvergenceError
from PyDSS.pyPostprocessor.pyPostprocessAbstract import AbstractPostprocess
from PyDSS.pyPostprocessor.PostprocessScripts.postprocess_voltage_upgrades import postprocess_voltage_upgrades
from PyDSS.utils.utils import iter_elements, check_redirect

plt.rcParams.update({'font.size': 14})


# to get metadata: source bus, substation xfmr information
[docs]def get_ckt_info(): data_dict = {} dss.Vsources.First() source_bus = dss.CktElement.BusNames()[0].split(".")[0] data_dict['source_bus'] = source_bus dss.Transformers.First() while True: bus_names = dss.CktElement.BusNames() bus_names_only = [] for buses in bus_names: bus_names_only.append(buses.split(".")[0].lower()) if source_bus.lower() in bus_names_only: sub_xfmr = dss.Transformers.Name() data_dict["substation_xfmr"] = { "xfmr_name": sub_xfmr, "xfmr_kva": dss.Transformers.kVA(), "xfmr_kv": dss.Transformers.kV(), "bus_names": bus_names_only } if not dss.Transformers.Next() > 0: break return data_dict
# function to get regulator information
[docs]def get_reg_control_info(): reg_name = dss.RegControls.Name() data_dict = {"name": reg_name, "xfmr_name": dss.RegControls.Transformer(), "ptratio": dss.RegControls.PTRatio(), "delay": dss.RegControls.Delay()} dss.Circuit.SetActiveElement("Regcontrol.{}".format(reg_name)) data_dict["v_setpoint"] = dss.Properties.Value("vreg"), # this returns a tuple - account in pp data_dict["v_deadband"] = dss.Properties.Value("band"), # this returns a tuple - account in pp data_dict["enabled"] = dss.CktElement.Enabled(), # this returns a tuple - account in postprocess data_dict["reg_bus"] = dss.CktElement.BusNames()[0].split(".")[0] dss.Circuit.SetActiveBus(data_dict["reg_bus"]) data_dict["bus_num_phases"] = dss.CktElement.NumPhases() data_dict["bus_kv"] = dss.Bus.kVBase() dss.Circuit.SetActiveElement("Transformer.{}".format(data_dict["xfmr_name"])) data_dict["xfmr_kva"] = float(dss.Properties.Value("kva")) dss.Transformers.Wdg(1) # setting winding to 1, to get kV for winding 1 data_dict["xfmr_kv"] = dss.Transformers.kV() data_dict["xfmr_bus1"] = dss.CktElement.BusNames()[0].split(".")[0] data_dict["xfmr_bus2"] = dss.CktElement.BusNames()[1].split(".")[0] return data_dict
# function to get capacitor information
[docs]def get_capacitor_info(): return {"name": dss.Capacitors.Name(), "kv": dss.Capacitors.kV(), "kvar": dss.Capacitors.kvar()}
[docs]def get_cap_controls_info(): ctrl_name = dss.CapControls.Name() dss.Circuit.SetActiveElement("CapControl.{}".format(ctrl_name)) ## data_dict = { "name": ctrl_name, "cap_name": dss.CapControls.Capacitor(), "offsetting": dss.CapControls.OFFSetting(), "onsetting": dss.CapControls.ONSetting(), "control_type": dss.Properties.Value("type"), "ctratio": dss.CapControls.CTRatio(), "ptratio": dss.CapControls.PTRatio(), } dss.Capacitors.Name(data_dict["cap_name"]) data_dict["cap_kvar"] = dss.Capacitors.kvar() data_dict["cap_kv"] = dss.Capacitors.kV() return data_dict
[docs]class AutomatedVoltageUpgrade(AbstractPostprocess): """ This class is used to determine Voltage Upgrades """ REQUIRED_INPUT_FIELDS = ( "target_v", "initial_voltage_upper_limit", "initial_voltage_lower_limit", "final_voltage_upper_limit", "final_voltage_lower_limit", "nominal_voltage", "nominal_pu_voltage", "tps_to_test", "create_topology_plots", "cap_sweep_voltage_gap", "reg_control_bands", "reg_v_delta", "max_regulators", "use_ltc_placement", "thermal_scenario_name", ) def __init__(self, project, scenario, inputs, dssInstance, dssSolver, dssObjects, dssObjectsByClass, simulationSettings, Logger): """Constructor method """ super(AutomatedVoltageUpgrade, self).__init__(project, scenario, inputs, dssInstance, dssSolver, dssObjects, dssObjectsByClass, simulationSettings, Logger) self._simulation = None self._step = None dss = dssInstance self.dssSolver = dssSolver self.config["project_dss_files_path"] = project.dss_files_path self.config["thermal_scenario_path"] = project.get_post_process_directory(self.config["thermal_scenario_name"]) if simulationSettings.project.simulation_type != SimulationType.SNAPSHOT: raise InvalidParameter("Upgrade post-processors are only supported on Snapshot simulations") def _run(self): # Just send this list as input to the upgrades code via DISCO - this list may be empty or have as many # paths as the user desires - if empty the mults in the 'tps_to_test' input will be used else if non-empty # max and min load mults from the load.dss files will be used. Tne tps to test input should always be specified # irrespective of whether it gets used or not # these parameters are used only if multiple load and pv files are present # TODO: only fixed_tps (using tps_to_test list from config) works in this version # associated function to compute violations need to be changed to make the multiple dss files option work use_fixed_tps = True if not use_fixed_tps: self.other_pv_dss_files = self.config["project_data"]["pydss_other_pvs_dss_files"] self.other_load_dss_files = self.config["project_data"]["pydss_other_loads_dss_files"] self.get_load_pv_mults_individual_object() # multipliers are computed for individual load and pv # self.get_load_mults() # max and min are taken else: self.other_load_dss_files = [] self.other_pv_dss_files = [] thermal_filename = "thermal_upgrades.dss" thermal_dss_file = os.path.join(self.config["thermal_scenario_path"], thermal_filename) self.logger.info("thermal_dss_file=%s", thermal_dss_file) if not os.path.exists(thermal_dss_file): raise InvalidParameter(f"AutomatedThermalUpgrade did not produce thermal_filename") check_redirect(thermal_dss_file) self.start = time.time() # reading original objects (before upgrades) self.orig_ckt_info = get_ckt_info() self.orig_reg_controls = {x["name"]: x for x in iter_elements(dss.RegControls, get_reg_control_info)} self.orig_capacitors = {x["name"]: x for x in iter_elements(dss.Capacitors, get_capacitor_info)} self.orig_capcontrols = {x["name"]: x for x in iter_elements(dss.CapControls, get_cap_controls_info)} self.orig_xfmr_info = dss.Transformers.AllNames() # Get feeder head meta data self.feeder_head_name = dss.Circuit.Name() self.feeder_head_bus = dss.CktElement.BusNames()[0].split(".")[0] dss.Circuit.SetActiveBus(self.feeder_head_bus) self.feeder_head_basekv = dss.Bus.kVBase() num_nodes = dss.Bus.NumNodes() if num_nodes > 1: self.feeder_head_basekv = round(self.feeder_head_basekv * math.sqrt(3), 1) # Cap bank default settings - self.capON = round((self.config["nominal_voltage"] - self.config["cap_sweep_voltage_gap"] / 2), 1) self.capOFF = round((self.config["nominal_voltage"] + self.config["cap_sweep_voltage_gap"] / 2), 1) self.capONdelay = 0 self.capOFFdelay = 0 self.capdeadtime = 0 self.PTphase = "AVG" self.cap_control = "voltage" self.max_regs = self.config["max_regulators"] self.terminal = 1 self.plot_violations_counter = 0 # TODO: Regs default settings # Substation LTC default settings self.LTC_setpoint = 1.03 * self.config["nominal_voltage"] self.LTC_wdg = 2 self.LTC_delay = 45 # in seconds self.LTC_band = 2 # deadband in volts self.place_new_regulators = False # flag to determine whether to place new regulators or not # Initialize dss upgrades file self.dss_upgrades = [ "//This file has all the upgrades determined using the control device placement algorithm \n"] self.dssSolver = dss.Solution # Get correct source bus and conn type for all downstream regs - since regs are added before DTs and after # sub xfmr - their connection should be same as that of secondary wdg of sub xfmr dss.Vsources.First() self.source = dss.CktElement.BusNames()[0].split(".")[0] self.reg_conn = "wye" dss.Transformers.First() while True: xfmr_buses = dss.CktElement.BusNames() for bus in xfmr_buses: if bus.split(".")[0].lower() == self.source: num_wdgs = dss.Transformers.NumWindings() for wdg in range(0, num_wdgs, 1): self.reg_conn = dss.Properties.Value("conn") if not dss.Transformers.Next() > 0: break self.start_t = time.time() # used to determine time taken for run self.generate_nx_representation() self.get_existing_controller_info() if self.config["create_topology_plots"]: self.plot_feeder() pass self.write_flag = 1 self.feeder_parameters = {} self.create_result_comparison_voltages(comparison_stage='Before Upgrades') self.initial_buses_with_violations = self.buses_with_violations # save initial bus violations self.upgrade_status = '' # status - whether voltage upgrades done or not # if there are no buses with violations based on initial check, don't get into upgrade process # directly go to end of file if len(self.buses_with_violations) <= 0: self.logger.info("No Voltage Upgrades Required.") self.upgrade_status = 'No Voltage Upgrades needed' # status - whether voltage upgrades done or not # else, if there are bus violations based on initial check, start voltage upgrades process else: # change voltage checking thresholds self.upper_limit = self.config["final_voltage_upper_limit"] self.lower_limit = self.config["final_voltage_lower_limit"] self.upgrade_status = 'Voltage Upgrades were needed' # status - whether voltage upgrades done or not self.logger.info("Voltage Upgrades Required.") # Use this block for capacitor settings if len(self.buses_with_violations) > 0: if self.config["create_topology_plots"]: self.plot_violations() # Correct cap banks settings if caps are present in the feeder if dss.Capacitors.Count() > 0: self.logger.info("Cap bank settings sweep, if capacitors are present.") self.get_capacitor_state() self.correct_cap_bank_settings() if self.config["create_topology_plots"]: self.plot_violations() if len(self.buses_with_violations) > 0: self.cap_settings_sweep(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) if self.config["create_topology_plots"]: self.plot_violations() else: self.logger.info("No cap banks exist in the system") # Do a settings sweep of existing reg control devices (other than sub LTC) after correcting their other # parameters such as ratios etc self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.reg_sweep_viols = {} if dss.RegControls.Count() > 0 and len(self.buses_with_violations) > 0: self.logger.info("Settings sweep for existing reg control devices (other than sub LTC).") self.initial_regctrls_settings = {} reg_cnt = 0 dss.RegControls.First() while True: name = dss.RegControls.Name() xfmr = dss.RegControls.Transformer() dss.Circuit.SetActiveElement("Transformer.{}".format(xfmr)) xfmr_buses = dss.CktElement.BusNames() xfmr_b1 = xfmr_buses[0].split(".")[0] xfmr_b2 = xfmr_buses[1].split(".")[0] # # Skipping over substation LTC if it exists # for n, d in self.G.in_degree().items(): # if d==0: # sourcebus = n sourcebus = self.source if xfmr_b1.lower() == sourcebus.lower() or xfmr_b2.lower() == sourcebus.lower(): dss.Circuit.SetActiveElement("Regcontrol.{}".format(name)) # dss.RegControls.Next() if not dss.RegControls.Next() > 0: break continue reg_cnt+=1 dss.Circuit.SetActiveElement("Regcontrol.{}".format(name)) bus_name = dss.CktElement.BusNames()[0].split(".")[0] dss.Circuit.SetActiveBus(bus_name) phases = dss.CktElement.NumPhases() kV = dss.Bus.kVBase() dss.Circuit.SetActiveElement("Regcontrol.{}".format(name)) winding = self.LTC_wdg reg_delay = self.LTC_delay pt_ratio = kV * 1000 / (self.config["nominal_voltage"]) try: Vreg = dss.Properties.Value("vreg") except: Vreg = self.config["nominal_voltage"] try: bandwidth = dss.Properties.Value("band") except: bandwidth = 3.0 self.initial_regctrls_settings[name] = [Vreg, bandwidth] command_string = "Edit RegControl.{rn} transformer={tn} winding={w} vreg={sp} ptratio={rt} band={b} " \ "enabled=true delay={d} !original".format( rn=name, tn=xfmr, w=winding, sp=Vreg, rt=pt_ratio, b=bandwidth, d=reg_delay ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) # add this to a dss_upgrades.dss file self.write_dss_file(command_string) if not dss.RegControls.Next() > 0: break self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) if reg_cnt > 1: self.reg_sweep_viols["original"] = self.severity_indices[2] if len(self.buses_with_violations) > 0: self.reg_controls_sweep(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) if self.config["create_topology_plots"]: self.plot_violations() # Writing out the results before adding new devices self.logger.info("Write upgrades to dss file, before adding new devices.") self.write_upgrades_to_file() # TODO: decide whether postprocess should be done once before going to next stage of adding objects # postprocess_voltage_upgrades( # { # "outputs": self.config["Outputs"], # "feederhead_name": self.feeder_head_name, # "feederhead_basekV": self.feeder_head_basekv # }, # self.logger, # ) # New devices might be added after this self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.cluster_optimal_reg_nodes = {} self.cluster_optimal_reg_nodes["pre_reg"] = [self.severity_indices[2]] self.sub_LTC_added_flag = 0 # Use this block for adding a substation LTC, correcting its settings and running a sub LTC settings sweep - # if LTC exists first try to correct its non set point simulation settings - if this does not correct everything # correct its set points through a sweep - if LTC does not exist add one including a xfmr if required - then # do a settings sweep if required # self.add_ctrls_flag = 0 # TODO: If adding a substation LTC increases violations even after the control sweep then before then remove it # TODO: - this might however interfere with voltage regulator logic so may be let it be there if self.config["use_ltc_placement"]: self.logger.info("Add/Correct/Sweep settings for Substation LTC.") self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) if len(self.buses_with_violations) > 0: if self.config["create_topology_plots"]: self.plot_violations() # Add substation LTC if not available (may require addition of a new substation xfmr as well) # if available correct its settings self.subLTC_sweep_viols = {} self.LTC_exists_flag = 0 if dss.RegControls.Count() > 0 and len(self.buses_with_violations) > 0: self.initial_subLTC_settings = {} dss.RegControls.First() while True: name = dss.RegControls.Name() xfmr = dss.RegControls.Transformer() dss.Circuit.SetActiveElement("Transformer.{}".format(xfmr)) xfmr_buses = dss.CktElement.BusNames() xfmr_b1 = xfmr_buses[0].split(".")[0] xfmr_b2 = xfmr_buses[1].split(".")[0] # for n, d in self.G.in_degree().items(): # if d==0: # sourcebus = n sourcebus = self.source # Skipping over all reg controls other than sub LTC if xfmr_b1.lower() == sourcebus.lower() or xfmr_b2.lower() == sourcebus.lower(): self.LTC_exists_flag = 1 dss.Circuit.SetActiveElement("Regcontrol.{}".format(name)) bus_name = dss.CktElement.BusNames()[0].split(".")[0] dss.Circuit.SetActiveBus(bus_name) phases = dss.CktElement.NumPhases() kV = dss.Bus.kVBase() winding = self.LTC_wdg reg_delay = self.LTC_delay pt_ratio = kV * 1000 / (self.config["nominal_voltage"]) try: Vreg = dss.Properties.Value("vreg") except: Vreg = self.config["nominal_voltage"] try: bandwidth = dss.Properties.Value("band") except: bandwidth = 3.0 self.initial_subLTC_settings[name] = [Vreg, bandwidth] command_string = "Edit RegControl.{rn} transformer={tn} winding={w} vreg={sp} ptratio={rt} band={b} " \ "enabled=true delay={d} !original".format( rn=name, tn=xfmr, w=winding, sp=Vreg, rt=pt_ratio, b=bandwidth, d=reg_delay ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) # add this to a dss_upgrades.dss file self.write_dss_file(command_string) dss.Circuit.SetActiveElement("Regcontrol.{}".format(name)) if not dss.RegControls.Next() > 0: break if self.LTC_exists_flag == 1: self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.subLTC_sweep_viols["original"] = self.severity_indices[2] if len(self.buses_with_violations) > 0: self.LTC_controls_sweep(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.create_final_comparison(project_path=self.config["project_dss_files_path"], thermal_dss_file=thermal_dss_file) pass_flag = self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit, raise_exception=False) # TODO # TODO: pass flag to be used: if pass_flag is false, just go to create comparison file if self.config["create_topology_plots"]: self.plot_violations() elif self.LTC_exists_flag == 0: self.add_substation_LTC() self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.cluster_optimal_reg_nodes["sub_LTC"] = [self.severity_indices[2]] self.sub_LTC_added_flag = 1 if len(self.buses_with_violations) > 0: self.LTC_controls_sweep(upper_limit=self.upper_limit, lower_limit=self.lower_limit) pass_flag = self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit, raise_exception=False) # TODO: this pass flag is to be used: # if pass_flag is false, then just go to create comparison file if self.config["create_topology_plots"]: self.plot_violations() elif dss.RegControls.Count() == 0 and len(self.buses_with_violations) > 0: self.add_substation_LTC() self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.cluster_optimal_reg_nodes["sub_LTC"] = [self.severity_indices[2]] self.sub_LTC_added_flag = 1 if len(self.buses_with_violations) > 0: self.LTC_controls_sweep(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) if self.config["create_topology_plots"]: self.plot_violations() # Correct regulator settings if regs are present in the feeder other than the sub station LTC # TODO: Remove regs of last iteration pass_flag = self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit, raise_exception=False) # TODO # TODO: pass flag to be used: if pass_flag is false, then just go to create comparison file self.logger.info(f"Total number of buses in circuit: {len(dss.Circuit.AllBusNames())}") # if number of buses with violations is very high, the loop for adding new regulators will take very long # so disable this block - current condition is if number of violations > 250 if len(self.buses_with_violations) >= min((100 * len(self.initial_buses_with_violations)), 500, len(dss.Circuit.AllBusNames()) ): self.logger.info(f"At this point, number of buses with violations is {len(self.buses_with_violations)}," f" but initial number of buses with violations is " f"{len(self.initial_buses_with_violations)}") self.logger.info("So disable option for addition of new regulators") self.place_new_regulators = False # if option to place new regulators is disabled if (len(self.buses_with_violations) > 1) and (not self.place_new_regulators): self.logger.info("Ignoring block for adding new regulators") # determining key with minimum objective func. at various levels # (at this point includes pre-reg, sub-LTC) min_cluster = '' min_severity = 1000000000 # TODO - below logic for min_severity was used previously - however, error cases were encountered # for some feeders due to min_severity being not large enough # min_severity = pow(len(self.all_bus_names), 2) * len(self.config["tps_to_test"]) * self.upper_limit for key, vals in self.cluster_optimal_reg_nodes.items(): if vals[0] < min_severity: min_severity = vals[0] min_cluster = key self.logger.info(f"Checking objective function to determine best possible upgrades.\n" f"At stages: 1) Before addition of new devices. " f"2) With Substation LTC.") self.compare_objective_function(min_cluster) # if option to place new regulators is enabled elif (len(self.buses_with_violations) > 1) and (self.place_new_regulators): if self.config["create_topology_plots"]: self.plot_violations() # for n, d in self.G.in_degree().items(): # if d == 0: # self.source = n # Place additional regulators if required self.logger.info("Sweep by placing additional regulators") self.logger.info(f"Number of buses with violations:{len(self.buses_with_violations)}") self.max_regs = int(min(self.config["max_regulators"], len(self.buses_with_violations))) self.get_shortest_path() self.get_full_distance_dict() self.cluster_square_array() min_severity = 1000000000 # TODO - below logic for min_severity was used previously - however, error cases were encountered # for some feeders due to min_severity being not large enough # min_severity = pow(len(self.all_bus_names), 2) * len(self.config["tps_to_test"]) * self.upper_limit # determining key with minimum objective func. at various levels # (at this point includes pre-reg, sub-LTC, and all newly added regulators) min_cluster = '' for key, vals in self.cluster_optimal_reg_nodes.items(): if vals[0] < min_severity: min_severity = vals[0] min_cluster = key # Logic is if violations were less before addition of any device, revert back to that condition by removing # added LTC and not adding best determined in-line regs, else if LTC was best - pass and do not add new # in-line regs, or if some better LTC placemenr was determined apply that self.logger.info(f"Checking objective function to determine best possible upgrades.\n" f"At stages: 1) Before addition of new devices. " f"2) With Substation LTC. 3) With newly added regulators") self.compare_objective_function(min_cluster) self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.logger.info("Compare objective with best and applied settings, %s, %s", self.cluster_optimal_reg_nodes[min_cluster][0], self.severity_indices[2]) self.logger.info("Additional regctrl devices: %s", min_cluster) self.logger.info("cluster_optimal_reg_nodes=%s", self.cluster_optimal_reg_nodes) self.end_t = time.time() # used to determine time taken for run self.create_final_comparison(project_path=self.config["project_dss_files_path"], thermal_dss_file=thermal_dss_file) # go to voltage upgrades post processing script postprocess_voltage_upgrades( { "outputs": self.config["Outputs"], "feederhead_name": self.feeder_head_name, "feederhead_basekV": self.feeder_head_basekv, "orig_ckt_info": self.orig_ckt_info, "new_reg_controls": self.new_reg_controls, "orig_reg_controls": self.orig_reg_controls, "new_capacitors": self.new_capacitors, "orig_capacitors": self.orig_capacitors, "new_capcontrols": self.new_capcontrols, "orig_capcontrols": self.orig_capcontrols, "orig_xfmr_info": self.orig_xfmr_info, "new_xfmr_info": self.new_xfmr_info, "new_ckt_info": self.new_ckt_info, }, self.logger, ) self.has_converged = dss.Solution.Converged() self.error = dss.Solution.Convergence() # TODO This is fake for now, find how to get this from Opendssdirect @staticmethod def _get_required_input_fields(): return AutomatedVoltageUpgrade.REQUIRED_INPUT_FIELDS
[docs] def create_final_comparison(self, project_path=None, thermal_dss_file=None): self.logger.debug("Writing upgrades to DSS file") self.write_upgrades_to_file() self.logger.info("Checking impact of redirected upgrades file") dss.run_command("Clear") base_dss = os.path.join(project_path, self.Settings.project.dss_file) check_redirect(base_dss) check_redirect(thermal_dss_file) upgrades_file = os.path.join(self.config["Outputs"], "voltage_upgrades.dss") check_redirect(upgrades_file) self.dssSolver.Solve() self._simulation.RunStep(self._step) self.new_reg_controls = {x["name"]: x for x in iter_elements(dss.RegControls, get_reg_control_info)} self.new_capacitors = {x["name"]: x for x in iter_elements(dss.Capacitors, get_capacitor_info)} self.new_capcontrols = {x["name"]: x for x in iter_elements(dss.CapControls, get_cap_controls_info)} self.new_xfmr_info = dss.Transformers.AllNames() self.new_ckt_info = get_ckt_info() self.create_result_comparison_voltages(comparison_stage='After Upgrades') self.feeder_parameters["Simulation time (seconds)"] = self.end_t-self.start_t self.feeder_parameters["Upgrade status"] = self.upgrade_status self.feeder_parameters["feederhead_name"] = self.feeder_head_name self.feeder_parameters["feederhead_basekV"] = self.feeder_head_basekv self.write_to_json(self.feeder_parameters, "Voltage_violations_comparison") return
# this function create comparison file
[docs] def create_result_comparison_voltages(self, comparison_stage=''): # If initial and final limits are different, # also doing with final limits to get comparison between initial and final violation numbers if (self.config["final_voltage_upper_limit"] != self.config["initial_voltage_upper_limit"]) or \ (self.config["final_voltage_lower_limit"] != self.config["initial_voltage_lower_limit"]): self.logger.info(f"Initial and Final voltage limits are not the same. " f"\ninitial_voltage_lower_limit: {self.config['initial_voltage_lower_limit']}, " f"initial_voltage_upper_limit: {self.config['initial_voltage_upper_limit']} " f"\nfinal_voltage_lower_limit: {self.config['final_voltage_lower_limit']}, " f"final_voltage_upper_limit: {self.config['final_voltage_upper_limit']}") self.upper_limit = self.config["final_voltage_upper_limit"] self.lower_limit = self.config["final_voltage_lower_limit"] self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.logger.info(f"Based on Lower limit: {self.lower_limit}, Upper limit: {self.upper_limit}") self.logger.info("{} number of buses with violations are: {}".format(comparison_stage, len(self.buses_with_violations))) self.logger.info("{} objective function value: {}".format(comparison_stage, self.severity_indices[2])) self.feeder_parameters["{}_violations_2".format(comparison_stage)] = { "Voltage upper threshold": self.upper_limit, "Voltage lower threshold": self.lower_limit, "Number of buses with violations": len(self.buses_with_violations), "Number of buses with overvoltage violations": len(self.buses_with_overvoltage_violations), "Number of buses with undervoltage violations": len(self.buses_with_undervoltage_violations), "Buses at all tps with violations": self.severity_indices[0], "Severity of bus violations": self.severity_indices[1], "Objective function value": self.severity_indices[2], "Maximum voltage observed": self.max_V_viol, "Minimum voltage observed": self.min_V_viol } # change violation checking thresholds to initial limit - to ensure uniform comparison betn initial & final self.upper_limit = self.config["initial_voltage_upper_limit"] self.lower_limit = self.config["initial_voltage_lower_limit"] self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) if self.config["create_topology_plots"]: self.plot_violations() self.logger.info("{} maximum voltage observed on any node: {} {}".format(comparison_stage, self.max_V_viol, self.busvmax)) self.logger.info("{} minimum voltage observed on any node: {}".format(comparison_stage, self.min_V_viol)) self.logger.info(f"Based on Lower limit: {self.lower_limit}, Upper limit: {self.upper_limit}") self.logger.info("{} number of buses with violations are: {}".format(comparison_stage, len(self.buses_with_violations))) self.logger.info("{} objective function value: {}".format(comparison_stage, self.severity_indices[2])) self.feeder_parameters["{}_violations".format(comparison_stage)] = { "Voltage upper threshold": self.upper_limit, "Voltage lower threshold": self.lower_limit, "Number of buses with violations": len(self.buses_with_violations), "Number of buses with overvoltage violations": len(self.buses_with_overvoltage_violations), "Number of buses with undervoltage violations": len(self.buses_with_undervoltage_violations), "Buses at all tps with violations": self.severity_indices[0], "Severity of bus violations": self.severity_indices[1], "Objective function value": self.severity_indices[2], "Maximum voltage observed": self.max_V_viol, "Minimum voltage observed": self.min_V_viol } return
[docs] def get_load_pv_mults_individual_object(self): self.orig_loads = {} self.orig_pvs = {} self.dssSolver.Solve() self._simulation.RunStep(self._step) if dss.Loads.Count() > 0: dss.Loads.First() while True: load_name = dss.Loads.Name().split(".")[0].lower() kW = dss.Loads.kW() self.orig_loads[load_name] = [kW] if not dss.Loads.Next() > 0: break for key, dss_paths in self.other_load_dss_files.items(): self.read_load_files_individual_object(key, dss_paths) if dss.PVsystems.Count() > 0: dss.PVsystems.First() while True: pv_name = dss.PVsystems.Name().split(".")[0].lower() pmpp = float(dss.Properties.Value("irradiance")) self.orig_pvs[pv_name] = [pmpp] if not dss.PVsystems.Next() > 0: break for key, dss_paths in self.other_pv_dss_files.items(): self.read_pv_files_individual_object(key, dss_paths)
[docs] def read_load_files_individual_object(self,key_paths,dss_path): # Add all load kW values temp_dict = {} for path_f in dss_path: with open(path_f, "r") as datafile: for line in datafile: if line.lower().startswith("new load."): for params in line.split(): if params.lower().startswith("load."): ld_name = params.lower().split("load.")[1] if params.lower().startswith("kw"): ld_kw = float(params.lower().split("=")[1]) temp_dict[ld_name] = ld_kw for key,vals in self.orig_loads.items(): if key in temp_dict: self.orig_loads[key].append(temp_dict[key]) elif key not in temp_dict: self.orig_loads[key].append(self.orig_loads[key][0])
[docs] def read_pv_files_individual_object(self, key_paths, dss_path): # Add all PV pmpp values temp_dict = {} for path_f in self.other_pv_dss_files[key_paths]: with open(path_f, "r") as datafile: for line in datafile: if line.lower().startswith("new pvsystem."): for params in line.split(): if params.lower().startswith("pvsystem."): pv_name = params.lower().split("pvsystem.")[1] if params.lower().startswith("irradiance"): pv_pmpp = float(params.lower().split("=")[1]) temp_dict[pv_name] = pv_pmpp for key, vals in self.orig_pvs.items(): if key in temp_dict: self.orig_pvs[key].append(temp_dict[key]) elif key not in temp_dict: self.orig_pvs[key].append(self.orig_pvs[key][0])
[docs] def get_load_mults(self): self.orig_loads = {} self.dssSolver.Solve() self._simulation.RunStep(self._step) dss.Loads.First() while True: load_name = dss.Loads.Name().split(".")[0].lower() kW = dss.Loads.kW() self.orig_loads[load_name] = [kW] if not dss.Loads.Next()>0: break for dss_path in self.other_load_dss_files: self.read_load_files(dss_path) self.get_min_max_load_mult()
[docs] def read_load_files(self, dss_path): with open(dss_path, "r") as datafile: for line in datafile: if line.lower().startswith("new load."): for params in line.split(): if params.lower().startswith("load."): ld_name = params.lower().split("load.")[1] if params.lower().startswith("kw"): ld_kw = float(params.lower().split("=")[1]) if ld_name in self.orig_loads: self.orig_loads[ld_name].append(ld_kw)
[docs] def get_min_max_load_mult(self): self.min_max_load_kw = {} for key,vals in self.orig_loads.items(): self.min_max_load_kw[key] = [min(vals),max(vals)]
[docs] def compare_objective_function(self, min_cluster): # Logic is if violations were less before addition of any device, revert back to that condition by removing # added LTC and not adding best determined in-line regs, else if LTC was best - pass and do not add new # in-line regs, or if some better LTC placement was determined apply that if min_cluster == "pre_reg": self.logger.info("Violations were less before addition of any new regulator or substation LTC.") self.logger.info("Remove substation LTC and best in-line reg.") # This will remove LTC controller, but if initially there was no substation transformer # (highly unlikely) the added transformer will still be there if self.sub_LTC_added_flag == 1: if self.subxfmr == '': LTC_reg_node = self.source elif self.subxfmr != '': LTC_reg_node = self.sub_LTC_bus LTC_name = "New_regctrl_" + LTC_reg_node command_string = "Edit RegControl.{ltc_nm} enabled=False".format( ltc_nm=LTC_name ) dss.run_command(command_string) self.write_dss_file(command_string) else: pass elif min_cluster == "sub_LTC": self.logger.info("Setting with substation LTC is best.") pass else: self.logger.info("Setting with new regulator placement is best.") for reg_nodes in self.cluster_optimal_reg_nodes[min_cluster][2]: self.write_flag = 1 self.add_new_xfmr(reg_nodes) self.add_new_regctrl(reg_nodes) rn_name = "New_regctrl_" + reg_nodes command_string = "Edit RegControl.{rn} vreg={vsp} band={b}".format( rn=rn_name, vsp=self.cluster_optimal_reg_nodes[min_cluster][1][0], b=self.cluster_optimal_reg_nodes[min_cluster][1][1] ) dss.run_command(command_string) dss.run_command("CalcVoltageBases") self.write_dss_file(command_string) # After all additional devices have been placed perform the cap bank settings sweep again- # only if new devices were accepted self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) if dss.CapControls.Count() > 0 and len(self.buses_with_violations) > 0: self.logger.info("Violations still exist -> Sweep capacitor settings again, " "after new devices are placed.") self.cap_settings_sweep(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) if self.config["create_topology_plots"]: self.plot_violations() self.write_dss_file("CalcVoltageBases")
[docs] def get_existing_controller_info(self): self.cap_control_info = {} self.reg_control_info = {} cap_bank_list = [] if dss.CapControls.Count() > 0: dss.CapControls.First() while True: cap_ctrl = dss.CapControls.Name().lower() cap_name = dss.CapControls.Capacitor().lower() cap_bank_list.append(cap_name) ctrl_type = dss.Properties.Value("type") on_setting = dss.CapControls.ONSetting() off_setting = dss.CapControls.OFFSetting() dss.Capacitors.Name(cap_name) if not cap_name.lower() == dss.Capacitors.Name().lower(): raise InvalidParameter("Incorrect Active Element") cap_size = dss.Capacitors.kvar() cap_kv = dss.Capacitors.kV() self.cap_control_info[cap_ctrl] = { "cap_name": cap_name, "cap kVAR": cap_size, "cap_kv": cap_kv, "Control type": ctrl_type, "ON": on_setting, "OFF": off_setting } dss.Circuit.SetActiveElement("CapControl.{}".format(cap_ctrl)) if not dss.CapControls.Next() > 0: break if dss.RegControls.Count() > 0: dss.RegControls.First() while True: reg_ctrl = dss.RegControls.Name().lower() dss.Circuit.SetActiveElement("Regcontrol.{}".format(reg_ctrl)) ## reg_vsp = dss.Properties.Value("vreg") reg_band = dss.Properties.Value("band") xfmr_name = dss.RegControls.Transformer().lower() dss.Transformers.Name(xfmr_name) if not xfmr_name.lower() == dss.Transformers.Name().lower(): raise InvalidParameter("Incorrect Active Element") xfmr_buses = dss.CktElement.BusNames() # bus_names = [] # for buses in xfmr_buses: # bus_names.append(buses.split(".")[0].lower()) # if self.source.lower() in bus_names: # self.sub_xfmr_cap = 1 xfmr_size = dss.Transformers.kVA() xfmr_kv = dss.Transformers.kV() self.reg_control_info[reg_ctrl] = { "reg_vsp": reg_vsp, "reg_band": reg_band, "xfmr_name": xfmr_name, "xfmr kVA": xfmr_size, "xfmr_kv": xfmr_kv } dss.Circuit.SetActiveElement("RegControl.{}".format(reg_ctrl)) if not dss.RegControls.Next() > 0: break # if self.sub_xfmr_cap==0: dss.Transformers.First() while True: bus_names = dss.CktElement.BusNames() bus_names_only = [] for buses in bus_names: bus_names_only.append(buses.split(".")[0].lower()) if self.source.lower() in bus_names_only: sub_xfmr = dss.Transformers.Name() self.reg_control_info["orig_substation_xfmr"] = { "xfmr_name": sub_xfmr, "xfmr kVA": dss.Transformers.kVA(), "xfmr_kv": dss.Transformers.kV(), "bus_names": bus_names_only } if not dss.Transformers.Next() > 0: break if dss.Capacitors.Count() > 0: dss.Capacitors.First() while True: cap_name = dss.Capacitors.Name().lower() cap_size = dss.Capacitors.kvar() cap_kv = dss.Capacitors.kV() ctrl_type = "NA" if cap_name not in cap_bank_list: self.cap_control_info["capbank_noctrl_{}".format(cap_name)] = { "cap_name": cap_name, "cap kVAR": cap_size, "cap_kv": cap_kv, "Control type": ctrl_type } if not dss.Capacitors.Next() > 0: break self.write_to_json(self.cap_control_info, "Initial_capacitors") self.write_to_json(self.reg_control_info, "Initial_regulators")
[docs] def write_to_json(self, dict, file_name): with open(os.path.join(self.config["Outputs"], "{}.json".format(file_name)), "w") as fp: json.dump(dict, fp, indent=4)
[docs] def generate_nx_representation(self): self.all_bus_names = dss.Circuit.AllBusNames() self.G = nx.DiGraph() self.generate_nodes() self.generate_edges() self.pos_dict = nx.get_node_attributes(self.G, 'pos') if self.config["create_topology_plots"]: self.correct_node_coords()
[docs] def correct_node_coords(self): # If node doesn't have node attributes, attach parent or child node's attributes new_temp_graph = self.G temp_graph = new_temp_graph.to_undirected() # for n, d in self.G.in_degree().items(): # if d == 0: # self.source = n for key, vals in self.pos_dict.items(): if vals[0] == 0.0 and vals[1] == 0.0: new_x = 0 new_y = 0 pred_buses = nx.shortest_path(temp_graph, source=key, target=self.source) if len(pred_buses) > 0: for pred_bus in pred_buses: if pred_bus == key: continue if self.pos_dict[pred_bus][0] != 0.0 and self.pos_dict[pred_bus][1] != 0.0: new_x = self.pos_dict[pred_bus][0] new_y = self.pos_dict[pred_bus][1] self.G.node[key]["pos"] = [new_x, new_y] break if new_x == 0 and new_y == 0: # Since either predecessor nodes were not available or they did not have # non-zero coordinates, try successor nodes # Get a leaf node for x in self.G.nodes(): if self.G.out_degree(x) == 0 and self.G.in_degree(x) == 1: leaf_node = x break succ_buses = nx.shortest_path(temp_graph, source=key, target=leaf_node) if len(succ_buses) > 0: for pred_bus in succ_buses: if pred_bus == key: continue if self.pos_dict[pred_bus][0] != 0.0 and self.pos_dict[pred_bus][1] != 0.0: new_x = self.pos_dict[pred_bus][0] new_y = self.pos_dict[pred_bus][1] self.G.node[key]["pos"] = [new_x, new_y] break # Update pos dict with new coordinates self.pos_dict = nx.get_node_attributes(self.G, 'pos')
[docs] def generate_nodes(self): self.nodes_list = [] for b in self.all_bus_names: dss.Circuit.SetActiveBus(b) name = b.lower() position = [] position.append(dss.Bus.X()) position.append(dss.Bus.Y()) self.G.add_node(name, pos=position) self.nodes_list.append(b)
[docs] def generate_edges(self): ''' All lines, switches, reclosers etc are modeled as lines, so calling lines takes care of all of them. However we also need to loop over transformers as they form the edge between primary and secondary nodes :return: ''' dss.Lines.First() while True: from_bus = dss.Lines.Bus1().split('.')[0].lower() to_bus = dss.Lines.Bus2().split('.')[0].lower() phases = dss.Lines.Phases() length = dss.Lines.Length() name = dss.Lines.Name() if dss.Lines.Units() == 1: length = length * 1609.34 elif dss.Lines.Units() == 2: length = length * 304.8 elif dss.Lines.Units() == 3: length = length * 1000 elif dss.Lines.Units() == 4: length = length elif dss.Lines.Units() == 5: length = length * 0.3048 elif dss.Lines.Units() == 6: length = length * 0.0254 elif dss.Lines.Units() == 7: length = length * 0.01 self.G.add_edge(from_bus, to_bus, phases=phases, length=length, name=name) if not dss.Lines.Next() > 0: break dss.Transformers.First() while True: bus_names = dss.CktElement.BusNames() from_bus = bus_names[0].split('.')[0].lower() to_bus = bus_names[1].split('.')[0].lower() phases = dss.CktElement.NumPhases() length = 0.0 name = dss.Transformers.Name() self.G.add_edge(from_bus, to_bus, phases=phases, length=length, name=name) if not dss.Transformers.Next() > 0: break
[docs] def plot_feeder(self): plt.figure(figsize=(7, 7)) ec = nx.draw_networkx_edges(self.G, pos=self.pos_dict, alpha=1.0, width=0.3) ldn = nx.draw_networkx_nodes(self.G, pos=self.pos_dict, nodelist=self.nodes_list, node_size=8, node_color='k', alpha=1) ld = nx.draw_networkx_nodes(self.G, pos=self.pos_dict, nodelist=self.nodes_list, node_size=6, node_color='yellow', alpha=0.7) nx.draw_networkx_labels(self.G, pos=self.pos_dict, node_size=1, font_size=15) plt.title("Feeder with all customers having DPV systems") plt.axis("off") plt.show()
# (not used) check voltage violations when we have multipliers for individual load and PV
[docs] def check_voltage_violations_multi_tps_individual_object(self, upper_limit, lower_limit): # TODO: This objective currently gives more weightage if same node has violations at more than 1 time point num_nodes_counter = 0 severity_counter = 0 self.max_V_viol = 0 self.min_V_viol = 2 self.buses_with_violations = [] self.buses_with_violations_pos = {} self.nodal_violations_dict = {} # If multiple load files are being used, the 'tps_to_test property is not used, else if a single load file is # used use the 'tps to test' input for tp_cnt in range(len(self.other_load_dss_files)): # Apply correct pmpp values to all PV systems if dss.PVsystems.Count() > 0: dss.PVsystems.First() while True: pv_name = dss.PVsystems.Name().split(".")[0].lower() if pv_name not in self.orig_pvs: raise Exception("PV system not found, quitting...") new_pmpp = self.orig_pvs[pv_name][tp_cnt] dss.run_command(f"Edit PVsystem.{pv_name} irradiance={new_pmpp}") if not dss.PVsystems.Next()>0: break # Apply correct kW value to all loads if dss.Loads.Count() > 0: dss.Loads.First() while True: load_name = dss.Loads.Name().split(".")[0].lower() if load_name not in self.orig_loads: raise Exception("Load not found, quitting...") new_kw = self.orig_loads[load_name][tp_cnt] dss.Loads.kW(new_kw) if not dss.Loads.Next() > 0: break self.dssSolver.Solve() self._simulation.RunStep(self._step) if not dss.Solution.Converged(): self.logger.info("Write upgrades till this step in debug_upgrades.dss") self.write_upgrades_to_file(output_path=os.path.join(self.config["Outputs"], "debug_upgrades.dss")) raise OpenDssConvergenceError("OpenDSS solution did not converge") for b in self.all_bus_names: dss.Circuit.SetActiveBus(b) bus_v = dss.Bus.puVmagAngle()[::2] # Select that bus voltage of the three phases which is outside bounds the most, # else if everything is within bounds use nominal pu voltage. maxv_dev = 0 minv_dev = 0 if max(bus_v) > self.max_V_viol: self.max_V_viol = max(bus_v) self.busvmax = b if min(bus_v) < self.min_V_viol: self.min_V_viol = min(bus_v) if max(bus_v) > upper_limit: maxv = max(bus_v) maxv_dev = maxv - upper_limit if min(bus_v) < lower_limit: minv = min(bus_v) minv_dev = upper_limit - minv if maxv_dev > minv_dev: v_used = maxv num_nodes_counter += 1 severity_counter += maxv_dev if b.lower() not in self.buses_with_violations: self.buses_with_violations.append(b.lower()) self.buses_with_violations_pos[b.lower()] = self.pos_dict[b.lower()] elif minv_dev > maxv_dev: v_used = minv num_nodes_counter += 1 severity_counter += minv_dev if b.lower() not in self.buses_with_violations: self.buses_with_violations.append(b.lower()) self.buses_with_violations_pos[b.lower()] = self.pos_dict[b.lower()] else: v_used = self.config["nominal_pu_voltage"] if b not in self.nodal_violations_dict: self.nodal_violations_dict[b.lower()] = [v_used] elif b in self.nodal_violations_dict: self.nodal_violations_dict[b.lower()].append(v_used) self.severity_indices = [num_nodes_counter, severity_counter, num_nodes_counter * severity_counter] return
# this function checks for voltage violations based on upper and lower limit passed
[docs] def check_voltage_violations_multi_tps(self, upper_limit, lower_limit, raise_exception=True): # TODO: This objective currently gives more weightage if same node has violations at more than 1 time point num_nodes_counter = 0 severity_counter = 0 self.max_V_viol = 0 self.min_V_viol = 2 self.buses_with_violations = [] self.buses_with_undervoltage_violations = [] self.buses_with_overvoltage_violations = [] self.buses_with_violations_pos = {} self.nodal_violations_dict = {} # If multiple load files are being used, the 'tps_to_test property is not used, else if a single load file is # used use the 'tps to test' input if len(self.other_load_dss_files)>0: for tp_cnt in range(len(self.config["tps_to_test"])): # First two tps are for disabled PV case if tp_cnt == 0 or tp_cnt == 1: dss.run_command("BatchEdit PVSystem..* Enabled=False") dss.Loads.First() while True: load_name = dss.Loads.Name().split(".")[0].lower() if load_name not in self.min_max_load_kw: raise Exception("Load not found, quitting...") new_kw = self.min_max_load_kw[load_name][tp_cnt] dss.Loads.kW(new_kw) if not dss.Loads.Next()>0: break self.dssSolver.Solve() self._simulation.RunStep(self._step) if not dss.Solution.Converged(): self.logger.info("Write upgrades before Convergence Error in debug_upgrades.dss") self.write_upgrades_to_file( output_path=os.path.join(self.config["Outputs"], "debug_upgrades.dss")) if raise_exception: raise OpenDssConvergenceError("OpenDSS solution did not converge") else: return False if tp_cnt == 2 or tp_cnt == 3: dss.run_command("BatchEdit PVSystem..* Enabled=True") dss.Loads.First() while True: load_name = dss.Loads.Name().split(".")[0].lower() if load_name not in self.min_max_load_kw: raise Exception("Load not found, quitting...") new_kw = self.min_max_load_kw[load_name][tp_cnt-2] dss.Loads.kW(new_kw) if not dss.Loads.Next() > 0: break self.dssSolver.Solve() self._simulation.RunStep(self._step) if not dss.Solution.Converged(): self.logger.info("Write upgrades before Convergence Error in debug_upgrades.dss") self.write_upgrades_to_file( output_path=os.path.join(self.config["Outputs"], "debug_upgrades.dss")) if raise_exception: raise OpenDssConvergenceError("OpenDSS solution did not converge") else: return False for b in self.all_bus_names: dss.Circuit.SetActiveBus(b) bus_v = dss.Bus.puVmagAngle()[::2] # Select that bus voltage of the three phases which is outside bounds the most, # else if everything is within bounds use nominal pu voltage. maxv_dev = 0 minv_dev = 0 if max(bus_v) > self.max_V_viol: self.max_V_viol = max(bus_v) self.busvmax = b if min(bus_v) < self.min_V_viol: self.min_V_viol = min(bus_v) if max(bus_v) > upper_limit: maxv = max(bus_v) maxv_dev = maxv - upper_limit if b.lower() not in self.buses_with_overvoltage_violations: self.buses_with_overvoltage_violations.append(b.lower()) if min(bus_v) < lower_limit: minv = min(bus_v) minv_dev = upper_limit - minv if maxv_dev > minv_dev: v_used = maxv num_nodes_counter += 1 severity_counter += maxv_dev if b.lower() not in self.buses_with_violations: self.buses_with_violations.append(b.lower()) self.buses_with_violations_pos[b.lower()] = self.pos_dict[b.lower()] if b.lower() not in self.buses_with_overvoltage_violations: self.buses_with_overvoltage_violations.append(b.lower()) elif minv_dev > maxv_dev: v_used = minv num_nodes_counter += 1 severity_counter += minv_dev if b.lower() not in self.buses_with_violations: self.buses_with_violations.append(b.lower()) self.buses_with_violations_pos[b.lower()] = self.pos_dict[b.lower()] if b.lower() not in self.buses_with_undervoltage_violations: self.buses_with_undervoltage_violations.append(b.lower()) else: v_used = self.config["nominal_pu_voltage"] if b not in self.nodal_violations_dict: self.nodal_violations_dict[b.lower()] = [v_used] elif b in self.nodal_violations_dict: self.nodal_violations_dict[b.lower()].append(v_used) elif len(self.other_load_dss_files)==0: for tp_cnt in range(len(self.config["tps_to_test"])): # First two tps are for disabled PV case if tp_cnt == 0 or tp_cnt == 1: dss.run_command("BatchEdit PVSystem..* Enabled=False") dss.run_command("set LoadMult = {LM}".format(LM=self.config["tps_to_test"][tp_cnt])) self.dssSolver.Solve() self._simulation.RunStep(self._step) if not dss.Solution.Converged(): self.logger.info("Write upgrades before Convergence Error in debug_upgrades.dss") self.write_upgrades_to_file( output_path=os.path.join(self.config["Outputs"], "debug_upgrades.dss")) if raise_exception: raise OpenDssConvergenceError("OpenDSS solution did not converge") else: return False if tp_cnt == 2 or tp_cnt == 3: dss.run_command("BatchEdit PVSystem..* Enabled=True") dss.run_command("set LoadMult = {LM}".format(LM=self.config["tps_to_test"][tp_cnt])) self.dssSolver.Solve() self._simulation.RunStep(self._step) if not dss.Solution.Converged(): self.logger.info("Write upgrades before Convergence Error in debug_upgrades.dss") self.write_upgrades_to_file( output_path=os.path.join(self.config["Outputs"], "debug_upgrades.dss")) if raise_exception: raise OpenDssConvergenceError("OpenDSS solution did not converge") else: return False for b in self.all_bus_names: dss.Circuit.SetActiveBus(b) bus_v = dss.Bus.puVmagAngle()[::2] # Select that bus voltage of the three phases which is outside bounds the most, # else if everything is within bounds use nominal pu voltage. maxv_dev = 0 minv_dev = 0 if max(bus_v) > self.max_V_viol: self.max_V_viol = max(bus_v) self.busvmax = b if min(bus_v) < self.min_V_viol: self.min_V_viol = min(bus_v) if max(bus_v) > upper_limit: maxv = max(bus_v) maxv_dev = maxv - upper_limit if b.lower() not in self.buses_with_overvoltage_violations: self.buses_with_overvoltage_violations.append(b.lower()) if min(bus_v) < lower_limit: minv = min(bus_v) minv_dev = upper_limit - minv if maxv_dev > minv_dev: v_used = maxv num_nodes_counter += 1 severity_counter += maxv_dev if b.lower() not in self.buses_with_violations: self.buses_with_violations.append(b.lower()) self.buses_with_violations_pos[b.lower()] = self.pos_dict[b.lower()] if b.lower() not in self.buses_with_overvoltage_violations: self.buses_with_overvoltage_violations.append(b.lower()) elif minv_dev > maxv_dev: v_used = minv num_nodes_counter += 1 severity_counter += minv_dev if b.lower() not in self.buses_with_violations: self.buses_with_violations.append(b.lower()) self.buses_with_violations_pos[b.lower()] = self.pos_dict[b.lower()] if b.lower() not in self.buses_with_undervoltage_violations: self.buses_with_undervoltage_violations.append(b.lower()) else: v_used = self.config["nominal_pu_voltage"] if b not in self.nodal_violations_dict: self.nodal_violations_dict[b.lower()] = [v_used] elif b in self.nodal_violations_dict: self.nodal_violations_dict[b.lower()].append(v_used) self.severity_indices = [num_nodes_counter, severity_counter, num_nodes_counter * severity_counter] return True
[docs] def plot_violations(self): #plt.figure(figsize=(8, 7)) plt.figure(figsize=(40, 40), dpi=10) plt.clf() numV = len(self.buses_with_violations) plt.title("Number of buses in the feeder with voltage violations: {}".format(numV)) ec = nx.draw_networkx_edges(self.G, pos=self.pos_dict, alpha=1.0, width=0.3) ld = nx.draw_networkx_nodes(self.G, pos=self.pos_dict, nodelist=self.nodes_list, node_size=2, node_color='b') # Show buses with violations if len(self.buses_with_violations) > 0: m = nx.draw_networkx_nodes(self.G, pos=self.buses_with_violations_pos, nodelist=self.buses_with_violations, node_size=10, node_color='r') plt.axis("off") plt.savefig(os.path.join(self.config["Outputs"],"Nodal_violations_{}.pdf".format(str(self.plot_violations_counter)))) self.plot_violations_counter+=1
[docs] def get_capacitor_state(self): # TODO: How to figure out whether cap banks are 3 phase, 2 phase or 1 phase. 1 phase caps will have LN voltage self.cap_correct_PTratios = {} self.cap_initial_settings = {} dss.Capacitors.First() while True: name = dss.Capacitors.Name() # Get original cap bank control settings if dss.CapControls.Count() > 0: dss.CapControls.First() while True: cap_name = dss.CapControls.Capacitor() cap_type = dss.Properties.Value("type") if cap_name == name and cap_type.lower().startswith("volt"): self.cap_initial_settings[name] = [dss.CapControls.ONSetting(), dss.CapControls.OFFSetting()] if not dss.CapControls.Next() > 0: break dss.Circuit.SetActiveElement("Capacitor." + name) cap_bus = dss.CktElement.BusNames()[0].split(".")[0] dss.Circuit.SetActiveBus(cap_bus) cap_kv = float(dss.Bus.kVBase()) dss.Circuit.SetActiveElement("Capacitor." + name) PT_ratio = (cap_kv * 1000) / (self.config["nominal_voltage"]) self.cap_correct_PTratios[name] = PT_ratio if not dss.Capacitors.Next() > 0: break
[docs] def correct_cap_bank_settings(self): # TODO: Add a function to sweep through possible capacitor bank settings caps_with_control = [] cap_on_settings_check = {} # Correct settings of those cap banks for which cap control object is available if dss.CapControls.Count() > 0: dss.CapControls.First() while True: name = dss.CapControls.Name() cap_name = dss.CapControls.Capacitor() caps_with_control.append(cap_name) orig_sett = '' if dss.Properties.Value("type").lower() == "voltage": orig_sett = " !original" control_command = "Edit CapControl.{cc} PTRatio={pt} Type={tp} ONsetting={o} OFFsetting={of}" \ " PTphase={ph} Delay={d} DelayOFF={dof} DeadTime={dt} enabled=True".format( cc=name, pt=self.cap_correct_PTratios[cap_name], tp=self.cap_control, o=self.capON, of=self.capOFF, ph=self.PTphase, d=self.capONdelay, dof=self.capOFFdelay, dt=self.capdeadtime ) control_command = control_command + orig_sett cap_on_settings_check[cap_name] = self.capON dss.run_command(control_command) self.dssSolver.Solve() self._simulation.RunStep(self._step) pass_flag = True pass_flag = self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit, raise_exception=False) # If pass_flag returned false, means it had convergence error if not pass_flag: # change command new_control_command = self.edit_capacitor_settings_for_convergence(control_command) dss.run_command(new_control_command) self.dssSolver.Solve() self._simulation.RunStep(self._step) self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit, raise_exception=True) control_command = new_control_command self.write_dss_file(control_command) if not dss.CapControls.Next() > 0: break # if there are caps without cap control add a cap control if dss.Capacitors.Count() > len(caps_with_control): dss.Capacitors.First() while True: cap_name = dss.Capacitors.Name() if cap_name not in caps_with_control: cap_ctrl_name = "capctrl" + cap_name cap_bus = dss.CktElement.BusNames()[0].split(".")[0] # Get line to be controlled dss.Lines.First() while True: Line_name = dss.Lines.Name() bus1 = dss.Lines.Bus1().split(".")[0] if bus1 == cap_bus: break if not dss.Lines.Next() > 0: break control_command = "New CapControl.{cc} element=Line.{el} terminal={trm} capacitor={cbank} PTRatio={pt} Type={tp}" \ " ONsetting={o} OFFsetting={of} PTphase={ph} Delay={d} DelayOFF={dof} DeadTime={dt} enabled=True".format( cc=cap_ctrl_name, el=Line_name, trm=self.terminal, cbank=cap_name, pt=self.cap_correct_PTratios[cap_name], tp=self.cap_control, o=self.capON, of=self.capOFF, ph=self.PTphase, d=self.capONdelay, dof=self.capOFFdelay, dt=self.capdeadtime ) if len(self.cap_initial_settings) > 0 and cap_ctrl_name not in self.cap_initial_settings: self.cap_initial_settings[cap_name] = [self.capON, self.capOFF] dss.run_command(control_command) cap_on_settings_check[cap_name] = self.capON self.dssSolver.Solve() self._simulation.RunStep(self._step) pass_flag = True pass_flag = self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit, raise_exception=False) # If pass_flag returned false, means it had convergence error if not pass_flag: # change command new_control_command = self.edit_capacitor_settings_for_convergence(control_command) dss.run_command(new_control_command) self.dssSolver.Solve() self._simulation.RunStep(self._step) self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit, raise_exception=True) control_command = new_control_command self.write_dss_file(control_command) dss.Circuit.SetActiveElement("Capacitor." + cap_name) if not dss.Capacitors.Next() > 0: break self.dssSolver.Solve() self._simulation.RunStep(self._step) # Check whether settings have been applied or not if dss.CapControls.Count() > 0: dss.CapControls.First() while True: cap_on = dss.CapControls.ONSetting() name = dss.CapControls.Name() if abs(cap_on - cap_on_settings_check[cap_name]) > 0.1: self.logger.info("Settings for cap bank %s not implemented", cap_name) if not dss.CapControls.Next() > 0: break # self.get_nodal_violations() self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit)
[docs] def get_viols_with_initial_cap_settings(self): if len(self.cap_initial_settings) > 0: for key, vals in self.cap_initial_settings.items(): dss.CapControls.First() while True: cap_name = dss.CapControls.Capacitor() if cap_name == key: dss.CapControls.ONSetting(vals[0]) dss.CapControls.OFFSetting(vals[1]) if not dss.CapControls.Next() > 0: break self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) key = "original" self.cap_sweep_res_dict[key] = self.severity_indices[2]
[docs] def cap_settings_sweep(self, upper_limit, lower_limit): # This function increases differences between cap ON and OFF voltages in user defined increments, # default 1 volt, until upper and lower bounds are reached. self.cap_sweep_res_dict = {} self.get_viols_with_initial_cap_settings() self.cap_on_setting = self.capON self.cap_off_setting = self.capOFF self.cap_control_gap = self.config["cap_sweep_voltage_gap"] while self.cap_on_setting > lower_limit * self.config[ "nominal_voltage"] or self.cap_off_setting < upper_limit * self.config[ "nominal_voltage"]: # Apply cap ON and OFF settings and determine their impact key = "{}_{}".format(self.cap_on_setting, self.cap_off_setting) dss.CapControls.First() while True: cc_name = dss.CapControls.Name() dss.run_command("Edit CapControl.{cc} ONsetting={o} OFFsetting={of}".format( cc=cc_name, o=self.cap_on_setting, of=self.cap_off_setting )) self.dssSolver.Solve() self._simulation.RunStep(self._step) if not dss.CapControls.Next() > 0: break self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.cap_sweep_res_dict[key] = self.severity_indices[2] if (self.cap_on_setting - self.cap_control_gap / 2) <= lower_limit * self.config[ "nominal_voltage"]: self.cap_on_setting = lower_limit * self.config["nominal_voltage"] else: self.cap_on_setting = self.cap_on_setting - self.cap_control_gap / 2 if (self.cap_off_setting + self.cap_control_gap / 2) >= upper_limit * self.config[ "nominal_voltage"]: self.cap_off_setting = upper_limit * self.config["nominal_voltage"] else: self.cap_off_setting = self.cap_off_setting + self.cap_control_gap / 2 self.apply_best_capsetting(upper_limit=self.upper_limit)
[docs] def apply_orig_cap_setting(self): for key, vals in self.cap_initial_settings.items(): dss.CapControls.First() while True: cap_name = dss.CapControls.Capacitor() if cap_name == key: command_string = "Edit CapControl.{ccn} ONsetting={o} OFFsetting={of} !original".format( ccn=dss.CapControls.Name(), o=vals[0], of=vals[1] ) dss.run_command(command_string) self.write_dss_file(command_string) if not dss.CapControls.Next() > 0: break self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit)
[docs] def apply_best_capsetting(self, upper_limit): best_setting = '' # Start with assumption that each node has a violation at all time points and each violation if outside bounds # by upper voltage limit - basically the maximum possible severity min_severity = pow(len(self.all_bus_names), 2) * len(self.config["tps_to_test"]) * upper_limit for key, val in self.cap_sweep_res_dict.items(): if val < min_severity: min_severity = val best_setting = key # Apply best settings which give minimum severity index if best_setting == "original": self.apply_orig_cap_setting() else: best_on_setting = best_setting.split("_")[0] best_off_setting = best_setting.split("_")[1] dss.CapControls.First() while True: cc_name = dss.CapControls.Name() command_string = ("Edit CapControl.{cc} ONsetting={o} OFFsetting={of}".format( cc=cc_name, o=best_on_setting, of=best_off_setting )) dss.run_command(command_string) self.write_dss_file(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) if not dss.CapControls.Next() > 0: break self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit)
[docs] def edit_capacitor_settings_for_convergence(self, control_command): new_deadtime = 50 new_delay = 50 self.capON = round((self.config["nominal_voltage"] - (self.config["cap_sweep_voltage_gap"]+1) / 2), 1) self.capOFF = round((self.config["nominal_voltage"] + (self.config["cap_sweep_voltage_gap"]+1) / 2), 1) self.logger.info("Changed Initial On and Off Cap settings to avoid convergence issues ") new_capON = self.capON new_capOFF = self.capOFF new_control_command = control_command # self.remove_line_from_dss_file(control_command) # remove command that caused convergence issue control_command = control_command.replace('New', 'Edit') control_command = re.sub("enabled=True", "enabled=False", control_command) dss.run_command(control_command) # disable and run previous control command new_control_command = re.sub("DeadTime=\d+", 'DeadTime=' + str(new_deadtime), new_control_command) new_control_command = re.sub("Delay=\d+", 'Delay=' + str(new_delay), new_control_command) new_control_command = re.sub("ONsetting=\d+\.\d+", 'ONsetting=' + str(new_capON), new_control_command) new_control_command = re.sub("OFFsetting=\d+\.\d+", 'OFFsetting=' + str(new_capOFF), new_control_command) return new_control_command
[docs] def write_dss_file(self, device_command): self.dss_upgrades.append(device_command + "\n") return
[docs] def write_upgrades_to_file(self, output_path=None): if output_path is None: output_path = os.path.join(self.config["Outputs"], "voltage_upgrades.dss") with open(output_path, "w") as datafile: for line in self.dss_upgrades: datafile.write(line) return
[docs] def reg_controls_sweep(self, upper_limit, lower_limit): self.vsps = [] v = lower_limit * self.config["nominal_voltage"] while v < upper_limit * self.config["nominal_voltage"]: self.vsps.append(v) v += self.config["reg_v_delta"] for reg_sp in self.vsps: for bandw in self.config["reg_control_bands"]: dss.RegControls.First() while True: regctrl_name = dss.RegControls.Name() xfmr = dss.RegControls.Transformer() dss.Circuit.SetActiveElement("Transformer.{}".format(xfmr)) xfmr_buses = dss.CktElement.BusNames() xfmr_b1 = xfmr_buses[0].split(".")[0] xfmr_b2 = xfmr_buses[1].split(".")[0] # Skipping over substation LTC if it exists # for n, d in self.G.in_degree().items(): # if d == 0: # sourcebus = n sourcebus = self.source if xfmr_b1.lower() == sourcebus.lower() or xfmr_b2.lower() == sourcebus.lower(): dss.Circuit.SetActiveElement("Regcontrol.{}".format(regctrl_name)) if not dss.RegControls.Next() > 0: break continue dss.Circuit.SetActiveElement("Regcontrol.{}".format(regctrl_name)) command_string = "Edit Regcontrol.{rn} vreg={sp} band={b}".format( rn=regctrl_name, sp=reg_sp, b=bandw ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) if not dss.RegControls.Next() > 0: break self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.reg_sweep_viols["{}_{}".format(str(reg_sp), str(bandw))] = self.severity_indices[2] self.apply_best_regsetting(upper_limit=self.upper_limit)
[docs] def apply_best_regsetting(self, upper_limit): # TODO: Remove substation LTC from the settings sweep best_setting = '' # Start with assumption that each node has a violation at all time points and each violation if outside bounds # by upper voltage limit - basically the maximum possible severity min_severity = pow(len(self.all_bus_names), 2) * len(self.config["tps_to_test"]) * upper_limit for key, val in self.reg_sweep_viols.items(): if val < min_severity: min_severity = val best_setting = key if best_setting == "original": self.apply_orig_reg_setting() else: self.v_sp = best_setting.split("_")[0] self.reg_band = best_setting.split("_")[1] dss.RegControls.First() while True: reg_ctrl_nm = dss.RegControls.Name() xfmr = dss.RegControls.Transformer() dss.Circuit.SetActiveElement("Transformer.{}".format(xfmr)) xfmr_buses = dss.CktElement.BusNames() xfmr_b1 = xfmr_buses[0].split(".")[0] xfmr_b2 = xfmr_buses[1].split(".")[0] dss.Circuit.SetActiveElement("Regcontrol.{}".format(reg_ctrl_nm)) # Skipping over substation LTC if it exists # for n, d in self.G.in_degree().items(): # if d == 0: # sourcebus = n sourcebus = self.source if xfmr_b1.lower() == sourcebus.lower() or xfmr_b2.lower() == sourcebus.lower(): dss.Circuit.SetActiveElement("Regcontrol.{}".format(reg_ctrl_nm)) if not dss.RegControls.Next() > 0: break continue command_string = "Edit RegControl.{rn} vreg={sp} band={b}".format( rn=reg_ctrl_nm, sp=self.v_sp, b=self.reg_band, ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) if self.write_flag == 1: self.write_dss_file(command_string) if not dss.RegControls.Next() > 0: break
[docs] def apply_orig_reg_setting(self): for key, vals in self.initial_regctrls_settings.items(): self.v_sp = vals[0] self.reg_band = vals[1] dss.Circuit.SetActiveElement("RegControl.{}".format(key)) command_string = "Edit RegControl.{rn} vreg={sp} band={b} !original".format( rn=key, sp=vals[0], b=vals[1], ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) if self.write_flag == 1: self.write_dss_file(command_string)
[docs] def add_substation_LTC(self): # This function identifies whether or not a substation LTC exists - if not adds one along with a new sub xfmr # if required - if one exists corrects its settings # Identify source bus # It seems that networkx in a directed graph only counts edges incident on a node towards degree. # This is why source bus is the only bus which has a degree of zero # for n, d in self.G.in_degree().items(): # if d == 0: # self.source = n # Identify whether a transformer is connected to this bus or not self.subxfmr = '' dss.Transformers.First() while True: bus_names = dss.CktElement.BusNames() from_bus = bus_names[0].split('.')[0].lower() to_bus = bus_names[1].split('.')[0].lower() if from_bus == self.source or to_bus == self.source: self.subxfmr = dss.Transformers.Name() self.sub_LTC_bus = to_bus break if not dss.Transformers.Next() > 0: break if self.subxfmr == '': # Add new transformer if no transformer is connected to source bus, then add LTC self.add_new_xfmr(self.source) self.write_flag = 1 self.add_new_regctrl(self.source) elif self.subxfmr != '': # add LTC onto existing substation transformer self.write_flag = 1 self.add_new_regctrl(self.sub_LTC_bus) self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) return
[docs] def add_new_xfmr(self, node): # If substation does not have a transformer add a transformer at the source bus so a new reg # control object may be created - after the transformer and reg control have been added the feeder would have # to be re compiled as system admittance matrix has changed # Find line to which this node is connected to # node = node.lower() degree = 1 if node.lower() == self.source.lower(): degree = 0 dss.Lines.First() while True: # For sub LTC if degree == 0: if dss.Lines.Bus1().split(".")[0] == node: bus1 = dss.Lines.Bus1() bus2 = dss.Lines.Bus2() new_node = "Regctrl_" + bus2 xfmr_name = "New_xfmr_" + node line_name = dss.Lines.Name() phases = dss.Lines.Phases() amps = dss.CktElement.NormalAmps() dss.Circuit.SetActiveBus(bus1) x = dss.Bus.X() y = dss.Bus.Y() dss.Circuit.SetActiveBus(bus2) kV_node = dss.Bus.kVBase() if phases > 1: kV_DT = kV_node * 1.732 kVA = int(kV_DT * amps * 1.1) # 10% over sized transformer - ideally we # would use an auto transformer which would need a much smaller kVA rating command_string = "New Transformer.{xfn} phases={p} windings=2 buses=({b1},{b2}) conns=({cntp},{cntp})" \ " kvs=({kv},{kv}) kvas=({kva},{kva}) xhl=0.001 wdg=1 %r=0.001 wdg=2 %r=0.001" \ " Maxtap=1.1 Mintap=0.9 enabled=True".format( xfn=xfmr_name, p=phases, b1=bus1, b2=new_node, cntp=self.reg_conn, kv=kV_DT, kva=kVA ) elif phases == 1: kVA = int(kV_node * amps * 1.1) # 10% over sized transformer - ideally we # would use an auto transformer which would need a much smaller kVA rating # make bus1 of line as reg ctrl node command_string = "New Transformer.{xfn} phases={p} windings=2 buses=({b1},{b2}) conns=({cntp},{cntp})" \ " kvs=({kv},{kv}) kvas=({kva},{kva}) xhl=0.001 wdg=1 %r=0.001 wdg=2 %r=0.001" \ " Maxtap=1.1 Mintap=0.9 enabled=True".format( xfn=xfmr_name, p=phases, b1=bus1, b2=new_node, cntp=self.reg_conn, kv=kV_node, kva=kVA ) control_command = "Edit Line.{} bus1={}".format(line_name, new_node) dss.run_command(control_command) if self.write_flag == 1: self.write_dss_file(control_command) dss.run_command(command_string) if self.write_flag == 1: self.write_dss_file(command_string) # Update system admittance matrix dss.run_command("CalcVoltageBases") dss.Circuit.SetActiveBus(new_node) dss.Bus.X(x) dss.Bus.Y(y) if self.write_flag == 1: self.write_dss_file("//{},{},{}".format(new_node.split(".")[0], x, y)) self.dssSolver.Solve() self._simulation.RunStep(self._step) self.generate_nx_representation() dss.Circuit.SetActiveElement("Line." + line_name) break # For regulator elif degree > 0: if dss.Lines.Bus2().split(".")[0] == node: bus1 = dss.Lines.Bus1() bus2 = dss.Lines.Bus2() new_node = "Regctrl_" + bus2 xfmr_name = "New_xfmr_" + node line_name = dss.Lines.Name() phases = dss.Lines.Phases() amps = dss.CktElement.NormalAmps() dss.Circuit.SetActiveBus(bus2) x = dss.Bus.X() y = dss.Bus.Y() kV_node = dss.Bus.kVBase() if phases > 1: kV_DT = kV_node * 1.732 kVA = int(kV_DT * amps * 1.1) # 10% over sized transformer - ideally we # would use an auto transformer which would need a much smaller kVA rating command_string = "New Transformer.{xfn} phases={p} windings=2 buses=({b1},{b2}) conns=(wye,wye)" \ " kvs=({kv},{kv}) kvas=({kva},{kva}) xhl=0.001 wdg=1 %r=0.001 wdg=2 %r=0.001" \ " Maxtap=1.1 Mintap=0.9 enabled=True".format( xfn=xfmr_name, p=phases, b1=new_node, b2=bus2, kv=kV_DT, kva=kVA ) elif phases == 1: kVA = int(kV_node * amps * 1.1) # 10% over sized transformer - ideally we # would use an auto transformer which would need a much smaller kVA rating command_string = "New Transformer.{xfn} phases={p} windings=2 buses=({b1},{b2}) conns=(wye,wye)" \ " kvs=({kv},{kv}) kvas=({kva},{kva}) xhl=0.001 wdg=1 %r=0.001 wdg=2 %r=0.001" \ " Maxtap=1.1 Mintap=0.9 enabled=True".format( xfn=xfmr_name, p=phases, b1=new_node, b2=bus2, kv=kV_node, kva=kVA ) control_command = "Edit Line.{} bus2={}".format(line_name, new_node) dss.run_command(control_command) if self.write_flag == 1: self.write_dss_file(control_command) dss.run_command(command_string) if self.write_flag == 1: self.write_dss_file(command_string) # Update system admittance matrix dss.run_command("CalcVoltageBases") dss.Circuit.SetActiveBus(new_node) dss.Bus.X(x) dss.Bus.Y(y) if self.write_flag == 1: self.write_dss_file("//{},{},{}".format(new_node.split(".")[0], x, y)) self.dssSolver.Solve() self._simulation.RunStep(self._step) self.generate_nx_representation() dss.Circuit.SetActiveElement("Line." + line_name) break if not dss.Lines.Next() > 0: break return
[docs] def add_new_regctrl(self, node): # Identify whether or not a reg contrl exists at the transformer connected to this bus - a transformer should # definitely exist by now. If it does correct its settings else add a new reg ctrl with correct settings # If transformer exists check if it already has a reg control object if dss.Transformers.Count() > 0: dss.Transformers.First() while True: # Identify transformer connected to this node bus_prim = dss.CktElement.BusNames()[0].split(".")[0] bus_sec = dss.CktElement.BusNames()[1].split(".")[0] if bus_prim == node or bus_sec == node: xfmr_name = dss.Transformers.Name() phases = dss.CktElement.NumPhases() dss.Circuit.SetActiveBus(node) # node info is only used to get correct kv values kV = dss.Bus.kVBase() winding = self.LTC_wdg vreg = self.LTC_setpoint reg_delay = self.LTC_delay deadband = self.LTC_band pt_ratio = kV * 1000 / (self.config["nominal_voltage"]) xfmr_regctrl = '' # Identify whether a reg control exists on this transformer if dss.RegControls.Count() > 0: dss.RegControls.First() while True: xfmr_name_reg = dss.RegControls.Transformer() if xfmr_name_reg == xfmr_name: # if reg control already exists correct its settings xfmr_regctrl = dss.RegControls.Name() command_string = "Edit RegControl.{rn} transformer={tn} winding={w} vreg={sp} ptratio={rt} band={b} " \ "enabled=true delay={d}".format( rn=xfmr_regctrl, tn=xfmr_name, w=winding, sp=vreg, rt=pt_ratio, b=deadband, d=reg_delay ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) dss.run_command("Calcvoltagebases") # add this to a dss_upgrades.dss file if self.write_flag == 1: self.write_dss_file(command_string) # check for violations # self.get_nodal_violations() self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) break if not dss.RegControls.Next() > 0: break if xfmr_regctrl == '': # if reg control does not exist on the transformer add one xfmr_regctrl = "New_regctrl_" + node command_string = "New RegControl.{rn} transformer={tn} winding={w} vreg={sp} ptratio={rt} band={b} " \ "enabled=true delay={d}".format( rn=xfmr_regctrl, tn=xfmr_name, w=winding, sp=vreg, rt=pt_ratio, b=deadband, d=reg_delay ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) dss.run_command("CalcVoltageBases") # add this to a dss_upgrades.dss file if self.write_flag == 1: self.write_dss_file(command_string) # check for violations # self.get_nodal_violations() self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) break dss.Circuit.SetActiveElement("Transformer." + xfmr_name) if not dss.Transformers.Next() > 0: break return
[docs] def LTC_controls_sweep(self, upper_limit, lower_limit): self.vsps = [] v = lower_limit * self.config["nominal_voltage"] while v < upper_limit * self.config["nominal_voltage"]: self.vsps.append(v) v += self.config["reg_v_delta"] for reg_sp in self.vsps: for bandw in self.config["reg_control_bands"]: dss.RegControls.First() while True: regctrl_name = dss.RegControls.Name() xfmr = dss.RegControls.Transformer() dss.Circuit.SetActiveElement("Transformer.{}".format(xfmr)) xfmr_buses = dss.CktElement.BusNames() xfmr_b1 = xfmr_buses[0].split(".")[0] xfmr_b2 = xfmr_buses[1].split(".")[0] # Skipping over substation LTC if it exists # for n, d in self.G.in_degree().items(): # if d == 0: # sourcebus = n sourcebus = self.source if xfmr_b1.lower() == sourcebus.lower() or xfmr_b2.lower() == sourcebus.lower(): dss.Circuit.SetActiveElement("Regcontrol.{}".format(regctrl_name)) command_string = "Edit Regcontrol.{rn} vreg={sp} band={b}".format( rn=regctrl_name, sp=reg_sp, b=bandw ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) pass_flag = True pass_flag = self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit, raise_exception=False) dss.Circuit.SetActiveElement("Regcontrol.{}".format(regctrl_name)) if not dss.RegControls.Next() > 0: break self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.subLTC_sweep_viols["{}_{}".format(str(reg_sp), str(bandw))] = self.severity_indices[2] self.apply_best_LTCsetting(upper_limit=self.upper_limit)
[docs] def apply_best_LTCsetting(self, upper_limit): # TODO: Remove substation LTC from the settings sweep best_setting = '' # Start with assumption that each node has a violation at all time points and each violation if outside bounds # by upper voltage limit - basically the maximum possible severity min_severity = 10000000000000 # TODO - below logic for min_severity was used previously - however, error cases were encountered # for some feeders due to min_severity being not large enough # min_severity = pow(len(self.all_bus_names), 2) * len(self.config["tps_to_test"]) * upper_limit self.logger.info(f"Severity: {pow(len(self.all_bus_names), 2) * len(self.config['tps_to_test']) * upper_limit}") self.logger.debug(self.subLTC_sweep_viols) for key, val in self.subLTC_sweep_viols.items(): if val < min_severity: min_severity = val best_setting = key if best_setting == "original": self.apply_orig_LTC_setting() else: self.logger.debug("Best_setting: %s", best_setting) v_sp = best_setting.split("_")[0] reg_band = best_setting.split("_")[1] dss.RegControls.First() while True: reg_ctrl_nm = dss.RegControls.Name() xfmr = dss.RegControls.Transformer() dss.Circuit.SetActiveElement("Transformer.{}".format(xfmr)) xfmr_buses = dss.CktElement.BusNames() xfmr_b1 = xfmr_buses[0].split(".")[0] xfmr_b2 = xfmr_buses[1].split(".")[0] # # Skipping over substation LTC if it exists # for n, d in self.G.in_degree().items(): # if d == 0: # sourcebus = n sourcebus = self.source if xfmr_b1.lower() == sourcebus.lower() or xfmr_b2.lower() == sourcebus.lower(): dss.Circuit.SetActiveElement("Regcontrol.{}".format(reg_ctrl_nm)) command_string = "Edit RegControl.{rn} vreg={sp} band={b}".format( rn=reg_ctrl_nm, sp=v_sp, b=reg_band, ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) pass_flag = True pass_flag = self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit, raise_exception=False) # TODO : add code to change settings if there is a convergence error if pass_flag: self.write_dss_file(command_string) else: self.apply_orig_LTC_setting() dss.Circuit.SetActiveElement("Regcontrol.{}".format(reg_ctrl_nm)) if not dss.RegControls.Next() > 0: break self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit)
[docs] def apply_orig_LTC_setting(self): for key, vals in self.initial_subLTC_settings.items(): dss.Circuit.SetActiveElement("RegControl.{}".format(key)) command_string = "Edit RegControl.{rn} vreg={sp} band={b} !original".format( rn=key, sp=vals[0], b=vals[1], ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) self.write_dss_file(command_string)
[docs] def get_shortest_path(self): new_graph = self.G.to_undirected() precal_paths = [] self.UT_paths_dict = {} # Get upper triangular distance matrix - reduces computational time by half for bus1 in self.buses_with_violations: self.UT_paths_dict[bus1] = [] for bus_n in self.buses_with_violations: if bus_n == bus1: path_length = 0.0 elif bus_n in precal_paths: continue else: path = nx.shortest_path(new_graph, source=bus1, target=bus_n) path_length = 0.0 for nodes_count in range(len(path) - 1): path_length += float(new_graph[path[nodes_count + 1]][path[nodes_count]]['length']) self.UT_paths_dict[bus1].append(round(path_length, 3)) precal_paths.append(bus1)
[docs] def get_full_distance_dict(self): self.square_array = [] self.square_dict = {} self.cluster_nodes_list = [] temp_nodes_list = [] ll = [] max_length = 0 for key, values in self.UT_paths_dict.items(): self.cluster_nodes_list.append(key) if len(values) > max_length: max_length = len(values) # Create a square dict with zeros for lower triangle values for key, values in self.UT_paths_dict.items(): temp_nodes_list.append(key) temp_list = [] if len(values) < max_length: new_items_req = max_length - len(values) for items_cnt in range(0, new_items_req, 1): temp_list.append(0.0) for item in values: temp_list.append(float(item)) self.square_dict[key] = temp_list # Replace lower triangle zeros with upper triangle values key_count = 0 for key, values in self.UT_paths_dict.items(): for items_count in range(len(values)): self.square_dict[temp_nodes_list[items_count]][key_count] = values[items_count] key_count += 1 temp_nodes_list.remove(key) # from dict create a list of lists for key, values in self.square_dict.items(): ll.append(values) # Create numpy array from list of lists self.square_array = np.array(ll)
[docs] def plot_heatmap_distmatrix(self): plt.figure(figsize=(7, 7)) ax = sns.heatmap(self.square_array, linewidth=0.5) plt.title("Distance matrix of nodes with violations") plt.savefig(os.path.join(self.config["Outputs"],"Nodal_violations_heatmap.pdf"))
[docs] def cluster_square_array(self): # Clustering the distance matrix into clusters equal to optimal clusters if self.config["create_topology_plots"]: self.plot_heatmap_distmatrix() for self.optimal_clusters in range(1, self.max_regs + 1, 1): self.no_reg_flag = 0 self.clusters_dict = {} model = AgglomerativeClustering(n_clusters=self.optimal_clusters, affinity='euclidean', linkage='ward') model.fit(self.square_array) labels = model.labels_ for lab in range(len(labels)): if labels[lab] not in self.clusters_dict: self.clusters_dict[labels[lab]] = [self.cluster_nodes_list[lab]] else: self.clusters_dict[labels[lab]].append(self.cluster_nodes_list[lab]) self.identify_correct_reg_node() self.add_new_reg_common_nodes(upper_limit=self.upper_limit) if self.no_reg_flag == 1: continue self.write_flag = 0 self.reg_controls_sweep(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.write_flag = 1 self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) self.cluster_optimal_reg_nodes[self.optimal_clusters] = [self.severity_indices[2], [self.v_sp, self.reg_band], []] # Store all optimal nodes for a given number of clusters for key, vals in self.upstream_reg_node.items(): self.cluster_optimal_reg_nodes[self.optimal_clusters][2].append(vals) if self.config["create_topology_plots"]: self.plot_created_clusters() self.plot_violations() self.logger.info("max_V_viol=%s, min_V_viol=%s, severity_indices=%s", self.max_V_viol, self.min_V_viol, self.severity_indices) self.disable_regctrl_current_cluster() if (len(self.buses_with_violations)) == 0: self.logger.info("All nodal violations have been removed successfully.....quitting") break
[docs] def disable_regctrl_current_cluster(self): disable_index = self.optimal_clusters if disable_index in self.cluster_optimal_reg_nodes: for node in self.cluster_optimal_reg_nodes[disable_index][2]: self.write_flag = 0 self.disable_added_xfmr(node) self.write_flag = 1 command_string = "Edit RegControl.{rn} enabled=False".format( rn="New_regctrl_" + node ) dss.run_command(command_string) # self.write_dss_file(command_string) return
[docs] def disable_added_xfmr(self, node): # Unfortunately since OpenDSS disables by transformer by opening the circuit instead of creating a short circuit, # this function will remove the transformer by first disabling it, then it will connect the line properly to # remove the islands # Substation will always have a xfmr by this point so only regulator transformers have to be removed transformer_name = "New_xfmr_" + node dss.Transformers.First() while True: if dss.Transformers.Name().lower() == transformer_name.lower(): prim_bus = dss.CktElement.BusNames()[0].split(".")[0] sec_bus = dss.CktElement.BusNames()[1] command_string = "Edit Transformer.{xfmr} enabled=False".format(xfmr=transformer_name) dss.run_command(command_string) if self.write_flag == 1: self.write_dss_file(command_string) command_string = "Edit Transformer.{xfmr} buses=({b1},{b2})".format( xfmr=transformer_name, b1=dss.CktElement.BusNames()[0], b2=dss.CktElement.BusNames()[0] ) dss.run_command(command_string) if self.write_flag == 1: self.write_dss_file(command_string) dss.Lines.First() while True: if dss.Lines.Bus2().split(".")[0].lower() == prim_bus.lower(): command_string = "Edit Line.{ln} bus2={b}".format( ln=dss.Lines.Name(), b=sec_bus ) dss.run_command(command_string) if self.write_flag == 1: self.write_dss_file(command_string) # Update system admittance matrix dss.run_command("CalcVoltageBases") self.dssSolver.Solve() self._simulation.RunStep(self._step) self.generate_nx_representation() break if not dss.Lines.Next() > 0: break break if not dss.Transformers.Next() > 0: break return
[docs] def add_new_reg_common_nodes(self, upper_limit): # Identify whether a transformer exists at this node or not. If yes simply add a new reg control - # in fact calling the add_new_regctrl function will automatically check whether a reg control exists or not # - so only thing to be ensured is that a transformer should exist - for next time when this function is called # a new set of clusters will be passed self.upstream_reg_node = {} for cluster, common_nodes in self.upstream_nodes_dict.items(): self.vdev_cluster_nodes = {} for node in common_nodes: continue_flag = 0 # Here do not add a new reg control to source bus as it already has a LTC # for n, d in self.G.in_degree().items(): # if n == node and d==0: if node.lower() == self.source.lower(): continue_flag = 1 if continue_flag == 1: continue dss.Transformers.First() xfmr_flag = 0 while True: xfmr_name = dss.Transformers.Name() # dss.Circuit.SetActiveElement("Transformer."+xfmr_name) prim_bus = dss.CktElement.BusNames()[0].split(".")[0] sec_bus = dss.CktElement.BusNames()[1].split(".")[0] if node == sec_bus or node == prim_bus: xfmr_flag = 1 break if not dss.Transformers.Next() > 0: break if xfmr_flag == 0: self.write_flag = 0 self.add_new_xfmr(node) # These are just trial settings and do not have to be written in the output file self.add_new_regctrl(node) self.write_flag = 1 elif xfmr_flag == 1: # The reason is that we are skipping over LTC node already, and all other other nodes with # pre-existing xfmrs will be primary to secondary DTs which we do not want to control as regs are # primarily in line and not on individual distribution transformers continue self.vdev_cluster_nodes[node] = self.severity_indices[2] # self.get_nodes_withV(node) # Now disable the added regulator control and remove the added transformer if xfmr_flag == 0: command_string = "Edit RegControl.{rn} enabled=No".format( rn="New_regctrl_" + node ) dss.run_command(command_string) self.write_flag = 0 self.disable_added_xfmr(node) self.write_flag = 1 # For a given cluster identify the node which leads to minimum number of buses with violations min_severity = 1000000000 # TODO - below logic for min_severity was used previously - however, error cases were encountered # for some feeders due to min_severity being not large enough # min_severity = pow(len(self.all_bus_names), 2) * len(self.config["tps_to_test"]) * upper_limit min_node = '' for key, value in self.vdev_cluster_nodes.items(): if value <= min_severity: min_severity = value min_node = key self.logger.info("Min node is: %s", min_node) # If no nodes is found break the loop and go to next number of clusters: if min_node == '': continue self.upstream_reg_node[cluster] = min_node # Since this is an optimal location add the transformer here - this transformer will stay as long as # self.optimal_clusters does not increment. If this parameter changes then all devices at nodes mentioned # in previous optimal cluster number in self.cluster_optimal_reg_nodes should be disabled self.write_flag = 0 self.add_new_xfmr(min_node) self.write_flag = 1 command_string = "Edit RegControl.{rn} enabled=True".format( rn="New_regctrl_" + min_node ) dss.run_command(command_string) self.dssSolver.Solve() self._simulation.RunStep(self._step) self.check_voltage_violations_multi_tps(upper_limit=self.upper_limit, lower_limit=self.lower_limit) # Even here we do not need to write out the setting as the only setting to be written would # self.write_dss_file(command_string) # if no reg control nodes are found then continue if len(self.upstream_reg_node) == 0: self.no_reg_flag = 1 return
[docs] def identify_correct_reg_node(self): # In this function the very first common upstream node and all upstream nodes for all members of the # cluster are stored # TODO: include some type of optimization - such as look at multiple upstream nodes and place where sum of # TODO: downstream node voltage deviations is minimum as long as it doesn't overlap with other clusters # Currently it only identifies the common upstream nodes for all cluster nodes self.upstream_nodes_dict = {} temp_graph = self.G new_graph = temp_graph.to_undirected() for key, items in self.clusters_dict.items(): paths_dict_cluster = {} common_nodes = [] for buses in items: path = nx.shortest_path(new_graph, source=self.source, target=buses) paths_dict_cluster[buses] = path for common_bus in path: flag = 1 for bus, paths in paths_dict_cluster.items(): if common_bus not in paths: flag = 0 break if flag == 1: common_nodes.append(common_bus) self.upstream_nodes_dict[key] = common_nodes # self.upstream_reg_node[key] = common_nodes[-1] return
[docs] def plot_created_clusters(self): plt.figure(figsize=(7, 7)) # Plots clusters and common paths from clusters to source plt.clf() self.pos_dict = nx.get_node_attributes(self.G, 'pos') ec = nx.draw_networkx_edges(self.G, pos=self.pos_dict, alpha=1.0, width=0.3) ld = nx.draw_networkx_nodes(self.G, pos=self.pos_dict, nodelist=self.cluster_nodes_list, node_size=2, node_color='b') # Show min V violations col = 0 try: for key, values in self.clusters_dict.items(): nodal_violations_pos = {} common_nodes_pos = {} reg_nodes_pos = {} for cluster_nodes in values: nodal_violations_pos[cluster_nodes] = self.pos_dict[cluster_nodes] for common_nodes in self.upstream_nodes_dict[key]: common_nodes_pos[common_nodes] = self.pos_dict[common_nodes] self.logger.info("%s", self.upstream_reg_node[key]) reg_nodes_pos[self.upstream_reg_node[key]] = self.pos_dict[self.upstream_reg_node[key]] nx.draw_networkx_nodes(self.G, pos=nodal_violations_pos, nodelist=values, node_size=5, node_color='C{}'.format(col)) nx.draw_networkx_nodes(self.G, pos=common_nodes_pos, nodelist=self.upstream_nodes_dict[key], node_size=5, node_color='C{}'.format(col), alpha=0.3) nx.draw_networkx_nodes(self.G, pos=reg_nodes_pos, nodelist=[self.upstream_reg_node[key]], node_size=25, node_color='r') col += 1 except: pass plt.axis("off") plt.title("All buses with violations grouped in {} clusters".format(self.optimal_clusters)) plt.savefig( os.path.join(self.config["Outputs"], "Cluster_{}_reglocations.pdf".format(str(self.optimal_clusters))))
[docs] def run(self, step, stepMax, simulation=None): self.logger.info('Running voltage upgrade post process') self._simulation = simulation self._step = step try: self._run() has_converged = self.has_converged error = self.error return step, has_converged, error finally: self._simulation = None self._step = None
[docs] def finalize(self): pass