• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NREL / bifacial_radiance / 9194573527

22 May 2024 03:56PM UTC coverage: 70.326% (+0.06%) from 70.268%
9194573527

push

github

cdeline
Fix pytests

3728 of 5301 relevant lines covered (70.33%)

1.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.65
/bifacial_radiance/module.py
1
# -*- coding: utf-8 -*-
2
"""
2✔
3
@author: cdeline
4

5
ModuleObj class for defining module geometry
6

7
"""
8
import os
2✔
9
import numpy as np
2✔
10
import pvlib
2✔
11
import pandas as pd
2✔
12

13
from bifacial_radiance.main import _missingKeyWarning, _popen, DATA_PATH
2✔
14
 
15
class SuperClass:
2✔
16
    def __repr__(self):
2✔
17
        return str(self.getDataDict())
×
18
    def getDataDict(self):
2✔
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]))   
2✔
24

25
class ModuleObj(SuperClass):
2✔
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
    
35
    def __init__(self, name=None, x=None, y=None, z=None, bifi=1, modulefile=None, 
2✔
36
                 text=None, customtext='', customObject='', xgap=0.01, ygap=0.0, zgap=0.1,
37
                 numpanels=1, rewriteModulefile=True, cellModule=None,  
38
                 glass=False, modulematerial='black', tubeParams=None,
39
                 frameParams=None, omegaParams=None, CECMod=None, hpc=False): 
40
                 
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
        bifi : numeric
57
            Bifaciality of the panel (used for CalculatePerformance). Between 0 (monofacial) 
58
            and 1, default 1.
59
        modulefile : str
60
            Existing radfile location in \objects.  Otherwise a default value is used
61
        text : str
62
            Text used in the radfile to generate the module. Manually passing
63
            this value will overwrite module definition
64
        customtext : str
65
            Added-text used in the radfile to generate any
66
            extra details in the racking/module. Does not overwrite
67
            generated module (unlike "text"), but adds to it at the end.
68
        customObject : str
69
            Append to the module object file a pre-genereated radfile. This 
70
            must start with the file path\name. Does not overwrite
71
            generated module (unlike "text"), but adds to it at the end.
72
            It automatically inserts radiance's text before the object name so
73
            its inserted into scene properly ('!xform -rz 0')
74
        rewriteModulefile : bool
75
            Default True. Will rewrite module file each time makeModule is run.
76
        numpanels : int
77
            Number of modules arrayed in the Y-direction. e.g.
78
            1-up or 2-up, etc. (supports any number for carport/Mesa simulations)
79
        xgap : float
80
            Panel space in X direction. Separation between modules in a row.
81
        ygap : float
82
            Gap between modules arrayed in the Y-direction if any.
83
        zgap : float
84
            Distance behind the modules in the z-direction to the edge of the tube (m)
85
        glass : bool
86
            Add 5mm front and back glass to the module (glass/glass). Warning:
87
            glass increases the analysis variability.  Recommend setting
88
            accuracy='high' in AnalysisObj.analysis()
89
        cellModule : dict
90
            Dictionary with input parameters for creating a cell-level module.
91
            Shortcut for ModuleObj.addCellModule()
92
        tubeParams : dict
93
            Dictionary with input parameters for creating a torque tube as part of the 
94
            module. Shortcut for ModuleObj.addTorquetube()
95
        frameParams : dict
96
            Dictionary with input parameters for creating a frame as part of the module.
97
            Shortcut for ModuleObj.addFrame()
98
        omegaParams : dict
99
            Dictionary with input parameters for creating a omega or module support 
100
            structure. Shortcut for ModuleObj.addOmega()
101
        CECMod : Dictionary
102
            lpha_sc, a_ref, I_L_ref, I_o_ref,  R_sh_ref, R_s, Adjust
103
        hpc : bool (default False)
104
            Set up module in HPC mode.  Namely turn off read/write to module.json
105
            and just pass along the details in the module object. Note that 
106
            calling e.g. addTorquetube() after this will tend to write to the
107
            module.json so pass all geometry parameters at once in to makeModule
108
            for best response.
109
        #Efficiency=None, Temp_coeff=None, Peak_Power=None, Module_name=None):
110
        # TODO: REMOVE THESE INPUT PARAMETERS?
111
        Efficiency : float (default None)
112
            Information about module efficiency in percentage. Not currently 
113
            used to calculate performance.
114
        Temp_coeff : float (default None) 
115
            Information about module temperature coefficient in %. Not 
116
            currently used to calculate performance.    
117
        Peak_Power : float (default None) 
118
            Information about module Peak Power in Watts. Not currently used to
119
            calculate performance.            
120
        Module name : string (default None) 
121
            Information about module's name.  
122
            
123
        '"""
124

125
        self.keys = ['x', 'y', 'z', 'modulematerial', 'scenex','sceney',
2✔
126
            'scenez','numpanels','bifi','text','modulefile', 'glass',
127
            'offsetfromaxis','xgap','ygap','zgap', 'CECMod' ] 
128
        
129
        #replace whitespace with underlines. what about \n and other weird characters?
130
        # TODO: Address above comment?        
131
        self.name = str(name).strip().replace(' ', '_') 
2✔
132
        self.customtext = customtext
2✔
133
        self.customObject = customObject
2✔
134
        self._manual_text = text
2✔
135
        
136
        """
2✔
137
        if Efficiency is not None:
138
            self.Efficiency = Efficiency
139
        if Temp_coeff is not None:
140
            self.Temp_coeff = Temp_coeff
141
        if Peak_Power is not None:
142
            self.Peak_Power = Peak_Power
143
        if Module_name is not None:
144
            self.Module_name = Module_name
145
        """
146
        
147
        # are we writing to JSON with passed data or just reading existing?
148
        if (x is None) & (y is None) & (cellModule is None) & (text is None):
2✔
149
            #just read in file. If .rad file doesn't exist, make it.
150
            self.readModule(name=name)
2✔
151
            if name is not None:
2✔
152
                self._saveModule(savedata=None, json=False, 
2✔
153
                                 rewriteModulefile=False)
154

155
        else:
156
            # set initial variables that aren't passed in 
157
            scenex = sceney = scenez = offsetfromaxis = 0
2✔
158
            """
2✔
159
            # TODO: this is kind of confusing and should probably be changed
160
            # set torque tube internal dictionary
161
            tubeBool = torquetube
162
            torquetube = {'bool':tubeBool,
163
                          'diameter':tubeParams['diameter'],
164
                          'tubetype':tubeParams['tubetype'],
165
                          'material':tubeParams['material']
166
                          }   
167
            try:
168
                self.axisofrotationTorqueTube = tubeParams['axisofrotation']
169
            except AttributeError:
170
                self.axisofrotationTorqueTube = False
171
            """
172
            if tubeParams:
2✔
173
                if 'bool' in tubeParams:  # backward compatible with pre-0.4
2✔
174
                    tubeParams['visible'] = tubeParams.pop('bool')
×
175
                if 'torqueTubeMaterial' in tubeParams:  #  pre-0.4
2✔
176
                    tubeParams['material'] = tubeParams.pop('torqueTubeMaterial')
×
177
                self.addTorquetube(**tubeParams, recompile=False)
2✔
178
            if omegaParams:
2✔
179
                self.addOmega(**omegaParams, recompile=False)
2✔
180
                
181
            if frameParams:
2✔
182
                self.addFrame(**frameParams, recompile=False)
2✔
183
                
184
            if cellModule:
2✔
185
                self.addCellModule(**cellModule, recompile=False)
2✔
186
            
187
            if CECMod:
2✔
188
                self.addCEC(CECMod, glass)
×
189
            
190
            if self._manual_text:
