Skip to content
1 change: 1 addition & 0 deletions docs/sphinx/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ calculations.
:toctree: generated/

solarposition.solar_zenith_analytical
solarposition.solar_azimuth_analytical
solarposition.declination_spencer71
solarposition.declination_cooper69
solarposition.equation_of_time_spencer71
Expand Down
3 changes: 3 additions & 0 deletions docs/sphinx/source/whatsnew/v0.4.6.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Bug fixes
Enhancements
~~~~~~~~~~~~
* Added default values to docstrings of all functions (:issue:`336`)
* Added analytical method that calculates solar azimuth angle (:issue:`291`)

API Changes
~~~~~~~~~~~
Expand All @@ -31,6 +32,7 @@ Testing

* Added explicit tests for aoi and aoi_projection functions.
* Update test of `ModelChain.__repr__` to take in account :issue:`352`
* Added a test for solar_azimuth_analytical function.


Contributors
Expand All @@ -41,3 +43,4 @@ Contributors
* Alaina Kafkes
* Birgit Schachler
* Jonathan Gaffiot
* Siyan (Veronica) Guo
47 changes: 47 additions & 0 deletions pvlib/solarposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,53 @@ def declination_cooper69(dayofyear):
return np.deg2rad(23.45 * np.sin(day_angle + (2.0 * np.pi / 365.0) * 285.0))


def solar_azimuth_analytical(latitude, hour_angle, declination, zenith):
"""
Analytical expression of solar azimuth angle based on spherical
trigonometry.

Parameters
----------
latitude : numeric
Latitude of location in radians.
hour_angle : numeric
Hour angle in the local solar time in radians.
declination : numeric
Declination of the sun in radians.
zenith : numeric
Solar zenith angle in radians.

Returns
-------
azimuth : numeric
Solar azimuth angle in radians.

References
----------
[1] J. A. Duffie and W. A. Beckman, "Solar Engineering of Thermal
Processes, 3rd Edition" pp. 14, J. Wiley and Sons, New York (2006)

[2] J. H. Seinfeld and S. N. Pandis, "Atmospheric Chemistry and Physics"
p. 132, J. Wiley (1998)

[3] `Wikipedia: Solar Azimuth Angle
<https://en.wikipedia.org/wiki/Solar_azimuth_angle>`_

[4] `PVCDROM: Azimuth Angle <http://www.pveducation.org/pvcdrom/2-properties
-sunlight/azimuth-angle>`_

See Also
--------
declination_spencer71
declination_cooper69
hour_angle
solar_zenith_analytical
"""
return np.sign(hour_angle) * np.abs(np.arccos((np.cos(zenith) * np.sin(
Copy link
Member

Choose a reason for hiding this comment

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

@veronicaguo and @wholmgren sorry for super late review, and just a tiny nitpick, but I don't think np.abs is necessary here because according to numpy documentation on np.arccos it is only defined from on [0, pi]

The angle of the ray intersecting the unit circle at the given x-coordinate in radians [0, pi].

Therefore, it will never return a negative number, so no need for absolute value.

latitude) - np.sin(declination)) / (np.sin(zenith) * np.cos(
latitude)))) + np.pi


def solar_zenith_analytical(latitude, hour_angle, declination):
"""
Analytical expression of solar zenith angle based on spherical trigonometry.
Expand Down
30 changes: 30 additions & 0 deletions pvlib/test/test_solarposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,33 @@ def test_analytical_zenith():
zenith_2 = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
assert np.allclose(zenith_1, solar_zenith, atol=0.015)
assert np.allclose(zenith_2, solar_zenith, atol=0.025)


def test_analytical_azimuth():
times = pd.DatetimeIndex(start="1/1/2015 0:00", end="12/31/2015 23:00",
freq="H").tz_localize('Etc/GMT+8')
lat, lon = 37.8, -122.25
lat_rad = np.deg2rad(lat)
output = solarposition.spa_python(times, lat, lon, 100)
solar_azimuth = np.deg2rad(output['azimuth']) # spa
solar_zenith = np.deg2rad(output['zenith'])
# spencer
eot = solarposition.equation_of_time_spencer71(times.dayofyear)
hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
decl = solarposition.declination_spencer71(times.dayofyear)
zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
azimuth_1 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle,
decl, zenith)
# pvcdrom and cooper
eot = solarposition.equation_of_time_pvcdrom(times.dayofyear)
hour_angle = np.deg2rad(solarposition.hour_angle(times, lon, eot))
decl = solarposition.declination_cooper69(times.dayofyear)
zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl)
azimuth_2 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle,
decl, zenith)

idx = np.where(solar_zenith < np.pi/2)
assert np.allclose(azimuth_1[idx], solar_azimuth.as_matrix()[idx],
atol=0.01)
assert np.allclose(azimuth_2[idx], solar_azimuth.as_matrix()[idx],
atol=0.017)