From edf3b5328295c0551563df0b22a8cedc4b30d90a Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 17 May 2021 21:40:59 -0600 Subject: [PATCH 01/10] initial __repr__ for ModelChainResult --- pvlib/modelchain.py | 50 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 8cc49153fa..65568e9e19 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -245,6 +245,18 @@ def get_orientation(strategy, **kwargs): return surface_tilt, surface_azimuth +def _getmcattr(self, attr): + """ + Helper for __repr__ methods, needed to avoid recursion in property + lookups + """ + out = getattr(self, attr) + try: + out = out.__name__ + except AttributeError: + pass + return out + # Type for fields that vary between arrays T = TypeVar('T') @@ -373,6 +385,32 @@ def __setattr__(self, key, value): super().__setattr__(key, value) + def __repr__(self): + system_front_attrs = ['weather', 'solar_position', 'airmass'] + per_array_attrs = ['tracking', 'aoi', 'aoi_modifier', 'total_irrad', + 'spectral_modifier', 'effective_irradiance', 'cell_temperature', + 'dc', 'dc_ohmic_losses' + ] + system_back_attrs = ['losses', 'ac'] + + if type(self.dc) is tuple: + num_arrays = len(self.dc) + else: + num_arrays = 1 + desc1 = ('ModelChainResult: \n ' + '\n '.join( + f'{attr} \n {_getmcattr(self, attr)} \n' + for attr in system_front_attrs)) + '\n' + desc2 = ('\n'.join( + f'--------------- \n Array {j} \n' + '\n'.join( + f' {attr} \n {_getmcattr(self, attr)} \n' + for attr in per_array_attrs) + '--------------- \n' + for j in range(num_arrays))) + desc3 = ('\n ' + '\n '.join( + f'{attr} \n {_getmcattr(self, attr)} \n' + for attr in system_back_attrs)) + return(desc1 + desc2 + desc3) + + class ModelChain: """ The ModelChain class to provides a standardized, high-level @@ -663,18 +701,8 @@ def __repr__(self): 'airmass_model', 'dc_model', 'ac_model', 'aoi_model', 'spectral_model', 'temperature_model', 'losses_model' ] - - def getmcattr(self, attr): - """needed to avoid recursion in property lookups""" - out = getattr(self, attr) - try: - out = out.__name__ - except AttributeError: - pass - return out - return ('ModelChain: \n ' + '\n '.join( - f'{attr}: {getmcattr(self, attr)}' for attr in attrs)) + f'{attr}: {_getmcattr(self, attr)}' for attr in attrs)) @property def dc_model(self): From 0551447b9a6b5d887050202e2860742efa4fd578 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 4 Jun 2021 10:52:46 -0600 Subject: [PATCH 02/10] fix timezone handling --- pvlib/forecast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/forecast.py b/pvlib/forecast.py index 55539f5760..ebb0c7b099 100644 --- a/pvlib/forecast.py +++ b/pvlib/forecast.py @@ -416,7 +416,8 @@ def set_time(self, time): only_use_cftime_datetimes=False, only_use_python_datetimes=True) self.time = pd.DatetimeIndex(pd.Series(times), tz=self.location.tz) - +# self.time = pd.DatetimeIndex(pd.Series(times)).tz_localize('UTC').tz_convert(self.location.tz) + def cloud_cover_to_ghi_linear(self, cloud_cover, ghi_clear, offset=35, **kwargs): """ From 068c72bbadee3a905165cbf29abf492aab71e1c6 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 21 Jun 2023 08:37:24 -0600 Subject: [PATCH 03/10] working on it --- pvlib/modelchain.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 558179c99e..7112aad08b 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -266,6 +266,7 @@ def _getmcattr(self, attr): pass return out + # Type for fields that vary between arrays T = TypeVar('T') @@ -397,7 +398,6 @@ def __setattr__(self, key, value): value = self._result_type(value) super().__setattr__(key, value) - def __repr__(self): system_front_attrs = ['weather', 'solar_position', 'airmass'] per_array_attrs = ['tracking', 'aoi', 'aoi_modifier', 'total_irrad', @@ -410,18 +410,28 @@ def __repr__(self): num_arrays = len(self.dc) else: num_arrays = 1 - desc1 = ('ModelChainResult: \n ' + '\n '.join( - f'{attr} \n {_getmcattr(self, attr)} \n' - for attr in system_front_attrs)) + '\n' - desc2 = ('\n'.join( - f'--------------- \n Array {j} \n' + '\n'.join( - f' {attr} \n {_getmcattr(self, attr)} \n' - for attr in per_array_attrs) + '--------------- \n' + + if num_arrays is 1: + front_attrs = [f'{attr} \n {_getmcattr(self, attr).head()} \n' + if all(hasattr(self, 'head'), hasattr(self, attr)) + else + f'{attr} \n {_getmcattr(self, attr)} \n' + if hasattr(self, attr) + for attr in system_front_attrs] + array_attrs = [f' {attr} \n {_getmcattr(self, attr)} \n' + for attr in per_array_attrs: + if hasattr(self, attr)] + + desc1 = ('ModelChainResult: \n ') + desc2 = ('\n'.join(front_attrs) + '\n') + desc3 = ('\n'.join( + f'--------------- \n Array {j} \n --------------- \n' + + '\n'.join(array_attrs) + '\n \n' for j in range(num_arrays))) - desc3 = ('\n ' + '\n '.join( + desc4 = ('\n ' + '\n '.join( f'{attr} \n {_getmcattr(self, attr)} \n' for attr in system_back_attrs)) - return(desc1 + desc2 + desc3) + return(desc1 + desc2 + desc3 + desc4) class ModelChain: From db6885387106d1a8fd2c10da3dc3a9aa8699d884 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 22 Jun 2023 10:01:44 -0600 Subject: [PATCH 04/10] working now --- pvlib/modelchain.py | 79 +++++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 7112aad08b..6f33108478 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -399,39 +399,76 @@ def __setattr__(self, key, value): super().__setattr__(key, value) def __repr__(self): - system_front_attrs = ['weather', 'solar_position', 'airmass'] + # once per MC + mc_front_attrs = ['solar_position', 'airmass'] + # per array per_array_attrs = ['tracking', 'aoi', 'aoi_modifier', 'total_irrad', 'spectral_modifier', 'effective_irradiance', 'cell_temperature', 'dc', 'dc_ohmic_losses' ] + # once per MC system_back_attrs = ['losses', 'ac'] + def _df_head(df): + try: + return df.head() + except: + return df + + one_weather = isinstance(self.weather, pd.DataFrame) + + if one_weather: + mc_front_attrs.insert(0, 'weather') + else: + per_array_attrs.insert(0, 'weather') + # once per MC + front_attrs = [f'{attr} \n {_df_head(_getmcattr(self, attr))} \n' + for attr in mc_front_attrs + if hasattr(self, attr)] + + if type(self.dc) is tuple: num_arrays = len(self.dc) else: num_arrays = 1 - if num_arrays is 1: - front_attrs = [f'{attr} \n {_getmcattr(self, attr).head()} \n' - if all(hasattr(self, 'head'), hasattr(self, attr)) - else - f'{attr} \n {_getmcattr(self, attr)} \n' - if hasattr(self, attr) - for attr in system_front_attrs] - array_attrs = [f' {attr} \n {_getmcattr(self, attr)} \n' - for attr in per_array_attrs: - if hasattr(self, attr)] - - desc1 = ('ModelChainResult: \n ') - desc2 = ('\n'.join(front_attrs) + '\n') - desc3 = ('\n'.join( - f'--------------- \n Array {j} \n --------------- \n' - + '\n'.join(array_attrs) + '\n \n' - for j in range(num_arrays))) - desc4 = ('\n ' + '\n '.join( - f'{attr} \n {_getmcattr(self, attr)} \n' + array_attrs = {k: [] for k in range(num_arrays)} + + if num_arrays > 1: + for k in range(num_arrays): + for attr in per_array_attrs: + if hasattr(self, attr): + if type(_getmcattr(self, attr)) is tuple: + s = (f' {attr} \n' + + f'{_df_head(_getmcattr(self, attr)[k])}' + + ' \n') + else: + s = (f' {attr} \n' + + f'{_df_head(_getmcattr(self, attr))}' + + ' \n') + array_attrs[k].append(s) + + else: + array_attrs[0] = \ + ([f' {attr} \n {_df_head(_getmcattr(self, attr))} \n' + for attr in per_array_attrs + if hasattr(self, attr)]) + + + desc1 = ('=== ModelChainResult === \n') + desc2 = ('\n '.join(front_attrs) + '\n ') + desc3 = (f'\n Number of Arrays: {num_arrays} \n') + desc4 = ('\n'.join([ + f'------------------- \n Array {j} \n' + + '------------------- \n' + + '\n'.join(array_attrs[j]) + for j in range(num_arrays)]) + + '\n End of Arrays \n' + + '------------------- \n') + desc5 = ('\n ' + '\n'.join( + f' {attr} \n' + f'{_df_head(_getmcattr(self, attr))} \n ' for attr in system_back_attrs)) - return(desc1 + desc2 + desc3 + desc4) + return(desc1 + desc2 + desc3 + desc4 + desc5) class ModelChain: From 3b005a2dc6d9722addaa3144dfa3e2d91bf4dc71 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 22 Jun 2023 10:10:15 -0600 Subject: [PATCH 05/10] jettison that space --- pvlib/modelchain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 6f33108478..c2a049c82c 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -450,7 +450,8 @@ def _df_head(df): else: array_attrs[0] = \ - ([f' {attr} \n {_df_head(_getmcattr(self, attr))} \n' + ([f' {attr} \n' + + f'{_df_head(_getmcattr(self, attr))} \n' for attr in per_array_attrs if hasattr(self, attr)]) From 0963ca93cc739c4cc7fc14e9d6dd3b8f08788bef Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 22 Jun 2023 10:38:37 -0600 Subject: [PATCH 06/10] whatsnew --- docs/sphinx/source/whatsnew/v0.10.0.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index f7ad2b7d7c..e9a605657c 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -28,6 +28,8 @@ Enhancements ~~~~~~~~~~~~ * Added `map_variables` parameter to :py:func:`pvlib.iotools.read_srml` and :py:func:`pvlib.iotools.read_srml_month_from_solardat` (:pull:`1773`) +* Improved `ModelChainResult.__repr__` (:pull:`1236`) + Bug fixes ~~~~~~~~~ @@ -52,3 +54,5 @@ Contributors ~~~~~~~~~~~~ * Taos Transue (:ghuser:`reepoi`) * Adam R. Jensen (:ghuser:`AdamRJensen`) +* Cliff Hansen (:ghuser:`cwhanse`) + From db1a33d18a323831c088668d42be48923b7c7758 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 22 Jun 2023 10:42:01 -0600 Subject: [PATCH 07/10] inline comments --- pvlib/modelchain.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index c2a049c82c..60e8364ec1 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -415,6 +415,9 @@ def _df_head(df): except: return df + # weather can be a single DataFrame or a tuple. If single we'll print + # it with the System attributes. If a tuple, we'll + # print it for each Array one_weather = isinstance(self.weather, pd.DataFrame) if one_weather: @@ -434,10 +437,13 @@ def _df_head(df): array_attrs = {k: [] for k in range(num_arrays)} + # if/else here avoids various exceptions if num_arrays > 1: for k in range(num_arrays): for attr in per_array_attrs: if hasattr(self, attr): + # attribute value may not be a tuple even with + # several arrays if type(_getmcattr(self, attr)) is tuple: s = (f' {attr} \n' + f'{_df_head(_getmcattr(self, attr)[k])}' + From 7f66f433d90b12fc0fd7f5a02f6384ed6be16db3 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 23 Jun 2023 09:18:14 -0600 Subject: [PATCH 08/10] shorten it up --- pvlib/modelchain.py | 98 +++++++++++++++------------------------------ 1 file changed, 32 insertions(+), 66 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 60e8364ec1..b4d747dd37 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -267,6 +267,20 @@ def _getmcattr(self, attr): return out +def _mcr_repr(obj): + ''' + Helper for ModelChainResult.__repr__ + ''' + if isinstance(obj, tuple): + return "Tuple (" + ", ".join([_mcr_repr(o) for o in obj]) + ")" + if isinstance(obj, pd.DataFrame): + return "DataFrame ({} rows x {} columns)".format(*obj.shape) + if isinstance(obj, pd.Series): + return "Series (length {})".format(len(obj)) + # scalar, None, other? + return repr(obj) + + # Type for fields that vary between arrays T = TypeVar('T') @@ -400,82 +414,34 @@ def __setattr__(self, key, value): def __repr__(self): # once per MC - mc_front_attrs = ['solar_position', 'airmass'] - # per array - per_array_attrs = ['tracking', 'aoi', 'aoi_modifier', 'total_irrad', - 'spectral_modifier', 'effective_irradiance', 'cell_temperature', - 'dc', 'dc_ohmic_losses' - ] - # once per MC - system_back_attrs = ['losses', 'ac'] + mc_attrs = ['weather', 'solar_position', 'airmass', 'tracking', 'aoi', + 'aoi_modifier', 'total_irrad', 'spectral_modifier', + 'effective_irradiance', 'cell_temperature', 'diode_params', + 'dc', 'dc_ohmic_losses', 'losses', 'ac'] - def _df_head(df): + def _head(obj): try: - return df.head() + return obj[:3] except: - return df - - # weather can be a single DataFrame or a tuple. If single we'll print - # it with the System attributes. If a tuple, we'll - # print it for each Array - one_weather = isinstance(self.weather, pd.DataFrame) - - if one_weather: - mc_front_attrs.insert(0, 'weather') - else: - per_array_attrs.insert(0, 'weather') - # once per MC - front_attrs = [f'{attr} \n {_df_head(_getmcattr(self, attr))} \n' - for attr in mc_front_attrs - if hasattr(self, attr)] - + return obj if type(self.dc) is tuple: num_arrays = len(self.dc) else: num_arrays = 1 - array_attrs = {k: [] for k in range(num_arrays)} - - # if/else here avoids various exceptions - if num_arrays > 1: - for k in range(num_arrays): - for attr in per_array_attrs: - if hasattr(self, attr): - # attribute value may not be a tuple even with - # several arrays - if type(_getmcattr(self, attr)) is tuple: - s = (f' {attr} \n' + - f'{_df_head(_getmcattr(self, attr)[k])}' + - ' \n') - else: - s = (f' {attr} \n' + - f'{_df_head(_getmcattr(self, attr))}' + - ' \n') - array_attrs[k].append(s) - - else: - array_attrs[0] = \ - ([f' {attr} \n' + - f'{_df_head(_getmcattr(self, attr))} \n' - for attr in per_array_attrs - if hasattr(self, attr)]) - - desc1 = ('=== ModelChainResult === \n') - desc2 = ('\n '.join(front_attrs) + '\n ') - desc3 = (f'\n Number of Arrays: {num_arrays} \n') - desc4 = ('\n'.join([ - f'------------------- \n Array {j} \n' + - '------------------- \n' + - '\n'.join(array_attrs[j]) - for j in range(num_arrays)]) + - '\n End of Arrays \n' + - '------------------- \n') - desc5 = ('\n ' + '\n'.join( - f' {attr} \n' + f'{_df_head(_getmcattr(self, attr))} \n ' - for attr in system_back_attrs)) - return(desc1 + desc2 + desc3 + desc4 + desc5) + desc2 = (f'Number of Arrays: {num_arrays} \n') + attr = 'times' + desc3 = ('Times (first 3)\n' + + f'{_head(_getmcattr(self, attr))}' + + '\n') + lines = [] + for attr in mc_attrs: + if hasattr(self, attr): + lines.append(f' {attr}: ' + _mcr_repr(getattr(self, attr))) + desc4 = '\n'.join(lines) + return (desc1 + desc2 + desc3 + desc4) class ModelChain: From ce3b77b64a8e0829a7a934396e51c16e050b9753 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 23 Jun 2023 12:17:34 -0600 Subject: [PATCH 09/10] test, add ModelChain.__repr__ test also --- pvlib/modelchain.py | 10 +++------ pvlib/tests/test_modelchain.py | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index b4d747dd37..1a8a0cabaa 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -413,11 +413,7 @@ def __setattr__(self, key, value): super().__setattr__(key, value) def __repr__(self): - # once per MC - mc_attrs = ['weather', 'solar_position', 'airmass', 'tracking', 'aoi', - 'aoi_modifier', 'total_irrad', 'spectral_modifier', - 'effective_irradiance', 'cell_temperature', 'diode_params', - 'dc', 'dc_ohmic_losses', 'losses', 'ac'] + mc_attrs = dir(self) def _head(obj): try: @@ -433,12 +429,12 @@ def _head(obj): desc1 = ('=== ModelChainResult === \n') desc2 = (f'Number of Arrays: {num_arrays} \n') attr = 'times' - desc3 = ('Times (first 3)\n' + + desc3 = ('times (first 3)\n' + f'{_head(_getmcattr(self, attr))}' + '\n') lines = [] for attr in mc_attrs: - if hasattr(self, attr): + if not (attr.startswith('_') or attr=='times'): lines.append(f' {attr}: ' + _mcr_repr(getattr(self, attr))) desc4 = '\n'.join(lines) return (desc1 + desc2 + desc3 + desc4) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 62b71f2042..83765f5a4d 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -2063,3 +2063,40 @@ def test__irrad_for_celltemp(): assert len(poa) == 2 assert_series_equal(poa[0], effect_irrad) assert_series_equal(poa[1], effect_irrad) + + +@pytest.mark.parametrize('strategy, strategy_str', [ + ('south_at_latitude_tilt', 'south_at_latitude_tilt'), + (None, 'None')]) # GitHub issue 352 +def test_ModelChain___repr__(sapm_dc_snl_ac_system, location, strategy, + strategy_str): + + mc = ModelChain(sapm_dc_snl_ac_system, location, + name='my mc') + + expected = '\n'.join([ + 'ModelChain: ', + ' name: my mc', + ' clearsky_model: ineichen', + ' transposition_model: haydavies', + ' solar_position_method: nrel_numpy', + ' airmass_model: kastenyoung1989', + ' dc_model: sapm', + ' ac_model: sandia_inverter', + ' aoi_model: sapm_aoi_loss', + ' spectral_model: sapm_spectral_loss', + ' temperature_model: sapm_temp', + ' losses_model: no_extra_losses' + ]) + + assert mc.__repr__() == expected + + +def test_ModelChainResult___repr__(sapm_dc_snl_ac_system, location, weather): + mc = ModelChain(sapm_dc_snl_ac_system, location) + mc.run_model(weather) + mcres = mc.results.__repr__() + mc_attrs = dir(mc.results) + mc_attrs = [a for a in mc_attrs if not a.startswith('_')] + assert all([a in mcres for a in mc_attrs]) + \ No newline at end of file From dde8221d2a07a12bbb14cbfe2b3301348ac88ba5 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 23 Jun 2023 13:33:26 -0600 Subject: [PATCH 10/10] remove unneeded decorator --- pvlib/tests/test_modelchain.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 83765f5a4d..ee975f3d21 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -2065,11 +2065,7 @@ def test__irrad_for_celltemp(): assert_series_equal(poa[1], effect_irrad) -@pytest.mark.parametrize('strategy, strategy_str', [ - ('south_at_latitude_tilt', 'south_at_latitude_tilt'), - (None, 'None')]) # GitHub issue 352 -def test_ModelChain___repr__(sapm_dc_snl_ac_system, location, strategy, - strategy_str): +def test_ModelChain___repr__(sapm_dc_snl_ac_system, location): mc = ModelChain(sapm_dc_snl_ac_system, location, name='my mc')