Skip to content
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

Hayes temp model #1083

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft

Conversation

stephenjkaplan
Copy link

@stephenjkaplan stephenjkaplan commented Oct 16, 2020

  • Closes Hayes Time-Dependent Module Temperature Model should be added to temperature API.  #1080
  • I am familiar with the contributing guidelines
  • Tests added Note from Stephen: there are some placeholder unit tests to be updated after model is validated
  • Updates entries to docs/sphinx/source/api.rst for API changes.
  • Adds description and name entries in the appropriate "what's new" file in 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`). Note from Stephen: waiting on feature to be added to a particular release
  • New code is fully documented. Includes numpydoc compliant docstrings, examples, and comments where necessary.
  • Pull request is nearly complete and ready for detailed review.
  • Maintainer: Appropriate GitHub Labels and Milestone are assigned to the Pull Request and linked Issue.

@stephenjkaplan
Copy link
Author

stephenjkaplan commented Oct 16, 2020

I pushed up this code and submitted a draft PR before full completion because I have a number of questions/bugs that need to be addressed:

  • The outputs are ridiculous. The temperatures increase exponentially and basically hit a singularity. There seems to be a feedback loop where t_mod_delta calculated for each step is too large. Therefore, on the following step, the difference between t_mod_i and temp_air[i] is far too large. I can't get to the source of the feedback loop. You'd of course expect the module temperature to move back towards equilibrium with ambient temperature at night, but this is clearly not happening.
  • The paper presents module heat capacity as a specific heat capacity in units [J/ kg-K]. Nowhere in the main equation in the paper does it show module weight as an input, but it seems that based on the units you would need that as an input. For now I have it in there but looking for a gut check on this.
  • Similarly, the paper indicates that short wave radiative heat transfer is equal to effective POA. I am assuming that you have to multiply the POA by module area to get them in the same units. That being said, when I don't multiply by the module area, I get better results overall. Bit thrown off by that.
  • In the paper, equation (5) indicates that you'd need emissivities for both objects under consideration (module and sky, module and ground, etc). However the paper only suggests values for sky and ground emissivity, not module. I took that to mean that I should use the same emissivity for both objects, but that doesn't seem right.
  • On a similar note, the paper indicates that you should assume ground temperature equals module temperature. However, this would make that term in the long wave radiation calculation negligible. Seems odd.

I am currently testing the functionality with the following script as I work through it. I am using 5 minute PSM v3 data because the model is best suited for higher resolution inputs. I'm also assuming a fixed tilt array with 30 degree tilt, and using some module metadata from a First Solar Series 6 datasheet:

from pvlib import location
from pvlib.temperature import hayes, sapm_module, TEMPERATURE_MODEL_PARAMETERS
from pvlib.iotools import read_psm3
from pvlib.irradiance import get_total_irradiance, aoi
from pvlib.iam import ashrae

import matplotlib.pyplot as plt

data = read_psm3('test_psm3_5min.csv')
module_tilt = 30
module_azimuth = 180
site = location.Location(latitude=data[0]['Latitude'], longitude=data[0]['Longitude'], tz='MST')
solar_position = site.get_solarposition(times=data[1].index)
poa_global = get_total_irradiance(
    surface_tilt=module_tilt,
    surface_azimuth=module_azimuth,
    dni=data[1]['DNI'],
    ghi=data[1]['GHI'],
    dhi=data[1]['DHI'],
    solar_zenith=solar_position['apparent_zenith'],
    solar_azimuth=solar_position['azimuth']
)['poa_global']
temp_air = data[1]['Temperature']
wind_speed = data[1]['Wind Speed']

# 1. Calculate module temp with new model
aoi = aoi(module_tilt, module_azimuth, solar_position['zenith'], solar_position['azimuth'])
poa_effective = poa_global.multiply(ashrae(aoi))
module_efficiency = 0.176
module_area = 2.47 # m^2
module_weight = 34.5
tmod_hayes = hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area, module_weight, module_tilt)

# 2. Calculate module temp with existing model for comparison

# assume glass-glass with open rack since Hayes model was validated for CdTe
temp_model_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
tmod_sapm = sapm_module(poa_global, temp_air, wind_speed, temp_model_params['a'], temp_model_params['b'])

plt.plot(tmod_sapm[0:3000])
plt.plot(tmod_hayes[0:3000])
plt.ylim((-50, 100))
plt.legend(['SAPM', 'Hayes'])
plt.show()

JPV Accepted_A Time Dependent Model for CdTe PV Module Temperature in Utility Scale Systems_Grayscale.pdf

@kandersolar
Copy link
Member

The outputs are ridiculous. The temperatures increase exponentially and basically hit a singularity. There seems to be a feedback loop where t_mod_delta calculated for each step is too large.

The temperature change equation t_mod_delta = (dt / mod_heat_capacity) * (1/module_weight) * total_heat_transfer implies that positive values of total_heat_transfer mean more energy flowing into the module than out of it. However I think some of the individual heat terms are calculated with the wrong sign, causing the blow-up:

  • The currently implemented radiative heat calculation q = const * (T1**4 - T2**4) is positive when T1 > T2, meaning it represents the flow out of the module, whereas total_heat_transfer is the total flow into the module.
  • The currently implemented convection calculation is the same -- it is positive when tmod > tair, so again it's the flow out of the module.

I didn't check that inverting the sign of q_long_wave_radiation and q_convection fixes the blow-up, but at least it's a place to start. Also careful about degrees vs radians with math.cos.

@stephenjkaplan
Copy link
Author

stephenjkaplan commented Oct 17, 2020

@kanderso-nrel extremely helpful, I'll look into all of that, thanks.

update: reasoned through which heat transfer would be positive and negative for the module, and the temperature trend started to look a lot more normal. converted degrees to radians also improved things. thanks again.

@stephenjkaplan
Copy link
Author

stephenjkaplan commented Oct 17, 2020

Gut check against SAPM temp model as of 10/17. Definitely a lot of work to be done, although it seems like there is a relatively constant offset, and the signal noise matches somewhat closely, so that is a good sign.

Screen Shot 2020-10-17 at 10 21 59 AM

when I simply use POAeff for short wave radiation instead of multiplying it by module area to cancel out m^2, it becomes even closer (still with an offset)

Screen Shot 2020-10-18 at 11 44 15 PM

@cwhanse
Copy link
Member

cwhanse commented Oct 19, 2020

Gut check against SAPM temp model as of 10/17.

I would expect the dynamic model result to appear damped and lagged, compared with SAPM.

@kandersolar
Copy link
Member

@stephenjkaplan For the module_area*poa_effective question, could it be it a matter of different module sizes for Series 4 (or 3) vs Series 6 (0.72 vs 2.47 m^2)? From Table 1 in the paper, the data they used was all <=2012, so maybe using Series 6 module parameters is the issue. It would be interesting to see your plots regenerated with module_area=0.72, and I supposed updated module_weight and module_efficiency values as well.

For the offset from SAPM -- it is to be expected at least at night because the SAPM model just sets Tcell=Tamb at night (i.e. no radiative cooling to the sky). The magnitude of the night offset looks rather larger than I'd expect, but maybe that would change if the module parameters are updated.

@kandersolar
Copy link
Member

Following up on the above speculation, using Series 3/4 parameters results in much better agreement with the SAPM model:

image

Click to show source code (225 lines)
import pandas as pd
import math
import numpy as np
from pandas.tseries.frequencies import to_offset


def _calculate_radiative_heat(module_area, view_factor, emissivity,
                              temperature1, temperature2):
    """
    Calculate radiative heat transfer between two objects.
    Parameters
    ----------
    module_area : float
        Front-side surface area of PV module [m^2]
    view_factor : float
        View factor of object 1 with respect to object 2 [unitless]
    emissivity : float
        # TODO there are probably 2 of these values
        Thermal emissivity [unitless]. Must be between 0 and 1.
    temperature1 : float
        Temperature of object 1 [K]
    temperature2 : float
        Temperature of object 2 [K]
    Returns
    -------
    q : float
        Radiative heat transfer between object 1 and object 2 [W]
    """
    # Stefan-Boltzmann constant
    sigma = 5.670374419E-8      # W m^-2 K^-4
    q = sigma * module_area * view_factor * emissivity * \
        (temperature1 ** 4 - temperature2 ** 4)

    return q


def hayes(poa_effective, temp_air, wind_speed, module_efficiency, module_area,
          module_weight, module_tilt, mod_heat_capacity=840, t_mod_init=None,
          emissivity_sky=0.95, emissivity_ground=0.85, k_c=12.7, k_v=2.0,
          wind_sensor_height=2.5, z0=0.25):
    """
    Calculate module temperature at sub-hourly resolution for fixed tilt
    systems per the Hayes model.
    The Hayes model [1]_ enables more accurate modeling of module temperature
    at time scales less than one hour by introducing a time dependency based
    on module heat capacity. The model can only be used for fixed tilt
    systems. Additionally, it has only been validated using data from
    utility-scale PV systems with CdTe modules. It is more accurate for
    time intervals less than 5 minutes.
    Parameters
    ----------
    poa_effective : pandas Series
        Total incident irradiance adjusted for optical (IAM) losses [W/m^2]
    temp_air : pandas Series
        Ambient dry bulb temperature [C]
    wind_speed : pandas Series
        Wind speed [m/s]
    module_efficiency : float
        PV module efficiency [decimal]
    module_area : float
        Front-side surface area of PV module [m^2]
    module_weight : float
        Weight of PV module [kg]
    module_tilt : float
        Tilt angle of fixed tilt array [deg]
    mod_heat_capacity : float, default 840
        Specific heat capacity of PV module [J / kg-K].
    t_mod_init : float, default None
        Initial condition for module temperature [C]. If left as default,
        will be set to first value in temp_air (based on the assumption
        that if the first timestamp is in the middle of the night, the
        module would be in steady-state equilibrium with the environment
    emissivity_sky : float, default 0.95
        Thermal emissivity of sky [unitless]. Must be between 0 and 1.
    emissivity_ground : float, default 0.85
        Thermal emissivity of ground [unitless]. Default value is suggested
        value for sand. Suggested value for grass is 0.9. Must be between
        0 and 1.
    k_c : float, default 12.7
        Free convective heat coefficient. Defaults to value for "hot"
        climates (climates closest to Koppen-Geiger Dry B and Temperate C
        zones). Suggested value for "temperate" climates (climates closest
        to Koppen-Geiger Cold D zones) is 16.5
    k_v : float, default 2.0
        Forced convective heat coefficient. Defaults to value for "hot"
        climates (climates closest to Koppen-Geiger Dry B and Temperate C
        zones). Suggested value for "temperate" climates (climates closest
        to Koppen-Geiger Cold D zones) 3.2
    wind_sensor_height : float, default 2.5
        Height of wind sensor used to measure wind_speed [m]
    z0 : float, default 0.25
        Davenport-Wieringa roughness length [m]. Default value chosen in
        white-paper to minimize error.
    Returns
    -------
    tmod : pandas Series
        The modeled module temperature [C]
    References
    ----------
    .. [1] W. Hayes and L. Ngan, "A Time-Dependent Model for CdTe PV Module
           Temperature in Utility-Scale Systems," in IEEE Journal of
           Photovoltaics, vol. 5, no. 1, pp. 238-242, Jan. 2015, doi:
           10.1109/JPHOTOV.2014.2361653.
    """
    # ensure that time series inputs are all of the same length
    if not (len(poa_effective) == len(temp_air) and
            len(temp_air) == len(wind_speed)):

        raise ValueError('poa_effective, temp_air, and wind_speed must all be'
                         ' pandas Series of the same size.')

    # infer the time resolution from the inputted time series.
    # first get pandas frequency alias, then convert to seconds
    freq = pd.infer_freq(poa_effective.index)
    dt = pd.to_timedelta(to_offset(freq)).seconds

    # radiation (from sun)
    q_short_wave_radiation = module_area * poa_effective

    # converted electrical energy
    p_out = module_efficiency * module_area * poa_effective

    # adjust wind speed if sensor height not at 2.5 meters
    wind_speed_adj = wind_speed * (math.log(2.5 / z0) /
                                   math.log(wind_sensor_height / z0))

    # calculate view factors (simplified calculations)
    view_factor_mod_sky = (1 + math.cos(math.radians(module_tilt))) / 2
    view_factor_mod_ground = (1 - math.cos(math.radians(module_tilt))) / 2

    t_mod = np.zeros_like(poa_effective)
    t_mod_i = (t_mod_init if t_mod_init is not None else temp_air[0]) + 273.15
    t_mod[0] = t_mod_i
    # calculate successive module temperatures for each time stamp
    for i in range(len(t_mod) - 1):
        # calculate long wave radiation (radiative interactions between module
        # and objects within Earth's atmosphere)
        t_sky = temp_air[i] - 20 + 273.15
        q_mod_sky = _calculate_radiative_heat(
            module_area=module_area,
            view_factor=view_factor_mod_sky,
            emissivity=emissivity_sky,
            temperature1=t_mod_i,
            temperature2=t_sky
        )
        # TODO paper indicates temps equal, but that yields zero q
        q_mod_ground = _calculate_radiative_heat(
            module_area=module_area,
            view_factor=view_factor_mod_ground,
            emissivity=emissivity_ground,
            temperature1=t_mod_i,
            temperature2=t_mod_i
        )
        q_mod_mod = 0   # current assumption is that it is negligible
        q_long_wave_radiation = -1*(q_mod_sky + q_mod_ground + q_mod_mod)

        # calculation convective heat transfer
        q_convection = (k_c + k_v * wind_speed_adj[i]) * \
                       ((t_mod_i - 273.15) - temp_air[i])

        # calculate delta in module temp, add to the current module temp
        total_heat_transfer = (q_long_wave_radiation +
                               q_short_wave_radiation[i] -
                               q_convection - p_out[i])
        t_mod_delta = (dt / mod_heat_capacity) * \
                      (1/module_weight) * total_heat_transfer

        t_mod_i += t_mod_delta
        t_mod[i + 1] = t_mod_i

    return pd.Series(t_mod - 273.15, index=poa_effective.index, name='tmod')

# %%

import pvlib
from pvlib import location
from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
from pvlib.iam import ashrae

import matplotlib.pyplot as plt

headers, data = pvlib.iotools.get_psm3(40, -80, 'DEMO_KEY', '[email protected]', names='2019', interval=5)
data = data.iloc[:1500]

# %%

module_tilt = 30
module_azimuth = 180
site = location.Location(latitude=headers['Latitude'], longitude=headers['Longitude'])
solar_position = site.get_solarposition(times=data.index)
poa_global = pvlib.irradiance.get_total_irradiance(
    surface_tilt=module_tilt,
    surface_azimuth=module_azimuth,
    dni=data['DNI'],
    ghi=data['GHI'],
    dhi=data['DHI'],
    solar_zenith=solar_position['apparent_zenith'],
    solar_azimuth=solar_position['azimuth']
)['poa_global']
temp_air = data['Temperature']
wind_speed = data['Wind Speed']

# 1. Calculate module temp with new model

aoi = pvlib.irradiance.aoi(module_tilt, module_azimuth, solar_position['zenith'], solar_position['azimuth'])
poa_effective = poa_global.multiply(ashrae(aoi))
cases = pd.DataFrame({
    'FSLR Series 3': [0.72, 0.125, 12],  # https://www.firstsolar.com/-/media/First-Solar/Project-Documents/PD-5-401-03_Series3Black-4_NA.ashx
    'FSLR Series 4': [0.72, 0.160, 12],  # https://www.firstsolar.com/-/media/First-Solar/Technical-Documents/Series-4-Datasheets/Series-4V3-Module-Datasheet.ashx
    'FLSR Series 6': [2.47, 0.176, 34.5],  # https://www.firstsolar.com/-/media/First-Solar/Technical-Documents/Series-6-Datasheets/Series-6-Datasheet.ashx
}, index=['module_area', 'module_efficiency', 'module_weight']).T

for name, params in cases.iterrows():
    tmod_hayes = hayes(poa_effective, temp_air, wind_speed, module_tilt=module_tilt, **params)
    tmod_hayes.plot(label=name)

# 2. Calculate module temp with existing model for comparison

# assume glass-glass with open rack since Hayes model was validated for CdTe
temp_model_params = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
tmod_sapm = pvlib.temperature.sapm_module(poa_global, temp_air, wind_speed, temp_model_params['a'], temp_model_params['b'])
tmod_sapm.plot(label='SAPM')

plt.legend()

Note that I used q_short_wave_radiation = module_area * poa_effective to generate that plot. Otherwise no changes from
2399607.

@stephenjkaplan do you plan on picking this PR up again? If not, and other maintainers are willing to review, I'll volunteer to finish it.

@kandersolar
Copy link
Member

OK I merged master into this branch and did a bit of cleanup. Still need to write tests. @stephenjkaplan I hope you don't mind me pushing to your branch for continuity -- feel free to revert my commits if you want to take over again, otherwise I'll continue on.

@stephenjkaplan
Copy link
Author

@kanderso-nrel please continue on! I haven't been able to revisit this so I'm happy to see someone follow through.

@kandersolar
Copy link
Member

I realized I never actually came back to this... looks like unit tests are the main missing piece of this PR. @cwhanse in #1080 you suggested implementing the model in Matlab and comparing results -- is that how we should proceed here?

@cwhanse
Copy link
Member

cwhanse commented Dec 10, 2021

I realized I never actually came back to this... looks like unit tests are the main missing piece of this PR. @cwhanse in #1080 you suggested implementing the model in Matlab and comparing results -- is that how we should proceed here?

I think so. I haven't done it and don't think anyone else at Sandia has. In #717 there will be functions for computing view factors. I haven't factored them into pvlib.bifacial.util but that could be done.

@kandersolar kandersolar added this to the 0.9.4 milestone Sep 28, 2022
@kandersolar
Copy link
Member

FWIW here's a quick comparison with measured 1-minute Tmod data from the NIST ground array, plus the other transient models, showing that this Hayes implementation is right in there with the other models and the actual measurements:

image

Disclaimer: the measurements aren't from a FSLR system, I used all default parameters for the models, yadda yadda.

I haven't done it and don't think anyone else at Sandia has.

I wonder if someone at Sandia could be convinced to do it? I suppose I could download octave and make one myself, but my MATLAB knowledge is seriously rusty. It also seems less useful as a validation if the two implementations are from one person.

@cwhanse
Copy link
Member

cwhanse commented Sep 28, 2022

I wonder if someone at Sandia could be convinced to do it?

Let me see what can be done.

@kandersolar kandersolar mentioned this pull request Dec 9, 2022
9 tasks
@kandersolar kandersolar modified the milestones: 0.9.4, 0.9.5 Dec 14, 2022
@kandersolar kandersolar modified the milestones: 0.9.5, 0.9.6 Mar 18, 2023
@kandersolar kandersolar modified the milestones: 0.9.6, 0.10.0, Someday May 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Hayes Time-Dependent Module Temperature Model should be added to temperature API.
3 participants