Porównaj commity

..

3 Commity

Autor SHA1 Wiadomość Data
Peter Hinch 6d639f0008 astronomy: RiSet fix bug in .has_risen() and .has_set(). 2023-12-29 11:17:07 +00:00
Peter Hinch 8f6463845d astronomy: Fix README.md. 2023-12-25 10:27:40 +00:00
Peter Hinch 03c924d6b4 astronomy: Add moonphase.py. 2023-12-25 10:04:33 +00:00
4 zmienionych plików z 378 dodań i 65 usunięć

Wyświetl plik

@ -10,29 +10,36 @@
2.2 [Methods](./README.md#22-methods) 2.2 [Methods](./README.md#22-methods)
2.3 [Effect of local time](./README.md#23-effect-of-local-time) 2.3 [Effect of local time](./README.md#23-effect-of-local-time)
2.4 [Continuously running applications](./README.md#24-continuously-running-applications) 2.4 [Continuously running applications](./README.md#24-continuously-running-applications)
3. [The moonphase function](./README.md#3-the-moonphase-function) 3. [Utility functions](./README.md#3-utility-functions)
4. [Utility functions](./README.md#4-utility-functions) 4. [Demo script](./README.md#4-demo-script)
5. [Demo script](./README.md#5-demo-script) 5. [Scheduling events](./README.md#5-scheduling-events)
6. [Scheduling events](./README.md#6-scheduling-events) 6. [The moonphase module](./README.md#6-the-moonphase-module)
6.1 [Constructor](./README.md#61-constructor)
6.2 [Methods](./README.md#62-methods)
6.3 [Usage examples](./README.md#63-usage-examples)
6.4 [DST](./README.md#64-dst) Daylight savings time.
7. [Performance and accuracy](./README.md#7-performance-and-accuracy) 7. [Performance and accuracy](./README.md#7-performance-and-accuracy)
7.1 [RiSet class](./README.md#71-riset-class)
7.2 [moonphase class](./README.md#72-moonphase-class)
# 1. Overview # 1. Overview
This module enables sun and moon rise and set times to be determined at any The `sun_moon` module enables sun and moon rise and set times to be determined
geographical location. Times are in seconds from midnight and refer to any at any geographical location. Times are in seconds from midnight and refer to
event in a 24 hour period starting at midnight. The midnight datum is defined in any event in a 24 hour period starting at midnight. The midnight datum is
local time. The start is a day specified as the current day plus an offset in defined in local time. The start is a day specified as the current day plus an
days. offset in days. It can also compute Civil, Nautical or Astronomical twilight
times.
It can also compute Civil, Nautical or Astronomical twilight times. A The `moonphase` module enables the moon phase to be determined for any date, and
`moonphase` function is also provided enabling the moon phase to be determined the dates and times of lunar quarters to be calculated.
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 but I don't anticipate breaking changes The `moonphase` module is currently under development: API changes are possible.
to the API.
Moon phase options have been removed from `sun_moon` because accuracy was poor.
## 1.1 Applications ## 1.1 Applications
@ -43,6 +50,8 @@ lunar clocks such as this one - the "lunartick":
## 1.2 Licensing and acknowledgements ## 1.2 Licensing and acknowledgements
#### sun_moon.py
Some 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ß. I (Peter Hinch) performed the port and by Marcus Mendenhall and Raul Kompaß. I (Peter Hinch) performed the port and
@ -56,6 +65,11 @@ 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 lawyer; I have no idea of the legal status of code based on sourcecode in a
published work. published work.
#### moonphase.py
This was derived from unrestricted public sources and is released under the MIT
licence.
## 1.3 Installation ## 1.3 Installation
Installation copies files from the `astronomy` directory to a directory Installation copies files from the `astronomy` directory to a directory
@ -79,7 +93,8 @@ Move to `micropython-samples` on the PC, run `rshell` and issue:
``` ```
`mip` installs the following files in the `sched` directory. `mip` installs the following files in the `sched` directory.
* `sun_moon.py` * `sun_moon.py`
* `sun_moon_test.py` A test/demo script. * `sun_moon_test.py` A test/demo script for the above.
* `moonphase.py` Determine lunar quarters and phase.
After installation the `RiSet` class may be accessed with After installation the `RiSet` class may be accessed with
```python ```python
from sched.sun_moon import RiSet from sched.sun_moon import RiSet
@ -146,11 +161,8 @@ instance.
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.
* `has_set(sun: bool)->bool` Returns `True` if the selected object has set. * `has_set(sun: bool)->bool` Returns `True` if the selected object has set.
* `moonphase()->float` Return current phase: 0.0 <= result < 1.0. 0.0 is new
moon, 0.5 is full. See [section 3](./README.md#3-the-moonphase-function) for
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 system longitude. The value is checked to ensure
`-15.0 < lto < 15.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.
@ -223,28 +235,14 @@ bad idea. It is usually best to run winter time all year round. Where a DST
change occurs, the `RiSet.set_lto()` method should be run to ensure that `RiSet` change occurs, the `RiSet.set_lto()` method should be run to ensure that `RiSet`
operates in current local time. operates in current local time.
# 3. The moonphase function # 3. Utility functions
This is a simple function whose provenance is uncertain. It appears to produce
valid results but I plan to implement a better solution.
Args:
* `year: int` 4-digit year
* `month: int` 1..12
* `day: int` Day of month 1..31
* `hour: int` 0..23
Return value:
A float in range 0.0 <= result < 1.0, 0 being new moon, 0.5 being full moon.
# 4. Utility functions
`now_days() -> int` Returns the current time as days since the platform epoch. `now_days() -> int` Returns the current time as days since the platform epoch.
`abs_to_rel_days(days: int) -> int` Takes a number of days since the Unix epoch `abs_to_rel_days(days: int) -> int` Takes a number of days since the Unix epoch
(1970,1,1) and returns a number of days relative to the current date. Platform (1970,1,1) and returns a number of days relative to the current date. Platform
independent. This facilitates testing with pre-determined target dates. independent. This facilitates testing with pre-determined target dates.
# 5. Demo script # 4. Demo script
This produces output for the fixed date 4th Dec 2023 at three geographical This produces output for the fixed date 4th Dec 2023 at three geographical
locations. It can therefore be run on platforms where the system time is wrong. locations. It can therefore be run on platforms where the system time is wrong.
@ -290,7 +288,7 @@ Maximum error 0. Expect 0 on 64-bit platform, 30s on 32-bit
``` ```
Code comments show times retrieved from `timeanddate.com`. Code comments show times retrieved from `timeanddate.com`.
# 6. Scheduling events # 5. Scheduling events
A likely use case is to enable events to be timed relative to sunrise and set. A likely use case is to enable events to be timed relative to sunrise and set.
In simple cases this can be done with `asyncio`. This will execute a payload at In simple cases this can be done with `asyncio`. This will execute a payload at
@ -386,9 +384,114 @@ try:
finally: finally:
_ = asyncio.new_event_loop() _ = asyncio.new_event_loop()
``` ```
# 6. The moonphase module
This contains a single class `MoonPhase`. The term "machine time" below refers
to the time reported by the MicroPython `time` module. The "local time offset"
(LTO) passed to the constructor specifies the difference between machine time
and UTC based on system longitude. "Daylight saving time" (DST) allows reported
times to be offset to compensate for DST. Internally phases are calculated in
UTC, but where times are output they are adjusted for LTO and DST.
It is recommended that the machine clock is not adjusted for DST because large
changes can play havoc with program timing as described above. To accommodate
DST, a `dst` function can be provided to the constructor. The module uses this
to adjust reported times.
A `MoonPhase` instance has a time `datum`, which defaults to the instantiation
time. Phases are calculated with respect to this datum. It may be changed using
`.set_day` to enable future and past phases to be determined or to enable long
running applications to track time.
The module is imported as follows:
```python
from sched.moonphase import MoonPhase
```
## 6.1 Constructor
* `lto:float=0, dst = lambda x: x` Local time offset in hours to UTC (-ve is
West); the value is checked to ensure `-15 < lto < 15`. `dst` is an optional
user defined function for Daylight Saving Time (DST). See
[section 6.4](./README.md#64-dst)
## 6.2 Methods
* `quarter(q: int, text: bool = True)` Return the time of a given quarter. Five
quarters are calculated around the instance datum. By default the time
is last midnight machine time with an optional offset in days `doff` added. The
`quarter` arg specifies the quarter with 0 and 4 being new moons and quarter 2
being full. The `text` arg determines how the value is returned: as text or as
`int` is secs from the machine epoch. Results are adjusted for DST if a `dst`
function is provided to the constructor.
* `phase() -> float)` Returns moon phase where 0.0 <= phase < 1.0 with 0.5 being
full moon. The phase is that pertaining to the datum.
* `nextphase(text: bool = True)` This is a generator function. Each iteration
of the generator returns three values: the phase number, the lunation number and
the datetime of the phase. The `text` arg is as per `.quarter()`, defining the
format of the datetime.
* `set_day(doff: float = 0)` Set the `MoonPhase` datum time to machine time plus
an offset in days: this may include a fractional part if `.phase()` is required
to produce a time-precise value. The five quarters are calculated for the
lunation including the midnight at the start of the specified day.
* `set_lto(t:float)` Redefine the local time offset, `t` being in hours as
per the constructor arg.
* `datum(text: bool = True)` Returns the current datum.
## 6.3 Usage examples
```python
from sched.moonphase import MoonPhase
mp = MoonPhase() # datum is midnight last night
print(f"Full moon, current lunation {mp.quarter(2)}")
mp.set_day(0.5) # Adjust datum to noon today machine time
print(f"Phase at Noon {mp.phase()}")
mp.set_day(182) # Set datum ahead 6 months
print(f"Lunation 1st new moon: {mp.quarter(0)}, 2nd new moon: {mp.quarter(4)}")
mp.set_day(0) # Reset datum to today
n = mp.nextphase() # Instantiate generator
for _ in range(8):
print(next(n))
```
## 6.4 DST
Daylight saving time depends on country and geographic location, and there is no
built-in MicroPython support. The moonphase module supports DST via an optional
user supplied function. DST does not affect the calculation of quarters or phase
which is based on the machine clock. If the machine clock runs at a fixed offset
to UTC (which is recommended), a DST function can be used to enable reported
results to reflect local time.
A DST function takes as input a datetime measured in seconds since the machine
epoch (as returned by `time.time()`) and returns that number adjusted for local
time. The following example is for UK time, which adds one hour at 2:00 on the
last Sunday in March, reverting to winter time at 2:00 on the last Sunday in
October.
```python
def uk_dst(secs_epoch: int): # Change in March (3) and Oct (10)
t = time.gmtime(secs_epoch)
month = t[1]
mday = t[2]
wday = t[6]
winter = secs_epoch
summer = secs_epoch + 3600 # +1hr
if month in (1, 2, 11, 12): # Simple cases: depend only on month
return winter
if not month in (3, 10):
return summer # +1 hr
# We are in March or October. Find the day in month of last Sunday.
ld = (wday + 31 - mday) % 7 # weekday of 31st.
lsun = 31 - (1 + ld) % 7 # Monthday of last Sunday
thresh = time.mktime((t[0], month, lsun, 2, 0, 0, 6, 0)) # 2am last Sunday in month
return summer if ((secs_epoch >= thresh) ^ (month == 10)) else winter
```
# 7. Performance and accuracy # 7. Performance and accuracy
## 7.1 RiSet class
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
@ -401,7 +504,6 @@ 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 3s. than 3s.
The lunar phase calculation is poor. It is adequate for displaying a phase icon ## 7.2 MoonPhase class
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. TODO

Wyświetl plik

@ -0,0 +1,228 @@
# moonphase.py Calculate lunar phases
# Source Tech\ Notes/Astronomy/astro_references/moontool.c
# The information for this was drawn from public domain sources including C code
# written by John Walker and Ron Hitchens in 1987-88 and released with the "licence"
# Do what thou wilt shall be the whole of the law".
# Uses Python arbitrary length integers to maintain accuracy on platforms with
# 32-bit floating point.
# Copyright (c) Peter Hinch 2023 Released under the MIT license.
# Exports calc_phases()
from math import radians, sin, cos, floor
import time
import array
SYNMONTH = 29.53058868 # Synodic month (new Moon to new Moon)
# MEANPHASE -- Calculates time of the mean new Moon for a given base date.
# This argument K to this function is the precomputed synodic month index, given by:
# K = (year - 1900) * 12.3685
# where year is expressed as a year and fractional year.
# sdate is days from 1900 January 0.5. Returns days from 1900 January 0.5
def meanphase(sdate: float, k: int) -> float:
# Time in Julian centuries from 1900 January 0.5
t = sdate / 36525
t2 = t * t # Square for frequent use
t3 = t2 * t # Cube for frequent use
nt1 = 0.75933 + SYNMONTH * k + 0.0001178 * t2 - 0.000000155 * t3
return nt1 + 0.00033 * sin(radians(166.56 + 132.87 * t - 0.009173 * t2))
# TRUEPHASE -- Given a K value used to determine the mean phase of the new moon,
# and a phase no. (0..3), return the true, corrected phase time
# as integer Julian seconds.
def truephase(k: int, phi: int) -> int:
k += (0, 0.25, 0.5, 0.75)[phi] # Add phase to new moon time
t = k / 1236.85 # Time in Julian centuries from 1900 January 0.5
t2 = t * t # Square for frequent use
t3 = t2 * t # Cube for frequent use
# Sun's mean anomaly
m = 359.2242 + 29.10535608 * k - 0.0000333 * t2 - 0.00000347 * t3
# Moon's mean anomaly
mprime = 306.0253 + 385.81691806 * k + 0.0107306 * t2 + 0.00001236 * t3
# Moon's argument of latitude
f = 21.2964 + 390.67050646 * k - 0.0016528 * t2 - 0.00000239 * t3
if phi in (0, 2): # Corrections for New and Full Moon
pt = (0.1734 - 0.000393 * t) * sin(radians(m))
pt += 0.0021 * sin(radians(2 * m))
pt -= 0.4068 * sin(radians(mprime))
pt += 0.0161 * sin(radians(2 * mprime))
pt -= 0.0004 * sin(radians(3 * mprime))
pt += 0.0104 * sin(radians(2 * f))
pt -= 0.0051 * sin(radians(m + mprime))
pt -= 0.0074 * sin(radians(m - mprime))
pt += 0.0004 * sin(radians(2 * f + m))
pt -= 0.0004 * sin(radians(2 * f - m))
pt -= 0.0006 * sin(radians(2 * f + mprime))
pt += 0.0010 * sin(radians(2 * f - mprime))
pt += 0.0005 * sin(radians(m + 2 * mprime))
else: # First or last quarter
pt = (0.1721 - 0.0004 * t) * sin(radians(m))
pt += 0.0021 * sin(radians(2 * m))
pt -= 0.6280 * sin(radians(mprime))
pt += 0.0089 * sin(radians(2 * mprime))
pt -= 0.0004 * sin(radians(3 * mprime))
pt += 0.0079 * sin(radians(2 * f))
pt -= 0.0119 * sin(radians(m + mprime))
pt -= 0.0047 * sin(radians(m - mprime))
pt += 0.0003 * sin(radians(2 * f + m))
pt -= 0.0004 * sin(radians(2 * f - m))
pt -= 0.0006 * sin(radians(2 * f + mprime))
pt += 0.0021 * sin(radians(2 * f - mprime))
pt += 0.0003 * sin(radians(m + 2 * mprime))
pt += 0.0004 * sin(radians(m - 2 * mprime))
pt -= 0.0003 * sin(radians(2 * m + mprime))
if phi < 2: # First quarter correction
pt += 0.0028 - 0.0004 * cos(radians(m)) + 0.0003 * cos(radians(mprime))
else: # Last quarter correction
pt += -0.0028 + 0.0004 * cos(radians(m)) - 0.0003 * cos(radians(mprime))
pt = round(pt * 86400) # Integer seconds from here
pt += round(2_953_058_868 * 864 * k) // 1000_000 # round(SYNMONTH * k * 86400)
qq = 0.0001178 * t2 - 0.000000155 * t3
qq += 0.00033 * sin(radians(166.56 + 132.87 * t - 0.009173 * t2))
pt += round(qq * 86400) # qq amounts to 2s
return pt + 208_657_793_606
def dt_to_text(tim): # Convert a time to text
t = time.localtime(tim)
return f"{t[2]:02}/{t[1]:02}/{t[0]:4} {t[3]:02}:{t[4]:02}:{t[5]:02}"
class MoonPhase:
verbose = True
def __init__(self, lto: float = 0, dst=lambda x: x):
self.lto_s = self._check_lto(lto) # -15 < lto < 15
# local time = UTC + lto .lto_s = offset in secs
self.dst = dst
# Datetimes in secs since hardware epoch based on UTC
# With epoch 1970 this could need long ints.
self.phases = array.array("q", (0,) * 5)
# Calculate Julian date of machine epoch
# Multiply by 100 to avoid fraction
jepoch = 244058750 # Julian date of Unix epoch (1st Jan 1970) * 100
if time.gmtime(0)[0] == 2000: # Machine epoch
jepoch += 1095700
jepoch *= 864 # Seconds from epoch
self.jepoch = jepoch
self.secs = 0 # Time of calling .set_day in secs UTC
self.set_day() # Populate array and .secs
if MoonPhase.verbose:
print(f"Machine time: {dt_to_text(time.time())}")
MoonPhase.verbose = False
# Take offset in days from today, return time of last midnight in secs from machine epoch
# Take time of last midnight machine time in secs since machine epoch. Add a
# passed offset in days. Convert to UTC using LTO. The returned value is as
# if the hardware clock were running UTC.
def _midnight(self, doff: float = 0): # Midnight last night + days offset (UTC)
tl = round((time.time() // 86400 + doff) * 86400) # Target in local time
return tl - self.lto_s
def set_lto(self, t: float): # Update the offset from UTC
self.lto_s = self._check_lto(t) # Localtime offset in secs
def set_day(self, doff: float = 0):
self.secs = round(time.time() + doff * 86400 - self.lto_s)
start = self._midnight(doff) # Phases are calculated around this time (UTC)
self._populate(start) # Immediate return if .phases already OK
def datum(self, text: bool = True):
t = self.secs + self.lto_s
return dt_to_text(t) if text else t
def quarter(self, q: int, text: bool = True):
if not 0 <= q <= 4:
raise ValueError("Quarter nos must be from 0 to 4.")
tutc = self.phases[q] # Time of phase in secs UTC
# Adjust time: t is machine time in secs since machine epoch
t = self.dst(tutc + self.lto_s) # UTC secs from hardware epoch -> local time
return dt_to_text(t) if text else t # Secs since machine epoch
# Return moon phase as 0.0 <= n < 1.0 by defaut for current datetime.
def phase(self) -> float: # doff: days offset with optional fraction
t = self.secs # As set by .set_day()
if not (self.phases[0] <= t <= self.phases[4]): # set_day was not called
self.set_day() # Assume today
prev = self.phases[0]
for n, phi in enumerate(self.phases):
if phi > t:
break # phi is upcoming phase time
prev = phi # Last phase before now
if prev == phi: # Day is day of new moon: use synodic month/4
r = (t - prev) * 0.25 / 637860.715488
if r < 0:
r = 1 - r
else:
r = (n - 1) * 0.25 + (t - prev) * 0.25 / (phi - prev)
return min(r, 0.999999) # Rare pathological results where r slightly > 1.0
def _next_lunation(self): # Use approx time of next full moon to advance
self._populate(round(self.phases[2] + SYNMONTH * 86400))
# toff: days offset with optional fraction
def nextphase(self, text: bool = True):
n = 0
lun = 0 # Skip historic quarters
while True:
yield n, lun, self.quarter(n, text)
n += 1
n %= 4
if n == 0:
self._next_lunation()
lun += 1
def _check_lto(self, lto: float) -> int:
if not -15 < lto < 15:
raise ValueError("Invalid local time offset.")
return round(lto * 3600)
# Populate the phase array. Fast return if phases are alrady correct.
# Find time of phases of the moon which surround the passed datetime.
# Five phases are found, starting and ending with the new moons which bound
# the specified lunation.
# Passed time, and the result in .phases, are seconds since hardware epoch
# adjusted for UTC: i.e. as if the RTC were running UTC rather than local time.
def _populate(self, t: int):
if self.phases[0] < t < self.phases[4]:
return # Nothing to do
# Return days since Jan 0.5 1900 as a float. Returns same value on 32 and 64 bits
def jd1900(t: int) -> float:
y, m, mday = time.localtime(t)[:3]
if m <= 2:
m += 12
y -= 1
b = round(y / 400 - y / 100 + y / 4)
mjm = 365 * y - 679004 + b + int(30.6001 * (m + 1)) + mday
return mjm - 15019.5
sdate: float = jd1900(t) # Days since 1900 January 0.5
adate: float = sdate - 45
yy, mm, dd = time.localtime(t)[:3]
k1: int = floor((yy + ((mm - 1) * (1.0 / 12.0)) - 1900) * 12.3685) # 365.25/SYNMONTH
adate = meanphase(adate, k1) # Find new moon well before current date
nt1: float = adate
while True:
adate += SYNMONTH # For each lunar month
k2: int = k1 + 1
nt2: float = meanphase(adate, k2)
if nt1 <= sdate and nt2 > sdate:
break
nt1 = nt2
k1 = k2
# k is integer days since start of 1900, being the lunation number
# 1533, 1534 on both platforms.
for n, k in enumerate((k1, k1, k1, k1, k2)):
phi: int = truephase(k, n % 4) # Args lunation no., phase no. 0..3
self.phases[n] = phi - self.jepoch # Julian datetime to secs since hardware epoch
# Datetimes in secs since hardware epoch based on UTC

Wyświetl plik

@ -1,7 +1,8 @@
{ {
"urls": [ "urls": [
["sched/sun_moon.py", "github:peterhinch/micropython-samples/astronomy/sun_moon.py"], ["sched/sun_moon.py", "github:peterhinch/micropython-samples/astronomy/sun_moon.py"],
["sched/sun_moon_test.py", "github:peterhinch/micropython-samples/astronomy/sun_moon_test.py"] ["sched/sun_moon_test.py", "github:peterhinch/micropython-samples/astronomy/sun_moon_test.py"],
["sched/moonphase.py", "github:peterhinch/micropython-samples/astronomy/moonphase.py"]
], ],
"version": "0.1" "version": "0.1"
} }

Wyświetl plik

@ -12,6 +12,7 @@
# Raul Kompaß perfomed major simplification of the maths for deriving rise and # Raul Kompaß perfomed major simplification of the maths for deriving rise and
# set_times with improvements in precision with 32-bit floats. # set_times with improvements in precision with 32-bit floats.
# Moon phase now in separate module
import time import time
from math import sin, cos, sqrt, fabs, atan, radians, floor, pi from math import sin, cos, sqrt, fabs, atan, radians, floor, pi
@ -103,23 +104,6 @@ def to_int(x):
return None if x is None else round(x) return None if x is None else round(x)
# 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):
fty = year - floor((12.0 - month) / 10.0)
itm = month + 9
if itm >= 12:
itm -= 12
term1 = floor(365.25 * (fty + 4712))
term2 = floor(30.6 * itm + 0.5)
term3 = floor(floor((fty / 100) + 49) * 0.75) - 38
tmp = term1 + term2 + day + 59 + hour / 24.0
if tmp > 2299160.0:
tmp = tmp - term3
phi = (tmp - 2451550.1) / 29.530588853 # 29.530588853 is length of lunar cycle (days)
return phi % 1
def minisun(t): def minisun(t):
# Output sin(dec), cos(dec), ra # Output sin(dec), cos(dec), ra
# returns the ra and dec of the Sun # returns the ra and dec of the Sun
@ -231,7 +215,6 @@ class RiSet:
# time.time() assumes MicroPython clock is set to local time # time.time() assumes MicroPython clock is set to local time
self._t0 = ((round(time.time()) + day * spd) // spd) * spd self._t0 = ((round(time.time()) + day * spd) // spd) * spd
t = time.gmtime(time.time() + day * spd) t = time.gmtime(time.time() + day * spd)
self._phase = moonphase(*t[:4])
self.update(mjd) # Recalculate rise and set times self.update(mjd) # Recalculate rise and set times
return self # Allow r.set_day().sunrise() return self # Allow r.set_day().sunrise()
@ -255,28 +238,27 @@ class RiSet:
def tend(self, variant: int = 0): def tend(self, variant: int = 0):
return self._format(self._times[5], variant) return self._format(self._times[5], variant)
def moonphase(self) -> float:
return self._phase
def set_lto(self, t): # Update the offset from UTC def set_lto(self, t): # Update the offset from UTC
self.check_lto(t) # No need to recalc beause date is unchanged self.check_lto(t) # No need to recalc beause date is unchanged
self.lto = round(t * 3600) # Localtime offset in secs self.lto = round(t * 3600) # Localtime offset in secs
def has_risen(self, sun: bool): def has_risen(self, sun: bool):
now = round(time.time()) + self.lto # UTC now = round(time.time()) # Machine time
rt = self.sunrise(1) if sun else self.moonrise(1) rt = self.sunrise(1) if sun else self.moonrise(1) # Machine time
if rt is None: if rt is None:
now += self.lto # UTC
t = (now % 86400) / 3600 # Time as UTC hour of day (float) t = (now % 86400) / 3600 # Time as UTC hour of day (float)
return self.sin_alt(t, sun) > 0 # Above horizon return self.sin_alt(t, sun) > 0 # Above horizon
return rt < now return rt < now
def has_set(self, sun: bool): def has_set(self, sun: bool):
now = round(time.time()) + self.lto # UTC now = round(time.time())
st = self.sunset(1) if sun else self.moonset(1) st = self.sunset(1) if sun else self.moonset(1)
if st is None: if st is None:
now += self.lto # UTC
t = (now % 86400) / 3600 # Time as UTC hour of day (float) t = (now % 86400) / 3600 # Time as UTC hour of day (float)
return self.sin_alt(t, sun) < 0 return self.sin_alt(t, sun) < 0
return st > now return st < now
def is_up(self, sun: bool): # Return current state of sun or moon def is_up(self, sun: bool): # Return current state of sun or moon
return self.has_risen(sun) and not self.has_set(sun) return self.has_risen(sun) and not self.has_set(sun)