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

NREL / bifacial_radiance / 10778060626

09 Sep 2024 05:18PM UTC coverage: 72.218% (+0.06%) from 72.155%
10778060626

Pull #543

github

cdeline
add `results` property to RadianceObj.  rename demo.CompiledResults to demo.compiledResults
Pull Request #543: 512 cec performance

119 of 135 new or added lines in 4 files covered. (88.15%)

7 existing lines in 1 file now uncovered.

3699 of 5122 relevant lines covered (72.22%)

1.44 hits per line

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

91.51
/bifacial_radiance/module.py
1
# -*- coding: utf-8 -*-
2
"""
1✔
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())
2✔
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
    def __repr__(self):
2✔
35
        return str(type(self)) + ' : ' + str(self.getDataDict())
2✔
36
    def __init__(self, name=None, x=None, y=None, z=None, bifi=1, modulefile=None, 
2✔
37
                 text=None, customtext='', customObject='', xgap=0.01, ygap=0.0, zgap=0.1,
38
                 numpanels=1, rewriteModulefile=True, cellModule=None,  
39
                 glass=False, modulematerial='black', tubeParams=None,
40
                 frameParams=None, omegaParams=None, CECMod=None, hpc=False): 
41
                 
42
        """
43
        Add module details to the .JSON module config file module.json.
44
        Module definitions assume that the module .rad file is defined
45
        with zero tilt, centered along the x-axis and y-axis for the center
46
        of rotation of the module (+X/2, -X/2, +Y/2, -Y/2 on each side).
47
        Tip: to define a module that is in 'portrait' mode, y > x. 
48

49
        Parameters
50
        ------------
51
        name : str
52
            Input to name the module type
53
        x : numeric
54
            Width of module along the axis of the torque tube or rack. (meters)
55
        y : numeric
56
            Length of module (meters)
57
        bifi : numeric
58
            Bifaciality of the panel (used for calculatePerformance). Between 0 (monofacial) 
59
            and 1, default 1.
60
        modulefile : str
61
            Existing radfile location in \objects.  Otherwise a default value is used
62
        text : str
63
            Text used in the radfile to generate the module. Manually passing
64
            this value will overwrite module definition
65
        customtext : str
66
            Added-text used in the radfile to generate any
67
            extra details in the racking/module. Does not overwrite
68
            generated module (unlike "text"), but adds to it at the end.
69
        customObject : str
70
            Append to the module object file a pre-genereated radfile. This 
71
            must start with the file path\name. Does not overwrite
72
            generated module (unlike "text"), but adds to it at the end.
73
            It automatically inserts radiance's text before the object name so
74
            its inserted into scene properly ('!xform -rz 0')
75
        rewriteModulefile : bool
76
            Default True. Will rewrite module file each time makeModule is run.
77
        numpanels : int
78
            Number of modules arrayed in the Y-direction. e.g.
79
            1-up or 2-up, etc. (supports any number for carport/Mesa simulations)
80
        xgap : float
81
            Panel space in X direction. Separation between modules in a row.
82
        ygap : float
83
            Gap between modules arrayed in the Y-direction if any.
84
        zgap : float
85
            Distance behind the modules in the z-direction to the edge of the tube (m)
86
        glass : bool
87
            Add 5mm front and back glass to the module (glass/glass). Warning:
88
            glass increases the analysis variability.  Recommend setting
89
            accuracy='high' in AnalysisObj.analysis()
90
        cellModule : dict
91
            Dictionary with input parameters for creating a cell-level module.
92
            Shortcut for ModuleObj.addCellModule()
93
        tubeParams : dict
94
            Dictionary with input parameters for creating a torque tube as part of the 
95
            module. Shortcut for ModuleObj.addTorquetube()
96
        frameParams : dict
97
            Dictionary with input parameters for creating a frame as part of the module.
98
            Shortcut for ModuleObj.addFrame()
99
        omegaParams : dict
100
            Dictionary with input parameters for creating a omega or module support 
101
            structure. Shortcut for ModuleObj.addOmega()
102
        CECMod : Dictionary with performance parameters needed for self.calculatePerformance()
103
            lpha_sc, a_ref, I_L_ref, I_o_ref,  R_sh_ref, R_s, Adjust
104
        hpc : bool (default False)
105
            Set up module in HPC mode.  Namely turn off read/write to module.json
106
            and just pass along the details in the module object. Note that 
107
            calling e.g. addTorquetube() after this will tend to write to the
108
            module.json so pass all geometry parameters at once in to makeModule
109
            for best response.
110
                    
111
        '"""
112

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

143
        else:
144
            # set initial variables that aren't passed in 
145
            scenex = sceney = scenez = offsetfromaxis = 0
2✔
146
            """
2✔
147
            # TODO: this is kind of confusing and should probably be changed
148
            # set torque tube internal dictionary
149
            tubeBool = torquetube
150
            torquetube = {'bool':tubeBool,
151
                          'diameter':tubeParams['diameter'],
152
                          'tubetype':tubeParams['tubetype'],
153
                          'material':tubeParams['material']
154
                          }   
155
            try:
156
                self.axisofrotationTorqueTube = tubeParams['axisofrotation']
157
            except AttributeError:
158
                self.axisofrotationTorqueTube = False
159
            """
160
            if tubeParams:
2✔
161
                if 'bool' in tubeParams:  # backward compatible with pre-0.4
2✔
162
                    tubeParams['visible'] = tubeParams.pop('bool')
2✔
163
                if 'torqueTubeMaterial' in tubeParams:  #  pre-0.4
2✔
164
                    tubeParams['material'] = tubeParams.pop('torqueTubeMaterial')
2✔
165
                self.addTorquetube(**tubeParams, recompile=False)
2✔
166
            if omegaParams:
2✔
167
                self.addOmega(**omegaParams, recompile=False)
2✔
168
                
169
            if frameParams:
