Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5467922

Browse files
committed
astonomy: Fix bug in is_up method.
1 parent 6d639f0 commit 5467922

File tree

3 files changed

+189
-71
lines changed

3 files changed

+189
-71
lines changed

astronomy/README.md

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ the dates and times of lunar quarters to be calculated.
3737
Caveat. I am not an astronomer. If there are errors in the fundamental
3838
algorithms I am unlikely to be able to offer an opinion, still less a fix.
3939

40-
The `moonphase` module is currently under development: API changes are possible.
41-
4240
Moon phase options have been removed from `sun_moon` because accuracy was poor.
4341

4442
## 1.1 Applications
@@ -73,9 +71,9 @@ licence.
7371
## 1.3 Installation
7472

7573
Installation copies files from the `astronomy` directory to a directory
76-
`\lib\sched` on the target. This is for optional use with the
74+
`\lib\sched` on the target. This directory eases optional use with the
7775
[schedule module](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/SCHEDULE.md).
78-
This may be done with the official
76+
Installation may be done with the official
7977
[mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html):
8078
```bash
8179
$ mpremote mip install "github:peterhinch/micropython-samples/astronomy"
@@ -87,7 +85,8 @@ On networked platforms it may alternatively be installed with
8785
```
8886
Currently these tools install to `/lib` on the built-in Flash memory. To install
8987
to a Pyboard's SD card [rshell](https://github.com/dhylands/rshell) may be used.
90-
Move to `micropython-samples` on the PC, run `rshell` and issue:
88+
Clone the repo and move to `micropython-samples` on the PC, run `rshell` and
89+
issue:
9190
```
9291
> rsync astronomy /sd/sched
9392
```
@@ -104,13 +103,14 @@ from sched.sun_moon import RiSet
104103
Time is a slippery concept when applied to global locations. This document uses
105104
the following conventions:
106105
* `UTC` The international time standard based on the Greenwich meridian.
107-
* `LT (Local time)` Time as told on a clock at the device's location. May include
106+
* `LT (Local time)` Time on a clock at the device's location. May include
108107
daylight saving time (`DST`).
109108
* `MT (Machine time)` Time defined by the platform's hardware clock.
110-
* `LTO (Local time offset)` A `RiSet` instance contains a user supplied `LTO`.
111-
The class computes rise and set times in UTC, using `LTO` to output results in
112-
`LT` via `LT = UTC + LTO`. If an application maintains `LTO` to match `DST`, the
113-
rise and set times will be in `LT`.
109+
* `LTO (Local time offset)` A `RiSet` instance contains a user supplied `LTO`
110+
intended for timezone support. The class computes rise and set times in UTC,
111+
using `LTO` to compute results using `RESULT = UTC + LTO`. For output in `LT`
112+
there are two options: periodically adjust `LTO` to handle DST or (better)
113+
provide a `dst` function so that conversion is automatic.
114114

115115
# 2. The RiSet class
116116

