diff --git a/docs/sphinx/source/user_guide/extras/nomenclature.rst b/docs/sphinx/source/user_guide/extras/nomenclature.rst index 4c983dc912..b4922ee2e2 100644 --- a/docs/sphinx/source/user_guide/extras/nomenclature.rst +++ b/docs/sphinx/source/user_guide/extras/nomenclature.rst @@ -22,16 +22,23 @@ There is a convention on consistent variable names throughout the library: aoi Angle of incidence. Angle between the surface normal vector and the - vector pointing towards the sun’s center + vector pointing towards the sun's center. Must be >=0 and <=180 degrees. + When the sun is behind the surface, the value is >90 degrees. aoi_projection - cos(aoi) + cos(aoi). When the sun is behind the surface, the value is negative. + For many uses, negative values must be set to zero. ape Average photon energy apparent_zenith - Refraction-corrected solar zenith angle in degrees + Refraction-corrected solar zenith angle in degrees. Must be >=0 and <=180. + This angle accounts for atmospheric refraction effects. + + apparent_elevation + Refraction-corrected solar elevation angle in degrees. Must be >=-90 and <=90. + This is the complement of apparent_zenith (90 - apparent_zenith). bhi Beam/direct horizontal irradiance @@ -87,10 +94,10 @@ There is a convention on consistent variable names throughout the library: Sandia Array Performance Model IV curve parameters latitude - Latitude + Latitude in decimal degrees. Positive north of equator, negative to south. longitude - Longitude + Longitude in decimal degrees. Positive east of prime meridian, negative to west. pac, ac AC power @@ -141,10 +148,14 @@ There is a convention on consistent variable names throughout the library: Diode saturation current solar_azimuth - Azimuth angle of the sun in degrees East of North + Azimuth angle of the sun in degrees East of North. Must be >=0 and <=360. + The convention is defined as degrees east of north (e.g. North = 0°, + East = 90°, South = 180°, West = 270°). solar_zenith - Zenith angle of the sun in degrees + Zenith angle of the sun in degrees. Must be >=0 and <=180. + This is the angle between the sun's rays and the vertical direction. + This is the complement of :term:`solar_elevation` (90 - elevation). spectra spectra_components @@ -154,11 +165,14 @@ There is a convention on consistent variable names throughout the library: is composed of direct and diffuse components. surface_azimuth - Azimuth angle of the surface + Azimuth angle of the surface in degrees East of North. Must be >=0 and <=360. + The convention is defined as degrees east (clockwise) of north. This is pvlib's + convention; other tools may use different conventions. For example, North = 0°, + East = 90°, South = 180°, West = 270°. surface_tilt - Panel tilt from horizontal [°]. For example, a surface facing up = 0°, - surface facing horizon = 90°. + Panel tilt from horizontal [°]. Must be >=0 and <=180. + For example, a surface facing up = 0°, surface facing horizon = 90°. temp_air Temperature of the air diff --git a/example.py b/example.py new file mode 100644 index 0000000000..7dbd3cc3d7 --- /dev/null +++ b/example.py @@ -0,0 +1,56 @@ +# Simple pvlib demonstration script +import pvlib +import pandas as pd +from datetime import datetime, timedelta +import matplotlib.pyplot as plt + +# Create a location object for a specific site +location = pvlib.location.Location( + latitude=40.0, # New York City latitude + longitude=-74.0, # New York City longitude + tz='America/New_York', + altitude=10 # meters above sea level +) + +# Calculate solar position for a day +date = datetime(2024, 3, 15) +times = pd.date_range(date, date + timedelta(days=1), freq='1H', tz=location.tz) +solpos = location.get_solarposition(times) + +# Plot solar position +plt.figure(figsize=(10, 6)) +plt.plot(solpos.index, solpos['elevation'], label='Elevation') +plt.plot(solpos.index, solpos['azimuth'], label='Azimuth') +plt.title('Solar Position for New York City on March 15, 2024') +plt.xlabel('Time') +plt.ylabel('Angle (degrees)') +plt.legend() +plt.grid(True) +plt.show() + +# Calculate clear sky irradiance +clearsky = location.get_clearsky(times) + +# Plot clear sky irradiance +plt.figure(figsize=(10, 6)) +plt.plot(clearsky.index, clearsky['ghi'], label='Global Horizontal Irradiance') +plt.plot(clearsky.index, clearsky['dni'], label='Direct Normal Irradiance') +plt.plot(clearsky.index, clearsky['dhi'], label='Diffuse Horizontal Irradiance') +plt.title('Clear Sky Irradiance for New York City on March 15, 2024') +plt.xlabel('Time') +plt.ylabel('Irradiance (W/m²)') +plt.legend() +plt.grid(True) +plt.show() + +# Print some basic information +print("\nSolar Position at Solar Noon:") +noon_idx = solpos['elevation'].idxmax() +print(f"Time: {noon_idx}") +print(f"Elevation: {solpos.loc[noon_idx, 'elevation']:.2f}°") +print(f"Azimuth: {solpos.loc[noon_idx, 'azimuth']:.2f}°") + +print("\nMaximum Clear Sky Irradiance:") +print(f"GHI: {clearsky['ghi'].max():.2f} W/m²") +print(f"DNI: {clearsky['dni'].max():.2f} W/m²") +print(f"DHI: {clearsky['dhi'].max():.2f} W/m²") \ No newline at end of file diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index ab7530f3e5..ed0f242767 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -37,7 +37,7 @@ def pvefficiency_adr(effective_irradiance, temp_cell, the reference conditions. [unitless] k_d : numeric, negative - “Dark irradiance” or diode coefficient which influences the voltage + "Dark irradiance" or diode coefficient which influences the voltage increase with irradiance. [unitless] tc_d : numeric @@ -242,7 +242,45 @@ def _infer_k_huld(cell_type, pdc0): return k -def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None): +def _infer_k_huld_eu_jrc(cell_type, pdc0): + """ + Get the EU JRC updated coefficients for the Huld model. + + Parameters + ---------- + cell_type : str + Must be one of 'csi', 'cis', or 'cdte' + pdc0 : numeric + Power of the modules at reference conditions [W] + + Returns + ------- + tuple + The six coefficients (k1-k6) for the Huld model, scaled by pdc0 + + Notes + ----- + These coefficients are from the EU JRC paper [1]_. The coefficients are + for the version of Huld's equation that has factored Pdc0 out of the + polynomial, so they are multiplied by pdc0 before being returned. + + References + ---------- + .. [1] EU JRC paper, "Updated coefficients for the Huld model", + https://doi.org/10.1002/pip.3926 + """ + # Updated coefficients from EU JRC paper + huld_params = {'csi': (-0.017162, -0.040289, -0.004681, 0.000148, + 0.000169, 0.000005), + 'cis': (-0.005521, -0.038576, -0.003711, -0.000901, + -0.001251, 0.000001), + 'cdte': (-0.046477, -0.072509, -0.002252, 0.000275, + 0.000158, -0.000006)} + k = tuple([x*pdc0 for x in huld_params[cell_type.lower()]]) + return k + + +def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None, use_eu_jrc=False): r""" Power (DC) using the Huld model. @@ -274,6 +312,9 @@ def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None): cell_type : str, optional If provided, must be one of ``'cSi'``, ``'CIS'``, or ``'CdTe'``. Used to look up default values for ``k`` if ``k`` is not specified. + use_eu_jrc : bool, default False + If True, use the updated coefficients from the EU JRC paper [2]_. + Only used if ``k`` is not provided and ``cell_type`` is specified. Returns ------- @@ -332,10 +373,15 @@ def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None): E. Dunlop. A power-rating model for crystalline silicon PV modules. Solar Energy Materials and Solar Cells 95, (2011), pp. 3359-3369. :doi:`10.1016/j.solmat.2011.07.026`. + .. [2] EU JRC paper, "Updated coefficients for the Huld model", + https://doi.org/10.1002/pip.3926 """ if k is None: if cell_type is not None: - k = _infer_k_huld(cell_type, pdc0) + if use_eu_jrc: + k = _infer_k_huld_eu_jrc(cell_type, pdc0) + else: + k = _infer_k_huld(cell_type, pdc0) else: raise ValueError('Either k or cell_type must be specified') diff --git a/tests/test_pvarray.py b/tests/test_pvarray.py index 693ef78b2a..cec40273b3 100644 --- a/tests/test_pvarray.py +++ b/tests/test_pvarray.py @@ -69,3 +69,23 @@ def test_huld(): with pytest.raises(ValueError, match='Either k or cell_type must be specified'): res = pvarray.huld(1000, 25, 100) + + +def test_huld_eu_jrc(): + """Test the EU JRC updated coefficients for the Huld model.""" + pdc0 = 100 + # Use non-reference values so coefficients affect the result + eff_irr = 800 # W/m^2 (not 1000) + temp_mod = 35 # deg C (not 25) + # Test that EU JRC coefficients give different results than original for all cell types + for cell_type in ['cSi', 'CIS', 'CdTe']: + res_orig = pvarray.huld(eff_irr, temp_mod, pdc0, cell_type=cell_type) + res_eu_jrc = pvarray.huld(eff_irr, temp_mod, pdc0, cell_type=cell_type, use_eu_jrc=True) + assert not np.isclose(res_orig, res_eu_jrc), f"Results should differ for {cell_type}: {res_orig} vs {res_eu_jrc}" + # Also check that all cell types are supported and error is raised for invalid type + try: + pvarray.huld(eff_irr, temp_mod, pdc0, cell_type='invalid', use_eu_jrc=True) + except KeyError: + pass + else: + assert False, "Expected KeyError for invalid cell_type" diff --git a/tests/test_solarposition.py b/tests/test_solarposition.py index 88093e05f9..b2e2ad46d3 100644 --- a/tests/test_solarposition.py +++ b/tests/test_solarposition.py @@ -964,3 +964,50 @@ def test_spa_python_numba_physical_dst(expected_solpos, golden): temperature=11, delta_t=67, atmos_refract=0.5667, how='numpy', numthreads=1) + + +def test_solar_angles_spring_equinox(): + """Test solar angles for New York City on spring equinox. + + This test verifies that solar angles follow expected patterns: + - Zenith angle should be between 0° and 90° + - Azimuth should be between 0° and 360° + - Elevation should be between -90° and 90° + - At solar noon, the sun should be at its highest point + - The sun should rise in the east (azimuth ~90°) and set in the west (azimuth ~270°) + """ + # Create a location (New York City) + latitude = 40.7128 + longitude = -74.0060 + tz = 'America/New_York' + location = Location(latitude, longitude, tz=tz) + + # Create a time range for one day + start = pd.Timestamp('2024-03-20', tz=tz) # Spring equinox + times = pd.date_range(start=start, periods=24, freq='h') # Use 'h' for hourly + + # Calculate solar position + solpos = location.get_solarposition(times) + + # Test morning (9 AM) + morning = solpos.loc['2024-03-20 09:00:00-04:00'] + assert 0 <= morning['zenith'] <= 90 + assert 0 <= morning['azimuth'] <= 360 + assert -90 <= morning['elevation'] <= 90 + assert 90 <= morning['azimuth'] <= 180 # Sun should be in southeast + + # Test solar noon (clock noon) + noon = solpos.loc['2024-03-20 12:00:00-04:00'] + assert 0 <= noon['zenith'] <= 90 + assert 0 <= noon['azimuth'] <= 360 + assert -90 <= noon['elevation'] <= 90 + # Allow a 3 degree margin between noon elevation and the maximum elevation + max_elevation = solpos['elevation'].max() + assert abs(noon['elevation'] - max_elevation) < 3.0 # Allow 3 degree difference + + # Test evening (3 PM) + evening = solpos.loc['2024-03-20 15:00:00-04:00'] + assert 0 <= evening['zenith'] <= 90 + assert 0 <= evening['azimuth'] <= 360 + assert -90 <= evening['elevation'] <= 90 + assert 180 <= evening['azimuth'] <= 270 # Sun should be in southwest