2✔
191
                print('Warning: Module text manually passed and not '
2✔
192
                      f'generated: {self._manual_text}')
193
            
194
            
195
            # set data object attributes from datakey list. 
196
            for key in self.keys:
2✔
197
                setattr(self, key, eval(key))      
2✔
198
            
199
            if self.modulefile is None:
2✔
200
                self.modulefile = os.path.join('objects',
2✔
201
                                                       self.name + '.rad')
202
                print("\nModule Name:", self.name)
2✔
203
                  
204
            if hpc:
2✔
205
                self.compileText(rewriteModulefile, json=False)
2✔
206
            else:
207
                self.compileText(rewriteModulefile)
2✔
208
            
209
    def compileText(self, rewriteModulefile=True, json=True):
2✔
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()
2✔
223

224
        if hasattr(self,'cellModule'):
2✔
225
            saveDict = {**saveDict, 'cellModule':self.cellModule.getDataDict()}
2✔
226
        if hasattr(self,'torquetube'):
2✔
227
            saveDict = {**saveDict, 'torquetube':self.torquetube.getDataDict()}
2✔
228
        if hasattr(self,'omega'):
2✔
229
            saveDict = {**saveDict, 'omegaParams':self.omega.getDataDict()}
2✔
230
        if hasattr(self,'frame'):
2✔
231
            saveDict = {**saveDict, 'frameParams':self.frame.getDataDict()} 
2✔
232
            
233
        self._makeModuleFromDict(**saveDict)  
2✔
234

235
        #write JSON data out and write radfile if it doesn't exist
236
        self._saveModule({**saveDict, **self.getDataDict()}, json=json, 
2✔
237
                         rewriteModulefile=rewriteModulefile)
238

239
            
240
    def readModule(self, name=None):
2✔
241
        """
242
        Read in available modules in module.json.  If a specific module name is
243
        passed, return those details into the SceneObj. Otherwise 
244
        return available module list.
245

246
        Parameters:  name (str)  Name of module to be read
247

248
        Returns:  moduleDict dictionary or list of modulenames if name is not passed in.
249

250
        """
251
        import json
2✔
252
        filedir = os.path.join(DATA_PATH,'module.json')
2✔
253
        with open( filedir ) as configfile:
2✔
254
            data = json.load(configfile)
2✔
255

256
        modulenames = data.keys()
2✔
257
        if name is None:
2✔
258
            return list(modulenames)
2✔
259

260
        if name in modulenames:
2✔
261
            moduleDict = data[name]
2✔
262
            self.name = name
2✔
263
            # BACKWARDS COMPATIBILITY - look for missing keys
264
            if not 'scenex' in moduleDict:
2✔
265
                moduleDict['scenex'] = moduleDict['x']
×
266
            if not 'sceney' in moduleDict:
2✔
267
                moduleDict['sceney'] = moduleDict['y']
×
268
            if not 'offsetfromaxis' in moduleDict:
2✔
269
                moduleDict['offsetfromaxis'] = 0
×
270
            if not 'modulematerial' in moduleDict:
2✔
271
                moduleDict['modulematerial'] = 'black'
×
272
            if not 'glass' in moduleDict:
2✔
273
                moduleDict['glass'] = False    
×
274
            if not 'z' in moduleDict:
2✔
275
                moduleDict['z'] = 0.02
×
276
            # set ModuleObj attributes from moduleDict
277
            #self.data = moduleDict
278
            for keys in moduleDict:
2✔
279
                setattr(self, keys, moduleDict[keys])
2✔
280
            
281
            # Run torquetube, frame, omega, cellmodule
282
            if moduleDict.get('torquetube'):
2✔
283
                tubeParams = moduleDict['torquetube']
2✔
284
                if 'bool' in tubeParams:  # backward compatible with pre-0.4
2✔
285
                    tubeParams['visible'] = tubeParams.pop('bool')
×
286
                if 'torqueTubeMaterial' in tubeParams:  #  pre-0.4
2✔
287
                    tubeParams['material'] = tubeParams.pop('torqueTubeMaterial')
×
288
                self.addTorquetube(**tubeParams, recompile=False)
2✔
289
            if moduleDict.get('cellModule'):
2✔
290
                self.addCellModule(**moduleDict['cellModule'], recompile=False)
×
291
            if moduleDict.get('omegaParams'):
2✔
292
                self.addOmega(**moduleDict['omegaParams'], recompile=False) 
2✔
293
            if moduleDict.get('frameParams'):
2✔
294
                self.addFrame(**moduleDict['frameParams'], recompile=False)
2✔
295
            if moduleDict.get('CECMod'):
2✔
296
                self.addCEC(moduleDict['CECMod'], moduleDict['glass'])
×
297
            
298
            
299
            return moduleDict
2✔
300
        else:
301
            print('Error: module name {} doesnt exist'.format(name))
×
302
            return {}
×
303

304

305
    def _saveModule(self, savedata, json=True, rewriteModulefile=True):
2✔
306
        """
307
        write out changes to module.json and make radfile if it doesn't
308
        exist.  if rewriteModulefile is true, always overwrite Radfile.
309

310
        Parameters
311
        ----------
312
        json : bool, default is True.  Save JSON
313
        rewriteModulefile : bool, default is True.
314

315
        """
316
        import json as jsonmodule
2✔
317
        
318
        if json:
2✔
319
            filedir = os.path.join(DATA_PATH, 'module.json') 
2✔
320
            with open(filedir) as configfile:
2✔
321
                data = jsonmodule.load(configfile)
2✔
322
    
323
            data.update({self.name:savedata})
2✔
324
            with open(os.path.join(DATA_PATH, 'module.json') ,'w') as configfile:
2✔
325
                jsonmodule.dump(data, configfile, indent=4, sort_keys=True, 
2✔
326
                                cls=MyEncoder)
327
    
328
            print('Module {} updated in module.json'.format(self.name))
2✔
329
        # check that self.modulefile is not none
330
        if self.modulefile is None:
2✔
331
            self.modulefile = os.path.join('objects',
×
332
                                                   self.name + '.rad')
333
        
334
        if rewriteModulefile & os.path.isfile(self.modulefile):
2✔
335
            print(f"Pre-existing .rad file {self.modulefile} "
2✔
336
                  "will be overwritten\n")
337
            os.remove(self.modulefile)
2✔
338
            
339
        if not os.path.isfile(self.modulefile):
2✔
340
            # py2 and 3 compatible: binary write, encode text first
341
            with open(self.modulefile, 'wb') as f:
2✔
342
                f.write(self.text.encode('ascii'))
2✔
343
            
344
    def showModule(self):
2✔
345
        """ 
346
        Method to call objview and render the module object 
347
        (visualize it).
348
        
349
        Parameters: None
350

351
        """
352
       
353
        cmd = 'objview %s %s' % (os.path.join('materials', 'ground.rad'),
×
354
                                         self.modulefile)
355
        _,err = _popen(cmd,None)
×
356
        if err is not None:
×
357
            print('Error: {}'.format(err))
×
358
            print('possible solution: install radwinexe binary package from '
×
359
                  'http://www.jaloxa.eu/resources/radiance/radwinexe.shtml'
360
                  ' into your RADIANCE binaries path')
361
            return 
×
362

363
    def saveImage(self, filename=None):
2✔
364
        """
365
        Duplicate objview process to save an image of the module in /images/
366

367
        Parameters:    
368
            filename : string, optional. name for image file, defaults to module name               
369

370
        """
371
        import tempfile
2✔
372
        
373
        temp_dir = tempfile.TemporaryDirectory()
2✔
374
        pid = os.getpid()
2✔
375
        if filename is None:
2✔
376
            filename = f'{self.name}'
2✔
377
        # fake lighting temporary .radfile
