-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add a new and faster way to calculate the solarposition #730
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
Some issue of stickler-ci are not related to my changes. |
pvlib/solarposition.py
Outdated
return result | ||
|
||
|
||
def datetime2julian(times): |
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.
Does this function do the same thing as tools.datetime_to_djd
?
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.
No not quite the same but it is similar. Actually datetime_to_djd is referencing to the dublin standard and the function is not able to work with arrays. I will have a look If it is possble to adapt my function that it is working with dublin julian days so that we can use my implementation.
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 can't help generally with the stickler complaints. One line error is causing both test errors, I think.
Thanks for the contribution!
@cwhanse I hope i solved all issues for linter and lgtm. Lgtm is still running at the moment. |
Hi @meteoDaniel , FYI: I pushed a PR with new changes to SolarUtils that allows you to specify either the year or a specific sequence of datetimes, it's very fast In [1]: from solar_utils import solposAM, get_solposAM
...: location = [35.56836, -119.2022, -8.0]
...: weather = [1015.62055, 40.0]
...: from datetime import datetime, timedelta
...: times = [(datetime(2017,1,1,0,0,0)+timedelta(hours=h)).timetuple()[:6] for h in range(1000)]
In [2]: %timeit get_solposAM(location, times, weather)
1.82 ms ± 16.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) On my PC that's about twice as fast as the Spencer method per the 1000-timestep benchmark in this PR. SolarUtils is a very thin Python wrapper around the freely available NREL SOLPOS algorithm source code from Martin Rymes, 1998-2001. Although SolarUtils runtime scales linearly, while this Spencer method runtime stays fairly low, probably due to NumPy?
Porting SOLPOS to Python would probably have similar results. The source is freely available, so perhaps if you require even more speed that would be an option. In [6]: times = [(datetime(2017,1,1,0,0,0)+timedelta(hours=h)).timetuple()[:6] for h in range(10000)]
In [7]: %timeit get_solposAM(location, times, weather)
18.8 ms ± 117 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) One of the stated purpose of pvlib python is to catalog all useful solar algorithms, and IMO this Spencer method is very useful! Thanks so much for contributing this! Let me know if there's anyway I can help. |
Generally, the 'apparent' values account for atmospheric refraction, the non-apparent values are geometric. |
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.
Test failure was for get_psm3
probably an interruption in server response. I restarted the tests.
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 @meteoDaniel.
pvlib/solarposition.py
Outdated
def spencer_mc(times, latitude, longitude): | ||
""" | ||
Calculate the solar position using a python implementation of the | ||
Spencer (1972) formulation provided by meteocontrol |
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.
My preference is a code comment saying who added the code and what their affiliation is. I don't think this belongs in rendered documentation.
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.
Some subtle form of recognition seems fine with me. But if the method is identical to spencer's paper, then the suffix '_mc' might be too much.
@@ -425,3 +429,25 @@ def _golden_sect_DataFrame(params, VL, VH, func): | |||
raise Exception("EXCEPTION:iterations exceeded maximum (50)") | |||
|
|||
return func(df, 'V1'), df['V1'] | |||
|
|||
|
|||
def datetime_to_julian(times): |
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 don't like the duplication of datetime_to_djd
. Let's make the old function work with both datetime
and DatetimeIndex
. I suspect converting a datetime
input to a Timestamp
will do the job.
pvlib/test/test_tools.py
Outdated
import pytest | ||
|
||
import pvlib.tools |
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.
tools
is already imported on the next line.
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.
Ähh right! ;D
pvlib/test/test_solarposition.py
Outdated
@@ -754,8 +754,125 @@ def test_sun_rise_set_transit_geometric(expected_rise_set_spa, golden_mst): | |||
atol=np.abs(expected_transit_error).max()) | |||
|
|||
|
|||
# put numba tests at end of file to minimize reloading | |||
def test_spencer_mc(): |
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.
Please follow this test pattern so that we can be sure the output is correct to within some tolerance:
pvlib-python/pvlib/test/test_solarposition.py
Lines 420 to 455 in d68617c
def test_ephemeris_physical(expected_solpos, golden_mst): | |
times = pd.date_range(datetime.datetime(2003, 10, 17, 12, 30, 30), | |
periods=1, freq='D', tz=golden_mst.tz) | |
ephem_data = solarposition.ephemeris(times, golden_mst.latitude, | |
golden_mst.longitude, | |
pressure=82000, | |
temperature=11) | |
expected_solpos.index = times | |
expected_solpos = np.round(expected_solpos, 2) | |
ephem_data = np.round(ephem_data, 2) | |
assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) | |
def test_ephemeris_physical_dst(expected_solpos, golden): | |
times = pd.date_range(datetime.datetime(2003, 10, 17, 13, 30, 30), | |
periods=1, freq='D', tz=golden.tz) | |
ephem_data = solarposition.ephemeris(times, golden.latitude, | |
golden.longitude, pressure=82000, | |
temperature=11) | |
expected_solpos.index = times | |
expected_solpos = np.round(expected_solpos, 2) | |
ephem_data = np.round(ephem_data, 2) | |
assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) | |
def test_ephemeris_physical_no_tz(expected_solpos, golden_mst): | |
times = pd.date_range(datetime.datetime(2003, 10, 17, 19, 30, 30), | |
periods=1, freq='D') | |
ephem_data = solarposition.ephemeris(times, golden_mst.latitude, | |
golden_mst.longitude, | |
pressure=82000, | |
temperature=11) | |
expected_solpos.index = times | |
expected_solpos = np.round(expected_solpos, 2) | |
ephem_data = np.round(ephem_data, 2) | |
assert_frame_equal(expected_solpos, ephem_data[expected_solpos.columns]) |
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.
Good Idea. I will bring it in the next commit
""" | ||
Calculate the solar position using a python implementation of the | ||
Spencer (1972) formulation provided by meteocontrol | ||
|
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 recommend that we add a comment on the relative accuracy and speed of the function. Perhaps it belongs in a Notes
section below.
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.
a few places where it would be nice to collapse duplicate code with existing functions perhaps with some modifications to make them work better - thanks!
pvlib/solarposition.py
Outdated
eot -= 0.032077 * sin_gamma[0] | ||
eot -= 0.014615 * cos_gamma[1] | ||
eot -= 0.040849 * sin_gamma[1] | ||
eot *= 229.18 |
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 believe this section may be a repeat of equation_of_time_spencer71
:
day_angle = _calculate_simple_day_angle(dayofyear)
# convert from radians to minutes per day = 24[h/day] * 60[min/h] / 2 / pi
eot = (1440.0 / 2 / np.pi) * (
0.0000075 +
0.001868 * np.cos(day_angle) - 0.032077 * np.sin(day_angle) -
0.014615 * np.cos(2.0 * day_angle) - 0.040849 * np.sin(2.0 * day_angle)
)
return eot
I think we should try, if possible, to use DRY vs WET code -- can you pleases consider reusing the existing code functions?
EG: eot = equation_of_time_spencer71(dayofyear)
where I suspect that you can use the Julian dates you've calculated in place of day of year, or adjust them slightly
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.
Right. New implementatino uses the equation_of_time_spencer71 function.
Acually I am not really familiar with the Julian Days and Day Of Year usage. I will check it out if there are any ways to use existend code.
eot *= 229.18 | ||
|
||
# True local time | ||
tlt = (day_time + longitude / 15 + eot / 60) % 24 - 12 |
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 suspect this is similar as hour_angle
- perhaps there is a way to reuse the existing code, or modify it to suit your needs so we don't have duplicate code?
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.
looking in more detail they are the nearly the same:
tlt = hour_angle / 15.
but IMO this version here, looks a little easier to read, with the use of mod()
, so might be nice to replace the existing hour_angle
with this, assuming there is better or same performance
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 tried hour_angle
and it was not working in this code.
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.
But do you agree they are the same equation, that only differ by a factor of 1/15? we have noticed that different formulations have different "offsets" so perhaps adjusting the input may help? Or perhaps leave it, raise an issue to collapse this later, and maybe add a #TODO
in the code. If @cwhanse and @wholmgren are okay then so am I
@meteoDaniel just add |
@meteoDaniel please merge master into this PR, should take care of the test failures for |
I believe these calculations are identical to the ones in the "analytical" aka "geometric" solar position functions already in pvlib. (#583) |
OMG! @adriesse is correct! I guess I didn't review this very well. Might still be worth it, but would be better to use the existing "analytical/geometric" functions or make them better if there's room for improvement. azimuthpvlib-python/pvlib/solarposition.py Lines 1517 to 1524 in 03bb143
is the same as pvlib-python/pvlib/solarposition.py Lines 1237 to 1261 in e3635c2
zenithpvlib-python/pvlib/solarposition.py Lines 1490 to 1511 in 03bb143
is the same as https://github.com/pvlib/pvlib-python/blob/master/pvlib/solarposition.py#L1309-L1312 |
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.
As @adriesse pointed out, you have reproduced the analytical expressions
- update your branch from the latest pvlib master
- please replace code for zenith calc, julian time, hour angle, declination, etc, with a call to
solar_zenith_analytical(latitude, hourangle, declination)
- please replace code for azimuth calc, etc, with a call to
solar_azimuth_analytical(latitude, hourangle, declination, zenith)
- can you please provide a warning, similar to the one in the existing analytical functions, that the Spencer calculation does not include atmospheric refraction, and therefore it only returns extraterrestrial zenith, which can be up to 2-degrees different from apparent zenith.
thanks! still think this is a valuable contribution, would like to see it v0.7.0 !
zenith = np.pi / 2 - elevation | ||
|
||
# Compute the sun's azimuth angle. | ||
y = -(np.sin(latitude_radians) * np.sin(elevation) - np.sin(declination)) \ |
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.
@meteoDaniel can you please replace these lines
pvlib-python/pvlib/solarposition.py
Lines 1517 to 1524 in 03bb143
# Compute the sun's azimuth angle. | |
y = -(np.sin(latitude_radians) * np.sin(elevation) - np.sin(declination)) \ | |
/ (np.cos(latitude_radians) * np.cos(elevation)) | |
azimuth = np.arccos(y) | |
# Convert azimuth angle from 0-pi to 0-2pi. | |
tlt_filter = 0 <= tlt | |
azimuth[tlt_filter] = 2 * np.pi - azimuth[tlt_filter] |
with a call to
solar_azimuth_analytical(latitude, hourangle, declination, zenith)
?
Springer Science & Business Media, 2008. | ||
""" | ||
|
||
julians, julian_2000 = datetime_to_julian(times) |
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.
@meteoDaniel can you please replace all of these lines
pvlib-python/pvlib/solarposition.py
Lines 1490 to 1511 in 03bb143
julians, julian_2000 = datetime_to_julian(times) | |
julians_2000 = np.asarray(julians, dtype=np.float) - julian_2000 | |
latitude_radians = np.radians(latitude) | |
day_time = (julians_2000 % 1) * 24 | |
declination = np.array(declination_spencer71(times.dayofyear)) | |
# Equation of time (difference between standard time and solar time). | |
eot = np.array(equation_of_time_spencer71(times.dayofyear)) | |
# True local time | |
tlt = (day_time + longitude / 15 + eot / 60) % 24 - 12 | |
# Solar hour angle | |
ha = np.radians(tlt * 15) | |
# Calculate sun elevation. | |
sin_sun_elevation = ( | |
np.sin(declination) * np.sin(latitude_radians) + | |
np.cos(declination) * np.cos(latitude_radians) * np.cos(ha) | |
) |
with a call to
solar_zenith_analytical(latitude, hourangle, declination)
?
Maybe the title could be changed to something like: "Convenience function to calculate the complete solar position using the analytical/geometric methods." Not sure whether the name Spencer should be used. |
By the way, refraction reduces the apparent zenith angle at the horizon by about 0.5 degrees (not 2 degrees as noted above). |
Let's start fresh in a new PR if there's anything we want to continue here. |
pvlib python pull request guidelines
docs/sphinx/source/api.rst
for API changes.docs/sphinx/source/whatsnew
file for all changes.Brief description of the problem and proposed solution (if not already fully described in the issue linked to above):
A new way to calculate solar position. Let us benchmark this against
nrel_numba
. A first test against the default configuration shows the code runs nearly 100 times faster:1000 timesteps:
Default: 0:00:00.023037
spencer_mc: 0:00:00.003093
10000 timesteps:
Default: 0:00:00.178758
spencer_mc: 0:00:00.007183
100000 timesteps:
Default: 0:00:01.400652
spencer_mc: 0:00:00.049189