2✔
170
                self.addFrame(**frameParams, recompile=False)
2✔
171
                
172
            if cellModule:
2✔
173
                self.addCellModule(**cellModule, recompile=False)
2✔
174
            
175
            self.addCEC(CECMod, glass, bifi=bifi)
2✔
176
            
177
            if self._manual_text:
2✔
178
                print('Warning: Module text manually passed and not '
2✔
179
                      f'generated: {self._manual_text}')
180
            
181
            
182
            # set data object attributes from datakey list. 
183
            for key in self.keys:
2✔
184
                setattr(self, key, eval(key))      
2✔
185
            
186
            if self.modulefile is None:
2✔
187
                self.modulefile = os.path.join('objects',
2✔
188
                                                       self.name + '.rad')
189
                print("\nModule Name:", self.name)
2✔
190
                  
191
            if hpc:
2✔
192
                self.compileText(rewriteModulefile, json=False)
2✔
193
            else:
194
                self.compileText(rewriteModulefile)
2✔
195
            
196
    def compileText(self, rewriteModulefile=True, json=True):
2✔
197
        """
198
        Generate the text for the module .rad file based on ModuleObj attributes.
199
        Optionally save details to the module.json and module.rad files.
200

201
        Parameters
202
        ------------
203
        rewriteModulefile : bool (default True)
204
            Overwrite the .rad file for the module
205
        json : bool  (default True)
206
            Update the module.json file with ModuleObj attributes
207
            
208
        """
209
        saveDict = self.getDataDict()
2✔
210

211
        if hasattr(self,'cellModule'):
2✔
212
            saveDict = {**saveDict, 'cellModule':self.cellModule.getDataDict()}
2✔
213
        if hasattr(self,'torquetube'):
2✔
214
            saveDict = {**saveDict, 'torquetube':self.torquetube.getDataDict()}
2✔
215
        if hasattr(self,'omega'):
2✔
216
            saveDict = {**saveDict, 'omegaParams':self.omega.getDataDict()}
2✔
217
        if hasattr(self,'frame'):
2✔
218
            saveDict = {**saveDict, 'frameParams':self.frame.getDataDict()}
2✔
219
        if getattr(self, 'CECMod', None) is not None:
2✔
220
            saveDict = {**saveDict, 'CECMod':self.CECMod.getDataDict()}
2✔
221
            
222
        self._makeModuleFromDict(**saveDict)  
2✔
223

224
        #write JSON data out and write radfile if it doesn't exist
225
        self._saveModule({**saveDict, **self.getDataDict()}, json=json, 
2✔
226
                         rewriteModulefile=rewriteModulefile)
227

228
            
229
    def readModule(self, name=None):
2✔
230
        """
231
        Read in available modules in module.json.  If a specific module name is
232
        passed, return those details into the SceneObj. Otherwise 
233
        return available module list.
234

235
        Parameters:  name (str)  Name of module to be read
236

237
        Returns:  moduleDict dictionary or list of modulenames if name is not passed in.
238

239
        """
240
        import json
2✔
241
        filedir = os.path.join(DATA_PATH,'module.json')
2✔
242
        with open( filedir ) as configfile:
2✔
243
            data = json.load(configfile)
2✔
244

245
        modulenames = data.keys()
2✔
246
        if name is None:
2✔
247
            return list(modulenames)
2✔
248

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

293

294
    def _saveModule(self, savedata, json=True, rewriteModulefile=True):
2✔
295
        """
296
        write out changes to module.json and make radfile if it doesn't
297
        exist.  if rewriteModulefile is true, always overwrite Radfile.
298

299
        Parameters
300
        ----------
301
        json : bool, default is True.  Save JSON
302
        rewriteModulefile : bool, default is True.
303

304
        """
305
        import json as jsonmodule
2✔
306
        
307
        if json:
2✔
308
            filedir = os.path.join(DATA_PATH, 'module.json') 
2✔
309
            with open(filedir) as configfile:
2✔
310
                data = jsonmodule.load(configfile)
2✔
311
    
312
            data.update({self.name:savedata})
2✔
313
            with open(os.path.join(DATA_PATH, 'module.json') ,'w') as configfile:
2✔
314
                jsonmodule.dump(data, configfile, indent=4, sort_keys=True, 
2✔
315
                                cls=MyEncoder)
316
    
317
            print('Module {} updated in module.json'.format(self.name))
2✔
318
        # check that self.modulefile is not none
319
        if self.modulefile is None:
2✔
320
            self.modulefile = os.path.join('objects',
×
321
                                                   self.name + '.rad')
322
        
323
        if rewriteModulefile & os.path.isfile(self.modulefile):
2✔
324
            print(f"Pre-existing .rad file {self.modulefile} "
2✔
325
                  "will be overwritten\n")
326
            os.remove(self.modulefile)
2✔
327
            
328
        if not os.path.isfile(self.modulefile):
2✔
329
            # py2 and 3 compatible: binary write, encode text first
330
            try:
2✔
331
                with open(self.modulefile, 'wb') as f:
2✔
332
                    f.write(self.text.encode('ascii'))
2✔
333
            except FileNotFoundError:
×
334
                raise Exception(f'ModuleObj Error: directory "\{os.path.dirname(self.modulefile)}" not found '\
×
335
                                f' in current path which is {os.getcwd()}. Cannot create '\
336
                                f'{os.path.basename(self.modulefile)}. '\
337
                                'Are you in a valid bifacial_radiance directory?')
338
            
339
    def showModule(self):
2✔
340
        """ 
341
        Method to call objview and render the module object 
342
        (visualize it).
343
        
344
        Parameters: None
345

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

358
    def saveImage(self, filename=None):
2✔
359
        """
360
        Duplicate objview process to save an image of the module in /images/
361

362
        Parameters:    
363
            filename : string, optional. name for image file, defaults to module name               
364

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

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

