0

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)
}
Community
  • 1
  • 1
Maury Markowitz
  • 9,082
  • 11
  • 46
  • 98
  • Hi Maury, I think I could help you, although I would only be able to do it in straight R, would this help you? – Derek Corcoran Jan 15 '17 at 00:54
  • It would not hurt! As I said, the main issue I had was understanding the [] syntax and I'm not sure I have converted it correctly. Actually, if you could use the R version and step through the lines and send me your outputs, that would likely be EXTREMELY useful! – Maury Markowitz Jan 15 '17 at 01:25
  • I can do it, I am not sure with your swift code up there the dates and coordinates that you need to use, If you tell me that I believe I can do it quite fast – Derek Corcoran Jan 15 '17 at 01:50
  • Here are the inputs: lat = 43.8509, lon = -79.0204 , date = june 21 2017 – Maury Markowitz Jan 15 '17 at 15:23

1 Answers1

4

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)
}
Matt Brown
  • 23
  • 4
Maury Markowitz
  • 9,082
  • 11
  • 46
  • 98
  • You might consider doing without R. See the implementation of the Solar Rubygem, for inspiration: https://github.com/jgoizueta/solar – cseelus Aug 13 '18 at 00:56
  • @cseelus - the code above is in Swift :-) I did not use the R version for anything other than debugging my own code. – Maury Markowitz Aug 13 '18 at 13:35