bifacial-radiance 0.5.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. bifacial_radiance/HPCScripts/BasicSimulations/addNewModule.py +15 -0
  2. bifacial_radiance/HPCScripts/BasicSimulations/dask_on_node.sh +11 -0
  3. bifacial_radiance/HPCScripts/BasicSimulations/run_sbatch.sbatch +51 -0
  4. bifacial_radiance/HPCScripts/BasicSimulations/simulate_fixedtilt_gencumsky.py +110 -0
  5. bifacial_radiance/HPCScripts/BasicSimulations/simulate_fixedtilt_gendaylit.py +102 -0
  6. bifacial_radiance/HPCScripts/BasicSimulations/simulate_tracking_gendaylit.py +126 -0
  7. bifacial_radiance/HPCScripts/Other Examples (unorganized)/PuertoRico.py +168 -0
  8. bifacial_radiance/HPCScripts/Other Examples (unorganized)/PuertoRico_2.py +166 -0
  9. bifacial_radiance/HPCScripts/Other Examples (unorganized)/PuertoRico_Original.py +195 -0
  10. bifacial_radiance/HPCScripts/Other Examples (unorganized)/basic_module_sampling.py +154 -0
  11. bifacial_radiance/HPCScripts/Other Examples (unorganized)/compile_B.py +162 -0
  12. bifacial_radiance/HPCScripts/Other Examples (unorganized)/compile_Cases.py +122 -0
  13. bifacial_radiance/HPCScripts/Other Examples (unorganized)/compile_CasesMonth.py +142 -0
  14. bifacial_radiance/HPCScripts/Other Examples (unorganized)/compile_PRNew.py +91 -0
  15. bifacial_radiance/HPCScripts/Other Examples (unorganized)/compile_PRNewP2.py +95 -0
  16. bifacial_radiance/HPCScripts/Other Examples (unorganized)/compile_TreeResults.py +108 -0
  17. bifacial_radiance/HPCScripts/Other Examples (unorganized)/compile_basic_module_sampling.py +103 -0
  18. bifacial_radiance/HPCScripts/Other Examples (unorganized)/simulate_JackHourly.py +160 -0
  19. bifacial_radiance/HPCScripts/Other Examples (unorganized)/simulate_improvedArray_Oct2127.py +623 -0
  20. bifacial_radiance/TEMP/.gitignore +4 -0
  21. bifacial_radiance/__init__.py +24 -0
  22. bifacial_radiance/data/CEC Modules.csv +16860 -0
  23. bifacial_radiance/data/default.ini +65 -0
  24. bifacial_radiance/data/falsecolor.exe +0 -0
  25. bifacial_radiance/data/gencumsky/License.txt +54 -0
  26. bifacial_radiance/data/gencumsky/Makefile +17 -0
  27. bifacial_radiance/data/gencumsky/README.txt +9 -0
  28. bifacial_radiance/data/gencumsky/Solar Irradiation Modelling.doc +0 -0
  29. bifacial_radiance/data/gencumsky/Sun.cpp +118 -0
  30. bifacial_radiance/data/gencumsky/Sun.h +45 -0
  31. bifacial_radiance/data/gencumsky/average_val.awk +3 -0
  32. bifacial_radiance/data/gencumsky/cPerezSkyModel.cpp +238 -0
  33. bifacial_radiance/data/gencumsky/cPerezSkyModel.h +57 -0
  34. bifacial_radiance/data/gencumsky/cSkyVault.cpp +536 -0
  35. bifacial_radiance/data/gencumsky/cSkyVault.h +86 -0
  36. bifacial_radiance/data/gencumsky/climateFile.cpp +312 -0
  37. bifacial_radiance/data/gencumsky/climateFile.h +37 -0
  38. bifacial_radiance/data/gencumsky/cumulative.cal +177 -0
  39. bifacial_radiance/data/gencumsky/cumulative.rad +14 -0
  40. bifacial_radiance/data/gencumsky/cumulativesky_rotated.rad +2 -0
  41. bifacial_radiance/data/gencumsky/gencumulativesky +0 -0
  42. bifacial_radiance/data/gencumsky/gencumulativesky.cpp +269 -0
  43. bifacial_radiance/data/gencumsky/make_gencumskyexe.py +107 -0
  44. bifacial_radiance/data/gencumsky/paths.h +62 -0
  45. bifacial_radiance/data/gencumulativesky +0 -0
  46. bifacial_radiance/data/gencumulativesky.exe +0 -0
  47. bifacial_radiance/data/ground.rad +83 -0
  48. bifacial_radiance/data/module.json +103 -0
  49. bifacial_radiance/gui.py +1696 -0
  50. bifacial_radiance/images/fig1_fixed_small.gif +0 -0
  51. bifacial_radiance/images/fig2_tracked_small.gif +0 -0
  52. bifacial_radiance/load.py +1156 -0
  53. bifacial_radiance/main.py +5673 -0
  54. bifacial_radiance/mismatch.py +461 -0
  55. bifacial_radiance/modelchain.py +299 -0
  56. bifacial_radiance/module.py +1427 -0
  57. bifacial_radiance/performance.py +466 -0
  58. bifacial_radiance/spectral_utils.py +555 -0
  59. bifacial_radiance-0.5.1.dist-info/METADATA +129 -0
  60. bifacial_radiance-0.5.1.dist-info/RECORD +63 -0
  61. bifacial_radiance-0.5.1.dist-info/WHEEL +6 -0
  62. bifacial_radiance-0.5.1.dist-info/licenses/LICENSE +30 -0
  63. bifacial_radiance-0.5.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1427 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ @author: cdeline