426
        """
427
        self.torquetube = Tube(diameter=diameter, tubetype=tubetype,
2✔
428
                           material=material, axisofrotation=axisofrotation,
429
                           visible=visible)
430
        if recompile:
2✔
431
            self.compileText()
2✔
432

433

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

441

442
        Parameters
443
        ----------
444

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

464
        """
465
        self.omega = Omega(self, omega_material=omega_material,
2✔
466
                           omega_thickness=omega_thickness,
467
                           inverted=inverted, x_omega1=x_omega1,
468
                           x_omega3=x_omega3, y_omega=y_omega, 
469
                           mod_overlap=mod_overlap)
470
        if recompile:
2✔
471
            self.compileText()
2✔
472

473
    def addFrame(self, frame_material='Metal_Grey', frame_thickness=0.05, 
2✔
474
                 frame_z=0.3, nSides_frame=4, frame_width=0.05, recompile=True):
475
        """
476
        Add a metal frame geometry around the module.
477
        
478
        Parameters
479
        ------------
480

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

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

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

535
    
536
    def _makeModuleFromDict(self,  x=None, y=None, z=None, xgap=None, ygap=None, 
2✔
537
                    zgap=None, numpanels=None, modulefile=None,
538
                    modulematerial=None, **kwargs):
539

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

590
            
591
        if modulematerial is None:
2✔
592
            modulematerial = 'black'
2✔
593
            self.modulematerial = 'black'
2✔
594
            
595
        if self._manual_text is not None:
2✔
596
            text = self._manual_text
2✔
597
            self._manual_text = None
2✔
598

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

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

667
        #if torquetube_bool is True:
668
        if hasattr(self,'torquetube'):
2✔
669
            if self.torquetube.visible:
2✔
670
                text += self.torquetube._makeTorqueTube(cc=_cc, zgap=zgap,   
2✔
671
                                         z_inc=_zinc, scenex=self.scenex)
672

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

684
        text += frametext
2✔
685
        if hasattr(self, 'omega'):
2✔
686
            text += self.omega.text    
2✔
687
        
688
        if self.customtext != "":
2✔
689
            text += '\n' + self.customtext  # For adding any other racking details at the module level that the user might want.
2✔
690

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

694
        self.text = text
2✔
695
        return text
2✔
696
    #End of makeModuleFromDict()
697

698
    def addCEC(self, CECMod, glassglass=None, bifi=None):
2✔
699
        """
700
        
701

702
        Parameters
703
        ----------
704
        CECMod : Dictionary or pandas.DataFrame including:
705
            alpha_sc, a_ref, I_L_ref, I_o_ref,  R_sh_ref, R_s, Adjust
706
        glassglass : Bool, optional. Create a glass-glass module w 5mm glass on each side
707
        bifi  :  Float, bifaciality coefficient < 1
708
        
709
        """
710
        keys = ['alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref',  'R_sh_ref', 'R_s', 'Adjust']
2✔
711
        
712
        if glassglass is not None:
2✔
713
            self.glassglass = glassglass
2✔
714
        if bifi:
2✔
715
            self.bifi = bifi
2✔
716
        
717
            
718
        if type(CECMod) == pd.DataFrame:
2✔
719
            # Check for attributes along the index and transpose
720
            if 'alpha_sc' in CECMod.index:
2✔
721
                CECMod = CECMod.T
2✔
722
            if len(CECMod) > 1:
2✔
723
                print('Warning: DataFrame with multiple rows passed to module.addCEC. '\
2✔
724
                      'Taking the first entry')
725
            CECModDict = CECMod.iloc[0].to_dict()
2✔
726
            try:
2✔
727
                CECModDict['name'] = CECMod.iloc[0].name
2✔
NEW
728
            except AttributeError:
×
NEW
729
                CECModDict['name'] = None
×
730
        elif type(CECMod) == pd.Series:
2✔
731
            CECModDict = CECMod.to_dict()
2✔
732
        elif type(CECMod) == dict:
2✔
733
            CECModDict = CECMod
2✔
734
        elif type(CECMod) == str:
2✔
735
            raise Exception('Error: string-based module selection is not yet enabled. '\
2✔
736
                  'Try back later!')
737
            return
738
        elif CECMod is None:
2✔
739
            self.CECMod = None
2✔
740
            return
2✔
741
        else:
742
            raise Exception(f"Unrecognized type '{type(CECMod)}' passed into addCEC ")
2✔
743
        
744
        for key in keys:
2✔
745
            if key not in CECModDict:
2✔
746
                raise KeyError(f"Error: required key '{key}' not passed into module.addCEC")
2✔
747
                
748
        
749
        self.CECMod = CECModule(**CECModDict)
2✔
750
        
751

752

753
    def calculatePerformance(self, effective_irradiance, CECMod=None, 
2✔
754
                             temp_air=None, wind_speed=1, temp_cell=None,  glassglass=None):
755
        '''
756
        The module parameters are given at the reference condition.
757
        Use pvlib.pvsystem.calcparams_cec() to generate the five SDM
758
        parameters at your desired irradiance and temperature to use
759
        with pvlib.pvsystem.singlediode() to calculate the IV curve information.:
760

761
        Inputs
762
        ------
763
        effective_irradiance : numeric
764
            Dataframe or single value. Must be same length as temp_cell
765
        CECMod : Dict
766
            Dictionary with CEC Module PArameters for the module selected. Must
767
            contain at minimum  alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref,
768
            R_s, Adjust
769
        temp_air : numeric
770
            Ambient temperature in Celsius. Dataframe or single value to calculate.
771
            Must be same length as effective_irradiance.  Default = 20C
772
        wind_speed : numeric
773
            Wind speed at a height of 10 meters [m/s].  Default = 1 m/s
774
        temp_cell : numeric
775
            Back of module temperature.  If provided, overrides temp_air and
776
            wind_speed calculation.  Default = None
777
        glassglass : boolean
778
            If module is glass-glass package (vs glass-polymer) to select correct
779
            thermal coefficients for module temperature calculation
780

781
        '''
782

783
        if CECMod is None:
2✔
784
            if getattr(self, 'CECMod', None) is not None:
2✔
785
                CECMod = self.CECMod
2✔
786
            else:
787
                print("No CECModule data passed; using default for Prism Solar BHC72-400")
2✔
788
                #url = 'https://raw.githubusercontent.com/NREL/SAM/patch/deploy/libraries/CEC%20Modules.csv'
789
                url = os.path.join(DATA_PATH,'CEC Modules.csv')
2✔
790
                db = pd.read_csv(url, index_col=0) # Reading this might take 1 min or so, the database is big.
2✔
791
                modfilter2 = db.index.str.startswith('Pr') & db.index.str.endswith('BHC72-400')
2✔
792
                CECMod = db[modfilter2]
2✔
793
                self.addCEC(CECMod)
2✔
794
        
795
        if hasattr(self, 'glassglass') and glassglass is None:
2✔
796
            glassglass = self.glassglass
2✔
797

798
        from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
2✔
799

800
        # Setting temperature_model_parameters
801
        if glassglass:
2✔
802
            temp_model_params = (
2✔
803
                TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'])
804
        else:
805
            temp_model_params = (
2✔
806
                TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_polymer'])
807

808
        if temp_cell is None:
2✔
809
            if temp_air is None:
2✔
NEW
810
                temp_air = 25  # STC
×
811

812
            temp_cell = pvlib.temperature.sapm_cell(effective_irradiance, temp_air,
2✔
813
                                                    wind_speed,
814
                                                    temp_model_params['a'],
815
                                                    temp_model_params['b'],
816
                                                    temp_model_params['deltaT'])
817

818
        IL, I0, Rs, Rsh, nNsVth = pvlib.pvsystem.calcparams_cec(
2✔
819
            effective_irradiance=effective_irradiance,
820
            temp_cell=temp_cell,
821
            alpha_sc=float(CECMod.alpha_sc),
822
            a_ref=float(CECMod.a_ref),
823
            I_L_ref=float(CECMod.I_L_ref),
824
            I_o_ref=float(CECMod.I_o_ref),
825
            R_sh_ref=float(CECMod.R_sh_ref),
826
            R_s=float(CECMod.R_s),
827
            Adjust=float(CECMod.Adjust)
828
            )
829

830
        IVcurve_info = pvlib.pvsystem.singlediode(
2✔
831
            photocurrent=IL,
832
            saturation_current=I0,
833
            resistance_series=Rs,
834
            resistance_shunt=Rsh,
835
            nNsVth=nNsVth
836
            )
837

838
        return IVcurve_info['p_mp']
2✔
839

840

841
# end of ModuleObj
842

843

844

845
class Omega(SuperClass):
2✔
846

847
    def __init__(self, module, omega_material='Metal_Grey', omega_thickness=0.004,
2✔
848
                 inverted=False, x_omega1=None, x_omega3=None, y_omega=None,
849
                 mod_overlap=None):
850
        """
