From 09a95d49aa855740fa300533f1707759fcadaba8 Mon Sep 17 00:00:00 2001 From: Omar Bahamida Date: Sun, 15 Jun 2025 21:52:40 +0200 Subject: [PATCH 1/3] docs: update solar angle definitions in nomenclature - Add detailed definitions for solar angles with range constraints - Clarify pvlib's east-of-north convention for azimuth angles - Add cross-references between related terms using :term: directive - Add coordinate system conventions for latitude/longitude - Enhance existing angle definitions with usage notes and examples This commit addresses issue #2448 by migrating angle definitions and conventions from parameter descriptions to the nomenclature page. (cherry picked from commit 16435c79d038375da476e7c1b09ef82c9dfb77e8) --- .../source/user_guide/extras/nomenclature.rst | 34 +++++++---- example.py | 56 +++++++++++++++++++ 2 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 example.py 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 From fa13cf343ebd0edce831d0a83ab0642c5ced31fa Mon Sep 17 00:00:00 2001 From: Omar Bahamida Date: Mon, 16 Jun 2025 10:01:48 +0200 Subject: [PATCH 2/3] Add test script for solar angle calculations (Issue #2448) - Created test_solar_angles.py to verify solar angle calculations - Tests zenith, azimuth, and elevation angles for different times of day - Uses New York City as example location on spring equinox - Verifies angles are within expected ranges and follow correct patterns (cherry picked from commit f202c5d415e8211ee8c57015dd73bd91d1608ac4) --- test_solar_angles.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 test_solar_angles.py diff --git a/test_solar_angles.py b/test_solar_angles.py new file mode 100644 index 0000000000..dc5dc97717 --- /dev/null +++ b/test_solar_angles.py @@ -0,0 +1,54 @@ +import pvlib +import pandas as pd +from datetime import datetime +import pytz + +def test_solar_angles(): + # Create a location (example: New York City) + latitude = 40.7128 + longitude = -74.0060 + tz = 'America/New_York' + location = pvlib.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') + + # Calculate solar position + solpos = location.get_solarposition(times) + + # Print results for key times + print("\nSolar Angles for New York City on Spring Equinox:") + print("=" * 50) + + # Morning (9 AM) + morning = solpos.loc['2024-03-20 09:00:00-04:00'] + print("\nMorning (9 AM):") + print(f"Solar Zenith: {morning['zenith']:.2f}°") + print(f"Solar Azimuth: {morning['azimuth']:.2f}°") + print(f"Solar Elevation: {morning['elevation']:.2f}°") + + # Solar Noon + noon = solpos.loc['2024-03-20 12:00:00-04:00'] + print("\nSolar Noon:") + print(f"Solar Zenith: {noon['zenith']:.2f}°") + print(f"Solar Azimuth: {noon['azimuth']:.2f}°") + print(f"Solar Elevation: {noon['elevation']:.2f}°") + + # Evening (3 PM) + evening = solpos.loc['2024-03-20 15:00:00-04:00'] + print("\nEvening (3 PM):") + print(f"Solar Zenith: {evening['zenith']:.2f}°") + print(f"Solar Azimuth: {evening['azimuth']:.2f}°") + print(f"Solar Elevation: {evening['elevation']:.2f}°") + + # Verify the angles make sense + print("\nVerification:") + print("- Zenith angle should be between 0° and 90°") + print("- Azimuth should be between 0° and 360°") + print("- Elevation should be between -90° and 90°") + print("- At solar noon, the sun should be at its highest point") + print("- The sun should rise in the east (azimuth ~90°) and set in the west (azimuth ~270°)") + +if __name__ == "__main__": + test_solar_angles() \ No newline at end of file From 8d1e7187b1030dd7247f65b799b2d9f907459d48 Mon Sep 17 00:00:00 2001 From: Omar Bahamida Date: Mon, 16 Jun 2025 11:00:20 +0200 Subject: [PATCH 3/3] Remove test_solar_angles.py and integrate its functionality into tests/test_solarposition.py - Deleted the standalone test_solar_angles.py file. - Added a new test function, test_solar_angles_spring_equinox, to tests/test_solarposition.py. - The new test verifies solar angles for New York City on the spring equinox, ensuring angles are within expected ranges and follow correct patterns. (cherry picked from commit 99636296657ba15174b8620eee9db01cd2d0156a) --- test_solar_angles.py | 54 ------------------------------------- tests/test_solarposition.py | 47 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 54 deletions(-) delete mode 100644 test_solar_angles.py diff --git a/test_solar_angles.py b/test_solar_angles.py deleted file mode 100644 index dc5dc97717..0000000000 --- a/test_solar_angles.py +++ /dev/null @@ -1,54 +0,0 @@ -import pvlib -import pandas as pd -from datetime import datetime -import pytz - -def test_solar_angles(): - # Create a location (example: New York City) - latitude = 40.7128 - longitude = -74.0060 - tz = 'America/New_York' - location = pvlib.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') - - # Calculate solar position - solpos = location.get_solarposition(times) - - # Print results for key times - print("\nSolar Angles for New York City on Spring Equinox:") - print("=" * 50) - - # Morning (9 AM) - morning = solpos.loc['2024-03-20 09:00:00-04:00'] - print("\nMorning (9 AM):") - print(f"Solar Zenith: {morning['zenith']:.2f}°") - print(f"Solar Azimuth: {morning['azimuth']:.2f}°") - print(f"Solar Elevation: {morning['elevation']:.2f}°") - - # Solar Noon - noon = solpos.loc['2024-03-20 12:00:00-04:00'] - print("\nSolar Noon:") - print(f"Solar Zenith: {noon['zenith']:.2f}°") - print(f"Solar Azimuth: {noon['azimuth']:.2f}°") - print(f"Solar Elevation: {noon['elevation']:.2f}°") - - # Evening (3 PM) - evening = solpos.loc['2024-03-20 15:00:00-04:00'] - print("\nEvening (3 PM):") - print(f"Solar Zenith: {evening['zenith']:.2f}°") - print(f"Solar Azimuth: {evening['azimuth']:.2f}°") - print(f"Solar Elevation: {evening['elevation']:.2f}°") - - # Verify the angles make sense - print("\nVerification:") - print("- Zenith angle should be between 0° and 90°") - print("- Azimuth should be between 0° and 360°") - print("- Elevation should be between -90° and 90°") - print("- At solar noon, the sun should be at its highest point") - print("- The sun should rise in the east (azimuth ~90°) and set in the west (azimuth ~270°)") - -if __name__ == "__main__": - test_solar_angles() \ No newline at end of file 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