Search code examples
rmathswift3geometryastronomy

Sun's position in Swift


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)
}

Solution

  • Ok, after downloading an R interpreter for OSX, finding that it had no debugger, discovering that there are multiple ways to do a print all with their own caveats, etc etc, I found the problem I was looking for. It was indeed clamping one of the values incorrectly. Here is a working Swift3 version that should be easy to convert to any C-like language and easier to read than the originals. You will have to provide your own versions of the first two functions that work with the date format of your target platform. And the truncatingRemainer is someone's ferbile idea that there shouldn't be a % operator on Double, it's a normal MOD.

    // convinience method to return a unit-epoch data from a julian date
    class func dateFromJulianDay(_ julianDay: Double) -> Date {
        let unixTime = (julianDay - 2440587) * 86400.0
        return Date(timeIntervalSince1970: unixTime)
    }
    class func julianDayFromDate(_ date: Date) -> Double {
        //==let JD = Integer(365.25 * (Y + 4716)) + Integer(30.6001 * (M +1)) +
        let ti = date.timeIntervalSince1970
        return ((ti / 86400.0) + 2440587.5)
    }
    // calculate the elevation and azimuth of the sun for a given date and location
    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. So if your date format is 1970-based, convert that
        // a pure julian date and pass that in. If your date is 2000-based, then
        // just let n = date
        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, which is probably a lot easier in non-Swift languages
        var utzCal = Calendar(identifier: .gregorian)
        utzCal.timeZone = TimeZone(secondsFromGMT: 0)!
        let h = Double(utzCal.component(.hour, from: date))
        let m = Double(utzCal.component(.minute, from: date))
        let f: Double // universal time in hours and decimals (not days!)
        if h == 0 && m == 0 {
            f = 0.0
        } else if h == 0 {
            f = m / 60.0
        } else if m == 0 {
            f = h
        } else {
            f = h + (m / 60.0)
        }
        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)
        let localtime_radians = localtime * 15.0  * deg2rad
    
        // 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
        }
    
        // all done!
        return (elevation_degrees, azimuth_degrees)
    }