13

I'm wondering if there is a python function/module that calculates the local time after midnight (or local solar time) given the UTC time and longitude? It doesn't need to take into account daylight saving time.

Thanks in advance.

mattexx
  • 6,456
  • 3
  • 36
  • 47
Shejo284
  • 4,541
  • 6
  • 32
  • 44
  • 5
    Isn't that just as simple as `solar_time = UTCtime + longitude / π * 12h`? – Celada Nov 09 '12 at 20:52
  • @Celada : Do you have a reference for this? I tried: 0 + np.arange(-180.0,190.0,15.0)/360*12 = array([-6.,-5.5,-5.,-4.5,-4.,-3.5,-3.,-2.5,-2.,-1.5,-1., -0.5,0.,0.5,1.,1.5,2.,2.5,3.,3.5,4.,4.5,5.,5.5,6.]) It only goes out to 6 hours. If you switch out 12h with 24h you can get the full globe. I don't get it if you use pi. Can you explain? – Shejo284 Nov 10 '12 at 22:42
  • @Celada: Another thing I missed is that the solar times are negative west of meridian. I solve this, hack is more like it, by taking the results in lst and doing: let[lst < 0] += 24 for a 24 hour clock. I'm sure a more math-minded person can do this in one line. – Shejo284 Nov 10 '12 at 23:05
  • Do you care about the difference between true solar time, mean solar time, sidereal time? UT1 vs. UTC? [`pyephem` has sidereal_time() method on Observer](http://rhodesmill.org/pyephem/quick.html). – jfs Nov 15 '12 at 01:21
  • @Celada, since longitude is measured in degrees and not radians I suspect the proper formula is `solar_time = UTCtime + longitude / 360 * 24h`. – Mark Ransom Nov 16 '12 at 19:07
  • 1
    @J.F.Sebastian I had no idea the question was so complex. A little digging at [Wikipedia](http://en.wikipedia.org/wiki/Solar_time) shows an example of the local time drifting by at least 16 minutes throughout the year, although it averages out in the long run. – Mark Ransom Nov 16 '12 at 22:23
  • @MarkRansom of course you have to use units correctly, that goes without saying. "Since longitude is measured in degrees" make no sense. Longitude is measured in whatever units it happens to be measured in (this is a truism!). Since `π = 180º`, your formula and mine are identical, up to a single algebraic simplification. – Celada Nov 17 '12 at 04:25
  • @Celada, no it doesn't go without saying - that's why I said it. I've seen people confuse degrees and radians before without knowing it. The human-readable measurement of longitude is always given in degrees no matter what internal representation the computer might use. P.S. J.F. Sebastian's answer compares your simple solution to a more accurate one, and the library he uses appears to use radians. – Mark Ransom Nov 17 '12 at 04:35

5 Answers5

14

Using ephem's sidereal_time() method:

import ephem # pip install pyephem (on Python 2)
             # pip install ephem   (on Python 3)

def solartime(observer, sun=ephem.Sun()):
    sun.compute(observer)
    # sidereal time == ra (right ascension) is the highest point (noon)
    hour_angle = observer.sidereal_time() - sun.ra
    return ephem.hours(hour_angle + ephem.hours('12:00')).norm  # norm for 24h

Note: ephem.hours is a float number that represents an angle in radians and converts to/from a string as "hh:mm:ss.ff".

For comparison, here's the "utc + longitude" formula:

import math
from datetime import timedelta

def ul_time(observer):
    utc_dt = observer.date.datetime()
    longitude = observer.long
    return utc_dt + timedelta(hours=longitude / math.pi * 12)

Example

from datetime import datetime

# "solar time" for some other cities
for name in ['Los Angeles', 'New York', 'London',
             'Paris', 'Moscow', 'Beijing', 'Tokyo']:
    city = ephem.city(name)
    print("%-11s %11s %s" % (name, solartime(city),
                             ul_time(city).strftime('%T')))

# set date, longitude manually
o = ephem.Observer()
o.date = datetime(2012, 4, 15, 1, 0, 2) # some utc time
o.long = '00:00:00.0' # longitude (you could also use a float (radians) here)
print("%s %s" % (solartime(o), ul_time(o).strftime('%T')))

Output

Los Angeles 14:59:34.11 14:44:30
New York    17:56:31.27 17:41:27
London      22:52:02.04 22:36:58
Paris       23:01:56.56 22:46:53
Moscow       1:23:00.33 01:07:57
Beijing      6:38:09.53 06:23:06
Tokyo        8:11:17.81 07:56:15
1:00:00.10 01:00:01
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • 2
    this answer is very old, so `ephem` might have changed, but `solartime(o)` returns a `float` now – duff18 Feb 12 '21 at 21:57
  • 2
    @duff18: it returns `ephem.Angle`. It is `float` instance that it is printed as "hh:mm:ss.f" as it is mentioned in the answer. Try `str(solartime(o))` to see that it is not just a float. – jfs Feb 13 '21 at 10:57
8

Or if you want to go even shorter, you could use NOAA's low-accuracy equations:

#!/usr/local/bin/python

import sys
from datetime import datetime, time, timedelta
from math import pi, cos, sin

def solar_time(dt, longit):
    return ha

def main():
    if len(sys.argv) != 4:
        print 'Usage: hour_angle.py [YYYY/MM/DD] [HH:MM:SS] [longitude]'
        sys.exit()
    else:
        dt = datetime.strptime(sys.argv[1] + ' ' + sys.argv[2], '%Y/%m/%d %H:%M:%S')
        longit = float(sys.argv[3])

    gamma = 2 * pi / 365 * (dt.timetuple().tm_yday - 1 + float(dt.hour - 12) / 24)
    eqtime = 229.18 * (0.000075 + 0.001868 * cos(gamma) - 0.032077 * sin(gamma) \
             - 0.014615 * cos(2 * gamma) - 0.040849 * sin(2 * gamma))
    decl = 0.006918 - 0.399912 * cos(gamma) + 0.070257 * sin(gamma) \
           - 0.006758 * cos(2 * gamma) + 0.000907 * sin(2 * gamma) \
           - 0.002697 * cos(3 * gamma) + 0.00148 * sin(3 * gamma)
    time_offset = eqtime + 4 * longit
    tst = dt.hour * 60 + dt.minute + dt.second / 60 + time_offset
    solar_time = datetime.combine(dt.date(), time(0)) + timedelta(minutes=tst)
    print solar_time

if __name__ == '__main__':
    main()
mattexx
  • 6,456
  • 3
  • 36
  • 47
  • 1
    `ha` inside `solar_time()` function is not defined. Surprisingly, your answer is [wrong only by ~12 seconds on `2012/01/15 11:15:22`](https://gist.github.com/zed/8a541bd0ce43700bf729) for cities from [my answer](http://stackoverflow.com/a/13425515/4279). – jfs Oct 04 '14 at 08:51
  • 1
    Strange, no idea what that was about. Yeah this works OK for the contiguous US. Not so much Alaska. Basically fine for most stuff where latitudes are small. – mattexx Oct 06 '14 at 00:21
  • The error is the same 12 seconds for Los Angeles and New York as [the code that I've linked in the previous comment shows](https://gist.github.com/zed/8a541bd0ce43700bf729) – jfs Oct 06 '14 at 02:28
  • Converted to python3 https://gist.github.com/mnpenner/8b6e7cceb930402d3f37ba9c59fa1d26. Thank you! – mpen Apr 03 '16 at 18:58
  • 2
    decl seems to be defined but never used? – Sebastian Sep 12 '16 at 07:39
  • 1
    The linked PDF says to use 366 when computing gamma for a leap year so you would need.. ` if dt.year % 4 == 0 and (dt.year % 100 != 0 or dt.year % 400 == 0):` ` yrlen = 366` ` else:` ` yrlen = 365` ` gamma = 2 * pi / yrlen * (dt.timetuple().tm_yday - 1 + float(dt.hour - 12) / 24)` – Darius Aug 05 '19 at 14:21
4

I took a look at Jean Meeus' Astronomical Algorithms. I think you might be asking for local hour angle, which can be expressed in time (0-24hr), degrees (0-360) or radians (0-2pi).

I'm guessing you can do this with ephem. But just for the heck of it, here's some python:

#!/usr/local/bin/python

import sys
from datetime import datetime, time, timedelta
from math import pi, sin, cos, atan2, asin

# helpful constant
DEG_TO_RAD = pi / 180

# hardcode difference between Dynamical Time and Universal Time
# delta_T = TD - UT
# This comes from IERS Bulletin A
# ftp://maia.usno.navy.mil/ser7/ser7.dat
DELTA = 35.0

def coords(yr, mon, day):
    # @input year (int)
    # @input month (int)
    # @input day (float)
    # @output right ascention, in radians (float)
    # @output declination, in radians (float)

    # get julian day (AA ch7)
    day += DELTA / 60 / 60 / 24 # use dynamical time
    if mon <= 2:
        yr -= 1
        mon += 12
    a = yr / 100
    b = 2 - a + a / 4
    jd = int(365.25 * (yr + 4716)) + int(30.6 * (mon + 1)) + day + b - 1524.5

    # get sidereal time at greenwich (AA ch12)
    t = (jd - 2451545.0) / 36525

    # Calculate mean equinox of date (degrees)
    l = 280.46646 + 36000.76983 * t + 0.0003032 * t**2
    while (l > 360):
        l -= 360
    while (l < 0):
        l += 360

    # Calculate mean anomoly of sun (degrees)
    m = 357.52911 + 35999.05029 * t - 0.0001537 * t**2

    # Calculate eccentricity of Earth's orbit
    e = 0.016708634 - 0.000042037 * t - 0.0000001267 * t**2

    # Calculate sun's equation of center (degrees)
    c = (1.914602 - 0.004817 * t - .000014 * t**2) * sin(m * DEG_TO_RAD) \
        + (0.019993 - .000101 * t) * sin(2 * m * DEG_TO_RAD) \
        + 0.000289 * sin(3 * m * DEG_TO_RAD)

    # Calculate the sun's radius vector (AU)
    o = l + c # sun's true longitude (degrees)
    v = m + c # sun's true anomoly (degrees)

    r = (1.000001018 * (1 - e**2)) / (1 + e * cos(v * DEG_TO_RAD))

    # Calculate right ascension & declination
    seconds = 21.448 - t * (46.8150 + t * (0.00059 - t * 0.001813))
    e0 = 23 + (26 + (seconds / 60)) / 60

    ra = atan2(cos(e0 * DEG_TO_RAD) * sin(o * DEG_TO_RAD), cos(o * DEG_TO_RAD)) # (radians)
    decl = asin(sin(e0 * DEG_TO_RAD) * sin(o * DEG_TO_RAD)) # (radians)

    return ra, decl

def hour_angle(dt, longit):
    # @input UTC time (datetime)
    # @input longitude (float, negative west of Greenwich)
    # @output hour angle, in degrees (float)

    # get gregorian time including fractional day
    y = dt.year
    m = dt.month
    d = dt.day + ((dt.second / 60.0 + dt.minute) / 60 + dt.hour) / 24.0 

    # get right ascention
    ra, _ = coords(y, m, d)

    # get julian day (AA ch7)
    if m <= 2:
        y -= 1
        m += 12
    a = y / 100
    b = 2 - a + a / 4
    jd = int(365.25 * (y + 4716)) + int(30.6 * (m + 1)) + d + b - 1524.5

    # get sidereal time at greenwich (AA ch12)
    t = (jd - 2451545.0) / 36525
    theta = 280.46061837 + 360.98564736629 * (jd - 2451545) \
            + .000387933 * t**2 - t**3 / 38710000

    # hour angle (AA ch13)
    ha = (theta + longit - ra / DEG_TO_RAD) % 360

    return ha

def main():
    if len(sys.argv) != 4:
        print 'Usage: hour_angle.py [YYYY/MM/DD] [HH:MM:SS] [longitude]'
        sys.exit()
    else:
        dt = datetime.strptime(sys.argv[1] + ' ' + sys.argv[2], '%Y/%m/%d %H:%M:%S')
        longit = float(sys.argv[3])
    ha = hour_angle(dt, longit)
    # convert hour angle to timedelta from noon
    days = ha / 360
    if days > 0.5:
        days -= 0.5
    td = timedelta(days=days)
    # make solar time
    solar_time = datetime.combine(dt.date(), time(12)) + td
    print solar_time

if __name__ == '__main__':
    main()
mattexx
  • 6,456
  • 3
  • 36
  • 47
  • Thanks for you input but you are going in another direction. What I want is a simple equation to get the local solar time of a lon and lat position given the UTC time. – Shejo284 Nov 15 '12 at 19:11
  • So is my answer (hour angle in degrees) not what you are looking for, or is the solution too complicated? – mattexx Nov 15 '12 at 21:19
  • 1
    I updated this to produce solar time as a datetime, in case that's part of your problem. I'll try to reproduce this with ephem in another answer, as I'm guessing this is just more lines of code than you are looking for. – mattexx Nov 16 '12 at 18:57
3

For a very simple function & very very approximated local time: Time variation goes from -12h to +12h and longitude goes from -180 to 180. Then:


import datetime as dt

def localTimeApprox(myDateTime, longitude):
   """Returns local hour approximation"""
   return myDateTime+dt.timedelta(hours=(longitude*12/180))

Sample calling: localTimeApprox(dt.datetime(2014, 7, 9, 20, 00, 00), -75)

Returns: datetime.datetime(2014, 7, 9, 15, 0)

Le Droid
  • 4,534
  • 3
  • 37
  • 32
  • Driod: elegant! Thanks! – Shejo284 Sep 03 '14 at 13:35
  • @Shejo284: -1. it is very simple (it is **the same** as `ul_time()` in my answer except it accepts the datetime, longitude (in degrees) instead of the observer that provides **the same** information) and it is **wrong**. It doesn't show your local solar time (as a simple sundial would). Compare its results with `solartime()` function. [The output at the end of my answer](http://stackoverflow.com/a/13425515/4279) shows ~15 minutes difference around the year. – jfs Sep 03 '14 at 15:20
  • @J.F.Sebastian Should I edit my answer to add another "very" at "very very approximated"? Only 15 minutes? That's better that what I was thinking, thanks. If you find it simpler to install additional library and write more than 10 lines instead of a one liner, that's your opinion but that wasn't an option for me has I can't install this on every servers without a really good reason. As for giving a penalty for a clever solution, humm... interesting. – Le Droid Oct 03 '14 at 21:36
  • Do you mean "the clever" solution that duplicates `ul_time()` from my answer? – jfs Oct 03 '14 at 21:57
2

Trying again, with ephem. I included latitude and elevation as arguments, but they are not needed of course. You can just call them 0 for your purposes.

#!/usr/local/bin/python

import sys
from datetime import datetime, time, timedelta
import ephem

def hour_angle(dt, longit, latit, elev):
    obs = ephem.Observer()
    obs.date = dt.strftime('%Y/%m/%d %H:%M:%S')
    obs.lon = longit
    obs.lat = latit
    obs.elevation = elev
    sun = ephem.Sun()
    sun.compute(obs)
    # get right ascention
    ra = ephem.degrees(sun.g_ra) - 2 * ephem.pi

    # get sidereal time at greenwich (AA ch12)
    jd = ephem.julian_date(dt)
    t = (jd - 2451545.0) / 36525
    theta = 280.46061837 + 360.98564736629 * (jd - 2451545) \
            + .000387933 * t**2 - t**3 / 38710000

    # hour angle (AA ch13)
    ha = (theta + longit - ra * 180 / ephem.pi) % 360
    return ha

def main():
    if len(sys.argv) != 6:
        print 'Usage: hour_angle.py [YYYY/MM/DD] [HH:MM:SS] [longitude] [latitude] [elev]'
        sys.exit()
    else:
        dt = datetime.strptime(sys.argv[1] + ' ' + sys.argv[2], '%Y/%m/%d %H:%M:%S')
        longit = float(sys.argv[3])
        latit = float(sys.argv[4])
        elev = float(sys.argv[5])

    # get hour angle
    ha = hour_angle(dt, longit, latit, elev)

    # convert hour angle to timedelta from noon
    days = ha / 360
    if days > 0.5:
        days -= 0.5
    td = timedelta(days=days)

    # make solar time
    solar_time = datetime.combine(dt.date(), time(12)) + td
    print solar_time

if __name__ == '__main__':
    main()
mattexx
  • 6,456
  • 3
  • 36
  • 47