#!/usr/bin/python # Copyright 2007-2010 Brandon Stafford # # This file is part of Pysolar. # # Pysolar is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # Pysolar is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with Pysolar. If not, see <http://www.gnu.org/licenses/>. """Solar geometry functions This module contains the most important functions for calculation of the position of the sun. """ import math import datetime import constants import julian import radiation #if __name__ == "__main__": def SolarTest(): latitude_deg = 42.364908 longitude_deg = -71.112828 d = datetime.datetime.utcnow() thirty_minutes = datetime.timedelta(hours = 0.5) for i in range(48): timestamp = d.ctime() altitude_deg = GetAltitude(latitude_deg, longitude_deg, d) azimuth_deg = GetAzimuth(latitude_deg, longitude_deg, d) power = radiation.GetRadiationDirect(d, altitude_deg) if (altitude_deg > 0): print timestamp, "UTC", altitude_deg, azimuth_deg, power d = d + thirty_minutes def EquationOfTime(day): b = (2 * math.pi / 364.0) * (day - 81) return (9.87 * math.sin(2 *b)) - (7.53 * math.cos(b)) - (1.5 * math.sin(b)) def GetAberrationCorrection(radius_vector): # r is earth radius vector [astronomical units] return -20.4898/(3600.0 * radius_vector) def GetAltitude(latitude_deg, longitude_deg, utc_datetime, elevation = 0, temperature_celsius = 25, pressure_millibars = 1013.25): '''See also the faster, but less accurate, GetAltitudeFast()''' # location-dependent calculations projected_radial_distance = GetProjectedRadialDistance(elevation, latitude_deg) projected_axial_distance = GetProjectedAxialDistance(elevation, latitude_deg) # time-dependent calculations jd = julian.GetJulianDay(utc_datetime) jde = julian.GetJulianEphemerisDay(jd, 65) jce = julian.GetJulianEphemerisCentury(jde) jme = julian.GetJulianEphemerisMillenium(jce) geocentric_latitude = GetGeocentricLatitude(jme) geocentric_longitude = GetGeocentricLongitude(jme) radius_vector = GetRadiusVector(jme) aberration_correction = GetAberrationCorrection(radius_vector) equatorial_horizontal_parallax = GetEquatorialHorizontalParallax(radius_vector) nutation = GetNutation(jde) apparent_sidereal_time = GetApparentSiderealTime(jd, jme, nutation) true_ecliptic_obliquity = GetTrueEclipticObliquity(jme, nutation) # calculations dependent on location and time apparent_sun_longitude = GetApparentSunLongitude(geocentric_longitude, nutation, aberration_correction) geocentric_sun_right_ascension = GetGeocentricSunRightAscension(apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude) geocentric_sun_declination = GetGeocentricSunDeclination(apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude) local_hour_angle = GetLocalHourAngle(apparent_sidereal_time, longitude_deg, geocentric_sun_right_ascension) parallax_sun_right_ascension = GetParallaxSunRightAscension(projected_radial_distance, equatorial_horizontal_parallax, local_hour_angle, geocentric_sun_declination) topocentric_local_hour_angle = GetTopocentricLocalHourAngle(local_hour_angle, parallax_sun_right_ascension) topocentric_sun_declination = GetTopocentricSunDeclination(geocentric_sun_declination, projected_axial_distance, equatorial_horizontal_parallax, parallax_sun_right_ascension, local_hour_angle) topocentric_elevation_angle = GetTopocentricElevationAngle(latitude_deg, topocentric_sun_declination, topocentric_local_hour_angle) refraction_correction = GetRefractionCorrection(pressure_millibars, temperature_celsius, topocentric_elevation_angle) return topocentric_elevation_angle + refraction_correction def GetAltitudeFast(latitude_deg, longitude_deg, utc_datetime): # expect 19 degrees for solar.GetAltitude(42.364908,-71.112828,datetime.datetime(2007, 2, 18, 20, 13, 1, 130320)) day = GetDayOfYear(utc_datetime) declination_rad = math.radians(GetDeclination(day)) latitude_rad = math.radians(latitude_deg) hour_angle = GetHourAngle(utc_datetime, longitude_deg) first_term = math.cos(latitude_rad) * math.cos(declination_rad) * math.cos(math.radians(hour_angle)) second_term = math.sin(latitude_rad) * math.sin(declination_rad) return math.degrees(math.asin(first_term + second_term)) def GetApparentSiderealTime(julian_day, jme, nutation): return GetMeanSiderealTime(julian_day) + nutation['longitude'] * math.cos(GetTrueEclipticObliquity(jme, nutation)) def GetApparentSunLongitude(geocentric_longitude, nutation, ab_correction): return geocentric_longitude + nutation['longitude'] + ab_correction def GetAzimuth(latitude_deg, longitude_deg, utc_datetime, elevation = 0): # location-dependent calculations projected_radial_distance = GetProjectedRadialDistance(elevation, latitude_deg) projected_axial_distance = GetProjectedAxialDistance(elevation, latitude_deg) # time-dependent calculations jd = julian.GetJulianDay(utc_datetime) jde = julian.GetJulianEphemerisDay(jd, 65) jce = julian.GetJulianEphemerisCentury(jde) jme = julian.GetJulianEphemerisMillenium(jce) geocentric_latitude = GetGeocentricLatitude(jme) geocentric_longitude = GetGeocentricLongitude(jme) radius_vector = GetRadiusVector(jme) aberration_correction = GetAberrationCorrection(radius_vector) equatorial_horizontal_parallax = GetEquatorialHorizontalParallax(radius_vector) nutation = GetNutation(jde) apparent_sidereal_time = GetApparentSiderealTime(jd, jme, nutation) true_ecliptic_obliquity = GetTrueEclipticObliquity(jme, nutation) # calculations dependent on location and time apparent_sun_longitude = GetApparentSunLongitude(geocentric_longitude, nutation, aberration_correction) geocentric_sun_right_ascension = GetGeocentricSunRightAscension(apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude) geocentric_sun_declination = GetGeocentricSunDeclination(apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude) local_hour_angle = GetLocalHourAngle(apparent_sidereal_time, longitude_deg, geocentric_sun_right_ascension) parallax_sun_right_ascension = GetParallaxSunRightAscension(projected_radial_distance, equatorial_horizontal_parallax, local_hour_angle, geocentric_sun_declination) topocentric_local_hour_angle = GetTopocentricLocalHourAngle(local_hour_angle, parallax_sun_right_ascension) topocentric_sun_declination = GetTopocentricSunDeclination(geocentric_sun_declination, projected_axial_distance, equatorial_horizontal_parallax, parallax_sun_right_ascension, local_hour_angle) return 180 - GetTopocentricAzimuthAngle(topocentric_local_hour_angle, latitude_deg, topocentric_sun_declination) def GetAzimuthFast(latitude_deg, longitude_deg, utc_datetime): # expect -50 degrees for solar.GetAzimuth(42.364908,-71.112828,datetime.datetime(2007, 2, 18, 20, 18, 0, 0)) day = GetDayOfYear(utc_datetime) declination_rad = math.radians(GetDeclination(day)) latitude_rad = math.radians(latitude_deg) hour_angle_rad = math.radians(GetHourAngle(utc_datetime, longitude_deg)) altitude_rad = math.radians(GetAltitude(latitude_deg, longitude_deg, utc_datetime)) azimuth_rad = math.asin(math.cos(declination_rad) * math.sin(hour_angle_rad) / math.cos(altitude_rad)) if(math.cos(hour_angle_rad) >= (math.tan(declination_rad) / math.tan(latitude_rad))): return math.degrees(azimuth_rad) else: return (180 - math.degrees(azimuth_rad)) def GetCoefficient(jme, constant_array): return sum([constant_array[i-1][0] * math.cos(constant_array[i-1][1] + (constant_array[i-1][2] * jme)) for i in range(len(constant_array))]) def GetDayOfYear(utc_datetime): year_start = datetime.datetime(utc_datetime.year, 1, 1, tzinfo=utc_datetime.tzinfo) delta = (utc_datetime - year_start) return delta.days def GetDeclination(day): '''The declination of the sun is the angle between Earth's equatorial plane and a line between the Earth and the sun. The declination of the sun varies between 23.45 degrees and -23.45 degrees, hitting zero on the equinoxes and peaking on the solstices. ''' return 23.45 * math.sin((2 * math.pi / 365.0) * (day - 81)) def GetEquatorialHorizontalParallax(radius_vector): return 8.794 / (3600 / radius_vector) def GetFlattenedLatitude(latitude): latitude_rad = math.radians(latitude) return math.degrees(math.atan(0.99664719 * math.tan(latitude_rad))) # Geocentric functions calculate angles relative to the center of the earth. def GetGeocentricLatitude(jme): return -1 * GetHeliocentricLatitude(jme) def GetGeocentricLongitude(jme): return (GetHeliocentricLongitude(jme) + 180) % 360 def GetGeocentricSunDeclination(apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude): apparent_sun_longitude_rad = math.radians(apparent_sun_longitude) true_ecliptic_obliquity_rad = math.radians(true_ecliptic_obliquity) geocentric_latitude_rad = math.radians(geocentric_latitude) a = math.sin(geocentric_latitude_rad) * math.cos(true_ecliptic_obliquity_rad) b = math.cos(geocentric_latitude_rad) * math.sin(true_ecliptic_obliquity_rad) * math.sin(apparent_sun_longitude_rad) delta = math.asin(a + b) return math.degrees(delta) def GetGeocentricSunRightAscension(apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude): apparent_sun_longitude_rad = math.radians(apparent_sun_longitude) true_ecliptic_obliquity_rad = math.radians(true_ecliptic_obliquity) geocentric_latitude_rad = math.radians(geocentric_latitude) a = math.sin(apparent_sun_longitude_rad) * math.cos(true_ecliptic_obliquity_rad) b = math.tan(geocentric_latitude_rad) * math.sin(true_ecliptic_obliquity_rad) c = math.cos(apparent_sun_longitude_rad) alpha = math.atan2((a - b), c) return math.degrees(alpha) % 360 # Heliocentric functions calculate angles relative to the center of the sun. def GetHeliocentricLatitude(jme): b0 = GetCoefficient(jme, constants.B0) b1 = GetCoefficient(jme, constants.B1) return math.degrees((b0 + (b1 * jme)) / 10 ** 8) def GetHeliocentricLongitude(jme): l0 = GetCoefficient(jme, constants.L0) l1 = GetCoefficient(jme, constants.L1) l2 = GetCoefficient(jme, constants.L2) l3 = GetCoefficient(jme, constants.L3) l4 = GetCoefficient(jme, constants.L4) l5 = GetCoefficient(jme, constants.L5) l = (l0 + l1 * jme + l2 * jme ** 2 + l3 * jme ** 3 + l4 * jme ** 4 + l5 * jme ** 5) / 10 ** 8 return math.degrees(l) % 360 def GetHourAngle(utc_datetime, longitude_deg): solar_time = GetSolarTime(longitude_deg, utc_datetime) return 15 * (12 - solar_time) def GetIncidenceAngle(topocentric_zenith_angle, slope, slope_orientation, topocentric_azimuth_angle): tza_rad = math.radians(topocentric_zenith_angle) slope_rad = math.radians(slope) so_rad = math.radians(slope_orientation) taa_rad = math.radians(topocentric_azimuth_angle) return math.degrees(math.acos(math.cos(tza_rad) * math.cos(slope_rad) + math.sin(slope_rad) * math.sin(tza_rad) * math.cos(taa_rad - math.pi - so_rad))) def GetLocalHourAngle(apparent_sidereal_time, longitude, geocentric_sun_right_ascension): return (apparent_sidereal_time + longitude - geocentric_sun_right_ascension) % 360 def GetMeanSiderealTime(julian_day): # This function doesn't agree with Andreas and Reda as well as it should. Works to ~5 sig figs in current unit test jc = julian.GetJulianCentury(julian_day) sidereal_time = 280.46061837 + (360.98564736629 * (julian_day - 2451545.0)) + (0.000387933 * jc ** 2) - (jc ** 3 / 38710000) return sidereal_time % 360 def GetNutationAberrationXY(jce, i, x): y = constants.aberration_sin_terms sigmaxy = 0.0 for j in range(len(x)): sigmaxy += x[j] * y[i][j] return sigmaxy def GetNutation(jde): abcd = constants.nutation_coefficients jce = julian.GetJulianEphemerisCentury(jde) nutation_long = [] nutation_oblique = [] x = PrecalculateAberrations(constants.buildPolyDict(), jce) for i in range(len(abcd)): sigmaxy = GetNutationAberrationXY(jce, i, x) nutation_long.append((abcd[i][0] + (abcd[i][1] * jce)) * math.sin(math.radians(sigmaxy))) nutation_oblique.append((abcd[i][2] + (abcd[i][3] * jce)) * math.cos(math.radians(sigmaxy))) # 36000000 scales from 0.0001 arcseconds to degrees nutation = {'longitude' : sum(nutation_long)/36000000.0, 'obliquity' : sum(nutation_oblique)/36000000.0} return nutation def GetParallaxSunRightAscension(projected_radial_distance, equatorial_horizontal_parallax, local_hour_angle, geocentric_sun_declination): prd = projected_radial_distance ehp_rad = math.radians(equatorial_horizontal_parallax) lha_rad = math.radians(local_hour_angle) gsd_rad = math.radians(geocentric_sun_declination) a = -1 * prd * math.sin(ehp_rad) * math.sin(lha_rad) b = math.cos(gsd_rad) - prd * math.sin(ehp_rad) * math.cos(lha_rad) parallax = math.atan2(a, b) return math.degrees(parallax) def GetProjectedRadialDistance(elevation, latitude): flattened_latitude_rad = math.radians(GetFlattenedLatitude(latitude)) latitude_rad = math.radians(latitude) return math.cos(flattened_latitude_rad) + (elevation * math.cos(latitude_rad) / constants.earth_radius) def GetProjectedAxialDistance(elevation, latitude): flattened_latitude_rad = math.radians(GetFlattenedLatitude(latitude)) latitude_rad = math.radians(latitude) return 0.99664719 * math.sin(flattened_latitude_rad) + (elevation * math.sin(latitude_rad) / constants.earth_radius) def GetRadiusVector(jme): r0 = GetCoefficient(jme, constants.R0) r1 = GetCoefficient(jme, constants.R1) r2 = GetCoefficient(jme, constants.R2) r3 = GetCoefficient(jme, constants.R3) r4 = GetCoefficient(jme, constants.R4) return (r0 + r1 * jme + r2 * jme ** 2 + r3 * jme ** 3 + r4 * jme ** 4) / 10 ** 8 def GetRefractionCorrection(pressure_millibars, temperature_celsius, topocentric_elevation_angle): tea = topocentric_elevation_angle temperature_kelvin = temperature_celsius + 273.15 a = pressure_millibars * 283.0 * 1.02 b = 1010.0 * temperature_kelvin * 60.0 * math.tan(math.radians(tea + (10.3/(tea + 5.11)))) return a / b def GetSolarTime(longitude_deg, utc_datetime): day = GetDayOfYear(utc_datetime) return (((utc_datetime.hour * 60) + utc_datetime.minute + (4 * longitude_deg) + EquationOfTime(day))/60) # Topocentric functions calculate angles relative to a location on the surface of the earth. def GetTopocentricAzimuthAngle(topocentric_local_hour_angle, latitude, topocentric_sun_declination): """Measured eastward from north""" tlha_rad = math.radians(topocentric_local_hour_angle) latitude_rad = math.radians(latitude) tsd_rad = math.radians(topocentric_sun_declination) a = math.sin(tlha_rad) b = math.cos(tlha_rad) * math.sin(latitude_rad) - math.tan(tsd_rad) * math.cos(latitude_rad) return 180.0 + math.degrees(math.atan2(a, b)) % 360 def GetTopocentricElevationAngle(latitude, topocentric_sun_declination, topocentric_local_hour_angle): latitude_rad = math.radians(latitude) tsd_rad = math.radians(topocentric_sun_declination) tlha_rad = math.radians(topocentric_local_hour_angle) return math.degrees(math.asin((math.sin(latitude_rad) * math.sin(tsd_rad)) + math.cos(latitude_rad) * math.cos(tsd_rad) * math.cos(tlha_rad))) def GetTopocentricLocalHourAngle(local_hour_angle, parallax_sun_right_ascension): return local_hour_angle - parallax_sun_right_ascension def GetTopocentricSunDeclination(geocentric_sun_declination, projected_axial_distance, equatorial_horizontal_parallax, parallax_sun_right_ascension, local_hour_angle): gsd_rad = math.radians(geocentric_sun_declination) pad = projected_axial_distance ehp_rad = math.radians(equatorial_horizontal_parallax) psra_rad = math.radians(parallax_sun_right_ascension) lha_rad = math.radians(local_hour_angle) a = (math.sin(gsd_rad) - pad * math.sin(ehp_rad)) * math.cos(psra_rad) b = math.cos(gsd_rad) - (pad * math.sin(ehp_rad) * math.cos(lha_rad)) return math.degrees(math.atan2(a, b)) def GetTopocentricSunRightAscension(projected_radial_distance, equatorial_horizontal_parallax, local_hour_angle, apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude): gsd = GetGeocentricSunDeclination(apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude) psra = GetParallaxSunRightAscension(projected_radial_distance, equatorial_horizontal_parallax, local_hour_angle, gsd) gsra = GetGeocentricSunRightAscension(apparent_sun_longitude, true_ecliptic_obliquity, geocentric_latitude) return psra + gsra def GetTopocentricZenithAngle(latitude, topocentric_sun_declination, topocentric_local_hour_angle, pressure_millibars, temperature_celsius): tea = GetTopocentricElevationAngle(latitude, topocentric_sun_declination, topocentric_local_hour_angle) return 90 - tea - GetRefractionCorrection(pressure_millibars, temperature_celsius, tea) def GetTrueEclipticObliquity(jme, nutation): u = jme/10.0 mean_obliquity = 84381.448 - (4680.93 * u) - (1.55 * u ** 2) + (1999.25 * u ** 3) \ - (51.38 * u ** 4) -(249.67 * u ** 5) - (39.05 * u ** 6) + (7.12 * u ** 7) \ + (27.87 * u ** 8) + (5.79 * u ** 9) + (2.45 * u ** 10) return (mean_obliquity / 3600.0) + nutation['obliquity'] def PrecalculateAberrations(p, jce): x = [] # order of 5 x.append lines below is important x.append(p['MeanElongationOfMoon'](jce)) x.append(p['MeanAnomalyOfSun'](jce)) x.append(p['MeanAnomalyOfMoon'](jce)) x.append(p['ArgumentOfLatitudeOfMoon'](jce)) x.append(p['LongitudeOfAscendingNode'](jce)) return x