-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add analytical azimuth method to solarposition.py. #349
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @veronicaguo, and sorry for forgetting to review this until now. It mostly looks good to me. It needs:
@mikofski wrote the closely related functions, so it would be nice if he could review as well.
pvlib/solarposition.py
Outdated
[2] J. H. Seinfeld and S. N. Pandis, "Atmospheric Chemistry and Physics" | ||
p. 132, J. Wiley (1998) | ||
|
||
`Wikipedia: Solar Azimuth Angle <https://en.wikipedia.org/wiki/Solar_azimuth_angle>`_ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reference needs a number
pvlib/solarposition.py
Outdated
|
||
`Wikipedia: Solar Azimuth Angle <https://en.wikipedia.org/wiki/Solar_azimuth_angle>`_ | ||
|
||
`PVCDROM: Azimuth Angle <http://www.pveducation.org/pvcdrom/2-properties-sunlight/azimuth-angle>`_ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
Not as accurate when azimuth angle is close to due north.
Thanks @wholmgren, I have made some updates to the method and test. The method is quite accurate when the sun is away from the north. The doc files are complete now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a few concerns about the test (below), but otherwise this looks good.
pvlib/test/test_solarposition.py
Outdated
zenith = solarposition.solar_zenith_analytical(lat_rad, hour_angle, decl) | ||
azimuth_2 = solarposition.solar_azimuth_analytical(lat_rad, hour_angle, | ||
decl, zenith) | ||
for idx, a in enumerate(azimuth_1): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add some comments as to why you've structured the for loop and the assert statements this way? Please also replace a
with something more descriptive such as azi
or azimuth
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. The analytical azimuth formula is quite accurate in every direction, with an error or 1-2 degrees, except when the sun is in the north. So I separated those cases out through the for loops and if statements and gave them different atol
values. Also, since the sun is only in the north during midnight when no power is produced, it shouldn't impact solar performance calculations. The test code is not in the most concise form right now. I will try slim it down a bit. Thank you for the feedback.
pvlib/test/test_solarposition.py
Outdated
for idx, a in enumerate(azimuth_1): | ||
if a < 0.7: | ||
assert np.allclose(a, solar_azimuth[idx], atol=0.025) or \ | ||
np.allclose(a + np.pi * 2, solar_azimuth[idx], atol=0.55) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
atol=0.55
implies an accuracy of 32 degrees, correct? That seems quite bad. Not sure if the test needs to change or if that's a limitation of the algorithm under some input parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
atol=0.55
only when the sun is in the north, which is every night from 12-1 AM. It is otherwise quite accurate during the rest of the day. I assume it's probably a limitation with the analytical method that most references share.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case a system in the Southern hemisphere is analyzed, the sun will be in the North during the day. Shouldn't the code also be able to correctly handle this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @trichrt for pointing it out. The analytical formula actually takes care of it inherently. When in the Southern hemisphere, the calculation is less accurate when the sun is in the south during midnight. The current test is for a Northern hemisphere location. I can put together a test for Southern hemisphere if desired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case we should restrict the atol=0.55
tests to azimuth angles that are close to 0/360 (as defined by the more accurate models).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. The discrepancies start to appear within a 30-degree range around 0/360, i.e. from 345 to 15 degree. Have updated the azimuth threshold in the latest commit.
I just noticed that you're testing a year of data with for loops. How long does this take to run? A couple of np.testing.assert_allclose calls might be a lot faster and more readable. |
I'm using for loops to run the assert statements on each of the results one by one, and the np.allclose call doesn't allow for that. It takes ~0.06s to run the loops. With the threshold azimuth being 0.275, discrepancies start to appear between the analytical and spa methods, but there are results smaller than 0.275 and yet still are very accurate. It is determined by the time of the year. The greatest discrepancy is observed around late January. So np.allclose doesn't work in this case, and also assert statements need to be separated for results that span across 0/360. On that note, a test on a Southern hemisphere location wouldn't require the loops since azimuths around 180/360 don't run across 0. Therefore, a few np.allclose calls would work here. Would you prefer that? |
You could use also just use
|
Thanks @mikofski. The thing is that you cannot put a hard cut on a single value. For example, say the azimuth is evaluated to be 0.2 at one timestamp, and the real azimuth is 6.22. This case it would need a 2*pi correction as well as |
That is plenty fast. What is the maximum azimuth angle error as a function of latitude? I'd also like to know this for the related functions and update the documentation accordingly. I think we can restrict the question to times at which the sun is above the horizon. |
Thanks for the plots and the updated tests. I would not have expected that shape. I'll merge tomorrow unless anyone has further comments. |
Thanks @wholmgren ! I was not expecting that shape either... |
I think I can explain the surprising discontinuity in azimuth abs. error going north. TL;DR: The errors are exaggerated during the summer when azimuth near zero is actually likely, if one calculation is close to zero, but the other is closer to 360. Even though these azimuths are actually very close, their absolute difference is quite large! To evaluate the real absolute error, I just shifted all azimuth from 0-360 scale to the equivalent -180 to 180 scale. EG: 2 is still 2, but 358 would be -2. With the correctly shifted scale, the errors are more reasonable, although there are still some surprising discontinuities!
stats:
Figure 1: latitude vs. Figure 3: If I don't shift the azimuth I get the same discontinuity as @veronicaguo Figure 4: you can see errors are concentrated in summer, which is when the very long days occur - I had looked at July 1st at 70 degrees and already seen the large (northern) azimuths at sunrise/sunset, but it didn't occur to me until I saw this distribution I evaluated all northern latitudes from 0 - 75 in 1 degree increments for hourly data and removed all hours for which zenith > 90. from pvlib.solarposition import (
solar_azimuth_analytical, get_solarposition, solar_zenith_analytical,
hour_angle, declination_spencer71, equation_of_time_spencer71
)
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
plt.ion()
dt = pd.DatetimeIndex(start='2017-01-01T00:00:00-0800',
end='2017-12-31T23:59:59-0800', freq='H')
LAT = np.arange(0, 76, 1.0, dtype=float)
LON = -120
abserr = []
abserr_shift = []
for lat in LAT:
sp = get_solarposition(time=dt, latitude=lat, longitude=LON)
decl = declination_spencer71(dt.dayofyear) # radians
eot = equation_of_time_spencer71(dt.dayofyear) # minutes
hr = hour_angle(times=dt, longitude=LON, equation_of_time=eot) # degrees
ze = solar_zenith_analytical(np.radians(lat), np.radians(hr), decl)
az = solar_azimuth_analytical(np.radians(lat), np.radians(hr), decl, ze)
sp['analytical_zenith'] = np.degrees(ze)
sp['analytical_azimuth'] = np.degrees(az)
sp['daytime'] = sp['zenith'] < 90.0
sp['az_reldiff'] = 1 - sp['azimuth'] / sp['analytical_azimuth']
sp['ze_reldiff'] = 1 - sp['zenith'] / sp['analytical_zenith']
abserr.append(np.abs(sp[['az_reldiff', 'ze_reldiff']][sp.daytime]).mean())
sp['shift_sphere_az'] = (
(sp.analytical_azimuth - 360)*(sp.analytical_azimuth > 180) +
sp.analytical_azimuth*(sp.analytical_azimuth<=180)
)
sp['shift_az'] = ((sp.azimuth - 360)*(sp.azimuth>180) +
sp.azimuth*(sp.azimuth<=180))
sp['az_reldiff_shift'] = 1 - sp['shift_az'] / sp['shift_sphere_az']
abserr_shift.append(np.abs(sp[['az_reldiff_shift', 'ze_reldiff']][sp.daytime]).mean())
plt.figure()
plt.plot(LAT, abserr, '-o')
plt.legend(['analytical azimuth abs. err.', 'analytical zenith abs. err.'])
np.abs(sp[['az_reldiff', 'ze_reldiff']][sp.daytime]).plot()
plt.figure()
plt.plot(LAT, abserr_shift, '-o')
plt.legend(['analytical azimuth shift abs. err.', 'analytical zenith abs. err.'])
np.abs(sp[['az_reldiff_shift', 'ze_reldiff']][sp.daytime]).plot() |
hour_angle | ||
solar_zenith_analytical | ||
""" | ||
return np.sign(hour_angle) * np.abs(np.arccos((np.cos(zenith) * np.sin( |
There was a problem hiding this comment.
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.
Very interesting. Perhaps something like this could be used to compare angles:
|
Your script Fu is definitely better than mine! Nice use of Let me be clear, there is no need to change these angles at all. I only shifted them for the comparison because of the issue I described where errors were exaggerated because one azimuth was near zero and the other calculation was near 360 |
I understand the shift is just for the comparison, but it moves the comparison problem to the South, doesn't it? |
Ah, yes! Now errors where one azimuth is close to -180 and the other is closer to 180 form discontinuities. Now I understand your formulation. I will recalculate abs error with your expression and see if that removes all discontinuities. Thanks! |
absolute errors for analytical functions
recalculation with better shifting of angle differenceOK, using @adriesse magic script fu has significantly improved the absolute error calculations, although there are still two (2) surprise discontinuities, they occur in both zenith and azimuth absolute errors, so it may be a machine precision or other mathematical/computational limitation, and I'm not going to worry about it. The distribution still shows a pretty massive spike in summer for azimuth, what could it be, I can't think about it right now, perhaps someone will figure out that it's totally obvious and tell me. the new abs error with script magic by @adriesse from pvlib.solarposition import (
solar_azimuth_analytical, get_solarposition, solar_zenith_analytical,
hour_angle, declination_spencer71, equation_of_time_spencer71
)
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
plt.ion()
dt = pd.DatetimeIndex(start='2017-01-01T00:00:00-0800',
end='2017-12-31T23:59:59-0800', freq='H')
LAT = np.arange(0, 76, 1.0, dtype=float)
LON = -120
abserr = []
# this is the magic by Anton Driesse, to detect where to shift the circle
def angle_difference(a1, a2):
da = a2 - a1
da = np.where(da > 180, da-360, da)
da = np.where(da<=-180, da+360, da)
return da
for lat in LAT:
sp = get_solarposition(time=dt, latitude=lat, longitude=LON)
decl = declination_spencer71(dt.dayofyear) # radians
eot = equation_of_time_spencer71(dt.dayofyear) # minutes
hr = hour_angle(times=dt, longitude=LON, equation_of_time=eot) # degrees
ze = solar_zenith_analytical(np.radians(lat), np.radians(hr), decl)
az = solar_azimuth_analytical(np.radians(lat), np.radians(hr), decl, ze)
sp['analytical_zenith'] = np.degrees(ze)
sp['analytical_azimuth'] = np.degrees(az)
sp['daytime'] = sp['zenith'] < 90.0
sp['az_reldiff'] = angle_difference(sp['azimuth'], sp['analytical_azimuth']) / sp['azimuth']
sp['ze_reldiff'] = 1 - sp['zenith'] / sp['analytical_zenith']
abserr.append(np.abs(sp[['az_reldiff', 'ze_reldiff']][sp.daytime]).mean())
plt.figure()
plt.plot(LAT, abserr, '-o')
plt.legend(['analytical azimuth abs. err.', 'analytical zenith abs. err.'])
np.abs(sp[['az_reldiff', 'ze_reldiff']][sp.daytime]).plot() |
Ok, now I see more clearly what you are doing: "absolute error" means "absolute value of the relative error". This gets you in trouble when the zenith is at or near zero, which happens at latitudes < 23. The other trouble when the zenith is very small is that a small change in sun position along the path of the sun creates a large change in azimuth angle. I think you should be looking at degree differences rather than percentage difference, and even that won't fix the latter problem. I also suggest running this at minute rather than hourly intervals and/or pick fixed intervals of solar time and I think you will lose some of the funny stuff. |
Good point, I'll redo in degrees, I like non dimensional better, but you're right of course about over exaggerating differences of very small numbers. Also usually instead of |
@adriesse in degrees, using 180° to scale azimuth angle differences and 90° to scale zenith I get these scaled/non-dimensionalized absolute errors, using |
@wholmgren surprisingly accurate, but the zenith still doesn't account for atmospheric refraction, and that error isn't included here. |
Addresses issue #291