A free, fast and accurate Python package to compute the position of the Sun

# SolTrack for Python

A free, fast and simple Python package to compute the position of the Sun, as well as its rise and set times. SolTrack was originally written in C/C++ by Marc van der Sluys of the department of astrophysics of the Radboud University Nijmegen, the Netherlands and the Sustainable energy research group of the HAN University of Applied Sciences in Arnhem, the Netherlands (now at the Netherlands Institute for Nuclear and High-Energy Physics (Nikhef) and the Institute for Gravitational and Subatomic Physics (GRASP) at Utrecht University in the Netherlands), and Paul van Kan of the Sustainable energy research group of the HAN University of Applied Sciences in Arnhem, the Netherlands. The code has now been translated to pure Python and can be used under the conditions of the EUPL 1.2 licence.

SolTrack can perform up to 1.5 million position calculations per second on a single 3.4 GHz core of my laptop (the C version of SolTrack is about twice as fast) with an accuracy of 0.0030 ± 0.0016°.

## Installation

This package can be installed using pip install soltrack. This should automatically install the dependency packages numpy, pandas, astroconst and astrotool if they haven't been installed already (if you're not using a Python version older than 3.7, you will need to install dataclasses as well). If you are installing by hand, ensure that these packages are installed as well.

## Example use

### Update from v0.1.x to v0.2.0

The update from SolTrack v0.1.4 to v0.3.0 is not backwards compatible. Most public members/variables and methods/functions, as well as dummy arguments have been renamed in order to comply better with the Python standards. I have tried to put as much nuisance into a single update as possible (as opposed to in multiple updates, rather than as opposed to as little nuisance as possible), so that this will hopefully not happen again in the future. For public methods, the obsolescent old name is kept as an alias, and warnings are issued when used, instructing how to adapt your code. In many cases a variable or function name likeThis will have been replaced to one like_this, i.e. upper case replaced with an underscore and lower case. See the documentation for more details.

### Code example for a number or range of datetimes

# Create a Pandas DatetimeIndex range every 20 days 1hour and 10 minutes, in my timezone:
import pandas as pd
dti = pd.date_range('2022-01-01 01:00', '2022-12-31 23:00', freq='20d1h10min', tz='Europe/Amsterdam')

# Set the geographic location to Arnhem, the Netherlands (we'll use degrees in SolTrack):
geo_lon =  5.950270  # Positive -> east of Greenwich (degrees)
geo_lat = 51.987380  # Positive -> northern hemisphere (degrees)

# Create a SolTrack instance and specify preferences:
from soltrack import SolTrack
st = SolTrack(geo_lon, geo_lat, use_degrees=True)  # Use default values for all but use_degrees
st.set_date_time(dti)  # Pass my dates and times to SolTrack
st.compute_position()  # Compute the Sun's position
st.compute_rise_set()  # Compute the rise and set times of the Sun

# Print some selected results as arrays and create chaos:
if st.lt is not None:  # If local time was used
print('Local time:     ', *st.lt)  # Asterisk (*) unpacks the DTI

print('UTC:            ', *st.utc)
print('azimuth:        ', *st.azimuth)
print('altitude:       ', *st.altitude)
print('distance:       ', *st.distance)
print('riseTime:       ', *st.rise_time)
print('transTime:      ', *st.transit_time)
print('setTime:        ', *st.set_time)

# Store selected results in a Pandas DataFrame and print that in a more orderly fashion:
st.create_df(utc=True, jd=True, ecl=True, rts_pos=True)
with pd.option_context('display.max_columns',None, 'display.width',None):  # Want all columns
print(st.df)


### Code example for a single instant

Note that in most cases, you should use the vector option instead, as it is faster starting from calculations for two instances. See the section Performance for more details. The code listing below is provided for completeness only.

"""Example Python script to compute the position of the Sun and its rise and set times for a single instant
and demonstrate some other features."""

from soltrack import SolTrack
import datetime as dt
import pytz as tz

# Set the geographic location to Arnhem, the Netherlands:
geo_lon =  5.950270  # Positive -> east of Greenwich (degrees)
geo_lat = 51.987380  # Positive -> northern hemisphere (degrees)

st = SolTrack(geo_lon, geo_lat, use_degrees=True)  # Same as above, using default values for all but use_degrees.

# Set SolTrack date and time using separate (UTC!) year, month, day, hour, minute and second variables:
st.set_date_and_time(2023, 7, 16,  6, 2, 49.217348)  # Date: 2023-07-16, time: 06:02:49.217348 UTC

