Skip to content

Commit 4b7bcd2

Browse files
authored
Merge pull request #15 from AssessingSolar/Add-solar-position-models
Add solar position models
2 parents 9e5e91d + 8d17cd6 commit 4b7bcd2

File tree

10 files changed

+933
-2
lines changed

10 files changed

+933
-2
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@ jobs:
2222
pip install flake8
2323
- name: Lint with flake8
2424
run: |
25-
flake8 . --count --max-line-length=99 --statistics
25+
flake8 . --count --max-line-length=99 --statistics --ignore=E741

docs/source/solarposition.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,9 @@ Solar position algorithms
88
:toctree: generated/
99

1010
solarposition.iqbal
11+
solarposition.michalsky
12+
solarposition.noaa
13+
solarposition.psa
14+
solarposition.sg2
15+
solarposition.usno
16+
solarposition.walraven

src/solposx/refraction/spa.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ def spa(elevation, pressure=101325., temperature=12., refraction_limit=0.5667):
4444
Applications (Revised)." 2008. NREL Report No. TP-560-34302, pp. 55
4545
:doi:`10.2172/15003974`.
4646
""" # noqa: #501
47-
4847
pressure = pressure / 100 # convert to hPa
4948
# switch sets elevation when the sun is below the horizon
5049
switch = elevation >= -1.0 * (0.26667 + refraction_limit)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
from solposx.solarposition.iqbal import iqbal # noqa: F401
2+
from solposx.solarposition.michalsky import michalsky # noqa: F401
3+
from solposx.solarposition.noaa import noaa # noqa: F401
4+
from solposx.solarposition.psa import psa # noqa: F401
5+
from solposx.solarposition.sg2 import sg2 # noqa: F401
6+
from solposx.solarposition.usno import usno # noqa: F401
7+
from solposx.solarposition.walraven import walraven # noqa: F401
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
from pvlib.tools import sind, cosd, asind
2+
import numpy as np
3+
import pandas as pd
4+
from solposx import refraction
5+
from solposx.tools import _pandas_to_utc, _fractional_hour
6+
7+
8+
def michalsky(times, latitude, longitude, spencer_correction=True,
9+
julian_date='original'):
10+
"""
11+
Calculate solar position using Michalsky's algorithm.
12+
13+
Michalsky's algorithm [1]_ has a stated accuracy of 0.01 degrees
14+
from 1950 to 2050.
15+
16+
Parameters
17+
----------
18+
times : pandas.DatetimeIndex
19+
Must be localized or UTC will be assumed.
20+
latitude : float
21+
Latitude in decimal degrees. Positive north of equator, negative
22+
to south. [degrees]
23+
longitude : float
24+
Longitude in decimal degrees. Positive east of prime meridian,
25+
negative to west. [degrees]
26+
spencer_correction : bool, default True
27+
Applies the correction suggested by Spencer [2]_ so the algorithm
28+
works for all latitudes.
29+
julian_date : string, default 'original'
30+
Julian date calculation. Can be one of the following:
31+
* ``'original'``: calculation based on Michalsky's paper [1]_.
32+
* ``'pandas'``: calculation using a pandas build-in function
33+
34+
Returns
35+
-------
36+
DataFrame with the following columns (all values in degrees):
37+
38+
* elevation : actual sun elevation (not accounting for refraction).
39+
* apparent_elevation : sun elevation, accounting for
40+
atmospheric refraction.
41+
* zenith : actual sun zenith (not accounting for refraction).
42+
* apparent_zenith : sun zenith, accounting for atmospheric
43+
refraction.
44+
* azimuth : sun azimuth, east of north.
45+
46+
Raises
47+
------
48+
ValueError
49+
An error is raised if the julian_date calculation is not `original`
50+
or `pandas`.
51+
52+
Notes
53+
-----
54+
The Michalsky algorithm is based equations in the Astronomical Almanac.
55+
56+
As pointed out by Spencer (1989) [2]_, the original Michalsky
57+
algorithm [1]_ did not work for the southern hemisphere. This
58+
implementation includes by default the correction provided by Spencer such
59+
that it works for all latitudes.
60+
61+
Minor clarifications were made to the original paper have been published
62+
as errata in [3]_ and [4]_.
63+
64+
The Julian date calculation in the original Michalsky paper [1]_ ensures
65+
the stated accuracy (0.01 degrees) only for the period 1950 - 2050. Outside
66+
this period, the Julian date does not handle leap years correctly. Thus,
67+
there is the option to use the
68+
:py:func:`pandas.DatetimeIndex.to_julian_date` method.
69+
70+
References
71+
----------
72+
.. [1] J. J. Michalsky, "The Astronomical Almanac’s algorithm for
73+
approximate solar position (1950–2050)," Solar Energy, vol. 40, no. 3.
74+
Elsevier BV, pp. 227–235, 1988. :doi:`10.1016/0038-092x(88)90045-x`.
75+
.. [2] J. W. Spencer, “Comments on The Astronomical Almanac’s Algorithm for
76+
Approximate Solar Position (1950–2050),” Solar Energy, vol. 42, no. 4.
77+
Elsevier BV, p. 353, 1989. :doi:`10.1016/0038-092x(89)90039-x`.
78+
.. [3] J. J. Michalsky, “Errata,” Solar Energy, vol. 41, no. 1. Elsevier
79+
BV, p. 113, 1988. :doi:`10.1016/0038-092x(88)90122-3`.
80+
.. [4] J. J. Michalsky, “Errata,” Solar Energy, vol. 43, no. 5. Elsevier
81+
BV, p. 323, 1989. :doi:`10.1016/0038-092x(89)90122-9`.
82+
"""
83+
times_utc = _pandas_to_utc(times)
84+
85+
hour = _fractional_hour(times_utc)
86+
87+
if julian_date == 'original':
88+
year = times_utc.year
89+
day = times_utc.dayofyear
90+
delta = year - 1949
91+
leap = np.floor(delta / 4)
92+
jd = 2432916.5 + delta * 365 + leap + day + hour / 24
93+
elif julian_date == 'pandas':
94+
jd = times_utc.to_julian_date()
95+
else:
96+
raise ValueError(
97+
"Either `original` or `pandas` has to be chosen for the Julian"
98+
" date calculation.")
99+
100+
n = jd - 2451545.0
101+
102+
# L - mean longitude [degrees]
103+
L = 280.460 + 0.9856474 * n
104+
# L has to be between 0 and 360 deg
105+
L = L % 360
106+
107+
# g - mean anomaly [degrees]
108+
g = 357.528 + 0.9856003 * n
109+
# g has to be between 0 and 360 deg
110+
g = g % 360
111+
112+
# l - ecliptic longitude [degrees]
113+
l = L + 1.915 * sind(g) + 0.02 * sind(2*g)
114+
# l has to be between 0 and 360 deg
115+
l = l % 360
116+
117+
# ep - obliquity of the ecliptic [degrees]
118+
ep = 23.439 - 0.0000004 * n
119+
120+
# ra - right ascension [degrees]
121+
ra = np.rad2deg(np.arctan2(cosd(ep) * sind(l), cosd(l)))
122+
# ra has to be between 0 and 360 deg
123+
ra = ra % 360
124+
125+
# dec - declination angle [degrees]
126+
dec = asind(sind(ep) * sind(l))
127+
128+
# gmst - Greenwich mean sidereal time [hr]
129+
gmst = 6.697375 + 0.0657098242 * n + hour
130+
# gmst has to be between 0 and 24 h
131+
gmst = gmst % 24
132+
133+
# lmst - local mean sideral time [hr]
134+
lmst = gmst + longitude / 15 # to convert deg to h, divide with 15
135+
# lmst has to be between 0 and 24 h
136+
lmst = lmst % 24
137+
138+
# ha - hour angle [hr]
139+
ha = lmst - ra / 15
140+
# ha has to be between -12 and 12 h
141+
ha = (ha + 12) % 24 - 12
142+
143+
# el - solar elevation angle [degrees]
144+
el = asind(sind(dec) * sind(latitude) + cosd(dec) * cosd(latitude)
145+
* cosd(15 * ha)) # to convert h to deg, multiply with 15
146+
147+
# az - azimuth [degrees]
148+
az = asind(-cosd(dec) * sind(15 * ha) / cosd(el))
149+
150+
if spencer_correction:
151+
# Spencer correction for the azimuth quadrant assignment
152+
cos_az = sind(dec) - sind(el) * sind(latitude)
153+
az = np.where((cos_az >= 0) & (sind(az) < 0), 360 + az, az)
154+
az = np.where(cos_az < 0, 180 - az, az)
155+
else:
156+
# Original Michalsky implementation does not work for all latitudes
157+
# calcualte critical elevation
158+
elc = asind(sind(dec) / sind(latitude))
159+
# correct azimuth using critical elevation
160+
az = np.where(el >= elc, 180 - az, az)
161+
az = np.where((el <= elc) & (ha > 0), az + 360, az)
162+
az = az % 360
163+
164+
# refraction correction
165+
r = refraction.michalsky(el)
166+
167+
result = pd.DataFrame({
168+
'elevation': el,
169+
'apparent_elevation': el + r,
170+
'zenith': 90 - el,
171+
'apparent_zenith': 90 - (el + r),
172+
'azimuth': az,
173+
}, index=times)
174+
return result

src/solposx/solarposition/noaa.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import pandas as pd
2+
from pvlib.tools import sind, cosd, asind, acosd, tand
3+
import numpy as np
4+
from solposx import refraction
5+
from solposx.tools import _pandas_to_utc, _fractional_hour
6+
7+
8+
def noaa(times, latitude, longitude, delta_t=67.0):
9+
"""
10+
Calculate solar position using NOAA's algorithm.
11+
12+
NOAA's algorithm [1]_ has a stated accuracy of 0.0167 degrees
13+
from years -2000 to +3000 for latitudes within +/- 72 degrees. For
14+
latitudes outside this the accuracy is 0.167 degrees.
15+
16+
The NOAA algorithm uses by default the Hughes refraction model,
17+
see :py:func:`package_name.refraction.hughes`.
18+
19+
Parameters
20+
----------
21+
times : pandas.DatetimeIndex
22+
Must be localized or UTC will be assumed.
23+
latitude : float
24+
Latitude in decimal degrees. Positive north of equator, negative
25+
to south. [degrees]
26+
longitude : float
27+
Longitude in decimal degrees. Positive east of prime meridian,
28+
negative to west. [degrees]
29+
delta_t : float or array, optional, default 67.0
30+
Difference between terrestrial time and UT1.
31+
If delta_t is None, uses spa.calculate_deltat
32+
using time.year and time.month from pandas.DatetimeIndex.
33+
For most simulations the default delta_t is sufficient.
34+
The USNO has historical and forecasted delta_t [3]_.
35+
[seconds]
36+
37+
Returns
38+
-------
39+
DataFrame with the following columns (all values in degrees):
40+
41+
* elevation : actual sun elevation (not accounting for refraction).
42+
* apparent_elevation : sun elevation, accounting for
43+
atmospheric refraction.
44+
* zenith : actual sun zenith (not accounting for refraction).
45+
* apparent_zenith : sun zenith, accounting for atmospheric
46+
refraction.
47+
* azimuth : sun azimuth, east of north.
48+
49+
Notes
50+
-----
51+
The algorithm is from Astronomical Algorithms by Jean Meeus [2]_.
52+
53+
References
54+
----------
55+
.. [1] Solar Calculation Details. Global Monitoring
56+
Laboratory Earth System Research Laboratories.
57+
https://gml.noaa.gov/grad/solcalc/calcdetails.html
58+
.. [2] Meeus, J. "Astronomical Algorithms", 1991.
59+
https://archive.org/details/astronomicalalgorithmsjeanmeeus1991
60+
.. [3] USNO delta T:
61+
https://maia.usno.navy.mil/products/deltaT
62+
"""
63+
times_utc = _pandas_to_utc(times)
64+
julian_date = times_utc.to_julian_date()
65+
jc = (julian_date - 2451545) / 36525
66+
67+
# [degrees]
68+
mean_long = (
69+
280.46646 + jc*(36000.76983 + jc*0.0003032)
70+
) % 360
71+
72+
mean_anom = 357.52911 + jc * (35999.05029 - 0.0001537 * jc)
73+
74+
eccent_earth_orbit = (
75+
0.016708634
76+
- jc * (0.000042037 + 0.0000001267 * jc))
77+
78+
sun_eq_ctr = (
79+
sind(mean_anom)*(1.914602 - jc*(0.004817 + 0.000014*jc))
80+
+ sind(2*mean_anom)*(0.019993 - 0.000101*jc)
81+
+ sind(3*mean_anom)*0.000289
82+
)
83+
84+
sun_true_long = mean_long + sun_eq_ctr
85+
86+
sun_app_long = sun_true_long - 0.00569 - 0.00478*sind(125.04 - 1934.136*jc)
87+
88+
mean_obliq_ecliptic = 23 + (26 + (
89+
21.448 - jc*(46.815 + jc*(0.00059 - jc*0.001813)))/60)/60
90+
91+
obliq_corr = mean_obliq_ecliptic + 0.00256*cosd(125.04 - 1934.136*jc)
92+
93+
sun_declin = asind(sind(obliq_corr) * sind(sun_app_long))
94+
95+
var_y = tand(obliq_corr / 2)**2
96+
97+
eot = 4*np.degrees(
98+
+ var_y*sind(2*mean_long)
99+
- 2*eccent_earth_orbit*sind(mean_anom)
100+
+ 4*eccent_earth_orbit*var_y*sind(mean_anom)*cosd(2*mean_long)
101+
- 0.5*(var_y**2)*sind(4*mean_long)
102+
- 1.25*(eccent_earth_orbit**2)*sind(2*mean_anom)
103+
)
104+
105+
minutes = _fractional_hour(times_utc)*60
106+
107+
true_solar_time = (minutes + eot + 4*longitude) % 1440
108+
109+
hour_angle = np.where(
110+
true_solar_time / 4 < 0,
111+
true_solar_time / 4 + 180,
112+
true_solar_time / 4 - 180,
113+
)
114+
115+
zenith = acosd(sind(latitude)*sind(sun_declin) +
116+
cosd(latitude)*cosd(sun_declin)*cosd(hour_angle))
117+
118+
azimuth = np.where(
119+
hour_angle > 0,
120+
(acosd(((sind(latitude)*cosd(zenith)) - sind(sun_declin)) /
121+
(cosd(latitude)*sind(zenith)))+180 % 360),
122+
(540 - acosd(((sind(latitude)*cosd(zenith)) - sind(sun_declin)) /
123+
(cosd(latitude)*sind(zenith)))) % 360,
124+
)
125+
126+
elevation = 90 - zenith
127+
refraction_correction = refraction.hughes(
128+
elevation=elevation, pressure=101325, temperature=10)
129+
130+
result = pd.DataFrame({
131+
'elevation': elevation,
132+
'apparent_elevation': elevation + refraction_correction,
133+
'zenith': zenith,
134+
'apparent_zenith': zenith - refraction_correction,
135+
'azimuth': azimuth,
136+
}, index=times)
137+
return result

0 commit comments

Comments
 (0)