378
        ltfile = os.path.join(temp_dir.name, f'lt{pid}.rad')
2✔
379
        with open(ltfile, 'w') as f:
2✔
380
                f.write("void glow dim  0  0  4  .1 .1 .15  0\n" +\
2✔
381
                    "dim source background  0  0  4  0 0 1  360\n"+\
382
                    "void light bright  0  0  3  1000 1000 1000\n"+\
383
                    "bright source sun1  0  0  4  1 .2 1  5\n"+\
384
                    "bright source sun2  0  0  4  .3 1 1  5\n"+\
385
                    "bright source sun3  0  0  4  -1 -.7 1  5")
386

387
        # make .rif and run RAD
388
        riffile = os.path.join(temp_dir.name, f'ov{pid}.rif')
2✔
389
        with open(riffile, 'w') as f:
2✔
390
                f.write("scene= materials/ground.rad " +\
2✔
391
                        f"{self.modulefile} {ltfile}\n".replace("\\",'/') +\
392
                    "EXPOSURE= .5\nUP= Z\nview= XYZ\n" +\
393
                    #f"OCTREE= ov{pid}.oct\n"+\
394
                    f"oconv= -f\nPICT= images/{filename}")
395
        _,err = _popen(["rad",'-s',riffile], None)
2✔
396
        if err:
2✔
397
            print(err)
×
398
        else:
399
            print(f'Module image saved: images/{filename}_XYZ.hdr')
2✔
400
        
401
        temp_dir.cleanup()
2✔
402
        
403
    
404
    
405
    def addTorquetube(self, diameter=0.1, tubetype='Round', material='Metal_Grey', 
2✔
406
                      axisofrotation=True, visible=True,  recompile=True):
407
        """
408
        For adding torque tubes to the module simulation. 
409
        
410
        Parameters
411
        ----------
412
        diameter : float   Tube diameter in meters. For square, diameter means 
413
                           the length of one of the square-tube side.  For Hex, 
414
                           diameter is the distance between two vertices 
415
                           (diameter of the circumscribing circle). Default 0.1
416
        tubetype : str     Options: 'Square', 'Round' (default), 'Hex' or 'Oct'
417
                           Tube cross section
418
        material : str     Options: 'Metal_Grey' or 'black'. Material for the 
419
                           torque tube.
420
        axisofrotation     (bool) :  Default True. IF true, creates geometry
421
                           so center of rotation is at the center of the 
422
                           torquetube, with an offsetfromaxis equal to half the
423
                           torquetube diameter + the zgap. If there is no 
424
                           torquetube (visible=False), offsetformaxis will 
425
                           equal the zgap.
426
        visible            (bool) :  Default True. If false, geometry is set
427
                           as if the torque tube were present (e.g. zgap, 
428
                           axisofrotation) but no geometry for the tube is made
429
        recompile : Bool          Rewrite .rad file and module.json file (default True)
430

431
        """
432
        self.torquetube = Tube(diameter=diameter, tubetype=tubetype,
2✔
433
                           material=material, axisofrotation=axisofrotation,
434
                           visible=visible)
435
        if recompile:
2✔
436
            self.compileText()
2✔
437

438

439
    def addOmega(self, omega_material='Metal_Grey', omega_thickness=0.004,
2✔
440
                 inverted=False, x_omega1=None, x_omega3=None, y_omega=None,
441
                 mod_overlap=None, recompile=True):
442
        """
443
        Add the racking structure element `omega`, which connects 
444
        the frame to the torque tube. 
445

446

447
        Parameters
448
        ----------
449

450
        omega_material : str      The material the omega structure is made of.
451
                                  Default: 'Metal_Grey'
452
        x_omega1  : float         The length of the module-adjacent arm of the 
453
                                  omega parallel to the x-axis of the module
454
        mod_overlap : float       The length of the overlap between omega and 
455
                                  module surface on the x-direction
456
        y_omega  : float          Length of omega (Y-direction)
457
        omega_thickness : float   Omega thickness. Default 0.004
458
        x_omega3  : float         X-direction length of the torquetube adjacent 
459
                                  arm of omega
460
        inverted : Bool           Modifies the way the Omega is set on the Torquetbue
461
                                  Looks like False: u  vs True: n  (default False)
462
                                  NOTE: The part that bridges the x-gap for a False
463
                                  regular orientation omega (inverted = False),
464
                                  is the x_omega3;
465
                                  and for inverted omegas (inverted=True) it is
466
                                  x_omega1.
467
        recompile : Bool          Rewrite .rad file and module.json file (default True)
468

469
        """
470
        self.omega = Omega(self, omega_material=omega_material,
2✔
471
                           omega_thickness=omega_thickness,
472
                           inverted=inverted, x_omega1=x_omega1,
473
                           x_omega3=x_omega3, y_omega=y_omega, 
474
                           mod_overlap=mod_overlap)
475
        if recompile:
2✔
476
            self.compileText()
2✔
477

478
    def addFrame(self, frame_material='Metal_Grey', frame_thickness=0.05, 
2✔
479
                 frame_z=0.3, nSides_frame=4, frame_width=0.05, recompile=True):
480
        """
481
        Add a metal frame geometry around the module.
482
        
483
        Parameters
484
        ------------
485

486
        frame_material : str    The material the frame structure is made of
487
        frame_thickness : float The profile thickness of the frame 
488
        frame_z : float         The Z-direction length of the frame that extends 
489
                                below the module plane
490
        frame_width : float     The length of the bottom frame that is bolted 
491
                                with the omega
492
        nSides_frame : int      The number of sides of the module that are framed.
493
                                4 (default) or 2
494

495
        """
496
        
497
        self.frame = Frame(frame_material=frame_material,
2✔
498
                   frame_thickness=frame_thickness,
499
                   frame_z=frame_z, nSides_frame=nSides_frame,
500
                   frame_width=frame_width)
501
        if recompile:
2✔
502
            self.compileText()
2✔
503
            
504
    def addCellModule(self, numcellsx, numcellsy ,xcell, ycell,
2✔
505
                      xcellgap=0.02, ycellgap=0.02, centerJB=None, recompile=True):
506
        """
507
        Create a cell-level module, with individually defined cells and gaps
508
        
509
        Parameters
510
        ------------
511
        numcellsx : int    Number of cells in the X-direction within the module
512
        numcellsy : int    Number of cells in the Y-direction within the module
513
        xcell : float      Width of each cell (X-direction) in the module
514
        ycell : float      Length of each cell (Y-direction) in the module
515
        xcellgap : float   Spacing between cells in the X-direction. 0.02 default
516
        ycellgap : float   Spacing between cells in the Y-direction. 0.02 default
517
        centerJB : float   (optional) Distance betwen both sides of cell arrays 
518
                           in a center-JB half-cell module. If 0 or not provided,
519
                           module will not have the center JB spacing. 
520
                           Only implemented for 'portrait' mode at the moment.
521
                           (numcellsy > numcellsx). 
522
        
523

524
        """
525
        import warnings
2✔
526
        if centerJB:
2✔
527
            warnings.warn(
2✔
528
                'centerJB functionality is currently experimental and subject '
529
                'to change in future releases. ' )
530
            
531
        
532
        self.cellModule = CellModule(numcellsx=numcellsx, numcellsy=numcellsy,
2✔
533
                                     xcell=xcell, ycell=ycell, xcellgap=xcellgap,
534
                                     ycellgap=ycellgap, centerJB=centerJB)
535
                                     
536
        if recompile:
2✔
537
            self.compileText()
2✔
538
    
539

540
    
541
    def _makeModuleFromDict(self,  x=None, y=None, z=None, xgap=None, ygap=None, 
2✔
542
                    zgap=None, numpanels=None, modulefile=None,
543
                    modulematerial=None, **kwargs):