851
        ====================    ===============================================
852
        Keys : type             Description
853
        ================        =============================================== 
854
        module : ModuleObj      Parent object with details related to geometry
855
        omega_material : str    The material the omega structure is made of
856
        omega_thickness : float Omega thickness
857
        inverted : Bool         Modifies the way the Omega is set on the Torquetbue
858
                                Looks like False: u  vs True: n  (default False)
859
        x_omega1  : float       The length of the module-adjacent arm of the 
860
                                omega parallel to the x-axis of the module
861
        y_omega  : float         Length of omega (Y-direction)
862
        x_omega3  : float       X-direction length of the torquetube adjacent 
863
                                arm of omega
864
        mod_overlap : float     The length of the overlap between omega and 
865
                                module surface on the x-direction
866

867
        =====================   ===============================================
868

869
        """
870
        self.keys = ['omega_material', 'x_omega1', 'mod_overlap', 'y_omega', 
2✔
871
            'omega_thickness','x_omega3','inverted']
872

873
        if x_omega1 is None:
2✔
874
            if inverted:
2✔
875
                x_omega1 = module.xgap*0.5
2✔
876
            else:
877
                x_omega1 = module.xgap*0.5*0.6
2✔
878
            _missingKeyWarning('Omega', 'x_omega1', x_omega1)
2✔
879
                
880
        if x_omega3 is None:
2✔
881
            if inverted:
2✔
882
                x_omega3 = module.xgap*0.5*0.3
2✔
883
            else:
884
                x_omega3 = module.xgap*0.5
2✔
885
            _missingKeyWarning('Omega', 'x_omega3', x_omega3)
2✔
886
            
887
        if y_omega is None:
2✔
888
            y_omega = module.y/2
2✔
889
            _missingKeyWarning('Omega', 'y_omega', y_omega)
2✔
890
        
891
        if mod_overlap is None:
2✔
892
           mod_overlap = x_omega1*0.6
2✔
893
           _missingKeyWarning('Omega', 'mod_overlap', mod_overlap)
2✔
894

895
        # set data object attributes from datakey list. 
896
        for key in self.keys:
2✔
897
            setattr(self, key, eval(key))        
2✔
898

899
        
900
        
901
    def _makeOmega(self, x, y, xgap, zgap, offsetfromaxis, z_inc = 0, **kwargs):
2✔
902
        """
903
        Helper function for creating a module that includes the racking 
904
        structure element `omega`.  
905

906
        TODO: remove some or all of this documentation since this is an internal function    
907
        
908
        Parameters
909
        ------------
910
        x : numeric
911
            Width of module along the axis of the torque tube or racking structure. (meters).
912
        y : numeric
913
            Length of module (meters)
914
        xgap : float
915
            Panel space in X direction. Separation between modules in a row.
916
        zgap : float
