kopia lustrzana https://github.com/peterhinch/micropython-samples
astronomy/sun_moon.py: Beta version and docs.
rodzic
6535469252
commit
d29fb19eb4
|
@ -0,0 +1,65 @@
|
||||||
|
# Astronomical calculations in MicroPython
|
||||||
|
|
||||||
|
This module enables sun and moon rise and set times to be determined at any
|
||||||
|
geographical location. Times are in seconds from midnight and refer to any
|
||||||
|
event in a 24 hour period starting at midnight. The midnight datum is defined in
|
||||||
|
local time. The start is a day being the current day plus an offset in days.
|
||||||
|
|
||||||
|
A `moonphase` function is also provided enabling the moon phase to be determined
|
||||||
|
for any date.
|
||||||
|
|
||||||
|
The code was ported from C/C++ as presented in "Astronomy on the Personal
|
||||||
|
Computer" by Montenbruck and Pfleger, with mathematical improvements contributed
|
||||||
|
by Raul Kompaß and Marcus Mendenhall.
|
||||||
|
|
||||||
|
Caveat. I am not an astronomer. If there are errors in the fundamental
|
||||||
|
algorithms I am unlikely to be able to offer an opinion, still less a fix.
|
||||||
|
|
||||||
|
The code is currently under development: the API may change.
|
||||||
|
|
||||||
|
# The RiSet class
|
||||||
|
|
||||||
|
## Constructor
|
||||||
|
|
||||||
|
Args (float):
|
||||||
|
* `lat=LAT` Latitude in degrees. Defaults are my location. :)
|
||||||
|
* `long=LONG` Longitude in degrees (-ve is West).
|
||||||
|
* `lto=0` Local time offset in hours (-ve is West).
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
* `set_day(day: int = 0)` The arg is the offset from the current system date.
|
||||||
|
Calling this with a changed arg causes the rise and set times to be updated.
|
||||||
|
* `sunrise(variant: int = 0)` See below for details and the `variant` arg.
|
||||||
|
* `sunset(variant: int = 0)`
|
||||||
|
* `moonrise(variant: int = 0)`
|
||||||
|
* `moonset(variant: int = 0)`
|
||||||
|
* `moonphase()` Return current phase as a float: 0.0 <= result < 1.0. 0.0 is new
|
||||||
|
moon, 0.5 is full.
|
||||||
|
|
||||||
|
The return value of the rise and set method is determined by the `variant` arg.
|
||||||
|
In all cases rise and set events are identified which occur in the current 24
|
||||||
|
hour period. Note that a given event may be absent in the period: this can occur
|
||||||
|
with the moon at most locations, and with the sun in polar regions.
|
||||||
|
|
||||||
|
Variants:
|
||||||
|
* 0 Return integer seconds since midnight local time (or `None` if no event).
|
||||||
|
* 1 Return integer seconds since since epoch of the MicroPython platform
|
||||||
|
(or `None`).
|
||||||
|
* 2 Return text of form hh:mm:ss (or --:--:--) being local time.
|
||||||
|
|
||||||
|
# The moonphase function
|
||||||
|
|
||||||
|
This is a simple function whose provenance is uncertain. I have a lunar clock
|
||||||
|
which uses a C version of this which has run for 14 years without issue, but I
|
||||||
|
can't vouch for its absolute accuracy over long time intervals. The Montenbruck
|
||||||
|
and Pfleger version is very much more involved but they claim accuracy over
|
||||||
|
centuries.
|
||||||
|
|
||||||
|
Args (all integers):
|
||||||
|
* `year` 4-digit year
|
||||||
|
* `month` 1..12
|
||||||
|
* `day` Day of month 1..31
|
||||||
|
* `hour` 0..23
|
||||||
|
|
||||||
|
Return value:
|
||||||
|
A float in range 0.0 <= result < 1.0, 0 being new moon, 0.5 being full moon.
|
|
@ -6,17 +6,18 @@
|
||||||
|
|
||||||
# Source "Astronomy on the Personal Computer" by Montenbruck and Pfleger
|
# Source "Astronomy on the Personal Computer" by Montenbruck and Pfleger
|
||||||
# ISBN 978-3-540-67221-0
|
# ISBN 978-3-540-67221-0
|
||||||
|
# Also contributions from Raul Kompaß and Marcus Mendenhall: see
|
||||||
|
# https://github.com/orgs/micropython/discussions/13075
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from math import sin, cos, sqrt, fabs, atan, radians, floor
|
from math import sin, cos, sqrt, fabs, atan, radians, floor
|
||||||
|
|
||||||
LAT = 53.29756504536339 # Local defaults
|
LAT = 53.29756504536339 # Local defaults
|
||||||
LONG = -2.102811634540558
|
LONG = -2.102811634540558
|
||||||
MOON_PHASE_LENGTH = 29.530588853
|
|
||||||
|
|
||||||
|
|
||||||
def quad(ym, yz, yp):
|
def quad(ym, yz, yp):
|
||||||
# See Astronomy on the PC P48-49
|
# See Astronomy on the PC P48-49, plus contribution from Marcus Mendenhall
|
||||||
# finds the parabola throuh the three points (-1,ym), (0,yz), (1, yp)
|
# finds the parabola throuh the three points (-1,ym), (0,yz), (1, yp)
|
||||||
# and returns the values of x where the parabola crosses zero
|
# and returns the values of x where the parabola crosses zero
|
||||||
# (roots of the quadratic)
|
# (roots of the quadratic)
|
||||||
|
@ -29,9 +30,11 @@ def quad(ym, yz, yp):
|
||||||
ye = (a * xe + b) * xe + c
|
ye = (a * xe + b) * xe + c
|
||||||
dis = b * b - 4.0 * a * c # discriminant of y=a*x^2 +bx +c
|
dis = b * b - 4.0 * a * c # discriminant of y=a*x^2 +bx +c
|
||||||
if dis > 0: # parabola has roots
|
if dis > 0: # parabola has roots
|
||||||
dx = 0.5 * sqrt(dis) / fabs(a)
|
if b < 0:
|
||||||
z1 = xe - dx
|
z2 = (-b + sqrt(dis)) / (2 * a) # z2 is larger root in magnitude
|
||||||
z2 = xe + dx
|
else:
|
||||||
|
z2 = (-b - sqrt(dis)) / (2 * a)
|
||||||
|
z1 = (c / a) / z2 # z1 is always closer to zero
|
||||||
if fabs(z1) <= 1.0:
|
if fabs(z1) <= 1.0:
|
||||||
nz += 1
|
nz += 1
|
||||||
if fabs(z2) <= 1.0:
|
if fabs(z2) <= 1.0:
|
||||||
|
@ -53,8 +56,8 @@ def quad(ym, yz, yp):
|
||||||
# (date(2000, 1, 1) - date(1970, 1, 1)).days * 24*60*60 = 946684800
|
# (date(2000, 1, 1) - date(1970, 1, 1)).days * 24*60*60 = 946684800
|
||||||
# (date(2000, 1, 1) - date(1970, 1, 1)).days = 10957
|
# (date(2000, 1, 1) - date(1970, 1, 1)).days = 10957
|
||||||
|
|
||||||
# Re platform comparisons get_mjd does integer arithmetic and returns the same
|
# Re platform comparisons get_mjd returns the same value regardless of
|
||||||
# value regardless of the platform's epoch
|
# the platform's epoch: integer days since 00:00 on 17 November 1858.
|
||||||
def get_mjd(ndays: int = 0) -> int: # Days offset from today
|
def get_mjd(ndays: int = 0) -> int: # Days offset from today
|
||||||
secs_per_day = 86400 # 24 * 3600
|
secs_per_day = 86400 # 24 * 3600
|
||||||
tsecs = time.time() # Time now in secs since epoch
|
tsecs = time.time() # Time now in secs since epoch
|
||||||
|
@ -78,6 +81,7 @@ def to_int(x):
|
||||||
|
|
||||||
|
|
||||||
# Approximate moon phase in range 0.0..1.0 0.0 is new moon, 0.5 full moon
|
# Approximate moon phase in range 0.0..1.0 0.0 is new moon, 0.5 full moon
|
||||||
|
# Provenance of this cde is uncertain.
|
||||||
def moonphase(year, month, day, hour):
|
def moonphase(year, month, day, hour):
|
||||||
fty = year - floor((12.0 - month) / 10.0)
|
fty = year - floor((12.0 - month) / 10.0)
|
||||||
itm = month + 9
|
itm = month + 9
|
||||||
|
@ -89,7 +93,7 @@ def moonphase(year, month, day, hour):
|
||||||
tmp = term1 + term2 + day + 59 + hour / 24.0
|
tmp = term1 + term2 + day + 59 + hour / 24.0
|
||||||
if tmp > 2299160.0:
|
if tmp > 2299160.0:
|
||||||
tmp = tmp - term3
|
tmp = tmp - term3
|
||||||
phi = (tmp - 2451550.1) / MOON_PHASE_LENGTH
|
phi = (tmp - 2451550.1) / 29.530588853 # 29.530588853 is length of lunar cycle (days)
|
||||||
return phi % 1
|
return phi % 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -181,53 +185,74 @@ def minimoon(t):
|
||||||
|
|
||||||
|
|
||||||
class RiSet:
|
class RiSet:
|
||||||
def __init__(self, lat=LAT, long=LONG): # Local defaults
|
def __init__(self, lat=LAT, long=LONG, lto=0): # Local defaults
|
||||||
self.sglat = sin(radians(lat))
|
self.sglat = sin(radians(lat))
|
||||||
self.cglat = cos(radians(lat))
|
self.cglat = cos(radians(lat))
|
||||||
self.long = long
|
self.long = long
|
||||||
|
self.lto = round(lto * 3600) # Localtime offset in secs
|
||||||
self.mjd = None # Current integer MJD
|
self.mjd = None # Current integer MJD
|
||||||
# Times in integer secs from midnight on current day
|
# Times in integer secs from midnight on current day (in local time)
|
||||||
self._sr = None # Sunrise
|
# [sunrise, sunset, moonrise, moonset]
|
||||||
self._ss = None # Sunset
|
self._times = [None] * 4
|
||||||
self._mr = None # Moonrise
|
|
||||||
self._ms = None # Moon set
|
|
||||||
self.set_day() # Initialise to today's date
|
self.set_day() # Initialise to today's date
|
||||||
|
|
||||||
# ***** API start *****
|
# ***** API start *****
|
||||||
# 109μs on PBD-SF2W 166μs on ESP32-S3 394μs on RP2 (standard clocks)
|
# 109μs on PBD-SF2W 166μs on ESP32-S3 394μs on RP2 (standard clocks)
|
||||||
def set_day(self, day=0):
|
def set_day(self, day: int = 0):
|
||||||
mjd = get_mjd(day)
|
mjd = get_mjd(day)
|
||||||
if self.mjd is None or self.mjd != mjd:
|
if self.mjd is None or self.mjd != mjd:
|
||||||
spd = 86400 # Secs per day
|
spd = 86400 # Secs per day
|
||||||
self._t0 = ((round(time.time()) + day * spd) // spd) * spd # Midnight on target day
|
# ._t0 is time of midnight (local time) in secs since MicroPython epoch
|
||||||
|
# time.time() assumes MicroPython clock is set to local time
|
||||||
|
self._t0 = ((round(time.time()) + day * spd) // spd) * spd
|
||||||
|
self._times = [None] * 4
|
||||||
|
self._ms = None # Moon set
|
||||||
|
for day in range(3):
|
||||||
|
self.mjd = mjd + day - 1
|
||||||
|
sr, ss = self.rise_set(True) # Sun
|
||||||
|
mr, ms = self.rise_set(False) # Moon
|
||||||
|
# Adjust for local time. Store in ._times if value is in 24-hour
|
||||||
|
# local time window
|
||||||
|
self.adjust(sr, day, 0)
|
||||||
|
self.adjust(ss, day, 1)
|
||||||
|
self.adjust(mr, day, 2)
|
||||||
|
self.adjust(ms, day, 3)
|
||||||
self.mjd = mjd
|
self.mjd = mjd
|
||||||
self._sr, self._ss = self.rise_set(True) # Sun
|
t = time.gmtime(time.time() + day * spd)
|
||||||
self._mr, self._ms = self.rise_set(False) # Moon
|
|
||||||
t = time.gmtime(time.time() + day * 86400)
|
|
||||||
self._phase = moonphase(*t[:4])
|
self._phase = moonphase(*t[:4])
|
||||||
|
return self # Allow r.set_day().sunrise()
|
||||||
|
|
||||||
def sunrise(self, to=0):
|
# variants: 0 secs since 00:00:00 localtime. 1 secs since MicroPython epoch
|
||||||
return self._format(self._sr, to)
|
# (relies on system being set to localtime). 2 human-readable text.
|
||||||
|
def sunrise(self, variant: int = 0):
|
||||||
|
return self._format(self._times[0], variant)
|
||||||
|
|
||||||
def sunset(self, to=0):
|
def sunset(self, variant: int = 0):
|
||||||
return self._format(self._ss, to)
|
return self._format(self._times[1], variant)
|
||||||
|
|
||||||
def moonrise(self, to=0):
|
def moonrise(self, variant: int = 0):
|
||||||
return self._format(self._mr, to)
|
return self._format(self._times[2], variant)
|
||||||
|
|
||||||
def moonset(self, to=0):
|
def moonset(self, variant: int = 0):
|
||||||
return self._format(self._ms, to)
|
return self._format(self._times[3], variant)
|
||||||
|
|
||||||
def moonphase(self):
|
def moonphase(self):
|
||||||
return self._phase
|
return self._phase
|
||||||
|
|
||||||
# ***** API end *****
|
# ***** API end *****
|
||||||
def _format(self, n, to):
|
def adjust(self, n, day, idx):
|
||||||
if to == 0: # Default: secs since Midnight
|
if n is not None:
|
||||||
|
n += self.lto + (day - 1) * 86400
|
||||||
|
h = n // 3600
|
||||||
|
if 0 <= h < 24:
|
||||||
|
self._times[idx] = n
|
||||||
|
|
||||||
|
def _format(self, n, variant):
|
||||||
|
if variant == 0: # Default: secs since Midnight (local time)
|
||||||
return n
|
return n
|
||||||
elif to == 1: # Secs since epoch
|
elif variant == 1: # Secs since epoch of MicroPython platform
|
||||||
return None if n is None else n + self._t0
|
return None if n is None else n + self._t0
|
||||||
# to == 3
|
# variant == 3
|
||||||
if n is None:
|
if n is None:
|
||||||
return "--:--:--"
|
return "--:--:--"
|
||||||
else:
|
else:
|
||||||
|
@ -235,26 +260,31 @@ class RiSet:
|
||||||
mi, sec = divmod(tmp, 60)
|
mi, sec = divmod(tmp, 60)
|
||||||
return f"{hr:02d}:{mi:02d}:{sec:02d}"
|
return f"{hr:02d}:{mi:02d}:{sec:02d}"
|
||||||
|
|
||||||
def lmst(self, mjd):
|
# See https://github.com/orgs/micropython/discussions/13075
|
||||||
|
def lmstt(self, t):
|
||||||
# Takes the mjd and the longitude (west negative) and then returns
|
# Takes the mjd and the longitude (west negative) and then returns
|
||||||
# the local sidereal time in hours. Im using Meeus formula 11.4
|
# the local sidereal time in hours. Im using Meeus formula 11.4
|
||||||
# instead of messing about with UTo and so on
|
# instead of messing about with UTo and so on
|
||||||
d = mjd - 51544.5
|
# modified to use the pre-computed 't' value from sin_alt
|
||||||
t = d / 36525.0
|
d = t * 36525
|
||||||
lst = 280.46061837 + 360.98564736629 * d + 0.000387933 * t * t - t * t * t / 38710000
|
df = frac(d)
|
||||||
|
c1 = 360
|
||||||
|
c2 = 0.98564736629
|
||||||
|
dsum = c1 * df + c2 * d # no large integer * 360 here
|
||||||
|
lst = 280.46061837 + dsum + 0.000387933 * t * t - t * t * t / 38710000
|
||||||
return (lst % 360) / 15.0 + self.long / 15
|
return (lst % 360) / 15.0 + self.long / 15
|
||||||
|
|
||||||
def sin_alt(self, hour, func):
|
def sin_alt(self, hour, func):
|
||||||
# Returns the sine of the altitude of the object (moon or sun)
|
# Returns the sine of the altitude of the object (moon or sun)
|
||||||
# at an hour relative to the current date (mjd)
|
# at an hour relative to the current date (mjd)
|
||||||
mjd = self.mjd + hour / 24.0
|
mjd1 = (self.mjd - 51544.5) + hour / 24.0
|
||||||
t = (mjd - 51544.5) / 36525.0
|
t1 = mjd1 / 36525.0
|
||||||
dec, ra = func(t)
|
# print(f"sin_alt mjd0={mjd0:.7f} t0={t0:.9f} mjd1={mjd1:.7f} t1={t1:.9f}")
|
||||||
|
dec, ra = func(t1)
|
||||||
# hour angle of object: one hour = 15 degrees. Note lmst() uses longitude
|
# hour angle of object: one hour = 15 degrees. Note lmst() uses longitude
|
||||||
tau = 15.0 * (self.lmst(mjd) - ra)
|
tau = 15.0 * (self.lmstt(t1) - ra)
|
||||||
# sin(alt) of object using the conversion formulas
|
# sin(alt) of object using the conversion formulas
|
||||||
salt = self.sglat * sin(radians(dec)) + self.cglat * cos(radians(dec)) * cos(radians(tau))
|
return self.sglat * sin(radians(dec)) + self.cglat * cos(radians(dec)) * cos(radians(tau))
|
||||||
return salt
|
|
||||||
|
|
||||||
# Modified to find sunrise and sunset only, not twilight events.
|
# Modified to find sunrise and sunset only, not twilight events.
|
||||||
def rise_set(self, sun):
|
def rise_set(self, sun):
|
||||||
|
@ -296,8 +326,17 @@ class RiSet:
|
||||||
|
|
||||||
|
|
||||||
r = RiSet()
|
r = RiSet()
|
||||||
|
# Seattle RiSet(lat=47.61, long=-122.35, lto=-8)
|
||||||
|
# t = time.ticks_us()
|
||||||
|
# r.set_day()
|
||||||
|
# print("Elapsed us", time.ticks_diff(time.ticks_us(), t))
|
||||||
for d in range(7):
|
for d in range(7):
|
||||||
print(f"Day {d}")
|
print(f"Day {d}")
|
||||||
r.set_day(d)
|
r.set_day(d)
|
||||||
print(f"Sun rise {r.sunrise(3)} set {r.sunset(3)}")
|
print(f"Sun rise {r.sunrise(3)} set {r.sunset(3)}")
|
||||||
print(f"Moon rise {r.moonrise(3)} set {r.moonset(3)}")
|
print(f"Moon rise {r.moonrise(3)} set {r.moonset(3)}")
|
||||||
|
|
||||||
|
print(r.set_day().sunrise(0))
|
||||||
|
# for d in range(30):
|
||||||
|
# r.set_day(d)
|
||||||
|
# print(round(r.moonphase() * 1000))
|
||||||
|
|
Ładowanie…
Reference in New Issue