A fast, vectorized Python port of suncalc.js
Project description
suncalc-py
A fast, vectorized Python implementation of suncalc.js
for
calculating sun position and sunlight phases (times for sunrise, sunset, dusk,
etc.) for the given location and time.
While other similar libraries exist, I didn't originally encounter any that met my requirements of being both openly-licensed and vectorized 1
Install
pip install suncalc
Using
Example
suncalc
is designed to work both with single values and with arrays of values.
First, import the module:
from suncalc import get_position, get_times
from datetime import datetime
There are currently two methods: get_position
, to get the sun azimuth and
altitude for a given date and position, and get_times
, to get sunlight phases
for a given date and position.
date = datetime.now()
lon = 20
lat = 45
get_position(date, lon, lat)
# {'azimuth': -0.8619668996997687, 'altitude': 0.5586446727994595}
get_times(date, lon, lat)
# {'solar_noon': Timestamp('2020-11-20 08:47:08.410863770'),
# 'nadir': Timestamp('2020-11-19 20:47:08.410863770'),
# 'sunrise': Timestamp('2020-11-20 03:13:22.645455322'),
# 'sunset': Timestamp('2020-11-20 14:20:54.176272461'),
# 'sunrise_end': Timestamp('2020-11-20 03:15:48.318936035'),
# 'sunset_start': Timestamp('2020-11-20 14:18:28.502791748'),
# 'dawn': Timestamp('2020-11-20 02:50:00.045539551'),
# 'dusk': Timestamp('2020-11-20 14:44:16.776188232'),
# 'nautical_dawn': Timestamp('2020-11-20 02:23:10.019832520'),
# 'nautical_dusk': Timestamp('2020-11-20 15:11:06.801895264'),
# 'night_end': Timestamp('2020-11-20 01:56:36.144269287'),
# 'night': Timestamp('2020-11-20 15:37:40.677458252'),
# 'golden_hour_end': Timestamp('2020-11-20 03:44:46.795967773'),
# 'golden_hour': Timestamp('2020-11-20 13:49:30.025760010')}
These methods also work for arrays of data, and since the implementation is vectorized it's much faster than a for loop in Python.
import pandas as pd
df = pd.DataFrame({
'date': [date] * 10,
'lon': [lon] * 10,
'lat': [lat] * 10
})
pd.DataFrame(get_position(df['date'], df['lon'], df['lat']))
# azimuth altitude
# 0 -1.485509 -1.048223
# 1 -1.485509 -1.048223
# ...
pd.DataFrame(get_times(df['date'], df['lon'], df['lat']))['solar_noon']
# 0 2020-11-20 08:47:08.410863872+00:00
# 1 2020-11-20 08:47:08.410863872+00:00
# ...
# Name: solar_noon, dtype: datetime64[ns, UTC]
If you want to join this data back to your DataFrame
, you can use pd.concat
:
times = pd.DataFrame(get_times(df['date'], df['lon'], df['lat']))['solar_noon']
pd.concat([df, times], axis=1)
API
get_position
Calculate sun position for a given date and latitude/longitude
date
(datetime
or a pandas series of datetimes): date and time to find sun position oflng
(float
or numpy array offloat
): longitude to find sun position oflat
(float
or numpy array offloat
): latitude to find sun position of
get_times
-
date
(datetime
or a pandas series of datetimes): date and time to find sunlight phases of -
lng
(float
or numpy array offloat
): longitude to find sunlight phases of -
lat
(float
or numpy array offloat
): latitude to find sunlight phases of -
height
(float
or numpy array offloat
, default0
): observer height in meters -
times
(Iterable[Tuple[float, str, str]]
): an iterable defining the angle above the horizon and strings for custom sunlight phases. The default is:# (angle, morning name, evening name) DEFAULT_TIMES = [ (-0.833, 'sunrise', 'sunset'), (-0.3, 'sunrise_end', 'sunset_start'), (-6, 'dawn', 'dusk'), (-12, 'nautical_dawn', 'nautical_dusk'), (-18, 'night_end', 'night'), (6, 'golden_hour_end', 'golden_hour') ]
Benchmark
This benchmark is to show that the vectorized implementation is nearly 100x faster than a for loop in Python.
First set up a DataFrame
with random data. Here I create 100,000 rows.
from suncalc import get_position, get_times
import pandas as pd
def random_dates(start, end, n=10):
"""Create an array of random dates"""
start_u = start.value//10**9
end_u = end.value//10**9
return pd.to_datetime(np.random.randint(start_u, end_u, n), unit='s')
start = pd.to_datetime('2015-01-01')
end = pd.to_datetime('2018-01-01')
dates = random_dates(start, end, n=100_000)
lons = np.random.uniform(low=-179, high=179, size=(100_000,))
lats = np.random.uniform(low=-89, high=89, size=(100_000,))
df = pd.DataFrame({'date': dates, 'lat': lats, 'lon': lons})
Then compute SunCalc.get_position
two ways: the first using the vectorized
implementation and the second using df.apply
, which is equivalent to a for
loop. The first is more than 100x faster than the second.
%timeit get_position(df['date'], df['lon'], df['lat'])
# 41.4 ms ± 437 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit df.apply(lambda row: get_position(row['date'], row['lon'], row['lat']), axis=1)
# 4.89 s ± 184 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Likewise, compute SunCalc.get_times
the same two ways: first using the
vectorized implementation and the second using df.apply
. The first is 2800x
faster than the second!
%timeit get_times(df['date'], df['lon'], df['lat'])
# 55.3 ms ± 1.91 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%time df.apply(lambda row: get_times(row['date'], row['lon'], row['lat']), axis=1)
# CPU times: user 2min 33s, sys: 288 ms, total: 2min 34s
# Wall time: 2min 34s
1: pyorbital
looks great but is
GPL3-licensed; pysolar
is also
GPL3-licensed. pyEphem
is LGPL3-licensed. I
recently discovered sunpy
and
astropy
, both of which probably would've
worked but I didn't see them at first...
Changelog
[0.1.0] - 2020-11-19
- Initial release on PyPI
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.