Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/dpinney/omf
Browse files Browse the repository at this point in the history
  • Loading branch information
SaeedRazavi committed Mar 22, 2024
2 parents 337c4de + e09a238 commit 2f0fc27
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 136 deletions.
4 changes: 2 additions & 2 deletions omf/models/hostingCapacity.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
<style>td, th {padding:7 0 5 20;text-align: left;font-size:0.8em; border: 1px solid #cccccc;} </style>
<div id="output">
{% if allInputDataDict['runAmiAlgorithm'] == 'on' %}
<p class="reportTitle">AMI-Based Hosting Capacity Runtime </p>
<p class="reportTitle">AMI-Based Hosting Capacity Runtime ( H:M:S:MS ) </p>
<div id="AMI_runtime" class="tightContent">
<span style="border: 1px solid grey; padding: 3px;"> {{ allOutputDataDict['AMI_runtime']}} </span>
</div>
Expand Down Expand Up @@ -138,7 +138,7 @@
</div>
{% endif %}
{% if allInputDataDict['optionalCircuitFile'] == 'on' %}
<p class="reportTitle">Traditional/Model-Based Hosting Capacity Runtime</p>
<p class="reportTitle">Traditional/Model-Based Hosting Capacity Runtime ( H:M:S:MS )</p>
<div id="traditionalRunTime" class="tightContent">
<span style="border: 1px solid grey; padding: 3px;"> {{ allOutputDataDict['traditionalRuntime'] }} </span>
</div>
Expand Down
25 changes: 20 additions & 5 deletions omf/models/hostingCapacity.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
modelName, template = __neoMetaModel__.metadata(__file__)
hidden = False

def convert_seconds_to_hms_ms( seconds ):
milliseconds = seconds * 1000

# Calculate hours, minutes, seconds, and milliseconds
hours, remainder = divmod(milliseconds, 3600000)
minutes, remainder = divmod(remainder, 60000)
seconds, milliseconds = divmod(remainder, 1000)

return "{:02d}:{:02d}:{:02d}.{:03d}".format(int(hours), int(minutes), int(seconds), int(milliseconds))

def bar_chart_coloring( row ):
color = 'black'
if row['thermal_violation'] and not row['voltage_violation']:
Expand All @@ -39,6 +49,7 @@ def createColorCSV(modelDir, df):

def work(modelDir, inputDict):
outData = {}

if inputDict['runAmiAlgorithm'] == 'on':
run_ami_algorithm(modelDir, inputDict, outData)
if inputDict.get('optionalCircuitFile', outData) == 'on':
Expand All @@ -51,13 +62,16 @@ def run_ami_algorithm(modelDir, inputDict, outData):
# mohca data-driven hosting capacity
inputPath = Path(modelDir, inputDict['AMIDataFileName'])
inputAsString = inputPath.read_text()

outputPath = Path(modelDir, 'AMI_output.csv')
AMI_output = []

try:
csvValidateAndLoad(inputAsString, modelDir=modelDir, header=0, nrows=None, ncols=5, dtypes=[str, pd.to_datetime, float, float, float], return_type='df', ignore_nans=False, save_file=None, ignore_errors=False )
except:
errorMessage = "AMI-Data CSV file is incorrect format. Please see valid format definition at <a target='_blank' href='https://github.com/dpinney/omf/wiki/Models-~-hostingCapacity#meter-data-input-csv-file-format'>OMF Wiki hostingCapacity</a>"
raise Exception(errorMessage)
outputPath = Path(modelDir, 'AMI_output.csv')
AMI_output = []

AMI_start_time = time.time()
if inputDict[ "algorithm" ] == "sandia1":
AMI_output = mohca_cl.sandia1( inputPath, outputPath )
Expand All @@ -67,21 +81,23 @@ def run_ami_algorithm(modelDir, inputDict, outData):
errorMessage = "Algorithm name error"
raise Exception(errorMessage)
AMI_end_time = time.time()

AMI_results = AMI_output[0].rename(columns={'kW_hostable': 'voltage_cap_kW'})
histogramFigure = px.histogram( AMI_results, x='voltage_cap_kW', template="simple_white", color_discrete_sequence=["MediumPurple"] )
histogramFigure.update_layout(bargap=0.5)
# TBD - Needs to be modified when the MoHCA algorithm supports calculating thermal hosting capacity
min_value = 5
max_value = 8
AMI_results['thermal_cap_kW'] = np.random.randint(min_value, max_value + 1, size=len(AMI_results))

AMI_results['max_cap_allowed_kW'] = np.minimum( AMI_results['voltage_cap_kW'], AMI_results['thermal_cap_kW'])
barChartFigure = px.bar(AMI_results, x='busname', y=['voltage_cap_kW', 'thermal_cap_kW', 'max_cap_allowed_kW'], barmode='group', color_discrete_sequence=["green", "lightblue", "MediumPurple"], template="simple_white" )
barChartFigure.add_traces( list(px.line(AMI_results, x='busname', y='max_cap_allowed_kW', markers=True).select_traces()) )
outData['histogramFigure'] = json.dumps( histogramFigure, cls=py.utils.PlotlyJSONEncoder )
outData['barChartFigure'] = json.dumps( barChartFigure, cls=py.utils.PlotlyJSONEncoder )
outData['AMI_tableHeadings'] = AMI_results.columns.values.tolist()
outData['AMI_tableValues'] = ( list(AMI_results.sort_values( by="max_cap_allowed_kW", ascending=False, ignore_index=True ).itertuples(index=False, name=None)) )
outData['AMI_runtime'] = AMI_end_time - AMI_start_time
outData['AMI_runtime'] = convert_seconds_to_hms_ms( AMI_end_time - AMI_start_time )


def run_traditional_algorithm(modelDir, inputDict, outData):
Expand Down Expand Up @@ -135,10 +151,9 @@ def run_traditional_algorithm(modelDir, inputDict, outData):
outData['traditionalGraphData'] = json.dumps(traditionalHCFigure, cls=py.utils.PlotlyJSONEncoder )
outData['traditionalHCTableHeadings'] = tradHCDF.columns.values.tolist()
outData['traditionalHCTableValues'] = (list(tradHCDF.itertuples(index=False, name=None)))
outData['traditionalRuntime'] = traditional_end_time - traditional_start_time
outData['traditionalRuntime'] = convert_seconds_to_hms_ms( traditional_end_time - traditional_start_time )
outData['traditionalHCResults'] = traditionalHCResults


def runtimeEstimate(modelDir):
''' Estimated runtime of model in minutes. '''
return 1.0
Expand Down
7 changes: 3 additions & 4 deletions omf/models/transformerPairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ def work(modelDir, inputDict):

outData = {}
useTrueLabels = True
outData["useTrueLabels"] = useTrueLabels

saveResultsPath = modelDir
test_data_file_path = Path(omf.omfDir,'static','testFiles', 'transformerPairing')

outData["useTrueLabels"] = useTrueLabels

voltageInputPathCSV = Path( modelDir, inputDict['voltageDataFileName'])
realPowerInputPathCSV = Path( modelDir, inputDict['realPowerDataFileName'])
custIDInputPathCSV = Path( modelDir, inputDict['customerIDDataFileName'])
Expand All @@ -44,8 +45,6 @@ def work(modelDir, inputDict):
shutil.copyfile( Path( test_data_file_path, transformer_labels_true_file_name ), Path(modelDir, transformer_labels_true_file_name) )
transformerLabelsTruePath = Path(modelDir, transformer_labels_true_file_name)

saveResultsPath = modelDir

if inputDict['algorithm'] == 'reactivePower':
sdsmc.MeterTransformerPairing.TransformerPairing.run( voltageInputPathCSV, realPowerInputPathCSV, reactivePowerInputPathCSV, custIDInputPathCSV, transformerLabelsErrorsPathCSV, transformerLabelsTruePath, saveResultsPath, useTrueLabels )

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
225 changes: 225 additions & 0 deletions omf/scratch/hostingcapacity/downlineLoad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
import networkx as nx
from pathlib import Path
import pandas as pd
import math
import warnings
import matplotlib.pyplot as plt
import numpy as np

import omf
from omf.solvers import opendss
import os

'''
Simply adds up all load further away from the substation than the given bus to estimate rough hosting capacity.
with open(Path(modelDir, 'treefile.txt'), "w") as file:
for item in tree:
file.write(f"{item}\n")
voltagePlot
getVoltages
'''

def my_GetCoords(dssFilePath):
'''Takes in an OpenDSS circuit definition file and outputs the bus coordinates as a dataframe.'''
dssFileLoc = os.path.dirname(dssFilePath)
opendss.runDSS(dssFilePath)
opendss.runDssCommand(f'export buscoords "{dssFileLoc}/coords.csv"')
coords = pd.read_csv(dssFileLoc + '/coords.csv', header=None)
# JENNY - Deleted Radius and everything after.
coords.columns = ['Element', 'X', 'Y']
return coords

def my_NetworkPlot(filePath, figsize=(20,20), output_name='networkPlot.png', show_labels=True, node_size=300, font_size=8):
''' Plot the physical topology of the circuit.
Returns a networkx graph of the circuit as a bonus. '''
dssFileLoc = os.path.dirname(os.path.abspath(filePath))
opendss.runDSS(filePath)
coords = my_GetCoords(filePath)
opendss.runDssCommand(f'export voltages "{dssFileLoc}/volts.csv"')
volts = pd.read_csv(dssFileLoc + '/volts.csv')

# JENNY - Deleted radius
coords.columns = ['Bus', 'X', 'Y']
G = nx.Graph()
# Get the coordinates.
pos = {}
for index, row in coords.iterrows():
try:
bus_name = str(int(row['Bus']))
except:
bus_name = row['Bus']
G.add_node(bus_name, pos=(float(row['X']), float(row['Y'])))
pos[bus_name] = (float(row['X']), float(row['Y']))


# Get the connecting edges using Pandas.
lines = opendss.dss.utils.lines_to_dataframe()
edges = []
for index, row in lines.iterrows():
#HACK: dss upercases everything in the coordinate output.
bus1 = row['Bus1'].split('.')[0].upper()
bus2 = row['Bus2'].split('.')[0].upper()
edges.append((bus1, bus2))
G.add_edges_from(edges)


# JENNY ?? - WILL THERE BE BUSES WITH LOADS WITHOUT COORDS?
# Remove buses withouts coords
no_pos_nodes = set(G.nodes()) - set(pos)
if len(no_pos_nodes) > 0:
warnings.warn(f'Node positions missing for {no_pos_nodes}')
G.remove_nodes_from(list(no_pos_nodes))


# We'll color the nodes according to voltage.
volt_values = {}
labels = {}
for index, row in volts.iterrows():
if row['Bus'].upper() in [x.upper() for x in pos]:
volt_values[row['Bus']] = row[' pu1']
labels[row['Bus']] = row['Bus']
# JENNY
G.nodes[row['Bus']]['max_pu_voltage'] = float(max(row[' pu1'], row[' pu2'],row[' pu3']))
colorCode = [volt_values.get(node, 0.0) for node in G.nodes()]


# Start drawing.
plt.figure(figsize=figsize)
nodes = nx.draw_networkx_nodes(G, pos, node_color=colorCode, node_size=node_size)
edges = nx.draw_networkx_edges(G, pos)
if show_labels:
nx.draw_networkx_labels(G, pos, labels, font_size=font_size)
plt.colorbar(nodes)
plt.legend()
plt.title('Network Voltage Layout')
plt.tight_layout()
plt.savefig(dssFileLoc + '/' + output_name)
plt.clf()
return G

def temp_func_name_nx(dssFilePath, tree=None, figsize=(20,20), output_name='directedNetworkPlot.png', show_labels=True, node_size=300, font_size=8):
''' Return a networkx directed graph from a dss file. If tree is provided, build graph from that instead of the file. '''
if tree == None:
tree = opendss.dssConvert.dssToTree(dssFilePath)
dssFileLoc = os.path.dirname(dssFilePath)

opendss.runDSS(dssFilePath)
coords = my_GetCoords(dssFilePath)
opendss.runDssCommand(f'export voltages "{dssFileLoc}/volts.csv"')
volts = pd.read_csv(dssFileLoc + '/volts.csv')

omd = opendss.dssConvert.evilDssTreeToGldTree(tree)

coords.columns = ['Bus', 'X', 'Y']

G = nx.DiGraph()
# Get the coordinates.
pos = {}
for index, row in coords.iterrows():
try:
bus_name = str(int(row['Bus']))
except:
bus_name = row['Bus']
G.add_node(bus_name.lower(), pos=(float(row['X']), float(row['Y'])))
pos[bus_name] = (float(row['X']), float(row['Y']))

#for x in pos.keys():
#print( str(x) + " => " + str(pos[x]))

# Gather edges, leave out source and circuit objects
edges = [(ob['from'],ob['to']) for ob in omd.values() if 'from' in ob and 'to' in ob]
edges_sub = [
(ob['parent'],ob['name']) for ob in omd.values()
if 'name' in ob and 'parent' in ob and ob.get('object') not in ['circuit', 'vsource']
]
full_edges = edges + edges_sub
G.add_edges_from(full_edges)

# We'll color the nodes according to voltage.
# Getting values from volts.csv and setting it as attributes
volt_values = {}
labels = {}
for index, row in volts.iterrows():
if row['Bus'] in [x for x in pos]:
volt_values[row['Bus']] = row[' pu1']
labels[row['Bus']] = row['Bus']
# JENNY
G.nodes[row['Bus'].lower()]['max_pu_voltage'] = float(max(row[' pu1'], row[' pu2'],row[' pu3']))
colorCode = [volt_values.get(node, 0.0) for node in G.nodes()]

# Getting values out of tree and setting it as attributes
# parse load
# parse bus_name
# get kw
meters = [x for x in tree if x.get('object','N/A').startswith('load.')]
bus_names = [x['bus1'] for x in meters if 'bus1' in x]
kw = [x['kw'] for x in meters if 'kw' in x] # these are lists
just_name_no_conn = [x.split('.')[0] for x in bus_names]
for bus_name, kwVal in zip(just_name_no_conn, kw):
G.nodes[bus_name.lower()]['kw'] = float(kwVal)


# Print positions of all nodes
# for node, position in nx.get_node_attributes(G, 'pos').items():
# print(f"Node: {node}, Position: {position}")

# Start drawing.
plt.figure(figsize=figsize)
positions = nx.spring_layout( G )
nodes = nx.draw_networkx_nodes(G, positions, node_color=colorCode, node_size=node_size)
edges = nx.draw_networkx_edges(G, positions)
if show_labels:
nx.draw_networkx_labels(G, pos, labels, font_size=font_size)
plt.colorbar(nodes)
plt.legend()
plt.title('Network Voltage Layout')
plt.tight_layout()
plt.savefig(dssFileLoc + '/' + output_name)
plt.clf()
return G

def downline_hosting_capacity( FNAME, BUS_LIST ):
fullpath = os.path.abspath(FNAME)
tree = opendss.dssConvert.omdToTree(fullpath)

if __name__ == '__main__':
modelDir = Path(omf.omfDir, 'scratch', 'hostingcapacity')
beginning_test_file = Path( omf.omfDir, 'static', 'publicFeeders', 'iowa240.clean.dss.omd')
circuit_file = 'omdToDSScircuit.dss'

tree = opendss.dssConvert.omdToTree( beginning_test_file.resolve() ) # this tree is a list
opendss.dssConvert.treeToDss(tree, Path(modelDir, circuit_file))

# This graph is undirected.
# G = my_NetworkPlot( os.path.join( modelDir, circuit_file) )
# print( "info about graph made from networkplot: ", G )
# max_pu_voltage = nx.get_node_attributes(G, "max_pu_voltage")
# pos = nx.get_node_attributes(G,'pos')
# print( "max_pu_voltage of BUS1006: ", max_pu_voltage['BUS1006'])
# print( "pos of BUS1006: ", pos['BUS1006'])

# This graph is directed
# But this has a lot of other stuff. The other one is just busses which I think we want...
g2 = temp_func_name_nx( os.path.join( modelDir, circuit_file), tree )
buses = opendss.get_meter_buses(circuit_file )
# print( type( sorted(nx.descendants(g2, 't_bus3056_l'))) )
# print( sorted(nx.descendants(g2, 'bus2033')) )
max_pu_voltage = nx.get_node_attributes(g2, "max_pu_voltage")
kwFromGraph = nx.get_node_attributes(g2, 'kw')
print( "type( max_pu_voltage )", type( max_pu_voltage ))
print( "kw of t_bus3056_l: ", kwFromGraph['t_bus3056_l'] )
print( "max_pu_voltage of t_bus3056_l: ", max_pu_voltage['t_bus3056_l'] )
test_descendents_bus_list = sorted(nx.descendants(g2, 'bus2033'))
# print( max_pu_voltage )
voltage_sum = 0
kwSum = 0
for node in test_descendents_bus_list:
if node in buses:
voltage_sum += max_pu_voltage[node]
print( "voltage_sum: ", voltage_sum)

# nx.draw(g2, with_labels=True, node_size=1000, node_color='lightblue', font_size=10)
# print( "info about graph made from my_BIGBOYGRAPH: ", g2 )
# g2 = netx( Path(modelDir, circuit_file), tree )
# print( "info about graph made from netx: ", g2 )
39 changes: 39 additions & 0 deletions omf/scratch/hostingcapacity/hosting_cap_repro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import omf
from pathlib import Path
from omf.models import __neoMetaModel__
from omf.models.__neoMetaModel__ import *
from omf.solvers import opendss
from omf.solvers import mohca_cl
import time

meter_file_name = 'mohcaInputCustom.csv'
meter_file_path = Path(omf.omfDir,'static','testFiles', 'hostingCapacity', meter_file_name)
modeldir = Path(omf.omfDir, 'scratch', 'hostingCapacity')

# 2541.865383386612 - iowa state
def run_ami_algorithm(modelDir, meterfileinput, outData):
# mohca data-driven hosting capacity
inputPath = Path(modelDir, meterfileinput)
outputPath = Path(modelDir, 'AMI_output.csv')
AMI_start_time = time.time()
AMI_output = mohca_cl.sandia1( inputPath, outputPath )
AMI_end_time = time.time()
runtime = AMI_end_time - AMI_start_time
return runtime


def convert_seconds_to_hms_ms(seconds):
# Convert seconds to milliseconds
milliseconds = seconds * 1000

# Calculate hours, minutes, seconds, and milliseconds
hours, remainder = divmod(milliseconds, 3600000) # 3600000 milliseconds in an hour
minutes, remainder = divmod(remainder, 60000) # 60000 milliseconds in a minute
seconds, milliseconds = divmod(remainder, 1000) # 1000 milliseconds in a second

# Format the output
return "{:02d}:{:02d}:{:02d}.{:03d}".format(int(hours), int(minutes), int(seconds), int(milliseconds))

runtime = run_ami_algorithm(modeldir, meter_file_path, 'output.csv')
print( 'AMI_runtime: ', runtime)
print( 'formatted time: ', convert_seconds_to_hms_ms( runtime ))
Binary file added omf/scratch/hostingcapacity/networkPlot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 2f0fc27

Please sign in to comment.