I am trying to implement this solution for calculating the sun's position in Swift3. I then wrap this in another function that simply cycles through a day from midnight stepping every 10 minutes until 23:50.
I do not really understand R and there are some details of the answer I do not fully comprehend, notably what appears to be some sort of if/clamp function with the square brackets. I did my best, comparing with the Python version when I got confused. Otherwise the only differences are due to the use of NSDate
, which simplified some of the code at the top.
Some of the values I get back seem correct and I can see the basis of a curve when I plot the results. However, the result from one call, say 7AM, and then the next, 7:10, are wildly different.
I strongly suspect I did something wrong with the clamping, and that minor changes in the inputs get mod/trunced in different ways and swing the output. But I can't spot it. Can anyone who understands this algo help?
Here's a sample of the output I'm getting:
2017-06-21 00:10:00 +0000 -16.0713262209521 31.7135341633943
2017-06-21 00:20:00 +0000 61.9971433936385 129.193513530349
2017-06-21 00:30:00 +0000 22.5263575559266 78.5445189561018
2017-06-21 00:40:00 +0000 29.5973897349096 275.081637736092
2017-06-21 00:50:00 +0000 41.9552795956374 262.989819486864
As you can see, it swings wildly between iterations. The Earth does not turn that way! My code follows, this version simply sends the results to the log:
class func julianDayFromDate(_ date: Date) -> Double {
let ti = date.timeIntervalSince1970
return ((ti / 86400.0) + 2440587)
}
class func sunPath(lat: Double, lon: Double, size: CGSize) -> UIImage {
var utzCal = Calendar(identifier: .gregorian)
utzCal.timeZone = TimeZone(secondsFromGMT: 0)!
let year = utzCal.component(.year, from: Date())
let june = DateComponents(calendar: utzCal, year: year, month: 6, day: 21).date!
// now we loop for every 10 minutes (2 degrees) and plot those points
for time in stride(from:0, to:(24 * 60), by: 10) {
let calcdate = june.addingTimeInterval(Double(time) * 60.0)
let (alt, az) = sun(date: calcdate, lat: lat, lon: lon)
print(calcdate, alt, az)
}
class func sun(date: Date, lat: Double, lon: Double) -> (altitude: Double, azimuth: Double) {
// these come in handy
let twopi = Double.pi * 2
let deg2rad = Double.pi / 180.0
// latitude to radians
let lat_radians = lat * deg2rad
// the Astronomer's Almanac method used here is based on Epoch 2000, so we need to
// convert the date into that format. We start by calculating "n", the number of
// days since 1 January 2000
let n = julianDayFromDate(date) - 2451545.0
// it continues by calculating the position in ecliptic coordinates,
// starting with the mean longitude of the sun in degrees, corrected for aberation
var meanlong_degrees = 280.460 + (0.9856474 * n)
meanlong_degrees = meanlong_degrees.truncatingRemainder(dividingBy: 360.0)
// and the mean anomaly in degrees
var meananomaly_degrees = 357.528 + (0.9856003 * n)
meananomaly_degrees = meananomaly_degrees.truncatingRemainder(dividingBy: 360.0)
let meananomaly_radians = meananomaly_degrees * deg2rad
// and finally, the eliptic longitude in degrees
var elipticlong_degrees = meanlong_degrees + (1.915 * sin(meananomaly_radians)) + (0.020 * sin(2 * meananomaly_radians))
elipticlong_degrees = elipticlong_degrees.truncatingRemainder(dividingBy: 360.0)
let elipticlong_radians = elipticlong_degrees * deg2rad
// now we want to convert that to equatorial coordinates
let obliquity_degrees = 23.439 - (0.0000004 * n)
let obliquity_radians = obliquity_degrees * deg2rad
// right ascention in radians
let num = cos(obliquity_radians) * sin(elipticlong_radians)
let den = cos(elipticlong_radians)
var ra_radians = atan(num / den)
ra_radians = ra_radians.truncatingRemainder(dividingBy: Double.pi)
if den < 0 {
ra_radians = ra_radians + Double.pi
} else if num < 0 {
ra_radians = ra_radians + twopi
}
// declination is simpler...
let dec_radians = asin(sin(obliquity_radians) * sin(elipticlong_radians))
// and from there, to local coordinates
// start with the UTZ sidereal time
let cal = Calendar.current
let h = Double(cal.component(.hour, from: date))
let m = Double(cal.component(.minute, from: date))
let f: Double
if h == 0 && m == 0 {
f = 0.0
} else if h == 0 {
f = 60.0 / m
} else if h == 0 {
f = 24.0 / h
} else {
f = (24.0 / h) + (60.0 / m)
}
var utz_sidereal_time = 6.697375 + 0.0657098242 * n + f
utz_sidereal_time = utz_sidereal_time.truncatingRemainder(dividingBy: 24.0)
// then convert that to local sidereal time
var localtime = utz_sidereal_time + lon / 15.0
localtime = localtime.truncatingRemainder(dividingBy: 24.0)
var localtime_radians = localtime * 15.0 * deg2rad
localtime_radians = localtime.truncatingRemainder(dividingBy: Double.pi)
// hour angle in radians
var hourangle_radians = localtime_radians - ra_radians
hourangle_radians = hourangle_radians.truncatingRemainder(dividingBy: twopi)
// get elevation in degrees
let elevation_radians = (asin(sin(dec_radians) * sin(lat_radians) + cos(dec_radians) * cos(lat_radians) * cos(hourangle_radians)))
let elevation_degrees = elevation_radians / deg2rad
// and azimuth
let azimuth_radians = asin( -cos(dec_radians) * sin(hourangle_radians) / cos(elevation_radians))
// now clamp the output
let azimuth_degrees: Double
if (sin(dec_radians) - sin(elevation_radians) * sin(lat_radians) < 0) {
azimuth_degrees = (Double.pi - azimuth_radians) / deg2rad
} else if (sin(azimuth_radians) < 0) {
azimuth_degrees = (azimuth_radians + twopi) / deg2rad
} else {
azimuth_degrees = azimuth_radians / deg2rad
}
return (elevation_degrees, azimuth_degrees)
}