544

545
        """
546
        go through and generate the text required to make a module
547
        """
548
        import warnings
2✔
549
        #aliases for equations below
550
        Ny = numpanels
2✔
551
        _cc = 0  # cc is an offset given to the module when cells are used
2✔
552
                  # so that the sensors don't fall in air when numcells is even.
553
                  # For non cell-level modules default is 0.
554
        # Update values for rotating system around torque tube.  
555
        diam=0
2✔
556
        if hasattr(self, 'torquetube'):
2✔
557
            diam = self.torquetube.diameter
2✔
558
            if self.torquetube.axisofrotation is True:
2✔
559
                self.offsetfromaxis = np.round(zgap + diam/2.0,8)
2✔
560
            if hasattr(self, 'frame'):
2✔
561
                self.offsetfromaxis = self.offsetfromaxis + self.frame.frame_z
2✔
562
        # TODO: make sure the above is consistent with old version below
563
        """
2✔
564
        if torquetube:
565
            diam = torquetube['diameter']
566
            torquetube_bool = torquetube['bool']
567
        else:
568
            diam=0
569
            torquetube_bool = False
570
        if self.axisofrotationTorqueTube == True:
571
            if torquetube_bool == True:
572
                self.offsetfromaxis = np.round(zgap + diam/2.0,8)
573
            else:
574
                self.offsetfromaxis = zgap
575
            if hasattr(self, 'frame'):
576
                self.offsetfromaxis = self.offsetfromaxis + self.frame.frame_z
577
        """
578
        # Adding the option to replace the module thickess
579
        if self.glass:
2✔
580
            zglass = 0.01
2✔
581
            print("\nWarning: module glass increases analysis variability. "  
2✔
582
                          "Recommend setting `accuracy='high'` in AnalysisObj.analysis().\n")
583
        else:
584
            zglass = 0.0
2✔
585
            
586
        if z is None:
2✔
587
            if self.glass:
2✔
588
                z = 0.001
×
589
            else:
590
                z = 0.020
2✔
591
                
592
        self.z = z
2✔
593
        self.zglass = zglass
2✔
594

595
            
596
        if modulematerial is None:
2✔
597
            modulematerial = 'black'
2✔
598
            self.modulematerial = 'black'
2✔
599
            
600
        if self._manual_text is not None:
2✔
601
            text = self._manual_text
2✔
602
            self._manual_text = None
2✔
603

604
        else:
605
            
606
            if hasattr(self, 'cellModule'):
2✔
607
                (text, x, y, _cc) = self.cellModule._makeCellLevelModule(self, z, Ny, ygap, 
2✔
608
                                       modulematerial) 
609
            else:
610
                try:
2✔
611
                    text = '! genbox {} {} {} {} {} '.format(modulematerial, 
2✔
612
                                                              self.name, x, y, z)
613
                    text +='| xform -t {} {} {} '.format(-x/2.0,
2✔
614
                                            (-y*Ny/2.0)-(ygap*(Ny-1)/2.0),
615
                                            self.offsetfromaxis)
616
                    text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
617
                    packagingfactor = 100.0
2✔
618
    
619
                except Exception as err: # probably because no x or y passed
×
620
                    raise Exception('makeModule variable {}'.format(err.args[0])+
×
621
                                    ' and cellModule is None.  '+
622
                                    'One or the other must be specified.')
623
 
624
            
625
        self.scenex = x + xgap
2✔
626
        self.sceney = np.round(y*numpanels + ygap*(numpanels-1), 8)
2✔
627
        self.scenez = np.round(zgap + diam / 2.0, 8)
2✔
628
        
629

630
        if hasattr(self, 'frame'):
2✔
631
            _zinc, frametext = self.frame._makeFrames( 
2✔
632
                                    x=x,y=y, ygap=ygap,numpanels=Ny, 
633
                                    offsetfromaxis=self.offsetfromaxis- 0.5*zglass)
634
        else:
635
            frametext = ''
2✔
636
            _zinc = 0  # z increment from frame thickness 
2✔
637
        _zinc = _zinc + 0.5 * zglass
2✔
638
            
639
        if hasattr(self, 'omega'):
2✔
640
            # This also defines scenex for length of the torquetube.
641
            omega2omega_x, omegatext = self.omega._makeOmega(x=x,y=y, xgap=xgap,  
2✔
642
                                            zgap=zgap, z_inc=_zinc, 
643
                                            offsetfromaxis=self.offsetfromaxis)
644
            if omega2omega_x > self.scenex:
2✔
645
                self.scenex =  omega2omega_x
2✔
646
            
647
            # TODO: is the above line better than below?
648
            #       I think this causes it's own set of problems, need to check.
649
            """
2✔
650
            if self.scenex <x:
651
                scenex = x+xgap #overwriting scenex to maintain torquetube continuity
652
        
653
                print ('Warning: Omega values have been provided, but' +
654
                       'the distance between modules with the omega'+
655
                       'does not match the x-gap provided.'+
656
                       'Setting x-gap to be the space between modules'+
657
                       'from the omega.')
658
            else:
659
                print ('Warning: Using omega-to-omega distance to define'+
660
                       'gap between modules'
661
                       +'xgap value not being used')
662
            """
663
        else:
664
            omegatext = ''
2✔
665
        
666
        # Defining scenex if it was not defined by the Omegas, 
667
        # after the module has been created in case it is a 
668
        # cell-level Module, in which the "x" gets calculated internally.
669
        # Also sanity check in case omega-to-omega distance is smaller
670
        # than module.
671

672
        #if torquetube_bool is True:
673
        if hasattr(self,'torquetube'):
2✔
674
            if self.torquetube.visible:
2✔
675
                text += self.torquetube._makeTorqueTube(cc=_cc, zgap=zgap,   
2✔
676
                                         z_inc=_zinc, scenex=self.scenex)
677

678
        # TODO:  should there be anything updated here like scenez?
679
        #        YES.
680
        if self.glass: 
2✔
681
                edge = 0.01                     
2✔
682
                text = text+'\r\n! genbox stock_glass {} {} {} {} '.format(self.name+'_Glass',x+edge, y+edge, zglass)
2✔
683
                text +='| xform -t {} {} {} '.format(-x/2.0-0.5*edge + _cc,
2✔
684
                                        (-y*Ny/2.0)-(ygap*(Ny-1)/2.0)-0.5*edge,
685
                                        self.offsetfromaxis - 0.5*zglass)
686
                text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
687
            
688

689
        text += frametext
2✔
690
        if hasattr(self, 'omega'):
2✔
691
            text += self.omega.text    
2✔
692
        
693
        if self.customtext != "":
2✔
694
            text += '\n' + self.customtext  # For adding any other racking details at the module level that the user might want.
2✔
695

696
        if self.customObject != "":
2✔
697
            text += '\n!xform -rz 0 ' + self.customObject  # For adding a specific .rad file
×
698

699
        self.text = text
2✔
700
        return text
2✔
701
    #End of makeModuleFromDict()
702

703
    def addCEC(self, CECMod, glassglass=False, bifi=None):
2✔
704
        """
705
        
706

707
        Parameters
708
        ----------
709
        CECMod : Dictionary
710
            alpha_sc, a_ref, I_L_ref, I_o_ref,  R_sh_ref, R_s, Adjust
711
        glassglass : Bool, optional
712
        
713
        """
714
        keys = ['alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref',  'R_sh_ref', 'R_s', 'Adjust']
2✔
715
        
716
        self.CECMod = CECMod
2✔
717
        self.glassglass = glassglass
2✔
718
        if bifi is None:
2✔
719
            bifi = self.bifi
2✔
720
        
721

722

