[1]:
# This information helps with debugging and getting support :)
import sys, platform
import pandas as pd
import bifacial_radiance as br
print("Working on a ", platform.system(), platform.release())
print("Python version ", sys.version)
print("Pandas version ", pd.__version__)
print("bifacial_radiance version ", br.__version__)
Working on a Windows 10
Python version 3.11.7 | packaged by Anaconda, Inc. | (main, Dec 15 2023, 18:05:47) [MSC v.1916 64 bit (AMD64)]
Pandas version 2.1.4
bifacial_radiance version 0+untagged.1553.g23d2640.dirty
2 - Single Axis Tracking Yearly Simulation#
Method Gencumsky has been modified to divide the yearly-cumulative sky into various skies, each one representing the cumulative irradiance for the hours at which the tracker is at a certain angle. For faster running, for a tracker that moves between 45 and -45 degrees limit angle, if only positions every 5 degrees are considered (45, 40, 35 …. -4-, -45), then only 18 skies (and 18 simulations) will be run for the whole year.
This procedure was presented in:
Ayala Pelaez S, Deline C, Greenberg P, Stein JS, Kostuk RK. Model and validation of single-axis tracking with bifacial PV. IEEE J Photovoltaics. 2019;9(3):715–21. https://ieeexplore.ieee.org/document/8644027 and https://www.nrel.gov/docs/fy19osti/72039.pdf (pre-print, conference version)
And finally: Condensed instructions
1. Create a folder for your simulation, and load bifacial_radiance#
First let’s set the folder where the simulation will be saved. By default, this is the TEMP folder in the bifacial_radiance distribution.
The lines below find the location of the folder relative to this Jupyter Journal. You can alternatively point to an empty directory (it will open a load GUI Visual Interface) or specify any other directory in your computer, for example:
testfolder = r’C::nbsphinx-math:`Users`:nbsphinx-math:`sayala`:nbsphinx-math:`Documents`:nbsphinx-math:`RadianceScenes`:nbsphinx-math:`Tutorials`:nbsphinx-math:`Journal`2’
[1]:
import os
from pathlib import Path
testfolder = Path().resolve().parent.parent / 'bifacial_radiance' / 'TEMP' / 'Tutorial_02'
# Another option using relative address; for some operative systems you might need '/' instead of '\'
# testfolder = os.path.abspath(r'..\..\bifacial_radiance\TEMP')
print ("Your simulation will be stored in %s" % testfolder)
if not os.path.exists(testfolder):
os.makedirs(testfolder)
Your simulation will be stored in C:\Users\mprillim\sam_dev\bifacial_radiance\bifacial_radiance\TEMP\Tutorial_02
This will load bifacial_radiance and other libraries from python that will be useful for this Jupyter Journal:
[2]:
from bifacial_radiance import *
import numpy as np
2. Create a Radiance Object, Set Albedo, and Download and Load Weather File#
These are all repeated steps from Tutorial 1, so condensing:
[3]:
# Create a RadianceObj 'object' named bifacial_example. no whitespace allowed
demo = RadianceObj('tutorial_2', path = str(testfolder))
albedo = 0.25
demo.setGround(albedo)
# Pull in meteorological data using pyEPW for any global lat/lon
epwfile = demo.getEPW(lat = 37.5, lon = -77.6) # This location corresponds to Richmond, VA.
# Read in the weather data pulled in above.
metdata = demo.readWeatherFile(weatherFile = epwfile)
path = C:\Users\mprillim\sam_dev\bifacial_radiance\bifacial_radiance\TEMP\Tutorial_02
Loading albedo, 1 value(s), 0.250 avg
1 nonzero albedo values.
Getting weather file: USA_VA_Richmond.724010_TMY2.epw
... OK!
8760 line in WeatherFile. Assuming this is a standard hourly WeatherFile for the year for purposes of saving Gencumulativesky temporary weather files in EPW folder.
Coercing year to 2021
Saving file EPWs\metdata_temp.csv, # points: 8760
Calculating Sun position for Metdata that is right-labeled with a delta of -30 mins. i.e. 12 is 11:30 sunpos
TRACKING Workflow#
Until now, all the steps looked the same from Tutorial 1. The following section follows similar steps, but the functions are specific for working with single axis tracking.
3. Set Tracking Angles#
This function will read the weather file, and based on the sun position it will calculate the angle the tracker should be at for each hour. It will create metdata files for each of the tracker angles considered.
[4]:
limit_angle = 5 # tracker rotation limit angle. Setting it ridiculously small so this runs faster.
angledelta = 5 # sampling between the limit angles.
backtrack = True
gcr = 0.33
cumulativesky = True # This is important for this example!
trackerdict = demo.set1axis(metdata = metdata, limit_angle = limit_angle, backtrack = backtrack,
gcr = gcr, cumulativesky = cumulativesky)
Saving file EPWs\1axis_-5.0.csv, # points: 2214
Saving file EPWs\1axis_0.0.csv, # points: 57
Saving file EPWs\1axis_5.0.csv, # points: 2096
Setting backtrack to True is important in this step, so the trackers correct for self-shading when following the sun at high zenith angles.
4. Generate the Sky#
This will create the skies for each sub-metdata file created by set1axis.
[5]:
trackerdict = demo.genCumSky1axis()
message: There were 2174 sun up hours in this climate file
Total Ibh/Lbh: 0.000000
Created skyfile skies\1axis_-5.0.rad
message: There were 50 sun up hours in this climate file
Total Ibh/Lbh: 0.000000
Created skyfile skies\1axis_0.0.rad
message: There were 2065 sun up hours in this climate file
Total Ibh/Lbh: 0.000000
Created skyfile skies\1axis_5.0.rad
This is how one of the cumulative sky .cal files associated with each .rad file generated look like:
Each of the values corresponds to the cumulative rradiance of one of those patches, for when the tracker is at that specific angle through the year.
5. Define the Module type#
Let’s make a more interesting module in this example. Let’s do 2-up configuration in portrait, with the modules rotating around a 10 centimeter round torque tube. Let’s add a gap between the two modules in 2-UP of 10 centimeters, as well as gap between the torque tube and the modules of 5 centimeters. Along the row, the modules are separated only 2 centimeters for this example. The torquetube is painted Metal_Grey in this example (it’s one of the materials available in Ground.rad, and it is 40% reflective).
Note that starting with bifacial_radiance version 0.4.0, the module object has a new geometry generation function addTorquetube
. The old way of passing a properly formatted dictionary as a keyword argument will still work too.
[6]:
x = 0.984 # meters
y = 1.7 # meters
moduletype = 'test-module'
numpanels = 2
zgap = 0.05
ygap = 0.10
xgap = 0.02
module = demo.makeModule(name=moduletype, x=x, y=y,xgap=xgap, ygap=ygap, zgap=zgap,
numpanels=numpanels)
module.addTorquetube(diameter=0.1, material='Metal_Grey', tubetype='round') # New torquetube generation function
print()
print(module)
print()
print(module.torquetube)
Module Name: test-module
Module test-module updated in module.json
Pre-existing .rad file objects\test-module.rad will be overwritten
Module test-module updated in module.json
Pre-existing .rad file objects\test-module.rad will be overwritten
{'x': 0.984, 'y': 1.7, 'z': 0.02, 'modulematerial': 'black', 'scenex': 1.004, 'sceney': 3.5, 'scenez': 0.1, 'numpanels': 2, 'bifi': 1, 'text': '! genbox black test-module 0.984 1.7 0.02 | xform -t -0.492 -1.75 0.1 -a 2 -t 0 1.8 0\r\n! genrev Metal_Grey tube1 t*1.004 0.05 32 | xform -ry 90 -t -0.502 0 0', 'modulefile': 'objects\\test-module.rad', 'glass': False, 'offsetfromaxis': 0.1, 'xgap': 0.02, 'ygap': 0.1, 'zgap': 0.05}
{'diameter': 0.1, 'tubetype': 'round', 'material': 'Metal_Grey', 'visible': True}
6. Make the Scene#
The scene Dictionary specifies the information of the scene. For tracking, different input parameters are expected in the dictionary, such as number of rows, number of modules per row, row azimuth, hub_height (distance between the axis of rotation of the modules and the ground).
[7]:
hub_height = 2.3
sceneDict = {'gcr': gcr,'hub_height':hub_height, 'nMods': 20, 'nRows': 7}
To make the scene we have to create a Scene Object through the method makeScene1axis. This method will create a .rad file in the objects folder, with the parameters specified in sceneDict and the module created above.
[8]:
trackerdict = demo.makeScene1axis(trackerdict = trackerdict, module = module, sceneDict = sceneDict)
Making .rad files for cumulativesky 1-axis workflow
3 Radfiles created in /objects/
7. Combine Ground, Sky and Scene Objects#
makeOct1axis joins the sky.rad file, ground.rad file, and the geometry.rad files created in makeScene.
[ ]:
trackerdict = demo.makeOct1axis(trackerdict = trackerdict)
Making 3 octfiles in root directory.
8. Analyze and get results#
We can choose to analyze any module in the Scene we have created. The default, if no modWanted or rowWanted is passed, is to sample the center module of the center row.
For this example we will sample row 2, module 9.
[ ]:
modWanted = 9
rowWanted = 2
customname = '_Row_2_Module_09' # This is useful if we want to do various analysis.
trackerdict = demo.analysis1axis(trackerdict, modWanted=9, rowWanted = 2, customname=customname)
Let’s look at the results with more detail. The analysis1axis routine created individual result .csv files for each angle, as well as one cumulative result .csv where the irradiance is added by sensor.
[ ]:
results = load.read1Result('cumulative_results__Row_2_Module_09.csv')
results
There are various things to notice:
The materials column has a specific format that will tell you if you are sampling the correct module:
a{ModWanted}.{rowWanted}.a{numPanel}.{moduletype}.material_key
Since for this journal numPanels = 2, numPanel can either be 0 or 1, for the East-most and West-most module in the collector width.
numPanel, ModWanted and RowWanted are indexed starting at 0 in the results.
material_key is from the surface generated inside radiance. Usually it is 6457 for top surface of hte module and .2310 for the bottom one.
Sensors sample always in the same direction. For this N-S aligned tracker, that is East-most to West. For this 2-up portrait tracker which is 3.5 meters, 20x7 rows and we are sampling module 9 on row 2, the East to West sampling goes from 22.6 m to 19.81 m = 2.79m. It is not exatly 3.5 because the sensors are spaced evenly through the collector width (CW):
When there is a ygap in the collector width (2-UP or more configuration), some of the sensors might end up sampling the torque tube, or the sky. You can ses that in the materials columns. This also happens if the number of sensors is quite high, the edges of the module might be sampled instead of the sensors. For this reason, before calculating bifacial gain these results must be cleaned. For more advanced simulations, make sure you clean each result csv file individually. We provide some options on load.py but some are very use-specific, so you might have to develop your own cleaning tool (or let us know on issues!)
Important: If you have torquetubes and y-gap values, make sure you clean your results.
9. Clean Results#
We have two options for cleaning results. The simples on is load.cleanResults, but there is also a deepClean for specific purposes.
cleanResults will find materials that should not have values and set them to NaN.
[ ]:
results_clean = load.cleanResult(results)
results_clean
These are the total irradiance values over all the hours of the year that the module at each sampling point will receive. Dividing the back irradiance average by the front irradiance average will give us the bifacial gain for the year:
Assuming that our module from Prism Solar has a bifaciality factor (rear to front performance) of 90%, our bifacial gain is of:
[ ]:
bifacialityfactor = 0.9
print('Annual bifacial ratio: %0.3f ' %( np.nanmean(results_clean.Wm2Back) * bifacialityfactor / np.nanmean(results_clean.Wm2Front)) )
CONDENSED VERSION#
Everything we’ve done so far in super short condensed version:
[ ]:
albedo = 0.25
lat = 37.5
lon = -77.6
nMods = 20
nRows = 7
hub_height = 2.3
gcr = 0.33
moduletype = 'test-module' # this must already exist since we are not calling makeModule in this CONDENSED example.
#testfolder = r'C:\Users\sayala\Documents\RadianceScenes\Tutorials\Journal2'
limit_angle = 5
angeldelta = 5
backtrack = True
gcr = gcr
modWanted = 9
rowWanted = 2
cumulativesky = True
import bifacial_radiance
demo = RadianceObj('test')
demo.setGround(albedo)
epwfile = demo.getEPW(lat, lon)
metdata = demo.readWeatherFile(epwfile)
demo.set1axis(limit_angle=limit_angle, backtrack=backtrack, gcr=gcr, cumulativesky=cumulativesky)
demo.genCumSky1axis()
sceneDict = {'gcr': gcr,'hub_height':hub_height, 'nMods': nMods, 'nRows': nRows} # orientation deprecated on v.0.2.4.
demo.makeScene1axis(module=moduletype, sceneDict=sceneDict)
demo.makeOct1axis()
demo.analysis1axis(modWanted=modWanted, rowWanted=rowWanted);