astonomy: Fix bug in is_up method.

master
Peter Hinch 2024-01-03 14:40:05 +00:00
rodzic 6d639f0008
commit 5467922014
3 zmienionych plików z 188 dodań i 70 usunięć

Wyświetl plik

@ -37,8 +37,6 @@ the dates and times of lunar quarters to be calculated.
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 `moonphase` module is currently under development: API changes are possible.
Moon phase options have been removed from `sun_moon` because accuracy was poor. Moon phase options have been removed from `sun_moon` because accuracy was poor.
## 1.1 Applications ## 1.1 Applications
@ -73,9 +71,9 @@ 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
`\lib\sched` on the target. This is for optional use with the `\lib\sched` on the target. This directory eases optional use with the
[schedule module](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/SCHEDULE.md). [schedule module](https://github.com/peterhinch/micropython-async/blob/master/v3/docs/SCHEDULE.md).
This may be done with the official Installation may be done with the official
[mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html): [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html):
```bash ```bash
$ mpremote mip install "github:peterhinch/micropython-samples/astronomy" $ mpremote mip install "github:peterhinch/micropython-samples/astronomy"
@ -87,7 +85,8 @@ On networked platforms it may alternatively be installed with
``` ```
Currently these tools install to `/lib` on the built-in Flash memory. To install Currently these tools install to `/lib` on the built-in Flash memory. To install
to a Pyboard's SD card [rshell](https://github.com/dhylands/rshell) may be used. to a Pyboard's SD card [rshell](https://github.com/dhylands/rshell) may be used.
Move to `micropython-samples` on the PC, run `rshell` and issue: Clone the repo and move to `micropython-samples` on the PC, run `rshell` and
issue:
``` ```
> rsync astronomy /sd/sched > rsync astronomy /sd/sched
``` ```
@ -104,13 +103,14 @@ from sched.sun_moon import RiSet
Time is a slippery concept when applied to global locations. This document uses Time is a slippery concept when applied to global locations. This document uses
the following conventions: the following conventions:
* `UTC` The international time standard based on the Greenwich meridian. * `UTC` The international time standard based on the Greenwich meridian.
* `LT (Local time)` Time as told on a clock at the device's location. May include * `LT (Local time)` Time on a clock at the device's location. May include
daylight saving time (`DST`). daylight saving time (`DST`).
* `MT (Machine time)` Time defined by the platform's hardware clock. * `MT (Machine time)` Time defined by the platform's hardware clock.
* `LTO (Local time offset)` A `RiSet` instance contains a user supplied `LTO`. * `LTO (Local time offset)` A `RiSet` instance contains a user supplied `LTO`
The class computes rise and set times in UTC, using `LTO` to output results in intended for timezone support. The class computes rise and set times in UTC,
`LT` via `LT = UTC + LTO`. If an application maintains `LTO` to match `DST`, the using `LTO` to compute results using `RESULT = UTC + LTO`. For output in `LT`
rise and set times will be in `LT`. there are two options: periodically adjust `LTO` to handle DST or (better)
provide a `dst` function so that conversion is automatic.
# 2. The RiSet class # 2. The RiSet class
@ -140,10 +140,16 @@ time (`MT`).
(6 is Civil, 12 is Nautical, 18 is Astronomical). By default twilight times are (6 is Civil, 12 is Nautical, 18 is Astronomical). By default twilight times are
not computed, saving some processor time. Offsets are positive numbers not computed, saving some processor time. Offsets are positive numbers
representing degrees below the horizon where twilight is deemed to start and end. representing degrees below the horizon where twilight is deemed to start and end.
* `dst=lambda x: x` This is an optional user defined function for Daylight
Saving Time (DST). The assumption is that machine time is not changed, typically
permanently in winter time. A `dst` function handles seasonal changes. The
default assumes no DST is applicable. For how to write a DST function for a
given country see [section 6.4](./README.md#64-dst).
By default when an application instantiates `RiSet` for the first time the By default when an application instantiates `RiSet` for the first time the
constructor prints the system date and time. This can be inhibited by setting constructor prints the system date and time. This can be inhibited by setting
the class variable `verbose` to `False`. the class variable `verbose` to `False`. The purpose is to alert the user to a
common source of error where machine time is not set.
## 2.2 Methods ## 2.2 Methods
@ -162,8 +168,9 @@ 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.
* `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 system longitude. The value is checked to ensure intended for timezone support, but this function can be used to support DST. The
`-15.0 < lto < 15.0`. See [section 2.3](./README.md#23-effect-of-local-time). value is checked to ensure `-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
@ -173,7 +180,8 @@ with the moon at most locations, and with the sun in polar regions.
Variants: Variants:
* 0 Return integer seconds since midnight `LT` (or `None` if no event). * 0 Return integer seconds since midnight `LT` (or `None` if no event).
* 1 Return integer seconds since since epoch of the MicroPython platform * 1 Return integer seconds since since epoch of the MicroPython platform
(or `None`). This is machine time (`MT`) as per `time.time()`. (or `None`). This allows comparisons with machine time (`MT`) as per
`time.time()`.
* 2 Return text of form hh:mm:ss (or --:--:--) being local time (`LT`). * 2 Return text of form hh:mm:ss (or --:--:--) being local time (`LT`).
Example constructor invocations: Example constructor invocations:
@ -184,13 +192,16 @@ r = RiSet(lat=-33.87667, long=151.21, lto=11) # Sydney 33°5204″S 151°12
``` ```
## 2.3 Effect of local time ## 2.3 Effect of local time
MicroPython has no concept of local time. The hardware platform has a clock MicroPython has no concept of timezones. The hardware platform has a clock
which reports machine time (`MT`): this might be set to local winter time or which reports machine time (`MT`): this might be set to local winter time or
summer time. The `RiSet` instances' `LTO` should be set to represent the summer time. The `RiSet` instances' `LTO` should be set to represent the
difference between `MT` and `UTC`. In continuously running applications it is difference between `MT` and `UTC`. In continuously running applications it is
best to avoid changing the hardware clock (`MT`) for reasons discussed below. best to avoid changing the hardware clock (`MT`) for reasons discussed below.
Daylight savings time should be implemented by changing the `RiSet` instances' Daylight savings time may be implemented in one of two ways:
`LTO`. * By changing the `RiSet` instances' `LTO` accordingly.
* Or by providing a `dst` function as discussed in
[section 6.4](./README.md#64-dst). This is the preferred solution as DST is then
handled automatically.
Rise and set times are computed relative to UTC and then adjusted using the Rise and set times are computed relative to UTC and then adjusted using the
`RiSet` instance's `LTO` before being returned (see `.adjust()`). This means `RiSet` instance's `LTO` before being returned (see `.adjust()`). This means
@ -199,7 +210,7 @@ is used in determining rise and set times.
The `.has_risen()`, `.has_set()` and `.is_up()` methods do use machine time The `.has_risen()`, `.has_set()` and `.is_up()` methods do use machine time
(`MT`) and rely on `MT == UTC + LTO`: if `MT` has drifted, precision will be (`MT`) and rely on `MT == UTC + LTO`: if `MT` has drifted, precision will be
reduced. lost at times close to rise and set events.
The constructor and the `set_day()` method set the instance's date relative to The constructor and the `set_day()` method set the instance's date relative to
`MT`. They use only the date component of `MT`, hence they may be run at any `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
jumps. jumps.
For this reason changing system time to accommodate daylight saving time is a For this reason changing system time to accommodate daylight saving time is a
bad idea. It is usually best to run winter time all year round. Where a DST bad idea. It is usually best to run winter time all year round and to use the
change occurs, the `RiSet.set_lto()` method should be run to ensure that `RiSet` `dst` constructor arg to handle time changes.
operates in current local time.
# 3. Utility functions # 3. 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
(1970,1,1) and returns a number of days relative to the current date. Platform
independent. This facilitates testing with pre-determined target dates.
# 4. Demo script # 4. Demo script
@ -288,6 +295,10 @@ 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`.
The script includes some commented out code at the end. This tests `is_up`,
`has_risen` and `has_set` over 365 days. It is commented out to reduce printed
output.
# 5. 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.
@ -436,7 +447,8 @@ to produce a time-precise value. The five quarters are calculated for the
lunation including the midnight at the start of the specified day. 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 * `set_lto(t:float)` Redefine the local time offset, `t` being in hours as
per the constructor arg. per the constructor arg.
* `datum(text: bool = True)` Returns the current datum. * `datum(text: bool = True)` Returns the current datum in secs since local epoch
or in human-readable text form.
## 6.3 Usage examples ## 6.3 Usage examples
@ -506,4 +518,6 @@ than 3s.
## 7.2 MoonPhase class ## 7.2 MoonPhase class
TODO This uses Python's arbitrary precision integers to overcome the limitations of
32-bit floating point units. Results on 32 bit platforms match those on 64-bits
to within ~1 minute. Results match those on `timeanddate.com` within ~3 minutes.

Wyświetl plik

@ -33,22 +33,11 @@ LONG = -2.102811634540558
# right number of days for platform epoch at UTC. # right number of days for platform epoch at UTC.
def now_days() -> int: def now_days() -> int:
secs_per_day = 86400 # 24 * 3600 secs_per_day = 86400 # 24 * 3600
t = time.time() t = RiSet.mtime() # Machine time as int. Can be overridden for test.
t -= t % secs_per_day # Previous Midnight t -= t % secs_per_day # Previous Midnight
return round(t / secs_per_day) # Days since datum return round(t / secs_per_day) # Days since datum
# Convert number of days relative to the Unix epoch (1970,1,1) to a number of
# days relative to the current date. e.g. 19695 = 4th Dec 2023
# Platform independent.
def abs_to_rel_days(days: int) -> int:
secs_per_day = 86400 # 24 * 3600
now = now_days() # Days since platform epoch
if time.gmtime(0)[0] == 2000: # Machine epoch
now += 10957
return days - now
def quad(ym, yz, yp): def quad(ym, yz, yp):
# See Astronomy on the PC P48-49, plus contribution from Marcus Mendenhall # 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)
@ -186,16 +175,30 @@ def minimoon(t):
class RiSet: class RiSet:
verbose = True verbose = True
# Riset.mtime() returns machine time as an int. The class variable tim is for
# test purposes only and allows the hardware clock to be overridden
tim = None
def __init__(self, lat=LAT, long=LONG, lto=0, tl=None): # Local defaults @classmethod
def mtime(cls):
return round(time.time()) if cls.tim is None else cls.tim
@classmethod
def set_time(cls, t): # Given time from Unix epoch set time
if time.gmtime(0)[0] == 2000: # Machine epoch
t -= 10957 * 86400
cls.tim = t
def __init__(self, lat=LAT, long=LONG, lto=0, tl=None, dst=lambda x: x): # 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.check_lto(lto) # -15 < lto < 15 self.check_lto(lto) # -15 < lto < 15
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.tlight = sin(radians(tl)) if tl is not None else tl
self.dst = dst
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 machine time adjusted for DST)
# [sunrise, sunset, moonrise, moonset, cvend, cvstart] # [sunrise, sunset, moonrise, moonset, cvend, cvstart]
self._times = [None] * 6 self._times = [None] * 6
self.set_day() # Initialise to today's date self.set_day() # Initialise to today's date
@ -212,9 +215,8 @@ class RiSet:
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
# ._t0 is time of midnight (local time) in secs since MicroPython epoch # ._t0 is time of midnight (local time) in secs since MicroPython epoch
# time.time() assumes MicroPython clock is set to local time # time.time() assumes MicroPython clock is set to geographic local time
self._t0 = ((round(time.time()) + day * spd) // spd) * spd self._t0 = ((self.mtime() + day * spd) // spd) * spd
t = time.gmtime(time.time() + day * spd)
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()
@ -243,30 +245,55 @@ class RiSet:
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()) # Machine time return self.has_x(True, sun)
rt = self.sunrise(1) if sun else self.moonrise(1) # Machine time
if rt is None:
now += self.lto # UTC
t = (now % 86400) / 3600 # Time as UTC hour of day (float)
return self.sin_alt(t, sun) > 0 # Above horizon
return rt < now
def has_set(self, sun: bool): def has_set(self, sun: bool):
now = round(time.time()) return self.has_x(False, sun)
st = self.sunset(1) if sun else self.moonset(1)
if st is None:
now += self.lto # UTC
t = (now % 86400) / 3600 # Time as UTC hour of day (float)
return self.sin_alt(t, sun) < 0
return st < now
def is_up(self, sun: bool): # Return current state of sun or moon # Return current state of sun or moon. The moon has a special case where it
return self.has_risen(sun) and not self.has_set(sun) # rises and sets in a 24 hour period. If its state is queried after both these
# events or before either has occurred, the current state depends on the order
# in which they occurred (the sun always sets afer it rises).
# The case is (.has_risen(False) and .has_set(False)) and if it occurs then
# .moonrise() and .moonset() must return valid times (not None).
def is_up(self, sun: bool):
hr = self.has_risen(sun)
hs = self.has_set(sun)
rt = self.sunrise() if sun else self.moonrise()
st = self.sunset() if sun else self.moonset()
if rt is None and st is None: # No event in 24hr period.
return self.above_horizon(sun)
# Handle special case: moon has already risen and set or moon has neither
# risen nor set, yet there is a rise and set event in the day
if not (hr ^ hs):
if not ((rt is None) or (st is None)):
return rt > st
if not (hr or hs): # No event has yet occurred
return rt is None
return hr and not hs # Default case: up if it's risen but not set
# ***** API end ***** # ***** API end *****
# Generic has_risen/has_set function
def has_x(self, risen: bool, sun: bool):
if risen:
st = self.sunrise(1) if sun else self.moonrise(1) # DST- adjusted machine time
else:
st = self.sunset(1) if sun else self.moonset(1)
if st is not None:
return st < self.dst(self.mtime()) # Machine time
return False
def above_horizon(self, sun: bool):
now = self.mtime() + self.lto # UTC
tutc = (now % 86400) / 3600 # Time as UTC hour of day (float)
return self.sin_alt(tutc, sun) > 0 # Object is above horizon
# Re-calculate rise and set times # Re-calculate rise and set times
def update(self, mjd): def update(self, mjd):
self._times = [None] * 6 for x in range(len(self._times)):
self._times[x] = None # Assume failure
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 tr = None # Assume no twilight calculations
ts = None ts = None
@ -277,8 +304,8 @@ class RiSet:
if self.tlight is not None: if self.tlight is not None:
tr, ts = self.rise_set(True, True) tr, ts = self.rise_set(True, True)
mr, ms = self.rise_set(False, False) # Moon 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 and DST. Store in ._times if value is in
# local time window # 24-hour local time window
self.adjust((sr, ss, mr, ms, tr, ts), day) self.adjust((sr, ss, mr, ms, tr, ts), day)
self.mjd = mjd self.mjd = mjd
@ -286,6 +313,7 @@ class RiSet:
for idx, n in enumerate(times): for idx, n in enumerate(times):
if n is not None: if n is not None:
n += self.lto + (day - 1) * 86400 n += self.lto + (day - 1) * 86400
n = self.dst(n) # Adjust for DST on day of n
h = n // 3600 h = n // 3600
if 0 <= h < 24: if 0 <= h < 24:
self._times[idx] = n self._times[idx] = n
@ -332,7 +360,6 @@ class RiSet:
tl = self.lstt(t, hour) + self.long # Local mean sidereal time adjusted for logitude tl = self.lstt(t, hour) + self.long # Local mean sidereal time adjusted for logitude
return self.sglat * z + self.cglat * (x * cos(radians(tl)) + y * sin(radians(tl))) return self.sglat * z + self.cglat * (x * cos(radians(tl)) + y * sin(radians(tl)))
# 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, tl): def rise_set(self, sun, tl):

Wyświetl plik

@ -9,9 +9,19 @@
# import sun_moon_test # import sun_moon_test
try: try:
from .sun_moon import RiSet, abs_to_rel_days from .sun_moon import RiSet
except ImportError: # Running on PC in astronomy directory except ImportError: # Running on PC in astronomy directory
from sun_moon import RiSet, abs_to_rel_days from sun_moon import RiSet
import time
def mtime(h, m, t=None):
if t is None:
t = round(time.time())
tm = (t // 86400) * 86400 + h * 3600 + m * 60
print(time.gmtime(tm))
return tm
nresults = [] # Times in seconds from local midnight nresults = [] # Times in seconds from local midnight
@ -20,27 +30,40 @@ def show(rs):
print(f"Sun rise {rs.sunrise(3)} set {rs.sunset(3)}") print(f"Sun rise {rs.sunrise(3)} set {rs.sunset(3)}")
print(f"Moon rise {rs.moonrise(3)} set {rs.moonset(3)}") print(f"Moon rise {rs.moonrise(3)} set {rs.moonset(3)}")
nresults.extend([rs.sunrise(), rs.sunset(), rs.moonrise(), rs.moonset()]) nresults.extend([rs.sunrise(), rs.sunset(), rs.moonrise(), rs.moonset()])
print()
print("4th Dec 2023: Seattle UTC-8") print("4th Dec 2023: Seattle UTC-8")
rs = RiSet(lat=47.61, long=-122.35, lto=-8) # Seattle 47°3635″N 122°1959″W rs = RiSet(lat=47.61, long=-122.35, lto=-8) # Seattle 47°3635″N 122°1959″W
rs.set_day(abs_to_rel_days(19695)) # 4th Dec 2023 RiSet.set_time(19695 * 86400)
rs.set_day()
show(rs) show(rs)
print()
print("4th Dec 2023: Sydney UTC+11") print("4th Dec 2023: Sydney UTC+11")
rs = RiSet(lat=-33.86, long=151.21, lto=11) # Sydney 33°5204″S 151°1236″E rs = RiSet(lat=-33.86, long=151.21, lto=11) # Sydney 33°5204″S 151°1236″E
rs.set_day(abs_to_rel_days(19695)) # 4th Dec 2023 RiSet.set_time(19695 * 86400)
rs.set_day()
show(rs) show(rs)
print()
print("From 4th Dec 2023: UK, UTC") print("From 4th Dec 2023: UK, UTC")
rs = RiSet() rs = RiSet()
for day in range(7): for day in range(7):
rs.set_day(abs_to_rel_days(19695 + day)) # Start 4th Dec 2023 RiSet.set_time(19695 * 86400)
rs.set_day(day)
# rs.set_day(abs_to_rel_days(19695 + day)) # Start 4th Dec 2023
print(f"Day: {day}") print(f"Day: {day}")
show(rs) show(rs)
print("4th Dec 2023: Sydney UTC+11 - test DST")
# Sydney 33°5204″S 151°1236″E
rs = RiSet(lat=-33.86, long=151.21, lto=11, dst=lambda x: x + 3600)
RiSet.set_time(19695 * 86400 + 86400 / 2)
rs.set_day()
# rs.set_day(abs_to_rel_days(19695)) # 4th Dec 2023
show(rs)
# Expected results as computed on Unix build (64-bit FPU) # Expected results as computed on Unix build (64-bit FPU)
exp = [ exp = [
27628, 27628,
@ -79,6 +102,10 @@ exp = [
57019, 57019,
19082, 19082,
50384, 50384,
20212 + 3600,
71598 + 3600,
2747 + 3600,
41257 + 3600,
] ]
print() print()
max_error = 0 max_error = 0
@ -88,6 +115,7 @@ for act, requirement in zip(nresults, exp):
max_error = max(max_error, err) max_error = max(max_error, err)
if err > 30: if err > 30:
print(f"Error {requirement - act}") print(f"Error {requirement - act}")
print(f"Maximum error {max_error}. Expect 0 on 64-bit platform, 30s on 32-bit") print(f"Maximum error {max_error}. Expect 0 on 64-bit platform, 30s on 32-bit")
# Times from timeanddate.com # Times from timeanddate.com
@ -97,3 +125,52 @@ print(f"Maximum error {max_error}. Expect 0 on 64-bit platform, 30s on 32-bit")
# Sunrise 5:37 sunset 19:53 Moonrise 00:45 Moonset 11:28 # Sunrise 5:37 sunset 19:53 Moonrise 00:45 Moonset 11:28
# UK # UK
# Sunrise 8:04 sunset 15:52 Moonrise 23:02 Moonset 13:01 # Sunrise 8:04 sunset 15:52 Moonrise 23:02 Moonset 13:01
def testup(t): # Time in secs since machine epoch
t = round((t // 86400) * 86400 + 60) # 1 minute past midnight
rs = RiSet()
RiSet.set_time(t)
rs.set_day()
tr = rs.moonrise()
ts = rs.moonset()
print(f"testup rise {rs.moonrise(2)} set {rs.moonset(2)}")
if tr is None and ts is None:
print(time.gmtime(t), "No moon events")
print(
f"Is up {rs.is_up(False)} Has risen {rs.has_risen(False)} has set {rs.has_set(False)}"
)
return
# Initial state: not risen or set
assert not rs.has_set(False)
assert not rs.has_risen(False)
if tr is not None and (ts is None or ts > tr):
assert not rs.is_up(False)
rs.set_time(t + tr)
assert rs.has_risen(False)
assert rs.is_up(False)
assert not rs.has_set(False)
if ts is not None:
rs.set_time(t + ts)
assert rs.has_risen(False)
assert not rs.is_up(False)
assert rs.has_set(False)
return
if ts is not None:
assert rs.is_up(False)
rs.set_time(t + ts)
assert not rs.has_risen(False)
assert not rs.is_up(False)
assert rs.has_set(False)
if tr is not None:
rs.set_time(t + tr)
assert rs.has_risen(False)
assert rs.is_up(False)
assert rs.has_set(False)
return
print(f"Untested state tr {tr} ts {ts}")
# t = time.time()
# for d in range(365):
# testup(t + d * 86400)