723
    def calculatePerformance(self, effective_irradiance, CECMod=None, 
2✔
724
                             temp_air=None, wind_speed=1, temp_cell=None,  glassglass=None):
725
        '''
726
        The module parameters are given at the reference condition.
727
        Use pvlib.pvsystem.calcparams_cec() to generate the five SDM
728
        parameters at your desired irradiance and temperature to use
729
        with pvlib.pvsystem.singlediode() to calculate the IV curve information.:
730

731
        Inputs
732
        ------
733
        effective_irradiance : numeric
734
            Dataframe or single value. Must be same length as temp_cell
735
        CECMod : Dict
736
            Dictionary with CEC Module PArameters for the module selected. Must
737
            contain at minimum  alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref,
738
            R_s, Adjust
739
        temp_air : numeric
740
            Ambient temperature in Celsius. Dataframe or single value to calculate.
741
            Must be same length as effective_irradiance.  Default = 20C
742
        wind_speed : numeric
743
            Wind speed at a height of 10 meters [m/s].  Default = 1 m/s
744
        temp_cell : numeric
745
            Back of module temperature.  If provided, overrides temp_air and
746
            wind_speed calculation.  Default = None
747
        glassglass : boolean
748
            If module is glass-glass package (vs glass-polymer) to select correct
749
            thermal coefficients for module temperature calculation
750

751
        '''
752

753
        if CECMod is None:
2✔
754
            if getattr(self, 'CECMod', None) is not None:
2✔
755
                CECMod = self.CECMod
2✔
756
            else:
757
                print("No CECModule data passed; using default for Prism Solar BHC72-400")
2✔
758
                #url = 'https://raw.githubusercontent.com/NREL/SAM/patch/deploy/libraries/CEC%20Modules.csv'
759
                url = os.path.join(DATA_PATH,'CEC Modules.csv')
2✔
760
                db = pd.read_csv(url, index_col=0) # Reading this might take 1 min or so, the database is big.
2✔
761
                modfilter2 = db.index.str.startswith('Pr') & db.index.str.endswith('BHC72-400')
2✔
762
                CECMod = db[modfilter2]
2✔
763
                self.CECMod = CECMod
2✔
764
        
765
        if hasattr(self, 'glassglass') and glassglass is None:
2✔
766
            glassglass = self.glassglass
2✔
767

768
        from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
2✔
769

770
        # Setting temperature_model_parameters
771
        if glassglass:
2✔
772
            temp_model_params = (
2✔
773
                TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'])
774
        else:
775
            temp_model_params = (
2✔
776
                TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer'])
777

778
        if temp_cell is None:
2✔
779
            if temp_air is None:
2✔
780
                temp_air = 25  # STC
×
781

782
            temp_cell = pvlib.temperature.sapm_cell(effective_irradiance, temp_air,
2✔
783
                                                    wind_speed,
784
                                                    temp_model_params['a'],
785
                                                    temp_model_params['b'],
786
                                                    temp_model_params['deltaT'])
787

788
        IL, I0, Rs, Rsh, nNsVth = pvlib.pvsystem.calcparams_cec(
2✔
789
            effective_irradiance=effective_irradiance,
790
            temp_cell=temp_cell,
791
            alpha_sc=float(CECMod.alpha_sc),
792
            a_ref=float(CECMod.a_ref),
793
            I_L_ref=float(CECMod.I_L_ref),
794
            I_o_ref=float(CECMod.I_o_ref),
795
            R_sh_ref=float(CECMod.R_sh_ref),
796
            R_s=float(CECMod.R_s),
797
            Adjust=float(CECMod.Adjust)
798
            )
799

800
        IVcurve_info = pvlib.pvsystem.singlediode(
2✔
801
            photocurrent=IL,
802
            saturation_current=I0,
803
            resistance_series=Rs,
804
            resistance_shunt=Rsh,
805
            nNsVth=nNsVth
806
            )
807

808
        return IVcurve_info['p_mp']
2✔
809

810

811
# end of ModuleObj
812

813

814

815
class Omega(SuperClass):
2✔
816

817
    def __init__(self, module, omega_material='Metal_Grey', omega_thickness=0.004,
2✔
818
                 inverted=False, x_omega1=None, x_omega3=None, y_omega=None,
819
                 mod_overlap=None):
820
        """
821
        ====================    ===============================================
822
        Keys : type             Description
823
        ================        =============================================== 
824
        module : ModuleObj      Parent object with details related to geometry
825
        omega_material : str    The material the omega structure is made of
826
        omega_thickness : float Omega thickness
827
        inverted : Bool         Modifies the way the Omega is set on the Torquetbue
828
                                Looks like False: u  vs True: n  (default False)
829
        x_omega1  : float       The length of the module-adjacent arm of the 
830
                                omega parallel to the x-axis of the module
831
        y_omega  : float         Length of omega (Y-direction)
832
        x_omega3  : float       X-direction length of the torquetube adjacent 
833
                                arm of omega
834
        mod_overlap : float     The length of the overlap between omega and 
835
                                module surface on the x-direction
836

837
        =====================   ===============================================
838

839
        """
840
        self.keys = ['omega_material', 'x_omega1', 'mod_overlap', 'y_omega', 
2✔
841
            'omega_thickness','x_omega3','inverted']
842

843
        if x_omega1 is None:
2✔
844
            if inverted:
2✔
845
                x_omega1 = module.xgap*0.5
2✔
846
            else:
847
                x_omega1 = module.xgap*0.5*0.6
2✔
848
            _missingKeyWarning('Omega', 'x_omega1', x_omega1)
2✔
849
                
850
        if x_omega3 is None:
2✔
851
            if inverted:
2✔
852
                x_omega3 = module.xgap*0.5*0.3
2✔
853
            else:
854
                x_omega3 = module.xgap*0.5
2✔
855
            _missingKeyWarning('Omega', 'x_omega3', x_omega3)
2✔
856
            
857
        if y_omega is None:
2✔
858
            y_omega = module.y/2
2✔
859
            _missingKeyWarning('Omega', 'y_omega', y_omega)
2✔
860
        
861
        if mod_overlap is None:
2✔
862
           mod_overlap = x_omega1*0.6
2✔
863
           _missingKeyWarning('Omega', 'mod_overlap', mod_overlap)
2✔
864

865
        # set data object attributes from datakey list. 
866
        for key in self.keys:
2✔
867
            setattr(self, key, eval(key))        
2✔
868

869
        
870
        
871
    def _makeOmega(self, x, y, xgap, zgap, offsetfromaxis, z_inc = 0, **kwargs):
2✔
872
        """
873
        Helper function for creating a module that includes the racking 
874
        structure element `omega`.  
875

876
        TODO: remove some or all of this documentation since this is an internal function    
877
        
878
        Parameters
879
        ------------
880
        x : numeric
881
            Width of module along the axis of the torque tube or racking structure. (meters).
882
        y : numeric
883
            Length of module (meters)
884
        xgap : float
885
            Panel space in X direction. Separation between modules in a row.
886
        zgap : float
887
            Distance behind the modules in the z-direction to the edge of the tube (m)
888
        offsetfromaxis : float
889
            Internally defined variable in makeModule that specifies how much
890
            the module is offset from the Axis of Rotation due to zgap and or 
891
            frame thickness.
892
        z_inc : dict
893
            Internally defined variable in makeModule that specifies how much
894
            the module is offseted by the Frame.
895
        
896
        """
897
        
898
        # set local variables
899
        omega_material = self.omega_material
2✔
900
        x_omega1 = self.x_omega1
2✔
901
        mod_overlap = self.mod_overlap
2✔
902
        y_omega = self.y_omega
2✔
903
        omega_thickness = self.omega_thickness
2✔
904
        x_omega3 = self.x_omega3
2✔
905

906
        
907
        z_omega2 = zgap
2✔
908
        x_omega2 = omega_thickness 
2✔
909
        z_omega1 = omega_thickness
2✔
910
        z_omega3 = omega_thickness
2✔
911
        
912
        #naming the omega pieces
913
        name1 = 'mod_adj'
2✔
914
        name2 = 'verti'
2✔
915
        name3 = 'tt_adj'
2✔
916
        
917
        
918
        # defining the module adjacent member of omega
919
        x_translate1 = -x/2 - x_omega1 + mod_overlap
2✔
920
        y_translate = -y_omega/2 #common for all the pieces
2✔
921
        z_translate1 = offsetfromaxis-z_omega1
2✔
922
        
923
        #defining the vertical (zgap) member of the omega
924
        x_translate2 = x_translate1
2✔
925
        z_translate2 = offsetfromaxis-z_omega2
2✔
926
            
927
        #defining the torquetube adjacent member of omega
928
        x_translate3 = x_translate1-x_omega3
2✔
929
        z_translate3 = z_translate2
2✔
930
        
931
        if z_inc != 0: 
2✔
932
            z_translate1 += -z_inc
2✔
933
            z_translate2 += -z_inc
2✔
934
            z_translate3 += -z_inc
2✔
935
        
936
        # for this code, only the translations need to be shifted for the inverted omega
937
        
938
        if self.inverted == True:
2✔
939
            # shifting the non-inv omega shape of west as inv omega shape of east
940
            x_translate1_inv_east = x/2-mod_overlap
2✔
941
            x_shift_east = x_translate1_inv_east - x_translate1
2✔
942

943
            # shifting the non-inv omega shape of west as inv omega shape of east
944
            x_translate1_inv_west = -x_translate1_inv_east - x_omega1
2✔
945
            x_shift_west = -x_translate1_inv_west + (-x_translate1-x_omega1)
2✔
946
            
947
            #customizing the East side of the module for omega_inverted
948

949
            omegatext = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1_inv_east, y_translate, z_translate1) 
2✔
950
            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)