4
+
5
+ ModuleObj class for defining module geometry
6
+
7
+ """
8
+ import os
9
+ import numpy as np
10
+ import pvlib
11
+ import pandas as pd
12
+
13
+ from bifacial_radiance.main import _missingKeyWarning, _popen, DATA_PATH
14
+
15
+ class SuperClass:
16
+ def __repr__(self):
17
+ return str(self.getDataDict())
18
+ def getDataDict(self):
19
+ """
20
+ return dictionary values from self. Originally stored as self.data
21
+
22
+ """
23
+ return dict(zip(self.keys,[getattr(self,k) for k in self.keys]))
24
+
25
+ class ModuleObj(SuperClass):
26
+ """
27
+ Module object to store module & torque tube details.
28
+ Does the heavy lifting of demo.makeModule()
29
+ Module details are passed in and stored in module.json.
30
+ Pass this object into makeScene or makeScene1axis.
31
+
32
+ """
33
+
34
+ def __repr__(self):
35
+ return str(type(self)) + ' : ' + str(self.getDataDict())
36
+ def __init__(self, name=None, x=None, y=None, z=None, bifi=1, modulefile=None,
37
+ text=None, customtext='', customObject='', xgap=0.01, ygap=0.0, zgap=0.1,
38
+ numpanels=1, rewriteModulefile=True, cellModule=None,
39
+ glass=False, glassEdge=0.01, modulematerial='black', tubeParams=None,
40
+ frameParams=None, omegaParams=None, CECMod=None, hpc=False):
41
+ """
42
+ Add module details to the .JSON module config file module.json.
43
+ Module definitions assume that the module .rad file is defined
44
+ with zero tilt, centered along the x-axis and y-axis for the center
45
+ of rotation of the module (+X/2, -X/2, +Y/2, -Y/2 on each side).
46
+ Tip: to define a module that is in 'portrait' mode, y > x.
47
+
48
+ Parameters
49
+ ------------
50
+ name : str
51
+ Input to name the module type
52
+ x : numeric
53
+ Width of module along the axis of the torque tube or rack. (meters)
54
+ y : numeric
55
+ Length of module (meters)
56
+ z : numeric
57
+ Thickness of the module (meters), or of the glass if glass = True,
58
+ in which case absorber thickness will be 0.001 and glass whatever
59
+ thickness is given, with absorber in the middle of the glass.
60
+ bifi : numeric
61
+ Bifaciality of the panel (used for calculatePerformance). Between 0 (monofacial)
62
+ and 1, default 1.
63
+ modulefile : str
64
+ Existing radfile location in \\objects. Otherwise a default value
65
+ is used
66
+ text : str
67
+ Text used in the radfile to generate the module. Manually passing
68
+ this value will overwrite module definition
69
+ customtext : str
70
+ Added-text used in the radfile to generate any
71
+ extra details in the racking/module. Does not overwrite
72
+ generated module (unlike "text"), but adds to it at the end.
73
+ customObject : str
74
+ Append to the module object file a pre-genereated radfile. This
75
+ must start with the file path name. Does not overwrite
76
+ generated module (unlike "text"), but adds to it at the end.
77
+ It automatically inserts radiance's text before the object name so
78
+ its inserted into scene properly ('!xform -rz 0')
79
+ rewriteModulefile : bool
80
+ Default True. Will rewrite module file each time makeModule is run.
81
+ numpanels : int
82
+ Number of modules arrayed in the Y-direction. e.g.
83
+ 1-up or 2-up, etc. (supports any number for carport/Mesa
84
+ simulations)
85
+ xgap : float
86
+ Panel space in X direction. Separation between modules in a row.
87
+ ygap : float
88
+ Gap between modules arrayed in the Y-direction if any.
89
+ zgap : float
90
+ Distance behind the modules in the z-direction to the edge of the
91
+ torquetube (m)
92
+ glass : bool
93
+ Add 5mm front and back glass to the module (glass/glass). Warning:
94
+ glass increases the analysis variability. Recommend setting
95
+ accuracy='high' in AnalysisObj.analysis()
96
+ glassEdge: float
97
+ Difference in space between module size and absorber part of the
98
+ module (or if cell-level module, full cell-level module size;
99
+ value will be applied as extra glass 1/2 to each side on x and y.
100
+ cellModule : dict
101
+ Dictionary with input parameters for creating a cell-level module.
102
+ Shortcut for ModuleObj.addCellModule()
103
+ tubeParams : dict
104
+ Dictionary with input parameters for creating a torque tube as
105
+ part of the module. Shortcut for ModuleObj.addTorquetube()
106
+ frameParams : dict
107
+ Dictionary with input parameters for creating a frame as part of
108
+ the module. Shortcut for ModuleObj.addFrame()
109
+ omegaParams : dict
110
+ Dictionary with input parameters for creating a omega or module
111
+ support structure. Shortcut for ModuleObj.addOmega()
112
+ CECMod : Dictionary with performance parameters needed for self.calculatePerformance()
113
+ lpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, Adjust
114
+ hpc : bool (default False)
115
+ Set up module in HPC mode. Namely turn off read/write to module.json
116
+ and just pass along the details in the module object. Note that
117
+ calling e.g. addTorquetube() after this will tend to write to the
118
+ module.json so pass all geometry parameters at once in to makeModule
119
+ for best response.
120
+
121
+ """
122
+
123
+ self.keys = ['x', 'y', 'z', 'modulematerial', 'scenex', 'sceney',
124
+ 'scenez', 'numpanels', 'bifi', 'text', 'modulefile', 'glass',
125
+ 'glassEdge', 'offsetfromaxis', 'xgap', 'ygap', 'zgap' ]
126
+
127
+ #replace whitespace with underlines. what about \n and other weird characters?
128
+ # TODO: Address above comment?
129
+ self.name = str(name).strip().replace(' ', '_')
130
+ self.customtext = customtext
131
+ self.customObject = customObject
132
+ self._manual_text = text
133
+
134
+ """
135
+ if Efficiency is not None:
136
+ self.Efficiency = Efficiency
137
+ if Temp_coeff is not None:
138
+ self.Temp_coeff = Temp_coeff
139
+ if Peak_Power is not None:
140
+ self.Peak_Power = Peak_Power
141
+ if Module_name is not None:
142
+ self.Module_name = Module_name
143
+ """
144
+
145
+ # are we writing to JSON with passed data or just reading existing?
146
+ if (x is None) & (y is None) & (cellModule is None) & (text is None):
147
+ #just read in file. If .rad file doesn't exist, make it.
148
+ self.readModule(name=name)
149
+ if name is not None:
150
+ self._saveModule(savedata=None, json=False,
151
+ rewriteModulefile=False)
152
+
153
+ else:
154
+ # set initial variables that aren't passed in
155
+ scenex = sceney = scenez = offsetfromaxis = 0
156
+ """
157
+ # TODO: this is kind of confusing and should probably be changed
158
+ # set torque tube internal dictionary
159
+ tubeBool = torquetube
160
+ torquetube = {'bool':tubeBool,
161
+ 'diameter':tubeParams['diameter'],
162
+ 'tubetype':tubeParams['tubetype'],
163
+ 'material':tubeParams['material']
164
+ }
165
+ try:
166
+ self.axisofrotationTorqueTube = tubeParams['axisofrotation']
167
+ except AttributeError:
168
+ self.axisofrotationTorqueTube = False
169
+ """
170
+
171
+ # set data object attributes from datakey list.
172
+ for key in self.keys:
173
+ setattr(self, key, eval(key))
174
+
175
+ if tubeParams:
176
+ if 'bool' in tubeParams: # backward compatible with pre-0.4
177
+ tubeParams['visible'] = tubeParams.pop('bool')
178
+ if 'torqueTubeMaterial' in tubeParams: # pre-0.4
179
+ tubeParams['material'] = tubeParams.pop('torqueTubeMaterial')
180
+ self.addTorquetube(**tubeParams, recompile=False)
181
+ if omegaParams:
182
+ self.addOmega(**omegaParams, recompile=False)
183
+
184
+ if frameParams:
185
+ self.addFrame(**frameParams, recompile=False)
186
+
187
+ if cellModule:
188
+ self.addCellModule(**cellModule, recompile=False)
189
+
190
+ self.addCEC(CECMod, glass, bifi=bifi)
191
+
192
+ if self._manual_text:
193
+ print('Warning: Module text manually passed and not '
194
+ f'generated: {self._manual_text}')
195
+
196
+
197
+
198
+
199
+ if self.modulefile is None:
200
+ self.modulefile = os.path.join('objects',
201
+ self.name + '.rad')
202
+ print("\nModule Name:", self.name)
203
+
204
+ if hpc:
205
+ self.compileText(rewriteModulefile, json=False)
206
+ else:
207
+ self.compileText(rewriteModulefile)
208
+
209
+ def compileText(self, rewriteModulefile=True, json=True):
210
+ """
211
+ Generate the text for the module .rad file based on ModuleObj attributes.
212
+ Optionally save details to the module.json and module.rad files.
213
+
214
+ Parameters
215
+ ------------
216
+ rewriteModulefile : bool (default True)
217
+ Overwrite the .rad file for the module
218
+ json : bool (default True)
219
+ Update the module.json file with ModuleObj attributes
220
+
221
+ """
222
+ saveDict = self.getDataDict()
223
+
224
+ if hasattr(self,'cellModule'):
225
+ saveDict = {**saveDict, 'cellModule':self.cellModule.getDataDict()}
226
+ if hasattr(self,'torquetube'):
227
+ saveDict = {**saveDict, 'torquetube':self.torquetube.getDataDict()}
228
+ if hasattr(self,'omega'):
229
+ saveDict = {**saveDict, 'omegaParams':self.omega.getDataDict()}
230
+ if hasattr(self,'frame'):
231
+ saveDict = {**saveDict, 'frameParams':self.frame.getDataDict()}
232
+ if getattr(self, 'CECMod', None) is not None:
233
+ saveDict = {**saveDict, 'CECMod':self.CECMod.getDataDict()}
234
+
235
+ self._makeModuleFromDict(**saveDict)
236
+
237
+ #write JSON data out and write radfile if it doesn't exist
238
+ self._saveModule({**saveDict, **self.getDataDict()}, json=json,
239
+ rewriteModulefile=rewriteModulefile)
240
+
241
+
242
+ def readModule(self, name=None):
243
+ """
244
+ Read in available modules in module.json. If a specific module name is
245
+ passed, return those details into the SceneObj. Otherwise
246
+ return available module list.
247
+
248
+ Parameters: name (str) Name of module to be read
249
+
250
+ Returns: moduleDict dictionary or list of modulenames if name is not passed in.
251
+
252
+ """
253
+ import json
254
+ filedir = os.path.join(DATA_PATH,'module.json')
255
+ with open( filedir ) as configfile:
256
+ data = json.load(configfile)
257
+
258
+ modulenames = data.keys()
259
+ if name is None:
260
+ return list(modulenames)
261
+
262
+ if name in modulenames:
263
+ moduleDict = data[name]
264
+ self.name = name
265
+ # BACKWARDS COMPATIBILITY - look for missing keys
266
+ if not 'scenex' in moduleDict:
267
+ moduleDict['scenex'] = moduleDict['x']
268
+ if not 'sceney' in moduleDict:
269
+ moduleDict['sceney'] = moduleDict['y']
270
+ if not 'offsetfromaxis' in moduleDict:
271
+ moduleDict['offsetfromaxis'] = 0
272
+ if not 'modulematerial' in moduleDict:
273
+ moduleDict['modulematerial'] = 'black'
274
+ if not 'glass' in moduleDict:
275
+ moduleDict['glass'] = False
276
+ if not 'glassEdge' in moduleDict:
277
+ moduleDict['glassEdge'] = 0.01
278
+ if not 'z' in moduleDict:
279
+ moduleDict['z'] = 0.02
280
+ # set ModuleObj attributes from moduleDict
281
+ #self.data = moduleDict
282
+ for keys in moduleDict:
283
+ setattr(self, keys, moduleDict[keys])
284
+
285
+ # Run torquetube, frame, omega, cellmodule
286
+ if moduleDict.get('torquetube'):
287
+ tubeParams = moduleDict['torquetube']
288
+ if 'bool' in tubeParams: # backward compatible with pre-0.4
289
+ tubeParams['visible'] = tubeParams.pop('bool')
290
+ if 'torqueTubeMaterial' in tubeParams: # pre-0.4
291
+ tubeParams['material'] = tubeParams.pop('torqueTubeMaterial')
292
+ self.addTorquetube(**tubeParams, recompile=False)
293
+ if moduleDict.get('cellModule'):
294
+ self.addCellModule(**moduleDict['cellModule'], recompile=False)
295
+ if moduleDict.get('omegaParams'):
296
+ self.addOmega(**moduleDict['omegaParams'], recompile=False)
297
+ if moduleDict.get('frameParams'):
298
+ self.addFrame(**moduleDict['frameParams'], recompile=False)
299
+ if moduleDict.get('CECMod'):
300
+ self.addCEC(moduleDict['CECMod'], moduleDict['glass'])
301
+
302
+
303
+ return moduleDict
304
+ else:
305
+ raise Exception('Error: module name "{}" doesnt exist'.format(name))
306
+ return {}
307
+
308
+
309
+ def _saveModule(self, savedata, json=True, rewriteModulefile=True):
310
+ """
311
+ write out changes to module.json and make radfile if it doesn't
312
+ exist. if rewriteModulefile is true, always overwrite Radfile.
313
+
314
+ Parameters
315
+ ----------
316
+ json : bool, default is True. Save JSON
317
+ rewriteModulefile : bool, default is True.
318
+
319
+ """
320
+ import json as jsonmodule
321
+
322
+ if json:
323
+ filedir = os.path.join(DATA_PATH, 'module.json')
324
+ with open(filedir) as configfile:
325
+ data = jsonmodule.load(configfile)
326
+
327
+ data.update({self.name:savedata})
328
+ with open(os.path.join(DATA_PATH, 'module.json') ,'w') as configfile:
329
+ jsonmodule.dump(data, configfile, indent=4, sort_keys=True,
330
+ cls=MyEncoder)
331
+
332
+ print('Module {} updated in module.json'.format(self.name))
333
+ # check that self.modulefile is not none
334
+ if self.modulefile is None:
335
+ self.modulefile = os.path.join('objects',
336
+ self.name + '.rad')
337
+
338
+ if rewriteModulefile & os.path.isfile(self.modulefile):
339
+ print(f"Pre-existing .rad file {self.modulefile} "
340
+ "will be overwritten\n")
341
+ os.remove(self.modulefile)
342
+
343
+ if not os.path.isfile(self.modulefile):
344
+ # py2 and 3 compatible: binary write, encode text first
345
+ try:
346
+ with open(self.modulefile, 'wb') as f:
347
+ f.write(self.text.encode('ascii'))
348
+ except FileNotFoundError:
349
+ raise Exception(f'ModuleObj Error: directory "/{os.path.dirname(self.modulefile)}" not found '\
350
+ f' in current path which is {os.getcwd()}. Cannot create '\
351
+ f'{os.path.basename(self.modulefile)}. '\
352
+ 'Are you in a valid bifacial_radiance directory?')
353
+
354
+ def showModule(self):
355
+ """
356
+ Method to call objview and render the module object
357
+ (visualize it).
358
+
359
+ Parameters: None
360
+
361
+ """
362
+
363
+ cmd = 'objview %s %s' % (os.path.join('materials', 'ground.rad'),
364
+ self.modulefile)
365
+ _,err = _popen(cmd,None)
366
+ if err is not None:
367
+ print('Error: {}'.format(err))
368
+ print('possible solution: install radwinexe binary package from '
369
+ 'http://www.jaloxa.eu/resources/radiance/radwinexe.shtml'
370
+ ' into your RADIANCE binaries path')
371
+ return
372
+
373
+ def saveImage(self, filename=None):
374
+ """
375
+ Duplicate objview process to save an image of the module in /images/
376
+
377
+ Parameters:
378
+ filename : string, optional. name for image file, defaults to module name
379
+
380
+ """
381
+ import tempfile
382
+
383
+ temp_dir = tempfile.TemporaryDirectory()
384
+ pid = os.getpid()
385
+ if filename is None:
386
+ filename = f'{self.name}'
387
+ # fake lighting temporary .radfile
388
+ ltfile = os.path.join(temp_dir.name, f'lt{pid}.rad')
389
+ with open(ltfile, 'w') as f:
390
+ f.write("void glow dim 0 0 4 .1 .1 .15 0\n" +\
391
+ "dim source background 0 0 4 0 0 1 360\n"+\
392
+ "void light bright 0 0 3 1000 1000 1000\n"+\
393
+ "bright source sun1 0 0 4 1 .2 1 5\n"+\
394
+ "bright source sun2 0 0 4 .3 1 1 5\n"+\
395
+ "bright source sun3 0 0 4 -1 -.7 1 5")
396
+
397
+ # make .rif and run RAD
398
+ riffile = os.path.join(temp_dir.name, f'ov{pid}.rif')
399
+ with open(riffile, 'w') as f:
400
+ f.write("scene= materials/ground.rad " +\
401
+ f"{self.modulefile} {ltfile}\n".replace("\\",'/') +\
402
+ "EXPOSURE= .5\nUP= Z\nview= XYZ\n" +\
403
+ #f"OCTREE= ov{pid}.oct\n"+\
404
+ f"oconv= -f\nPICT= images/{filename}")
405
+ _,err = _popen(["rad",'-s',riffile], None)
406
+ if err:
407
+ print(err)
408
+ else:
409
+ print(f'Module image saved: images/{filename}_XYZ.hdr')
410
+
411
+ temp_dir.cleanup()
412
+
413
+
414
+
415
+ def addTorquetube(self, diameter=0.1, tubetype='Round', material='Metal_Grey',
416
+ axisofrotation=True, visible=True, recompile=True):
417
+ """
418
+ For adding torque tubes to the module simulation.
419
+
420
+ Parameters
421
+ ----------
422
+ diameter : float Tube diameter in meters. For square, diameter means
423
+ the length of one of the square-tube side. For Hex,
424
+ diameter is the distance between two vertices
425
+ (diameter of the circumscribing circle). Default 0.1
426
+ tubetype : str Options: 'Square', 'Round' (default), 'Hex' or 'Oct'
427
+ Tube cross section
428
+ material : str Options: 'Metal_Grey' or 'black'. Material for the
429
+ torque tube.
430
+ axisofrotation (bool) : Default True. IF true, creates geometry
431
+ so center of rotation is at the center of the
432
+ torquetube, with an offsetfromaxis equal to half the
433
+ torquetube diameter + the zgap. If there is no
434
+ torquetube (visible=False), offsetformaxis will
435
+ equal the zgap.
436
+ visible (bool) : Default True. If false, geometry is set
437
+ as if the torque tube were present (e.g. zgap,
438
+ axisofrotation) but no geometry for the tube is made
439
+ recompile : Bool Rewrite .rad file and module.json file (default True)
440
+
441
+ """
442
+ self.torquetube = Tube(diameter=diameter, tubetype=tubetype,
443
+ material=material, axisofrotation=axisofrotation,
444
+ visible=visible)
445
+ if recompile:
446
+ self.compileText()
447
+
448
+
449
+ def addOmega(self, omega_material='Metal_Grey', omega_thickness=0.004,
450
+ inverted=False, x_omega1=None, x_omega3=None, y_omega=None,
451
+ mod_overlap=None, recompile=True):
452
+ """
453
+ Add the racking structure element `omega`, which connects
454
+ the frame to the torque tube.
455
+
456
+
457
+ Parameters
458
+ ----------
459
+
460
+ omega_material : str The material the omega structure is made of.
461
+ Default: 'Metal_Grey'
462
+ x_omega1 : float The length of the module-adjacent arm of the
463
+ omega parallel to the x-axis of the module
464
+ mod_overlap : float The length of the overlap between omega and
465
+ module surface on the x-direction
466
+ y_omega : float Length of omega (Y-direction)
467
+ omega_thickness : float Omega thickness. Default 0.004
468
+ x_omega3 : float X-direction length of the torquetube adjacent
469
+ arm of omega
470
+ inverted : Bool Modifies the way the Omega is set on the Torquetbue
471
+ Looks like False: u vs True: n (default False)
472
+ NOTE: The part that bridges the x-gap for a False
473
+ regular orientation omega (inverted = False),
474
+ is the x_omega3;
475
+ and for inverted omegas (inverted=True) it is
476
+ x_omega1.
477
+ recompile : Bool Rewrite .rad file and module.json file (default True)
478
+
479
+ """
480
+ self.omega = Omega(module=self, omega_material=omega_material,
481
+ omega_thickness=omega_thickness,
482
+ inverted=inverted, x_omega1=x_omega1,
483
+ x_omega3=x_omega3, y_omega=y_omega,
484
+ mod_overlap=mod_overlap)
485
+ if recompile:
486
+ self.compileText()
487
+
488
+ def addFrame(self, frame_material='Metal_Grey', frame_thickness=0.05,
489
+ frame_z=0.3, nSides_frame=4, frame_width=0.05, recompile=True):
490
+ """
491
+ Add a metal frame geometry around the module.
492
+
493
+ Parameters
494
+ ------------
495
+
496
+ frame_material : str The material the frame structure is made of
497
+ frame_thickness : float The profile thickness of the frame
498
+ frame_z : float The Z-direction length of the frame that extends
499
+ below the module plane
500
+ frame_width : float The length of the bottom frame that is bolted
501
+ with the omega
502
+ nSides_frame : int The number of sides of the module that are framed.
503
+ 4 (default) or 2
504
+
505
+ """
506
+
507
+ self.frame = Frame(frame_material=frame_material,
508
+ frame_thickness=frame_thickness,
509
+ frame_z=frame_z, nSides_frame=nSides_frame,
510
+ frame_width=frame_width)
511
+ if recompile:
512
+ self.compileText()
513
+
514
+ def addCellModule(self, numcellsx, numcellsy ,xcell, ycell,
515
+ xcellgap=0.02, ycellgap=0.02, centerJB=None, recompile=True):
516
+ """
517
+ Create a cell-level module, with individually defined cells and gaps
518
+
519
+ Parameters
520
+ ------------
521
+ numcellsx : int Number of cells in the X-direction within the module
522
+ numcellsy : int Number of cells in the Y-direction within the module
523
+ xcell : float Width of each cell (X-direction) in the module
524
+ ycell : float Length of each cell (Y-direction) in the module
525
+ xcellgap : float Spacing between cells in the X-direction. 0.02 default
526
+ ycellgap : float Spacing between cells in the Y-direction. 0.02 default
527
+ centerJB : float (optional) Distance betwen both sides of cell arrays
528
+ in a center-JB half-cell module. If 0 or not provided,
529
+ module will not have the center JB spacing.
530
+ Only implemented for 'portrait' mode at the moment.
531
+ (numcellsy > numcellsx).
532
+
533
+
534
+ """
535
+ import warnings
536
+ if centerJB:
537
+ warnings.warn(
538
+ 'centerJB functionality is currently experimental and subject '
539
+ 'to change in future releases. ' )
540
+
541
+
542
+ self.cellModule = CellModule(numcellsx=numcellsx, numcellsy=numcellsy,
543
+ xcell=xcell, ycell=ycell, xcellgap=xcellgap,
544
+ ycellgap=ycellgap, centerJB=centerJB)
545
+
546
+ if recompile:
547
+ self.compileText()
548
+
549
+
550
+
551
+ def _makeModuleFromDict(self, x=None, y=None, z=None, xgap=None, ygap=None,
552
+ zgap=None, numpanels=None, modulefile=None,
553
+ modulematerial=None, **kwargs):
554
+
555
+ """
556
+ go through and generate the text required to make a module
557
+ """
558
+ import warnings
559
+ #aliases for equations below
560
+ Ny = numpanels
561
+ _cc = 0 # cc is an offset given to the module when cells are used
562
+ # so that the sensors don't fall in air when numcells is even.
563
+ # For non cell-level modules default is 0.
564
+ # Update values for rotating system around torque tube.
565
+ diam=0
566
+ if hasattr(self, 'torquetube'):
567
+ diam = self.torquetube.diameter
568
+ if self.torquetube.axisofrotation is True:
569
+ self.offsetfromaxis = np.round(zgap + diam/2.0,8)
570
+ if hasattr(self, 'frame'):
571
+ self.offsetfromaxis = self.offsetfromaxis + self.frame.frame_z
572
+
573
+ # Adding the option to replace the module thickess
574
+ if self.glass:
575
+ print("\nWarning: module glass increases analysis variability. "
576
+ "Recommend setting `accuracy='high'` in AnalysisObj.analysis().\n")
577
+ if z is None:
578
+ zglass = 0.01
579
+ z = 0.001
580
+ else:
581
+ zglass = z
582
+ z = 0.001
583
+
584
+ else: # no glass
585
+ zglass = 0.0
586
+ if z is None:
587
+ z = 0.020
588
+
589
+ self.z = z
590
+ self.zglass = zglass
591
+
592
+
593
+ if modulematerial is None:
594
+ modulematerial = 'black'
595
+ self.modulematerial = 'black'
596
+
597
+ if self._manual_text is not None:
598
+ text = self._manual_text
599
+ self._manual_text = None
600
+ else:
601
+
602
+ if hasattr(self, 'cellModule'):
603
+ (text, x, y, _cc) = self.cellModule._makeCellLevelModule(self, z, Ny, ygap,
604
+ modulematerial)
605
+ else:
606
+ try:
607
+ text = '! genbox {} {} {} {} {} '.format(modulematerial,
608
+ self.name, x, y, z)
609
+ text +='| xform -t {} {} {} '.format(np.round(-x/2.0,6),
610
+ np.round((-y*Ny/2.0)-(ygap*(Ny-1)/2.0),6),
611
+ self.offsetfromaxis)
612
+ text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
613
+ packagingfactor = 100.0
614
+
615
+ except Exception as err: # probably because no x or y passed
616
+ raise Exception('makeModule variable {}'.format(err.args[0])+
617
+ ' and cellModule is None. '+
618
+ 'One or the other must be specified.')
619
+
620
+
621
+ self.scenex = np.round(x + xgap, 6)
622
+ self.sceney = np.round(y*numpanels + ygap*(numpanels-1), 8)
623
+ self.scenez = np.round(zgap + diam / 2.0, 6)
624
+
625
+
626
+ if hasattr(self, 'frame'):
627
+ _zinc, frametext = self.frame._makeFrames(
628
+ x=x,y=y, ygap=ygap,numpanels=Ny,
629
+ offsetfromaxis=self.offsetfromaxis- 0.5*zglass)
630
+ else:
631
+ frametext = ''
632
+ _zinc = 0 # z increment from frame thickness
633
+ _zinc = _zinc + 0.5 * zglass
634
+
635
+ if hasattr(self, 'omega'):
636
+ # This also defines scenex for length of the torquetube.
637
+ omega2omega_x, omegatext = self.omega._makeOmega(x=x,y=y, xgap=xgap,
638
+ zgap=zgap, z_inc=_zinc,
639
+ offsetfromaxis=self.offsetfromaxis)
640
+ if omega2omega_x > self.scenex:
641
+ self.scenex = omega2omega_x
642
+ else:
643
+ omegatext = ''
644
+
645
+
646
+ #if torquetube_bool is True:
647
+ if hasattr(self,'torquetube'):
648
+ if self.torquetube.visible:
649
+ text += self.torquetube._makeTorqueTube(cc=_cc, zgap=zgap,
650
+ z_inc=_zinc, scenex=self.scenex)
651
+
652
+ if self.glass:
653
+ if hasattr(self,'glassEdge'):
654
+ glassEdge = self.glassEdge
655
+ else:
656
+ glassEdge = 0.01
657
+ self.glassEdge = glassEdge
658
+
659
+ text = text+'\r\n! genbox stock_glass {} {} {} {} '.format(self.name+'_Glass',x+glassEdge, y+glassEdge, zglass)
660
+ text +='| xform -t {} {} {} '.format(round(-x/2.0-0.5*glassEdge + _cc, 6),
661
+ round((-y*Ny/2.0)-(ygap*(Ny-1)/2.0)-0.5*glassEdge, 6),
662
+ round(self.offsetfromaxis - 0.5*zglass, 6) )
663
+ text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
664
+
665
+
666
+ text += frametext
667
+ if hasattr(self, 'omega'):
668
+ text += self.omega.text
669
+
670
+ if self.customtext != "":
671
+ text += '\n' + self.customtext # For adding any other racking details at the module level that the user might want.
672
+
673
+ if self.customObject != "":
674
+ text += '\n!xform -rz 0 ' + self.customObject # For adding a specific .rad file
675
+
676
+ self.text = text
677
+ return text
678
+ #End of makeModuleFromDict()
679
+
680
+ def addCEC(self, CECMod, glassglass=None, bifi=None):
681
+ """
682
+
683
+
684
+ Parameters
685
+ ----------
686
+ CECMod : Dictionary or pandas.DataFrame including:
687
+ alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, Adjust
688
+ glassglass : Bool, optional. Create a glass-glass module w 5mm glass on each side
689
+ bifi : Float, bifaciality coefficient < 1
690
+
691
+ """
692
+ keys = ['alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s', 'Adjust']
693
+
694
+ if glassglass is not None:
695
+ self.glassglass = glassglass
696
+ if bifi:
697
+ self.bifi = bifi
698
+
699
+
700
+ if type(CECMod) == pd.DataFrame:
701
+ # Check for attributes along the index and transpose
702
+ if 'alpha_sc' in CECMod.index:
703
+ CECMod = CECMod.T
704
+ if len(CECMod) > 1:
705
+ print('Warning: DataFrame with multiple rows passed to module.addCEC. '\
706
+ 'Taking the first entry')
707
+ CECModDict = CECMod.iloc[0].to_dict()
708
+ try:
709
+ CECModDict['name'] = CECMod.iloc[0].name
710
+ except AttributeError:
711
+ CECModDict['name'] = None
712
+ elif type(CECMod) == pd.Series:
713
+ CECModDict = CECMod.to_dict()
714
+ elif type(CECMod) == dict:
715
+ CECModDict = CECMod
716
+ elif type(CECMod) == str:
717
+ raise Exception('Error: string-based module selection is not yet enabled. '\
718
+ 'Try back later!')
719
+ return
720
+ elif CECMod is None:
721
+ self.CECMod = None
722
+ return
723
+ else:
724
+ raise Exception(f"Unrecognized type '{type(CECMod)}' passed into addCEC ")
725
+
726
+ for key in keys:
727
+ if key not in CECModDict:
728
+ raise KeyError(f"Error: required key '{key}' not passed into module.addCEC")
729
+
730
+
731
+ self.CECMod = CECModule(**CECModDict)
732
+
733
+
734
+
735
+ def calculatePerformance(self, effective_irradiance, CECMod=None,
736
+ temp_air=None, wind_speed=1, temp_cell=None, glassglass=None):
737
+ '''
738
+ The module parameters are given at the reference condition.
739
+ Use pvlib.pvsystem.calcparams_cec() to generate the five SDM
740
+ parameters at your desired irradiance and temperature to use
741
+ with pvlib.pvsystem.singlediode() to calculate the IV curve information.:
742
+
743
+ Inputs
744
+ ------
745
+ effective_irradiance : numeric
746
+ Dataframe or single value. Must be same length as temp_cell
747
+ CECMod : Dict
748
+ Dictionary with CEC Module PArameters for the module selected. Must
749
+ contain at minimum alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref,
750
+ R_s, Adjust
751
+ temp_air : numeric
752
+ Ambient temperature in Celsius. Dataframe or single value to calculate.
753
+ Must be same length as effective_irradiance. Default = 20C
754
+ wind_speed : numeric
755
+ Wind speed at a height of 10 meters [m/s]. Default = 1 m/s
756
+ temp_cell : numeric
757
+ Back of module temperature. If provided, overrides temp_air and
758
+ wind_speed calculation. Default = None
759
+ glassglass : boolean
760
+ If module is glass-glass package (vs glass-polymer) to select correct
761
+ thermal coefficients for module temperature calculation
762
+
763
+ '''
764
+
765
+ if CECMod is None:
766
+ if getattr(self, 'CECMod', None) is not None:
767
+ CECMod = self.CECMod
768
+ else:
769
+ print("No CECModule data passed; using default for Prism Solar BHC72-400")
770
+ #url = 'https://raw.githubusercontent.com/NREL/SAM/patch/deploy/libraries/CEC%20Modules.csv'
771
+ url = os.path.join(DATA_PATH,'CEC Modules.csv')
772
+ db = pd.read_csv(url, index_col=0) # Reading this might take 1 min or so, the database is big.
773
+ modfilter2 = db.index.str.startswith('Pr') & db.index.str.endswith('BHC72-400')
774
+ CECMod = db[modfilter2]
775
+ self.addCEC(CECMod)
776
+
777
+ if hasattr(self, 'glassglass') and glassglass is None:
778
+ glassglass = self.glassglass
779
+
780
+ from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
781
+
782
+ # Setting temperature_model_parameters
783
+ if glassglass:
784
+ temp_model_params = (
785
+ TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'])
786
+ else:
787
+ temp_model_params = (
788
+ TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer'])
789
+
790
+ if temp_cell is None:
791
+ if temp_air is None:
792
+ temp_air = 25 # STC
793
+
794
+ temp_cell = pvlib.temperature.sapm_cell(effective_irradiance, temp_air,
795
+ wind_speed,
796
+ temp_model_params['a'],
797
+ temp_model_params['b'],
798
+ temp_model_params['deltaT'])
799
+
800
+ if isinstance(CECMod, pd.DataFrame):
801
+ #CECMod.to_pickle("CECMod.pkl")
802
+ if len(CECMod) == 1:
803
+ CECMod = CECMod.iloc[0]
804
+ else:
805
+ print("More than one Module passed. Error, using 1st one")
806
+ CECMod = CECMod.iloc[0]
807
+
808
+
809
+ IL, I0, Rs, Rsh, nNsVth = pvlib.pvsystem.calcparams_cec(
810
+ effective_irradiance=effective_irradiance,
811
+ temp_cell=temp_cell,
812
+ alpha_sc=float(CECMod.alpha_sc),
813
+ a_ref=float(CECMod.a_ref),
814
+ I_L_ref=float(CECMod.I_L_ref),
815
+ I_o_ref=float(CECMod.I_o_ref),
816
+ R_sh_ref=float(CECMod.R_sh_ref),
817
+ R_s=float(CECMod.R_s),
818
+ Adjust=float(CECMod.Adjust)
819
+ )
820
+
821
+ IVcurve_info = pvlib.pvsystem.singlediode(
822
+ photocurrent=IL,
823
+ saturation_current=I0,
824
+ resistance_series=Rs,
825
+ resistance_shunt=Rsh,
826
+ nNsVth=nNsVth
827
+ )
828
+
829
+ return IVcurve_info['p_mp']
830
+
831
+
832
+ # end of ModuleObj
833
+
834
+
835
+
836
+ class Omega(SuperClass):
837
+
838
+ def __init__(self, module, omega_material='Metal_Grey', omega_thickness=0.004,
839
+ inverted=False, x_omega1=None, x_omega3=None, y_omega=None,
840
+ mod_overlap=None):
841
+ """
842
+ ==================== ===============================================
843
+ Keys : type Description
844
+ ================ ===============================================
845
+ module : ModuleObj Parent object with details related to geometry
846
+ omega_material : str The material the omega structure is made of
847
+ omega_thickness : float Omega thickness
848
+ inverted : Bool Modifies the way the Omega is set on the Torquetbue
849
+ Looks like False: u vs True: n (default False)
850
+ x_omega1 : float The length of the module-adjacent arm of the
851
+ omega parallel to the x-axis of the module
852
+ y_omega : float Length of omega (Y-direction)
853
+ x_omega3 : float X-direction length of the torquetube adjacent
854
+ arm of omega
855
+ mod_overlap : float The length of the overlap between omega and
856
+ module surface on the x-direction
857
+
858
+ ===================== ===============================================
859
+
860
+ """
861
+ self.keys = ['omega_material', 'x_omega1', 'mod_overlap', 'y_omega',
862
+ 'omega_thickness','x_omega3','inverted']
863
+
864
+ if x_omega1 is None:
865
+ if inverted:
866
+ x_omega1 = module.xgap*0.5
867
+ else:
868
+ x_omega1 = module.xgap*0.5*0.6
869
+ _missingKeyWarning('Omega', 'x_omega1', x_omega1)
870
+
871
+ if x_omega3 is None:
872
+ if inverted:
873
+ x_omega3 = module.xgap*0.5*0.3
874
+ else:
875
+ x_omega3 = module.xgap*0.5
876
+ _missingKeyWarning('Omega', 'x_omega3', x_omega3)
877
+
878
+ if y_omega is None:
879
+ y_omega = module.y/2
880
+ _missingKeyWarning('Omega', 'y_omega', y_omega)
881
+
882
+ if mod_overlap is None:
883
+ mod_overlap = x_omega1*0.6
884
+ _missingKeyWarning('Omega', 'mod_overlap', mod_overlap)
885
+
886
+ # set data object attributes from datakey list.
887
+ for key in self.keys:
888
+ setattr(self, key, eval(key))
889
+
890
+
891
+
892
+ def _makeOmega(self, x, y, xgap, zgap, offsetfromaxis, z_inc = 0, **kwargs):
893
+ """
894
+ Helper function for creating a module that includes the racking
895
+ structure element `omega`.
896
+
897
+ TODO: remove some or all of this documentation since this is an internal function
898
+
899
+ Parameters
900
+ ------------
901
+ x : numeric
902
+ Width of module along the axis of the torque tube or racking structure. (meters).
903
+ y : numeric
904
+ Length of module (meters)
905
+ xgap : float
906
+ Panel space in X direction. Separation between modules in a row.
907
+ zgap : float
908
+ Distance behind the modules in the z-direction to the edge of the tube (m)
909
+ offsetfromaxis : float
910
+ Internally defined variable in makeModule that specifies how much
911
+ the module is offset from the Axis of Rotation due to zgap and or
912
+ frame thickness.
913
+ z_inc : dict
914
+ Internally defined variable in makeModule that specifies how much
915
+ the module is offseted by the Frame.
916
+
917
+ """
918
+
919
+ # set local variables
920
+ omega_material = self.omega_material
921
+ x_omega1 = self.x_omega1
922
+ mod_overlap = self.mod_overlap
923
+ y_omega = self.y_omega
924
+ omega_thickness = self.omega_thickness
925
+ x_omega3 = self.x_omega3
926
+
927
+
928
+ z_omega2 = zgap
929
+ x_omega2 = omega_thickness
930
+ z_omega1 = omega_thickness
931
+ z_omega3 = omega_thickness
932
+
933
+ #naming the omega pieces
934
+ name1 = 'mod_adj'
935
+ name2 = 'verti'
936
+ name3 = 'tt_adj'
937
+
938
+
939
+ # defining the module adjacent member of omega
940
+ x_translate1 = -x/2 - x_omega1 + mod_overlap
941
+ y_translate = -y_omega/2 #common for all the pieces
942
+ z_translate1 = round(offsetfromaxis-z_omega1, 6)
943
+
944
+ #defining the vertical (zgap) member of the omega
945
+ x_translate2 = x_translate1
946
+ z_translate2 = round(offsetfromaxis-z_omega2, 6)
947
+
948
+ #defining the torquetube adjacent member of omega
949
+ x_translate3 = round(x_translate1-x_omega3, 6)
950
+ z_translate3 = z_translate2
951
+
952
+ if z_inc != 0:
953
+ z_translate1 = round(z_translate1 - z_inc, 6)
954
+ z_translate2 = round(z_translate2 - z_inc, 6)
955
+ z_translate3 = round(z_translate3 - z_inc, 6)
956
+
957
+ # for this code, only the translations need to be shifted for the inverted omega
958
+
959
+ if self.inverted == True:
960
+ # shifting the non-inv omega shape of west as inv omega shape of east
961
+ x_translate1_inv_east = x/2-mod_overlap
962
+ x_shift_east = x_translate1_inv_east - x_translate1
963
+
964
+ # shifting the non-inv omega shape of west as inv omega shape of east
965
+ x_translate1_inv_west = -x_translate1_inv_east - x_omega1
966
+ x_shift_west = -x_translate1_inv_west + (-x_translate1-x_omega1)
967
+
968
+ #customizing the East side of the module for omega_inverted
969
+
970
+ omegatext = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1_inv_east, y_translate, z_translate1)
971
+ 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)
972
+ 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)
973
+
974
+ #customizing the West side of the module for omega_inverted
975
+
976
+ omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1_inv_west, y_translate, z_translate1)
977
+ 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)
978
+ 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)
979
+
980
+ omega2omega_x = round(-x_translate1_inv_east*2,6)
981
+
982
+ else:
983
+
984
+ #customizing the West side of the module for omega
985
+
986
+ omegatext = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1, y_translate, z_translate1)
987
+ omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name2, x_omega2, y_omega, z_omega2, x_translate2, y_translate, z_translate2)
988
+ omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name3, x_omega3, y_omega, z_omega3, x_translate3, y_translate, z_translate3)
989
+
990
+ #customizing the East side of the module for omega
991
+
992
+ omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, -x_translate1-x_omega1, y_translate, z_translate1)
993
+ omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name2, x_omega2, y_omega, z_omega2, -x_translate2-x_omega2, y_translate, z_translate2)
994
+ omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name3, x_omega3, y_omega, z_omega3, round(-x_translate3-x_omega3,6), y_translate, z_translate3)
995
+
996
+ omega2omega_x = round(-x_translate3*2,6)
997
+ self.text = omegatext
998
+ self.omega2omega_x = omega2omega_x
999
+ return omega2omega_x,omegatext
1000
+
1001
+ class Frame(SuperClass):
1002
+
1003
+ def __init__(self, frame_material='Metal_Grey', frame_thickness=0.05,
1004
+ frame_z=None, nSides_frame=4, frame_width=0.05):
1005
+ """
1006
+ Parameters
1007
+ ------------
1008
+
1009
+ frame_material : str The material the frame structure is made of
1010
+ frame_thickness : float The profile thickness of the frame
1011
+ frame_z : float The Z-direction length of the frame that extends
1012
+ below the module plane
1013
+ frame_width : float The length of the bottom frame that is bolted
1014
+ with the omega
1015
+ nSides_frame : int The number of sides of the module that are framed.
1016
+ 4 (default) or 2
1017
+
1018
+
1019
+ """
1020
+ self.keys = ['frame_material', 'frame_thickness', 'frame_z', 'frame_width',
1021
+ 'nSides_frame']
1022
+
1023
+ if frame_z is None:
1024
+ frame_z = 0.03
1025
+ _missingKeyWarning('Frame', 'frame_z', frame_z)
1026
+
1027
+ # set data object attributes from datakey list.
1028
+ for key in self.keys:
1029
+ setattr(self, key, eval(key))
1030
+
1031
+ def _makeFrames(self, x, y, ygap, numpanels, offsetfromaxis):
1032
+ """
1033
+ Helper function for creating a module that includes the frames attached to the module,
1034
+
1035
+
1036
+ Parameters
1037
+ ------------
1038
+ frameParams : dict
1039
+ Dictionary with input parameters for creating a frame as part of the module.
1040
+ See details below for keys needed.
1041
+ x : numeric
1042
+ Width of module along the axis of the torque tube or racking structure. (meters).
1043
+ y : numeric
1044
+ Length of module (meters)
1045
+ ygap : float
1046
+ Gap between modules arrayed in the Y-direction if any.
1047
+ numpanels : int
1048
+ Number of modules arrayed in the Y-direction. e.g.
1049
+ 1-up or 2-up, etc. (supports any number for carport/Mesa simulations)
1050
+ offsetfromaxis : float
1051
+ Internally defined variable in makeModule that specifies how much
1052
+ the module is offset from the Axis of Rotation due to zgap and or
1053
+ frame thickness.
1054
+
1055
+
1056
+ """
1057
+
1058
+ #
1059
+ if self.nSides_frame == 2 and x>y:
1060
+ print("Development Warning: Frames has only 2 sides and module is"+
1061
+ "in ladscape. This functionality is not working properly yet"+
1062
+ "for this release. We are overwriting nSide_frame = 4 to continue."+
1063
+ "If this functionality is pivotal to you we can prioritize adding it but"+
1064
+ "please comunicate with the development team. Thank you.")
1065
+ self.nSides_frame = 4
1066
+
1067
+ #Defining internal names
1068
+ frame_material = self.frame_material
1069
+ f_thickness = self.frame_thickness
1070
+ f_height = self.frame_z
1071
+ n_frame = self.nSides_frame
1072
+ fl_x = self.frame_width
1073
+
1074
+ y_trans_shift = 0 #pertinent to the case of x>y with 2-sided frame
1075
+
1076
+
1077
+ # Recalculating width ignoring the thickness of the aluminum
1078
+ # for internal positioining and sizing of hte pieces
1079
+ fl_x = fl_x-f_thickness
1080
+
1081
+ if x>y and n_frame==2:
1082
+ x_temp,y_temp = y,x
1083
+ rotframe = 90
1084
+ frame_y = x
1085
+ y_trans_shift = x/2-y/2
1086
+ else:
1087
+ x_temp,y_temp = x,y
1088
+ frame_y = y
1089
+ rotframe = 0
1090
+
1091
+ Ny = numpanels
1092
+ y_half = (y*Ny/2)+(ygap*(Ny-1)/2)
1093
+
1094
+ # taking care of lengths and translation points
1095
+ # The pieces are same and symmetrical for west and east
1096
+
1097
+ # naming the frame pieces
1098
+ nameframe1 = 'frameside'
1099
+ nameframe2 = 'frameleg'
1100
+
1101
+ #frame sides
1102
+ few_x = f_thickness
1103
+ few_y = frame_y
1104
+ few_z = f_height
1105
+
1106
+ fw_xt = -x_temp/2 # in case of x_temp = y this doesn't reach panel edge
1107
+ fe_xt = x_temp/2-f_thickness
1108
+ few_yt = -y_half-y_trans_shift
1109
+ few_zt = offsetfromaxis-f_height
1110
+
1111
+ #frame legs for east-west
1112
+
1113
+ flw_xt = -x_temp/2 + f_thickness
1114
+ fle_xt = round(x_temp/2 - f_thickness-fl_x,6)
1115
+ flew_yt = -y_half-y_trans_shift
1116
+ flew_zt = offsetfromaxis-f_height
1117
+
1118
+
1119
+ #pieces for the shorter side (north-south in this case)
1120
+
1121
+ #filler
1122
+
1123
+ fns_x = x_temp-2*f_thickness
1124
+ fns_y = f_thickness
1125
+ fns_z = f_height-f_thickness
1126
+
1127
+ fns_xt = -x_temp/2+f_thickness
1128
+ fn_yt = round(-y_half+y-f_thickness,6)
1129
+ fs_yt = -y_half
1130
+ fns_zt = offsetfromaxis-f_height+f_thickness
1131
+
1132
+ # the filler legs
1133
+
1134
+ filleg_x = round(x_temp-2*f_thickness-2*fl_x, 6)
1135
+ filleg_y = f_thickness + fl_x
1136
+ filleg_z = f_thickness
1137
+
1138
+ filleg_xt = round(-x_temp/2+f_thickness+fl_x, 6)
1139
+ fillegn_yt = round(-y_half+y-f_thickness-fl_x, 6)
1140
+ fillegs_yt = -y_half
1141
+ filleg_zt = offsetfromaxis-f_height
1142
+
1143
+
1144
+ # making frames: west side
1145
+
1146
+
1147
+ frame_text = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, few_x, few_y, few_z, fw_xt, few_yt, few_zt)
1148
+ frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
1149
+
1150
+ frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, fl_x, frame_y, f_thickness, flw_xt, flew_yt, flew_zt)
1151
+ frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
1152
+
1153
+ # making frames: east side
1154
+
1155
+ frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, few_x, few_y, few_z, fe_xt, few_yt, few_zt)
1156
+ frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
1157
+
1158
+ frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, fl_x, frame_y, f_thickness, fle_xt, flew_yt, flew_zt)
1159
+ frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
1160
+
1161
+
1162
+ if n_frame == 4:
1163
+ #making frames: north side
1164
+
1165
+ frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, fns_x, fns_y, fns_z, fns_xt, fn_yt, fns_zt)
1166
+ frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
1167
+
1168
+
1169
+ frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, filleg_x, filleg_y, filleg_z, filleg_xt, fillegn_yt, filleg_zt)
1170
+ frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
1171
+
1172
+ #making frames: south side
1173
+
1174
+ frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, fns_x, fns_y, fns_z, fns_xt, fs_yt, fns_zt)
1175
+ frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
1176
+
1177
+ frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, filleg_x, filleg_y, filleg_z, filleg_xt, fillegs_yt, filleg_zt)
1178
+ frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
1179
+
1180
+ z_inc = f_height
1181
+
1182
+ return z_inc, frame_text
1183
+
1184
+ class Tube(SuperClass):
1185
+
1186
+ def __init__(self, diameter=0.1, tubetype='Round', material='Metal_Grey',
1187
+ axisofrotation=True, visible=True):
1188
+ """
1189
+ ================ ====================================================
1190
+ Keys : type Description
1191
+ ================ ====================================================
1192
+ diameter : float Tube diameter in meters. For square, diameter means
1193
+ the length of one of the square-tube side. For Hex,
1194
+ diameter is the distance between two vertices
1195
+ (diameter of the circumscribing circle). Default 0.1
1196
+ tubetype : str Options: 'Square', 'Round' (default), 'Hex' or 'Oct'
1197
+ Tube cross section
1198
+ material : str Options: 'Metal_Grey' or 'black'. Material for the
1199
+ torque tube.
1200
+ axisofrotation (bool) : Default True. IF true, creates geometry
1201
+ so center of rotation is at the center of the
1202
+ torquetube, with an offsetfromaxis equal to half the
1203
+ torquetube diameter + the zgap. If there is no
1204
+ torquetube (visible=False), offsetformaxis will
1205
+ equal the zgap.
1206
+ visible (bool) : Default True. If false, geometry is set
1207
+ as if the torque tube were present (e.g. zgap,
1208
+ axisofrotation) but no geometry for the tube is made
1209
+ ================ ====================================================
1210
+ """
1211
+
1212
+ self.keys = ['diameter', 'tubetype', 'material', 'visible'] # what about axisofrotation?
1213
+
1214
+ self.axisofrotation = axisofrotation
1215
+ # set data object attributes from datakey list.
1216
+ for key in self.keys:
1217
+ setattr(self, key, eval(key))
1218
+
1219
+ def _makeTorqueTube(self, cc, z_inc, zgap, scenex):
1220
+ """
1221
+ Return text string for generating the torque tube geometry
1222
+
1223
+ Parameters
1224
+
1225
+ cc = module._cc #horizontal offset to center of a cell
1226
+ """
1227
+ import math
1228
+
1229
+
1230
+ text = ''
1231
+ tto = 0 # Torquetube Offset. Default = 0 if axisofrotationTT == True
1232
+ diam = self.diameter #alias
1233
+ material = self.material #alias
1234
+
1235
+ if self.tubetype.lower() == 'square':
1236
+ if self.axisofrotation == False:
1237
+ tto = -z_inc-zgap-diam/2.0
1238
+ text += '\r\n! genbox {} tube1 {} {} {} '.format(material,
1239
+ scenex, diam, diam)
1240
+ text += '| xform -t {} {} {}'.format(round(-(scenex)/2.0+cc, 6),
1241
+ round(-diam/2.0, 6), round( -diam/2.0+tto, 6))
1242
+
1243
+ elif self.tubetype.lower() == 'round':
1244
+ if self.axisofrotation == False:
1245
+ tto = -z_inc-zgap-diam/2.0
1246
+ text += '\r\n! genrev {} tube1 t*{} {} '.format(material, scenex, round(diam/2.0, 6))
1247
+ text += '32 | xform -ry 90 -t {} {} {}'.format(round(-(scenex)/2.0+cc, 6), 0, tto)
1248
+
1249
+ elif self.tubetype.lower() == 'hex':
1250
+ radius = 0.5*diam
1251
+
1252
+ if self.axisofrotation == False:
1253
+ tto = -z_inc-radius*math.sqrt(3.0)/2.0-zgap
1254
+
1255
+ text += '\r\n! genbox {} hextube1a {} {} {} | xform -t {} {} {}'.format(
1256
+ material, scenex, radius, round(radius*math.sqrt(3), 6),
1257
+ round(-(scenex)/2.0+cc, 6), round(-radius/2.0, 6), round(-radius*math.sqrt(3.0)/2.0+tto, 6)) #ztran -radius*math.sqrt(3.0)-tto
1258
+
1259
+
1260
+ # Create, translate to center, rotate, translate back to prev. position and translate to overal module position.
1261
+ text = text+'\r\n! genbox {} hextube1b {} {} {} | xform -t {} {} {} -rx 60 -t 0 0 {}'.format(
1262
+ material, scenex, radius, np.round(radius*math.sqrt(3), 6), np.round(-(scenex)/2.0+cc, 6),
1263
+ round(-radius/2.0, 6), np.round(radius*math.sqrt(3.0)/2.0, 6), tto) #ztran (radius*math.sqrt(3.0)/2.0)-radius*math.sqrt(3.0)-tto)
1264
+
1265
+ text = text+'\r\n! genbox {} hextube1c {} {} {} | xform -t {} {} {} -rx -60 -t 0 0 {}'.format(
1266
+ material, scenex, radius, round(radius*math.sqrt(3), 6),
1267
+ round(-(scenex)/2.0+cc, 6), round( -radius/2.0, 6), round(-radius*math.sqrt(3.0)/2, 6), tto) #ztran (radius*math.sqrt(3.0)/2.0)-radius*math.sqrt(3.0)-tto)
1268
+
1269
+ elif self.tubetype.lower()=='oct':
1270
+ radius = 0.5*diam
1271
+ s = diam / (1+math.sqrt(2.0)) #
1272
+
1273
+ if self.axisofrotation == False:
1274
+ tto = -z_inc-radius-zgap
1275
+
1276
+ text = text+'\r\n! genbox {} octtube1a {} {} {} | xform -t {} {} {}'.format(
1277
+ material, scenex, s, diam, round(-(scenex)/2.0, 6), round(-s/2.0, 6), -radius+tto)
1278
+
1279
+ # Create, translate to center, rotate, translate back to prev. position and translate to overal module position.
1280
+ text = text+'\r\n! genbox {} octtube1b {} {} {} | xform -t {} {} {} -rx 45 -t 0 0 {}'.format(
1281
+ material, scenex, s, diam, round(-(scenex)/2.0+cc, 6), round(-s/2.0, 6), -radius, tto)
1282
+
1283
+ text = text+'\r\n! genbox {} octtube1c {} {} {} | xform -t {} {} {} -rx 90 -t 0 0 {}'.format(
1284
+ material, scenex, s, diam, round(-(scenex)/2.0+cc, 6), -s/2.0, -radius, tto)
1285
+
1286
+ text = text+'\r\n! genbox {} octtube1d {} {} {} | xform -t {} {} {} -rx 135 -t 0 0 {} '.format(
1287
+ material, scenex, s, diam, -(scenex)/2.0+cc, -s/2.0, -radius, tto)
1288
+
1289
+
1290
+ else:
1291
+ raise Exception("Incorrect torque tube type. "+
1292
+ "Available options: 'square' 'oct' 'hex' or 'round'."+
1293
+ " Value entered: {}".format(self.tubetype))
1294
+ self.text = text
1295
+ return text
1296
+
1297
+ class CellModule(SuperClass):
1298
+
1299
+ def __init__(self, numcellsx, numcellsy,
1300
+ xcell, ycell, xcellgap=0.02, ycellgap=0.02, centerJB=None):
1301
+ """
1302
+ For creating a cell-level module, the following input parameters should
1303
+ be in ``cellModule``:
1304
+
1305
+ ================ ====================================================
1306
+ Keys : type Description
1307
+ ================ ====================================================
1308
+ numcellsx : int Number of cells in the X-direction within the module
1309
+ numcellsy : int Number of cells in the Y-direction within the module
1310
+ xcell : float Width of each cell (X-direction) in the module
1311
+ ycell : float Length of each cell (Y-direction) in the module
1312
+ xcellgap : float Spacing between cells in the X-direction. 0.02 default
1313
+ ycellgap : float Spacing between cells in the Y-direction. 0.02 default
1314
+ centerJB : float (optional) Distance betwen both sides of cell arrays
1315
+ in a center-JB half-cell module. If 0 or not provided,
1316
+ module will not have the center JB spacing.
1317
+ Only implemented for 'portrait' mode at the moment.
1318
+ (numcellsy > numcellsx).
1319
+ cc : float center cell offset from x so scan is not at a gap
1320
+ between cells
1321
+ ================ ====================================================
1322
+
1323
+ """
1324
+ self.keys = ['numcellsx', 'numcellsy', 'xcell', 'ycell', 'xcellgap',
1325
+ 'ycellgap','centerJB']
1326
+
1327
+ # set data object attributes from datakey list.
1328
+ for key in self.keys:
1329
+ setattr(self, key, eval(key))
1330
+
1331
+
1332
+ def _makeCellLevelModule(self, module, z, Ny, ygap,
1333
+ modulematerial):
1334
+ """ Calculate the .radfile generation text for a cell-level module.
1335
+ """
1336
+ offsetfromaxis = module.offsetfromaxis
1337
+ c = self.getDataDict()
1338
+
1339
+ # For half cell modules with the JB on the center:
1340
+ if c['centerJB'] is not None:
1341
+ centerJB = c['centerJB']
1342
+ y = np.round(c['numcellsy']*c['ycell'] + (c['numcellsy']-2)*c['ycellgap'] + centerJB, 6)
1343
+ else:
1344
+ centerJB = 0
1345
+ y = np.round(c['numcellsy']*c['ycell'] + (c['numcellsy']-1)*c['ycellgap'], 6)
1346
+
1347
+ x = np.round(c['numcellsx']*c['xcell'] + (c['numcellsx']-1)*c['xcellgap'], 6)
1348
+
1349
+ #center cell -
1350
+ if c['numcellsx'] % 2 == 0:
1351
+ _cc = np.round(c['xcell']/2.0, 6)
1352
+ print("Module was shifted by {} in X to avoid sensors on air".format(_cc))
1353
+ else:
1354
+ _cc = 0
1355
+
1356
+ text = '! genbox {} cellPVmodule {} {} {} | '.format(modulematerial,
1357
+ c['xcell'], c['ycell'], z)
1358
+ text +='xform -t {} {} {} '.format(round(-x/2.0 + _cc, 6),
1359
+ round((-y*Ny / 2.0)-(ygap*(Ny-1) / 2.0)-centerJB/2.0, 6),
1360
+ round(offsetfromaxis, 6))
1361
+
1362
+ text += '-a {} -t {} 0 0 '.format(c['numcellsx'], c['xcell'] + c['xcellgap'])
1363
+
1364
+ if centerJB != 0:
1365
+ trans0 = c['ycell'] + c['ycellgap']
1366
+ text += '-a {} -t 0 {} 0 '.format(round(c['numcellsy']/2, 6), trans0)
1367
+ #TODO: Continue playing with the y translation of the array in the next two lines
1368
+ # Until it matches. Close but not there.
1369
+ # This is 0 spacing
1370
+ #ytrans1 = y/2.0-c['ycell']/2.0-c['ycellgap']+centerJB/2.0 # Creating the 2nd array with the right Jbox distance
1371
+ ytrans1 = y/2.0-c['ycell']/2.0-c['ycellgap']+centerJB/2.0 + centerJB
1372
+ ytrans2= c['ycell'] - centerJB/2.0 + c['ycellgap']/2.0
1373
+ text += '-a {} -t 0 {} 0 '.format(2, round(ytrans1,6))
1374
+ text += '| xform -t 0 {} 0 '.format(round(ytrans2, 6))
1375
+
1376
+ else:
1377
+ text += '-a {} -t 0 {} 0 '.format(c['numcellsy'], c['ycell'] + c['ycellgap'])
1378
+
1379
+ text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
1380
+
1381
+ # OPACITY CALCULATION
1382
+ packagingfactor = np.round((c['xcell']*c['ycell']*c['numcellsx']*c['numcellsy'])/(x*y), 2)
1383
+ print("This is a Cell-Level detailed module with Packaging "+
1384
+ "Factor of {} ".format(packagingfactor))
1385
+
1386
+ module.x = x
1387
+ module.y = y
1388
+ self.text = text
1389
+
1390
+ return(text, x, y, _cc)
1391
+
1392
+ class CECModule(SuperClass):
1393
+
1394
+ def __init__(self, alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, Adjust, **kwargs):
1395
+ """
1396
+ For storing module performance parameters to be fed into pvlib.pvsystem.singlediode()
1397
+ Required passed parameters: alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, Adjust
1398
+ Usage: pass in **dictionary with at minimum these keys: 'alpha_sc', 'a_ref', 'I_L_ref',
1399
+ 'I_o_ref', 'R_sh_ref', 'R_s', 'Adjust', 'name' (opt)
1400
+
1401
+ """
1402
+ self.keys = ['alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref', 'R_sh_ref', 'R_s',
1403
+ 'Adjust', 'name']
1404
+ name = kwargs.get('name')
1405
+ if name is None:
1406
+ name = kwargs.get('Name')
1407
+
1408
+
1409
+ # set data object attributes from datakey list.
1410
+ for key in self.keys:
1411
+ setattr(self, key, eval(key))
1412
+
1413
+
1414
+
1415
+ # deal with Int32 JSON incompatibility
1416
+ # https://www.programmerall.com/article/57461489186/
1417
+ import json
1418
+ class MyEncoder(json.JSONEncoder):
1419
+ def default(self, obj):
1420
+ if isinstance(obj, np.integer):
1421
+ return int(obj)
1422
+ elif isinstance(obj, np.floating):
1423
+ return float(obj)
1424
+ elif isinstance(obj, np.ndarray):
1425
+ return obj.tolist()
1426
+ else:
1427
+ return super(MyEncoder, self).default(obj)