From 5cf1a7e77385bdfc06cc505930a42aa067cbe524 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 13 Mar 2021 13:29:47 -0700 Subject: [PATCH 1/4] move ModelChain.weather and times to ModelChainResult --- docs/sphinx/source/whatsnew/v0.9.0.rst | 2 + pvlib/modelchain.py | 88 +++++++++++++++----------- pvlib/tests/test_modelchain.py | 40 ++++++------ 3 files changed, 74 insertions(+), 56 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index cb8a34756e..92ac35f5b7 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -64,6 +64,8 @@ Deprecations * ``ModelChain.spectral_modifier`` * ``ModelChain.total_irrad`` * ``ModelChain.tracking`` + * ``ModelChain.weather`` + * ``ModelChain.times`` Enhancements ~~~~~~~~~~~~ diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 990598ae1e..80989f8c71 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -255,7 +255,8 @@ class ModelChainResult: _singleton_tuples: bool = field(default=False) _per_array_fields = {'total_irrad', 'aoi', 'aoi_modifier', 'spectral_modifier', 'cell_temperature', - 'effective_irradiance', 'dc', 'diode_params'} + 'effective_irradiance', 'dc', 'diode_params', + 'weather'} # system-level information solar_position: Optional[pd.DataFrame] = field(default=None) @@ -276,6 +277,9 @@ class ModelChainResult: field(default=None) diode_params: Optional[PerArray[pd.DataFrame]] = field(default=None) + weather: Optional[PerArray[pd.DataFrame]] = None + times: Optional[pd.DatetimeIndex] = None + def _result_type(self, value): """Coerce `value` to the correct type according to ``self._singleton_tuples``.""" @@ -368,7 +372,8 @@ class ModelChain: _deprecated_attrs = ['solar_position', 'airmass', 'total_irrad', 'aoi', 'aoi_modifier', 'spectral_modifier', 'cell_temperature', 'effective_irradiance', - 'dc', 'ac', 'diode_params', 'tracking'] + 'dc', 'ac', 'diode_params', 'tracking', + 'weather', 'times'] def __init__(self, system, location, clearsky_model='ineichen', @@ -397,9 +402,6 @@ def __init__(self, system, location, self.losses_model = losses_model - self.weather = None - self.times = None - self.results = ModelChainResult() def __getattr__(self, key): @@ -899,7 +901,7 @@ def infer_spectral_model(self): def first_solar_spectral_loss(self): self.results.spectral_modifier = self.system.first_solar_spectral_loss( - _tuple_from_dfs(self.weather, 'precipitable_water'), + _tuple_from_dfs(self.results.weather, 'precipitable_water'), self.results.airmass['airmass_absolute'] ) return self @@ -992,8 +994,8 @@ def _set_celltemp(self, model): poa = _irrad_for_celltemp(self.results.total_irrad, self.results.effective_irradiance) - temp_air = _tuple_from_dfs(self.weather, 'temp_air') - wind_speed = _tuple_from_dfs(self.weather, 'wind_speed') + temp_air = _tuple_from_dfs(self.results.weather, 'temp_air') + wind_speed = _tuple_from_dfs(self.results.weather, 'wind_speed') self.results.cell_temperature = model(poa, temp_air, wind_speed) return self @@ -1121,16 +1123,16 @@ def complete_irradiance(self, weather): self._check_multiple_input(weather) # Don't use ModelChain._assign_weather() here because it adds # temperature and wind-speed columns which we do not need here. - self.weather = _copy(weather) + self.results.weather = _copy(weather) self._assign_times() self.results.solar_position = self.location.get_solarposition( - self.times, method=self.solar_position_method) + self.results.times, method=self.solar_position_method) if isinstance(weather, tuple): - for w in self.weather: + for w in self.results.weather: self._complete_irradiance(w) else: - self._complete_irradiance(self.weather) + self._complete_irradiance(self.results.weather) return self @@ -1175,7 +1177,7 @@ def _prep_inputs_solar_pos(self, weather): pass self.results.solar_position = self.location.get_solarposition( - self.times, method=self.solar_position_method, + self.results.times, method=self.solar_position_method, **kwargs) return self @@ -1238,15 +1240,23 @@ def _verify(data, index=None): for (i, array_data) in enumerate(data): _verify(array_data, i) - def _configure_results(self): - """Configure the type used for per-array fields in ModelChainResult. + def _configure_results(self, per_array_data): + """Configure the type used for per-array fields in + ModelChainResult. + + If ``per_array_data`` is True and the number of arrays in the + system is 1, then per-array results are stored as length-1 + tuples. This overrides the PVSystem defaults of unpacking a 1 + length tuple into a singleton. - Must be called after ``self.weather`` has been assigned. If - ``self.weather`` is a tuple and the number of arrays in the system - is 1, then per-array results are stored as length-1 tuples. + Parameters + ---------- + per_array_data : bool + If input data is provided for each array, pass True. If a + single input data is provided for all arrays, pass False. """ self.results._singleton_tuples = ( - self.system.num_arrays == 1 and isinstance(self.weather, tuple) + self.system.num_arrays == 1 and per_array_data ) def _assign_weather(self, data): @@ -1258,13 +1268,13 @@ def _build_weather(data): if weather.get('temp_air') is None: weather['temp_air'] = 20 return weather - if not isinstance(data, tuple): - self.weather = _build_weather(data) + if isinstance(data, tuple): + weather = tuple(_build_weather(wx) for wx in data) + self._configure_results(per_array_data=True) else: - self.weather = tuple( - _build_weather(weather) for weather in data - ) - self._configure_results() + weather = _build_weather(data) + self._configure_results(per_array_data=False) + self.results.weather = weather self._assign_times() return self @@ -1281,18 +1291,20 @@ def _build_irrad(data): return self def _assign_times(self): - """Assign self.times according the the index of self.weather. - - If there are multiple DataFrames in self.weather then the index - of the first one is assigned. It is assumed that the indices of - each data frame in `weather` are the same. This can be verified - by calling :py:func:`_all_same_index` or - :py:meth:`self._check_multiple_weather` before calling this method. + """Assign self.results.times according the the index of + self.results.weather. + + If there are multiple DataFrames in self.results.weather then + the index of the first one is assigned. It is assumed that the + indices of each DataFrame in self.results.weather are the same. + This can be verified by calling :py:func:`_all_same_index` or + :py:meth:`self._check_multiple_weather` before calling this + method. """ - if isinstance(self.weather, tuple): - self.times = self.weather[0].index + if isinstance(self.results.weather, tuple): + self.results.times = self.results.weather[0].index else: - self.times = self.weather.index + self.results.times = self.results.weather.index def prepare_inputs(self, weather): """ @@ -1358,9 +1370,9 @@ def prepare_inputs(self, weather): self.results.solar_position['azimuth']) self.results.total_irrad = get_irradiance( - _tuple_from_dfs(self.weather, 'dni'), - _tuple_from_dfs(self.weather, 'ghi'), - _tuple_from_dfs(self.weather, 'dhi'), + _tuple_from_dfs(self.results.weather, 'dni'), + _tuple_from_dfs(self.results.weather, 'ghi'), + _tuple_from_dfs(self.results.weather, 'dhi'), airmass=self.results.airmass['airmass_relative'], model=self.transposition_model ) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index d4cb14814e..35a1635a8b 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -557,10 +557,10 @@ def test_ModelChain_times_arrays(sapm_dc_snl_ac_system_Array, location): weather_one = pd.DataFrame(irradiance_one, index=times) weather_two = pd.DataFrame(irradiance_two, index=times) mc.prepare_inputs((weather_one, weather_two)) - assert mc.times.equals(times) + assert mc.results.times.equals(times) mc = ModelChain(sapm_dc_snl_ac_system_Array, location) mc.prepare_inputs(weather_one) - assert mc.times.equals(times) + assert mc.results.times.equals(times) @pytest.mark.parametrize("missing", ['dhi', 'ghi', 'dni']) @@ -755,7 +755,7 @@ def test_prepare_inputs_from_poa(sapm_dc_snl_ac_system, location, weather_expected = weather_expected[ ['ghi', 'dhi', 'dni', 'wind_speed', 'temp_air']] # weather attribute - assert_frame_equal(mc.weather, weather_expected) + assert_frame_equal(mc.results.weather, weather_expected) # total_irrad attribute assert_frame_equal(mc.results.total_irrad, total_irrad) assert not pd.isnull(mc.results.solar_position.index[0]) @@ -815,7 +815,8 @@ def test__prepare_temperature(sapm_dc_snl_ac_system, location, weather, data[['poa_global', 'poa_diffuse', 'poa_direct']] = total_irrad mc = ModelChain(sapm_dc_snl_ac_system, location, aoi_model='no_loss', spectral_model='no_loss') - # prepare_temperature expects mc.total_irrad and mc.weather to be set + # prepare_temperature expects mc.total_irrad and mc.results.weather + # to be set mc._assign_weather(data) mc._assign_total_irrad(data) mc._prepare_temperature(data) @@ -870,7 +871,7 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, data_two = data.copy() mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location, aoi_model='no_loss', spectral_model='no_loss') - # prepare_temperature expects mc.total_irrad and mc.weather to be set + # prepare_temperature expects mc.total_irrad and mc.results.weather to be set mc._assign_weather((data, data_two)) mc._assign_total_irrad((data, data_two)) mc._prepare_temperature((data, data_two)) @@ -1126,6 +1127,7 @@ def test_run_model_singleton_weather_single_array(cec_dc_snl_ac_system, mc = ModelChain(cec_dc_snl_ac_system, location, aoi_model="no_loss", spectral_model="no_loss") mc.run_model([weather]) + assert isinstance(mc.results.weather, tuple) assert isinstance(mc.results.total_irrad, tuple) assert isinstance(mc.results.aoi, tuple) assert isinstance(mc.results.aoi_modifier, tuple) @@ -1144,6 +1146,7 @@ def test_run_model_from_poa_singleton_weather_single_array( ac = mc.run_model_from_poa([total_irrad]).results.ac expected = pd.Series(np.array([149.280238, 96.678385]), index=total_irrad.index) + assert isinstance(mc.results.weather, tuple) assert isinstance(mc.results.cell_temperature, tuple) assert len(mc.results.cell_temperature) == 1 assert isinstance(mc.results.cell_temperature[0], pd.Series) @@ -1160,6 +1163,7 @@ def test_run_model_from_effective_irradiance_weather_single_array( ac = mc.run_model_from_effective_irradiance([data]).results.ac expected = pd.Series(np.array([149.280238, 96.678385]), index=data.index) + assert isinstance(mc.results.weather, tuple) assert isinstance(mc.results.cell_temperature, tuple) assert len(mc.results.cell_temperature) == 1 assert isinstance(mc.results.cell_temperature[0], pd.Series) @@ -1697,11 +1701,11 @@ def test_complete_irradiance_clean_run(sapm_dc_snl_ac_system, location): mc.complete_irradiance(i) - assert_series_equal(mc.weather['dni'], + assert_series_equal(mc.results.weather['dni'], pd.Series([2, 3], index=times, name='dni')) - assert_series_equal(mc.weather['dhi'], + assert_series_equal(mc.results.weather['dhi'], pd.Series([4, 6], index=times, name='dhi')) - assert_series_equal(mc.weather['ghi'], + assert_series_equal(mc.results.weather['ghi'], pd.Series([9, 5], index=times, name='ghi')) @@ -1716,18 +1720,18 @@ def test_complete_irradiance(sapm_dc_snl_ac_system, location): with pytest.warns(UserWarning): mc.complete_irradiance(i[['ghi', 'dni']]) - assert_series_equal(mc.weather['dhi'], + assert_series_equal(mc.results.weather['dhi'], pd.Series([356.543700, 465.44400], index=times, name='dhi')) with pytest.warns(UserWarning): mc.complete_irradiance(i[['dhi', 'dni']]) - assert_series_equal(mc.weather['ghi'], + assert_series_equal(mc.results.weather['ghi'], pd.Series([372.103976116, 497.087579068], index=times, name='ghi')) mc.complete_irradiance(i[['dhi', 'ghi']]) - assert_series_equal(mc.weather['dni'], + assert_series_equal(mc.results.weather['dni'], pd.Series([49.756966, 62.153947], index=times, name='dni')) @@ -1748,7 +1752,7 @@ def test_complete_irradiance_arrays( match=r"Input DataFrames must have same index\."): mc.complete_irradiance(input_type((weather, weather[1:]))) mc.complete_irradiance(input_type((weather, weather))) - for mc_weather in mc.weather: + for mc_weather in mc.results.weather: assert_series_equal(mc_weather['dni'], pd.Series([2, 3], index=times, name='dni')) assert_series_equal(mc_weather['dhi'], @@ -1758,16 +1762,16 @@ def test_complete_irradiance_arrays( mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location) mc.complete_irradiance(input_type((weather[['ghi', 'dhi']], weather[['dhi', 'dni']]))) - assert 'dni' in mc.weather[0].columns - assert 'ghi' in mc.weather[1].columns + assert 'dni' in mc.results.weather[0].columns + assert 'ghi' in mc.results.weather[1].columns mc.complete_irradiance(input_type((weather, weather[['ghi', 'dni']]))) - assert_series_equal(mc.weather[0]['dhi'], + assert_series_equal(mc.results.weather[0]['dhi'], pd.Series([4, 6], index=times, name='dhi')) - assert_series_equal(mc.weather[0]['ghi'], + assert_series_equal(mc.results.weather[0]['ghi'], pd.Series([9, 5], index=times, name='ghi')) - assert_series_equal(mc.weather[0]['dni'], + assert_series_equal(mc.results.weather[0]['dni'], pd.Series([2, 3], index=times, name='dni')) - assert 'dhi' in mc.weather[1].columns + assert 'dhi' in mc.results.weather[1].columns @pytest.mark.parametrize("input_type", [tuple, list]) From b328c872cdbe9d6ba852e204d2640bff511ddb7a Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sat, 13 Mar 2021 13:32:00 -0700 Subject: [PATCH 2/4] stickler --- pvlib/tests/test_modelchain.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 35a1635a8b..3f3db99c96 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -871,7 +871,8 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, data_two = data.copy() mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location, aoi_model='no_loss', spectral_model='no_loss') - # prepare_temperature expects mc.total_irrad and mc.results.weather to be set + # prepare_temperature expects mc.total_irrad and mc.results.weather + # to be set mc._assign_weather((data, data_two)) mc._assign_total_irrad((data, data_two)) mc._prepare_temperature((data, data_two)) From 79a6b8ae0f623373cbbfb16267b8e0ab090f278e Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 14 Mar 2021 12:30:35 -0700 Subject: [PATCH 3/4] Update pvlib/tests/test_modelchain.py Co-authored-by: Cliff Hansen --- pvlib/tests/test_modelchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 3f3db99c96..06aa759ff8 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -871,7 +871,7 @@ def test__prepare_temperature_arrays_weather(sapm_dc_snl_ac_system_same_arrays, data_two = data.copy() mc = ModelChain(sapm_dc_snl_ac_system_same_arrays, location, aoi_model='no_loss', spectral_model='no_loss') - # prepare_temperature expects mc.total_irrad and mc.results.weather + # prepare_temperature expects mc.results.total_irrad and mc.results.weather # to be set mc._assign_weather((data, data_two)) mc._assign_total_irrad((data, data_two)) From 2712e32ee72478537fe0b49593bc6003a2fba8d4 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Sun, 14 Mar 2021 12:40:00 -0700 Subject: [PATCH 4/4] update docs --- docs/sphinx/source/modelchain.rst | 7 +++++-- pvlib/modelchain.py | 34 +++++++++++++++++-------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/docs/sphinx/source/modelchain.rst b/docs/sphinx/source/modelchain.rst index d2e1f7ba91..716e37b396 100644 --- a/docs/sphinx/source/modelchain.rst +++ b/docs/sphinx/source/modelchain.rst @@ -420,7 +420,10 @@ function if you wanted to. def pvusa_mc_wrapper(mc): # calculate the dc power and assign it to mc.dc - mc.dc = pvusa(mc.total_irrad['poa_global'], mc.weather['wind_speed'], mc.weather['temp_air'], + # in the future, need to explicitly iterate over system.arrays + # https://github.com/pvlib/pvlib-python/issues/1115 + mc.dc = pvusa(mc.results.total_irrad['poa_global'], + mc.results.weather['wind_speed'], mc.results.weather['temp_air'], mc.system.module_parameters['a'], mc.system.module_parameters['b'], mc.system.module_parameters['c'], mc.system.module_parameters['d']) @@ -436,7 +439,7 @@ function if you wanted to. def no_loss_temperature(mc): # keep it simple - mc.cell_temperature = mc.weather['temp_air'] + mc.cell_temperature = mc.results.weather['temp_air'] return mc diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 80989f8c71..7c1b1f7e46 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -1100,7 +1100,7 @@ def complete_irradiance(self, weather): Notes ----- - Assigns attributes: ``weather`` + Assigns attributes to ``results``: ``times``, ``weather`` Examples -------- @@ -1113,7 +1113,7 @@ def complete_irradiance(self, weather): >>> # my_weather containing 'dhi' and 'ghi'. >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP >>> mc.complete_irradiance(my_weather) # doctest: +SKIP - >>> mc.run_model(mc.weather) # doctest: +SKIP + >>> mc.run_model(mc.results.weather) # doctest: +SKIP >>> # my_weather containing 'dhi', 'ghi' and 'dni'. >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP @@ -1335,8 +1335,8 @@ def prepare_inputs(self, weather): Notes ----- - Assigns attributes: ``weather``, ``solar_position``, ``airmass``, - ``total_irrad``, ``aoi`` + Assigns attributes to ``results``: ``times``, ``weather``, + ``solar_position``, ``airmass``, ``total_irrad``, ``aoi`` See also -------- @@ -1431,8 +1431,8 @@ def prepare_inputs_from_poa(self, data): Notes ----- - Assigns attributes: ``weather``, ``total_irrad``, ``solar_position``, - ``airmass``, ``aoi``. + Assigns attributes to ``results``: ``times``, ``weather``, + ``total_irrad``, ``solar_position``, ``airmass``, ``aoi``. See also -------- @@ -1591,10 +1591,12 @@ def run_model(self, weather): Notes ----- - Assigns attributes: ``solar_position``, ``airmass``, ``weather``, - ``total_irrad``, ``aoi``, ``aoi_modifier``, ``spectral_modifier``, - and ``effective_irradiance``, ``cell_temperature``, ``dc``, ``ac``, - ``losses``, ``diode_params`` (if dc_model is a single diode model). + Assigns attributes to ``results``: ``times``, ``weather``, + ``solar_position``, ``airmass``, ``total_irrad``, ``aoi``, + ``aoi_modifier``, ``spectral_modifier``, and + ``effective_irradiance``, ``cell_temperature``, ``dc``, ``ac``, + ``losses``, ``diode_params`` (if dc_model is a single diode + model). See also -------- @@ -1650,10 +1652,12 @@ def run_model_from_poa(self, data): Notes ----- - Assigns attributes: ``solar_position``, ``airmass``, ``weather``, - ``total_irrad``, ``aoi``, ``aoi_modifier``, ``spectral_modifier``, - and ``effective_irradiance``, ``cell_temperature``, ``dc``, ``ac``, - ``losses``, ``diode_params`` (if dc_model is a single diode model). + Assigns attributes to results: ``times``, ``weather``, + ``solar_position``, ``airmass``, ``total_irrad``, ``aoi``, + ``aoi_modifier``, ``spectral_modifier``, and + ``effective_irradiance``, ``cell_temperature``, ``dc``, ``ac``, + ``losses``, ``diode_params`` (if dc_model is a single diode + model). See also -------- @@ -1746,7 +1750,7 @@ def run_model_from_effective_irradiance(self, data=None): If optional column ``'poa_global'`` is present, these data are used. If ``'poa_global'`` is not present, ``'effective_irradiance'`` is used. - Assigns attributes: ``weather``, ``total_irrad``, + Assigns attributes to results: ``times``, ``weather``, ``total_irrad``, ``effective_irradiance``, ``cell_temperature``, ``dc``, ``ac``, ``losses``, ``diode_params`` (if dc_model is a single diode model).