917
            Distance behind the modules in the z-direction to the edge of the tube (m)
918
        offsetfromaxis : float
919
            Internally defined variable in makeModule that specifies how much
920
            the module is offset from the Axis of Rotation due to zgap and or 
921
            frame thickness.
922
        z_inc : dict
923
            Internally defined variable in makeModule that specifies how much
924
            the module is offseted by the Frame.
925
        
926
        """
927
        
928
        # set local variables
929
        omega_material = self.omega_material
2✔
930
        x_omega1 = self.x_omega1
2✔
931
        mod_overlap = self.mod_overlap
2✔
932
        y_omega = self.y_omega
2✔
933
        omega_thickness = self.omega_thickness
2✔
934
        x_omega3 = self.x_omega3
2✔
935

936
        
937
        z_omega2 = zgap
2✔
938
        x_omega2 = omega_thickness 
2✔
939
        z_omega1 = omega_thickness
2✔
940
        z_omega3 = omega_thickness
2✔
941
        
942
        #naming the omega pieces
943
        name1 = 'mod_adj'
2✔
944
        name2 = 'verti'
2✔
945
        name3 = 'tt_adj'
2✔
946
        
947
        
948
        # defining the module adjacent member of omega
949
        x_translate1 = -x/2 - x_omega1 + mod_overlap
2✔
950
        y_translate = -y_omega/2 #common for all the pieces
2✔
951
        z_translate1 = offsetfromaxis-z_omega1
2✔
952
        
953
        #defining the vertical (zgap) member of the omega
954
        x_translate2 = x_translate1
2✔
955
        z_translate2 = offsetfromaxis-z_omega2
2✔
956
            
957
        #defining the torquetube adjacent member of omega
958
        x_translate3 = x_translate1-x_omega3
2✔
959
        z_translate3 = z_translate2
2✔
960
        
961
        if z_inc != 0: 
2✔
962
            z_translate1 += -z_inc
2✔
963
            z_translate2 += -z_inc
2✔
964
            z_translate3 += -z_inc
2✔
965
        
966
        # for this code, only the translations need to be shifted for the inverted omega
967
        
968
        if self.inverted == True:
2✔
969
            # shifting the non-inv omega shape of west as inv omega shape of east
970
            x_translate1_inv_east = x/2-mod_overlap
2✔
971
            x_shift_east = x_translate1_inv_east - x_translate1
2✔
972

973
            # shifting the non-inv omega shape of west as inv omega shape of east
974
            x_translate1_inv_west = -x_translate1_inv_east - x_omega1
2✔
975
            x_shift_west = -x_translate1_inv_west + (-x_translate1-x_omega1)
2✔
976
            
977
            #customizing the East side of the module for omega_inverted
978

979
            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✔
980
            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✔
981
            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✔
982

983
            #customizing the West side of the module for omega_inverted
984

985
            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✔
986
            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✔
987
            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✔
988
            
989
            omega2omega_x = -x_translate1_inv_east*2
2✔
990
        
991
        else:
992
            
993
            #customizing the West side of the module for omega
994
            
995
            omegatext = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name1, x_omega1, y_omega, z_omega1, x_translate1, y_translate, z_translate1) 
2✔
996
            omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name2, x_omega2, y_omega, z_omega2, x_translate2, y_translate, z_translate2)
2✔
997
            omegatext += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(omega_material, name3, x_omega3, y_omega, z_omega3, x_translate3, y_translate, z_translate3)
2✔
998
                
999
            #customizing the East side of the module for omega
1000
                
1001
            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✔
1002
            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✔
1003
            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✔
1004
        
1005
            omega2omega_x = -x_translate3*2
2✔
1006
        self.text = omegatext
2✔
1007
        self.omega2omega_x = omega2omega_x
2✔
1008
        return omega2omega_x,omegatext
2✔
1009
    
1010
class Frame(SuperClass):
2✔
1011

1012
    def __init__(self, frame_material='Metal_Grey', frame_thickness=0.05, 
2✔
1013
                 frame_z=None, nSides_frame=4, frame_width=0.05):
1014
        """
1015
        Parameters
1016
        ------------
1017

1018
        frame_material : str    The material the frame structure is made of
1019
        frame_thickness : float The profile thickness of the frame 
1020
        frame_z : float         The Z-direction length of the frame that extends 
1021
                                below the module plane
1022
        frame_width : float     The length of the bottom frame that is bolted 
1023
                                with the omega
1024
        nSides_frame : int      The number of sides of the module that are framed.
1025
                                4 (default) or 2
1026

1027

1028
        """
1029
        self.keys = ['frame_material', 'frame_thickness', 'frame_z', 'frame_width',
2✔
1030
            'nSides_frame']
1031
        
1032
        if frame_z is None:
2✔
1033
            frame_z = 0.03
×
1034
            _missingKeyWarning('Frame', 'frame_z', frame_z)
×
1035
        
1036
        # set data object attributes from datakey list. 
1037
        for key in self.keys:
2✔
1038
            setattr(self, key, eval(key))  
2✔
1039
        
1040
    def _makeFrames(self,  x, y, ygap, numpanels, offsetfromaxis):
2✔
1041
        """
1042
        Helper function for creating a module that includes the frames attached to the module, 
1043

1044
            
1045
        Parameters
1046
        ------------
1047
        frameParams : dict
1048
            Dictionary with input parameters for creating a frame as part of the module.
1049
            See details below for keys needed.
1050
        x : numeric
1051
            Width of module along the axis of the torque tube or racking structure. (meters).
1052
        y : numeric
1053
            Length of module (meters)
1054
        ygap : float
1055
            Gap between modules arrayed in the Y-direction if any.
1056
        numpanels : int
1057
            Number of modules arrayed in the Y-direction. e.g.
1058
            1-up or 2-up, etc. (supports any number for carport/Mesa simulations)
1059
        offsetfromaxis : float