2✔
951
            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)
2✔
952

953
            #customizing the West side of the module for omega_inverted
954

955
            omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1_inv_west, y_translate, z_translate1) 
2✔
956
            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)
2✔
957
            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)
2✔
958
            
959
            omega2omega_x = -x_translate1_inv_east*2
2✔
960
        
961
        else:
962
            
963
            #customizing the West side of the module for omega
964
            
965
            omegatext = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1, y_translate, z_translate1) 
2✔
966
            omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name2, x_omega2, y_omega, z_omega2, x_translate2, y_translate, z_translate2)
2✔
967
            omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name3, x_omega3, y_omega, z_omega3, x_translate3, y_translate, z_translate3)
2✔
968
                
969
            #customizing the East side of the module for omega
970
                
971
            omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, -x_translate1-x_omega1, y_translate, z_translate1) 
2✔
972
            omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name2, x_omega2, y_omega, z_omega2, -x_translate2-x_omega2, y_translate, z_translate2)
2✔
973
            omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name3, x_omega3, y_omega, z_omega3, -x_translate3-x_omega3, y_translate, z_translate3)
2✔
974
        
975
            omega2omega_x = -x_translate3*2
2✔
976
        self.text = omegatext
2✔
977
        self.omega2omega_x = omega2omega_x
2✔
978
        return omega2omega_x,omegatext
2✔
979
    
980
class Frame(SuperClass):
2✔
981

982
    def __init__(self, frame_material='Metal_Grey', frame_thickness=0.05, 
2✔
983
                 frame_z=None, nSides_frame=4, frame_width=0.05):
984
        """
985
        Parameters
986
        ------------
987

988
        frame_material : str    The material the frame structure is made of
989
        frame_thickness : float The profile thickness of the frame 
990
        frame_z : float         The Z-direction length of the frame that extends 
991
                                below the module plane
992
        frame_width : float     The length of the bottom frame that is bolted 
993
                                with the omega
994
        nSides_frame : int      The number of sides of the module that are framed.
995
                                4 (default) or 2
996

997

998
        """
999
        self.keys = ['frame_material', 'frame_thickness', 'frame_z', 'frame_width',
2✔
1000
            'nSides_frame']
1001
        
1002
        if frame_z is None:
2✔
1003
            frame_z = 0.03
×
1004
            _missingKeyWarning('Frame', 'frame_z', frame_z)
×
1005
        
1006
        # set data object attributes from datakey list. 
1007
        for key in self.keys:
2✔
1008
            setattr(self, key, eval(key))  
2✔
1009
        
1010
    def _makeFrames(self,  x, y, ygap, numpanels, offsetfromaxis):
2✔
1011
        """
1012
        Helper function for creating a module that includes the frames attached to the module, 
1013

1014
            
1015
        Parameters
1016
        ------------
1017
        frameParams : dict
1018
            Dictionary with input parameters for creating a frame as part of the module.
1019
            See details below for keys needed.
1020
        x : numeric
1021
            Width of module along the axis of the torque tube or racking structure. (meters).
1022
        y : numeric
1023
            Length of module (meters)
1024
        ygap : float
1025
            Gap between modules arrayed in the Y-direction if any.
1026
        numpanels : int
1027
            Number of modules arrayed in the Y-direction. e.g.
1028
            1-up or 2-up, etc. (supports any number for carport/Mesa simulations)
1029
        offsetfromaxis : float
1030
            Internally defined variable in makeModule that specifies how much
1031
            the module is offset from the Axis of Rotation due to zgap and or 
1032
            frame thickness.
1033

1034

1035
        """
1036
        
1037
        # 
1038
        if self.nSides_frame == 2 and x>y:
2✔
1039
            print("Development Warning: Frames has only 2 sides and module is"+
×
1040
                  "in ladscape. This functionality is not working properly yet"+
1041
                  "for this release. We are overwriting nSide_frame = 4 to continue."+
1042
                  "If this functionality is pivotal to you we can prioritize adding it but"+
1043
                  "please comunicate with the development team. Thank you.")
1044
            self.nSides_frame = 4
×
1045
        
1046
        #Defining internal names
1047
        frame_material = self.frame_material 
2✔
1048
        f_thickness = self.frame_thickness 
2✔
1049
        f_height = self.frame_z
2✔
1050
        n_frame = self.nSides_frame  
2✔
1051
        fl_x = self.frame_width
2✔
1052

1053
        y_trans_shift = 0 #pertinent to the case of x>y with 2-sided frame
2✔
1054
                
1055

1056
        # Recalculating width ignoring the thickness of the aluminum
1057
        # for internal positioining and sizing of hte pieces
1058
        fl_x = fl_x-f_thickness
2✔
1059
        
1060
        if x>y and n_frame==2:
2✔
1061
            x_temp,y_temp = y,x
×
1062
            rotframe = 90
×
1063
            frame_y = x
×
1064
            y_trans_shift = x/2-y/2
×
1065
        else:
1066
            x_temp,y_temp = x,y
2✔
1067
            frame_y = y
2✔
1068
            rotframe = 0
2✔
1069
    
1070
        Ny = numpanels
2✔
1071
        y_half = (y*Ny/2)+(ygap*(Ny-1)/2)
2✔
1072
    
1073
        # taking care of lengths and translation points
1074
        # The pieces are same and symmetrical for west and east
1075
    
1076
        # naming the frame pieces
1077
        nameframe1 = 'frameside'
2✔
1078
        nameframe2 = 'frameleg'
2✔
1079
        
1080
        #frame sides
1081
        few_x = f_thickness
2✔
1082
        few_y = frame_y
2✔
1083
        few_z = f_height
2✔
1084
    
