astronomy: Add twilight support.

master
Peter Hinch 2023-12-15 08:54:45 +00:00
rodzic 8c587764da
commit 0062b0f9a1
2 zmienionych plików z 60 dodań i 28 usunięć

Wyświetl plik

@ -24,13 +24,15 @@ event in a 24 hour period starting at midnight. The midnight datum is defined in
local time. The start is a day specified as the current day plus an offset in local time. The start is a day specified as the current day plus an offset in
days. days.
A `moonphase` function is also provided enabling the moon phase to be determined It can also compute Civil, Nautical or Astronomical twilight times. A
`moonphase` function is also provided enabling the moon phase to be determined
for any date. for any date.
Caveat. I am not an astronomer. If there are errors in the fundamental 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. 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 code is currently under development but I don't anticipate breaking changes
to the API.
## 1.1 Applications ## 1.1 Applications
@ -41,14 +43,18 @@ lunar clocks such as this one - the "lunartick":
## 1.2 Licensing and acknowledgements ## 1.2 Licensing and acknowledgements
The code was ported from C/C++ as presented in "Astronomy on the Personal Some code was ported from C/C++ as presented in "Astronomy on the Personal
Computer" by Montenbruck and Pfleger, with mathematical improvements contributed Computer" by Montenbruck and Pfleger, with mathematical improvements contributed
by Marcus Mendenhall and Raul Kompaß. Raul Kompaß substantially improved the by Marcus Mendenhall and Raul Kompaß. I (Peter Hinch) performed the port and
accuracy when using 32-bit floating point. The sourcecode exists in the book and enabled support for timezones. Raul Kompaß substantially improved the accuracy
also on an accompanying CD-R. The file `CDR_license.txt` contains a copy of the when run on hardware with 32-bit floating point.
license file on the disk, which contains source, executable code, and databases.
This module (obviously) only references the source. I am not a lawyer; I have no The sourcecode exists in the book and also on an accompanying CD-R. The file
idea of the legal status of code translated from sourcecode in a published work. `CDR_license.txt` contains a copy of the license file on the disk, which
contains source, executable code, and databases. This module only references the
source. I have not spotted any restrictions on use in the book. I am not a
lawyer; I have no idea of the legal status of code based on sourcecode in a
published work.
## 1.3 Installation ## 1.3 Installation
@ -112,9 +118,13 @@ Args (float):
* `lat=LAT` Latitude in degrees (-ve is South). Defaults are my location. :) * `lat=LAT` Latitude in degrees (-ve is South). Defaults are my location. :)
* `long=LONG` Longitude in degrees (-ve is West). * `long=LONG` Longitude in degrees (-ve is West).
* `lto=0` Local time offset in hours to UTC (-ve is West); the value is checked * `lto=0` Local time offset in hours to UTC (-ve is West); the value is checked
to ensure `-12 < lto < 12`. See [section 2.3](./README.md#23-effect-of-local-time). to ensure `-15 < lto < 15`. See [section 2.3](./README.md#23-effect-of-local-time).
The constructor sets the object's date to the system date as defined by machine The constructor sets the object's date to the system date as defined by machine
time (`MT`). time (`MT`).
* `tl=None` If provided, set an offset in degrees for the definition of twilight
(6 is Civil, 12 is Nautical, 18 is Astronomical). By default twilight times are
not computed, saving some processor time. Offsets are positive numbers
representing degrees below the horizon where twilight is deemed to start and end.
## 2.2 Methods ## 2.2 Methods
@ -126,6 +136,8 @@ instance.
* `sunset(variant: int = 0)` * `sunset(variant: int = 0)`
* `moonrise(variant: int = 0)` * `moonrise(variant: int = 0)`
* `moonset(variant: int = 0)` * `moonset(variant: int = 0)`
* `tstart(variant: int = 0)` Twilight start
* `tend(variant: int = 0)` Twilight end
* `is_up(sun: bool)-> bool` Returns `True` if the selected object is above the * `is_up(sun: bool)-> bool` Returns `True` if the selected object is above the
horizon. horizon.
* `has_risen(sun: bool)->bool` Returns `True` if the selected object has risen. * `has_risen(sun: bool)->bool` Returns `True` if the selected object has risen.
@ -135,7 +147,7 @@ moon, 0.5 is full. See [section 3](./README.md#3-the-moonphase-function) for
observations about this. observations about this.
* `set_lto(t)` Set local time offset `LTO` in hours relative to UTC. Primarily * `set_lto(t)` Set local time offset `LTO` in hours relative to UTC. Primarily
intended for daylight saving time. The value is checked to ensure intended for daylight saving time. The value is checked to ensure
`-12.0 < lto < 12.0`. See [section 2.3](./README.md#23-effect-of-local-time). `-15.0 < lto < 15.0`. See [section 2.3](./README.md#23-effect-of-local-time).
The return value of the rise and set method is determined by the `variant` arg. 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 In all cases rise and set events are identified which occur in the current 24
@ -376,13 +388,16 @@ finally:
A recalculation is triggered whenever the 24 hour local time window is changed, A recalculation is triggered whenever the 24 hour local time window is changed,
such as calling `.set_day()` where the stored date changes. Normally two days of such as calling `.set_day()` where the stored date changes. Normally two days of
data are calculated, except where the local time is UTC where only one day is data are calculated, except where the local time is UTC where only one day is
required. The time to derive one day's data on RP2040 was 707μs. required. The time to derive one day's data on RP2040 was 707μs (no twilight
calculation, standard clock).
The accuracy of rise and set times was checked against online sources for The accuracy of rise and set times was checked against online sources for
several geographic locations. The online data had 1 minute resolution and the several geographic locations. The online data had 1 minute resolution and the
checked values corresponded with data computed on a platform with 64 bit checked values corresponded with data computed on a platform with 64 bit
floating point unit. The loss of precision from using a 32 bit FPU was no more floating point unit. The loss of precision from using a 32 bit FPU was no more
than 30s. than 3s.
For reasons which are unclear, the `is_up()` method is less precise, showing The lunar phase calculation is poor. It is adequate for displaying a phase icon
incorrect results when within a few minutes of the rise or set time. or adjusting a pointer, but not good enough for predicting lunar quarters.
I plan to improve this, but it may be via a separate module.

Wyświetl plik

@ -201,17 +201,17 @@ def minimoon(t):
class RiSet: class RiSet:
def __init__(self, lat=LAT, long=LONG, lto=0): # Local defaults def __init__(self, lat=LAT, long=LONG, lto=0, tl=None): # 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
if not -12 < lto < 12: self.check_lto(lto) # -15 < lto < 15
raise ValueError("Invalid local time offset.")
self.lto = round(lto * 3600) # Localtime offset in secs self.lto = round(lto * 3600) # Localtime offset in secs
self.tlight = sin(radians(tl)) if tl is not None else tl
self.mjd = None # Current integer MJD self.mjd = None # Current integer MJD
# Times in integer secs from midnight on current day (in local time) # Times in integer secs from midnight on current day (in local time)
# [sunrise, sunset, moonrise, moonset] # [sunrise, sunset, moonrise, moonset, cvend, cvstart]
self._times = [None] * 4 self._times = [None] * 6
self.set_day() # Initialise to today's date self.set_day() # Initialise to today's date
# ***** API start ***** # ***** API start *****
@ -243,12 +243,17 @@ class RiSet:
def moonset(self, variant: int = 0): def moonset(self, variant: int = 0):
return self._format(self._times[3], variant) return self._format(self._times[3], variant)
def tend(self, variant: int = 0):
return self._format(self._times[4], variant)
def tstart(self, variant: int = 0):
return self._format(self._times[5], variant)
def moonphase(self) -> float: def moonphase(self) -> float:
return self._phase return self._phase
def set_lto(self, t): # Update the offset from UTC def set_lto(self, t): # Update the offset from UTC
if not -12 < t < 12: # No need to recalc beause date is unchanged self.check_lto(t) # No need to recalc beause date is unchanged
raise ValueError("Invalid local time offset.")
lto = round(t * 3600) # Localtime offset in secs lto = round(t * 3600) # Localtime offset in secs
def has_risen(self, sun: bool): def has_risen(self, sun: bool):
@ -273,15 +278,20 @@ class RiSet:
# ***** API end ***** # ***** API end *****
# Re-calculate rise and set times # Re-calculate rise and set times
def update(self, mjd): def update(self, mjd):
self._times = [None] * 4 self._times = [None] * 6
days = (1, 2) if self.lto < 0 else (1,) if self.lto == 0 else (0, 1) days = (1, 2) if self.lto < 0 else (1,) if self.lto == 0 else (0, 1)
tr = None # Assume no twilight calculations
ts = None
for day in days: for day in days:
self.mjd = mjd + day - 1 self.mjd = mjd + day - 1
sr, ss = self.rise_set(True) # Sun sr, ss = self.rise_set(True, False) # Sun
mr, ms = self.rise_set(False) # Moon # Twilight: only calculate if required
if self.tlight is not None:
tr, ts = self.rise_set(True, True)
mr, ms = self.rise_set(False, False) # Moon
# Adjust for local time. Store in ._times if value is in 24-hour # Adjust for local time. Store in ._times if value is in 24-hour
# local time window # local time window
self.adjust((sr, ss, mr, ms), day) self.adjust((sr, ss, mr, ms, tr, ts), day)
self.mjd = mjd self.mjd = mjd
def adjust(self, times, day): def adjust(self, times, day):
@ -305,6 +315,10 @@ 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 check_lto(self, t):
if not -15 < t < 15:
raise ValueError("Invalid local time offset.")
# See https://github.com/orgs/micropython/discussions/13075 # See https://github.com/orgs/micropython/discussions/13075
def lstt(self, t, h): def lstt(self, t, h):
# Takes the mjd and the longitude (west negative) and then returns # Takes the mjd and the longitude (west negative) and then returns
@ -333,10 +347,13 @@ class RiSet:
# Modified to find sunrise and sunset only, not twilight events. # Modified to find sunrise and sunset only, not twilight events.
# Calculate rise and set times of sun or moon for the current MJD. Times are # Calculate rise and set times of sun or moon for the current MJD. Times are
# relative to that 24 hour period. # relative to that 24 hour period.
def rise_set(self, sun): def rise_set(self, sun, tl):
t_rise = None # Rise and set times in secs from midnight t_rise = None # Rise and set times in secs from midnight
t_set = None t_set = None
sinho = sin(radians(-0.833)) if sun else sin(radians(8 / 60)) if tl:
sinho = -self.tlight
else:
sinho = sin(radians(-0.833)) if sun else sin(radians(8 / 60))
# moonrise taken as centre of moon at +8 arcmin # moonrise taken as centre of moon at +8 arcmin
# sunset upper limb simple refraction # sunset upper limb simple refraction
# The loop finds the sin(alt) for sets of three consecutive # The loop finds the sin(alt) for sets of three consecutive