1060
            Internally defined variable in makeModule that specifies how much
1061
            the module is offset from the Axis of Rotation due to zgap and or 
1062
            frame thickness.
1063

1064

1065
        """
1066
        
1067
        # 
1068
        if self.nSides_frame == 2 and x>y:
2✔
1069
            print("Development Warning: Frames has only 2 sides and module is"+
×
1070
                  "in ladscape. This functionality is not working properly yet"+
1071
                  "for this release. We are overwriting nSide_frame = 4 to continue."+
1072
                  "If this functionality is pivotal to you we can prioritize adding it but"+
1073
                  "please comunicate with the development team. Thank you.")
1074
            self.nSides_frame = 4
×
1075
        
1076
        #Defining internal names
1077
        frame_material = self.frame_material 
2✔
1078
        f_thickness = self.frame_thickness 
2✔
1079
        f_height = self.frame_z
2✔
1080
        n_frame = self.nSides_frame  
2✔
1081
        fl_x = self.frame_width
2✔
1082

1083
        y_trans_shift = 0 #pertinent to the case of x>y with 2-sided frame
2✔
1084
                
1085

1086
        # Recalculating width ignoring the thickness of the aluminum
1087
        # for internal positioining and sizing of hte pieces
1088
        fl_x = fl_x-f_thickness
2✔
1089
        
1090
        if x>y and n_frame==2:
2✔
1091
            x_temp,y_temp = y,x
×
1092
            rotframe = 90
×
1093
            frame_y = x
×
1094
            y_trans_shift = x/2-y/2
×
1095
        else:
1096
            x_temp,y_temp = x,y
2✔
1097
            frame_y = y
2✔
1098
            rotframe = 0
2✔
1099
    
1100
        Ny = numpanels
2✔
1101
        y_half = (y*Ny/2)+(ygap*(Ny-1)/2)
2✔
1102
    
1103
        # taking care of lengths and translation points
1104
        # The pieces are same and symmetrical for west and east
1105
    
1106
        # naming the frame pieces
1107
        nameframe1 = 'frameside'
2✔
1108
        nameframe2 = 'frameleg'
2✔
1109
        
1110
        #frame sides
1111
        few_x = f_thickness
2✔
1112
        few_y = frame_y
2✔
1113
        few_z = f_height
2✔
1114
    
1115
        fw_xt = -x_temp/2 # in case of x_temp = y this doesn't reach panel edge
2✔
1116
        fe_xt = x_temp/2-f_thickness 
2✔
1117
        few_yt = -y_half-y_trans_shift
2✔
1118
        few_zt = offsetfromaxis-f_height
2✔
1119
    
1120
        #frame legs for east-west 
1121
    
1122
        flw_xt = -x_temp/2 + f_thickness
2✔
1123
        fle_xt = x_temp/2 - f_thickness-fl_x
2✔
1124
        flew_yt = -y_half-y_trans_shift
2✔
1125
        flew_zt = offsetfromaxis-f_height
2✔
1126
    
1127
    
1128
        #pieces for the shorter side (north-south in this case)
1129
    
1130
        #filler
1131
    
1132
        fns_x = x_temp-2*f_thickness
2✔
1133
        fns_y = f_thickness
2✔
1134
        fns_z = f_height-f_thickness
2✔
1135
    
1136
        fns_xt = -x_temp/2+f_thickness
2✔
1137
        fn_yt = -y_half+y-f_thickness
2✔
1138
        fs_yt = -y_half
2✔
1139
        fns_zt = offsetfromaxis-f_height+f_thickness
2✔
1140
    
1141
        # the filler legs
1142
    
1143
        filleg_x = x_temp-2*f_thickness-2*fl_x
2✔
1144
        filleg_y = f_thickness + fl_x
2✔
1145
        filleg_z = f_thickness
2✔
1146
    
1147
        filleg_xt = -x_temp/2+f_thickness+fl_x
2✔
1148
        fillegn_yt = -y_half+y-f_thickness-fl_x
2✔
1149
        fillegs_yt = -y_half
2✔
1150
        filleg_zt = offsetfromaxis-f_height
2✔
1151
    
1152
    
1153
        # making frames: west side
1154
        
1155
        
1156
        frame_text = '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, few_x, few_y, few_z, fw_xt, few_yt, few_zt) 
2✔
1157
        frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
2✔
1158
    
1159
        frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, fl_x, frame_y, f_thickness, flw_xt, flew_yt, flew_zt)
2✔
1160
        frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
2✔
1161
                
1162
        # making frames: east side
1163
    
1164
        frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, few_x, few_y, few_z, fe_xt, few_yt, few_zt) 
2✔
1165
        frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
2✔
1166
    
1167
        frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, fl_x, frame_y, f_thickness, fle_xt, flew_yt, flew_zt)
2✔
1168
        frame_text += ' -a {} -t 0 {} 0 | xform -rz {}'.format(Ny, y_temp+ygap, rotframe)
2✔
1169

1170
    
1171
        if n_frame == 4:
2✔
1172
            #making frames: north side
1173
    
1174
            frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, fns_x, fns_y, fns_z, fns_xt, fn_yt, fns_zt) 
2✔
1175
            frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1176
    
1177
    
1178
            frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, filleg_x, filleg_y, filleg_z, filleg_xt, fillegn_yt, filleg_zt)
2✔
1179
            frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1180
    
1181
            #making frames: south side
1182
    
1183
            frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe1, fns_x, fns_y, fns_z, fns_xt, fs_yt, fns_zt) 
2✔
1184
            frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1185
    
1186
            frame_text += '\r\n! genbox {} {} {} {} {} | xform -t {} {} {}'.format(frame_material, nameframe2, filleg_x, filleg_y, filleg_z, filleg_xt, fillegs_yt, filleg_zt)
2✔
1187
            frame_text += ' -a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1188

1189
        z_inc = f_height
2✔
1190

1191
        return z_inc, frame_text
2✔
1192

1193
class Tube(SuperClass):
2✔
1194

1195
    def __init__(self, diameter=0.1, tubetype='Round', material='Metal_Grey', 
2✔
1196
                      axisofrotation=True, visible=True):
1197
        """