# Alternatively, use a (localised) datetime object:
cet     = tz.timezone('Europe/Amsterdam')
my_date = dt.datetime(2023, 7, 16,  8, 2, 49, 217348)  # Same as above, in local time for TZ=+2 (08:02:49.217348 LT)
my_date = cet.localize(my_date)
st.set_date_time(my_date)  # Set SolTrack date and time using a Python datetime object.

# Compute the Sun's position:
st.compute_position()

# Compute the rise and set times of the Sun:
st.compute_rise_set()

# Write results to standard output:
print("Location:   %0.3lf E, %0.3lf N"  % (st.geo_longitude, st.geo_latitude))
# print("Date/time:  %4d-%02d-%02d %02d:%02d:%09.6lf" % (st.year, st.month, st.day,  st.hour, st.minute, st.second))
print("Date/time:  %s"                  % my_date)
print("JD:         %0.11lf"             % (st.julian_day))
print()

print("Ecliptic longitude, latitude:        %10.6lf° %10.6lf°"     % (st.longitude, 0.0))  # Note: latitude is always 0 in this model
print("Distance:                            %10.6lf°"              % (st.distance))
print("Right ascension, declination:        %10.6lf° %10.6lf°"     % (st._right_ascension_uncorr, st._declination_uncorr))
print("Uncorrected altitude:                            %10.6lf°"  % (st._altitude_uncorr))
print("Corrected azimuth, altitude:         %10.6lf° %10.6lf°"     % (st.azimuth, st.altitude))
print("Corrected hour angle, declination:   %10.6lf° %10.6lf°"     % (st.hour_angle, st.declination))
print()

print("Rise time:      %s,    azimuth:   %11.5lf" % (*st.rise_time,     *st.rise_azimuth))
print("Transit time:   %s,    altitude:  %11.5lf" % (*st.transit_time,  *st.transit_altitude))
print("Set time:       %s,    azimuth:   %11.5lf" % (*st.set_time,      *st.set_azimuth))
print()

# Change the location whilst keeping the same SolTrack object:
st.set_location(geo_lon, -geo_lat)

# Compute the current position of the Sun for the new location:
st.now()
st.compute_position()

print("Location:   %0.3lf E, %0.3lf N"                         % (st.geo_longitude, st.geo_latitude))
print("Date (UT):  %4d-%02d-%02d"                              % (st.year, st.month, st.day))
print("Time (UT):  %02d:%02d:%09.6lf"                          % (st.hour, st.minute, st.second))
print("Corrected azimuth, altitude:         %10.6lf° %10.6lf°" % (st.azimuth, st.altitude))


### Code example for SolTrack v0.1.x (old)

"""Example Python script to compute the position of the Sun and its rise and set times for a single instant."""

import soltrack as st

# Set preferences (all are False by default):
useDegrees             = True   # Input (geographic position) and output are in degrees
useNorthEqualsZero     = True   # Azimuth: 0 = South, pi/2 (90deg) = West  ->  0 = North, pi/2 (90deg) = East
computeRefrEquatorial  = True   # Compure refraction-corrected equatorial coordinates (Hour angle, declination)
computeDistance        = True   # Compute the distance to the Sun

# Set up geographical location (in degrees, since useDegrees=True) in a SolTrack Location dataclass object:
loc = st.Location(5.950270, 51.987380)  # longitude (>0: east of Greenwich),  latitude (>0: northern hemisphere)

# Set (UT!) date and time in a SolTrack Time dataclass object:
time = st.Time(2045, 7, 16,  6, 2, 49.217348)  # Date: 2045-07-16, time: 06:02:49.217348

# Compute positions - returns a st.Position object:
pos = st.computeSunPosition(loc, time, useDegrees, useNorthEqualsZero, computeRefrEquatorial, computeDistance)

# Compute rise and set times - returns a st.RiseSet object:
riseSet = st.computeSunRiseSet(loc, time, 0.0, useDegrees, useNorthEqualsZero)

# Write results to standard output:
print("Location:  %0.3lf E, %0.3lf N" % (loc.longitude, loc.latitude))
print("Date:      %4d %2d %2d" % (time.year, time.month, time.day))
print("Time:      %2d %2d %9.6lf" % (time.hour, time.minute, time.second))
print("JD:        %0.11lf" % (pos.julianDay))
print()