@@ -140,10 +140,16 @@ time (`MT`).
140140
(6 is Civil, 12 is Nautical, 18 is Astronomical). By default twilight times are
141141
not computed, saving some processor time. Offsets are positive numbers
142142
representing degrees below the horizon where twilight is deemed to start and end.
143+
* `dst=lambda x: x` This is an optional user defined function for Daylight
144+
Saving Time (DST). The assumption is that machine time is not changed, typically
145+
permanently in winter time. A `dst` function handles seasonal changes. The
146+
default assumes no DST is applicable. For how to write a DST function for a
147+
given country see [section 6.4](./README.md#64-dst).
143148

144149
By default when an application instantiates `RiSet` for the first time the
145150
constructor prints the system date and time. This can be inhibited by setting
146-
the class variable `verbose` to `False`.
151+
the class variable `verbose` to `False`. The purpose is to alert the user to a
152+
common source of error where machine time is not set.
147153

148154
## 2.2 Methods
149155

@@ -162,8 +168,9 @@ horizon.
162168
* `has_risen(sun: bool)->bool` Returns `True` if the selected object has risen.
163169
* `has_set(sun: bool)->bool` Returns `True` if the selected object has set.
164170
* `set_lto(t)` Set local time offset `LTO` in hours relative to UTC. Primarily
165-
intended for system longitude. The value is checked to ensure
166-
`-15.0 < lto < 15.0`. See [section 2.3](./README.md#23-effect-of-local-time).
171+
intended for timezone support, but this function can be used to support DST. The
172+
value is checked to ensure `-15.0 < lto < 15.0`. See
173+
[section 2.3](./README.md#23-effect-of-local-time).
167174

168175
The return value of the rise and set method is determined by the `variant` arg.
169176
In all cases rise and set events are identified which occur in the current 24
@@ -173,7 +180,8 @@ with the moon at most locations, and with the sun in polar regions.
173180
Variants:
174181
* 0 Return integer seconds since midnight `LT` (or `None` if no event).
175182
* 1 Return integer seconds since since epoch of the MicroPython platform
176-
(or `None`). This is machine time (`MT`) as per `time.time()`.
183+
(or `None`). This allows comparisons with machine time (`MT`) as per
184+
`time.time()`.
177185
* 2 Return text of form hh:mm:ss (or --:--:--) being local time (`LT`).
178186

179187
Example constructor invocations:
@@ -184,13 +192,16 @@ r = RiSet(lat=-33.87667, long=151.21, lto=11) # Sydney 33°52′04″S 151°12
184192
```
185193
## 2.3 Effect of local time
186194

187-
MicroPython has no concept of local time. The hardware platform has a clock
195+
MicroPython has no concept of timezones. The hardware platform has a clock
188196
which reports machine time (`MT`): this might be set to local winter time or
189197
summer time. The `RiSet` instances' `LTO` should be set to represent the
190198
difference between `MT` and `UTC`. In continuously running applications it is
191199
best to avoid changing the hardware clock (`MT`) for reasons discussed below.
192-
Daylight savings time should be implemented by changing the `RiSet` instances'
193-
`LTO`.
200+
Daylight savings time may be implemented in one of two ways:
201+
* By changing the `RiSet` instances' `LTO` accordingly.
202+
* Or by providing a `dst` function as discussed in
203+
[section 6.4](./README.md#64-dst). This is the preferred solution as DST is then
204+
handled automatically.
194205

195206
Rise and set times are computed relative to UTC and then adjusted using the
196207
`RiSet` instance's `LTO` before being returned (see `.adjust()`). This means
@@ -199,7 +210,7 @@ is used in determining rise and set times.
199210

200211
The `.has_risen()`, `.has_set()` and `.is_up()` methods do use machine time
201212
(`MT`) and rely on `MT == UTC + LTO`: if `MT` has drifted, precision will be
202-
reduced.
213+
lost at times close to rise and set events.
203214

204215
The constructor and the `set_day()` method set the instance's date relative to
205216
`MT`. They use only the date component of `MT`, hence they may be run at any
@@ -231,16 +242,12 @@ synchronisation is required it is best done frequently to minimise the size of
231242
jumps.
232243

233244
For this reason changing system time to accommodate daylight saving time is a
234-
bad idea. It is usually best to run winter time all year round. Where a DST
235-
change occurs, the `RiSet.set_lto()` method should be run to ensure that `RiSet`
236-
operates in current local time.
245+
bad idea. It is usually best to run winter time all year round and to use the
246+
`dst` constructor arg to handle time changes.
237247

238248
# 3. Utility functions
239249

240250
`now_days() -> int` Returns the current time as days since the platform epoch.
241-
`abs_to_rel_days(days: int) -> int` Takes a number of days since the Unix epoch
242-
(1970,1,1) and returns a number of days relative to the current date. Platform
243-
independent. This facilitates testing with pre-determined target dates.
244251

245252
# 4. Demo script
246253

@@ -288,6 +295,10 @@ Maximum error 0. Expect 0 on 64-bit platform, 30s on 32-bit
288295
```
289296
Code comments show times retrieved from `timeanddate.com`.
290297

298+
The script includes some commented out code at the end. This tests `is_up`,
299+
`has_risen` and `has_set` over 365 days. It is commented out to reduce printed
300+
output.
301+
291302
# 5. Scheduling events
292303

293304
A likely use case is to enable events to be timed relative to sunrise and set.
@@ -436,7 +447,8 @@ to produce a time-precise value. The five quarters are calculated for the
436447
lunation including the midnight at the start of the specified day.
437448
* `set_lto(t:float)` Redefine the local time offset, `t` being in hours as
438449
per the constructor arg.
439-
* `datum(text: bool = True)` Returns the current datum.
450+
* `datum(text: bool = True)` Returns the current datum in secs since local epoch
451+
or in human-readable text form.
440452

441453
## 6.3 Usage examples
442454

@@ -506,4 +518,6 @@ than 3s.
506518

507519
## 7.2 MoonPhase class
508520

509-
TODO
521+
This uses Python's arbitrary precision integers to overcome the limitations of
522+
32-bit floating point units. Results on 32 bit platforms match those on 64-bits
523+
to within ~1 minute. Results match those on `timeanddate.com` within ~3 minutes.

astronomy/sun_moon.py

Lines changed: 65 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,11 @@
3333
# right number of days for platform epoch at UTC.
3434
def now_days() -> int:
3535
secs_per_day = 86400 # 24 * 3600
36-
t = time.time()
36+
t = RiSet.mtime() # Machine time as int. Can be overridden for test.
3737
t -= t % secs_per_day # Previous Midnight
3838
return round(t / secs_per_day) # Days since datum
3939

4040

41-
# Convert number of days relative to the Unix epoch (1970,1,1) to a number of
42-
# days relative to the current date. e.g. 19695 = 4th Dec 2023
43-
# Platform independent.
44-
def abs_to_rel_days(days: int) -> int:
45-
secs_per_day = 86400 # 24 * 3600
46-
now = now_days() # Days since platform epoch
47-
if time.gmtime(0)[0] == 2000: # Machine epoch
48-
now += 10957
49-
return days - now
50-
51-
5241
def quad(ym, yz, yp):
5342
# See Astronomy on the PC P48-49, plus contribution from Marcus Mendenhall
5443
# finds the parabola throuh the three points (-1,ym), (0,yz), (1, yp)
@@ -186,16 +175,30 @@ def minimoon(t):
186175

187176
class RiSet:
188177
verbose = True
178+
# Riset.mtime() returns machine time as an int. The class variable tim is for
179+
# test purposes only and allows the hardware clock to be overridden
180+
tim = None
189181

190-
def __init__(self, lat=LAT, long=LONG, lto=0, tl=None): # Local defaults
182+
@classmethod
183+
def mtime(cls):
184+
return round(time.time()) if cls.tim is None else cls.tim
185+
186+
@classmethod
187+
def set_time(cls, t): # Given time from Unix epoch set time
188+
if time.gmtime(0)[0] == 2000: # Machine epoch
189+
t -= 10957 * 86400
190+
cls.tim = t
191+
192+
def __init__(self, lat=LAT, long=LONG, lto=0, tl=None, dst=lambda x: x): # Local defaults
191193
self.sglat = sin(radians(lat))
192194
self.cglat = cos(radians(lat))
193195
self.long = long
194196
self.check_lto(lto) # -15 < lto < 15
195197
self.lto = round(lto * 3600) # Localtime offset in secs
196198
self.tlight = sin(radians(tl)) if tl is not None else tl
199+
self.dst = dst
197200
self.mjd = None # Current integer MJD
198-
# Times in integer secs from midnight on current day (in local time)
201+
# Times in integer secs from midnight on current day (in machine time adjusted for DST)
199202
# [sunrise, sunset, moonrise, moonset, cvend, cvstart]
200203
self._times = [None] * 6
201204
self.set_day() # Initialise to today's date
@@ -212,9 +215,8 @@ def set_day(self, day: int = 0):
212215
if self.mjd is None or self.mjd != mjd:
213216
spd = 86400 # Secs per day
214217
# ._t0 is time of midnight (local time) in secs since MicroPython epoch
215-
# time.time() assumes MicroPython clock is set to local time
216-
self._t0 = ((round(time.time()) + day * spd) // spd) * spd
217-
t = time.gmtime(time.time() + day * spd)
218+
# time.time() assumes MicroPython clock is set to geographic local time
219+
self._t0 = ((self.mtime() + day * spd) // spd) * spd
218220
self.update(mjd) # Recalculate rise and set times
219221
return self # Allow r.set_day().sunrise()
220222

@@ -243,30 +245,55 @@ def set_lto(self, t): # Update the offset from UTC
243245
self.lto = round(t * 3600) # Localtime offset in secs
244246

245247
def has_risen(self, sun: bool):
246-
now = round(time.time()) # Machine time
247-
rt = self.sunrise(1) if sun else self.moonrise(1) # Machine time
248-
if rt is None:
249-
now += self.lto # UTC
250-
t = (now % 86400) / 3600 # Time as UTC hour of day (float)
251-
return self.sin_alt(t, sun) > 0 # Above horizon
252-
return rt < now
248+
return self.has_x(True, sun)
253249

254250
def has_set(self, sun: bool):
255-
now = round(time.time())
256-
st = self.sunset(1) if sun else self.moonset(1)
257-
if st is None:
258-
now += self.lto # UTC
259-
t = (now % 86400) / 3600 # Time as UTC hour of day (float)
260-
return self.sin_alt(t, sun) < 0
261-
return st < now
262-
263-
def is_up(self, sun: bool): # Return current state of sun or moon
264-
return self.has_risen(sun) and not self.has_set(sun)
251+
return self.has_x(False, sun)
252+
253+
# Return current state of sun or moon. The moon has a special case where it
254+
# rises and sets in a 24 hour period. If its state is queried after both these
255+
# events or before either has occurred, the current state depends on the order
256+
# in which they occurred (the sun always sets afer it rises).
257+
# The case is (.has_risen(False) and .has_set(False)) and if it occurs then
258+
# .moonrise() and .moonset() must return valid times (not None).
259+
def is_up(self, sun: bool):
260+
hr = self.has_risen(sun)
261+
hs = self.has_set(sun)
262+
rt = self.sunrise() if sun else self.moonrise()
263+
st = self.sunset() if sun else self.moonset()
264+
if rt is None and st is None: # No event in 24hr period.
265+
return self.above_horizon(sun)
266+
# Handle special case: moon has already risen and set or moon has neither
267+
# risen nor set, yet there is a rise and set event in the day
268+
if not (hr ^ hs):
269+
if not ((rt is None) or (st is None)):
270+
return rt > st
271+
if not (hr or hs): # No event has yet occurred
272+
return rt is None
273+
274+
return hr and not hs # Default case: up if it's risen but not set
265275

266276
# ***** API end *****
277+
278+
# Generic has_risen/has_set function
279+
def has_x(self, risen: bool, sun: bool):
280+
if risen:
281+
st = self.sunrise(1) if sun else self.moonrise(1) # DST- adjusted machine time
282+
else:
283+
st = self.sunset(1) if sun else self.moonset(1)
284+
if st is not None:
285+
return st < self.dst(self.mtime()) # Machine time
286+
return False
287+
288+
def above_horizon(self, sun: bool):
289+
now = self.mtime() + self.lto # UTC
290+
tutc = (now % 86400) / 3600 # Time as UTC hour of day (float)
291+
return self.sin_alt(tutc, sun) > 0 # Object is above horizon
292+
267293
# Re-calculate rise and set times
268294
def update(self, mjd):
269-
self._times = [None] * 6
295+
for x in range(len(self._times)):
296+
self._times[x] = None # Assume failure
270297
days = (1, 2) if self.lto < 0 else (1,) if self.lto == 0 else (0, 1)
271298
tr = None # Assume no twilight calculations
272299
ts = None
@@ -277,15 +304,16 @@ def update(self, mjd):
277304
if self.tlight is not None:
278305
tr, ts = self.rise_set(True, True)
279306
mr, ms = self.rise_set(False, False) # Moon
280-
# Adjust for local time. Store in ._times if value is in 24-hour
281-
# local time window
307+
# Adjust for local time and DST. Store in ._times if value is in
308+
# 24-hour local time window
282309
self.adjust((sr, ss, mr, ms, tr, ts), day)
283310
self.mjd = mjd
284311

285312
def adjust(self, times, day):
286313
for idx, n in enumerate(times):
287314
if n is not None:
288315
n += self.lto + (day - 1) * 86400
316+
n = self.dst(n) # Adjust for DST on day of n
289317
h = n // 3600
290318
if 0 <= h < 24:
291319
self._times[idx] = n
@@ -332,7 +360,6 @@ def sin_alt(self, hour, sun):
332360
tl = self.lstt(t, hour) + self.long # Local mean sidereal time adjusted for logitude
333361
return self.sglat * z + self.cglat * (x * cos(radians(tl)) + y * sin(radians(tl)))
334362

335-
# Modified to find sunrise and sunset only, not twilight events.
336363
# Calculate rise and set times of sun or moon for the current MJD. Times are
337364
# relative to that 24 hour period.
338365
def rise_set(self, sun, tl):

0 commit comments

Comments
 (0)