# -*- coding: utf-8 -*-
"""
@author: cdeline
ModuleObj class for defining module geometry
"""
import os
import numpy as np
from bifacial_radiance.main import _missingKeyWarning, _popen, DATA_PATH
class SuperClass:
def __repr__(self):
return str(self.getDataDict())
def getDataDict(self):
"""
return dictionary values from self. Originally stored as self.data
"""
return dict(zip(self.keys,[getattr(self,k) for k in self.keys]))
[docs]class ModuleObj(SuperClass):
"""
Module object to store module & torque tube details.
Does the heavy lifting of demo.makeModule()
Module details are passed in and stored in module.json.
Pass this object into makeScene or makeScene1axis.
"""
[docs] def __init__(self, name=None, x=None, y=None, z=None, bifi=1, modulefile=None,
text=None, customtext='', xgap=0.01, ygap=0.0, zgap=0.1,
numpanels=1, rewriteModulefile=True, cellModule=None,
glass=False, modulematerial='black', tubeParams=None,
frameParams=None, omegaParams=None, hpc=False):
"""
Add module details to the .JSON module config file module.json
Module definitions assume that the module .rad file is defined
with zero tilt, centered along the x-axis and y-axis for the center
of rotation of the module (+X/2, -X/2, +Y/2, -Y/2 on each side).
Tip: to define a module that is in 'portrait' mode, y > x.
Parameters
------------
name : str
Input to name the module type
x : numeric
Width of module along the axis of the torque tube or rack. (meters)
y : numeric
Length of module (meters)
bifi : numeric
Bifaciality of the panel (not currently used). Between 0 (monofacial)
and 1, default 1.
modulefile : str
Existing radfile location in \objects. Otherwise a default value is used
text : str
Text used in the radfile to generate the module. Manually passing
this value will overwrite module definition
customtext : str
Added-text used in the radfile to generate any
extra details in the racking/module. Does not overwrite
generated module (unlike "text"), but adds to it at the end.
rewriteModulefile : bool
Default True. Will rewrite module file each time makeModule is run.
numpanels : int
Number of modules arrayed in the Y-direction. e.g.
1-up or 2-up, etc. (supports any number for carport/Mesa simulations)
xgap : float
Panel space in X direction. Separation between modules in a row.
ygap : float
Gap between modules arrayed in the Y-direction if any.
zgap : float
Distance behind the modules in the z-direction to the edge of the tube (m)
glass : bool
Add 5mm front and back glass to the module (glass/glass). Warning:
glass increases the analysis variability. Recommend setting
accuracy='high' in AnalysisObj.analysis()
cellModule : dict
Dictionary with input parameters for creating a cell-level module.
Shortcut for ModuleObj.addCellModule()
tubeParams : dict
Dictionary with input parameters for creating a torque tube as part of the
module. Shortcut for ModuleObj.addTorquetube()
frameParams : dict
Dictionary with input parameters for creating a frame as part of the module.
Shortcut for ModuleObj.addFrame()
omegaParams : dict
Dictionary with input parameters for creating a omega or module support
structure. Shortcut for ModuleObj.addOmega()
hpc : bool (default False)
Set up module in HPC mode. Namely turn off read/write to module.json
and just pass along the details in the module object. Note that
calling e.g. addTorquetube() after this will tend to write to the
module.json so pass all geometry parameters at once in to makeModule
for best response.
'"""
self.keys = ['x', 'y', 'z', 'modulematerial', 'scenex','sceney',
'scenez','numpanels','bifi','text','modulefile', 'glass',
'offsetfromaxis','xgap','ygap','zgap' ]
#replace whitespace with underlines. what about \n and other weird characters?
# TODO: Address above comment?
self.name = str(name).strip().replace(' ', '_')
self.customtext = customtext
self._manual_text = text
# are we writing to JSON with passed data or just reading existing?
if (x is None) & (y is None) & (cellModule is None) & (text is None):
#just read in file. If .rad file doesn't exist, make it.
self.readModule(name=name)
if name is not None:
self._saveModule(savedata=None, json=False,
rewriteModulefile=False)
else:
# set initial variables that aren't passed in
scenex = sceney = scenez = offsetfromaxis = 0
"""
# TODO: this is kind of confusing and should probably be changed
# set torque tube internal dictionary
tubeBool = torquetube
torquetube = {'bool':tubeBool,
'diameter':tubeParams['diameter'],
'tubetype':tubeParams['tubetype'],
'material':tubeParams['material']
}
try:
self.axisofrotationTorqueTube = tubeParams['axisofrotation']
except AttributeError:
self.axisofrotationTorqueTube = False
"""
if tubeParams:
if 'bool' in tubeParams: # backward compatible with pre-0.4
tubeParams['visible'] = tubeParams.pop('bool')
if 'torqueTubeMaterial' in tubeParams: # pre-0.4
tubeParams['material'] = tubeParams.pop('torqueTubeMaterial')
self.addTorquetube(**tubeParams, recompile=False)
if omegaParams:
self.addOmega(**omegaParams, recompile=False)
if frameParams:
self.addFrame(**frameParams, recompile=False)
if cellModule:
self.addCellModule(**cellModule, recompile=False)
if self._manual_text:
print('Warning: Module text manually passed and not '
f'generated: {self._manual_text}')
# set data object attributes from datakey list.
for key in self.keys:
setattr(self, key, eval(key))
if self.modulefile is None:
self.modulefile = os.path.join('objects',
self.name + '.rad')
print("\nModule Name:", self.name)
if hpc:
self.compileText(rewriteModulefile, json=False)
else:
self.compileText(rewriteModulefile)
[docs] def compileText(self, rewriteModulefile=True, json=True):
"""
Generate the text for the module .rad file based on ModuleObj attributes.
Optionally save details to the module.json and module.rad files.
Parameters
------------
rewriteModulefile : bool (default True)
Overwrite the .rad file for the module
json : bool (default True)
Update the module.json file with ModuleObj attributes
"""
saveDict = self.getDataDict()
if hasattr(self,'cellModule'):
saveDict = {**saveDict, 'cellModule':self.cellModule.getDataDict()}
if hasattr(self,'torquetube'):
saveDict = {**saveDict, 'torquetube':self.torquetube.getDataDict()}
if hasattr(self,'omega'):
saveDict = {**saveDict, 'omegaParams':self.omega.getDataDict()}
if hasattr(self,'frame'):
saveDict = {**saveDict, 'frameParams':self.frame.getDataDict()}
self._makeModuleFromDict(**saveDict)
#write JSON data out and write radfile if it doesn't exist
self._saveModule({**saveDict, **self.getDataDict()}, json=json,
rewriteModulefile=rewriteModulefile)
[docs] 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 (str) Name of module to be read
Returns: moduleDict dictionary 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:
return list(modulenames)
if name in modulenames:
moduleDict = data[name]
self.name = name
# BACKWARDS COMPATIBILITY - look for missing keys
if not 'scenex' in moduleDict:
moduleDict['scenex'] = moduleDict['x']
if not 'sceney' in moduleDict:
moduleDict['sceney'] = moduleDict['y']
if not 'offsetfromaxis' in moduleDict:
moduleDict['offsetfromaxis'] = 0
if not 'modulematerial' in moduleDict:
moduleDict['modulematerial'] = 'black'
if not 'glass' in moduleDict:
moduleDict['glass'] = False
if not 'z' in moduleDict:
moduleDict['z'] = 0.02
# set ModuleObj attributes from moduleDict
#self.data = moduleDict
for keys in moduleDict:
setattr(self, keys, moduleDict[keys])
# Run torquetube, frame, omega, cellmodule
if moduleDict.get('torquetube'):
tubeParams = moduleDict['torquetube']
if 'bool' in tubeParams: # backward compatible with pre-0.4
tubeParams['visible'] = tubeParams.pop('bool')
if 'torqueTubeMaterial' in tubeParams: # pre-0.4
tubeParams['material'] = tubeParams.pop('torqueTubeMaterial')
self.addTorquetube(**tubeParams, recompile=False)
if moduleDict.get('cellModule'):
self.addCellModule(**moduleDict['cellModule'], recompile=False)
if moduleDict.get('omegaParams'):
self.addOmega(**moduleDict['omegaParams'], recompile=False)
if moduleDict.get('frameParams'):
self.addFrame(**moduleDict['frameParams'], recompile=False)
return moduleDict
else:
print('Error: module name {} doesnt exist'.format(name))
return {}
def _saveModule(self, savedata, json=True, rewriteModulefile=True):
"""
write out changes to module.json and make radfile if it doesn't
exist. if rewriteModulefile is true, always overwrite Radfile.
Parameters
----------
json : bool, default is True. Save JSON
rewriteModulefile : bool, default is True.
"""
import json as jsonmodule
if json:
filedir = os.path.join(DATA_PATH, 'module.json')
with open(filedir) as configfile:
data = jsonmodule.load(configfile)
data.update({self.name:savedata})
with open(os.path.join(DATA_PATH, 'module.json') ,'w') as configfile:
jsonmodule.dump(data, configfile, indent=4, sort_keys=True,
cls=MyEncoder)
print('Module {} updated in module.json'.format(self.name))
# check that self.modulefile is not none
if self.modulefile is None:
self.modulefile = os.path.join('objects',
self.name + '.rad')
if rewriteModulefile & os.path.isfile(self.modulefile):
print(f"Pre-existing .rad file {self.modulefile} "
"will be overwritten\n")
os.remove(self.modulefile)
if not os.path.isfile(self.modulefile):
# py2 and 3 compatible: binary write, encode text first
with open(self.modulefile, 'wb') as f:
f.write(self.text.encode('ascii'))
[docs] def showModule(self):
"""
Method to call objview and render the module object
(visualize it).
Parameters: None
"""
cmd = 'objview %s %s' % (os.path.join('materials', 'ground.rad'),
self.modulefile)
_,err = _popen(cmd,None)
if err is not None:
print('Error: {}'.format(err))
print('possible solution: install radwinexe binary package from '
'http://www.jaloxa.eu/resources/radiance/radwinexe.shtml'
' into your RADIANCE binaries path')
return
[docs] def saveImage(self, filename=None):
"""
Duplicate objview process to save an image of the module in /images/
Parameters:
filename : string, optional. name for image file, defaults to module name
"""
import tempfile
temp_dir = tempfile.TemporaryDirectory()
pid = os.getpid()
if filename is None:
filename = f'{self.name}'
# fake lighting temporary .radfile
ltfile = os.path.join(temp_dir.name, f'lt{pid}.rad')
with open(ltfile, 'w') as f:
f.write("void glow dim 0 0 4 .1 .1 .15 0\n" +\
"dim source background 0 0 4 0 0 1 360\n"+\
"void light bright 0 0 3 1000 1000 1000\n"+\
"bright source sun1 0 0 4 1 .2 1 5\n"+\
"bright source sun2 0 0 4 .3 1 1 5\n"+\
"bright source sun3 0 0 4 -1 -.7 1 5")
# make .rif and run RAD
riffile = os.path.join(temp_dir.name, f'ov{pid}.rif')
with open(riffile, 'w') as f:
f.write("scene= materials/ground.rad " +\
f"{self.modulefile} {ltfile}\n".replace("\\",'/') +\
"EXPOSURE= .5\nUP= Z\nview= XYZ\n" +\
#f"OCTREE= ov{pid}.oct\n"+\
f"oconv= -f\nPICT= images/{filename}")
_,err = _popen(["rad",'-s',riffile], None)
if err:
print(err)
else:
print(f'Module image saved: images/{filename}_XYZ.hdr')
temp_dir.cleanup()
[docs] def addTorquetube(self, diameter=0.1, tubetype='Round', material='Metal_Grey',
axisofrotation=True, visible=True, recompile=True):
"""
For adding torque tubes to the module simulation.
Parameters
----------
diameter : float Tube diameter in meters. For square, diameter means
the length of one of the square-tube side. For Hex,
diameter is the distance between two vertices
(diameter of the circumscribing circle). Default 0.1
tubetype : str Options: 'Square', 'Round' (default), 'Hex' or 'Oct'
Tube cross section
material : str Options: 'Metal_Grey' or 'black'. Material for the
torque tube.
axisofrotation (bool) : Default True. IF true, creates geometry
so center of rotation is at the center of the
torquetube, with an offsetfromaxis equal to half the
torquetube diameter + the zgap. If there is no
torquetube (visible=False), offsetformaxis will
equal the zgap.
visible (bool) : Default True. If false, geometry is set
as if the torque tube were present (e.g. zgap,
axisofrotation) but no geometry for the tube is made
recompile : Bool Rewrite .rad file and module.json file (default True)
"""
self.torquetube = Tube(diameter=diameter, tubetype=tubetype,
material=material, axisofrotation=axisofrotation,
visible=visible)
if recompile:
self.compileText()
[docs] def addOmega(self, omega_material='Metal_Grey', omega_thickness=0.004,
inverted=False, x_omega1=None, x_omega3=None, y_omega=None,
mod_overlap=None, recompile=True):
"""
Add the racking structure element `omega`, which connects
the frame to the torque tube.
Parameters
----------
omega_material : str The material the omega structure is made of.
Default: 'Metal_Grey'
x_omega1 : float The length of the module-adjacent arm of the
omega parallel to the x-axis of the module
mod_overlap : float The length of the overlap between omega and
module surface on the x-direction
y_omega : float Length of omega (Y-direction)
omega_thickness : float Omega thickness. Default 0.004
x_omega3 : float X-direction length of the torquetube adjacent
arm of omega
inverted : Bool Modifies the way the Omega is set on the Torquetbue
Looks like False: u vs True: n (default False)
NOTE: The part that bridges the x-gap for a False
regular orientation omega (inverted = False),
is the x_omega3;
and for inverted omegas (inverted=True) it is
x_omega1.
recompile : Bool Rewrite .rad file and module.json file (default True)
"""
self.omega = Omega(self, omega_material=omega_material,
omega_thickness=omega_thickness,
inverted=inverted, x_omega1=x_omega1,
x_omega3=x_omega3, y_omega=y_omega,
mod_overlap=mod_overlap)
if recompile:
self.compileText()
[docs] def addFrame(self, frame_material='Metal_Grey', frame_thickness=0.05,
frame_z=0.3, nSides_frame=4, frame_width=0.05, recompile=True):
"""
Add a metal frame geometry around the module.
Parameters
------------
frame_material : str The material the frame structure is made of
frame_thickness : float The profile thickness of the frame
frame_z : float The Z-direction length of the frame that extends
below the module plane
frame_width : float The length of the bottom frame that is bolted
with the omega
nSides_frame : int The number of sides of the module that are framed.
4 (default) or 2
"""
self.frame = Frame(frame_material=frame_material,
frame_thickness=frame_thickness,
frame_z=frame_z, nSides_frame=nSides_frame,
frame_width=frame_width)
if recompile:
self.compileText()
[docs] def addCellModule(self, numcellsx, numcellsy ,xcell, ycell,
xcellgap=0.02, ycellgap=0.02, centerJB=None, recompile=True):
"""
Create a cell-level module, with individually defined cells and gaps
Parameters
------------
numcellsx : int Number of cells in the X-direction within the module
numcellsy : int Number of cells in the Y-direction within the module
xcell : float Width of each cell (X-direction) in the module
ycell : float Length of each cell (Y-direction) in the module
xcellgap : float Spacing between cells in the X-direction. 0.02 default
ycellgap : float Spacing between cells in the Y-direction. 0.02 default
centerJB : float (optional) Distance betwen both sides of cell arrays
in a center-JB half-cell module. If 0 or not provided,
module will not have the center JB spacing.
Only implemented for 'portrait' mode at the moment.
(numcellsy > numcellsx).
"""
import warnings
if centerJB:
warnings.warn(
'centerJB functionality is currently experimental and subject '
'to change in future releases. ' )
self.cellModule = CellModule(numcellsx=numcellsx, numcellsy=numcellsy,
xcell=xcell, ycell=ycell, xcellgap=xcellgap,
ycellgap=ycellgap, centerJB=centerJB)
if recompile:
self.compileText()
def _makeModuleFromDict(self, x=None, y=None, z=None, xgap=None, ygap=None,
zgap=None, numpanels=None, modulefile=None,
modulematerial=None, **kwargs):
"""
go through and generate the text required to make a module
"""
import warnings
#aliases for equations below
Ny = numpanels
_cc = 0 # cc is an offset given to the module when cells are used
# so that the sensors don't fall in air when numcells is even.
# For non cell-level modules default is 0.
# Update values for rotating system around torque tube.
diam=0
if hasattr(self, 'torquetube'):
diam = self.torquetube.diameter
if self.torquetube.axisofrotation is True:
self.offsetfromaxis = np.round(zgap + diam/2.0,8)
if hasattr(self, 'frame'):
self.offsetfromaxis = self.offsetfromaxis + self.frame.frame_z
# TODO: make sure the above is consistent with old version below
"""
if torquetube:
diam = torquetube['diameter']
torquetube_bool = torquetube['bool']
else:
diam=0
torquetube_bool = False
if self.axisofrotationTorqueTube == True:
if torquetube_bool == True:
self.offsetfromaxis = np.round(zgap + diam/2.0,8)
else:
self.offsetfromaxis = zgap
if hasattr(self, 'frame'):
self.offsetfromaxis = self.offsetfromaxis + self.frame.frame_z
"""
# Adding the option to replace the module thickess
if self.glass:
zglass = 0.01
print("\nWarning: module glass increases analysis variability. "
"Recommend setting `accuracy='high'` in AnalysisObj.analysis().\n")
else:
zglass = 0.0
if z is None:
if self.glass:
z = 0.001
else:
z = 0.020
self.z = z
self.zglass = zglass
if modulematerial is None:
modulematerial = 'black'
self.modulematerial = 'black'
if self._manual_text is not None:
text = self._manual_text
self._manual_text = None
else:
if hasattr(self, 'cellModule'):
(text, x, y, _cc) = self.cellModule._makeCellLevelModule(self, z, Ny, ygap,
modulematerial)
else:
try:
text = '! genbox {} {} {} {} {} '.format(modulematerial,
self.name, x, y, z)
text +='| xform -t {} {} {} '.format(-x/2.0,
(-y*Ny/2.0)-(ygap*(Ny-1)/2.0),
self.offsetfromaxis)
text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
packagingfactor = 100.0
except Exception as err: # probably because no x or y passed
raise Exception('makeModule variable {}'.format(err.args[0])+
' and cellModule is None. '+
'One or the other must be specified.')
self.scenex = x + xgap
self.sceney = np.round(y*numpanels + ygap*(numpanels-1), 8)
self.scenez = np.round(zgap + diam / 2.0, 8)
if hasattr(self, 'frame'):
_zinc, frametext = self.frame._makeFrames(
x=x,y=y, ygap=ygap,numpanels=Ny,
offsetfromaxis=self.offsetfromaxis- 0.5*zglass)
else:
frametext = ''
_zinc = 0 # z increment from frame thickness
_zinc = _zinc + 0.5 * zglass
if hasattr(self, 'omega'):
# This also defines scenex for length of the torquetube.
omega2omega_x, omegatext = self.omega._makeOmega(x=x,y=y, xgap=xgap,
zgap=zgap, z_inc=_zinc,
offsetfromaxis=self.offsetfromaxis)
if omega2omega_x > self.scenex:
self.scenex = omega2omega_x
# TODO: is the above line better than below?
# I think this causes it's own set of problems, need to check.
"""
if self.scenex <x:
scenex = x+xgap #overwriting scenex to maintain torquetube continuity
print ('Warning: Omega values have been provided, but' +
'the distance between modules with the omega'+
'does not match the x-gap provided.'+
'Setting x-gap to be the space between modules'+
'from the omega.')
else:
print ('Warning: Using omega-to-omega distance to define'+
'gap between modules'
+'xgap value not being used')
"""
else:
omegatext = ''
# Defining scenex if it was not defined by the Omegas,
# after the module has been created in case it is a
# cell-level Module, in which the "x" gets calculated internally.
# Also sanity check in case omega-to-omega distance is smaller
# than module.
#if torquetube_bool is True:
if hasattr(self,'torquetube'):
if self.torquetube.visible:
text += self.torquetube._makeTorqueTube(cc=_cc, zgap=zgap,
z_inc=_zinc, scenex=self.scenex)
# TODO: should there be anything updated here like scenez?
# YES.
if self.glass:
edge = 0.01
text = text+'\r\n! genbox stock_glass {} {} {} {} '.format(self.name+'_Glass',x+edge, y+edge, zglass)
text +='| xform -t {} {} {} '.format(-x/2.0-0.5*edge + _cc,
(-y*Ny/2.0)-(ygap*(Ny-1)/2.0)-0.5*edge,
self.offsetfromaxis - 0.5*zglass)
text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
text += frametext
if hasattr(self, 'omega'):
text += self.omega.text
text += self.customtext # For adding any other racking details at the module level that the user might want.
self.text = text
return text
#End of makeModuleFromDict()
# end of ModuleObj
class Omega(SuperClass):
def __init__(self, module, omega_material='Metal_Grey', omega_thickness=0.004,
inverted=False, x_omega1=None, x_omega3=None, y_omega=None,
mod_overlap=None):
"""
==================== ===============================================
Keys : type Description
================ ===============================================
module : ModuleObj Parent object with details related to geometry
omega_material : str The material the omega structure is made of
omega_thickness : float Omega thickness
inverted : Bool Modifies the way the Omega is set on the Torquetbue
Looks like False: u vs True: n (default False)
x_omega1 : float The length of the module-adjacent arm of the
omega parallel to the x-axis of the module
y_omega : float Length of omega (Y-direction)
x_omega3 : float X-direction length of the torquetube adjacent
arm of omega
mod_overlap : float The length of the overlap between omega and
module surface on the x-direction
===================== ===============================================
"""
self.keys = ['omega_material', 'x_omega1', 'mod_overlap', 'y_omega',
'omega_thickness','x_omega3','inverted']
if x_omega1 is None:
if inverted:
x_omega1 = module.xgap*0.5
else:
x_omega1 = module.xgap*0.5*0.6
_missingKeyWarning('Omega', 'x_omega1', x_omega1)
if x_omega3 is None:
if inverted:
x_omega3 = module.xgap*0.5*0.3
else:
x_omega3 = module.xgap*0.5
_missingKeyWarning('Omega', 'x_omega3', x_omega3)
if y_omega is None:
y_omega = module.y/2
_missingKeyWarning('Omega', 'y_omega', y_omega)
if mod_overlap is None:
mod_overlap = x_omega1*0.6
_missingKeyWarning('Omega', 'mod_overlap', mod_overlap)
# set data object attributes from datakey list.
for key in self.keys:
setattr(self, key, eval(key))
def _makeOmega(self, x, y, xgap, zgap, offsetfromaxis, z_inc = 0, **kwargs):
"""
Helper function for creating a module that includes the racking
structure element `omega`.
TODO: remove some or all of this documentation since this is an internal function
Parameters
------------
x : numeric
Width of module along the axis of the torque tube or racking structure. (meters).
y : numeric
Length of module (meters)
xgap : float
Panel space in X direction. Separation between modules in a row.
zgap : float
Distance behind the modules in the z-direction to the edge of the tube (m)
offsetfromaxis : float
Internally defined variable in makeModule that specifies how much
the module is offset from the Axis of Rotation due to zgap and or
frame thickness.
z_inc : dict
Internally defined variable in makeModule that specifies how much
the module is offseted by the Frame.
"""
# set local variables
omega_material = self.omega_material
x_omega1 = self.x_omega1
mod_overlap = self.mod_overlap
y_omega = self.y_omega
omega_thickness = self.omega_thickness
x_omega3 = self.x_omega3
z_omega2 = zgap
x_omega2 = omega_thickness
z_omega1 = omega_thickness
z_omega3 = omega_thickness
#naming the omega pieces
name1 = 'mod_adj'
name2 = 'verti'
name3 = 'tt_adj'
# defining the module adjacent member of omega
x_translate1 = -x/2 - x_omega1 + mod_overlap
y_translate = -y_omega/2 #common for all the pieces
z_translate1 = offsetfromaxis-z_omega1
#defining the vertical (zgap) member of the omega
x_translate2 = x_translate1
z_translate2 = offsetfromaxis-z_omega2
#defining the torquetube adjacent member of omega
x_translate3 = x_translate1-x_omega3
z_translate3 = z_translate2
if z_inc != 0:
z_translate1 += -z_inc
z_translate2 += -z_inc
z_translate3 += -z_inc
# for this code, only the translations need to be shifted for the inverted omega
if self.inverted == True:
# shifting the non-inv omega shape of west as inv omega shape of east
x_translate1_inv_east = x/2-mod_overlap
x_shift_east = x_translate1_inv_east - x_translate1
# shifting the non-inv omega shape of west as inv omega shape of east
x_translate1_inv_west = -x_translate1_inv_east - x_omega1
x_shift_west = -x_translate1_inv_west + (-x_translate1-x_omega1)
#customizing the East side of the module for omega_inverted
omegatext = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1_inv_east, y_translate, z_translate1)
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name2, x_omega2, y_omega, z_omega2, x_translate2 + x_shift_east, y_translate, z_translate2)
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name3, x_omega3, y_omega, z_omega3, x_translate3 + x_shift_east, y_translate, z_translate3)
#customizing the West side of the module for omega_inverted
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1_inv_west, y_translate, z_translate1)
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name2, x_omega2, y_omega, z_omega2, -x_translate2-x_omega2 -x_shift_west, y_translate, z_translate2)
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name3, x_omega3, y_omega, z_omega3, -x_translate3-x_omega3 - x_shift_west, y_translate, z_translate3)
omega2omega_x = -x_translate1_inv_east*2
else:
#customizing the West side of the module for omega
omegatext = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1, y_translate, z_translate1)
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name2, x_omega2, y_omega, z_omega2, x_translate2, y_translate, z_translate2)
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name3, x_omega3, y_omega, z_omega3, x_translate3, y_translate, z_translate3)
#customizing the East side of the module for omega
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, -x_translate1-x_omega1, y_translate, z_translate1)
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name2, x_omega2, y_omega, z_omega2, -x_translate2-x_omega2, y_translate, z_translate2)
omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name3, x_omega3, y_omega, z_omega3, -x_translate3-x_omega3, y_translate, z_translate3)
omega2omega_x = -x_translate3*2
self.text = omegatext
self.omega2omega_x = omega2omega_x
return omega2omega_x,omegatext
class Frame(SuperClass):
def __init__(self, frame_material='Metal_Grey', frame_thickness=0.05,
frame_z=None, nSides_frame=4, frame_width=0.05):
"""
Parameters
------------
frame_material : str The material the frame structure is made of
frame_thickness : float The profile thickness of the frame
frame_z : float The Z-direction length of the frame that extends
below the module plane
frame_width : float The length of the bottom frame that is bolted
with the omega
nSides_frame : int The number of sides of the module that are framed.
4 (default) or 2
"""
self.keys = ['frame_material', 'frame_thickness', 'frame_z', 'frame_width',
'nSides_frame']
if frame_z is None:
frame_z = 0.03
_missingKeyWarning('Frame', 'frame_z', frame_z)
# set data object attributes from datakey list.
for key in self.keys:
setattr(self, key, eval(key))
def _makeFrames(self, x, y, ygap, numpanels, offsetfromaxis):
"""
Helper function for creating a module that includes the frames attached to the module,
Parameters
------------
frameParams : dict
Dictionary with input parameters for creating a frame as part of the module.
See details below for keys needed.
x : numeric
Width of module along the axis of the torque tube or racking structure. (meters).
y : numeric
Length of module (meters)
ygap : float
Gap between modules arrayed in the Y-direction if any.
numpanels : int
Number of modules arrayed in the Y-direction. e.g.
1-up or 2-up, etc. (supports any number for carport/Mesa simulations)
offsetfromaxis : float
Internally defined variable in makeModule that specifies how much
the module is offset from the Axis of Rotation due to zgap and or
frame thickness.
"""
#
if self.nSides_frame == 2 and x>y:
print("Development Warning: Frames has only 2 sides and module is"+
"in ladscape. This functionality is not working properly yet"+
"for this release. We are overwriting nSide_frame = 4 to continue."+
"If this functionality is pivotal to you we can prioritize adding it but"+
"please comunicate with the development team. Thank you.")
self.nSides_frame = 4
#Defining internal names
frame_material = self.frame_material
f_thickness = self.frame_thickness
f_height = self.frame_z
n_frame = self.nSides_frame
fl_x = self.frame_width
y_trans_shift = 0 #pertinent to the case of x>y with 2-sided frame
# Recalculating width ignoring the thickness of the aluminum
# for internal positioining and sizing of hte pieces
fl_x = fl_x-f_thickness
if x>y and n_frame==2:
x_temp,y_temp = y,x
rotframe = 90
frame_y = x
y_trans_shift = x/2-y/2
else:
x_temp,y_temp = x,y
frame_y = y
rotframe = 0
Ny = numpanels
y_half = (y*Ny/2)+(ygap*(Ny-1)/2)
# taking care of lengths and translation points
# The pieces are same and symmetrical for west and east
# naming the frame pieces
nameframe1 = 'frameside'
nameframe2 = 'frameleg'
#frame sides
few_x = f_thickness
few_y = frame_y
few_z = f_height
fw_xt = -x_temp/2 # in case of x_temp = y this doesn't reach panel edge
fe_xt = x_temp/2-f_thickness
few_yt = -y_half-y_trans_shift
few_zt = offsetfromaxis-f_height
#frame legs for east-west
flw_xt = -x_temp/2 + f_thickness
fle_xt = x_temp/2 - f_thickness-fl_x
flew_yt = -y_half-y_trans_shift
flew_zt = offsetfromaxis-f_height
#pieces for the shorter side (north-south in this case)
#filler
fns_x = x_temp-2*f_thickness
fns_y = f_thickness
fns_z = f_height-f_thickness
fns_xt = -x_temp/2+f_thickness
fn_yt = -y_half+y-f_thickness
fs_yt = -y_half
fns_zt = offsetfromaxis-f_height+f_thickness
# the filler legs
filleg_x = x_temp-2*f_thickness-2*fl_x
filleg_y = f_thickness + fl_x
filleg_z = f_thickness
filleg_xt = -x_temp/2+f_thickness+fl_x
fillegn_yt = -y_half+y-f_thickness-fl_x
fillegs_yt = -y_half
filleg_zt = offsetfromaxis-f_height
# making frames: west side
frame_text = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, few_x, few_y, few_z, fw_xt, few_yt, few_zt)
frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, fl_x, frame_y, f_thickness, flw_xt, flew_yt, flew_zt)
frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
# making frames: east side
frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, few_x, few_y, few_z, fe_xt, few_yt, few_zt)
frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, fl_x, frame_y, f_thickness, fle_xt, flew_yt, flew_zt)
frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
if n_frame == 4:
#making frames: north side
frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, fns_x, fns_y, fns_z, fns_xt, fn_yt, fns_zt)
frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, filleg_x, filleg_y, filleg_z, filleg_xt, fillegn_yt, filleg_zt)
frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
#making frames: south side
frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, fns_x, fns_y, fns_z, fns_xt, fs_yt, fns_zt)
frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, filleg_x, filleg_y, filleg_z, filleg_xt, fillegs_yt, filleg_zt)
frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
z_inc = f_height
return z_inc, frame_text
class Tube(SuperClass):
def __init__(self, diameter=0.1, tubetype='Round', material='Metal_Grey',
axisofrotation=True, visible=True):
"""
================ ====================================================
Keys : type Description
================ ====================================================
diameter : float Tube diameter in meters. For square, diameter means
the length of one of the square-tube side. For Hex,
diameter is the distance between two vertices
(diameter of the circumscribing circle). Default 0.1
tubetype : str Options: 'Square', 'Round' (default), 'Hex' or 'Oct'
Tube cross section
material : str Options: 'Metal_Grey' or 'black'. Material for the
torque tube.
axisofrotation (bool) : Default True. IF true, creates geometry
so center of rotation is at the center of the
torquetube, with an offsetfromaxis equal to half the
torquetube diameter + the zgap. If there is no
torquetube (visible=False), offsetformaxis will
equal the zgap.
visible (bool) : Default True. If false, geometry is set
as if the torque tube were present (e.g. zgap,
axisofrotation) but no geometry for the tube is made
================ ====================================================
"""
self.keys = ['diameter', 'tubetype', 'material', 'visible'] # what about axisofrotation?
self.axisofrotation = axisofrotation
# set data object attributes from datakey list.
for key in self.keys:
setattr(self, key, eval(key))
def _makeTorqueTube(self, cc, z_inc, zgap, scenex):
"""
Return text string for generating the torque tube geometry
Parameters
cc = module._cc #horizontal offset to center of a cell
"""
import math
text = ''
tto = 0 # Torquetube Offset. Default = 0 if axisofrotationTT == True
diam = self.diameter #alias
material = self.material #alias
if self.tubetype.lower() == 'square':
if self.axisofrotation == False:
tto = -z_inc-zgap-diam/2.0
text += '\r\n! genbox {} tube1 {} {} {} '.format(material,
scenex, diam, diam)
text += '| xform -t {} {} {}'.format(-(scenex)/2.0+cc,
-diam/2.0, -diam/2.0+tto)
elif self.tubetype.lower() == 'round':
if self.axisofrotation == False:
tto = -z_inc-zgap-diam/2.0
text += '\r\n! genrev {} tube1 t*{} {} '.format(material, scenex, diam/2.0)
text += '32 | xform -ry 90 -t {} {} {}'.format(-(scenex)/2.0+cc, 0, tto)
elif self.tubetype.lower() == 'hex':
radius = 0.5*diam
if self.axisofrotation == False:
tto = -z_inc-radius*math.sqrt(3.0)/2.0-zgap
text += '\r\n! genbox {} hextube1a {} {} {} | xform -t {} {} {}'.format(
material, scenex, radius, radius*math.sqrt(3),
-(scenex)/2.0+cc, -radius/2.0, -radius*math.sqrt(3.0)/2.0+tto) #ztran -radius*math.sqrt(3.0)-tto
# Create, translate to center, rotate, translate back to prev. position and translate to overal module position.
text = text+'\r\n! genbox {} hextube1b {} {} {} | xform -t {} {} {} -rx 60 -t 0 0 {}'.format(
material, scenex, radius, radius*math.sqrt(3), -(scenex)/2.0+cc, -radius/2.0, -radius*math.sqrt(3.0)/2.0, tto) #ztran (radius*math.sqrt(3.0)/2.0)-radius*math.sqrt(3.0)-tto)
text = text+'\r\n! genbox {} hextube1c {} {} {} | xform -t {} {} {} -rx -60 -t 0 0 {}'.format(
material, scenex, radius, radius*math.sqrt(3), -(scenex)/2.0+cc, -radius/2.0, -radius*math.sqrt(3.0)/2.0, tto) #ztran (radius*math.sqrt(3.0)/2.0)-radius*math.sqrt(3.0)-tto)
elif self.tubetype.lower()=='oct':
radius = 0.5*diam
s = diam / (1+math.sqrt(2.0)) #
if self.axisofrotation == False:
tto = -z_inc-radius-zgap
text = text+'\r\n! genbox {} octtube1a {} {} {} | xform -t {} {} {}'.format(
material, scenex, s, diam, -(scenex)/2.0, -s/2.0, -radius+tto)
# Create, translate to center, rotate, translate back to prev. position and translate to overal module position.
text = text+'\r\n! genbox {} octtube1b {} {} {} | xform -t {} {} {} -rx 45 -t 0 0 {}'.format(
material, scenex, s, diam, -(scenex)/2.0+cc, -s/2.0, -radius, tto)
text = text+'\r\n! genbox {} octtube1c {} {} {} | xform -t {} {} {} -rx 90 -t 0 0 {}'.format(
material, scenex, s, diam, -(scenex)/2.0+cc, -s/2.0, -radius, tto)
text = text+'\r\n! genbox {} octtube1d {} {} {} | xform -t {} {} {} -rx 135 -t 0 0 {} '.format(
material, scenex, s, diam, -(scenex)/2.0+cc, -s/2.0, -radius, tto)
else:
raise Exception("Incorrect torque tube type. "+
"Available options: 'square' 'oct' 'hex' or 'round'."+
" Value entered: {}".format(self.tubetype))
self.text = text
return text
class CellModule(SuperClass):
def __init__(self, numcellsx, numcellsy,
xcell, ycell, xcellgap=0.02, ycellgap=0.02, centerJB=None):
"""
For creating a cell-level module, the following input parameters should
be in ``cellModule``:
================ ====================================================
Keys : type Description
================ ====================================================
numcellsx : int Number of cells in the X-direction within the module
numcellsy : int Number of cells in the Y-direction within the module
xcell : float Width of each cell (X-direction) in the module
ycell : float Length of each cell (Y-direction) in the module
xcellgap : float Spacing between cells in the X-direction. 0.02 default
ycellgap : float Spacing between cells in the Y-direction. 0.02 default
centerJB : float (optional) Distance betwen both sides of cell arrays
in a center-JB half-cell module. If 0 or not provided,
module will not have the center JB spacing.
Only implemented for 'portrait' mode at the moment.
(numcellsy > numcellsx).
cc : float center cell offset from x so scan is not at a gap
between cells
================ ====================================================
"""
self.keys = ['numcellsx', 'numcellsy', 'xcell', 'ycell', 'xcellgap',
'ycellgap','centerJB']
# set data object attributes from datakey list.
for key in self.keys:
setattr(self, key, eval(key))
def _makeCellLevelModule(self, module, z, Ny, ygap,
modulematerial):
""" Calculate the .radfile generation text for a cell-level module.
"""
offsetfromaxis = module.offsetfromaxis
c = self.getDataDict()
# For half cell modules with the JB on the center:
if c['centerJB'] is not None:
centerJB = c['centerJB']
y = c['numcellsy']*c['ycell'] + (c['numcellsy']-2)*c['ycellgap'] + centerJB
else:
centerJB = 0
y = c['numcellsy']*c['ycell'] + (c['numcellsy']-1)*c['ycellgap']
x = c['numcellsx']*c['xcell'] + (c['numcellsx']-1)*c['xcellgap']
#center cell -
if c['numcellsx'] % 2 == 0:
_cc = c['xcell']/2.0
print("Module was shifted by {} in X to avoid sensors on air".format(_cc))
else:
_cc = 0
text = '! genbox {} cellPVmodule {} {} {} | '.format(modulematerial,
c['xcell'], c['ycell'], z)
text +='xform -t {} {} {} '.format(-x/2.0 + _cc,
(-y*Ny / 2.0)-(ygap*(Ny-1) / 2.0)-centerJB/2.0,
offsetfromaxis)
text += '-a {} -t {} 0 0 '.format(c['numcellsx'], c['xcell'] + c['xcellgap'])
if centerJB != 0:
trans0 = c['ycell'] + c['ycellgap']
text += '-a {} -t 0 {} 0 '.format(c['numcellsy']/2, trans0)
#TODO: Continue playing with the y translation of the array in the next two lines
# Until it matches. Close but not there.
# This is 0 spacing
#ytrans1 = y/2.0-c['ycell']/2.0-c['ycellgap']+centerJB/2.0 # Creating the 2nd array with the right Jbox distance
ytrans1 = y/2.0-c['ycell']/2.0-c['ycellgap']+centerJB/2.0 + centerJB
ytrans2= c['ycell'] - centerJB/2.0 + c['ycellgap']/2.0
text += '-a {} -t 0 {} 0 '.format(2, ytrans1)
text += '| xform -t 0 {} 0 '.format(ytrans2)
else:
text += '-a {} -t 0 {} 0 '.format(c['numcellsy'], c['ycell'] + c['ycellgap'])
text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
# OPACITY CALCULATION
packagingfactor = np.round((c['xcell']*c['ycell']*c['numcellsx']*c['numcellsy'])/(x*y), 2)
print("This is a Cell-Level detailed module with Packaging "+
"Factor of {} %".format(packagingfactor))
module.x = x
module.y = y
self.text = text
return(text, x, y, _cc)
# deal with Int32 JSON incompatibility
# https://www.programmerall.com/article/57461489186/
import json
class MyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, np.integer):
return int(obj)
elif isinstance(obj, np.floating):
return float(obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
else:
return super(MyEncoder, self).default(obj)