print("Ecliptic longitude, latitude:        %10.6lf° %10.6lf°" % (pos.longitude, 0.0))
print("Right ascension, declination:        %10.6lf° %10.6lf°" % (pos.rightAscension, pos.declination))
print("Uncorrected altitude:                            %10.6lf°" % (pos.altitude))
print("Corrected azimuth, altitude:         %10.6lf° %10.6lf°" % (pos.azimuthRefract, pos.altitudeRefract))
print("Corrected hour angle, declination:   %10.6lf° %10.6lf°" % (pos.hourAngleRefract, pos.declinationRefract))
print()

print("Rise time:      %11.5lf,    azimuth:   %11.5lf" % (riseSet.riseTime, riseSet.riseAzimuth))
print("Transit time:   %11.5lf,    altitude:  %11.5lf" % (riseSet.transitTime, riseSet.transitAltitude))
print("Set time:       %11.5lf,    azimuth:   %11.5lf" % (riseSet.setTime, riseSet.setAzimuth))
print()


## Performance

### Performance of position calculations for an array/vector or range of instants

From v0.2.0 on, SolTrack uses Pandas to allow the use of arrays or vectors of datetimes (Series, DatetimeIndex, ndarrays of datetime64, ...). This causes some overhead, which is relatively more significant for small numbers. SolTrack performs fastest (per position calculation and on my laptop) for about 105-106 calculations:

Ncalc (-) Time (s) Speed (/s) Speed (millions/s)
1e0 0.00081 1.23e+03 0.001
1e1 0.00080 1.25e+04 0.013
1e2 0.00105 9.52e+04 0.095
1e3 0.00256 3.91e+05 0.39
1e4 0.0132 7.58e+05 0.76
1e5 0.0688 1.45e+06 1.45
3e5 0.1960 1.53e+06 1.53
1e6 0.6660 1.50e+06 1.50
3e6 2.068 1.45e+06 1.45
1e7 8.691 1.15e+06 1.15

These benchmarks were done on a single CPU core, capped at 3.4GHz. The cpu and core were always the same, and the minimum of 10 benchmarks is listed. Timezone-naive datetimes (representing UTC) were used; using timezone-aware datetimes slows down the code by a factor of ~4.6 compared to the numbers in the table.

### Performance of calculations of rise, transit and set times

Benchmarks for rise, transit and set computations were performed in the same manner as above, but using return_datetimes=False in the st.compute_rise_set() call, which returns rise, transit and set times in decimal hours. If instead datetimes are desired, the code becomes a factor of about 3.1 times slower. The use of utc=True has little effect, and using timezone-aware datetimes instead of timezone-naive ones adds 6.1% to the computational times listed below.

Ncalc (-) Time (s) Speed (/s)
1 0.0020 500.0
10 0.0134 746.3
100 0.1247 801.9
1000 1.207 828.5
1e4 12.00 833.3

### Performance of position calculation for a single instant

Because of the overhead of Pandas and datetime-like objects, SolTrack has actually slowed down by a factor of ~10.4 between versions 0.1.4 and 0.2.0 when doing single calculations. I don't consider that a big issue, since a single position calculation still takes less than a millisecond, so that the effect is not noticable for humans. It does become noticable for large numbers, but then arrays can be used, which make the code ~1000x faster. In fact, the array version is faster starting from two iterations, so there is usually very little reason to use the single-datetime option.

Single calculations can be made about 19% faster by providing datetimes in UTC and specifying utc=True in the st.set_date_time() method. The use of timezone-naive versus timezone-aware datetimes has little influence on the performance for single calculations, and calculation times scale linearly when doing multiple calls (since the must then be done in a Python loop).

For single datetimes, 1000 calculations take about 0.68 seconds on the single CPU core of my laptop capped at 3.4 GHz, and about 0.55 seconds with utc=True.

## Author and licence

• AstroConst: a Python package that provides astronomical constants.
• AstroTool: a Python package for astronomical calculations in Python or on the command line.
• elp-mpp02: accurate Moon positions using the lunar solution ELP/MPP02 in Python.
• libTheSky: a Fortran library to compute the positions of celestial bodies (Sun, Moon, planets, stars, asteroids, comets) and events (e.g. lunar phases) with great accuracy.
• SolarEnergy: A Python module to do simple modelling in the field of solar energy.

## References

Copyright (c) 2019-2022 Marc van der Sluys

## Project details

Uploaded source
Uploaded py3