-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Temperature model parameter translation code #1463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Open for a first round of comments! @jsstein @cwhanse @wholmgren @mikofski @kanderso-nrel @mtheristis |
Is the basic idea here that |
@cwhanse that's right, that's the basic idea. |
I'm pleased that my class design has survived the first three days of intense scrutiny! :) |
This is an interesting idea. As I commented in #1442 i think it might be helpful to start with explaining the different temperature models, how they differ, what they have in common, and why we need conversions. Regarding this code, I think starting with a smaller PR of just the core functions that convert PVsyst to Faiman and v.v. would be better. Then we can consider a class structure that delegates methods to the core functions. This combo of procedural & OOP paradigms is a defining principle of pvlib, expressed in the user guide also regarding |
I don't really see these as core simulation functions. In fact, temperature.py might not be the best location. An example script or notebook are other options. |
From a user point of view, is it practical to consider the public code to a function like
The neutral Class could operate behind the curtain. Maybe you have already considered and rejected that interface. |
I was thinking of something similar but in reverse. From a computing standpoint I always think from simple to complex. So the core functions would be explicit: def convert_temp_params(eta, absorptance, u0, u1):
“””Core function to convert temperature parameters”””
# do stuff
return new_params Then a class would delegate methods to the core functions: class GenericTempParams:
“””Class to convert temperature parameters”””
def convert(self, from, to, params : <dict>):
return convert_temp_params(self.eta, self.absorptance, **params) this allows users to bypass the oop and use the procedural directly |
Thank you for this discussion! I would say that I've certainly considered and experimented with options in those directions. To say that I rejected them is too strong, but over some time I developed a preference for the solution I proposed. I like having a user interface where the empirical coefficients explicitly named in the methods, which steered me away from a do-it-all function. But indeed it would be easy to add such a function that uses my class as @cwhanse suggests and I have nothing against that. To first write all the conversion calculations as stand-alone functions seemed to me like an extra layer of code with little benefit. There would either have to be n*(n-1) functions to do conversions with a single function call, or 2*n functions requiring two function calls to make a conversion. What led me to a class conceptually is the differentiation between the extra inputs efficiency and absorptance, which are properties of the PV module, vs. the empirical parameters. For potential users this class interface is really quite simple and clean it seems (to me). |
What do you think about creating a module |
I do not like abbreviations that aren't common. |
|
Well, the main question is whether to create a separate module. The name would be secondary. |
I'm -1 on a new module; I think it's fine to put this code in |
@adriesse thank you for proposing this interesting idea. My initial reactions were similar to @mikofski's:
>>> glm = GenericLinearModel(module_efficiency=0.19, absorptance=0.88)
>>> glm.faiman = 16, 8
>>> glm.faiman
{'u0': 16.0, 'u1': 8.0}
>>> glm.pvsyst
{'u_c': 12.24, 'u_v': 6.12, 'module_efficiency': 0.15, 'alpha_absorption': 0.9} Doesn't feel like a win to me. Also, if I understand correctly, this is more than just converting between different model parameters. The class also has a |
Here's a mockup of what I was thinking, let me know if it's clear: """
Tools for converting between generic linear temperature models (GLM).
GLM defines 4 basic parameters for linear temperature models:
* ``u_const``
* ``du_wind``
* ``alpha``
* ``eta``
The parameters for Faiman, PVsyst, and other models can then be generated from
the GLM basic parameters.
"""
from dataclasses import dataclass
# basic functions
def _calc_net_absorptance_glm(alpha, eta):
return alpha - eta
def _calc_net_absorptance_pvsyst(alpha, eta):
return alpha * (1.0 - eta)
def _calc_absorptance_ratio(alpha, eta):
net_absorptance_glm = _calc_net_absorptance_glm(alpha, eta)
net_absorptance_pvsyst = _calc_net_absorptance_pvsyst(alpha, eta)
return net_absorptance_glm / net_absorptance_pvsyst
# Faiman
def from_faiman_to_glm(u0, u1, alpha, eta):
"""
Convert Faiman params ``(u0, u1)`` to GLM.
Parameters
----------
u0 : float
Faiman natural convection h.t. coefficient
Returns
-------
u_const : float
GLM natural convection h.t. parameter
du_wind : float
GLM forced convection h.t. parameter
"""
net_absorptance = _calc_net_absorptance_glm(alpha, eta)
u_const = u0 * net_absorptance
du_wind = u1 * net_absorptance
return u_const, du_wind
def to_faiman_from_glm(alpha, eta, u_const, du_wind):
net_absorptance = _calc_net_absorptance_glm(alpha, eta)
u0 = u_const / net_absorptance
u1 = du_wind / net_absorptance
return u0, u1
# PVsyst
def from_pvsyst_to_glm(u_c, u_v, eta, alpha):
# XXX: note in PVsyst eta comes before alpha!
absorptance_ratio = _calc_absorptance_ratio(alpha, eta)
u_const = u_c * absorptance_ratio
du_wind = u_v * absorptance_ratio
return u_const, du_wind
def to_pvsyst_from_glm(alpha, eta, u_const, du_wind):
absorptance_ratio = _calc_absorptance_ratio(alpha, eta)
u_c = u_const / absorptance_ratio
u_v = du_wind / absorptance_ratio
return u_c, u_v
# generic class
@dataclass
class GenericLinearModel:
alpha: float
eta: float
u_const: float = 29.0
du_wind: float = 0.0
def __repr__(self):
return self.__class__.__name__ + ': ' +vars(self).__repr__()
@property
def faiman(self):
"""Get Faiman model params matching GLM"""
u0, u1 = to_faiman_from_glm(
self.alpha, self.eta, self.u_const, self.du_wind)
params = {'u0': u0, 'u1': u1}
return params
@faiman.setter
def faiman(self, params):
"""
Set the generic model parms to match Faiman.
"""
# XXX: Will override existing GLM parameters!
u0 = params['u0']
u1 = params['u1']
u_const, du_wind = from_faiman_to_glm(u0, u1, self.alpha, self.eta)
self.u_const = u_const
self.du_wind = du_wind
@property
def pvsyst(self):
"""Get pvsyst model params matching GLM"""
u_c, u_v = to_pvsyst_from_glm(
self.alpha, self.eta, self.u_const, self.du_wind)
params = {
'u_c': u_c, 'u_v': u_v,
'module_efficiency': self.eta, 'alpha_absorption': self.alpha}
return params
@pvsyst.setter
def pvsyst(self, params):
"""
Set the generic model parms to match pvsyst.
"""
u_c = params['u_c']
u_v = params['u_v']
module_efficiency = params.get('module_efficiency')
alpha_absorption = params.get('alpha_absorption')
# XXX: Will override existing eta and alpha in GLM class!
if module_efficiency is not None:
self.eta = module_efficiency
if alpha_absorption is not None:
self.alpha = alpha_absorption
u_const, du_wind = from_pvsyst_to_glm(u_c, u_v, self.eta, self.alpha)
self.u_const = u_const
self.du_wind = du_wind
# tests
if __name__ == '__main__':
print('\nTesting GLM: create GLM(eta=0.19, alpha=0.88)')
glm = GenericLinearModel(eta=0.19, alpha=0.88)
print('\n\tGLM: %r' % glm)
print('\nTest setting Faiman {u0: 16, u1: 8}')
glm.faiman = {'u0': 16, 'u1': 8}
print('\n\tGLM: %r' % glm)
print('\n\tFaiman: %r' % glm.faiman)
print('\n\tPVsyst: %r' % glm.pvsyst)
print('\nTest setting PVsyst {u0: 29, u1: 0}')
glm.pvsyst = {'u_c': 29, 'u_v': 0}
print('\n\tGLM: %r' % glm)
print('\n\tFaiman: %r' % glm.faiman)
print('\n\tPVsyst: %r' % glm.pvsyst)
print('\nTest setting PVsyst {u0: 25, u1: 1.2, module_efficiency: 0.1, alpha_absorption: 0.9}')
glm.pvsyst = {
'u_c': 25, 'u_v': 1.2,
'module_efficiency': 0.1, 'alpha_absorption': 0.9}
print('\n\tGLM: %r' % glm)
print('\n\tFaiman: %r' % glm.faiman)
print('\n\tPVsyst: %r' % glm.pvsyst) this yields the following output:
Is this what you were expecting? |
Naming nitpick: |
@mikofski I like those functions. I'm less sure about the API for the setters... looks pretty complicated for a setter. Another thought is a frozen data class with constructors for each model. |
One thing I realized while mocking this is that the PR itself can be split into smaller chunks that are easier to review and blame:
Python property setters only take a single argument, so this was a compromise to use a dictionary, but I think a # in GLTM class
@pvsyst.setter
def pvsyst(self, klass):
u_c = klass.u_c
u_v = klass.u_v
# same as before
# no return
@dataclass
class PVsystTemperatureModel:
u_c: float = 29
u_v: float = 0
module_efficiency: float = 0.1
alpha_absorptivity: float = 0.9
# test
pvsyst = PVsystTemperatureModel(u_c=25, u_v=1.2)
gltm = GeneralLinearTemperatureModel(alpha=0.9, eta=0.1)
gltm.pvsyst = pvsyst # <-- setter uses dataclass instead of dict
gltm.other_temp_model_like_faiman
# {u0: 30.86, u1: 1.48}
I was thinking GLTM? |
I don't know what the eyes emoji means to you but to me it means I'm looking at all this stuff. |
note I think the dictionary is pretty robust against errors: >>> glm = GenericLinearModel(eta=0.16, alpha=0.89)
>>> glm
# GenericLinearModel: {'alpha': 0.89, 'eta': 0.16, 'u_const': 29.0, 'du_wind': 0.0}
>>> glm.pvsyst = {'uc': 25}
# ---------------------------------------------------------------------------
# KeyError Traceback (most recent call last)
# <ipython-input-14-b75df7ef15c2> in <module>
# ----> 1 glm.pvsyst = {'uc': 25}
# ~\Projects\pvl_temperature_glm.py in pvsyst(self, params)
# 119 Set the generic model parms to match pvsyst.
# 120 """
# --> 121 u_c = params['u_c']
# 122 u_v = params['u_v']
# 123 module_efficiency = params.get('module_efficiency')
# KeyError: 'u_c'
>>> glm.pvsyst = {'u_c': 25}
# ---------------------------------------------------------------------------
# KeyError Traceback (most recent call last)
# <ipython-input-15-c501e2033ccd> in <module>
# ----> 1 glm.pvsyst = {'u_c': 25}
# ~\Projects\pvl_temperature_glm.py in pvsyst(self, params)
# 120 """
# 121 u_c = params['u_c']
# --> 122 u_v = params['u_v']
# 123 module_efficiency = params.get('module_efficiency')
# 124 alpha_absorption = params.get('alpha_absorption')
# KeyError: 'u_v' |
I had a funny feeling that using "set*" and "get*" might trigger an association with synonymous python constructs. I had previously used "from*" and "to*" and perhaps I should have stuck to them. I can figure out how everything above works, but I'm just not sure how to evaluate what's better. One aspect is the user interface, how people will interact using a program, script or command line. The other aspect is code clarity, how people will understand it the code when reading it. To pick up on the robustness illustration, I prefer the error message below to the key error above:
|
Also, the |
I think the module temp calculate using
I think what's easiest for the developers. The only way to reckon what the user wants is by deploying and listening. But ultimately we and our decendents will have to maintain this soup. We need to make sure it's readable. I hesitate to even use decorators, but hard to know what is "too clever". When in doubt I try to be pythonic and use the batteries that Python comes with. |
I think the developers will be able to deal with much greater complexity than the average pvlib user, so the latter should be given some consideration even if we don't have very clear description of what such a user is like. I personally don't have a lot of experience with decorators so I still find that they make code harder to read and understand. I have less trouble with "dunder" methods (learned a new word today!) but I agree it would be clearer to give this one a more meaningful name than |
I'm not quite sure how to move toward a consensus here. I think perhaps the key to gaining acceptance for this particular class design is to really position it as an auxiliary tool, and to that end I have added a separate generic_linear() module temperature calculation function that fit the pattern of the existing ones. |
I comment not because I feel strongly or think my input is particularly useful here but because feedback was specifically requested :) Using a class seems a bit unnecessarily heavy to me; I think of classes as being useful for objects that carry around state throughout a chunk of code, but here it seems that the stored state ( I think it might be more idiomatic to use functions like @mikofski's #1463 (comment), although instead of wrapping them in (data)classes, I think I'd prefer @cwhanse's suggested interface #1463 (comment). Anyway all that said I don't hate the current approach. It has the benefit of not requiring repeated
This is an interesting point that I'm not sure anyone has explicitly responded to, but I don't have any thoughts beyond a general agreement that a gallery example could be a good alternative to new code in pvlib itself. |
I'll be happy to go with the majority on difficult challenge of setting a suitable name. I did not include "Temperature" in the name because none of the other functions in the "temperature" module do. It is important to include "Linear" or something similar because better and more generic temperature models are certainly feasible and hopefully forthcoming. Suggestions thus far are:
|
I vote For my edification, why the emphasis on "linear"? None of the models are linear at first glance, e.g., Sandia's is an exponential with a linear equation for the exponent, Faiman model is a rational function. |
Good question. It is much easier to see if you rearrange the equations as energy balances. The main linearity is the heat flow being proportional to the temperature difference Tm - Ta, which the SAPM model also has. exp(a) is just a constant after all. Secondary is the linear influence of wind on heat transfer coefficient in three of the models, which can be adjusted to fit the non-linear wind influence in the SAPM model adequately. |
Thanks for the explanation. The conductive heat transfer, proportional to the difference of 4th powers, has been linearized, I like the "linear" term better than that sentence. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpicking the sphinx docs. This also needs an entry in the 0.9.3 what's new file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approving the overall implementation. I still think users would appreciate a function like temperature.convert(from="sapm", to="faiman")
that handles the class methods for them. But let's get this capability in place and add something like that later.
The radiative heat transfer, proportional to the difference of 4th powers.... Convection is also non-linear though. |
Thanks very much for your helpful reviews @cwhanse and @kanderso-nrel ! That should get us pretty close to the finish line. |
I don't have an opinion here, but in case @cwhanse missed it, the class name as currently implemented is Other than that and a 0.9.3 what's new entry, this looks ready to me. @pvlib/pvlib-maintainer does anyone else plan to review this PR? |
Sorry, my list was inaccurate! Do we need a recount on the votes? |
|
With nobody else expressing a desire to chime in before this is merged, I'll go ahead and merge this one too. Thanks @adriesse for the |
Thank you all so much for your participation! Patience was exercised by all, I believe. |
docs/sphinx/source/reference
for API changes.docs/sphinx/source/whatsnew
for all changes. Includes link to the GitHub Issue with:issue:`num`
or this Pull Request with:pull:`num`
. Includes contributor name and/or GitHub username (link with:ghuser:`user`
).