From 38fc1429c0fce778ab00346e1379d82df6074528 Mon Sep 17 00:00:00 2001 From: Anton Driesse Date: Tue, 13 Sep 2022 22:10:45 +0200 Subject: [PATCH] Temperature model parameter translation code (#1463) * First commit to gather feedback on class design. * Changed set_*/get_* pattern to use_*/to_* * Add generic_linear() as a separate module temperature function and adjustments to doc strings. * Add tests. * Minor test edits. * One more test to achieve 100% coverage (hopefully). * Some formatting, docstring development. * Try to update sphynx. * Try sphynx again. * Docstrings nearing completion. * Docstrings complete. * One more small name change. * Fix problems identified in reviews. * Update whatsnew. Co-authored-by: Kevin Anderson --- docs/sphinx/source/reference/pv_modeling.rst | 2 + docs/sphinx/source/whatsnew/v0.9.3.rst | 8 +- pvlib/temperature.py | 416 +++++++++++++++++++ pvlib/tests/test_temperature.py | 87 ++++ 4 files changed, 512 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/reference/pv_modeling.rst b/docs/sphinx/source/reference/pv_modeling.rst index d1ae5a6559..31c380c1bb 100644 --- a/docs/sphinx/source/reference/pv_modeling.rst +++ b/docs/sphinx/source/reference/pv_modeling.rst @@ -45,6 +45,8 @@ PV temperature models temperature.noct_sam temperature.prilliman pvsystem.PVSystem.get_cell_temperature + temperature.generic_linear + temperature.GenericLinearModel Temperature Model Parameters ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/sphinx/source/whatsnew/v0.9.3.rst b/docs/sphinx/source/whatsnew/v0.9.3.rst index 568ebdfd78..60054f6482 100644 --- a/docs/sphinx/source/whatsnew/v0.9.3.rst +++ b/docs/sphinx/source/whatsnew/v0.9.3.rst @@ -9,6 +9,10 @@ Deprecations Enhancements ~~~~~~~~~~~~ +* New class and function translate module temperature model parameters + :py:func:`~pvlib.temperature.GenericLinearModel` + :py:func:`~pvlib.temperature.generic_linear` + (:issue:`1442`, :pull:`1463`) * Low resolution altitude lookup map :py:func:`~pvlib.location.lookup_altitude` (:issue:`1516`, :pull:`1518`) @@ -48,7 +52,9 @@ Contributors * Anton Driesse (:ghuser:`adriesse`) * Cliff Hansen (:ghuser:`cwhanse`) * Kevin Anderson (:ghuser:`kanderso-nrel`) +* Mark Mikofski (:ghuser:`mikofski`) +* Will Holmgren (:ghuser:`wholmgren`) * Mark Campanelli (:ghuser:`markcampanelli`) * Adam R. Jensen (:ghuser:`AdamRJensen`) * Abhishek Parikh (:ghuser:`abhisheksparikh`) -* Taos Transue (:ghuser:`reepoi`) +* Taos Transue (:ghuser:`reepoi`) \ No newline at end of file diff --git a/pvlib/temperature.py b/pvlib/temperature.py index 68a2869914..c55155606e 100644 --- a/pvlib/temperature.py +++ b/pvlib/temperature.py @@ -978,3 +978,419 @@ def prilliman(temp_cell, wind_speed, unit_mass=11.1, coefficients=None): smoothed[0] = temp_cell.values[0] smoothed = pd.Series(smoothed, index=temp_cell.index) return smoothed + + +def generic_linear(poa_global, temp_air, wind_speed, u_const, du_wind, + module_efficiency, absorptance): + """ + Calculate cell temperature using a generic linear heat loss factor model. + + The parameters for this model can be obtained from other model + parameters using :py:class:`GenericLinearModel`. A description of this + model and its relationship to other temperature models is found in [1]_. + + Parameters + ---------- + poa_global : numeric + Total incident irradiance [W/m^2]. + + temp_air : numeric + Ambient dry bulb temperature [C]. + + wind_speed : numeric + Wind speed at a height of 10 meters [m/s]. + + u_const : float + Combined heat transfer coefficient at zero wind speed [(W/m^2)/C] + + du_wind : float + Influence of wind speed on combined heat transfer coefficient + [(W/m^2)/C/(m/s)] + + module_efficiency : float + The electrical efficiency of the module. [-] + + absorptance : float + The light absorptance of the module. [-] + + Returns + ------- + numeric, values in degrees C. + + References + ---------- + .. [1] A. Driesse et al, "PV Module Operating Temperature + Model Equivalence and Parameter Translation". 2022 IEEE + Photovoltaic Specialists Conference (PVSC), 2022. + + See also + -------- + pvlib.temperature.GenericLinearModel + """ + # Contributed by Anton Driesse (@adriesse), PV Performance Labs, Sept. 2022 + + heat_input = poa_global * (absorptance - module_efficiency) + total_loss_factor = u_const + du_wind * wind_speed + temp_difference = heat_input / total_loss_factor + + return temp_air + temp_difference + + +class GenericLinearModel(): + ''' + A class that can both use and convert parameters of linear module + temperature models: faiman, pvsyst, noct_sam, sapm_module + and generic_linear. + + Parameters are converted between models by first converting + to the generic linear heat transfer model [1]_ by the ``use_`` + methods. The equivalent parameters for the target temperature + model are then obtained by the ``to_`` method. + Parameters are returned as a dictionary that is compatible with the + target model function to use in simulations. + + An instance of the class represents a specific module type and + the parameters ``module_efficiency`` and ``absorptance`` are required. + Although some temperature models do not use these properties, they + nevertheless exist and affect operating temperature. Values + should be representative of the conditions at which the input + model parameters were determined (usually high irradiance). + + Parameters + ---------- + module_efficiency : float + The electrical efficiency of the module. [-] + + absorptance : float + The light absorptance of the module. [-] + + Notes + ----- + After creating a GenericLinearModel object using the module properties, + one of the ``use_`` methods must be called to provide thermal model + parameters. If this is not done, the ``to_`` methods will return ``nan`` + values. + + References + ---------- + .. [1] A. Driesse et al, "PV Module Operating Temperature + Model Equivalence and Parameter Translation". 2022 IEEE + Photovoltaic Specialists Conference (PVSC), 2022. + + Examples + -------- + >>> glm = GenericLinearModel(module_efficiency=0.19, absorptance=0.88) + + >>> glm.use_faiman(16, 8) + GenericLinearModel: {'u_const': 11.04, 'du_wind': 5.52, + 'eta': 0.19, 'alpha': 0.88} + + >>> glm.to_pvsyst() + {'u_c': 11.404800000000002, 'u_v': 5.702400000000001, + 'module_efficiency': 0.19, 'alpha_absorption': 0.88} + + >>> parmdict = glm.to_pvsyst() + >>> pvsyst_cell(800, 20, 1, **parmdict) + 53.33333333333333 + + See also + -------- + pvlib.temperature.generic_linear + ''' + # Contributed by Anton Driesse (@adriesse), PV Performance Labs, Sept. 2022 + + def __init__(self, module_efficiency, absorptance): + + self.u_const = np.nan + self.du_wind = np.nan + self.eta = module_efficiency + self.alpha = absorptance + + return None + + def __repr__(self): + + return self.__class__.__name__ + ': ' + vars(self).__repr__() + + def __call__(self, poa_global, temp_air, wind_speed, + module_efficiency=None): + ''' + Calculate module temperature using the generic_linear model and + previously initialized parameters. + + Parameters + ---------- + poa_global : numeric + Total incident irradiance [W/m^2]. + + temp_air : numeric + Ambient dry bulb temperature [C]. + + wind_speed : numeric + Wind speed in m/s measured at the same height for which the wind + loss factor was determined. [m/s] + + module_efficiency : numeric, optional + Module electrical efficiency. The default value is the one + that was specified initially. [-] + + Returns + ------- + numeric, values in degrees Celsius + + See also + -------- + get_generic + pvlib.temperature.generic_linear + ''' + if module_efficiency is None: + module_efficiency = self.eta + + return generic_linear(poa_global, temp_air, wind_speed, + self.u_const, self.du_wind, + module_efficiency, self.alpha) + + def get_generic_linear(self): + ''' + Get the generic linear model parameters to use with the separate + generic linear module temperature calculation function. + + Returns + ------- + model_parameters : dict + + See also + -------- + pvlib.temperature.generic_linear + ''' + return dict(u_const=self.u_const, + du_wind=self.du_wind, + module_efficiency=self.eta, + absorptance=self.alpha) + + def use_faiman(self, u0, u1): + ''' + Use the Faiman model parameters to set the generic_model equivalents. + + Parameters + ---------- + u0, u1 : float + See :py:func:`pvlib.temperature.faiman` for details. + ''' + net_absorptance = self.alpha - self.eta + self.u_const = u0 * net_absorptance + self.du_wind = u1 * net_absorptance + + return self + + def to_faiman(self): + ''' + Convert the generic model parameters to Faiman equivalents. + + Returns + ---------- + model_parameters : dict + See :py:func:`pvlib.temperature.faiman` for + model parameter details. + ''' + net_absorptance = self.alpha - self.eta + u0 = self.u_const / net_absorptance + u1 = self.du_wind / net_absorptance + + return dict(u0=u0, u1=u1) + + def use_pvsyst(self, u_c, u_v, module_efficiency=None, + alpha_absorption=None): + ''' + Use the PVsyst model parameters to set the generic_model equivalents. + + Parameters + ---------- + u_c, u_v : float + See :py:func:`pvlib.temperature.pvsyst_cell` for details. + + module_efficiency, alpha_absorption : float, optional + See :py:func:`pvlib.temperature.pvsyst_cell` for details. + + Notes + ----- + The optional parameters are primarily for convenient compatibility + with existing function signatures. + ''' + if module_efficiency is not None: + self.eta = module_efficiency + + if alpha_absorption is not None: + self.alpha = alpha_absorption + + net_absorptance_glm = self.alpha - self.eta + net_absorptance_pvsyst = self.alpha * (1.0 - self.eta) + absorptance_ratio = net_absorptance_glm / net_absorptance_pvsyst + + self.u_const = u_c * absorptance_ratio + self.du_wind = u_v * absorptance_ratio + + return self + + def to_pvsyst(self): + ''' + Convert the generic model parameters to PVsyst model equivalents. + + Returns + ---------- + model_parameters : dict + See :py:func:`pvlib.temperature.pvsyst_cell` for + model parameter details. + ''' + net_absorptance_glm = self.alpha - self.eta + net_absorptance_pvsyst = self.alpha * (1.0 - self.eta) + absorptance_ratio = net_absorptance_glm / net_absorptance_pvsyst + + u_c = self.u_const / absorptance_ratio + u_v = self.du_wind / absorptance_ratio + + return dict(u_c=u_c, + u_v=u_v, + module_efficiency=self.eta, + alpha_absorption=self.alpha) + + def use_noct_sam(self, noct, module_efficiency=None, + transmittance_absorptance=None): + ''' + Use the NOCT SAM model parameters to set the generic_model equivalents. + + Parameters + ---------- + noct : float + See :py:func:`pvlib.temperature.noct_sam` for details. + + module_efficiency, transmittance_absorptance : float, optional + See :py:func:`pvlib.temperature.noct_sam` for details. + + Notes + ----- + The optional parameters are primarily for convenient compatibility + with existing function signatures. + ''' + if module_efficiency is not None: + self.eta = module_efficiency + + if transmittance_absorptance is not None: + self.alpha = transmittance_absorptance + + # NOCT is determined with wind speed near module height + # the adjustment reduces the wind coefficient for use with 10m wind + wind_adj = 0.51 + u_noct = 800.0 * self.alpha / (noct - 20.0) + self.u_const = u_noct * 0.6 + self.du_wind = u_noct * 0.4 * wind_adj + + return self + + def to_noct_sam(self): + ''' + Convert the generic model parameters to NOCT SAM model equivalents. + + Returns + ---------- + model_parameters : dict + See :py:func:`pvlib.temperature.noct_sam` for + model parameter details. + ''' + # NOCT is determined with wind speed near module height + # the adjustment reduces the wind coefficient for use with 10m wind + wind_adj = 0.51 + u_noct = self.u_const + self.du_wind / wind_adj + noct = 20.0 + (800.0 * self.alpha) / u_noct + + return dict(noct=noct, + module_efficiency=self.eta, + transmittance_absorptance=self.alpha) + + def use_sapm(self, a, b, wind_fit_low=1.4, wind_fit_high=5.4): + ''' + Use the SAPM model parameters to set the generic_model equivalents. + + In the SAPM the heat transfer coefficient increases exponentially + with windspeed, whereas in the other models the increase is linear. + This function equates the generic linear model to SAPM at two + specified winds speeds, thereby defining a linear approximation + for the exponential behavior. + + Parameters + ---------- + a, b : float + See :py:func:`pvlib.temperature.sapm_module` for details. + + wind_fit_low : float, optional + First wind speed value at which the generic linear model + must be equal to the SAPM model. [m/s] + + wind_fit_high : float, optional + Second wind speed value at which the generic linear model + must be equal to the SAPM model. [m/s] + + Notes + ----- + The two default wind speed values are based on measurements + at 10 m height. Both the SAPM model and the conversion + functions can work with wind speed data at different heights as + long as the same height is used consistently throughout. + ''' + u_low = 1.0 / np.exp(a + b * wind_fit_low) + u_high = 1.0 / np.exp(a + b * wind_fit_high) + + du_wind = (u_high - u_low) / (wind_fit_high - wind_fit_low) + u_const = u_low - du_wind * wind_fit_low + + net_absorptance = self.alpha - self.eta + self.u_const = u_const * net_absorptance + self.du_wind = du_wind * net_absorptance + + return self + + def to_sapm(self, wind_fit_low=1.4, wind_fit_high=5.4): + ''' + Convert the generic model parameters to SAPM model equivalents. + + In the SAPM the heat transfer coefficient increases exponentially + with windspeed, whereas in the other models the increase is linear. + This function equates SAPM to the generic linear model at two + specified winds speeds, thereby defining an exponential approximation + for the linear behavior. + + Parameters + ---------- + wind_fit_low : float, optional + First wind speed value at which the generic linear model + must be equal to the SAPM model. [m/s] + + wind_fit_high : float, optional + Second wind speed value at which the generic linear model + must be equal to the SAPM model. [m/s] + + Returns + ---------- + model_parameters : dict + See :py:func:`pvlib.temperature.sapm_module` for + model parameter details. + + Notes + ----- + The two default wind speed values are based on measurements + at 10 m height. Both the SAPM model and the conversion + functions can work with wind speed data at different heights as + long as the same height is used consistently throughout. + ''' + net_absorptance = self.alpha - self.eta + u_const = self.u_const / net_absorptance + du_wind = self.du_wind / net_absorptance + + u_low = u_const + du_wind * wind_fit_low + u_high = u_const + du_wind * wind_fit_high + + b = - ((np.log(u_high) - np.log(u_low)) / + (wind_fit_high - wind_fit_low)) + a = - (np.log(u_low) + b * wind_fit_low) + + return dict(a=a, b=b) diff --git a/pvlib/tests/test_temperature.py b/pvlib/tests/test_temperature.py index 5630f441e5..5e36714d12 100644 --- a/pvlib/tests/test_temperature.py +++ b/pvlib/tests/test_temperature.py @@ -363,3 +363,90 @@ def test_prilliman_nans(): # original implementation would return some values < 1 here expected = pd.Series(1., index=times) assert_series_equal(actual, expected) + + +def test_glm_conversions(): + # it is easiest and sufficient to test conversion from & to the same model + glm = temperature.GenericLinearModel(module_efficiency=0.1, + absorptance=0.9) + + inp = {'u0': 25.0, 'u1': 6.84} + glm.use_faiman(**inp) + out = glm.to_faiman() + for k, v in inp.items(): + assert np.isclose(out[k], v) + + inp = {'u_c': 25, 'u_v': 4} + glm.use_pvsyst(**inp) + out = glm.to_pvsyst() + for k, v in inp.items(): + assert np.isclose(out[k], v) + + # test with optional parameters + inp = {'u_c': 25, 'u_v': 4, + 'module_efficiency': 0.15, + 'alpha_absorption': 0.95} + glm.use_pvsyst(**inp) + out = glm.to_pvsyst() + for k, v in inp.items(): + assert np.isclose(out[k], v) + + inp = {'noct': 47} + glm.use_noct_sam(**inp) + out = glm.to_noct_sam() + for k, v in inp.items(): + assert np.isclose(out[k], v) + + # test with optional parameters + inp = {'noct': 47, + 'module_efficiency': 0.15, + 'transmittance_absorptance': 0.95} + glm.use_noct_sam(**inp) + out = glm.to_noct_sam() + for k, v in inp.items(): + assert np.isclose(out[k], v) + + inp = {'a': -3.5, 'b': -0.1} + glm.use_sapm(**inp) + out = glm.to_sapm() + for k, v in inp.items(): + assert np.isclose(out[k], v) + + +def test_glm_simulations(): + + glm = temperature.GenericLinearModel(module_efficiency=0.1, + absorptance=0.9) + wind = np.array([1.4, 1/.51, 5.4]) + weather = (800, 20, wind) + + inp = {'u0': 20.0, 'u1': 5.0} + glm.use_faiman(**inp) + out = glm(*weather) + expected = temperature.faiman(*weather, **inp) + assert np.allclose(out, expected) + + out = glm(*weather) + assert np.allclose(out, expected) + + out = glm(*weather, module_efficiency=0.1) + assert np.allclose(out, expected) + + inp = glm.get_generic_linear() + out = temperature.generic_linear(*weather, **inp) + assert np.allclose(out, expected) + + +def test_glm_repr(): + + glm = temperature.GenericLinearModel(module_efficiency=0.1, + absorptance=0.9) + inp = {'u0': 20.0, 'u1': 5.0} + glm.use_faiman(**inp) + expected = ("GenericLinearModel: {" + "'u_const': 16.0, " + "'du_wind': 4.0, " + "'eta': 0.1, " + "'alpha': 0.9}") + + assert glm.__repr__() == expected