1085
        fw_xt = -x_temp/2 # in case of x_temp = y this doesn't reach panel edge
2✔
1086
        fe_xt = x_temp/2-f_thickness 
2✔
1087
        few_yt = -y_half-y_trans_shift
2✔
1088
        few_zt = offsetfromaxis-f_height
2✔
1089
    
1090
        #frame legs for east-west 
1091
    
1092
        flw_xt = -x_temp/2 + f_thickness
2✔
1093
        fle_xt = x_temp/2 - f_thickness-fl_x
2✔
1094
        flew_yt = -y_half-y_trans_shift
2✔
1095
        flew_zt = offsetfromaxis-f_height
2✔
1096
    
1097
    
1098
        #pieces for the shorter side (north-south in this case)
1099
    
1100
        #filler
1101
    
1102
        fns_x = x_temp-2*f_thickness
2✔
1103
        fns_y = f_thickness
2✔
1104
        fns_z = f_height-f_thickness
2✔
1105
    
1106
        fns_xt = -x_temp/2+f_thickness
2✔
1107
        fn_yt = -y_half+y-f_thickness
2✔
1108
        fs_yt = -y_half
2✔
1109
        fns_zt = offsetfromaxis-f_height+f_thickness
2✔
1110
    
1111
        # the filler legs
1112
    
1113
        filleg_x = x_temp-2*f_thickness-2*fl_x
2✔
1114
        filleg_y = f_thickness + fl_x
2✔
1115
        filleg_z = f_thickness
2✔
1116
    
1117
        filleg_xt = -x_temp/2+f_thickness+fl_x
2✔
1118
        fillegn_yt = -y_half+y-f_thickness-fl_x
2✔
1119
        fillegs_yt = -y_half
2✔
1120
        filleg_zt = offsetfromaxis-f_height
2✔
1121
    
1122
    
1123
        # making frames: west side
1124
        
1125
        
1126
        frame_text = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, few_x, few_y, few_z, fw_xt, few_yt, few_zt) 
2✔
1127
        frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
2✔
1128
    
1129
        frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, fl_x, frame_y, f_thickness, flw_xt, flew_yt, flew_zt)
2✔
1130
        frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
2✔
1131
                
1132
        # making frames: east side
1133
    
1134
        frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, few_x, few_y, few_z, fe_xt, few_yt, few_zt) 
2✔
1135
        frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
2✔
1136
    
1137
        frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, fl_x, frame_y, f_thickness, fle_xt, flew_yt, flew_zt)
2✔
1138
        frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
2✔
1139

1140
    
1141
        if n_frame == 4:
2✔
1142
            #making frames: north side
1143
    
1144
            frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, fns_x, fns_y, fns_z, fns_xt, fn_yt, fns_zt) 
2✔
1145
            frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1146
    
1147
    
1148
            frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, filleg_x, filleg_y, filleg_z, filleg_xt, fillegn_yt, filleg_zt)
2✔
1149
            frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1150
    
1151
            #making frames: south side
1152
    
1153
            frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, fns_x, fns_y, fns_z, fns_xt, fs_yt, fns_zt) 
2✔
1154
            frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1155
    
1156
            frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, filleg_x, filleg_y, filleg_z, filleg_xt, fillegs_yt, filleg_zt)
2✔
1157
            frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1158

1159
        z_inc = f_height
2✔
1160

1161
        return z_inc, frame_text
2✔
1162

1163
class Tube(SuperClass):
2✔
1164

1165
    def __init__(self, diameter=0.1, tubetype='Round', material='Metal_Grey', 
2✔
1166
                      axisofrotation=True, visible=True):
1167
        """
1168
        ================   ====================================================
1169
        Keys : type        Description
1170
        ================   ====================================================  
1171
        diameter : float   Tube diameter in meters. For square, diameter means 
1172
                           the length of one of the square-tube side.  For Hex, 
1173
                           diameter is the distance between two vertices 
1174
                           (diameter of the circumscribing circle). Default 0.1
1175
        tubetype : str     Options: 'Square', 'Round' (default), 'Hex' or 'Oct'
1176
                           Tube cross section
1177
        material : str     Options: 'Metal_Grey' or 'black'. Material for the 
1178
                           torque tube.
1179
        axisofrotation     (bool) :  Default True. IF true, creates geometry
1180
                           so center of rotation is at the center of the 
1181
                           torquetube, with an offsetfromaxis equal to half the
1182
                           torquetube diameter + the zgap. If there is no 
1183
                           torquetube (visible=False), offsetformaxis will 
1184
                           equal the zgap.
1185
        visible            (bool) :  Default True. If false, geometry is set
1186
                           as if the torque tube were present (e.g. zgap, 
1187
                           axisofrotation) but no geometry for the tube is made
1188
        ================   ==================================================== 
1189
        """
1190
        
1191
        self.keys = ['diameter', 'tubetype', 'material', 'visible']   # what about axisofrotation?
2✔
1192
        
1193
        self.axisofrotation = axisofrotation
2✔
1194
        # set data object attributes from datakey list. 
1195
        for key in self.keys:
2✔
1196
            setattr(self, key, eval(key))    
2✔
1197
            
1198
    def _makeTorqueTube(self, cc,  z_inc, zgap, scenex):
2✔
1199
        """  
1200
        Return text string for generating the torque tube geometry
1201
        
1202
        Parameters
1203
        
1204
        cc = module._cc #horizontal offset to center of a cell
1205
        """
1206
        import math
2✔
1207
        
1208
        
1209
        text = ''
2✔
1210
        tto = 0  # Torquetube Offset. Default = 0 if axisofrotationTT == True
2✔
1211
        diam = self.diameter  #alias
2✔
1212
        material = self.material #alias
2✔
1213
        
1214
        if self.tubetype.lower() == 'square':
2✔
1215
            if self.axisofrotation == False:
2✔
1216
                tto = -z_inc-zgap-diam/2.0
2✔
1217
            text += '\r\n! genbox {} tube1 {} {} {} '.format(material,
2✔
1218
                                  scenex, diam, diam)
1219
            text += '| xform -t {} {} {}'.format(-(scenex)/2.0+cc,
2✔
1220
                                -diam/2.0, -diam/2.0+tto)
1221

1222
        elif self.tubetype.lower() == 'round':
2✔
1223
            if self.axisofrotation == False:
2✔
1224
                tto = -z_inc-zgap-diam/2.0
2✔
1225
            text += '\r\n! genrev {} tube1 t*{} {} '.format(material, scenex, diam/2.0)
2✔
1226
            text += '32 | xform -ry 90 -t {} {} {}'.format(-(scenex)/2.0+cc, 0, tto)
2✔
1227

1228
        elif self.tubetype.lower() == 'hex':
2✔
1229
            radius = 0.5*diam
2✔
1230

1231
            if self.axisofrotation == False:
2✔
1232
                tto = -z_inc-radius*math.sqrt(3.0)/2.0-zgap
2✔
1233

1234
            text += '\r\n! genbox {} hextube1a {} {} {} | xform -t {} {} {}'.format(
2✔
1235
                    material, scenex, radius, radius*math.sqrt(3),
1236
                    -(scenex)/2.0+cc, -radius/2.0, -radius*math.sqrt(3.0)/2.0+tto) #ztran -radius*math.sqrt(3.0)-tto
1237

1238

1239
            # Create, translate to center, rotate, translate back to prev. position and translate to overal module position.
1240
            text = text+'\r\n! genbox {} hextube1b {} {} {} | xform -t {} {} {} -rx 60 -t 0 0 {}'.format(
2✔
1241
                    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)
1242
            