1198
        ================   ====================================================
1199
        Keys : type        Description
1200
        ================   ====================================================  
1201
        diameter : float   Tube diameter in meters. For square, diameter means 
1202
                           the length of one of the square-tube side.  For Hex, 
1203
                           diameter is the distance between two vertices 
1204
                           (diameter of the circumscribing circle). Default 0.1
1205
        tubetype : str     Options: 'Square', 'Round' (default), 'Hex' or 'Oct'
1206
                           Tube cross section
1207
        material : str     Options: 'Metal_Grey' or 'black'. Material for the 
1208
                           torque tube.
1209
        axisofrotation     (bool) :  Default True. IF true, creates geometry
1210
                           so center of rotation is at the center of the 
1211
                           torquetube, with an offsetfromaxis equal to half the
1212
                           torquetube diameter + the zgap. If there is no 
1213
                           torquetube (visible=False), offsetformaxis will 
1214
                           equal the zgap.
1215
        visible            (bool) :  Default True. If false, geometry is set
1216
                           as if the torque tube were present (e.g. zgap, 
1217
                           axisofrotation) but no geometry for the tube is made
1218
        ================   ==================================================== 
1219
        """
1220
        
1221
        self.keys = ['diameter', 'tubetype', 'material', 'visible']   # what about axisofrotation?
2✔
1222
        
1223
        self.axisofrotation = axisofrotation
2✔
1224
        # set data object attributes from datakey list. 
1225
        for key in self.keys:
2✔
1226
            setattr(self, key, eval(key))    
2✔
1227
            
1228
    def _makeTorqueTube(self, cc,  z_inc, zgap, scenex):
2✔
1229
        """  
1230
        Return text string for generating the torque tube geometry
1231
        
1232
        Parameters
1233
        
1234
        cc = module._cc #horizontal offset to center of a cell
1235
        """
1236
        import math
2✔
1237
        
1238
        
1239
        text = ''
2✔
1240
        tto = 0  # Torquetube Offset. Default = 0 if axisofrotationTT == True
2✔
1241
        diam = self.diameter  #alias
2✔
1242
        material = self.material #alias
2✔
1243
        
1244
        if self.tubetype.lower() == 'square':
2✔
1245
            if self.axisofrotation == False:
2✔
1246
                tto = -z_inc-zgap-diam/2.0
2✔
1247
            text += '\r\n! genbox {} tube1 {} {} {} '.format(material,
2✔
1248
                                  scenex, diam, diam)
1249
            text += '| xform -t {} {} {}'.format(-(scenex)/2.0+cc,
2✔
1250
                                -diam/2.0, -diam/2.0+tto)
1251

1252
        elif self.tubetype.lower() == 'round':
2✔
1253
            if self.axisofrotation == False:
2✔
1254
                tto = -z_inc-zgap-diam/2.0
2✔
1255
            text += '\r\n! genrev {} tube1 t*{} {} '.format(material, scenex, diam/2.0)
2✔
1256
            text += '32 | xform -ry 90 -t {} {} {}'.format(-(scenex)/2.0+cc, 0, tto)
2✔
1257

1258
        elif self.tubetype.lower() == 'hex':
2✔
1259
            radius = 0.5*diam
2✔
1260

1261
            if self.axisofrotation == False:
2✔
1262
                tto = -z_inc-radius*math.sqrt(3.0)/2.0-zgap
2✔
1263

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

1268

1269
            # Create, translate to center, rotate, translate back to prev. position and translate to overal module position.
1270
            text = text+'\r\n! genbox {} hextube1b {} {} {} | xform -t {} {} {} -rx 60 -t 0 0 {}'.format(
2✔
1271
                    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)
1272
            
1273
            text = text+'\r\n! genbox {} hextube1c {} {} {} | xform -t {} {} {} -rx -60 -t 0 0 {}'.format(
2✔
1274
                    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)
1275

1276
        elif self.tubetype.lower()=='oct':
2✔
1277
            radius = 0.5*diam
2✔
1278
            s = diam / (1+math.sqrt(2.0))   # 
2✔
1279

1280
            if self.axisofrotation == False:
2✔
1281
                tto = -z_inc-radius-zgap
2✔
1282

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

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

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

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

1296

1297
        else:
1298
            raise Exception("Incorrect torque tube type.  "+
×
1299
                            "Available options: 'square' 'oct' 'hex' or 'round'."+
1300
                            "  Value entered: {}".format(self.tubetype))    
1301
        self.text = text
2✔
1302
        return text
2✔
1303
    
1304
class CellModule(SuperClass):
2✔
1305

1306
    def __init__(self, numcellsx, numcellsy,
2✔
1307
                 xcell, ycell, xcellgap=0.02, ycellgap=0.02, centerJB=None):
1308
        """
1309
        For creating a cell-level module, the following input parameters should 
1310
        be in ``cellModule``:
1311
        
1312
        ================   ====================================================
1313
        Keys : type        Description
1314
        ================   ====================================================  
1315
        numcellsx : int    Number of cells in the X-direction within the module
1316
        numcellsy : int    Number of cells in the Y-direction within the module
1317
        xcell : float      Width of each cell (X-direction) in the module
1318
        ycell : float      Length of each cell (Y-direction) in the module
1319
        xcellgap : float   Spacing between cells in the X-direction. 0.02 default
1320
        ycellgap : float   Spacing between cells in the Y-direction. 0.02 default
1321
        centerJB : float   (optional) Distance betwen both sides of cell arrays 
1322
                           in a center-JB half-cell module. If 0 or not provided,
1323
                           module will not have the center JB spacing. 
