def oneLineGridlab(temp_dir): ''' Create a one-line diagram of the input GLM and return a PNG of it. Form parameters: :param glm: a GLM file. :param useLatLons: 'True' if the diagram should be drawn with coordinate values taken from within the GLM, 'False' if the diagram should be drawn with artificial coordinates using Graphviz NEATO. Details: :OMF fuction: omf.feeder.latLonNxGraph(). :run-time: about 1 to 30 seconds. ''' glm_path = os.path.join(temp_dir, 'in.glm') feed = feeder.parse(glm_path) graph = feeder.treeToNxGraph(feed) neatoLayout = True if request.form.get('useLatLons') == 'False' else False # Clear old plots. plt.clf() plt.close() # Plot new plot. feeder.latLonNxGraph(graph, labels=False, neatoLayout=neatoLayout, showPlot=False) plt.savefig(os.path.join(temp_dir, filenames["ongl"]))
def genDiagram(outputDir, feederJson): # Load required data. tree = feederJson.get("tree", {}) links = feederJson.get("links", {}) # Generate lat/lons from nodes and links structures. for link in links: for typeLink in link.keys(): if typeLink in ['source', 'target']: for key in link[typeLink].keys(): if key in ['x', 'y']: objName = link[typeLink]['name'] for x in tree: leaf = tree[x] if leaf.get('name', '') == objName: if key == 'x': leaf['latitude'] = link[typeLink][key] else: leaf['longitude'] = link[typeLink][key] # Remove even more things (no lat, lon or from = node without a position). for key in tree.keys(): aLat = tree[key].get('latitude') aLon = tree[key].get('longitude') aFrom = tree[key].get('from') if aLat is None and aLon is None and aFrom is None: tree.pop(key) # Create and save the graphic. nxG = feeder.treeToNxGraph(tree) feeder.latLonNxGraph( nxG) # This function creates a .plt reference which can be saved here. plt.savefig(pJoin(outputDir, "feederChart.png"), dpi=800, pad_inches=0.0)
def hullOfOmd(pathToOmdFile, conversion=False): '''Convex hull of an omd in the form of a geojson dictionary with a single ploygon.''' if not conversion: with open(pathToOmdFile) as inFile: tree = json.load(inFile)['tree'] nxG = feeder.treeToNxGraph(tree) nxG = graphValidator(pathToOmdFile, nxG) #use conversion for testing other feeders if conversion: nxG = convertOmd(pathToOmdFile) points = np.array([ nxG.nodes[nodewithPosition]['pos'] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') ]) hull = ConvexHull(points) polygon = points[hull.vertices].tolist() for point in polygon: point.reverse() #Add first node to beginning to comply with geoJSON standard polygon.append(polygon[0]) #Create dict and bump to json file geoJsonDict = { "type": "FeatureCollection", "features": [{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [polygon] } }] } return geoJsonDict
def omdGeoJson(pathToOmdFile, conversion=False): '''Create a geojson standards compliant file (https://tools.ietf.org/html/rfc7946) from an omd.''' with open(pathToOmdFile) as inFile: tree = json.load(inFile)['tree'] nxG = feeder.treeToNxGraph(tree) #use conversion for testing other feeders nxG = graphValidator(pathToOmdFile, nxG) if conversion: nxG = convertOmd(pathToOmdFile) geoJsonDict = {"type": "FeatureCollection", "features": []} #Add nodes to geoJSON node_positions = { nodewithPosition: nxG.nodes[nodewithPosition]['pos'] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') } node_types = { nodewithType: nxG.nodes[nodewithType]['type'] for nodewithType in nx.get_node_attributes(nxG, 'type') } for node in node_positions: geoJsonDict['features'].append({ "type": "Feature", "geometry": { "type": "Point", "coordinates": [node_positions[node][1], node_positions[node][0]] }, "properties": { "name": node, #"pointType": node_types[node], #"pointColor": _obToCol(node_types[node]) } }) #Add edges to geoJSON edge_types = { edge: nxG[edge[0]][edge[1]]['type'] for edge in nx.get_edge_attributes(nxG, 'type') } edge_phases = { edge: nxG[edge[0]][edge[1]]['phases'] for edge in nx.get_edge_attributes(nxG, 'phases') } for edge in nx.edges(nxG): geoJsonDict['features'].append({ "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[node_positions[edge[0]][1], node_positions[edge[0]][0]], [node_positions[edge[1]][1], node_positions[edge[1]][0]]] }, "properties": { #"phase": edge_phases[edge], #"edgeType": edge_types[edge], #"edgeColor":_obToCol(edge_types[edge]) } }) return geoJsonDict
def shortestPathOmd(pathToOmdFile, sourceObjectName, targetObjectName): '''Get the shortest path between two points on a feeder''' with open(pathToOmdFile) as inFile: tree = json.load(inFile)['tree'] nxG = feeder.treeToNxGraph(tree) nxG = graphValidator(pathToOmdFile, nxG) tracePath = nx.bidirectional_shortest_path(nxG, sourceObjectName, targetObjectName) return tracePath
def milsoftToGridlabTests(keepFiles=False): openPrefix = '../uploads/' outPrefix = './milToGridlabTests/' import os, json, traceback, shutil from omf.solvers import gridlabd from matplotlib import pyplot as plt from milToGridlab import convert import omf.feeder as feeder try: os.mkdir(outPrefix) except: pass # Directory already there. exceptionCount = 0 # testFiles = [('INEC-RENOIR.std','INEC.seq'), ('INEC-GRAHAM.std','INEC.seq'), # ('Olin-Barre.std','Olin.seq'), ('Olin-Brown.std','Olin.seq'), # ('ABEC-FRANK.std','ABEC.seq'), ('ABEC-COLUMBIA.std','ABEC.seq'),('OMF_Norfork1.std', 'OMF_Norfork1.seq')] testFiles = [('Olin-Brown.std', 'Olin.seq')] testAttachments = {'schedules.glm':''} # testAttachments = {'schedules.glm':'', 'climate.tmy2':open('./data/Climate/KY-LEXINGTON.tmy2','r').read()} for stdString, seqString in testFiles: try: # Convert the std+seq. with open(openPrefix + stdString,'r') as stdFile, open(openPrefix + seqString,'r') as seqFile: outGlm,x,y = convert(stdFile.read(),seqFile.read()) with open(outPrefix + stdString.replace('.std','.glm'),'w') as outFile: outFile.write(feeder.sortedWrite(outGlm)) print 'WROTE GLM FOR', stdString try: # Draw the GLM. myGraph = feeder.treeToNxGraph(outGlm) feeder.latLonNxGraph(myGraph, neatoLayout=False) plt.savefig(outPrefix + stdString.replace('.std','.png')) print 'DREW GLM OF', stdString except: exceptionCount += 1 print 'FAILED DRAWING', stdString try: # Run powerflow on the GLM. HACK:blank attachments for now. output = gridlabd.runInFilesystem(outGlm, attachments=testAttachments, keepFiles=False) with open(outPrefix + stdString.replace('.std','.json'),'w') as outFile: json.dump(output, outFile, indent=4) print 'RAN GRIDLAB ON', stdString except: exceptionCount += 1 print 'POWERFLOW FAILED', stdString except: print 'FAILED CONVERTING', stdString exceptionCount += 1 traceback.print_exc() if not keepFiles: shutil.rmtree(outPrefix) return exceptionCount
def insert_coordinates(tree): # type: (dict) -> None """Insert additional latitude and longitude data into the dictionary.""" print("Force laying out the graph...") # Use graphviz to lay out the graph. inGraph = feeder.treeToNxGraph(tree) # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(inGraph.edges()) # HACK2: might miss nodes without edges without the following. cleanG.add_nodes_from(inGraph) pos = nx.nx_agraph.graphviz_layout(cleanG, prog='neato') # # Charting the feeder in matplotlib: # feeder.latLonNxGraph(inGraph, labels=False, neatoLayout=True, showPlot=True) # Insert the latlons. for key in tree: obName = tree[key].get('name', '') thisPos = pos.get(obName, None) if thisPos != None: tree[key]['longitude'] = thisPos[0] tree[key]['latitude'] = thisPos[1]
def voltPlot(omd, workDir=None, neatoLayout=False): ''' Draw a color-coded map of the voltage drop on a feeder. Returns a matplotlib object. ''' tree = omd.get('tree', {}) # # Get rid of schedules and climate: for key in tree.keys(): if tree[key].get("argument", "") == "\"schedules.glm\"" or tree[key].get( "tmyfile", "") != "": del tree[key] # Make sure we have a voltDump: def safeInt(x): try: return int(x) except: return 0 biggestKey = max([safeInt(x) for x in tree.keys()]) tree[str(biggestKey * 10)] = { "object": "voltdump", "filename": "voltDump.csv" } # Run Gridlab. if not workDir: workDir = tempfile.mkdtemp() print "gridlabD runInFilesystem with no specified workDir. Working in", workDir gridlabOut = gridlabd.runInFilesystem(tree, attachments=omd.get( 'attachments', {}), workDir=workDir) with open(pJoin(workDir, 'voltDump.csv'), 'r') as dumpFile: reader = csv.reader(dumpFile) reader.next() # Burn the header. keys = reader.next() voltTable = [] for row in reader: rowDict = {} for pos, key in enumerate(keys): rowDict[key] = row[pos] voltTable.append(rowDict) # Calculate average node voltage deviation. First, helper functions. def pythag(x, y): ''' For right triangle with sides a and b, return the hypotenuse. ''' return math.sqrt(x**2 + y**2) def digits(x): ''' Returns number of digits before the decimal in the float x. ''' return math.ceil(math.log10(x + 1)) def avg(l): ''' Average of a list of ints or floats. ''' return sum(l) / len(l) # Detect the feeder nominal voltage: for key in tree: ob = tree[key] if type(ob) == dict and ob.get('bustype', '') == 'SWING': feedVoltage = float(ob.get('nominal_voltage', 1)) # Tot it all up. nodeVolts = {} for row in voltTable: allVolts = [] for phase in ['A', 'B', 'C']: phaseVolt = pythag(float(row['volt' + phase + '_real']), float(row['volt' + phase + '_imag'])) if phaseVolt != 0.0: if digits(phaseVolt) > 3: # Normalize to 120 V standard phaseVolt = phaseVolt * (120 / feedVoltage) allVolts.append(phaseVolt) nodeVolts[row.get('node_name', '')] = avg(allVolts) # Color nodes by VOLTAGE. fGraph = feeder.treeToNxGraph(tree) voltChart = plt.figure(figsize=(15, 15)) plt.axes(frameon=0) plt.axis('off') #set axes step equal voltChart.gca().set_aspect('equal') if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(fGraph.edges()) cleanG.add_nodes_from(fGraph) positions = graphviz_layout(cleanG, prog='neato') else: positions = {n: fGraph.node[n].get('pos', (0, 0)) for n in fGraph} edgeIm = nx.draw_networkx_edges(fGraph, positions) nodeIm = nx.draw_networkx_nodes( fGraph, pos=positions, node_color=[nodeVolts.get(n, 0) for n in fGraph.nodes()], linewidths=0, node_size=30, cmap=plt.cm.jet) plt.sci(nodeIm) plt.clim(110, 130) plt.colorbar() return voltChart
def generateVoltChart(tree, rawOut, modelDir, neatoLayout=True): ''' Map the voltages on a feeder over time using a movie.''' # We need to timestamp frames with the system clock to make sure the browser caches them appropriately. genTime = str(datetime.datetime.now()).replace(':','.') # Detect the feeder nominal voltage: for key in tree: ob = tree[key] if type(ob)==dict and ob.get('bustype','')=='SWING': feedVoltage = float(ob.get('nominal_voltage',1)) # Make a graph object. fGraph = feeder.treeToNxGraph(tree) if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(fGraph.edges()) cleanG.add_nodes_from(fGraph) # was formerly : positions = nx.graphviz_layout(cleanG, prog='neato') but this threw an error positions = nx.nx_agraph.graphviz_layout(cleanG, prog='neato') else: rawPositions = {n:fGraph.node[n].get('pos',(0,0)) for n in fGraph} #HACK: the import code reverses the y coords. def yFlip(pair): try: return (pair[0], -1.0*pair[1]) except: return (0,0) positions = {k:yFlip(rawPositions[k]) for k in rawPositions} # Plot all time steps. nodeVolts = {} for step, stamp in enumerate(rawOut['aVoltDump.csv']['# timestamp']): # Build voltage map. nodeVolts[step] = {} for nodeName in [x for x in rawOut.get('aVoltDump.csv',{}).keys() + rawOut.get('1nVoltDump.csv',{}).keys() + rawOut.get('1mVoltDump.csv',{}).keys() if x != '# timestamp']: allVolts = [] for phase in ['a','b','c','1n','2n','1m','2m']: try: voltStep = rawOut[phase + 'VoltDump.csv'][nodeName][step] except: continue # the nodeName doesn't have the phase we're looking for. # HACK: Gridlab complex number format sometimes uses i, sometimes j, sometimes d. WTF? if type(voltStep) is str: voltStep = voltStep.replace('i','j') v = complex(voltStep) phaseVolt = abs(v) if phaseVolt != 0.0: if _digits(phaseVolt)>3: # Normalize to 120 V standard phaseVolt = phaseVolt*(120/feedVoltage) allVolts.append(phaseVolt) # HACK: Take average of all phases to collapse dimensionality. nodeVolts[step][nodeName] = avg(allVolts) # Line current calculations lineCurrents = {} if os.path.exists(pJoin(modelDir,'OH_line_current_phaseA.csv')): for step, stamp in enumerate(rawOut['OH_line_current_phaseA.csv']['# timestamp']): lineCurrents[step] = {} currentArray = [] # Finding currents of all phases on the line for key in [x for x in rawOut.get('OH_line_current_phaseA.csv',{}).keys() if x != '# timestamp']: currA = rawOut['OH_line_current_phaseA.csv'][key][step] currB = rawOut['OH_line_current_phaseB.csv'][key][step] currC = rawOut['OH_line_current_phaseC.csv'][key][step] flowDir = rawOut['OH_line_flow_direc.csv'][key][step] lineRating = rawOut['OH_line_cont_rating.csv'][key][step] if 'R' in flowDir: direction = -1 else : direction = 1 if type(currA) is str: currA = stringToMag(currA) currB = stringToMag(currB) currC = stringToMag(currC) maxCurrent = max(abs(currA),abs(currB),abs(currC)) directedCurrent = float(maxCurrent/lineRating * direction) for objt in tree: if 'name' in tree[objt].keys(): if tree[objt]['name'] == str(int(key)): keyTup = (tree[objt]['to'],tree[objt]['from']) lineCurrents[step][keyTup] = directedCurrent # Underground Lines if os.path.exists(pJoin(modelDir,'UG_line_current_phaseA.csv')): for step, stamp in enumerate(rawOut['UG_line_current_phaseA.csv']['# timestamp']): currentArray = [] # Finding currents of all phases on the line for key in [x for x in rawOut.get('UG_line_current_phaseA.csv',{}).keys() if x != '# timestamp']: currA = rawOut['UG_line_current_phaseA.csv'][key][step] currB = rawOut['UG_line_current_phaseB.csv'][key][step] currC = rawOut['UG_line_current_phaseC.csv'][key][step] flowDir = rawOut['UG_line_flow_direc.csv'][key][step] lineRating = rawOut['UG_line_cont_rating.csv'][key][step] if 'R' in flowDir: direction = -1 else : direction = 1 if type(currA) is str: currA = stringToMag(currA) currB = stringToMag(currB) currC = stringToMag(currC) maxCurrent = max(abs(currA),abs(currB),abs(currC)) directedCurrent = float(maxCurrent/lineRating * direction) for objt in tree: if 'name' in tree[objt].keys(): if tree[objt]['name'] == str(int(key)): keyTup = (tree[objt]['to'],tree[objt]['from']) lineCurrents[step][keyTup] = directedCurrent for step in lineCurrents: for edge in fGraph.edges(): if edge not in lineCurrents[step].keys(): lineCurrents[step][edge] = 0 # Draw animation. voltChart = plt.figure(figsize=(15,15)) plt.axes(frameon = 0) plt.axis('off') #set axes step equal voltChart.gca().set_aspect('equal') custom_cm = matplotlib.colors.LinearSegmentedColormap.from_list('custColMap',[(0.0,'blue'),(0.25,'darkgray'),(0.75,'darkgray'),(1.0,'yellow')]) custom_cm.set_under(color='black') current_cm = matplotlib.colors.LinearSegmentedColormap.from_list('custColMap',[(0.0,'green'),(0.999999,'green'),(1.0,'red')]) # current_cm = matplotlib.colors.LinearSegmentedColormap.from_list('custColMap',[(-1.0,'green'),(0.0, 'gray'),(1.0,'red'),(1.0,'red')]) # use edge color to set color and dashness of overloaded/negative currents if len(lineCurrents)>0: edgeIm = nx.draw_networkx_edges(fGraph, pos = positions, edge_color = [lineCurrents[0].get(n,0) for n in fGraph.edges()], edge_cmap = current_cm) else: edgeIm = nx.draw_networkx_edges(fGraph, positions) nodeIm = nx.draw_networkx_nodes(fGraph, pos = positions, node_color = [nodeVolts[0].get(n,0) for n in fGraph.nodes()], linewidths = 0, node_size = 30, cmap = custom_cm) plt.sci(nodeIm) plt.clim(110,130) plt.colorbar() plt.title(rawOut['aVoltDump.csv']['# timestamp'][0]) def update(step): nodeColors = np.array([nodeVolts[step].get(n,0) for n in fGraph.nodes()]) if len(lineCurrents)>0: edgeColors = np.array([lineCurrents[step].get(n,0) for n in fGraph.edges()]) edgeIm.set_array(edgeColors) plt.title(rawOut['aVoltDump.csv']['# timestamp'][step]) nodeIm.set_array(nodeColors) return nodeColors, mapTimestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") anim = FuncAnimation(voltChart, update, frames=len(rawOut['aVoltDump.csv']['# timestamp']), interval=200, blit=False) anim.save(pJoin(modelDir,'voltageChart_'+ mapTimestamp +'.mp4'), codec='h264', extra_args=['-pix_fmt', 'yuv420p']) # Reclaim memory by closing, deleting and garbage collecting the last chart. voltChart.clf() plt.close() del voltChart gc.collect() return genTime, mapTimestamp
def rasterTilesFromOmd(pathToOmdFile, outputPath, conversion=False): '''Save raster tiles of omd to serve from zoom/x/y directory''' if not conversion: with open(pathToOmdFile) as inFile: tree = json.load(inFile)['tree'] #networkx graph to work with nxG = feeder.treeToNxGraph(tree) nxG = graphValidator(pathToOmdFile, nxG) #use conversion for testing other feeders if conversion: nxG = convertOmd(pathToOmdFile) #Lat/lon min/max for caluclating tile coverage later latitude_min = min([ nxG.nodes[nodewithPosition]['pos'][0] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') ]) longitude_min = min([ nxG.nodes[nodewithPosition]['pos'][1] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') ]) latitude_max = max([ nxG.nodes[nodewithPosition]['pos'][0] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') ]) longitude_max = max([ nxG.nodes[nodewithPosition]['pos'][1] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') ]) #Set the plot settings plt.switch_backend('Agg') fig = plt.figure(frameon=False, figsize=[2.56, 2.56]) ax = fig.add_axes([0, 0, 1, 1]) ax.axis('off') #Create the default tile if not os.path.exists(outputPath): os.makedirs(outputPath) plt.savefig(pJoin(outputPath, 'default.png'), frameon=False, pad_inches=0, bbox='tight') #map latlon to projection epsg3857 = Proj(init='epsg:3857') wgs84 = Proj(init='EPSG:4326') node_positions = { nodewithPosition: nxG.nodes[nodewithPosition]['pos'] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') } for point in node_positions: node_positions[point] = transform(wgs84, epsg3857, node_positions[point][1], node_positions[point][0]) #Go through each zoom level and create tiles for each area covering the feeder nx.draw_networkx(nxG, pos=node_positions, nodelist=list(node_positions.keys()), with_labels=False, node_size=2, edge_size=1) for zoomLevel in range(0, 19): #Boundaries covering the omd locations for the current zoom level upperRightTile = tileXY(latitude_max, longitude_max, zoomLevel) lowerLeftTile = tileXY(latitude_min, longitude_min, zoomLevel) firstTileEdges = tileEdges(upperRightTile[0], upperRightTile[1], zoomLevel) lastTileEdges = tileEdges(lowerLeftTile[0], lowerLeftTile[1], zoomLevel) #Map omd for each x/y tile area for tileX in range(lowerLeftTile[0], upperRightTile[0] + 1): for tileY in range(upperRightTile[1], lowerLeftTile[1] + 1): currentTileEdges = tileEdges(tileX, tileY, zoomLevel) southWest = transform(wgs84, epsg3857, currentTileEdges[1], currentTileEdges[0]) northEast = transform(wgs84, epsg3857, currentTileEdges[3], currentTileEdges[2]) # S,W,N,E plt.ylim(top=northEast[1], bottom=southWest[1]) plt.xlim(southWest[0], northEast[0]) #create directory for tile savePath = pJoin(outputPath, str(zoomLevel), str(tileX)) if not os.path.exists(savePath): os.makedirs(savePath) plt.savefig(pJoin(savePath, '%s.png' % str(tileY)), frameon=False, pad_inches=0, bbox='tight')
def simplifiedOmdShape(pathToOmdFile, conversion=False): '''Use kmeans clustering to create simplified geojson object with convex hull and connected clusters from an omd.''' if not conversion: with open(pathToOmdFile) as inFile: tree = json.load(inFile)['tree'] nxG = feeder.treeToNxGraph(tree) nxG = graphValidator(pathToOmdFile, nxG) simplifiedGeoDict = hullOfOmd(pathToOmdFile) #use conversion for testing other feeders if conversion: nxG = convertOmd(pathToOmdFile) simplifiedGeoDict = hullOfOmd(pathToOmdFile, conversion=True) #Kmeans clustering function numpyGraph = np.array([[ node, float(nxG.nodes[node]['pos'][0]), float(nxG.nodes[node]['pos'][1]) ] for node in nx.get_node_attributes(nxG, 'pos')], dtype=object) Kmean = KMeans(n_clusters=20) Kmean.fit(numpyGraph[:, 1:3]) #Set up new graph with cluster centers as nodes to use in output centerNodes = Kmean.cluster_centers_ clusterDict = { i: numpyGraph[np.where(Kmean.labels_ == i)] for i in range(Kmean.n_clusters) } simplifiedGraph = nx.Graph() for centerNode in clusterDict: currentClusterGroup = clusterDict[centerNode] simplifiedGraph.add_node('centroid' + str(centerNode), type='centroid', pos=(centerNodes[centerNode][0], centerNodes[centerNode][1]), clusterSize=np.ma.size(currentClusterGroup, axis=0), lineCount=0) #Create edges between cluster centers for centerNode in clusterDict: currentClusterGroup = clusterDict[centerNode] nxG.add_node('centroid' + str(centerNode), type='centroid', pos=(centerNodes[centerNode][0], centerNodes[centerNode][1])) intraClusterLines = 0 for i in currentClusterGroup: currentNode = i[0] neighbors = nx.neighbors(nxG, currentNode) for neighbor in neighbors: #connect centroids if nxG.nodes[neighbor]['type'] == 'centroid': if ('centroid' + str(centerNode), neighbor) not in nx.edges(simplifiedGraph): simplifiedGraph.add_edge('centroid' + str(centerNode), neighbor, type='centroidConnector', lineCount=1) else: simplifiedGraph[ 'centroid' + str(centerNode)][neighbor]['lineCount'] += 1 #connect centroid to nodes in other clusters, which is replaced in subsequent loops elif neighbor not in currentClusterGroup[:, 0]: nxG.add_edge('centroid' + str(centerNode), neighbor, type='centroidConnector') else: simplifiedGraph.nodes['centroid' + str(centerNode)]['lineCount'] += 1 if currentNode in simplifiedGraph: simplifiedGraph.remove_node(currentNode) #Add nodes and edges to dict with convex hull for node in simplifiedGraph.nodes: simplifiedGeoDict['features'].append({ "type": "Feature", "geometry": { "type": "Point", "coordinates": [ simplifiedGraph.nodes[node]['pos'][1], simplifiedGraph.nodes[node]['pos'][0] ] }, "properties": { "name": node, "pointType": simplifiedGraph.nodes[node]['type'], "lineCount": simplifiedGraph.nodes[node]['lineCount'] } }) #Add edges to dict for edge in nx.edges(simplifiedGraph): simplifiedGeoDict['features'].append({ "type": "Feature", "geometry": { "type": "LineString", "coordinates": [[ simplifiedGraph.nodes[edge[0]]['pos'][1], simplifiedGraph.nodes[edge[0]]['pos'][0] ], [ simplifiedGraph.nodes[edge[1]]['pos'][1], simplifiedGraph.nodes[edge[1]]['pos'][0] ]] }, "properties": { "lineCount": simplifiedGraph[edge[0]][edge[1]]['lineCount'], "edgeType": simplifiedGraph[edge[0]][edge[1]]['type'] } }) return simplifiedGeoDict '''
def viz(pathToOmdOrGlm, forceLayout=False, outputPath=None): ''' Vizualize a distribution system.''' # HACK: make sure we have our homebrew binaries available. os.environ['PATH'] += os.pathsep + '/usr/local/bin' # Load in the feeder. with open(pathToOmdOrGlm, 'r') as feedFile: if pathToOmdOrGlm.endswith('.omd'): thisFeed = { 'tree': json.load(feedFile)['tree'] } # TODO: later bring back attachments. elif pathToOmdOrGlm.endswith('.glm'): thisFeed = {'tree': feeder.parse(pathToOmdOrGlm, filePath=True)} tree = thisFeed['tree'] # If there is zero lat/lon info, do force layout by default. latLonCount = 0 for key in tree: for subKey in ['latitude', 'longitude']: if subKey in tree[key]: latLonCount += 1 if latLonCount == 0: forceLayout = True # Force layout of feeders with no lat/lon information so we can actually see what's there. if forceLayout: print "Force laying out the graph..." # Use graphviz to lay out the graph. inGraph = feeder.treeToNxGraph(tree) # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(inGraph.edges()) # HACK2: might miss nodes without edges without the following. cleanG.add_nodes_from(inGraph) pos = nx.nx_agraph.graphviz_layout(cleanG, prog='neato') # # Charting the feeder in matplotlib: # feeder.latLonNxGraph(inGraph, labels=False, neatoLayout=True, showPlot=True) # Insert the latlons. for key in tree: obName = tree[key].get('name', '') thisPos = pos.get(obName, None) if thisPos != None: tree[key]['longitude'] = thisPos[0] tree[key]['latitude'] = thisPos[1] # Set up temp directory and copy the feeder and viewer in to it. if outputPath == None: tempDir = tempfile.mkdtemp() else: tempDir = outputPath #HACK: make sure we get the required files from the right place. SOURCE_DIR = os.path.dirname(__file__) + '/' shutil.copy(SOURCE_DIR + '/distNetViz.html', tempDir + '/viewer.html') shutil.copy(SOURCE_DIR + '/svg-pan-zoom.js', tempDir + '/svg-pan-zoom.js') # Grab the library we need. with open(SOURCE_DIR + 'svg-pan-zoom.js', 'r') as pzFile: pzData = pzFile.read() # Rewrite the load lines in viewer.html # Note: you can't juse open the file in r+ mode because, based on the way the file is mapped to memory, you can only overwrite a line with another of exactly the same length. for line in fileinput.input(tempDir + '/viewer.html', inplace=1): if line.lstrip().startswith("<script id='feederLoadScript''>"): print "" # Remove the existing load. elif line.lstrip().startswith("<script id='feederInsert'>"): print "<script id='feederInsert'>\ntestFeeder=" + json.dumps( thisFeed, indent=4) # load up the new feeder. elif line.lstrip().startswith("<script id='panZoomInsert'>"): print "<script id='panZoomInsert'>\n" + pzData # load up the new feeder. else: print line.rstrip() # os.system('open -a "Google Chrome" ' + '"file://' + tempDir + '/viewer.html"') webbrowser.open_new("file://" + tempDir + '/viewer.html')
def generateVoltChart(tree, rawOut, modelDir, neatoLayout=True): ''' Map the voltages on a feeder over time using a movie.''' # We need to timestamp frames with the system clock to make sure the browser caches them appropriately. genTime = str(datetime.datetime.now()).replace(':','.') # Detect the feeder nominal voltage: for key in tree: ob = tree[key] if type(ob)==dict and ob.get('bustype','')=='SWING': feedVoltage = float(ob.get('nominal_voltage',1)) # Make a graph object. fGraph = feeder.treeToNxGraph(tree) if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(fGraph.edges()) cleanG.add_nodes_from(fGraph) # was formerly : positions = nx.graphviz_layout(cleanG, prog='neato') but this threw an error positions = graphviz_layout(cleanG, prog='neato') else: rawPositions = {n:fGraph.node[n].get('pos',(0,0)) for n in fGraph} #HACK: the import code reverses the y coords. def yFlip(pair): try: return (pair[0], -1.0*pair[1]) except: return (0,0) positions = {k:yFlip(rawPositions[k]) for k in rawPositions} # Plot all time steps. nodeVolts = {} for step, stamp in enumerate(rawOut['aVoltDump.csv']['# timestamp']): # Build voltage map. nodeVolts[step] = {} for nodeName in [x for x in rawOut['aVoltDump.csv'].keys() if x != '# timestamp']: allVolts = [] for phase in ['a','b','c']: voltStep = rawOut[phase + 'VoltDump.csv'][nodeName][step] # HACK: Gridlab complex number format sometimes uses i, sometimes j, sometimes d. WTF? if type(voltStep) is str: voltStep = voltStep.replace('i','j') v = complex(voltStep) phaseVolt = abs(v) if phaseVolt != 0.0: if _digits(phaseVolt)>3: # Normalize to 120 V standard phaseVolt = phaseVolt*(120/feedVoltage) allVolts.append(phaseVolt) # HACK: Take average of all phases to collapse dimensionality. nodeVolts[step][nodeName] = avg(allVolts) # Draw animation. voltChart = plt.figure(figsize=(15,15)) plt.axes(frameon = 0) plt.axis('off') #set axes step equal voltChart.gca().set_aspect('equal') custom_cm = matplotlib.colors.LinearSegmentedColormap.from_list('custColMap',[(0.0,'blue'),(0.25,'darkgray'),(0.75,'darkgray'),(1.0,'yellow')]) edgeIm = nx.draw_networkx_edges(fGraph, positions) nodeIm = nx.draw_networkx_nodes(fGraph, pos = positions, node_color = [nodeVolts[0].get(n,0) for n in fGraph.nodes()], linewidths = 0, node_size = 30, cmap = custom_cm) plt.sci(nodeIm) plt.clim(110,130) plt.colorbar() plt.title(rawOut['aVoltDump.csv']['# timestamp'][0]) def update(step): nodeColors = np.array([nodeVolts[step].get(n,0) for n in fGraph.nodes()]) plt.title(rawOut['aVoltDump.csv']['# timestamp'][step]) nodeIm.set_array(nodeColors) return nodeColors, anim = FuncAnimation(voltChart, update, frames=len(rawOut['aVoltDump.csv']['# timestamp']), interval=200, blit=False) anim.save(pJoin(modelDir,'voltageChart.mp4'), codec='h264', extra_args=['-pix_fmt', 'yuv420p']) # Reclaim memory by closing, deleting and garbage collecting the last chart. voltChart.clf() plt.close() del voltChart gc.collect() return genTime
def generateVoltChart(tree, rawOut, modelDir, neatoLayout=True): ''' Map the voltages on a feeder over time using a movie.''' # We need to timestamp frames with the system clock to make sure the browser caches them appropriately. genTime = str(datetime.datetime.now()).replace(':','.') # Detect the feeder nominal voltage: for key in tree: ob = tree[key] if type(ob)==dict and ob.get('bustype','')=='SWING': feedVoltage = float(ob.get('nominal_voltage',1)) # Make a graph object. fGraph = feeder.treeToNxGraph(tree) if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(fGraph.edges()) cleanG.add_nodes_from(fGraph) positions = graphviz_layout(cleanG, prog='neato') else: rawPositions = {n:fGraph.node[n].get('pos',(0,0)) for n in fGraph} #HACK: the import code reverses the y coords. def yFlip(pair): try: return (pair[0], -1.0*pair[1]) except: return (0,0) positions = {k:yFlip(rawPositions[k]) for k in rawPositions} # Plot all time steps. nodeVolts = {} for step, stamp in enumerate(rawOut['aVoltDump.csv']['# timestamp']): # Build voltage map. nodeVolts[step] = {} for nodeName in [x for x in rawOut['aVoltDump.csv'].keys() if x != '# timestamp']: allVolts = [] for phase in ['a','b','c']: voltStep = rawOut[phase + 'VoltDump.csv'][nodeName][step] # HACK: Gridlab complex number format sometimes uses i, sometimes j, sometimes d. WTF? if type(voltStep) is str: voltStep = voltStep.replace('i','j') v = complex(voltStep) phaseVolt = abs(v) if phaseVolt != 0.0: if _digits(phaseVolt)>3: # Normalize to 120 V standard phaseVolt = phaseVolt*(120/feedVoltage) allVolts.append(phaseVolt) # HACK: Take average of all phases to collapse dimensionality. nodeVolts[step][nodeName] = avg(allVolts) # Draw animation. voltChart = plt.figure(figsize=(15,15)) plt.axes(frameon = 0) plt.axis('off') #set axes step equal voltChart.gca().set_aspect('equal') custom_cm = matplotlib.colors.LinearSegmentedColormap.from_list('custColMap',[(0.0,'blue'),(0.25,'darkgray'),(0.75,'darkgray'),(1.0,'yellow')]) edgeIm = nx.draw_networkx_edges(fGraph, positions) nodeIm = nx.draw_networkx_nodes(fGraph, pos = positions, node_color = [nodeVolts[0].get(n,0) for n in fGraph.nodes()], linewidths = 0, node_size = 30, cmap = custom_cm) plt.sci(nodeIm) plt.clim(110,130) plt.colorbar() plt.title(rawOut['aVoltDump.csv']['# timestamp'][0]) def update(step): nodeColors = np.array([nodeVolts[step].get(n,0) for n in fGraph.nodes()]) plt.title(rawOut['aVoltDump.csv']['# timestamp'][step]) nodeIm.set_array(nodeColors) return nodeColors, anim = FuncAnimation(voltChart, update, frames=len(rawOut['aVoltDump.csv']['# timestamp']), interval=200, blit=False) anim.save(pJoin(modelDir,'voltageChart.mp4'), codec='h264', extra_args=['-pix_fmt', 'yuv420p']) # Reclaim memory by closing, deleting and garbage collecting the last chart. voltChart.clf() plt.close() del voltChart gc.collect() return genTime
def drawPlot(path, workDir=None, neatoLayout=False, edgeLabs=None, nodeLabs=None, edgeCol=None, nodeCol=None, faultLoc=None, faultType=None, customColormap=False, scaleMin=None, scaleMax=None, rezSqIn=400, simTime='2000-01-01 0:00:00', loadLoc=None): ''' Draw a color-coded map of the voltage drop on a feeder. path is the full path to the GridLAB-D .glm file or OMF .omd file. workDir is where GridLAB-D will run, if it's None then a temp dir is used. neatoLayout=True means the circuit is displayed using a force-layout approach. edgeCol property must be either 'Current', 'Power', 'Rating', 'PercentOfRating', or None nodeCol property must be either 'Voltage', 'VoltageImbalance', 'perUnitVoltage', 'perUnit120Voltage', or None edgeLabs and nodeLabs properties must be either 'Name', 'Value', or None edgeCol and nodeCol can be set to false to avoid coloring edges or nodes customColormap=True means use a one that is nicely scaled to perunit values highlighting extremes. faultType and faultLoc are the type of fault and the name of the line that it occurs on. Returns a matplotlib object.''' # Be quiet matplotlib: # warnings.filterwarnings("ignore") if path.endswith('.glm'): tree = feeder.parse(path) attachments = [] elif path.endswith('.omd'): with open(path) as f: omd = json.load(f) tree = omd.get('tree', {}) attachments = omd.get('attachments', []) else: raise Exception('Invalid input file type. We require a .glm or .omd.') #print path # add fault object to tree def safeInt(x): try: return int(x) except: return 0 biggestKey = max([safeInt(x) for x in tree.keys()]) # Add Reliability module tree[str(biggestKey * 10)] = { "module": "reliability", "maximum_event_length": "18000", "report_event_log": "true" } CLOCK_START = simTime dt_start = parser.parse(CLOCK_START) dt_end = dt_start + relativedelta(seconds=+20) CLOCK_END = str(dt_end) CLOCK_RANGE = CLOCK_START + ',' + CLOCK_END if faultType != None: # Add eventgen object (the fault) tree[str(biggestKey * 10 + 1)] = { "object": "eventgen", "name": "ManualEventGen", "parent": "RelMetrics", "fault_type": faultType, "manual_outages": faultLoc + ',' + CLOCK_RANGE } # TODO: change CLOCK_RANGE to read the actual start and stop time, not just hard-coded # Add fault_check object tree[str(biggestKey * 10 + 2)] = { "object": "fault_check", "name": "test_fault", "check_mode": "ONCHANGE", "eventgen_object": "ManualEventGen", "output_filename": "Fault_check_out.txt" } # Add reliabilty metrics object tree[str(biggestKey * 10 + 3)] = { "object": "metrics", "name": "RelMetrics", "report_file": "Metrics_Output.csv", "module_metrics_object": "PwrMetrics", "metrics_of_interest": '"SAIFI,SAIDI,CAIDI,ASAI,MAIFI"', "customer_group": '"groupid=METERTEST"', "metric_interval": "5 h", "report_interval": "5 h" } # Add power_metrics object tree[str(biggestKey * 10 + 4)] = { "object": "power_metrics", "name": "PwrMetrics", "base_time_value": "1 h" } # HACK: set groupid for all meters so outage stats are collected. noMeters = True for key in tree: if tree[key].get('object', '') in ['meter', 'triplex_meter']: tree[key]['groupid'] = "METERTEST" noMeters = False if noMeters: raise Exception( "No meters detected on the circuit. Please add at least one meter to allow for collection of outage statistics." ) for key in tree: if 'clock' in tree[key]: tree[key]['starttime'] = "'" + CLOCK_START + "'" tree[key]['stoptime'] = "'" + CLOCK_END + "'" # dictionary to hold info on lines present in glm edge_bools = dict.fromkeys([ 'underground_line', 'overhead_line', 'triplex_line', 'transformer', 'regulator', 'fuse', 'switch' ], False) # Map to speed up name lookups. nameToIndex = {tree[key].get('name', ''): key for key in tree.keys()} # Get rid of schedules and climate and check for all edge types: for key in list(tree.keys()): obtype = tree[key].get("object", "") if obtype == 'underground_line': edge_bools['underground_line'] = True elif obtype == 'overhead_line': edge_bools['overhead_line'] = True elif obtype == 'triplex_line': edge_bools['triplex_line'] = True elif obtype == 'transformer': edge_bools['transformer'] = True elif obtype == 'regulator': edge_bools['regulator'] = True elif obtype == 'fuse': edge_bools['fuse'] = True elif obtype == 'switch': edge_bools['switch'] = True if tree[key].get("argument", "") == "\"schedules.glm\"" or tree[key].get( "tmyfile", "") != "": del tree[key] # Make sure we have a voltage dump and current dump: tree[str(biggestKey * 10 + 5)] = { "object": "voltdump", "filename": "voltDump.csv" } tree[str(biggestKey * 10 + 6)] = { "object": "currdump", "filename": "currDump.csv" } # Line rating dumps tree[feeder.getMaxKey(tree) + 1] = {'module': 'tape'} for key in edge_bools.keys(): if edge_bools[key]: tree[feeder.getMaxKey(tree) + 1] = { 'object': 'group_recorder', 'group': '"class=' + key + '"', 'property': 'continuous_rating', 'file': key + '_cont_rating.csv' } #Record initial status readout of each fuse/recloser/switch/sectionalizer before running # Reminder: fuse objects have 'phase_X_status' instead of 'phase_X_state' protDevices = dict.fromkeys( ['fuse', 'recloser', 'switch', 'sectionalizer'], False) #dictionary of protective device initial states for each phase protDevInitStatus = {} #dictionary of protective devices final states for each phase after running Gridlab-D protDevFinalStatus = {} #dictionary of protective device types to help the testing and debugging process protDevTypes = {} protDevOpModes = {} for key in tree: obj = tree[key] obType = obj.get('object') if obType in protDevices.keys(): obName = obj.get('name', '') protDevTypes[obName] = obType if obType != 'fuse': protDevOpModes[obName] = obj.get('operating_mode', 'INDIVIDUAL') protDevices[obType] = True protDevInitStatus[obName] = {} protDevFinalStatus[obName] = {} for phase in ['A', 'B', 'C']: if obType != 'fuse': phaseState = obj.get('phase_' + phase + '_state', 'CLOSED') else: phaseState = obj.get('phase_' + phase + '_status', 'GOOD') if phase in obj.get('phases', ''): protDevInitStatus[obName][phase] = phaseState #print protDevInitStatus #Create a recorder for protective device states for key in protDevices.keys(): if protDevices[key]: for phase in ['A', 'B', 'C']: if key != 'fuse': tree[feeder.getMaxKey(tree) + 1] = { 'object': 'group_recorder', 'group': '"class=' + key + '"', 'property': 'phase_' + phase + '_state', 'file': key + '_phase_' + phase + '_state.csv' } else: tree[feeder.getMaxKey(tree) + 1] = { 'object': 'group_recorder', 'group': '"class=' + key + '"', 'property': 'phase_' + phase + '_status', 'file': key + '_phase_' + phase + '_state.csv' } # Run Gridlab. if not workDir: workDir = tempfile.mkdtemp() print('@@@@@@', workDir) # for i in range(6): # gridlabOut = gridlabd.runInFilesystem(tree, attachments=attachments, workDir=workDir) # #HACK: workaround for shoddy macOS gridlabd build. # if 'error when setting parent' not in gridlabOut.get('stderr','OOPS'): # break gridlabOut = gridlabd.runInFilesystem(tree, attachments=attachments, workDir=workDir) #Record final status readout of each fuse/recloser/switch/sectionalizer after running try: for key in protDevices.keys(): if protDevices[key]: for phase in ['A', 'B', 'C']: with open(pJoin(workDir, key + '_phase_' + phase + '_state.csv'), newline='') as statusFile: reader = csv.reader(statusFile) # loop past the header, keys = [] vals = [] for row in reader: if '# timestamp' in row: keys = row i = keys.index('# timestamp') keys.pop(i) vals = next(reader) vals.pop(i) for pos, key2 in enumerate(keys): protDevFinalStatus[key2][phase] = vals[pos] except: pass #print protDevFinalStatus #compare initial and final states of protective devices #quick compare to see if they are equal #print cmp(protDevInitStatus, protDevFinalStatus) #find which values changed changedStates = {} #read voltDump values into a dictionary. try: with open(pJoin(workDir, 'voltDump.csv'), newline='') as dumpFile: reader = csv.reader(dumpFile) next(reader) # Burn the header. keys = next(reader) voltTable = [] for row in reader: rowDict = {} for pos, key in enumerate(keys): rowDict[key] = row[pos] voltTable.append(rowDict) except: raise Exception( 'GridLAB-D failed to run with the following errors:\n' + gridlabOut['stderr']) # read currDump values into a dictionary with open(pJoin(workDir, 'currDump.csv'), newline='') as currDumpFile: reader = csv.reader(currDumpFile) next(reader) # Burn the header. keys = next(reader) currTable = [] for row in reader: rowDict = {} for pos, key in enumerate(keys): rowDict[key] = row[pos] currTable.append(rowDict) # read line rating values into a single dictionary lineRatings = {} rating_in_VA = [] for key1 in edge_bools.keys(): if edge_bools[key1]: with open(pJoin(workDir, key1 + '_cont_rating.csv'), newline='') as ratingFile: reader = csv.reader(ratingFile) # loop past the header, keys = [] vals = [] for row in reader: if '# timestamp' in row: keys = row i = keys.index('# timestamp') keys.pop(i) vals = next(reader) vals.pop(i) for pos, key2 in enumerate(keys): lineRatings[key2] = abs(float(vals[pos])) #edgeTupleRatings = lineRatings copy with to-from tuple as keys for labeling edgeTupleRatings = {} for edge in lineRatings: for obj in tree.values(): if obj.get('name', '').replace('"', '') == edge: nodeFrom = obj.get('from') nodeTo = obj.get('to') coord = (nodeFrom, nodeTo) ratingVal = lineRatings.get(edge) edgeTupleRatings[coord] = ratingVal # Calculate average node voltage deviation. First, helper functions. def digits(x): ''' Returns number of digits before the decimal in the float x. ''' return math.ceil(math.log10(x + 1)) def avg(l): ''' Average of a list of ints or floats. ''' # HACK: add a small value to the denominator to avoid divide by zero for out of service locations (i.e. zero voltage). return sum(l) / (len(l) + 0.00000000000000001) # Detect the feeder nominal voltage: for key in tree: ob = tree[key] if type(ob) == dict and ob.get('bustype', '') == 'SWING': feedVoltage = float(ob.get('nominal_voltage', 1)) # Tot it all up. nodeVolts = {} nodeVoltsPU = {} nodeVoltsPU120 = {} voltImbalances = {} for row in voltTable: allVolts = [] allVoltsPU = [] allDiffs = [] nodeName = row.get('node_name', '') for phase in ['A', 'B', 'C']: realVolt = abs(float(row['volt' + phase + '_real'])) imagVolt = abs(float(row['volt' + phase + '_imag'])) phaseVolt = math.sqrt((realVolt**2) + (imagVolt**2)) if phaseVolt != 0.0: treeKey = nameToIndex.get(nodeName, 0) nodeObj = tree.get(treeKey, {}) try: nominal_voltage = float(nodeObj['nominal_voltage']) except: nominal_voltage = feedVoltage allVolts.append(phaseVolt) normVolt = (phaseVolt / nominal_voltage) allVoltsPU.append(normVolt) avgVolts = avg(allVolts) avgVoltsPU = avg(allVoltsPU) avgVoltsPU120 = 120 * avgVoltsPU nodeVolts[nodeName] = float("{0:.2f}".format(avgVolts)) nodeVoltsPU[nodeName] = float("{0:.2f}".format(avgVoltsPU)) nodeVoltsPU120[nodeName] = float("{0:.2f}".format(avgVoltsPU120)) if len(allVolts) == 3: voltA = allVolts.pop() voltB = allVolts.pop() voltC = allVolts.pop() allDiffs.append(abs(float(voltA - voltB))) allDiffs.append(abs(float(voltA - voltC))) allDiffs.append(abs(float(voltB - voltC))) maxDiff = max(allDiffs) voltImbal = maxDiff / avgVolts voltImbalances[nodeName] = float("{0:.2f}".format(voltImbal)) # Use float("{0:.2f}".format(avg(allVolts))) if displaying the node labels nodeLoadNames = {} nodeNames = {} for key in nodeVolts.keys(): nodeNames[key] = key if key == loadLoc: nodeLoadNames[key] = "LOAD: " + key # find edge currents by parsing currdump edgeCurrentSum = {} edgeCurrentMax = {} for row in currTable: allCurr = [] for phase in ['A', 'B', 'C']: realCurr = abs(float(row['curr' + phase + '_real'])) imagCurr = abs(float(row['curr' + phase + '_imag'])) phaseCurr = math.sqrt((realCurr**2) + (imagCurr**2)) allCurr.append(phaseCurr) edgeCurrentSum[row.get('link_name', '')] = sum(allCurr) edgeCurrentMax[row.get('link_name', '')] = max(allCurr) # When just showing current as labels, use sum of the three lines' current values, when showing the per unit values (current/rating), use the max of the three #edgeTupleCurrents = edgeCurrents copy with to-from tuple as keys for labeling edgeTupleCurrents = {} #edgeValsPU = values normalized per unit by line ratings edgeValsPU = {} #edgeTupleValsPU = edgeValsPU copy with to-from tuple as keys for labeling edgeTupleValsPU = {} #edgeTuplePower = dict with to-from tuples as keys and sim power as values for debugging edgeTuplePower = {} #edgeTupleNames = dict with to-from tuples as keys and names as values for debugging edgeTupleNames = {} #edgeTupleFaultNames = dict with to-from tuples as keys and the name of the Fault as the only value edgeTupleFaultNames = {} #edgeTupleProtDevs = dict with to-from tuples as keys and the initial of the type of protective device as the value edgeTupleProtDevs = {} #linePhases = dictionary containing the number of phases on each line for line-width purposes linePhases = {} edgePower = {} for edge in edgeCurrentSum: for obj in tree.values(): obname = obj.get('name', '').replace('"', '') if obname == edge: objType = obj.get('object') nodeFrom = obj.get('from') nodeTo = obj.get('to') coord = (nodeFrom, nodeTo) currVal = edgeCurrentSum.get(edge) voltVal = avg([nodeVolts.get(nodeFrom), nodeVolts.get(nodeTo)]) power = (currVal * voltVal) / 1000 lineRating = lineRatings.get(edge, 10.0**9) edgePerUnitVal = (edgeCurrentMax.get(edge)) / lineRating edgeTupleCurrents[coord] = "{0:.2f}".format(currVal) edgeTuplePower[coord] = "{0:.2f}".format(power) edgePower[edge] = power edgeValsPU[edge] = edgePerUnitVal edgeTupleValsPU[coord] = "{0:.2f}".format(edgePerUnitVal) edgeTupleNames[coord] = edge if faultLoc == edge: edgeTupleFaultNames[coord] = "FAULT: " + edge phaseStr = obj.get('phases', '').replace('"', '').replace( 'N', '').replace('S', '') numPhases = len(phaseStr) if (numPhases < 1) or (numPhases > 3): numPhases = 1 linePhases[edge] = numPhases protDevLabel = "" protDevBlownStr = "" if objType in protDevices.keys(): for phase in protDevFinalStatus[obname].keys(): if objType == 'fuse': if protDevFinalStatus[obname][phase] == "BLOWN": protDevBlownStr = "!" else: if protDevFinalStatus[obname][phase] == "OPEN": protDevBlownStr = "!" if objType == 'fuse': protDevLabel = 'F' elif objType == 'switch': protDevLabel = 'S' elif objType == 'recloser': protDevLabel = 'R' elif objType == 'sectionalizer': protDevLabel = 'X' edgeTupleProtDevs[coord] = protDevLabel + protDevBlownStr #define which dict will be used for edge line color edgeColors = edgeValsPU #define which dict will be used for edge label edgeLabels = edgeTupleValsPU # Build the graph. fGraph = feeder.treeToNxGraph(tree) # TODO: consider whether we can set figsize dynamically. wlVal = int(math.sqrt(float(rezSqIn))) voltChart = plt.figure(figsize=(wlVal, wlVal)) plt.axes(frameon=0) plt.axis('off') voltChart.gca().set_aspect('equal') plt.tight_layout() #set axes step equal if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(fGraph.edges()) cleanG.add_nodes_from(fGraph) positions = graphviz_layout(cleanG, prog='neato') else: remove_nodes = [ n for n in fGraph if fGraph.nodes[n].get('pos', (0, 0)) == (0, 0) ] fGraph.remove_nodes_from(remove_nodes) positions = {n: fGraph.nodes[n].get('pos', (0, 0)) for n in fGraph} # Need to get edge names from pairs of connected node names. edgeNames = [] for e in fGraph.edges(): edgeNames.append((fGraph.edges[e].get('name', 'BLANK')).replace('"', '')) #create custom colormap if customColormap: if scaleMin != None and scaleMax != None: scaleDif = scaleMax - scaleMin custom_cm = matplotlib.colors.LinearSegmentedColormap.from_list( 'custColMap', [(scaleMin, 'blue'), (scaleMin + (0.12 * scaleDif), 'darkgray'), (scaleMin + (0.56 * scaleDif), 'darkgray'), (scaleMin + (0.8 * scaleDif), 'red')]) vmin = scaleMin vmax = scaleMax else: custom_cm = matplotlib.colors.LinearSegmentedColormap.from_list( 'custColMap', [(0.0, 'blue'), (0.15, 'darkgray'), (0.7, 'darkgray'), (1.0, 'red')]) vmin = 0 vmax = 1.25 custom_cm.set_under(color='black') else: custom_cm = plt.cm.get_cmap('viridis') if scaleMin != None and scaleMax != None: vmin = scaleMin vmax = scaleMax else: vmin = None vmax = None drawColorbar = False emptyColors = {} #draw edges with or without colors if edgeCol != None: drawColorbar = True if edgeCol == "Current": edgeList = [edgeCurrentSum.get(n, 1) for n in edgeNames] drawColorbar = True elif edgeCol == "Power": edgeList = [edgePower.get(n, 1) for n in edgeNames] drawColorbar = True elif edgeCol == "Rating": edgeList = [lineRatings.get(n, 10.0**9) for n in edgeNames] drawColorbar = True elif edgeCol == "PercentOfRating": edgeList = [edgeValsPU.get(n, .5) for n in edgeNames] drawColorbar = True else: edgeList = [emptyColors.get(n, .6) for n in edgeNames] print( "WARNING: edgeCol property must be 'Current', 'Power', 'Rating', 'PercentOfRating', or None" ) else: edgeList = [emptyColors.get(n, .6) for n in edgeNames] edgeIm = nx.draw_networkx_edges( fGraph, pos=positions, edge_color=edgeList, width=[linePhases.get(n, 1) for n in edgeNames], edge_cmap=custom_cm) #draw edge labels if edgeLabs != None: if edgeLabs == "Name": edgeLabels = edgeTupleNames elif edgeLabs == "Fault": edgeLabels = edgeTupleFaultNames elif edgeLabs == "Value": if edgeCol == "Current": edgeLabels = edgeTupleCurrents elif edgeCol == "Power": edgeLabels = edgeTuplePower elif edgeCol == "Rating": edgeLabels = edgeTupleRatings elif edgeCol == "PercentOfRating": edgeLabels = edgeTupleValsPU else: edgeLabels = None print( "WARNING: edgeCol property cannot be set to None when edgeLabs property is set to 'Value'" ) elif edgeLabs == "ProtDevs": edgeLabels = edgeTupleProtDevs else: edgeLabs = None print( "WARNING: edgeLabs property must be either 'Name', 'Value', or None" ) if edgeLabs != None: edgeLabelsIm = nx.draw_networkx_edge_labels(fGraph, pos=positions, edge_labels=edgeLabels, font_size=8) # draw nodes with or without color if nodeCol != None: if nodeCol == "Voltage": nodeList = [nodeVolts.get(n, 1) for n in fGraph.nodes()] drawColorbar = True elif nodeCol == "VoltageImbalance": nodeList = [voltImbalances.get(n, 1) for n in fGraph.nodes()] drawColorbar = True elif nodeCol == "perUnitVoltage": nodeList = [nodeVoltsPU.get(n, .5) for n in fGraph.nodes()] drawColorbar = True elif nodeCol == "perUnit120Voltage": nodeList = [nodeVoltsPU120.get(n, 120) for n in fGraph.nodes()] drawColorbar = True else: nodeList = [emptyColors.get(n, 1) for n in fGraph.nodes()] print( "WARNING: nodeCol property must be 'Voltage', 'VoltageImbalance', 'perUnitVoltage', 'perUnit120Voltage', or None" ) else: nodeList = [emptyColors.get(n, .6) for n in fGraph.nodes()] nodeIm = nx.draw_networkx_nodes(fGraph, pos=positions, node_color=nodeList, linewidths=0, node_size=30, vmin=vmin, vmax=vmax, cmap=custom_cm) #draw node labels nodeLabels = {} if nodeLabs != None: if nodeLabs == "Name": nodeLabels = nodeNames elif nodeLabs == "Value": if nodeCol == "Voltage": nodeLabels = nodeVolts elif nodeCol == "VoltageImbalance": nodeLabels = voltImbalances elif nodeCol == "perUnitVoltage": nodeLabels = nodeVoltsPU elif nodeCol == "perUnit120Voltage": nodeLabels = nodeVoltsPU120 else: nodeLabels = None print( "WARNING: nodeCol property cannot be set to None when nodeLabs property is set to 'Value'" ) #HACK: add hidden node label option for displaying specified load name elif nodeLabs == "Load": nodeLabels = nodeLoadNames else: nodeLabs = None print( "WARNING: nodeLabs property must be either 'Name', 'Value', or None" ) if nodeLabs != None: nodeLabelsIm = nx.draw_networkx_labels(fGraph, pos=positions, labels=nodeLabels, font_size=8) plt.sci(nodeIm) # plt.clim(110,130) if drawColorbar: plt.colorbar() return voltChart
def work(modelDir, inputDict): ''' Run the model in the foreground. WARNING: can take about a minute. ''' # Global vars, and load data from the model directory. feederName = [x for x in os.listdir(modelDir) if x.endswith('.omd')][0][:-4] inputDict["feederName1"] = feederName feederPath = pJoin(modelDir,feederName+'.omd') feederJson = json.load(open(feederPath)) tree = feederJson.get("tree",{}) attachments = feederJson.get("attachments",{}) outData = {} ''' Run CVR analysis. ''' # Reformate monthData and rates. rates = {k:float(inputDict[k]) for k in ['capitalCost', 'omCost', 'wholesaleEnergyCostPerKwh', 'retailEnergyCostPerKwh', 'peakDemandCostSpringPerKw', 'peakDemandCostSummerPerKw', 'peakDemandCostFallPerKw', 'peakDemandCostWinterPerKw']} monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] monthToSeason = {'January':'Winter','February':'Winter','March':'Spring','April':'Spring', 'May':'Spring','June':'Summer','July':'Summer','August':'Summer', 'September':'Fall','October':'Fall','November':'Fall','December':'Winter'} monthData = [] for i, x in enumerate(monthNames): monShort = x[0:3].lower() season = monthToSeason[x] histAvg = float(inputDict.get(monShort + "Avg", 0)) histPeak = float(inputDict.get(monShort + "Peak", 0)) monthData.append({"monthId":i, "monthName":x, "histAverage":histAvg, "histPeak":histPeak, "season":season}) # Graph the SCADA data. fig = plt.figure(figsize=(10,6)) indices = [r['monthName'] for r in monthData] d1 = [r['histPeak']/(10**3) for r in monthData] d2 = [r['histAverage']/(10**3) for r in monthData] ticks = range(len(d1)) bar_peak = plt.bar(ticks,d1,color='gray') bar_avg = plt.bar(ticks,d2,color='dimgray') plt.legend([bar_peak[0],bar_avg[0]],['histPeak','histAverage'],bbox_to_anchor=(0., 1.015, 1., .102), loc=3, ncol=2, mode="expand", borderaxespad=0.1) plt.xticks([t+0.5 for t in ticks],indices) plt.ylabel('Mean and peak historical power consumptions (kW)') fig.autofmt_xdate() plt.savefig(pJoin(modelDir,"scadaChart.png")) outData["histPeak"] = d1 outData["histAverage"] = d2 outData["monthName"] = [name[0:3] for name in monthNames] # Graph feeder. fig = plt.figure(figsize=(10,10)) myGraph = feeder.treeToNxGraph(tree) feeder.latLonNxGraph(myGraph, neatoLayout=False) plt.savefig(pJoin(modelDir,"feederChart.png")) with open(pJoin(modelDir,"feederChart.png"),"rb") as inFile: outData["feederChart"] = inFile.read().encode("base64") # Get the load levels we need to test. allLoadLevels = [x.get('histPeak',0) for x in monthData] + [y.get('histAverage',0) for y in monthData] maxLev = _roundOne(max(allLoadLevels),'up') minLev = _roundOne(min(allLoadLevels),'down') tenLoadLevels = range(int(minLev),int(maxLev),int((maxLev-minLev)/10)) # Gather variables from the feeder. for key in tree.keys(): # Set clock to single timestep. if tree[key].get('clock','') == 'clock': tree[key] = {"timezone":"PST+8PDT", "stoptime":"'2013-01-01 00:00:00'", "starttime":"'2013-01-01 00:00:00'", "clock":"clock"} # Save swing node index. if tree[key].get('bustype','').lower() == 'swing': swingIndex = key swingName = tree[key].get('name') # Remove all includes. if tree[key].get('omftype','') == '#include': del key # Find the substation regulator and config. for key in tree: if tree[key].get('object','') == 'regulator' and tree[key].get('from','') == swingName: regIndex = key regConfName = tree[key]['configuration'] if not regConfName: regConfName = False for key in tree: if tree[key].get('name','') == regConfName: regConfIndex = key # Set substation regulator to manual operation. baselineTap = int(inputDict.get("baselineTap")) # GLOBAL VARIABLE FOR DEFAULT TAP POSITION tree[regConfIndex] = { 'name':tree[regConfIndex]['name'], 'object':'regulator_configuration', 'connect_type':'1', 'raise_taps':'10', 'lower_taps':'10', 'CT_phase':'ABC', 'PT_phase':'ABC', 'regulation':'0.10', #Yo, 0.10 means at tap_pos 10 we're 10% above 120V. 'Control':'MANUAL', 'control_level':'INDIVIDUAL', 'Type':'A', 'tap_pos_A':str(baselineTap), 'tap_pos_B':str(baselineTap), 'tap_pos_C':str(baselineTap) } # Attach recorders relevant to CVR. recorders = [ {'object': 'collector', 'file': 'ZlossesTransformer.csv', 'group': 'class=transformer', 'limit': '0', 'property': 'sum(power_losses_A.real),sum(power_losses_A.imag),sum(power_losses_B.real),sum(power_losses_B.imag),sum(power_losses_C.real),sum(power_losses_C.imag)'}, {'object': 'collector', 'file': 'ZlossesUnderground.csv', 'group': 'class=underground_line', 'limit': '0', 'property': 'sum(power_losses_A.real),sum(power_losses_A.imag),sum(power_losses_B.real),sum(power_losses_B.imag),sum(power_losses_C.real),sum(power_losses_C.imag)'}, {'object': 'collector', 'file': 'ZlossesOverhead.csv', 'group': 'class=overhead_line', 'limit': '0', 'property': 'sum(power_losses_A.real),sum(power_losses_A.imag),sum(power_losses_B.real),sum(power_losses_B.imag),sum(power_losses_C.real),sum(power_losses_C.imag)'}, {'object': 'recorder', 'file': 'Zregulator.csv', 'limit': '0', 'parent': tree[regIndex]['name'], 'property': 'tap_A,tap_B,tap_C,power_in.real,power_in.imag'}, {'object': 'collector', 'file': 'ZvoltageJiggle.csv', 'group': 'class=triplex_meter', 'limit': '0', 'property': 'min(voltage_12.mag),mean(voltage_12.mag),max(voltage_12.mag),std(voltage_12.mag)'}, {'object': 'recorder', 'file': 'ZsubstationTop.csv', 'limit': '0', 'parent': tree[swingIndex]['name'], 'property': 'voltage_A,voltage_B,voltage_C'}, {'object': 'recorder', 'file': 'ZsubstationBottom.csv', 'limit': '0', 'parent': tree[regIndex]['to'], 'property': 'voltage_A,voltage_B,voltage_C'} ] biggest = 1 + max([int(k) for k in tree.keys()]) for index, rec in enumerate(recorders): tree[biggest + index] = rec # Change constant PF loads to ZIP loads. (See evernote for rationale about 50/50 power/impedance mix.) blankZipModel = {'object':'triplex_load', 'name':'NAMEVARIABLE', 'base_power_12':'POWERVARIABLE', 'power_fraction_12': str(inputDict.get("p_percent")), 'impedance_fraction_12': str(inputDict.get("z_percent")), 'current_fraction_12': str(inputDict.get("i_percent")), 'power_pf_12': str(inputDict.get("power_factor")), #MAYBEFIX: we can probably get this PF data from the Milsoft loads. 'impedance_pf_12':str(inputDict.get("power_factor")), 'current_pf_12':str(inputDict.get("power_factor")), 'nominal_voltage':'120', 'phases':'PHASESVARIABLE', 'parent':'PARENTVARIABLE' } def powerClean(powerStr): ''' take 3339.39+1052.29j to 3339.39 ''' return powerStr[0:powerStr.find('+')] for key in tree: if tree[key].get('object','') == 'triplex_node': # Get existing variables. name = tree[key].get('name','') power = tree[key].get('power_12','') parent = tree[key].get('parent','') phases = tree[key].get('phases','') # Replace object and reintroduce variables. tree[key] = copy(blankZipModel) tree[key]['name'] = name tree[key]['base_power_12'] = powerClean(power) tree[key]['parent'] = parent tree[key]['phases'] = phases # Function to determine how low we can tap down in the CVR case: def loweringPotential(baseLine): ''' Given a baseline end of line voltage, how many more percent can we shave off the substation voltage? ''' ''' testsWePass = [122.0,118.0,200.0,110.0] ''' lower = int(math.floor((baseLine/114.0-1)*100)) - 1 # If lower is negative, we can't return it because we'd be undervolting beyond what baseline already was! if lower < 0: return baselineTap else: return baselineTap - lower # Run all the powerflows. powerflows = [] for doingCvr in [False, True]: # For each load level in the tenLoadLevels, run a powerflow with the load objects scaled to the level. for desiredLoad in tenLoadLevels: # Find the total load that was defined in Milsoft: loadList = [] for key in tree: if tree[key].get('object','') == 'triplex_load': loadList.append(tree[key].get('base_power_12','')) totalLoad = sum([float(x) for x in loadList]) # Rescale each triplex load: for key in tree: if tree[key].get('object','') == 'triplex_load': currentPow = float(tree[key]['base_power_12']) ratio = desiredLoad/totalLoad tree[key]['base_power_12'] = str(currentPow*ratio) # If we're doing CVR then lower the voltage. if doingCvr: # Find the minimum voltage we can tap down to: newTapPos = baselineTap for row in powerflows: if row.get('loadLevel','') == desiredLoad: newTapPos = loweringPotential(row.get('lowVoltage',114)) # Tap it down to there. # MAYBEFIX: do each phase separately because that's how it's done in the field... Oof. tree[regConfIndex]['tap_pos_A'] = str(newTapPos) tree[regConfIndex]['tap_pos_B'] = str(newTapPos) tree[regConfIndex]['tap_pos_C'] = str(newTapPos) # Run the model through gridlab and put outputs in the table. output = gridlabd.runInFilesystem(tree, attachments=attachments, keepFiles=True, workDir=modelDir) os.remove(pJoin(modelDir,"PID.txt")) p = output['Zregulator.csv']['power_in.real'][0] q = output['Zregulator.csv']['power_in.imag'][0] s = math.sqrt(p**2+q**2) lossTotal = 0.0 for device in ['ZlossesOverhead.csv','ZlossesTransformer.csv','ZlossesUnderground.csv']: for letter in ['A','B','C']: r = output[device]['sum(power_losses_' + letter + '.real)'][0] i = output[device]['sum(power_losses_' + letter + '.imag)'][0] lossTotal += math.sqrt(r**2 + i**2) ## Entire output: powerflows.append({ 'doingCvr':doingCvr, 'loadLevel':desiredLoad, 'realPower':p, 'powerFactor':p/s, 'losses':lossTotal, 'subVoltage': ( output['ZsubstationBottom.csv']['voltage_A'][0] + output['ZsubstationBottom.csv']['voltage_B'][0] + output['ZsubstationBottom.csv']['voltage_C'][0] )/3/60, 'lowVoltage':output['ZvoltageJiggle.csv']['min(voltage_12.mag)'][0]/2, 'highVoltage':output['ZvoltageJiggle.csv']['max(voltage_12.mag)'][0]/2 }) # For a given load level, find two points to interpolate on. def getInterpPoints(t): ''' Find the two points we can interpolate from. ''' ''' tests pass on [tenLoadLevels[0],tenLoadLevels[5]+499,tenLoadLevels[-1]-988] ''' loc = sorted(tenLoadLevels + [t]).index(t) if loc==0: return (tenLoadLevels[0],tenLoadLevels[1]) elif loc>len(tenLoadLevels)-2: return (tenLoadLevels[-2],tenLoadLevels[-1]) else: return (tenLoadLevels[loc-1],tenLoadLevels[loc+1]) # Calculate peak reduction. for row in monthData: peak = row['histPeak'] peakPoints = getInterpPoints(peak) peakTopBase = [x for x in powerflows if x.get('loadLevel','') == peakPoints[-1] and x.get('doingCvr','') == False][0] peakTopCvr = [x for x in powerflows if x.get('loadLevel','') == peakPoints[-1] and x.get('doingCvr','') == True][0] peakBottomBase = [x for x in powerflows if x.get('loadLevel','') == peakPoints[0] and x.get('doingCvr','') == False][0] peakBottomCvr = [x for x in powerflows if x.get('loadLevel','') == peakPoints[0] and x.get('doingCvr','') == True][0] # Linear interpolation so we aren't running umpteen million loadflows. x = (peakPoints[0],peakPoints[1]) y = (peakTopBase['realPower'] - peakTopCvr['realPower'], peakBottomBase['realPower'] - peakBottomCvr['realPower']) peakRed = y[0] + (y[1] - y[0]) * (peak - x[0]) / (x[1] - x[0]) row['peakReduction'] = peakRed # Calculate energy reduction and loss reduction based on average load. for row in monthData: avgEnergy = row['histAverage'] energyPoints = getInterpPoints(avgEnergy) avgTopBase = [x for x in powerflows if x.get('loadLevel','') == energyPoints[-1] and x.get('doingCvr','') == False][0] avgTopCvr = [x for x in powerflows if x.get('loadLevel','') == energyPoints[-1] and x.get('doingCvr','') == True][0] avgBottomBase = [x for x in powerflows if x.get('loadLevel','') == energyPoints[0] and x.get('doingCvr','') == False][0] avgBottomCvr = [x for x in powerflows if x.get('loadLevel','') == energyPoints[0] and x.get('doingCvr','') == True][0] # Linear interpolation so we aren't running umpteen million loadflows. x = (energyPoints[0], energyPoints[1]) y = (avgTopBase['realPower'] - avgTopCvr['realPower'], avgBottomBase['realPower'] - avgBottomCvr['realPower']) energyRed = y[0] + (y[1] - y[0]) * (avgEnergy - x[0]) / (x[1] - x[0]) row['energyReduction'] = energyRed lossY = (avgTopBase['losses'] - avgTopCvr['losses'], avgBottomBase['losses'] - avgBottomCvr['losses']) lossRed = lossY[0] + (lossY[1] - lossY[0]) * (avgEnergy - x[0]) / (x[1] - x[0]) row['lossReduction'] = lossRed # Multiply by dollars. for row in monthData: row['energyReductionDollars'] = row['energyReduction']/1000 * (rates['wholesaleEnergyCostPerKwh'] - rates['retailEnergyCostPerKwh']) row['peakReductionDollars'] = row['peakReduction']/1000 * rates['peakDemandCost' + row['season'] + 'PerKw'] row['lossReductionDollars'] = row['lossReduction']/1000 * rates['wholesaleEnergyCostPerKwh'] # Pretty output def plotTable(inData): fig = plt.figure(figsize=(10,5)) plt.axis('off') plt.tight_layout() plt.table(cellText=[row for row in inData[1:]], loc = 'center', rowLabels = range(len(inData)-1), colLabels = inData[0]) def dictalToMatrix(dictList): ''' Take our dictal format to a matrix. ''' matrix = [dictList[0].keys()] for row in dictList: matrix.append(row.values()) return matrix # Powerflow results. plotTable(dictalToMatrix(powerflows)) plt.savefig(pJoin(modelDir,"powerflowTable.png")) # Monetary results. ## To print partial money table monthDataMat = dictalToMatrix(monthData) dimX = len(monthDataMat) dimY = len(monthDataMat[0]) monthDataPart = [] for k in range (0,dimX): monthDatatemp = [] for m in range (4,dimY): monthDatatemp.append(monthDataMat[k][m]) monthDataPart.append(monthDatatemp) plotTable(monthDataPart) plt.savefig(pJoin(modelDir,"moneyTable.png")) outData["monthDataMat"] = dictalToMatrix(monthData) outData["monthDataPart"] = monthDataPart # Graph the money data. fig = plt.figure(figsize=(10,8)) indices = [r['monthName'] for r in monthData] d1 = [r['energyReductionDollars'] for r in monthData] d2 = [r['lossReductionDollars'] for r in monthData] d3 = [r['peakReductionDollars'] for r in monthData] ticks = range(len(d1)) bar_erd = plt.bar(ticks,d1,color='red') bar_lrd = plt.bar(ticks,d2,color='green') bar_prd = plt.bar(ticks,d3,color='blue',yerr=d2) plt.legend([bar_prd[0], bar_lrd[0], bar_erd[0]], ['peakReductionDollars','lossReductionDollars','energyReductionDollars'],bbox_to_anchor=(0., 1.015, 1., .102), loc=3, ncol=2, mode="expand", borderaxespad=0.1) plt.xticks([t+0.5 for t in ticks],indices) plt.ylabel('Utility Savings ($)') plt.tight_layout(5.5,1.3,1.2) fig.autofmt_xdate() plt.savefig(pJoin(modelDir,"spendChart.png")) outData["energyReductionDollars"] = d1 outData["lossReductionDollars"] = d2 outData["peakReductionDollars"] = d3 # Graph the cumulative savings. fig = plt.figure(figsize=(10,5)) annualSavings = sum(d1) + sum(d2) + sum(d3) annualSave = lambda x:(annualSavings - rates['omCost']) * x - rates['capitalCost'] simplePayback = rates['capitalCost']/(annualSavings - rates['omCost']) plt.xlabel('Year After Installation') plt.xlim(0,30) plt.ylabel('Cumulative Savings ($)') plt.plot([0 for x in range(31)],c='gray') plt.axvline(x=simplePayback, ymin=0, ymax=1, c='gray', linestyle='--') plt.plot([annualSave(x) for x in range(31)], c='green') plt.savefig(pJoin(modelDir,"savingsChart.png")) outData["annualSave"] = [annualSave(x) for x in range(31)] # For autotest, there won't be such file. return outData
def drawPlot(tree, nodeDict=None, edgeDict=None, edgeLabsDict=None, displayLabs=False, customColormap=False, perUnitScale=False, rezSqIn=400, neatoLayout=False, nodeFlagBounds=[-float('inf'), float('inf')], defaultNodeVal=1): ''' Draw a color-coded map of the voltage drop on a feeder. path is the full path to the GridLAB-D .glm file or OMF .omd file. workDir is where GridLAB-D will run, if it's None then a temp dir is used. neatoLayout=True means the circuit is displayed using a force-layout approach. edgeLabs property must be either 'Name', 'Current', 'Power', 'Rating', 'PercentOfRating', or None nodeLabs property must be either 'Name', 'Voltage', 'VoltageImbalance', or None edgeCol and nodeCol can be set to false to avoid coloring edges or nodes customColormap=True means use a one that is nicely scaled to perunit values highlighting extremes. Returns a matplotlib object.''' # Be quiet matplotlib: # warnings.filterwarnings('ignore') # Build the graph. fGraph = feeder.treeToNxGraph(tree) # TODO: consider whether we can set figsize dynamically. wlVal = int(math.sqrt(float(rezSqIn))) chart = plt.figure(figsize=(wlVal, wlVal)) plt.axes(frameon=0) plt.axis('off') chart.gca().set_aspect('equal') plt.tight_layout() # Need to get edge names from pairs of connected node names. edgeNames = [] for e in fGraph.edges(): edgeNames.append((fGraph.edges[e].get('name', 'BLANK')).replace('"', '')) #set axes step equal if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(fGraph.edges()) cleanG.add_nodes_from(fGraph) positions = graphviz_layout(cleanG, prog='neato') else: positions = { n: fGraph.nodes[n].get('pos', (0, 0))[::-1] for n in fGraph } #create custom colormap if customColormap: custom_cm = matplotlib.colors.LinearSegmentedColormap.from_list( 'custColMap', [(0.0, 'blue'), (0.15, 'darkgray'), (0.7, 'darkgray'), (1.0, 'red')]) custom_cm.set_under(color='black') else: custom_cm = plt.cm.get_cmap('viridis') drawColorbar = False emptyColors = {} #draw edges with or without colors if edgeDict != None: drawColorbar = True edgeList = [edgeDict.get(n, 1) for n in edgeNames] else: edgeList = [emptyColors.get(n, .6) for n in edgeNames] edgeIm = nx.draw_networkx_edges(fGraph, pos=positions, edge_color=edgeList, width=1, edge_cmap=custom_cm) #draw edge labels if displayLabs: edgeLabelsIm = nx.draw_networkx_edge_labels(fGraph, pos=positions, edge_labels=edgeLabsDict, font_size=8) # draw nodes with or without color if nodeDict != None: nodeList = [nodeDict.get(n, defaultNodeVal) for n in fGraph.nodes()] drawColorbar = True else: nodeList = [emptyColors.get(n, .6) for n in fGraph.nodes()] if perUnitScale: vmin = 0 vmax = 1.25 else: vmin = None vmax = None edgecolors = ['None'] * len(nodeList) for i in range(len(nodeList)): if nodeList[i] < nodeFlagBounds[0]: edgecolors[i] = '#ffa500' if nodeList[i] > nodeFlagBounds[1]: edgecolors[i] = 'r' nodeIm = nx.draw_networkx_nodes(fGraph, pos=positions, node_color=nodeList, edgecolors=edgecolors, linewidths=2, node_size=30, vmin=vmin, vmax=vmax, cmap=custom_cm) #draw node labels if displayLabs and nodeDict != None: nodeLabelsIm = nx.draw_networkx_labels(fGraph, pos=positions, labels=nodeDict, font_size=8) plt.sci(nodeIm) # plt.clim(110,130) if drawColorbar: plt.colorbar() return chart
def voltPlot(tree, workDir=None, neatoLayout=False): """ Draw a color-coded map of the voltage drop on a feeder. Returns a matplotlib object. """ # Get rid of schedules and climate: for key in tree.keys(): if tree[key].get("argument", "") == '"schedules.glm"' or tree[key].get("tmyfile", "") != "": del tree[key] # Make sure we have a voltDump: def safeInt(x): try: return int(x) except: return 0 biggestKey = max([safeInt(x) for x in tree.keys()]) tree[str(biggestKey * 10)] = {"object": "voltdump", "filename": "voltDump.csv"} # Run Gridlab. if not workDir: workDir = tempfile.mkdtemp() print "gridlabD runInFilesystem with no specified workDir. Working in", workDir gridlabOut = gridlabd.runInFilesystem(tree, attachments=[], workDir=workDir) with open(pJoin(workDir, "voltDump.csv"), "r") as dumpFile: reader = csv.reader(dumpFile) reader.next() # Burn the header. keys = reader.next() voltTable = [] for row in reader: rowDict = {} for pos, key in enumerate(keys): rowDict[key] = row[pos] voltTable.append(rowDict) # Calculate average node voltage deviation. First, helper functions. def pythag(x, y): """ For right triangle with sides a and b, return the hypotenuse. """ return math.sqrt(x ** 2 + y ** 2) def digits(x): """ Returns number of digits before the decimal in the float x. """ return math.ceil(math.log10(x + 1)) def avg(l): """ Average of a list of ints or floats. """ return sum(l) / len(l) # Detect the feeder nominal voltage: for key in tree: ob = tree[key] if type(ob) == dict and ob.get("bustype", "") == "SWING": feedVoltage = float(ob.get("nominal_voltage", 1)) # Tot it all up. nodeVolts = {} for row in voltTable: allVolts = [] for phase in ["A", "B", "C"]: phaseVolt = pythag(float(row["volt" + phase + "_real"]), float(row["volt" + phase + "_imag"])) if phaseVolt != 0.0: if digits(phaseVolt) > 3: # Normalize to 120 V standard phaseVolt = phaseVolt * (120 / feedVoltage) allVolts.append(phaseVolt) nodeVolts[row.get("node_name", "")] = avg(allVolts) # Color nodes by VOLTAGE. fGraph = feeder.treeToNxGraph(tree) voltChart = plt.figure(figsize=(15, 15)) plt.axes(frameon=0) plt.axis("off") # set axes step equal voltChart.gca().set_aspect("equal") if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(fGraph.edges()) cleanG.add_nodes_from(fGraph) positions = nx.graphviz_layout(cleanG, prog="neato") else: positions = {n: fGraph.node[n].get("pos", (0, 0)) for n in fGraph} edgeIm = nx.draw_networkx_edges(fGraph, positions) nodeIm = nx.draw_networkx_nodes( fGraph, pos=positions, node_color=[nodeVolts.get(n, 0) for n in fGraph.nodes()], linewidths=0, node_size=30, cmap=plt.cm.jet, ) plt.sci(nodeIm) plt.clim(110, 130) plt.colorbar() return voltChart
import omf.feeder as feeder, json import os, networkx as nx from os.path import join as pJoin from networkx.drawing.nx_agraph import graphviz_layout _myDir = os.path.dirname(os.path.abspath(__file__)) IN_PATH_OMD = pJoin(_myDir,'superModel Tomorrow.omd') OUT_PATH_OMD = pJoin(_myDir,'superModel Tomorrow with latlons.omd') with open(IN_PATH_OMD,'r') as jsonFile: omd = json.load(jsonFile) tree = omd['tree'] # Use graphviz to lay out the graph. inGraph = feeder.treeToNxGraph(tree) # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(inGraph.edges()) # HACK2: might miss nodes without edges without the following. cleanG.add_nodes_from(inGraph) pos = graphviz_layout(cleanG, prog='neato') # # Charting the feeder in matplotlib: # feeder.latLonNxGraph(inGraph, labels=False, neatoLayout=True, showPlot=True) # Insert the latlons. for key in tree: obName = tree[key].get('name','') thisPos = pos.get(obName, None) if thisPos != None: tree[key]['longitude'] = thisPos[0] tree[key]['latitude'] = thisPos[1] with open(OUT_PATH_OMD,'w') as outFile:
def voltPlot(omd, workDir=None, neatoLayout=False): ''' Draw a color-coded map of the voltage drop on a feeder. Returns a matplotlib object. ''' tree = omd.get('tree', {}) # # Get rid of schedules and climate: for key in tree.keys(): if tree[key].get("argument", "") == "\"schedules.glm\"" or tree[key].get( "tmyfile", "") != "": del tree[key] # Map to speed up name lookups. nameToIndex = {tree[key].get('name', ''): key for key in tree.keys()} # Make sure we have a voltDump: def safeInt(x): try: return int(x) except: return 0 biggestKey = max([safeInt(x) for x in tree.keys()]) tree[str(biggestKey * 10)] = { "object": "voltdump", "filename": "voltDump.csv" } # Run Gridlab. if not workDir: workDir = tempfile.mkdtemp() gridlabOut = omf.solvers.gridlabd_gridballast.runInFilesystem( tree, attachments=omd.get('attachments', {}), workDir=workDir) with open(pJoin(workDir, 'voltDump.csv'), 'r') as dumpFile: reader = csv.reader(dumpFile) reader.next() # Burn the header. keys = reader.next() voltTable = [] for row in reader: rowDict = {} for pos, key in enumerate(keys): rowDict[key] = row[pos] voltTable.append(rowDict) # Calculate average node voltage deviation. First, helper functions. def digits(x): ''' Returns number of digits before the decimal in the float x. ''' return math.ceil(math.log10(x + 1)) def avg(l): ''' Average of a list of ints or floats. ''' return sum(l) / len(l) # Use the swing bus voltage as a reasonable default voltage. for key in tree: ob = tree[key] if type(ob) == dict and ob.get('bustype', '') == 'SWING': swingVoltage = float(ob.get('nominal_voltage', 1)) # Tot it all up. nodeVolts = {} for row in voltTable: allVolts = [] for phase in ['A', 'B', 'C']: realV = float(row['volt' + phase + '_real']) imagV = float(row['volt' + phase + '_imag']) phaseVolt = math.hypot(realV, imagV) if phaseVolt != 0.0: if digits(phaseVolt) > 3: nodeName = row.get('node_name', '') treeKey = nameToIndex.get(nodeName, 0) nodeObj = tree.get(treeKey, {}) try: nominal_voltage = float(nodeObj['nominal_voltage']) except: nominal_voltage = swingVoltage # Normalize to 120 V standard phaseVolt = phaseVolt * (120 / nominal_voltage) allVolts.append(phaseVolt) # Hack: average across phases. nodeVolts[row.get('node_name', '')] = avg(allVolts) # Color nodes by VOLTAGE. fGraph = feeder.treeToNxGraph(tree) voltChart = plt.figure(figsize=(20, 20)) plt.axes(frameon=0) plt.axis('off') plt.tight_layout() #set axes step equal voltChart.gca().set_aspect('equal') if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(fGraph.edges()) cleanG.add_nodes_from(fGraph) positions = graphviz_layout(cleanG, prog='neato') else: positions = {n: fGraph.node[n].get('pos', (0, 0)) for n in fGraph} edgeIm = nx.draw_networkx_edges(fGraph, positions) nodeIm = nx.draw_networkx_nodes( fGraph, pos=positions, node_color=[nodeVolts.get(n, 0) for n in fGraph.nodes()], linewidths=0, node_size=30, cmap=plt.cm.viridis) plt.sci(nodeIm) plt.clim(110, 130) plt.colorbar(orientation='horizontal', fraction=0.05) return voltChart
def genDiagram(outputDir, feederJson, damageDict, critLoads, damagedLoads, edgeLabelsToAdd, generatorList): # print damageDict warnings.filterwarnings("ignore") # Load required data. tree = feederJson.get("tree",{}) links = feederJson.get("links",{}) # Generate lat/lons from nodes and links structures. for link in links: for typeLink in link.keys(): if typeLink in ['source', 'target']: for key in link[typeLink].keys(): if key in ['x', 'y']: objName = link[typeLink]['name'] for x in tree: leaf = tree[x] if leaf.get('name','')==objName: if key=='x': leaf['latitude'] = link[typeLink][key] else: leaf['longitude'] = link[typeLink][key] # Remove even more things (no lat, lon or from = node without a position). for key in tree.keys(): aLat = tree[key].get('latitude') aLon = tree[key].get('longitude') aFrom = tree[key].get('from') if aLat is None and aLon is None and aFrom is None: tree.pop(key) # Create and save the graphic. inGraph = feeder.treeToNxGraph(tree) #feeder.latLonNxGraph(nxG) # This function creates a .plt reference which can be saved here. labels=True neatoLayout=False showPlot=False plt.axis('off') plt.tight_layout() plt.gca().invert_yaxis() plt.gca().set_aspect('equal') # Layout the graph via GraphViz neato. Handy if there's no lat/lon data. if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(inGraph.edges()) # HACK2: might miss nodes without edges without the following. cleanG.add_nodes_from(inGraph) pos = nx.nx_agraph.graphviz_layout(cleanG, prog='neato') else: pos = {n:inGraph.node[n].get('pos',(0,0)) for n in inGraph} # Rescale using the magic number. for k in pos: newPos = (pos[k][0]/HACK_SCALING_CONSTANT, pos[k][1]/HACK_SCALING_CONSTANT) pos[k] = newPos # Draw all the edges selected_labels = {} for e in inGraph.edges(): edgeName = inGraph.edge[e[0]][e[1]].get('name') if edgeName in edgeLabelsToAdd.keys(): selected_labels[e] = edgeLabelsToAdd[edgeName] edgeColor = 'black' if edgeName in damageDict: if damageDict[edgeName] == 1: edgeColor = 'yellow' if damageDict[edgeName] == 2: edgeColor = 'orange' if damageDict[edgeName] >= 3: edgeColor = 'red' eType = inGraph.edge[e[0]][e[1]].get('type','underground_line') ePhases = inGraph.edge[e[0]][e[1]].get('phases',1) standArgs = {'edgelist':[e], 'edge_color':edgeColor, 'width':2, 'style':{'parentChild':'dotted','underground_line':'dashed'}.get(eType,'solid') } if ePhases==3: standArgs.update({'width':5}) nx.draw_networkx_edges(inGraph,pos,**standArgs) standArgs.update({'width':3,'edge_color':'gainsboro'}) nx.draw_networkx_edges(inGraph,pos,**standArgs) standArgs.update({'width':1,'edge_color':edgeColor}) nx.draw_networkx_edges(inGraph,pos,**standArgs) if ePhases==2: standArgs.update({'width':3}) nx.draw_networkx_edges(inGraph,pos,**standArgs) standArgs.update({'width':1,'edge_color':'gainsboro'}) nx.draw_networkx_edges(inGraph,pos,**standArgs) else: nx.draw_networkx_edges(inGraph,pos,**standArgs) # Get swing buses. green_list = [] for node in tree: if 'bustype' in tree[node] and tree[node]['bustype'] == 'SWING': green_list.append(tree[node]['name']) isFirst = {'green': False, 'red': False, 'blue': False, 'grey': False, 'dimgrey': False} nodeLabels = {'green': 'Swing Bus', 'red': 'Critical Load', 'blue': 'Load', 'dimgrey':'New Generator', 'grey': 'Other'} # Draw nodes and optional labels. for key in pos.keys(): isLoad = key[2:6] nodeColor = 'grey' nodeLabel = 'Other' if key in green_list: nodeColor = 'green' elif key in critLoads: nodeColor = 'red' elif isLoad == 'load': nodeColor = 'blue' elif key in generatorList and key not in green_list: nodeColor = 'dimgrey' kwargs = { 'nodelist': [key], 'node_color': nodeColor, 'node_size': 16, 'linewidths': 1.0 } if not isFirst[nodeColor]: kwargs['label'] = nodeLabels[nodeColor] isFirst[nodeColor] = True node = nx.draw_networkx_nodes(inGraph, pos, **kwargs) if key in generatorList: node.set_edgecolor('black') if labels: nx.draw_networkx_labels( inGraph, pos, labels=damagedLoads, font_color='white', font_weight='bold', font_size=3 ) nx.draw_networkx_edge_labels( inGraph, pos, edge_labels=selected_labels, bbox={'alpha':0}, font_color='red', font_size=4 ) # Hazard field. # xlim = plt.xlim(); ylim = plt.ylim() # capture network limits. # a = np.random.random((600, 600)) # plt.imshow(a, cmap='Greys', interpolation='nearest', alpha=0.3) # plt.xlim(*xlim); plt.ylim(*ylim) # reset limits to be tight on network # Final showing or saving. plt.legend(loc='lower right') if showPlot: plt.show() plt.savefig(pJoin(outputDir,"feederChart.png"), dpi=800, pad_inches=0.0)
def genDiagram(outputDir, feederJson, damageDict, critLoads, damagedLoads, edgeLabelsToAdd, generatorList): # print damageDict warnings.filterwarnings("ignore") # Load required data. tree = feederJson.get("tree", {}) links = feederJson.get("links", {}) # Generate lat/lons from nodes and links structures. for link in links: for typeLink in link.keys(): if typeLink in ['source', 'target']: for key in link[typeLink].keys(): if key in ['x', 'y']: objName = link[typeLink]['name'] for x in tree: leaf = tree[x] if leaf.get('name', '') == objName: if key == 'x': leaf['latitude'] = link[typeLink][key] else: leaf['longitude'] = link[typeLink][key] # Remove even more things (no lat, lon or from = node without a position). for key in tree.keys(): aLat = tree[key].get('latitude') aLon = tree[key].get('longitude') aFrom = tree[key].get('from') if aLat is None and aLon is None and aFrom is None: tree.pop(key) # Create and save the graphic. inGraph = feeder.treeToNxGraph(tree) labels = True neatoLayout = False showPlot = False plt.axis('off') plt.tight_layout() plt.gca().invert_yaxis() plt.gca().set_aspect('equal') # Layout the graph via GraphViz neato. Handy if there's no lat/lon data. if neatoLayout: # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(inGraph.edges()) # HACK2: might miss nodes without edges without the following. cleanG.add_nodes_from(inGraph) pos = nx.nx_agraph.graphviz_layout(cleanG, prog='neato') else: pos = {n: inGraph.node[n].get('pos', (0, 0)) for n in inGraph} # Rescale using the magic number. for k in pos: newPos = (pos[k][0] / HACK_SCALING_CONSTANT, pos[k][1] / HACK_SCALING_CONSTANT) pos[k] = newPos # Draw all the edges selected_labels = {} for e in inGraph.edges(): edgeName = inGraph.edge[e[0]][e[1]].get('name') if edgeName in edgeLabelsToAdd.keys(): selected_labels[e] = edgeLabelsToAdd[edgeName] edgeColor = 'black' if edgeName in damageDict: if damageDict[edgeName] == 1: edgeColor = 'yellow' if damageDict[edgeName] == 2: edgeColor = 'orange' if damageDict[edgeName] >= 3: edgeColor = 'red' eType = inGraph.edge[e[0]][e[1]].get('type', 'underground_line') ePhases = inGraph.edge[e[0]][e[1]].get('phases', 1) standArgs = { 'edgelist': [e], 'edge_color': edgeColor, 'width': 2, 'style': { 'parentChild': 'dotted', 'underground_line': 'dashed' }.get(eType, 'solid') } if ePhases == 3: standArgs.update({'width': 5}) nx.draw_networkx_edges(inGraph, pos, **standArgs) standArgs.update({'width': 3, 'edge_color': 'gainsboro'}) nx.draw_networkx_edges(inGraph, pos, **standArgs) standArgs.update({'width': 1, 'edge_color': edgeColor}) nx.draw_networkx_edges(inGraph, pos, **standArgs) if ePhases == 2: standArgs.update({'width': 3}) nx.draw_networkx_edges(inGraph, pos, **standArgs) standArgs.update({'width': 1, 'edge_color': 'gainsboro'}) nx.draw_networkx_edges(inGraph, pos, **standArgs) else: nx.draw_networkx_edges(inGraph, pos, **standArgs) # Get swing buses. green_list = [] for node in tree: if 'bustype' in tree[node] and tree[node]['bustype'] == 'SWING': green_list.append(tree[node]['name']) isFirst = { 'green': False, 'red': False, 'blue': False, 'grey': False, 'dimgrey': False } nodeLabels = { 'green': 'Swing Bus', 'red': 'Critical Load', 'blue': 'Load', 'dimgrey': 'New Generator', 'grey': 'Other' } # Draw nodes and optional labels. for key in pos.keys(): isLoad = key[2:6] nodeColor = 'grey' nodeLabel = 'Other' if key in green_list: nodeColor = 'green' elif key in critLoads: nodeColor = 'red' elif isLoad == 'load': nodeColor = 'blue' elif key in generatorList and key not in green_list: nodeColor = 'dimgrey' kwargs = { 'nodelist': [key], 'node_color': nodeColor, 'node_size': 16, 'linewidths': 1.0 } if not isFirst[nodeColor]: kwargs['label'] = nodeLabels[nodeColor] isFirst[nodeColor] = True node = nx.draw_networkx_nodes(inGraph, pos, **kwargs) if key in generatorList: node.set_edgecolor('black') if labels: nx.draw_networkx_labels(inGraph, pos, labels=damagedLoads, font_color='white', font_weight='bold', font_size=3) nx.draw_networkx_edge_labels(inGraph, pos, edge_labels=selected_labels, bbox={'alpha': 0}, font_color='red', font_size=4) # Final showing or saving. fig = matplotlib.pyplot.gcf() fig.set_size_inches(9, 6) plt.legend(loc='lower right') if showPlot: plt.show() plt.savefig(pJoin(outputDir, "feederChart.png"), dpi=800, pad_inches=0.0)
def mapOmd(pathToOmdFile, outputPath, fileFormat, openBrowser=False, conversion=False): ''' Draw an omd on a map. fileFormat options: html or png Use html option to create a geojson file to be displayed with an interactive leaflet map. Use the png file format to create a static png image. By default the file(s) is saved to the outputPath, but setting openBrowser to True with open in a new browser window. ''' if fileFormat == 'html': if not conversion: geoJsonDict = omdGeoJson(pathToOmdFile) #use conversion for testing other feeders if conversion: geoJsonDict = omdGeoJson(pathToOmdFile, conversion=True) if not os.path.exists(outputPath): os.makedirs(outputPath) shutil.copy(omf.omfDir + '/templates/geoJsonMap.html', outputPath) with open(pJoin(outputPath, 'geoJsonFeatures.js'), "w") as outFile: outFile.write("var geojson =") json.dump(geoJsonDict, outFile, indent=4) if openBrowser: openInBrowser(pJoin(outputPath, 'geoJsonMap.html')) elif fileFormat == 'png': if not conversion: with open(pathToOmdFile) as inFile: tree = json.load(inFile)['tree'] nxG = feeder.treeToNxGraph(tree) nxG = graphValidator(pathToOmdFile, nxG) #use conversion for testing other feeders if conversion: nxG = convertOmd(pathToOmdFile) latitude_min = min([ nxG.nodes[nodewithPosition]['pos'][0] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') ]) longitude_min = min([ nxG.nodes[nodewithPosition]['pos'][1] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') ]) latitude_max = max([ nxG.nodes[nodewithPosition]['pos'][0] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') ]) longitude_max = max([ nxG.nodes[nodewithPosition]['pos'][1] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') ]) #Set the plot settings plt.switch_backend('Agg') fig = plt.figure(frameon=False, figsize=[10, 10]) ax = fig.add_axes([0, 0, 1, 1]) ax.axis('off') #map latlon to projection epsg3857 = Proj(init='epsg:3857') wgs84 = Proj(init='EPSG:4326') node_positions = { nodewithPosition: nxG.nodes[nodewithPosition]['pos'] for nodewithPosition in nx.get_node_attributes(nxG, 'pos') } for point in node_positions: node_positions[point] = transform(wgs84, epsg3857, node_positions[point][1], node_positions[point][0]) for zoomLevel in range(18, 19): numberofTiles = numTiles(zoomLevel) #Get bounding tiles and their lat/lon edges upperRightTile = tileXY(latitude_max, longitude_max, zoomLevel) lowerLeftTile = tileXY(latitude_min, longitude_min, zoomLevel) firstTileEdges = tileEdges(upperRightTile[0], upperRightTile[1], zoomLevel) lastTileEdges = tileEdges(lowerLeftTile[0], lowerLeftTile[1], zoomLevel) #Get N S E W boundaries for outer tiles in mercator projection x/y mainsouthWest = transform(wgs84, epsg3857, lastTileEdges[1], lastTileEdges[0]) mainnorthEast = transform(wgs84, epsg3857, firstTileEdges[3], firstTileEdges[2]) nx.draw_networkx(nxG, pos=node_positions, nodelist=list(node_positions.keys()), with_labels=False, node_size=2, edge_size=1) for tileX in range(lowerLeftTile[0], upperRightTile[0] + 1): for tileY in range(upperRightTile[1], lowerLeftTile[1] + 1): #Get section of tree that covers this tile currentTileEdges = tileEdges(tileX, tileY, zoomLevel) southWest = transform(wgs84, epsg3857, currentTileEdges[1], currentTileEdges[0]) northEast = transform(wgs84, epsg3857, currentTileEdges[3], currentTileEdges[2]) #Get map background from tile url = 'https://a.tile.openstreetmap.org/%s/%s/%s.png' % ( zoomLevel, tileX, tileY) # Spoof the User-Agent so we don't get 429 response = requests.request( 'GET', url, stream=True, headers={ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:71.0) Gecko/20100101 Firefox/71.0' }) with tempfile.NamedTemporaryFile() as f: f.write(response.raw.read()) img = plt.imread(f) plt.imshow(img, extent=(southWest[0], northEast[0], southWest[1], northEast[1])) plt.ylim(top=mainnorthEast[1], bottom=mainsouthWest[1]) plt.xlim(mainsouthWest[0], mainnorthEast[0]) if not os.path.exists(outputPath): os.makedirs(outputPath) plt.savefig(pJoin(outputPath, 'graphOnMap.png'), frameon=False, pad_inches=0, bbox='tight') if openBrowser: openInBrowser(pJoin(outputPath, 'graphOnMap.png'))
def work(modelDir, inputDict): ''' Run the model in the foreground. WARNING: can take about a minute. ''' # Global vars, and load data from the model directory. feederName = [x for x in os.listdir(modelDir) if x.endswith('.omd')][0][:-4] inputDict["feederName1"] = feederName feederPath = pJoin(modelDir, feederName + '.omd') feederJson = json.load(open(feederPath)) tree = feederJson.get("tree", {}) attachments = feederJson.get("attachments", {}) outData = {} ''' Run CVR analysis. ''' # Reformate monthData and rates. rates = { k: float(inputDict[k]) for k in [ "capitalCost", "omCost", "wholesaleEnergyCostPerKwh", "retailEnergyCostPerKwh", "peakDemandCostSpringPerKw", "peakDemandCostSummerPerKw", "peakDemandCostFallPerKw", "peakDemandCostWinterPerKw" ] } monthNames = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ] monthToSeason = { 'January': 'Winter', 'February': 'Winter', 'March': 'Spring', 'April': 'Spring', 'May': 'Spring', 'June': 'Summer', 'July': 'Summer', 'August': 'Summer', 'September': 'Fall', 'October': 'Fall', 'November': 'Fall', 'December': 'Winter' } monthData = [] for i, x in enumerate(monthNames): monShort = x[0:3].lower() season = monthToSeason[x] histAvg = float(inputDict.get(monShort + "Avg", 0)) histPeak = float(inputDict.get(monShort + "Peak", 0)) monthData.append({ "monthId": i, "monthName": x, "histAverage": histAvg, "histPeak": histPeak, "season": season }) # Graph the SCADA data. fig = plt.figure(figsize=(10, 6)) indices = [r['monthName'] for r in monthData] d1 = [r['histPeak'] / (10**3) for r in monthData] d2 = [r['histAverage'] / (10**3) for r in monthData] ticks = range(len(d1)) bar_peak = plt.bar(ticks, d1, color='gray') bar_avg = plt.bar(ticks, d2, color='dimgray') plt.legend([bar_peak[0], bar_avg[0]], ['histPeak', 'histAverage'], bbox_to_anchor=(0., 1.015, 1., .102), loc=3, ncol=2, mode="expand", borderaxespad=0.1) plt.xticks([t + 0.5 for t in ticks], indices) plt.ylabel('Mean and peak historical power consumptions (kW)') fig.autofmt_xdate() plt.savefig(pJoin(modelDir, "scadaChart.png")) outData["histPeak"] = d1 outData["histAverage"] = d2 outData["monthName"] = [name[0:3] for name in monthNames] # Graph feeder. fig = plt.figure(figsize=(10, 10)) myGraph = feeder.treeToNxGraph(tree) feeder.latLonNxGraph(myGraph, neatoLayout=False) plt.savefig(pJoin(modelDir, "feederChart.png")) with open(pJoin(modelDir, "feederChart.png"), "rb") as inFile: outData["feederChart"] = inFile.read().encode("base64") # Get the load levels we need to test. allLoadLevels = [x.get('histPeak', 0) for x in monthData ] + [y.get('histAverage', 0) for y in monthData] maxLev = _roundOne(max(allLoadLevels), 'up') minLev = _roundOne(min(allLoadLevels), 'down') tenLoadLevels = range(int(minLev), int(maxLev), int( (maxLev - minLev) / 10)) # Gather variables from the feeder. for key in tree.keys(): # Set clock to single timestep. if tree[key].get('clock', '') == 'clock': tree[key] = { "timezone": "PST+8PDT", "stoptime": "'2013-01-01 00:00:00'", "starttime": "'2013-01-01 00:00:00'", "clock": "clock" } # Save swing node index. if tree[key].get('bustype', '').lower() == 'swing': swingIndex = key swingName = tree[key].get('name') # Remove all includes. if tree[key].get('omftype', '') == '#include': del key # Find the substation regulator and config. for key in tree: if tree[key].get('object', '') == 'regulator' and tree[key].get( 'from', '') == swingName: regIndex = key regConfName = tree[key]['configuration'] if not regConfName: regConfName = False for key in tree: if tree[key].get('name', '') == regConfName: regConfIndex = key # Set substation regulator to manual operation. baselineTap = int(inputDict.get( "baselineTap")) # GLOBAL VARIABLE FOR DEFAULT TAP POSITION tree[regConfIndex] = { 'name': tree[regConfIndex]['name'], 'object': 'regulator_configuration', 'connect_type': '1', 'raise_taps': '10', 'lower_taps': '10', 'CT_phase': 'ABC', 'PT_phase': 'ABC', 'regulation': '0.10', #Yo, 0.10 means at tap_pos 10 we're 10% above 120V. 'Control': 'MANUAL', 'control_level': 'INDIVIDUAL', 'Type': 'A', 'tap_pos_A': str(baselineTap), 'tap_pos_B': str(baselineTap), 'tap_pos_C': str(baselineTap) } # Attach recorders relevant to CVR. recorders = [{ 'object': 'collector', 'file': 'ZlossesTransformer.csv', 'group': 'class=transformer', 'limit': '0', 'property': 'sum(power_losses_A.real),sum(power_losses_A.imag),sum(power_losses_B.real),sum(power_losses_B.imag),sum(power_losses_C.real),sum(power_losses_C.imag)' }, { 'object': 'collector', 'file': 'ZlossesUnderground.csv', 'group': 'class=underground_line', 'limit': '0', 'property': 'sum(power_losses_A.real),sum(power_losses_A.imag),sum(power_losses_B.real),sum(power_losses_B.imag),sum(power_losses_C.real),sum(power_losses_C.imag)' }, { 'object': 'collector', 'file': 'ZlossesOverhead.csv', 'group': 'class=overhead_line', 'limit': '0', 'property': 'sum(power_losses_A.real),sum(power_losses_A.imag),sum(power_losses_B.real),sum(power_losses_B.imag),sum(power_losses_C.real),sum(power_losses_C.imag)' }, { 'object': 'recorder', 'file': 'Zregulator.csv', 'limit': '0', 'parent': tree[regIndex]['name'], 'property': 'tap_A,tap_B,tap_C,power_in.real,power_in.imag' }, { 'object': 'collector', 'file': 'ZvoltageJiggle.csv', 'group': 'class=triplex_meter', 'limit': '0', 'property': 'min(voltage_12.mag),mean(voltage_12.mag),max(voltage_12.mag),std(voltage_12.mag)' }, { 'object': 'recorder', 'file': 'ZsubstationTop.csv', 'limit': '0', 'parent': tree[swingIndex]['name'], 'property': 'voltage_A,voltage_B,voltage_C' }, { 'object': 'recorder', 'file': 'ZsubstationBottom.csv', 'limit': '0', 'parent': tree[regIndex]['to'], 'property': 'voltage_A,voltage_B,voltage_C' }] biggest = 1 + max([int(k) for k in tree.keys()]) for index, rec in enumerate(recorders): tree[biggest + index] = rec # Change constant PF loads to ZIP loads. (See evernote for rationale about 50/50 power/impedance mix.) blankZipModel = { 'object': 'triplex_load', 'name': 'NAMEVARIABLE', 'base_power_12': 'POWERVARIABLE', 'power_fraction_12': str(inputDict.get("p_percent")), 'impedance_fraction_12': str(inputDict.get("z_percent")), 'current_fraction_12': str(inputDict.get("i_percent")), 'power_pf_12': str( inputDict.get("power_factor") ), #MAYBEFIX: we can probably get this PF data from the Milsoft loads. 'impedance_pf_12': str(inputDict.get("power_factor")), 'current_pf_12': str(inputDict.get("power_factor")), 'nominal_voltage': '120', 'phases': 'PHASESVARIABLE', 'parent': 'PARENTVARIABLE' } def powerClean(powerStr): ''' take 3339.39+1052.29j to 3339.39 ''' return powerStr[0:powerStr.find('+')] for key in tree: if tree[key].get('object', '') == 'triplex_node': # Get existing variables. name = tree[key].get('name', '') power = tree[key].get('power_12', '') parent = tree[key].get('parent', '') phases = tree[key].get('phases', '') # Replace object and reintroduce variables. tree[key] = copy(blankZipModel) tree[key]['name'] = name tree[key]['base_power_12'] = powerClean(power) tree[key]['parent'] = parent tree[key]['phases'] = phases # Function to determine how low we can tap down in the CVR case: def loweringPotential(baseLine): ''' Given a baseline end of line voltage, how many more percent can we shave off the substation voltage? ''' ''' testsWePass = [122.0,118.0,200.0,110.0] ''' lower = int(math.floor((baseLine / 114.0 - 1) * 100)) - 1 # If lower is negative, we can't return it because we'd be undervolting beyond what baseline already was! if lower < 0: return baselineTap else: return baselineTap - lower # Run all the powerflows. powerflows = [] for doingCvr in [False, True]: # For each load level in the tenLoadLevels, run a powerflow with the load objects scaled to the level. for desiredLoad in tenLoadLevels: # Find the total load that was defined in Milsoft: loadList = [] for key in tree: if tree[key].get('object', '') == 'triplex_load': loadList.append(tree[key].get('base_power_12', '')) totalLoad = sum([float(x) for x in loadList]) # Rescale each triplex load: for key in tree: if tree[key].get('object', '') == 'triplex_load': currentPow = float(tree[key]['base_power_12']) ratio = desiredLoad / totalLoad tree[key]['base_power_12'] = str(currentPow * ratio) # If we're doing CVR then lower the voltage. if doingCvr: # Find the minimum voltage we can tap down to: newTapPos = baselineTap for row in powerflows: if row.get('loadLevel', '') == desiredLoad: newTapPos = loweringPotential( row.get('lowVoltage', 114)) # Tap it down to there. # MAYBEFIX: do each phase separately because that's how it's done in the field... Oof. tree[regConfIndex]['tap_pos_A'] = str(newTapPos) tree[regConfIndex]['tap_pos_B'] = str(newTapPos) tree[regConfIndex]['tap_pos_C'] = str(newTapPos) # Run the model through gridlab and put outputs in the table. output = gridlabd.runInFilesystem(tree, attachments=attachments, keepFiles=True, workDir=modelDir) os.remove(pJoin(modelDir, "PID.txt")) p = output['Zregulator.csv']['power_in.real'][0] q = output['Zregulator.csv']['power_in.imag'][0] s = math.sqrt(p**2 + q**2) lossTotal = 0.0 for device in [ 'ZlossesOverhead.csv', 'ZlossesTransformer.csv', 'ZlossesUnderground.csv' ]: for letter in ['A', 'B', 'C']: r = output[device]['sum(power_losses_' + letter + '.real)'][0] i = output[device]['sum(power_losses_' + letter + '.imag)'][0] lossTotal += math.sqrt(r**2 + i**2) ## Entire output: powerflows.append({ 'doingCvr': doingCvr, 'loadLevel': desiredLoad, 'realPower': p, 'powerFactor': p / s, 'losses': lossTotal, 'subVoltage': (output['ZsubstationBottom.csv']['voltage_A'][0] + output['ZsubstationBottom.csv']['voltage_B'][0] + output['ZsubstationBottom.csv']['voltage_C'][0]) / 3 / 60, 'lowVoltage': output['ZvoltageJiggle.csv']['min(voltage_12.mag)'][0] / 2, 'highVoltage': output['ZvoltageJiggle.csv']['max(voltage_12.mag)'][0] / 2 }) # For a given load level, find two points to interpolate on. def getInterpPoints(t): ''' Find the two points we can interpolate from. ''' ''' tests pass on [tenLoadLevels[0],tenLoadLevels[5]+499,tenLoadLevels[-1]-988] ''' loc = sorted(tenLoadLevels + [t]).index(t) if loc == 0: return (tenLoadLevels[0], tenLoadLevels[1]) elif loc > len(tenLoadLevels) - 2: return (tenLoadLevels[-2], tenLoadLevels[-1]) else: return (tenLoadLevels[loc - 1], tenLoadLevels[loc + 1]) # Calculate peak reduction. for row in monthData: peak = row['histPeak'] peakPoints = getInterpPoints(peak) peakTopBase = [ x for x in powerflows if x.get('loadLevel', '') == peakPoints[-1] and x.get('doingCvr', '') == False ][0] peakTopCvr = [ x for x in powerflows if x.get('loadLevel', '') == peakPoints[-1] and x.get('doingCvr', '') == True ][0] peakBottomBase = [ x for x in powerflows if x.get('loadLevel', '') == peakPoints[0] and x.get('doingCvr', '') == False ][0] peakBottomCvr = [ x for x in powerflows if x.get('loadLevel', '') == peakPoints[0] and x.get('doingCvr', '') == True ][0] # Linear interpolation so we aren't running umpteen million loadflows. x = (peakPoints[0], peakPoints[1]) y = (peakTopBase['realPower'] - peakTopCvr['realPower'], peakBottomBase['realPower'] - peakBottomCvr['realPower']) peakRed = y[0] + (y[1] - y[0]) * (peak - x[0]) / (x[1] - x[0]) row['peakReduction'] = peakRed # Calculate energy reduction and loss reduction based on average load. for row in monthData: avgEnergy = row['histAverage'] energyPoints = getInterpPoints(avgEnergy) avgTopBase = [ x for x in powerflows if x.get('loadLevel', '') == energyPoints[-1] and x.get('doingCvr', '') == False ][0] avgTopCvr = [ x for x in powerflows if x.get('loadLevel', '') == energyPoints[-1] and x.get('doingCvr', '') == True ][0] avgBottomBase = [ x for x in powerflows if x.get('loadLevel', '') == energyPoints[0] and x.get('doingCvr', '') == False ][0] avgBottomCvr = [ x for x in powerflows if x.get('loadLevel', '') == energyPoints[0] and x.get('doingCvr', '') == True ][0] # Linear interpolation so we aren't running umpteen million loadflows. x = (energyPoints[0], energyPoints[1]) y = (avgTopBase['realPower'] - avgTopCvr['realPower'], avgBottomBase['realPower'] - avgBottomCvr['realPower']) energyRed = y[0] + (y[1] - y[0]) * (avgEnergy - x[0]) / (x[1] - x[0]) row['energyReduction'] = energyRed lossY = (avgTopBase['losses'] - avgTopCvr['losses'], avgBottomBase['losses'] - avgBottomCvr['losses']) lossRed = lossY[0] + (lossY[1] - lossY[0]) * (avgEnergy - x[0]) / (x[1] - x[0]) row['lossReduction'] = lossRed # Multiply by dollars. for row in monthData: row['energyReductionDollars'] = row['energyReduction'] / 1000 * ( rates['wholesaleEnergyCostPerKwh'] - rates['retailEnergyCostPerKwh']) row['peakReductionDollars'] = row['peakReduction'] / 1000 * rates[ 'peakDemandCost' + row['season'] + 'PerKw'] row['lossReductionDollars'] = row['lossReduction'] / 1000 * rates[ 'wholesaleEnergyCostPerKwh'] # Pretty output def plotTable(inData): fig = plt.figure(figsize=(10, 5)) plt.axis('off') plt.tight_layout() plt.table(cellText=[row for row in inData[1:]], loc='center', rowLabels=range(len(inData) - 1), colLabels=inData[0]) def dictalToMatrix(dictList): ''' Take our dictal format to a matrix. ''' matrix = [dictList[0].keys()] for row in dictList: matrix.append(row.values()) return matrix # Powerflow results. plotTable(dictalToMatrix(powerflows)) plt.savefig(pJoin(modelDir, "powerflowTable.png")) # Monetary results. ## To print partial money table monthDataMat = dictalToMatrix(monthData) dimX = len(monthDataMat) dimY = len(monthDataMat[0]) monthDataPart = [] for k in range(0, dimX): monthDatatemp = [] for m in range(4, dimY): monthDatatemp.append(monthDataMat[k][m]) monthDataPart.append(monthDatatemp) plotTable(monthDataPart) plt.savefig(pJoin(modelDir, "moneyTable.png")) outData["monthDataMat"] = dictalToMatrix(monthData) outData["monthDataPart"] = monthDataPart # Graph the money data. fig = plt.figure(figsize=(10, 8)) indices = [r['monthName'] for r in monthData] d1 = [r['energyReductionDollars'] for r in monthData] d2 = [r['lossReductionDollars'] for r in monthData] d3 = [r['peakReductionDollars'] for r in monthData] ticks = range(len(d1)) bar_erd = plt.bar(ticks, d1, color='red') bar_lrd = plt.bar(ticks, d2, color='green') bar_prd = plt.bar(ticks, d3, color='blue', yerr=d2) plt.legend([bar_prd[0], bar_lrd[0], bar_erd[0]], [ 'peakReductionDollars', 'lossReductionDollars', 'energyReductionDollars' ], bbox_to_anchor=(0., 1.015, 1., .102), loc=3, ncol=2, mode="expand", borderaxespad=0.1) plt.xticks([t + 0.5 for t in ticks], indices) plt.ylabel('Utility Savings ($)') plt.tight_layout(5.5, 1.3, 1.2) fig.autofmt_xdate() plt.savefig(pJoin(modelDir, "spendChart.png")) outData["energyReductionDollars"] = d1 outData["lossReductionDollars"] = d2 outData["peakReductionDollars"] = d3 # Graph the cumulative savings. fig = plt.figure(figsize=(10, 5)) annualSavings = sum(d1) + sum(d2) + sum(d3) annualSave = lambda x: (annualSavings - rates['omCost']) * x - rates[ 'capitalCost'] simplePayback = rates['capitalCost'] / (annualSavings - rates['omCost']) plt.xlabel('Year After Installation') plt.xlim(0, 30) plt.ylabel('Cumulative Savings ($)') plt.plot([0 for x in range(31)], c='gray') plt.axvline(x=simplePayback, ymin=0, ymax=1, c='gray', linestyle='--') plt.plot([annualSave(x) for x in range(31)], c='green') plt.savefig(pJoin(modelDir, "savingsChart.png")) outData["annualSave"] = [annualSave(x) for x in range(31)] # For autotest, there won't be such file. return outData
import omf.feeder as feeder, json import os, networkx as nx from os.path import join as pJoin from networkx.drawing.nx_agraph import graphviz_layout _myDir = os.path.dirname(os.path.abspath(__file__)) IN_PATH_OMD = pJoin(_myDir, 'superModel Tomorrow.omd') OUT_PATH_OMD = pJoin(_myDir, 'superModel Tomorrow with latlons.omd') with open(IN_PATH_OMD, 'r') as jsonFile: omd = json.load(jsonFile) tree = omd['tree'] # Use graphviz to lay out the graph. inGraph = feeder.treeToNxGraph(tree) # HACK: work on a new graph without attributes because graphViz tries to read attrs. cleanG = nx.Graph(inGraph.edges()) # HACK2: might miss nodes without edges without the following. cleanG.add_nodes_from(inGraph) pos = graphviz_layout(cleanG, prog='neato') # # Charting the feeder in matplotlib: # feeder.latLonNxGraph(inGraph, labels=False, neatoLayout=True, showPlot=True) # Insert the latlons. for key in tree: obName = tree[key].get('name', '') thisPos = pos.get(obName, None) if thisPos != None: tree[key]['longitude'] = thisPos[0] tree[key]['latitude'] = thisPos[1] with open(OUT_PATH_OMD, 'w') as outFile: