From 585602adc249682ecfcaa13ecbc65d39c532132c Mon Sep 17 00:00:00 2001 From: cdeline Date: Thu, 5 Sep 2024 13:56:35 -0600 Subject: [PATCH 1/4] Update repr for AnalysisObj, start on RadianceObj. --- bifacial_radiance/main.py | 30 +++++++++++++++++++------ docs/sphinx/source/whatsnew/pending.rst | 1 + 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/bifacial_radiance/main.py b/bifacial_radiance/main.py index b10107f1..81949ea7 100644 --- a/bifacial_radiance/main.py +++ b/bifacial_radiance/main.py @@ -323,10 +323,24 @@ def _checkRaypath(): os.environ['RAYPATH'] = splitter.join(filter(None, raysplit + ['.'+splitter])) except (KeyError, AttributeError, TypeError): raise Exception('No RAYPATH set for RADIANCE. Please check your RADIANCE installation.') - + +class SuperClass: + def __repr__(self): + return str({key: self.__dict__[key] for key in self.columns}) + #return str(self.__dict__) + + @property + def columns(self): + return [attr for attr in dir(self) if not (attr.startswith('_') or attr.startswith('methods') + or attr.startswith('columns') or callable(getattr(self,attr)))] + @property + def methods(self): + return [attr for attr in dir(self) if (not (attr.startswith('_') or attr.startswith('methods') + or attr.startswith('columns')) and callable(getattr(self,attr)))] + -class RadianceObj: +class RadianceObj(SuperClass): """ The RadianceObj top level class is used to work on radiance objects, keep track of filenames, sky values, PV module configuration, etc. @@ -345,7 +359,8 @@ class RadianceObj: """ def __repr__(self): - return str(self.__dict__) + #return str(self.__dict__) + return str({key: self.__dict__[key] for key in self.columns if key != 'trackerdict'}) def __init__(self, name=None, path=None, hpc=False): ''' initialize RadianceObj with path of Radiance materials and objects, @@ -3116,7 +3131,7 @@ def saveImage(self, filename=None, view=None): -class MetObj: +class MetObj(SuperClass): """ Meteorological data from EPW file. @@ -3379,6 +3394,7 @@ def _set1axis(self, azimuth=180, limit_angle=45, angledelta=None, 'surf_azm':self.surface_azimuth[i], 'surf_tilt':self.surface_tilt[i], 'theta':self.tracker_theta[i], + 'dni':self.dni[i], 'ghi':self.ghi[i], 'dhi':self.dhi[i], 'temp_air':self.temp_air[i], @@ -3540,6 +3556,7 @@ def _makeTrackerCSV(self, theta_list, trackingdata): trackerdict[theta]['count'] = datetimetemp.__len__() #Create new temp csv file with zero values for all times not equal to datetimetemp # write 8760 2-column csv: GHI,DHI + dni_temp = [] ghi_temp = [] dhi_temp = [] for g, d, time in zip(self.ghi, self.dhi, @@ -3573,13 +3590,12 @@ def _makeTrackerCSV(self, theta_list, trackingdata): return trackerdict -class AnalysisObj: +class AnalysisObj(SuperClass): """ Analysis class for performing raytrace to obtain irradiance measurements at the array, as well plotting and reporting results. """ - def __repr__(self): - return str(self.__dict__) + def __init__(self, octfile=None, name=None, hpc=False): """ Initialize AnalysisObj by pointing to the octfile. Scan information diff --git a/docs/sphinx/source/whatsnew/pending.rst b/docs/sphinx/source/whatsnew/pending.rst index 43d4e7a2..8ebb8014 100644 --- a/docs/sphinx/source/whatsnew/pending.rst +++ b/docs/sphinx/source/whatsnew/pending.rst @@ -21,6 +21,7 @@ Bug fixes Documentation ~~~~~~~~~~~~~~ * No longer provide a warning message when both `hub_height` and `clearance_height` are passed to :py:class:`~bifacial_radiance.AnalysisObj.moduleAnalysis` (:pull:`540`) +* More useful __repr__ output in :py:class:`~bifacial_radiance.AnalysisObj and :py:class:`~bifacial_radiance.MetObj (:issue:`471`) Contributors ~~~~~~~~~~~~ From 28d40c0e114a2ae8621744f22dcafc705326dad1 Mon Sep 17 00:00:00 2001 From: cdeline Date: Thu, 5 Sep 2024 17:02:56 -0600 Subject: [PATCH 2/4] Make a nice __repr__ for MetObj class. fixes #471. --- bifacial_radiance/main.py | 40 +++++++++++++++++++++++++++++---- bifacial_radiance/module.py | 3 ++- tests/test_bifacial_radiance.py | 4 ++-- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/bifacial_radiance/main.py b/bifacial_radiance/main.py index 81949ea7..d6a44a78 100644 --- a/bifacial_radiance/main.py +++ b/bifacial_radiance/main.py @@ -328,7 +328,6 @@ class SuperClass: def __repr__(self): return str({key: self.__dict__[key] for key in self.columns}) #return str(self.__dict__) - @property def columns(self): return [attr for attr in dir(self) if not (attr.startswith('_') or attr.startswith('methods') @@ -2833,7 +2832,7 @@ def _makeGroundString(self, index=0, cumulativesky=False): -class SceneObj: +class SceneObj(SuperClass): ''' Scene information including PV module type, bifaciality, array info pv module orientation defaults: Azimuth = 180 (south) @@ -2855,7 +2854,7 @@ class SceneObj: ''' def __repr__(self): - return str(self.__dict__) + return 'SceneObj:\n'+str({key: self.__dict__[key] for key in self.columns}) def __init__(self, module=None, name=None): ''' initialize SceneObj ''' @@ -3155,6 +3154,28 @@ class MetObj(SuperClass): SAM and PVSyst use left-labeled interval data and NSRDB uses centered. """ + @property + def tmydata(self): + keys = ['ghi', 'dhi', 'dni', 'albedo', 'dewpoint', 'pressure', + 'temp_air', 'wind_speed', 'meastracker_angle', 'tracker_theta', + 'surface_tilt', 'surface_azimuth'] + return pd.DataFrame({key:self.__dict__.get(key, None) for key in keys }, + index = self.__dict__['datetime']).dropna(axis=1) + + @property + def metadata(self): + keys = ['latitude', 'longitude', 'elevation', 'timezone', 'city', 'label', + 'timezone'] + return {key:self.__dict__.get(key, None) for key in keys} + + def __repr__(self): + # return metadata and tmydata stats... + import io + buf = io.StringIO() + self.tmydata.info(memory_usage=False, buf=buf) + tmyinfo = buf.getvalue() + buf.close() + return f'\nMetObj.metadata:\n {self.metadata}\nMetObj.tmydata:\n {tmyinfo}\n' def __init__(self, tmydata, metadata, label = 'right'): @@ -3595,7 +3616,18 @@ class AnalysisObj(SuperClass): Analysis class for performing raytrace to obtain irradiance measurements at the array, as well plotting and reporting results. """ - + def __printval__(self, attr): + try: + t = type(getattr(self,attr, None)[0]) + except TypeError: + t = None + if t is float: + return np.array(getattr(self,attr)).round(3).tolist() + else: + return getattr(self,attr) + + def __repr__(self): + return 'AnalysisObj:\n' + str({key: self.__printval__(key) for key in self.columns}) def __init__(self, octfile=None, name=None, hpc=False): """ Initialize AnalysisObj by pointing to the octfile. Scan information diff --git a/bifacial_radiance/module.py b/bifacial_radiance/module.py index fc615a18..2504ff99 100644 --- a/bifacial_radiance/module.py +++ b/bifacial_radiance/module.py @@ -27,7 +27,8 @@ class ModuleObj(SuperClass): Pass this object into makeScene or makeScene1axis. """ - + def __repr__(self): + return 'ModuleObj:\n' + str(self.getDataDict()) def __init__(self, name=None, x=None, y=None, z=None, bifi=1, modulefile=None, text=None, customtext='', xgap=0.01, ygap=0.0, zgap=0.1, numpanels=1, rewriteModulefile=True, cellModule=None, diff --git a/tests/test_bifacial_radiance.py b/tests/test_bifacial_radiance.py index 2bad7492..0b114fd0 100644 --- a/tests/test_bifacial_radiance.py +++ b/tests/test_bifacial_radiance.py @@ -48,7 +48,7 @@ def test_RadianceObj_set1axis(): # test set1axis. requires metdata for boulder. name = "_test_set1axis" demo = bifacial_radiance.RadianceObj(name) - assert str(demo)[-16:-2]==name #this depends on the insertion order of the dictionary repr of demo - may not be consistent + assert len(str(demo)) > 300 # Make sure something is printed out here for demo.__repr__ #try: # epwfile = demo.getEPW(lat=40.01667, lon=-105.25) # From EPW: {N 40° 1'} {W 105° 15'} #except: # adding an except in case the internet connection in the lab forbids the epw donwload. @@ -322,7 +322,7 @@ def test_AnalysisObj_linePtsMake3D(): linepts = analysis._linePtsMake3D(0,0,0,1,1,1,0,0,0,1,2,3,'0 1 0') assert linepts == '0 0 0 0 1 0 \r1 1 1 0 1 0 \r0 0 0 0 1 0 \r1 1 1 0 1 0 \r0 0 0 0 1 0 \r1 1 1 0 1 0 \r' # v2.5.0 new linepts because now x and z also increase not only y. #assert linepts == '0 0 0 0 1 0 \r0 1 0 0 1 0 \r0 0 1 0 1 0 \r0 1 1 0 1 0 \r0 0 2 0 1 0 \r0 1 2 0 1 0 \r' - assert str(analysis)[12:16]=='None' + assert str(analysis)[-5:-1]=='None' # this depends on the order of the dict. but generally aligns with 'octfile' in alphabetical order.. def test_gendaylit2manual(): From c26715047ea7cf78cc6297fd9e4d254e18e7915e Mon Sep 17 00:00:00 2001 From: cdeline Date: Fri, 6 Sep 2024 14:39:31 -0600 Subject: [PATCH 3/4] More consistent __repr__ for different classes. more pytests --- bifacial_radiance/main.py | 14 +++++++------- bifacial_radiance/module.py | 2 +- tests/test_bifacial_radiance.py | 9 +++++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/bifacial_radiance/main.py b/bifacial_radiance/main.py index d6a44a78..f312129d 100644 --- a/bifacial_radiance/main.py +++ b/bifacial_radiance/main.py @@ -326,7 +326,7 @@ def _checkRaypath(): class SuperClass: def __repr__(self): - return str({key: self.__dict__[key] for key in self.columns}) + return str(type(self)) + ' : ' + str({key: self.__dict__[key] for key in self.columns}) #return str(self.__dict__) @property def columns(self): @@ -359,7 +359,7 @@ class RadianceObj(SuperClass): """ def __repr__(self): #return str(self.__dict__) - return str({key: self.__dict__[key] for key in self.columns if key != 'trackerdict'}) + return str(type(self)) + ' : ' + str({key: self.__dict__[key] for key in self.columns if key != 'trackerdict'}) def __init__(self, name=None, path=None, hpc=False): ''' initialize RadianceObj with path of Radiance materials and objects, @@ -2636,7 +2636,7 @@ def analysis1axis(self, trackerdict=None, singleindex=None, accuracy='low', # End RadianceObj definition -class GroundObj: +class GroundObj(SuperClass): """ Class to set and return details for the ground surface materials and reflectance. If 1 albedo value is passed, it is used as default. @@ -2853,8 +2853,7 @@ class SceneObj(SuperClass): ------- ''' - def __repr__(self): - return 'SceneObj:\n'+str({key: self.__dict__[key] for key in self.columns}) + def __init__(self, module=None, name=None): ''' initialize SceneObj ''' @@ -3175,7 +3174,8 @@ def __repr__(self): self.tmydata.info(memory_usage=False, buf=buf) tmyinfo = buf.getvalue() buf.close() - return f'\nMetObj.metadata:\n {self.metadata}\nMetObj.tmydata:\n {tmyinfo}\n' + return f".metadata:\n"\ + f"{self.metadata}\n.tmydata:\n {tmyinfo}\n" def __init__(self, tmydata, metadata, label = 'right'): @@ -3627,7 +3627,7 @@ def __printval__(self, attr): return getattr(self,attr) def __repr__(self): - return 'AnalysisObj:\n' + str({key: self.__printval__(key) for key in self.columns}) + return str(type(self)) + ' : ' + str({key: self.__printval__(key) for key in self.columns}) def __init__(self, octfile=None, name=None, hpc=False): """ Initialize AnalysisObj by pointing to the octfile. Scan information diff --git a/bifacial_radiance/module.py b/bifacial_radiance/module.py index 2504ff99..32fccfa9 100644 --- a/bifacial_radiance/module.py +++ b/bifacial_radiance/module.py @@ -28,7 +28,7 @@ class ModuleObj(SuperClass): """ def __repr__(self): - return 'ModuleObj:\n' + str(self.getDataDict()) + return str(type(self)) + ' : ' + str(self.getDataDict()) def __init__(self, name=None, x=None, y=None, z=None, bifi=1, modulefile=None, text=None, customtext='', xgap=0.01, ygap=0.0, zgap=0.1, numpanels=1, rewriteModulefile=True, cellModule=None, diff --git a/tests/test_bifacial_radiance.py b/tests/test_bifacial_radiance.py index 0b114fd0..db7c5d8c 100644 --- a/tests/test_bifacial_radiance.py +++ b/tests/test_bifacial_radiance.py @@ -196,6 +196,7 @@ def test_1axis_gencumSky(): demo = bifacial_radiance.RadianceObj(name) # Create a RadianceObj 'object' demo.setGround(albedo) # input albedo number or material name like 'concrete'. To see options, run this without any input. + assert demo.ground.methods == ['printGroundMaterials'] metdata = demo.readWeatherFile(weatherFile=MET_FILENAME, starttime='01_01_01', endtime = '01_01_23', coerce_year=2001) # read in the EPW weather data from above moduleText = '! genbox black test-module 0.98 1.95 0.02 | xform -t -0.49 -2.0 0 -a 2 -t 0 2.05 0' module=demo.makeModule(name='test-module',x=0.984,y=1.95, numpanels = 2, ygap = 0.1, text=moduleText) @@ -374,6 +375,7 @@ def test_left_label_metdata(): # right labeled MetObj import pvlib import pandas as pd + import unittest (tmydata, metadata) = pvlib.iotools.epw.read_epw(MET_FILENAME, coerce_year=2001) # rename different field parameters to match output from # pvlib.tmy.readtmy: DNI, DHI, DryBulb, Wspd @@ -385,6 +387,12 @@ def test_left_label_metdata(): 'albedo':'Alb' }, inplace=True) metdata1 = bifacial_radiance.MetObj(tmydata, metadata, label='left') + columnlist = ['ghi', 'dhi', 'dni', 'albedo', 'dewpoint', 'pressure', 'temp_air','wind_speed'] + assert all([col in list(metdata1.tmydata.columns) for col in columnlist]) + metadatalist = ['city', 'elevation', 'label', 'latitude', 'longitude', 'timezone'] + assert all([col in list(metdata1.metadata.keys()) for col in metadatalist]) + + demo = bifacial_radiance.RadianceObj('test') metdata2 = demo.readWeatherFile(weatherFile=MET_FILENAME, label='right', coerce_year=2001) pd.testing.assert_frame_equal(metdata1.solpos[:-1], metdata2.solpos[:-1]) @@ -417,6 +425,7 @@ def test_analyzeRow(): assert rowscan[rowscan.keys()[2]][0][0] == rowscan[rowscan.keys()[2]][1][0] # Assert Y is different for two different modules assert rowscan[rowscan.keys()[1]][0][0]+2 == rowscan[rowscan.keys()[1]][1][0] + assert (analysis.__printval__('x')[1] == 0) & (analysis.x[1] != 0) def test_addMaterialGroundRad(): From 0fc5040a056363ce182559b23aab797a5e2bec38 Mon Sep 17 00:00:00 2001 From: cdeline Date: Fri, 6 Sep 2024 14:59:11 -0600 Subject: [PATCH 4/4] more pytests... --- tests/test_module.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_module.py b/tests/test_module.py index c820668a..ba17d9f1 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -59,12 +59,14 @@ def test_CellLevelModule(): module.addCellModule(**cellParams, centerJB=0.01) #centerJB simulations still under development. # assert module.text == '! genbox black cellPVmodule 0.156 0.156 0.02 | xform -t -0.44 -0.87 0 -a 6 -t 0.176 0 0 -a 5.0 -t 0 0.176 0 -a 2 -t 0 0.772 0 | xform -t 0 0.181 0 -a 1 -t 0 1.73 0' - + assert len(module.cellModule.__repr__()) == 119 + assert len(module.__repr__()) > 490 def test_TorqueTubes_Module(): name = "_test_TorqueTubes" demo = bifacial_radiance.RadianceObj(name) # Create a RadianceObj 'object' - module = demo.makeModule(name='square', y=0.95,x=1.59, tubeParams={'tubetype':'square', 'axisofrotation':False}, hpc=True) #suppress saving .json + # test pre-0.4.0 compatibility keys 'bool' and 'torqueTubeMaterial'. Remove these when it's deprecated.. + module = demo.makeModule(name='square', y=0.95,x=1.59, tubeParams={'torqueTubeMaterial':'Metal_Grey','bool':True, 'tubetype':'square', 'axisofrotation':False}, hpc=True) #suppress saving .json assert module.x == 1.59 assert module.text == '! genbox black square 1.59 0.95 0.02 | xform -t -0.795 -0.475 0 -a 1 -t 0 0.95 0\r\n! genbox Metal_Grey tube1 1.6 0.1 0.1 | xform -t -0.8 -0.05 -0.2' module = demo.makeModule(name='round', y=0.95,x=1.59, tubeParams={'tubetype':'round', 'axisofrotation':False}, hpc=True)