1324
                           Only implemented for 'portrait' mode at the moment.
1325
                           (numcellsy > numcellsx). 
1326
        cc : float         center cell offset from x so scan is not at a gap 
1327
                           between cells
1328
        ================   ==================================================== 
1329

1330
        """
1331
        self.keys = ['numcellsx', 'numcellsy', 'xcell', 'ycell', 'xcellgap',
2✔
1332
            'ycellgap','centerJB'] 
1333
        
1334
        # set data object attributes from datakey list. 
1335
        for key in self.keys:
2✔
1336
            setattr(self, key, eval(key))    
2✔
1337
        
1338
    
1339
    def _makeCellLevelModule(self, module, z, Ny, ygap, 
2✔
1340
                         modulematerial):
1341
        """  Calculate the .radfile generation text for a cell-level module.
1342
        """
1343
        offsetfromaxis = module.offsetfromaxis
2✔
1344
        c = self.getDataDict()
2✔
1345

1346
        # For half cell modules with the JB on the center:
1347
        if c['centerJB'] is not None:
2✔
1348
            centerJB = c['centerJB']
2✔
1349
            y = c['numcellsy']*c['ycell'] + (c['numcellsy']-2)*c['ycellgap'] + centerJB            
2✔
1350
        else:
1351
            centerJB = 0
2✔
1352
            y = c['numcellsy']*c['ycell'] + (c['numcellsy']-1)*c['ycellgap']
2✔
1353

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

1356
        #center cell -
1357
        if c['numcellsx'] % 2 == 0:
2✔
1358
            _cc = c['xcell']/2.0
2✔
1359
            print("Module was shifted by {} in X to avoid sensors on air".format(_cc))
2✔
1360
        else:
1361
            _cc = 0
×
1362

1363
        text = '! genbox {} cellPVmodule {} {} {} | '.format(modulematerial,
2✔
1364
                                               c['xcell'], c['ycell'], z)
1365
        text +='xform -t {} {} {} '.format(-x/2.0 + _cc,
2✔
1366
                         (-y*Ny / 2.0)-(ygap*(Ny-1) / 2.0)-centerJB/2.0,
1367
                         offsetfromaxis)
1368
        
1369
        text += '-a {} -t {} 0 0 '.format(c['numcellsx'], c['xcell'] + c['xcellgap'])
2✔
1370
        
1371
        if centerJB != 0:
2✔
1372
            trans0 = c['ycell'] + c['ycellgap']
2✔
1373
            text += '-a {} -t 0 {} 0 '.format(c['numcellsy']/2, trans0)
2✔
1374
            #TODO: Continue playing with the y translation of the array in the next two lines
1375
                 # Until it matches. Close but not there.
1376
            # This is 0 spacing
1377
            #ytrans1 = y/2.0-c['ycell']/2.0-c['ycellgap']+centerJB/2.0   # Creating the 2nd array with the right Jbox distance
1378
            ytrans1 = y/2.0-c['ycell']/2.0-c['ycellgap']+centerJB/2.0 + centerJB
2✔
1379
            ytrans2= c['ycell'] - centerJB/2.0 + c['ycellgap']/2.0
2✔
1380
            text += '-a {} -t 0 {} 0 '.format(2, ytrans1)  
2✔
1381
            text += '| xform -t 0 {} 0 '.format(ytrans2)   
2✔
1382

1383
        else:
1384
            text += '-a {} -t 0 {} 0 '.format(c['numcellsy'], c['ycell'] + c['ycellgap'])
2✔
1385
            
1386
        text += '-a {} -t 0 {} 0'.format(Ny, y+ygap)
2✔
1387

1388
        # OPACITY CALCULATION
1389
        packagingfactor = np.round((c['xcell']*c['ycell']*c['numcellsx']*c['numcellsy'])/(x*y), 2)
2✔
1390
        print("This is a Cell-Level detailed module with Packaging "+
2✔
1391
              "Factor of {} ".format(packagingfactor)) 
1392
        
1393
        module.x = x
2✔
1394
        module.y = y
2✔
1395
        self.text = text
2✔
1396
        
1397
        return(text, x, y, _cc)    
2✔
1398

1399
class CECModule(SuperClass):
2✔
1400

1401
    def __init__(self, alpha_sc, a_ref, I_L_ref, I_o_ref,  R_sh_ref, R_s, Adjust, **kwargs):
2✔
1402
        """
1403
        For storing module performance parameters to be fed into pvlib.pvsystem.singlediode()
1404
        Required passed parameters: alpha_sc, a_ref, I_L_ref, I_o_ref,  R_sh_ref, R_s, Adjust
1405
        Usage: pass in **dictionary with at minimum these keys: 'alpha_sc', 'a_ref', 'I_L_ref', 
1406
        'I_o_ref',  'R_sh_ref', 'R_s', 'Adjust', 'name' (opt)
1407

1408
        """
1409
        self.keys = ['alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref',  'R_sh_ref', 'R_s', 
2✔
1410
                     'Adjust', 'name'] 
1411
        name = kwargs.get('name')
2✔
1412
        if name is None:
2✔
1413
            name = kwargs.get('Name')
2✔
1414

1415
        
1416
        # set data object attributes from datakey list. 
1417
        for key in self.keys:
2✔
1418
            setattr(self, key, eval(key))    
2✔
1419
        
1420

1421

1422
# deal with Int32 JSON incompatibility
1423
# https://www.programmerall.com/article/57461489186/
1424
import json
2✔
1425
class MyEncoder(json.JSONEncoder):
2✔
1426
    def default(self, obj):
2✔
1427
        if isinstance(obj, np.integer):
×
1428
            return int(obj)
×
1429
        elif isinstance(obj, np.floating):
×
1430
            return float(obj)
×
1431
        elif isinstance(obj, np.ndarray):
×
1432
            return obj.tolist()
×
1433
        else:
1434
            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