Skip to content

Adds k parameter to temperature.ross #2521

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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/sphinx/source/whatsnew/v0.13.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Bug fixes

Enhancements
~~~~~~~~~~~~
* Rename parameter name ``aparent_azimuth`` to ``solar_azimuth`` in :py:func:`~pvlib.tracking.singleaxis`.
(:issue:`2479`, :pull:`2480`)
* Add k coefficient in :py:func:`~pvlib.temperature.ross`
(:issue:`2506`, :pull:`2521`)
* Add :py:func:`pvlib.iotools.get_nasa_power` to retrieve data from NASA POWER free API.
(:pull:`2500`)
* :py:func:`pvlib.spectrum.spectral_factor_firstsolar` no longer emits warnings
Expand Down Expand Up @@ -54,4 +58,5 @@ Contributors
* Ioannis Sifnaios (:ghuser:`IoannisSifnaios`)
* Rajiv Daxini (:ghuser:`RDaxini`)
* Omar Bahamida (:ghuser:`OmarBahamida`)
* Rodrigo Amaro e Silva (:ghuser:`ramaroesilva`)
* Kevin Anderson (:ghuser:`kandersolar`)
54 changes: 50 additions & 4 deletions pvlib/temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ def faiman_rad(poa_global, temp_air, wind_speed=1.0, ir_down=None,
return temp_air + temp_difference


def ross(poa_global, temp_air, noct):
def ross(poa_global, temp_air, noct=None, k=None):
r'''
Calculate cell temperature using the Ross model.

Expand All @@ -638,6 +638,9 @@ def ross(poa_global, temp_air, noct):
noct : numeric
Nominal operating cell temperature [C], determined at conditions of
800 W/m^2 irradiance, 20 C ambient air temperature and 1 m/s wind.
k: numeric
Ross coefficient [Km^2/W], which is an alternative to employing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Ross coefficient [Km^2/W], which is an alternative to employing
Ross coefficient [Km²W⁻¹], which is an alternative to employing

Copy link
Member

@cwhanse cwhanse Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last comment for me: We should mark both noct and k as optional. I can't suggest for a line that isn't being modified, but it would be like this:

    noct: numeric, optional
        Nominal operating cell temperature [C], determined at conditions of
        800 W/m^2 irradiance, 20 C ambient air temperature and 1 m/s wind.
        If ``noct`` is not provided, ``k`` is required.

And similar for k

The double ticks "``" format the enclosed string as a parameter.

NOCT in Ross's equation.

Returns
-------
Expand All @@ -650,19 +653,62 @@ def ross(poa_global, temp_air, noct):

.. math::

T_{C} = T_{a} + \frac{NOCT - 20}{80} S
T_{C} = T_{a} + \frac{NOCT - 20}{80} S = T_{a} + k × S

where :math:`S` is the plane of array irradiance in :math:`mW/{cm}^2`.
This function expects irradiance in :math:`W/m^2`.
Comment on lines 658 to 659
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let others comment, but I think Wm⁻² looks cleaner than the larger font bolt+italic math type for units. Might be worth saving that for variables(?)
At least if sticking with :math: then I think superscript would be better than a /: `:math:`mW{cm}^{-2}

Suggested change
where :math:`S` is the plane of array irradiance in :math:`mW/{cm}^2`.
This function expects irradiance in :math:`W/m^2`.
where :math:`S` is the plane of array irradiance in mWm⁻².
This function expects irradiance in Wm⁻².


Representative values for k are provided in [2]_, covering different types
of mounting and degrees of back ventialtion. The naming designations,
however, are adapted from [3]_ to enhance clarity and usability.

+--------------------------------------+-----------+
| Mounting | :math:`k` |
+======================================+===========+
| Sloped roof, well ventilated | 0.02 |
+--------------------------------------+-----------+
| Free-standing system | 0.0208 |
+--------------------------------------+-----------+
| Flat roof, well ventilated | 0.026 |
+--------------------------------------+-----------+
| Sloped roof, poorly ventilated | 0.0342 |
+--------------------------------------+-----------+
| Facade integrated, semi-ventilated | 0.0455 |
+--------------------------------------+-----------+
| Facade integrated, poorly ventilted | 0.0538 |
+--------------------------------------+-----------+
| Sloped roof, non-ventilated | 0.0563 |
+--------------------------------------+-----------+

It is also worth noting that the semi-ventilated facade case refers to
partly transparent compound glass insulation modules, while the non-
ventilated case corresponds to opaque, insulated PV-cladding elements.
However, the emphasis in [3]_ appears to be on ventilation conditions
rather than module construction.

References
----------
.. [1] Ross, R. G. Jr., (1981). "Design Techniques for Flat-Plate
Photovoltaic Arrays". 15th IEEE Photovoltaic Specialist Conference,
Orlando, FL.
.. [2] E. Skoplaki and J. A. Palyvos, “Operating temperature of
photovoltaic modules: A survey of pertinent correlations,” Renewable
Energy, vol. 34, no. 1, pp. 23–29, Jan. 2009,
:doi:`10.1016/j.renene.2008.04.009`
.. [3] T. Nordmann and L. Clavadetscher, “Understanding temperature
effects on PV system performance," Proceedings of 3rd World Conference
on Photovoltaic Energy Conversion, May 2003.
'''
# factor of 0.1 converts irradiance from W/m2 to mW/cm2
return temp_air + (noct - 20.) / 80. * poa_global * 0.1
if (noct is None) & (k is None):
raise ValueError("Either noct or k need is required.")
elif (noct is not None) & (k is not None):
raise ValueError("Provide only one of noct or k, not both.")
elif k is None:
# factor of 0.1 converts irradiance from W/m2 to mW/cm2
return temp_air + (noct - 20.) / 80. * poa_global * 0.1
elif noct is None:
# k assumes irradiance in W.m-2, dismissing 0.1 factor
return temp_air + k * poa_global


def _fuentes_hconv(tave, windmod, tinoct, temp_delta, xlen, tilt,
Expand Down
44 changes: 39 additions & 5 deletions tests/test_temperature.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,45 @@ def test_faiman_rad_ir():


def test_ross():
result = temperature.ross(np.array([1000., 600., 1000.]),
np.array([20., 40., 60.]),
np.array([40., 100., 20.]))
expected = np.array([45., 100., 60.])
assert_allclose(expected, result)
# single values
result1 = temperature.ross(1000., 30., noct=50)
result2 = temperature.ross(1000., 30., k=0.0375)

expected = 67.5
assert_allclose(expected, result1)
assert_allclose(expected, result2)

# pd.Series
times = pd.date_range('2025-07-30 14:00', '2025-07-30 16:00', freq='h')

df = pd.DataFrame({'t_air': np.array([20., 30., 40.]),
'ghi': np.array([800., 700., 600.])},
index=times)

result1 = temperature.ross(df['ghi'], df['t_air'], noct=50.)
result2 = temperature.ross(df['ghi'], df['t_air'], k=0.0375)

expected = pd.Series([50., 56.25, 62.5], index=times)
assert_allclose(expected, result1)
assert_allclose(expected, result2)

# np.array
ghi_array = df['ghi'].values
t_air_array = df['t_air'].values

result1 = temperature.ross(ghi_array, t_air_array, noct=50.)
result2 = temperature.ross(ghi_array, t_air_array, k=0.0375)

expected = expected.values
assert_allclose(expected, result1)
assert_allclose(expected, result2)
Copy link
Member

@cwhanse cwhanse Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for the errors, to raise the code coverage.

Suggested change
assert_allclose(expected, result2)
assert_allclose(expected, result2)
def test_noct_sam_errors():
with pytest.raises(ValueError, match='Either noct or k need is required'):
temperature.ross(1000., 30.)
with pytest.raises(ValueError, match='Provide only one of noct or k'):
temperature.ross(1000., 30., noct=45., k=0.02)



def test_ross_errors():
with pytest.raises(ValueError, match='Either noct or k need is required'):
temperature.ross(1000., 30.)
with pytest.raises(ValueError, match='Provide only one of noct or k'):
temperature.ross(1000., 30., noct=45., k=0.02)


def test_faiman_series():
Expand Down
Loading