open-wether / Sources /App /Helper /Solar /SolarPositionAlgorithm.swift
soiz1's picture
Migrated from GitHub
6ee917b verified
import Foundation
import CHelper
/**
Solar position calculation based on the NREL Solar Position Algorithm SPA
https://www.nrel.gov/docs/fy08osti/34302.pdf
Only solar declination and equation of time are calculated.
The Swift version is approx 5 times faster than C by skipping unnecessary calculations.
Calculation of 50 years hourly solar position requires roughly 700ms.
*/
struct SolarPositionAlgorithm {
/// Calculate solar position for a given timerange
static func sunPosition(timerange: TimerangeDt) -> (declination: [Float], equationOfTime: [Float]) {
var declination = [Float]()
var equationOfTime = [Float]()
declination.reserveCapacity(timerange.count)
equationOfTime.reserveCapacity(timerange.count)
let spa = SolarPositionAlgorithm()
for time in timerange {
/*let date = time.toComponents()
var a = spa_data(
year: Int32(date.year),
month: Int32(date.month),
day: Int32(date.day),
hour: Int32(time.hour),
minute: Int32(time.minute),
second: Double(time.second),
delta_ut1: 0,
delta_t: 60,
timezone: 0,
longitude: 0, latitude: 0, elevation: 0, pressure: 1050, temperature: 20,
slope: 0, azm_rotation: 0, atmos_refract: 0.5667, function: Int32(SPA_ZA_RTS),
jd: 0, jc: 0, jde: 0, jce: 0, jme: 0, l: 0, b: 0, r: 0, theta: 0, beta: 0,
x0: 0, x1: 0, x2: 0, x3: 0, x4: 0, del_psi: 0, del_epsilon: 0, epsilon0: 0,
epsilon: 0, del_tau: 0, lamda: 0, nu0: 0, nu: 0, alpha: 0, delta: 0, h: 0, xi: 0,
del_alpha: 0, delta_prime: 0, alpha_prime: 0, h_prime: 0, e0: 0, del_e: 0, e: 0,
eot: 0, srha: 0, ssha: 0, sta: 0, zenith: 0, azimuth_astro: 0, azimuth: 0, incidence: 0,
suntransit: 0, sunrise: 0, sunset: 0)
guard spa_calculate(&a) == 0 else {
fatalError("SPA failed")
}*/
let a = spa.calculate(julianDate: time.julianDate)
declination.append(Float(a.delta))
equationOfTime.append(Float(a.eot))
}
return (declination, equationOfTime)
}
/*static func zenith(lat: Float, lon: Float, time: Timestamp) -> Float {
let date = time.toComponents()
var a = spa_data(
year: Int32(date.year),
month: Int32(date.month),
day: Int32(date.day),
hour: Int32(time.hour),
minute: Int32(time.minute),
second: Double(time.second),
delta_ut1: 0,
delta_t: 60,
timezone: 0,
longitude: Double(lon), latitude: Double(lat), elevation: 0, pressure: 1050, temperature: 20,
slope: 0, azm_rotation: 0, atmos_refract: 0.5667, function: Int32(SPA_ZA), jd: 0, jc: 0, jde: 0,
jce: 0, jme: 0, l: 0, b: 0, r: 0, theta: 0, beta: 0, x0: 0, x1: 0, x2: 0, x3: 0, x4: 0, del_psi: 0,
del_epsilon: 0, epsilon0: 0, epsilon: 0, del_tau: 0, lamda: 0, nu0: 0, nu: 0, alpha: 0, delta: 0,
h: 0, xi: 0, del_alpha: 0, delta_prime: 0, alpha_prime: 0, h_prime: 0, e0: 0, del_e: 0, e: 0, eot: 0,
srha: 0, ssha: 0, sta: 0, zenith: 0, azimuth_astro: 0, azimuth: 0, incidence: 0, suntransit: 0, sunrise: 0,
sunset: 0)
guard spa_calculate(&a) == 0 else {
fatalError("SPA failed")
}
print(a)
return Float(a.zenith)
}*/
@inlinable
func julian_ephemeris_day(jd: Double, deltaT: Double) -> Double {
return jd + deltaT / 86400.0
}
@inlinable
func julian_ephemeris_century(jde: Double) -> Double {
return (jde - 2451545.0) / 36525.0
}
@inlinable
func julian_ephemeris_millennium(jce: Double) -> Double {
return jce / 10.0
}
@inlinable
func limit_degrees(degrees: Double) -> Double {
let degrees = degrees / 360.0
var limited = 360.0*(degrees-floor(degrees))
if (limited < 0) { limited += 360.0 }
return limited
}
func limit_minutes(minutes: Double) -> Double {
var limited=minutes
if (limited < -20.0) { limited += 1440.0 }
else if (limited > 20.0) { limited -= 1440.0 }
return limited
}
func earth_periodic_term_summation(terms: [[(Double, Double, Double)]], jme: Double) -> Double {
return terms.enumerated().reduce(0, {
$0 + $1.element.reduce(0, { $0 + $1.0 * cos($1.2 * jme + $1.1) }) *
pow(jme, Double($1.offset))
}) / 1.0e8
}
func earth_heliocentric_longitude(jme: Double) -> Double {
let sum = earth_periodic_term_summation(terms: L_TERMS, jme: jme)
return limit_degrees(degrees: sum.rad2deg)
}
func earth_heliocentric_latitude(jme: Double) -> Double {
let sum = earth_periodic_term_summation(terms: B_TERMS, jme: jme)
return sum.rad2deg
}
func earth_radius_vector(jme: Double) -> Double {
let sum = earth_periodic_term_summation(terms: R_TERMS, jme: jme)
return sum
}
@inlinable
func geocentric_longitude(l: Double) -> Double {
let theta = l + 180.0
if (theta >= 360.0) {
return theta - 360.0
}
return theta
}
@inlinable
func geocentric_latitude(b: Double) -> Double {
return -b
}
@inlinable
func third_order_polynomial(_ a: Double, _ b: Double, _ c: Double, _ d: Double, _ x: Double) -> Double {
let a2 = x * a + b
let a1 = x * a2 + c
return x * a1 + d
}
func mean_elongation_moon_sun(jce: Double) -> Double {
return third_order_polynomial(1.0/189474.0, -0.0019142, 445267.11148, 297.85036, jce)
}
func mean_anomaly_sun(jce: Double) -> Double {
return third_order_polynomial(-1.0/300000.0, -0.0001603, 35999.05034, 357.52772, jce)
}
func mean_anomaly_moon(jce: Double) -> Double {
return third_order_polynomial(1.0/56250.0, 0.0086972, 477198.867398, 134.96298, jce)
}
func argument_latitude_moon(jce: Double) -> Double {
return third_order_polynomial(1.0/327270.0, -0.0036825, 483202.017538, 93.27191, jce)
}
func ascending_longitude_moon(jce: Double) -> Double {
return third_order_polynomial(1.0/450000.0, 0.0020708, -1934.136261, 125.04452, jce)
}
func nutation_longitude_and_obliquity(jce: Double, x: (Double, Double, Double, Double, Double)) -> (del_psi: Double, del_epsilon: Double) {
var sum_psi: Double = 0
var sum_epsilon: Double = 0
for i in 0..<Y_TERMS.count {
let a0 = x.0 * Y_TERMS[i].0
let a1 = x.1 * Y_TERMS[i].1 + a0
let a2 = x.2 * Y_TERMS[i].2 + a1
let a3 = x.3 * Y_TERMS[i].3 + a2
let a4 = x.4 * Y_TERMS[i].4 + a3
let xy_term_sum = a4.deg2rad
sum_psi = (jce * PE_TERMS[i].1 + PE_TERMS[i].0) * sin(xy_term_sum) + sum_psi
sum_epsilon = (jce * PE_TERMS[i].3 + PE_TERMS[i].2) * cos(xy_term_sum) + sum_epsilon
}
let del_psi = sum_psi / 36000000.0
let del_epsilon = sum_epsilon / 36000000.0
return (del_psi, del_epsilon)
}
func ecliptic_mean_obliquity(jme: Double) -> Double {
let u = jme/10.0
let a9 = u * 2.45 + 5.79
let a8 = u * a9 + 27.87
let a7 = u * a8 + 7.12
let a6 = u * a7 + -39.05
let a5 = u * a6 + -249.67
let a4 = u * a5 + -51.38
let a3 = u * a4 + 1999.25
let a2 = u * a3 + -1.55
let a1 = u * a2 + -4680.93
return u * a1 + 84381.448
}
@inlinable
func ecliptic_true_obliquity(delta_epsilon: Double, epsilon0: Double) -> Double {
return delta_epsilon + epsilon0/3600.0
}
@inlinable
func aberration_correction(r: Double) -> Double {
return -20.4898 / (3600.0*r)
}
@inlinable
func apparent_sun_longitude(theta: Double, delta_psi: Double, delta_tau: Double) -> Double {
return theta + delta_psi + delta_tau
}
func geocentric_right_ascension(lamda: Double, epsilon: Double, beta: Double) -> Double {
let lamda_rad = lamda.deg2rad
let epsilon_rad = epsilon.deg2rad
return limit_degrees(degrees: atan2(sin(lamda_rad)*cos(epsilon_rad) - tan(beta.deg2rad)*sin(epsilon_rad), cos(lamda_rad)).rad2deg)
}
func geocentric_declination(beta: Double, epsilon: Double, lamda: Double) -> Double {
let beta_rad = (beta.deg2rad)
let epsilon_rad = (epsilon.deg2rad)
return (asin(sin(beta_rad)*cos(epsilon_rad) + cos(beta_rad)*sin(epsilon_rad)*sin(lamda.deg2rad))).rad2deg
}
func sun_mean_longitude(jme: Double) -> Double {
let a4 = jme * -1/2000000.0 + -1/15300.0
let a3 = jme * a4 + 1/49931.0
let a2 = jme * a3 + 0.03032028
let a1 = jme * a2 + 360007.6982779
return limit_degrees(degrees: jme * a1 + 280.4664567)
}
func eot(m: Double, alpha: Double, del_psi: Double, epsilon: Double) -> Double {
return limit_minutes(minutes: 4.0*(m - 0.0057183 - alpha + del_psi*cos(epsilon.deg2rad)))
}
func calculate(julianDate jd: Double) -> (delta: Double, eot: Double) {
//double x[TERM_X_COUNT]
let delta_t: Double = 60
//spa->jc = julian_century(spa->jd)
let jde = julian_ephemeris_day(jd: jd, deltaT: delta_t)
let jce = julian_ephemeris_century(jde: jde)
let jme = julian_ephemeris_millennium(jce: jce)
let l = earth_heliocentric_longitude(jme: jme)
let b = earth_heliocentric_latitude(jme: jme)
let r = earth_radius_vector(jme: jme)
let theta = geocentric_longitude(l: l)
let beta = geocentric_latitude(b: b)
let x0 = mean_elongation_moon_sun(jce: jce)
let x1 = mean_anomaly_sun(jce: jce)
let x2 = mean_anomaly_moon(jce: jce)
let x3 = argument_latitude_moon(jce: jce)
let x4 = ascending_longitude_moon(jce: jce)
let (del_psi, del_epsilon) = nutation_longitude_and_obliquity(jce: jce, x: (x0,x1,x2,x3,x4))
let epsilon0 = ecliptic_mean_obliquity(jme: jme)
let epsilon = ecliptic_true_obliquity(delta_epsilon: del_epsilon, epsilon0: epsilon0)
let del_tau = aberration_correction(r: r)
let lamda = apparent_sun_longitude(theta: theta, delta_psi: del_psi, delta_tau: del_tau)
//spa->nu0 = greenwich_mean_sidereal_time (spa->jd, spa->jc)
//spa->nu = greenwich_sidereal_time (spa->nu0, spa->del_psi, spa->epsilon)
let alpha = geocentric_right_ascension(lamda: lamda, epsilon: epsilon, beta: beta)
let delta = geocentric_declination(beta: beta, epsilon: epsilon, lamda: lamda)
let m = sun_mean_longitude(jme: jme)
let eot = eot(m: m, alpha: alpha, del_psi: del_psi, epsilon: epsilon)
return (delta, eot)
}
let L_TERMS: [[(Double, Double, Double)]] = [
[
(175347046.0,0,0),
(3341656.0,4.6692568,6283.07585),
(34894.0,4.6261,12566.1517),
(3497.0,2.7441,5753.3849),
(3418.0,2.8289,3.5231),
(3136.0,3.6277,77713.7715),
(2676.0,4.4181,7860.4194),
(2343.0,6.1352,3930.2097),
(1324.0,0.7425,11506.7698),
(1273.0,2.0371,529.691),
(1199.0,1.1096,1577.3435),
(990,5.233,5884.927),
(902,2.045,26.298),
(857,3.508,398.149),
(780,1.179,5223.694),
(753,2.533,5507.553),
(505,4.583,18849.228),
(492,4.205,775.523),
(357,2.92,0.067),
(317,5.849,11790.629),
(284,1.899,796.298),
(271,0.315,10977.079),
(243,0.345,5486.778),
(206,4.806,2544.314),
(205,1.869,5573.143),
(202,2.458,6069.777),
(156,0.833,213.299),
(132,3.411,2942.463),
(126,1.083,20.775),
(115,0.645,0.98),
(103,0.636,4694.003),
(102,0.976,15720.839),
(102,4.267,7.114),
(99,6.21,2146.17),
(98,0.68,155.42),
(86,5.98,161000.69),
(85,1.3,6275.96),
(85,3.67,71430.7),
(80,1.81,17260.15),
(79,3.04,12036.46),
(75,1.76,5088.63),
(74,3.5,3154.69),
(74,4.68,801.82),
(70,0.83,9437.76),
(62,3.98,8827.39),
(61,1.82,7084.9),
(57,2.78,6286.6),
(56,4.39,14143.5),
(56,3.47,6279.55),
(52,0.19,12139.55),
(52,1.33,1748.02),
(51,0.28,5856.48),
(49,0.49,1194.45),
(41,5.37,8429.24),
(41,2.4,19651.05),
(39,6.17,10447.39),
(37,6.04,10213.29),
(37,2.57,1059.38),
(36,1.71,2352.87),
(36,1.78,6812.77),
(33,0.59,17789.85),
(30,0.44,83996.85),
(30,2.74,1349.87),
(25,3.16,4690.48)
],
[
(628331966747.0,0,0),
(206059.0,2.678235,6283.07585),
(4303.0,2.6351,12566.1517),
(425.0,1.59,3.523),
(119.0,5.796,26.298),
(109.0,2.966,1577.344),
(93,2.59,18849.23),
(72,1.14,529.69),
(68,1.87,398.15),
(67,4.41,5507.55),
(59,2.89,5223.69),
(56,2.17,155.42),
(45,0.4,796.3),
(36,0.47,775.52),
(29,2.65,7.11),
(21,5.34,0.98),
(19,1.85,5486.78),
(19,4.97,213.3),
(17,2.99,6275.96),
(16,0.03,2544.31),
(16,1.43,2146.17),
(15,1.21,10977.08),
(12,2.83,1748.02),
(12,3.26,5088.63),
(12,5.27,1194.45),
(12,2.08,4694),
(11,0.77,553.57),
(10,1.3,6286.6),
(10,4.24,1349.87),
(9,2.7,242.73),
(9,5.64,951.72),
(8,5.3,2352.87),
(6,2.65,9437.76),
(6,4.67,4690.48)
],
[
(52919.0,0,0),
(8720.0,1.0721,6283.0758),
(309.0,0.867,12566.152),
(27,0.05,3.52),
(16,5.19,26.3),
(16,3.68,155.42),
(10,0.76,18849.23),
(9,2.06,77713.77),
(7,0.83,775.52),
(5,4.66,1577.34),
(4,1.03,7.11),
(4,3.44,5573.14),
(3,5.14,796.3),
(3,6.05,5507.55),
(3,1.19,242.73),
(3,6.12,529.69),
(3,0.31,398.15),
(3,2.28,553.57),
(2,4.38,5223.69),
(2,3.75,0.98)
],
[
(289.0,5.844,6283.076),
(35,0,0),
(17,5.49,12566.15),
(3,5.2,155.42),
(1,4.72,3.52),
(1,5.3,18849.23),
(1,5.97,242.73)
],
[
(114.0,3.142,0),
(8,4.13,6283.08),
(1,3.84,12566.15)
],
[
(1,3.14,0)
]
]
let B_TERMS: [[(Double, Double, Double)]] = [
[
(280.0,3.199,84334.662),
(102.0,5.422,5507.553),
(80,3.88,5223.69),
(44,3.7,2352.87),
(32,4,1577.34)
],
[
(9,3.9,5507.55),
(6,1.73,5223.69)
]
]
let R_TERMS: [[(Double, Double, Double)]] = [
[
(100013989.0,0,0),
(1670700.0,3.0984635,6283.07585),
(13956.0,3.05525,12566.1517),
(3084.0,5.1985,77713.7715),
(1628.0,1.1739,5753.3849),
(1576.0,2.8469,7860.4194),
(925.0,5.453,11506.77),
(542.0,4.564,3930.21),
(472.0,3.661,5884.927),
(346.0,0.964,5507.553),
(329.0,5.9,5223.694),
(307.0,0.299,5573.143),
(243.0,4.273,11790.629),
(212.0,5.847,1577.344),
(186.0,5.022,10977.079),
(175.0,3.012,18849.228),
(110.0,5.055,5486.778),
(98,0.89,6069.78),
(86,5.69,15720.84),
(86,1.27,161000.69),
(65,0.27,17260.15),
(63,0.92,529.69),
(57,2.01,83996.85),
(56,5.24,71430.7),
(49,3.25,2544.31),
(47,2.58,775.52),
(45,5.54,9437.76),
(43,6.01,6275.96),
(39,5.36,4694),
(38,2.39,8827.39),
(37,0.83,19651.05),
(37,4.9,12139.55),
(36,1.67,12036.46),
(35,1.84,2942.46),
(33,0.24,7084.9),
(32,0.18,5088.63),
(32,1.78,398.15),
(28,1.21,6286.6),
(28,1.9,6279.55),
(26,4.59,10447.39)
],
[
(103019.0,1.10749,6283.07585),
(1721.0,1.0644,12566.1517),
(702.0,3.142,0),
(32,1.02,18849.23),
(31,2.84,5507.55),
(25,1.32,5223.69),
(18,1.42,1577.34),
(10,5.91,10977.08),
(9,1.42,6275.96),
(9,0.27,5486.78)
],
[
(4359.0,5.7846,6283.0758),
(124.0,5.579,12566.152),
(12,3.14,0),
(9,3.63,77713.77),
(6,1.87,5573.14),
(3,5.47,18849.23)
],
[
(145.0,4.273,6283.076),
(7,3.92,12566.15)
],
[
(4,2.56,6283.08)
]
]
////////////////////////////////////////////////////////////////
/// Periodic Terms for the nutation in longitude and obliquity
////////////////////////////////////////////////////////////////
let Y_TERMS: [(Double, Double, Double, Double, Double)] = [
(0,0,0,0,1),
(-2,0,0,2,2),
(0,0,0,2,2),
(0,0,0,0,2),
(0,1,0,0,0),
(0,0,1,0,0),
(-2,1,0,2,2),
(0,0,0,2,1),
(0,0,1,2,2),
(-2,-1,0,2,2),
(-2,0,1,0,0),
(-2,0,0,2,1),
(0,0,-1,2,2),
(2,0,0,0,0),
(0,0,1,0,1),
(2,0,-1,2,2),
(0,0,-1,0,1),
(0,0,1,2,1),
(-2,0,2,0,0),
(0,0,-2,2,1),
(2,0,0,2,2),
(0,0,2,2,2),
(0,0,2,0,0),
(-2,0,1,2,2),
(0,0,0,2,0),
(-2,0,0,2,0),
(0,0,-1,2,1),
(0,2,0,0,0),
(2,0,-1,0,1),
(-2,2,0,2,2),
(0,1,0,0,1),
(-2,0,1,0,1),
(0,-1,0,0,1),
(0,0,2,-2,0),
(2,0,-1,2,1),
(2,0,1,2,2),
(0,1,0,2,2),
(-2,1,1,0,0),
(0,-1,0,2,2),
(2,0,0,2,1),
(2,0,1,0,0),
(-2,0,2,2,2),
(-2,0,1,2,1),
(2,0,-2,0,1),
(2,0,0,0,1),
(0,-1,1,0,0),
(-2,-1,0,2,1),
(-2,0,0,0,1),
(0,0,2,2,1),
(-2,0,2,0,1),
(-2,1,0,2,1),
(0,0,1,-2,0),
(-1,0,1,0,0),
(-2,1,0,0,0),
(1,0,0,0,0),
(0,0,1,2,0),
(0,0,-2,2,2),
(-1,-1,1,0,0),
(0,1,1,0,0),
(0,-1,1,2,2),
(2,-1,-1,2,2),
(0,0,3,2,2),
(2,-1,0,2,2),
]
let PE_TERMS: [(Double, Double, Double, Double)] = [
(-171996,-174.2,92025,8.9),
(-13187,-1.6,5736,-3.1),
(-2274,-0.2,977,-0.5),
(2062,0.2,-895,0.5),
(1426,-3.4,54,-0.1),
(712,0.1,-7,0),
(-517,1.2,224,-0.6),
(-386,-0.4,200,0),
(-301,0,129,-0.1),
(217,-0.5,-95,0.3),
(-158,0,0,0),
(129,0.1,-70,0),
(123,0,-53,0),
(63,0,0,0),
(63,0.1,-33,0),
(-59,0,26,0),
(-58,-0.1,32,0),
(-51,0,27,0),
(48,0,0,0),
(46,0,-24,0),
(-38,0,16,0),
(-31,0,13,0),
(29,0,0,0),
(29,0,-12,0),
(26,0,0,0),
(-22,0,0,0),
(21,0,-10,0),
(17,-0.1,0,0),
(16,0,-8,0),
(-16,0.1,7,0),
(-15,0,9,0),
(-13,0,7,0),
(-12,0,6,0),
(11,0,0,0),
(-10,0,5,0),
(-8,0,3,0),
(7,0,-3,0),
(-7,0,0,0),
(-7,0,3,0),
(-7,0,3,0),
(6,0,0,0),
(6,0,-3,0),
(6,0,-3,0),
(-6,0,3,0),
(-6,0,3,0),
(5,0,0,0),
(-5,0,3,0),
(-5,0,3,0),
(-5,0,3,0),
(4,0,0,0),
(4,0,0,0),
(4,0,0,0),
(-4,0,0,0),
(-4,0,0,0),
(-4,0,0,0),
(3,0,0,0),
(-3,0,0,0),
(-3,0,0,0),
(-3,0,0,0),
(-3,0,0,0),
(-3,0,0,0),
(-3,0,0,0),
(-3,0,0,0),
]
}
/**
Fast lookup table for solar position
*/
public struct SolarPositonFastLookup {
let declination: [Float]
let equationOfTime: [Float]
/// Sample solar declination every 20 days over 200 years. With hermite interpolation, the error is less than a second in sunrise/set
/// Around 14k memory for each array
static let referenceTime = TimerangeDt(start: Timestamp(1950,1,1), to: Timestamp(2050,1,1), dtSeconds: 86400*20)
public init() {
(declination, equationOfTime) = SolarPositionAlgorithm.sunPosition(timerange: Self.referenceTime)
}
/// Calculate position of timestamp in refreence time
private func pos(_ time: Timestamp) -> (quotient: Int, fraction: Float) {
let start = Self.referenceTime.range.lowerBound.timeIntervalSince1970
let dt = Self.referenceTime.dtSeconds
let count = Self.referenceTime.range.count
let t = time.timeIntervalSince1970
return (t - start).moduloPositive(count).moduloFraction(dt)
}
/// Get sun declination for a given time in DEGREE
public func getDeclination(_ time: Timestamp) -> Float {
let (index, fraction) = pos(time)
return declination.interpolateHermiteRing(index, fraction)
}
/// Get sun equation of time for a given time in MINUTES
public func getEquationOfTime(_ time: Timestamp) -> Float {
let (index, fraction) = pos(time)
return equationOfTime.interpolateHermiteRing(index, fraction)
}
}
extension Double {
@inlinable
var rad2deg: Double {
return (180.0 / .pi)*self
}
@inlinable
var deg2rad: Double
{
return (.pi / 180.0)*self
}
}
extension Timestamp {
var julianDate: Double {
return Double(timeIntervalSince1970) / 86400.0 + 2440587.5;
}
}