diff --git a/bifacial_radiance/bifacial_radiance.py b/bifacial_radiance/bifacial_radiance.py index ef9fd70a..d14c65ee 100644 --- a/bifacial_radiance/bifacial_radiance.py +++ b/bifacial_radiance/bifacial_radiance.py @@ -4,7 +4,44 @@ bifacial_radiance.py - module to develop radiance bifacial scenes, including gendaylit and gencumulativesky 7/5/2016 - test script based on G173_journal_height 5/1/2017 - standalone module + +Pre-requisites: + This software is written in Python 2.7 leveraging many Anaconda tools (e.g. pandas, numpy, etc) + + *RADIANCE software should be installed from https://github.com/NREL/Radiance/releases + + *If you want to use gencumulativesky, move 'gencumulativesky.exe' from + 'bifacial_radiance\data' into your RADIANCE source directory. + + *If using a Windows machine you should download the Jaloxa executables at + http://www.jaloxa.eu/resources/radiance/radwinexe.shtml#Download + +Overview: + Bifacial_radiance includes several helper functions to make it easier to evaluate + different PV system orientations for rear bifacial irradiance. + Note that this is simply an optical model - identifying available rear irradiance under different conditions. + + There are two solar resource modes in bifacial_radiance: `gendaylit` uses hour-by-hour solar + resource descriptions using the Perez diffuse tilted plane model. + `gencumulativesky` is an annual average solar resource that combines hourly + Perez skies into one single solar source, and computes an annual average. + + bifacial_radiance includes five object-oriented classes: + + RadianceObj: top level class to work on radiance objects, keep track of filenames, + sky values, PV module type etc. + + GroundObj: details for the ground surface and reflectance + + SceneObj: scene information including array configuration (row spacing, ground height) + + MetObj: meteorological data from EPW (energyplus) file. + Future work: include other file support including TMY files + + AnalysisObj: Analysis class for plotting and reporting + ''' + #start in pylab space to enable plotting #get_ipython().magic(u'pylab') import os, datetime @@ -15,6 +52,11 @@ from subprocess import Popen, PIPE # replacement for os.system() #import shlex +import pkg_resources +global DATA_PATH # path to data files including module.json. Global context +DATA_PATH = pkg_resources.resource_filename('bifacial_radiance', 'data/') + + def _findme(lst, a): #find string match in a list. found this nifty script on stackexchange return [i for i, x in enumerate(lst) if x==a] @@ -39,8 +81,8 @@ def _popen(cmd, data_in, data_out=PIPE): return data + -# start developing the class class RadianceObj: ''' @@ -88,6 +130,7 @@ def __init__(self, basename=None, path=None): self.skyfiles = [] # skyfiles for oconv self.radfiles = [] # scene rad files for oconv self.octfile = [] #octfile name for analysis + now = datetime.datetime.now() self.nowstr = str(now.date())+'_'+str(now.hour)+str(now.minute)+str(now.second) @@ -136,15 +179,15 @@ def _checkPath(path): # create the file structure if it doesn't exist _checkPath('images/'); _checkPath('objects/'); _checkPath('results/'); _checkPath('skies/'); # if materials directory doesn't exist, populate it with ground.rad - if not os.path.exists('materials/'): + # figure out where pip installed support files. + from shutil import copy2 + + if not os.path.exists('materials/'): #copy ground.rad to /materials os.makedirs('materials/') print('Making path: materials/') - # figure out where pip installed support files. copy ground.rad to /materials - from shutil import copy2 - import pkg_resources - DATA_PATH = pkg_resources.resource_filename('bifacial_radiance', 'data/') + copy2(os.path.join(DATA_PATH,'ground.rad'),'materials') - # if views directory doesn't exist, create it with two default views + # if views directory doesn't exist, create it with two default views - side.vp and front.vp if not os.path.exists('views/'): os.makedirs('views/') with open('views/side.vp', 'wb') as f: @@ -497,10 +540,12 @@ def makeOct(self,filelist=None,octname = None): self.octfile = '%s.oct' % (octname) return '%s.oct' % (octname) + """ def analysis(self, octfile = None, basename = None): ''' default to AnalysisObj.PVSCanalysis(self.octfile, self.basename) - + Not sure how wise it is to have RadianceObj.analysis - perhaps this is best eliminated entirely? + Most analysis is not done on the PVSC scene any more... eliminate this and update example notebook? ''' if octfile is None: octfile = self.octfile @@ -511,13 +556,77 @@ def analysis(self, octfile = None, basename = None): analysis_obj.PVSCanalysis(octfile, basename) return analysis_obj + """ + def makeModule(self,name=None,x=1,y=1,bifi=1,orientation='portrait',modulefile = None,text = None): + ''' + add module details to the .JSON module config file module.json + This needs to be in the RadianceObj class because this is defined before a SceneObj is. + + Parameters + ------------ + name: string input to name the module type + + module configuration dictionary inputs: + x # width of module (meters). + y # height of module (meters). + bifi # bifaciality of the panel (not currently used) + orientation #default orientation of the scene (portrait or landscape) + modulefile # existing radfile location in \objects. Otherwise a default value is used + text = '' # generation text + + + Returns: None + ------- + + ''' + if name is None: + print('usage: makeModule(name,x,y)') + + import json + if modulefile is None: + #replace whitespace with underlines. what about \n and other weird characters? + name2 = str(name).strip().replace(' ', '_') + modulefile = 'objects\\' + name2 + '.rad' + if text is None: + text = '! genbox black PVmodule {} {} 0.02 | xform -t {} 0 0'.format(x, y, -x/2.0) + moduledict = {'x':x, + 'y':y, + 'bifi':bifi, + 'orientation':orientation, + 'text':text, + 'modulefile':modulefile + } + + + filedir = os.path.join(DATA_PATH,'module.json') # look in global DATA_PATH for module config file + with open( filedir ) as configfile: + data = json.load(configfile) + + + data.update({name:moduledict}) + with open(os.path.join(DATA_PATH,'module.json') ,'w') as configfile: + json.dump(data,configfile) + + print('Module {} successfully created'.format(name)) + + def printModules(self): + # print available module types by creating a dummy SceneObj + temp = SceneObj('simple_panel') + modulenames = temp.readModule() + print('Available module names: {}'.format([str(x) for x in modulenames])) + + + + + def makeScene(self, moduletype=None, sceneDict=None, nMods = 20, nRows = 7): ''' - return a SceneObj + return a SceneObj which contains details of the PV system configuration including + tilt, orientation, row pitch, height, nMods per row, nRows in the system... ''' if moduletype is None: - print('makeScene(moduletype, sceneDict, nMods, nRows). Available moduletypes: monopanel, simple_panel' ) + print('makeScene(moduletype, sceneDict, nMods, nRows). Available moduletypes: monopanel, simple_panel' ) #TODO: read in config file to identify available module types return self.scene = SceneObj(moduletype) @@ -636,42 +745,69 @@ class SceneObj: def __init__(self,moduletype=None): ''' initialize SceneObj ''' + modulenames = self.readModule() + if moduletype is None: - moduletype = 'simple_panel' - self.moduletype = moduletype - - if moduletype == 'simple_panel': #next module type - radfile = 'objects\\simple_panel.rad' - self.x = 0.95 # width of module. - self.y = 1.59 # height of module. - self.bifi = 1 # bifaciality of the panel - self.orientation = 'portrait' #default orientation of the scene - if not os.path.isfile(radfile): - with open(radfile, 'wb') as f: - f.write('!genbox black PVmodule 0.95 1.59 0.02 | xform -t -0.475 0 0 ') - self.modulefile = radfile + print('Usage: SceneObj(moduletype)\nNo module type selected. Available module types: {}'.format(modulenames)) + return + else: + if moduletype in modulenames: + # read in module details from configuration file. + self.readModule(name = moduletype) + else: + print('incorrect panel type selection') + return + + + + + def readModule(self,name = None): + ''' + Read in available modules in module.json. If a specific module name is + passed, return those details into the SceneObj. Otherwise return available module list. + + Parameters + ------------ + name # name of module. + + Returns + ------- + dict of module parameters + -or- + list of modulenames if name is not passed in + + ''' + import json + filedir = os.path.join(DATA_PATH,'module.json') + with open( filedir ) as configfile: + data = json.load(configfile) + + modulenames = data.keys() + if name is None: - elif moduletype == 'monopanel' : - self.x = 0.95 # width of module. - self.y = 1.59 # height of module. - self.bifi = 1 # bifaciality of the panel - self.orientation = 'portrait' #default orientation of the scene - self.modulefile = 'objects\\monopanel_1.rad' - - elif moduletype == 'mini_panel' : - radfile = 'objects\\mini_panel.rad' - self.x = 0.6096 # width of module. - self.y = 0.9144 # height of module. - self.bifi = 1 # bifaciality of the panel - self.orientation = 'landscape' #default orientation of the scene + return modulenames + + if name in modulenames: + moduledict = data[name] + self.moduletype = name + + radfile = moduledict['modulefile'] + self.x = moduledict['x'] # width of module. + self.y = moduledict['y'] # height of module. + self.bifi = moduledict['bifi'] # bifaciality of the panel. Not currently used + self.orientation = moduledict['orientation'] #default orientation of the scene + #create new .RAD file if not os.path.isfile(radfile): with open(radfile, 'wb') as f: - f.write('!genbox black PVmodule 0.6096 0.9144 0.012192 | xform -t -0.3048 0 0 ') + f.write(moduledict['text']) + #if not os.path.isfile(radfile): + # raise Exception('Error: module file not found {}'.format(radfile)) self.modulefile = radfile + return moduledict else: - print('incorrect panel type selection') - return + print('Error: module name {} doesnt exist'.format(name)) + return {} def makeSceneNxR(self, tilt, height, pitch, orientation = None, azimuth = 180, nMods = 20, nRows = 7): ''' @@ -705,8 +841,8 @@ def makeSceneNxR(self, tilt, height, pitch, orientation = None, azimuth = 180, n text += self.modulefile # save the .RAD file - - radfile = 'objects\\%s_%s_%s_%sx%s.rad'%(self.moduletype,height,pitch, nMods, nRows) + radname = str(self.moduletype).strip().replace(' ', '_')# remove whitespace + radfile = 'objects\\%s_%s_%s_%sx%s.rad'%(radname,height,pitch, nMods, nRows) with open(radfile, 'wb') as f: f.write(text) @@ -990,10 +1126,12 @@ def analysis(self, octfile, basename, frontscan, backscan, plotflag = False): if __name__ == "__main__": ''' Example of how to run a Radiance routine for a simple bifacial system - Pre-requisites: change testfolder to point to an empty directory on your computer ''' - testfolder = r'C:\Users\cdeline\Documents\Python Scripts\TestFolder' #point to an empty directory or existing Radiance directory + ''' + import easygui # this is only required if you want a graphical directory picker + #testfolder = r'C:\Users\cdeline\Documents\Python Scripts\TestFolder' #point to an empty directory or existing Radiance directory + testfolder = easygui.diropenbox(msg = 'Select or create an empty directory for the Radiance tree',title='Browse for empty Radiance directory') demo = RadianceObj('simple_panel',testfolder) # Create a RadianceObj 'object' demo.setGround(0.62) # input albedo number or material name like 'concrete'. To see options, run this without any input. try: @@ -1016,5 +1154,5 @@ def analysis(self, octfile, basename, frontscan, backscan, plotflag = False): analysis = AnalysisObj(octfile, demo.basename) # return an analysis object including the scan dimensions for back irradiance analysis.analysis(octfile, demo.basename, scene.frontscan, scene.backscan) # compare the back vs front irradiance print('Annual bifacial ratio: %0.3f - %0.3f' %(min(analysis.backRatio), np.mean(analysis.backRatio)) ) - - + ''' + diff --git a/bifacial_radiance/data/module.json b/bifacial_radiance/data/module.json new file mode 100644 index 00000000..967b6386 --- /dev/null +++ b/bifacial_radiance/data/module.json @@ -0,0 +1 @@ +{"mini_panel": {"orientation": "landscape", "text": "!genbox black PVmodule 0.6096 0.9144 0.012192 | xform -t -0.3048 0 0 ", "bifi": 1, "modulefile": "objects\\mini_panel.rad", "y": 0.9144, "x": 0.6096}, "simple_panel": {"orientation": "portrait", "text": "! genbox black PVmodule 0.95 1.59 0.02 | xform -t -0.475 0 0", "bifi": 1, "modulefile": "objects\\simple_panel.rad", "y": 1.59, "x": 0.95}, "monopanel": {"orientation": "portrait", "text": "", "bifi": 1, "modulefile": "objects\\monopanel_1.rad", "y": 1.59, "x": 0.95}, "Prism Solar Bi60": {"orientation": "portrait", "text": "! genbox black PVmodule 0.984 1.695 0.02 | xform -t -0.492 0 0", "bifi": 0.9, "modulefile": "objects\\Prism_Solar_Bi60.rad", "y": 1.695, "x": 0.984}} \ No newline at end of file diff --git a/docs/bifacial_radiance examples.ipynb b/docs/bifacial_radiance examples.ipynb index 3a033123..251025fb 100644 --- a/docs/bifacial_radiance examples.ipynb +++ b/docs/bifacial_radiance examples.ipynb @@ -41,51 +41,55 @@ "name": "stdout", "output_type": "stream", "text": [ - "path = C:\\Users\\cdeline\\Documents\\Python Scripts\\TestFolder\n", + "path = C:\\Users\\cdeline\\Documents\\Python Scripts\\TestFolder\n" + ] + } + ], + "source": [ + "# Simple example system using Radiance.\n", + "#point to an empty directory or existing Radiance directory\n", + "testfolder = r'C:\\Users\\cdeline\\Documents\\Python Scripts\\TestFolder' \n", + "demo = RadianceObj('bifacial_example',testfolder) # Create a RadianceObj 'object' named bifacial_example. no whitespace allowed" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ "Getting weather file: USA_VA_Richmond.Intl.AP.724010_TMY.epw ... OK!\n", "error: There were 4255 sun up hours in this climate file\n", - "Total Ibh/Lbh: 0.000000\n", - "incorrect panel type selection\n", - "created simple_panel.oct linescan in process: simple_panel_Front\n", - "linescan in process: simple_panel_Back\n", - "saved: results\\irr_simple_panel.csv\n", - "Annual bifacial ratio: 0.090 - 0.130\n" + "Total Ibh/Lbh: 0.000000\n" ] } ], "source": [ - "# Simple example system using Radiance.\n", - "testfolder = r'E:\\Documents\\Python Scripts\\TestFolder' #point to an empty directory or existing Radiance directory\n", - "demo = RadianceObj('simple_panel',testfolder) # Create a RadianceObj 'object'\n", "demo.setGround(0.62) # input albedo number or material name like 'concrete'. To see options, run this without any input.\n", - "try:\n", - " epwfile = demo.getEPW(37.5,-77.6) #can't run this within NREL firewall. Otherwise, pull TMY data for any global lat/lon\n", - "except:\n", - " pass\n", - " \n", - "metdata = demo.readEPW('EPWs\\\\USA_VA_Richmond.Intl.AP.724010_TMY.epw') # read in the weather data\n", - "# Now we either choose a single time point, or use cumulativesky for the entire year. \n", + "\n", + "# pull in meteorological data using pyEPW for any global lat/lon\n", + "epwfile = demo.getEPW(37.5,-77.6) \n", + "\n", + "# read in the weather data pulled in above. If you want a different location, replace this filename with the new EPW file name. \n", + "metdata = demo.readEPW('EPWs\\\\USA_VA_Richmond.Intl.AP.724010_TMY.epw') \n", + "\n", + "# Solar resource definition. Either choose a single time point, or use cumulativesky for the entire year. \n", "fullYear = True\n", "if fullYear:\n", " demo.genCumSky(demo.epwfile) # entire year.\n", + " # NOTE: this will claim to throw an 'error' if there are times when the sun is below the horizon. This is normal!\n", "else:\n", - " demo.gendaylit(metdata,4020) # Noon, June 17th\n", - " \n", - "# create a scene using panels in landscape at 10 deg tilt, 1.5m pitch. 0.2 m ground clearance\n", - "sceneDict = {'tilt':10,'pitch':1.5,'height':0.2,'orientation':'landscape','azimuth':180} \n", - "scene = demo.makeScene('simple_panel',sceneDict, nMods = 20, nRows = 7) #makeScene creates a .rad file with 20 modules per row, 7 rows.\n", - "octfile = demo.makeOct(demo.getfilelist()) # makeOct combines all of the ground, sky and object files into a .oct file.\n", - "analysis = AnalysisObj(octfile, demo.basename) # return an analysis object including the scan dimensions for back irradiance\n", - "analysis.analysis(octfile, demo.basename, scene.frontscan, scene.backscan) # compare the back vs front irradiance \n", - "# the frontscan and backscan include a linescan along a chord of the module, both on the front and back. \n", - "# Return the minimum of the irradiance values, and the average of the irradiance values along a chord of the module.\n", - "print('Annual bifacial ratio: %0.3f - %0.3f' %(min(analysis.backRatio), np.mean(analysis.backRatio)) )\n", - "\n" + " demo.gendaylit(metdata,4020) # Noon, June 17th (timepoint # 4020)\n" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": { "collapsed": false }, @@ -94,12 +98,48 @@ "name": "stdout", "output_type": "stream", "text": [ - "generating visible render of scene\n", - "generating scene in WM-2\n", - "saving scene in false color\n" + "Module Prism Solar Bi60 successfully created\n", + "Available module names: ['mini_panel', 'simple_panel', 'monopanel', 'Prism Solar Bi60']\n" ] } ], + "source": [ + "# create a custom PV module type. Prism Solar Bi60. x = .984 y = 1.695. Bifaciality = 0.90\n", + "# Note: modules are currently 100% opaque. This will be modified in the future\n", + "demo.makeModule(name='Prism Solar Bi60',x=0.984,y=1.695,bifi = 0.90)\n", + "\n", + "# print available module types in data/module.json\n", + "demo.printModules()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "# create a scene using panels in landscape at 10 deg tilt, 1.5m pitch. 0.2 m ground clearance\n", + "sceneDict = {'tilt':10,'pitch':1.5,'height':0.2,'orientation':'landscape','azimuth':180} \n", + "module_type = 'monopanel'\n", + "scene = demo.makeScene(module_type,sceneDict, nMods = 20, nRows = 7) #makeScene creates a .rad file with 20 modules per row, 7 rows.\n", + "octfile = demo.makeOct(demo.getfilelist()) # makeOct combines all of the ground, sky and object files into a .oct file.\n", + "analysis = AnalysisObj(octfile, demo.basename) # return an analysis object including the scan dimensions for back irradiance\n", + "analysis.analysis(octfile, demo.basename, scene.frontscan, scene.backscan) # compare the back vs front irradiance \n", + "# the frontscan and backscan include a linescan along a chord of the module, both on the front and back. \n", + "# Return the minimum of the irradiance values, and the average of the irradiance values along a chord of the module.\n", + "print('Annual bifacial ratio: %0.3f - %0.3f' %(min(analysis.backRatio), np.mean(analysis.backRatio)) )\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], "source": [ "# Make a color render and falsecolor image of the scene\n", "analysis.makeImage('side.vp')\n",