1243
            text = text+'\r\n! genbox {} hextube1c {} {} {} | xform -t {} {} {} -rx -60 -t 0 0 {}'.format(
2✔
1244
                    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)
1245

1246
        elif self.tubetype.lower()=='oct':
2✔
1247
            radius = 0.5*diam
2✔
1248
            s = diam / (1+math.sqrt(2.0))   # 
2✔
1249

1250
            if self.axisofrotation == False:
2✔
1251
                tto = -z_inc-radius-zgap
2✔
1252

1253
            text = text+'\r\n! genbox {} octtube1a {} {} {} | xform -t {} {} {}'.format(
2✔
1254
                    material, scenex, s, diam, -(scenex)/2.0, -s/2.0, -radius+tto)
1255

1256
            # Create, translate to center, rotate, translate back to prev. position and translate to overal module position.
1257
            text = text+'\r\n! genbox {} octtube1b {} {} {} | xform -t {} {} {} -rx 45 -t 0 0 {}'.format(
2✔
1258
                    material, scenex, s, diam, -(scenex)/2.0+cc, -s/2.0, -radius, tto)
1259

1260
            text = text+'\r\n! genbox {} octtube1c {} {} {} | xform -t {} {} {} -rx 90 -t 0 0 {}'.format(
2✔
1261
                    material, scenex, s, diam, -(scenex)/2.0+cc, -s/2.0, -radius, tto)
1262

1263
            text = text+'\r\n! genbox {} octtube1d {} {} {} | xform -t {} {} {} -rx 135 -t 0 0 {} '.format(
2✔
1264
                    material, scenex, s, diam, -(scenex)/2.0+cc, -s/2.0, -radius, tto)
1265

1266

1267
        else:
1268
            raise Exception("Incorrect torque tube type.  "+
×
1269
                            "Available options: 'square' 'oct' 'hex' or 'round'."+
1270
                            "  Value entered: {}".format(self.tubetype))    
1271
        self.text = text
2✔
1272
        return text
2✔
1273
    
1274
class CellModule(SuperClass):
2✔
1275

1276
    def __init__(self, numcellsx, numcellsy,
2✔
1277
                 xcell, ycell, xcellgap=0.02, ycellgap=0.02, centerJB=None):
1278
        """
1279
        For creating a cell-level module, the following input parameters should 
1280
        be in ``cellModule``:
1281
        
1282
        ================   ====================================================
1283
        Keys : type        Description
1284
        ================   ====================================================  
1285
        numcellsx : int    Number of cells in the X-direction within the module
1286
        numcellsy : int    Number of cells in the Y-direction within the module
1287
        xcell : float      Width of each cell (X-direction) in the module
1288
        ycell : float      Length of each cell (Y-direction) in the module
1289
        xcellgap : float   Spacing between cells in the X-direction. 0.02 default
1290
        ycellgap : float   Spacing between cells in the Y-direction. 0.02 default
1291
        centerJB : float   (optional) Distance betwen both sides of cell arrays 
1292
                           in a center-JB half-cell module. If 0 or not provided,
1293
                           module will not have the center JB spacing. 
1294
                           Only implemented for 'portrait' mode at the moment.
1295
                           (numcellsy > numcellsx). 
1296
        cc : float         center cell offset from x so scan is not at a gap 
1297
                           between cells
1298
        ================   ==================================================== 
1299

1300
        """
1301
        self.keys = ['numcellsx', 'numcellsy', 'xcell', 'ycell', 'xcellgap',
2✔
1302
            'ycellgap','centerJB'] 
1303
        
1304
        # set data object attributes from datakey list. 
1305
        for key in self.keys:
2✔
1306
            setattr(self, key, eval(key))    
2✔
1307
        
1308
    
1309
    def _makeCellLevelModule(self, module, z, Ny, ygap, 
2✔
1310
                         modulematerial):
1311
        """  Calculate the .radfile generation text for a cell-level module.
1312
        """
1313
        offsetfromaxis = module.offsetfromaxis
2✔
1314
        c = self.getDataDict()
2✔
1315

1316
        # For half cell modules with the JB on the center:
1317
        if c['centerJB'] is not None:
2✔
1318
            centerJB = c['centerJB']
2✔
1319
            y = c['numcellsy']*c['ycell'] + (c['numcellsy']-2)*c['ycellgap'] + centerJB            
2✔
1320
        else:
1321
            centerJB = 0
2✔
1322
            y = c['numcellsy']*c['ycell'] + (c['numcellsy']-1)*c['ycellgap']
2✔
1323

1324
        x = c['numcellsx']*c['xcell'] + (c['numcellsx']-1)*c['xcellgap']
2✔
1325

1326
        #center cell -
1327
        if c['numcellsx'] % 2 == 0:
2✔
1328
            _cc = c['xcell']/2.0
2✔
1329
            print("Module was shifted by {} in X to avoid sensors on air".format(_cc))
2✔
1330
        else:
1331
            _cc = 0
×
1332

1333
        text = '! genbox {} cellPVmodule {} {} {} | '.format(modulematerial,
2✔
1334
                                               c['xcell'], c['ycell'], z)
1335
        text +='xform -t {} {} {} '.format(-x/2.0 + _cc,
2✔
1336
                         (-y*Ny / 2.0)-(ygap*(Ny-1) / 2.0)-centerJB/2.0,
1337
                         offsetfromaxis)
1338
        
1339
        text += '-a {} -t {} 0 0 '.format(c['numcellsx'], c['xcell'] + c['xcellgap'])
2✔
1340
        
1341
        if centerJB != 0:
2✔
1342
            trans0 = c['ycell'] + c['ycellgap']
2✔
1343
            text += '-a {} -t 0 {} 0 '.format(c['numcellsy']/2, trans0)
2✔
1344
            #TODO: Continue playing with the y translation of the array in the next two lines
1345
                 # Until it matches. Close but not there.
1346
            # This is 0 spacing
1347
            #ytrans1 = y/2.0-c['ycell']/2.0-c['ycellgap']+centerJB/2.0   # Creating the 2nd array with the right Jbox distance
1348
            ytrans1 = y/2.0-c['ycell']/2.0-c['ycellgap']+centerJB/2.0 + centerJB
2✔
1349
            ytrans2= c['ycell'] - centerJB/2.0 + c['ycellgap']/2.0
2✔
1350
            text += '-a {} -t 0 {} 0 '.format(2, ytrans1)  
2✔
1351
            text += '| xform -t 0 {} 0 '.format(ytrans2)   
2✔
1352

1353
        else:
1354
            text += '-a {} -t 0 {} 0 '.format(c['numcellsy'], c['ycell'] + c['ycellgap'])
2✔
1355
            
1356
        text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1357

1358
        # OPACITY CALCULATION
1359
        packagingfactor = np.round((c['xcell']*c['ycell']*c['numcellsx']*c['numcellsy'])/(x*y), 2)
2✔
1360
        print("This is a Cell-Level detailed module with Packaging "+
2✔
1361
              "Factor of {} ".format(packagingfactor)) 
1362
        
1363
        module.x = x
2✔
1364
        module.y = y
2✔
1365
        self.text = text
2✔
1366
        
1367
        return(text, x, y, _cc)    
2✔
1368

1369
# deal with Int32 JSON incompatibility
1370
# https://www.programmerall.com/article/57461489186/
1371
import json
2✔
1372
class MyEncoder(json.JSONEncoder):
2✔
1373
    def default(self, obj):
2✔
1374
        if isinstance(obj, np.integer):
×
1375
            return int(obj)
×
1376
        elif isinstance(obj, np.floating):
×
1377
            return float(obj)
×
1378
        elif isinstance(obj, np.ndarray):
×
1379
            return obj.tolist()
×
1380
        else:
1381
            return super(MyEncoder, self).default(obj)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc