wwvb 3.0.7__py3-none-any.whl → 4.0.0a0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uwwvb.py +5 -2
- wwvb/__init__.py +212 -179
- wwvb/__version__.py +2 -2
- wwvb/decode.py +8 -9
- wwvb/gen.py +14 -16
- wwvb/iersdata.py +4 -5
- wwvb/iersdata_dist.py +4 -6
- wwvb/testcli.py +8 -5
- wwvb/testpm.py +2 -2
- wwvb/testuwwvb.py +42 -31
- wwvb/testwwvb.py +51 -40
- wwvb/updateiers.py +35 -33
- wwvb/wwvbtk.py +13 -13
- {wwvb-3.0.7.dist-info → wwvb-4.0.0a0.dist-info}/METADATA +1 -1
- wwvb-4.0.0a0.dist-info/RECORD +23 -0
- {wwvb-3.0.7.dist-info → wwvb-4.0.0a0.dist-info}/WHEEL +1 -1
- wwvb-3.0.7.dist-info/RECORD +0 -23
- {wwvb-3.0.7.dist-info → wwvb-4.0.0a0.dist-info}/entry_points.txt +0 -0
- {wwvb-3.0.7.dist-info → wwvb-4.0.0a0.dist-info}/top_level.txt +0 -0
wwvb/__init__.py
CHANGED
@@ -6,25 +6,24 @@
|
|
6
6
|
#
|
7
7
|
# SPDX-License-Identifier: GPL-3.0-only
|
8
8
|
|
9
|
-
import
|
9
|
+
from __future__ import annotations
|
10
|
+
|
10
11
|
import datetime
|
11
12
|
import enum
|
12
13
|
import json
|
13
14
|
import warnings
|
14
|
-
from typing import
|
15
|
+
from typing import Any, Generator, NamedTuple, TextIO, TypeVar
|
15
16
|
|
16
17
|
from . import iersdata
|
17
18
|
from .tz import Mountain
|
18
19
|
|
19
20
|
HOUR = datetime.timedelta(seconds=3600)
|
20
21
|
SECOND = datetime.timedelta(seconds=1)
|
21
|
-
|
22
|
-
T = TypeVar("T") # pylint: disable=invalid-name
|
22
|
+
T = TypeVar("T")
|
23
23
|
|
24
24
|
|
25
|
-
def
|
26
|
-
"""
|
27
|
-
giving a hint to the type system."""
|
25
|
+
def _require(x: T | None) -> T:
|
26
|
+
"""Check an Optional item is not None."""
|
28
27
|
assert x is not None
|
29
28
|
return x
|
30
29
|
|
@@ -35,24 +34,28 @@ def _removeprefix(s: str, p: str) -> str:
|
|
35
34
|
return s
|
36
35
|
|
37
36
|
|
38
|
-
def _date(dt:
|
37
|
+
def _date(dt: datetime.date) -> datetime.date:
|
39
38
|
"""Return the date object itself, or the date property of a datetime"""
|
40
39
|
if isinstance(dt, datetime.datetime):
|
41
40
|
return dt.date()
|
42
41
|
return dt
|
43
42
|
|
44
43
|
|
45
|
-
def _maybe_warn_update(dt: datetime.date) -> None:
|
44
|
+
def _maybe_warn_update(dt: datetime.date, stacklevel: int = 1) -> None:
|
46
45
|
"""Maybe print a notice to run updateiers, if it seems useful to do so."""
|
47
46
|
# We already know this date is not covered.
|
48
|
-
# If the date is less than
|
47
|
+
# If the date is less than 300 days after today, there should be (possibly)
|
49
48
|
# prospective available now.
|
50
|
-
today = datetime.
|
49
|
+
today = datetime.datetime.now(tz=datetime.timezone.utc).date()
|
50
|
+
print(f"_mwu {today=!r} {dt=!r} {iersdata.end=!r}")
|
51
51
|
if _date(dt) < today + datetime.timedelta(days=330):
|
52
|
-
warnings.warn(
|
52
|
+
warnings.warn(
|
53
|
+
"Note: Running `updateiers` may provide better DUT1 and LS information",
|
54
|
+
stacklevel=stacklevel + 1,
|
55
|
+
)
|
53
56
|
|
54
57
|
|
55
|
-
def get_dut1(dt:
|
58
|
+
def get_dut1(dt: datetime.date, *, warn_outdated: bool = True) -> float:
|
56
59
|
"""Return the DUT1 number for the given timestamp"""
|
57
60
|
date = _date(dt)
|
58
61
|
i = (date - iersdata.DUT1_DATA_START).days
|
@@ -60,7 +63,7 @@ def get_dut1(dt: DateOrDatetime, *, warn_outdated: bool = True) -> float:
|
|
60
63
|
v = iersdata.DUT1_OFFSETS[0]
|
61
64
|
elif i >= len(iersdata.DUT1_OFFSETS):
|
62
65
|
if warn_outdated:
|
63
|
-
_maybe_warn_update(dt)
|
66
|
+
_maybe_warn_update(dt, stacklevel=2)
|
64
67
|
v = iersdata.DUT1_OFFSETS[-1]
|
65
68
|
else:
|
66
69
|
v = iersdata.DUT1_OFFSETS[i]
|
@@ -74,7 +77,7 @@ def isly(year: int) -> bool:
|
|
74
77
|
return d1.year == d2.year
|
75
78
|
|
76
79
|
|
77
|
-
def isls(t:
|
80
|
+
def isls(t: datetime.date) -> bool:
|
78
81
|
"""Return True if a leap second occurs at the end of this month"""
|
79
82
|
dut1_today = get_dut1(t)
|
80
83
|
month_today = t.month
|
@@ -86,11 +89,11 @@ def isls(t: DateOrDatetime) -> bool:
|
|
86
89
|
|
87
90
|
def isdst(t: datetime.date, tz: datetime.tzinfo = Mountain) -> bool:
|
88
91
|
"""Return true if daylight saving time is active at the start of the given UTC day"""
|
89
|
-
|
90
|
-
return bool(
|
92
|
+
utc_daystart = datetime.datetime(t.year, t.month, t.day, tzinfo=datetime.timezone.utc)
|
93
|
+
return bool(utc_daystart.astimezone(tz).dst())
|
91
94
|
|
92
95
|
|
93
|
-
def
|
96
|
+
def _first_sunday_on_or_after(dt: datetime.date) -> datetime.date:
|
94
97
|
"""Return the first sunday on or after the reference time"""
|
95
98
|
days_to_go = 6 - dt.weekday()
|
96
99
|
if days_to_go:
|
@@ -98,17 +101,17 @@ def first_sunday_on_or_after(dt: DateOrDatetime) -> DateOrDatetime:
|
|
98
101
|
return dt
|
99
102
|
|
100
103
|
|
101
|
-
def
|
104
|
+
def _first_sunday_in_month(y: int, m: int) -> datetime.date:
|
102
105
|
"""Find the first sunday in a given month"""
|
103
|
-
return
|
106
|
+
return _first_sunday_on_or_after(datetime.datetime(y, m, 1, tzinfo=datetime.timezone.utc))
|
104
107
|
|
105
108
|
|
106
|
-
def
|
109
|
+
def _is_dst_change_day(t: datetime.date, tz: datetime.tzinfo = Mountain) -> bool:
|
107
110
|
"""Return True if the day is a DST change day"""
|
108
111
|
return isdst(t, tz) != isdst(t + datetime.timedelta(1), tz)
|
109
112
|
|
110
113
|
|
111
|
-
def
|
114
|
+
def _get_dst_change_hour(t: datetime.date, tz: datetime.tzinfo = Mountain) -> int | None:
|
112
115
|
"""Return the hour when DST changes"""
|
113
116
|
lt0 = datetime.datetime(t.year, t.month, t.day, hour=0, tzinfo=tz)
|
114
117
|
dst0 = lt0.dst()
|
@@ -122,28 +125,29 @@ def get_dst_change_hour(t: DateOrDatetime, tz: datetime.tzinfo = Mountain) -> Op
|
|
122
125
|
return None
|
123
126
|
|
124
127
|
|
125
|
-
def
|
126
|
-
d:
|
127
|
-
|
128
|
+
def _get_dst_change_date_and_row(
|
129
|
+
d: datetime.date,
|
130
|
+
tz: datetime.tzinfo = Mountain,
|
131
|
+
) -> tuple[datetime.date | None, int | None]:
|
128
132
|
"""Classify DST information for the WWVB phase modulation signal"""
|
129
133
|
if isdst(d, tz):
|
130
|
-
n =
|
134
|
+
n = _first_sunday_in_month(d.year, 11)
|
131
135
|
for offset in range(-28, 28, 7):
|
132
136
|
d1 = n + datetime.timedelta(days=offset)
|
133
|
-
if
|
137
|
+
if _is_dst_change_day(d1, tz):
|
134
138
|
return d1, (offset + 28) // 7
|
135
139
|
else:
|
136
|
-
m =
|
140
|
+
m = _first_sunday_in_month(d.year + (d.month > 3), 3)
|
137
141
|
for offset in range(0, 52, 7):
|
138
142
|
d1 = m + datetime.timedelta(days=offset)
|
139
|
-
if
|
143
|
+
if _is_dst_change_day(d1, tz):
|
140
144
|
return d1, offset // 7
|
141
145
|
|
142
146
|
return None, None
|
143
147
|
|
144
148
|
|
145
149
|
# "Table 8", likely with transcrption errors
|
146
|
-
|
150
|
+
_dsttable = [
|
147
151
|
[
|
148
152
|
[
|
149
153
|
0b110001,
|
@@ -211,19 +215,17 @@ dsttable = [
|
|
211
215
|
]
|
212
216
|
|
213
217
|
|
214
|
-
def
|
215
|
-
"""Generate the
|
216
|
-
except generate 255 bits so that we can simply use any range of [x:x+127]
|
217
|
-
bits"""
|
218
|
+
def _lfsr_gen(x: list[int]) -> None:
|
219
|
+
"""Generate the next bit of the 6-minute codes sequence"""
|
218
220
|
x.append(x[-7] ^ x[-6] ^ x[-5] ^ x[-2])
|
219
221
|
|
220
222
|
|
221
|
-
|
222
|
-
while len(
|
223
|
-
|
223
|
+
_lfsr_seq = [1] * 7
|
224
|
+
while len(_lfsr_seq) < 255:
|
225
|
+
_lfsr_gen(_lfsr_seq)
|
224
226
|
|
225
227
|
# Table 12 - Fixed 106-bit timing word
|
226
|
-
|
228
|
+
_ftw = [
|
227
229
|
int(c)
|
228
230
|
for c in "1101000111"
|
229
231
|
"0101100101"
|
@@ -239,11 +241,11 @@ ftw = [
|
|
239
241
|
]
|
240
242
|
|
241
243
|
|
242
|
-
def
|
244
|
+
def _get_dst_next(d: datetime.date, tz: datetime.tzinfo = Mountain) -> int:
|
243
245
|
"""Find the "dst next" value for the phase modulation signal"""
|
244
246
|
dst_now = isdst(d, tz) # dst_on[1]
|
245
|
-
dst_midwinter = isdst(datetime.datetime(d.year, 1, 1), tz)
|
246
|
-
dst_midsummer = isdst(datetime.datetime(d.year, 7, 1), tz)
|
247
|
+
dst_midwinter = isdst(datetime.datetime(d.year, 1, 1, tzinfo=datetime.timezone.utc), tz)
|
248
|
+
dst_midsummer = isdst(datetime.datetime(d.year, 7, 1, tzinfo=datetime.timezone.utc), tz)
|
247
249
|
|
248
250
|
if dst_midwinter and dst_midsummer:
|
249
251
|
return 0b101111
|
@@ -254,19 +256,19 @@ def get_dst_next(d: DateOrDatetime, tz: datetime.tzinfo = Mountain) -> int:
|
|
254
256
|
if dst_midwinter or not dst_midsummer:
|
255
257
|
return 0b100011
|
256
258
|
|
257
|
-
dst_change_date, dst_next_row =
|
259
|
+
dst_change_date, dst_next_row = _get_dst_change_date_and_row(d, tz)
|
258
260
|
if dst_change_date is None:
|
259
261
|
return 0b100011
|
260
262
|
assert dst_next_row is not None
|
261
263
|
|
262
|
-
dst_change_hour =
|
264
|
+
dst_change_hour = _get_dst_change_hour(dst_change_date, tz)
|
263
265
|
if dst_change_hour is None:
|
264
266
|
return 0b100011
|
265
267
|
|
266
|
-
return
|
268
|
+
return _dsttable[dst_now][dst_change_hour][dst_next_row]
|
267
269
|
|
268
270
|
|
269
|
-
|
271
|
+
_hamming_weight = [
|
270
272
|
[23, 21, 20, 17, 16, 15, 14, 13, 9, 8, 6, 5, 4, 2, 0],
|
271
273
|
[24, 22, 21, 18, 17, 16, 15, 14, 10, 9, 7, 6, 5, 3, 1],
|
272
274
|
[25, 23, 22, 19, 18, 17, 16, 15, 11, 10, 8, 7, 6, 4, 2],
|
@@ -281,25 +283,26 @@ SYNC_T = 0x768
|
|
281
283
|
SYNC_M = 0x1A3A
|
282
284
|
|
283
285
|
|
284
|
-
def
|
286
|
+
def _extract_bit(v: int, p: int) -> bool:
|
285
287
|
"""Extract bit 'p' from integer 'v' as a bool"""
|
286
288
|
return bool((v >> p) & 1)
|
287
289
|
|
288
290
|
|
289
|
-
def
|
291
|
+
def _hamming_parity(value: int) -> int:
|
290
292
|
"""Compute the "hamming parity" of a 26-bit number, such as the minute-of-century
|
291
293
|
|
292
|
-
For more details, see Enhanced WWVB Broadcast Format 4.3
|
294
|
+
For more details, see Enhanced WWVB Broadcast Format 4.3
|
295
|
+
"""
|
293
296
|
parity = 0
|
294
297
|
for i in range(4, -1, -1):
|
295
298
|
bit = 0
|
296
|
-
for j in range(
|
297
|
-
bit ^=
|
299
|
+
for j in range(15):
|
300
|
+
bit ^= _extract_bit(value, _hamming_weight[i][j])
|
298
301
|
parity = (parity << 1) | bit
|
299
302
|
return parity
|
300
303
|
|
301
304
|
|
302
|
-
|
305
|
+
_dst_ls_lut = [
|
303
306
|
0b01000,
|
304
307
|
0b10101,
|
305
308
|
0b10110,
|
@@ -318,35 +321,57 @@ dst_ls_lut = [
|
|
318
321
|
0b11111,
|
319
322
|
]
|
320
323
|
|
321
|
-
_WWVBMinute = collections.namedtuple("_WWVBMinute", "year days hour min dst ut1 ls ly")
|
322
324
|
|
323
|
-
|
324
|
-
class WWVBMinute(_WWVBMinute):
|
325
|
+
class _WWVBMinute(NamedTuple):
|
325
326
|
"""Uniquely identifies a minute of time in the WWVB system.
|
326
327
|
|
327
|
-
To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.
|
328
|
+
To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.
|
329
|
+
"""
|
328
330
|
|
329
331
|
year: int
|
332
|
+
"""2-digit year within the WWVB epoch"""
|
333
|
+
|
334
|
+
days: int
|
335
|
+
"""1-based day of year"""
|
336
|
+
|
330
337
|
hour: int
|
331
|
-
|
338
|
+
"""UTC hour of day"""
|
339
|
+
|
340
|
+
min: int
|
341
|
+
"""Minute of hour"""
|
342
|
+
|
332
343
|
dst: int
|
344
|
+
"""2-bit DST code """
|
345
|
+
|
333
346
|
ut1: int
|
334
|
-
|
347
|
+
"""UT1 offset in units of 100ms, range -900 to +900ms"""
|
348
|
+
|
335
349
|
ls: bool
|
350
|
+
"""Leap second warning flag"""
|
351
|
+
|
352
|
+
ly: bool
|
353
|
+
"""Leap year flag"""
|
354
|
+
|
355
|
+
|
356
|
+
class WWVBMinute(_WWVBMinute):
|
357
|
+
"""Uniquely identifies a minute of time in the WWVB system.
|
358
|
+
|
359
|
+
To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.
|
360
|
+
"""
|
336
361
|
|
337
362
|
epoch: int = 1970
|
338
363
|
|
339
|
-
def __new__( #
|
364
|
+
def __new__( # noqa: PYI034
|
340
365
|
cls,
|
341
366
|
year: int,
|
342
367
|
days: int,
|
343
368
|
hour: int,
|
344
369
|
minute: int,
|
345
|
-
dst:
|
346
|
-
ut1:
|
347
|
-
ls:
|
348
|
-
ly:
|
349
|
-
) ->
|
370
|
+
dst: int | None = None,
|
371
|
+
ut1: int | None = None,
|
372
|
+
ls: bool | None = None,
|
373
|
+
ly: bool | None = None,
|
374
|
+
) -> WWVBMinute:
|
350
375
|
"""Construct a WWVBMinute"""
|
351
376
|
if dst is None:
|
352
377
|
dst = cls.get_dst(year, days)
|
@@ -383,7 +408,7 @@ class WWVBMinute(_WWVBMinute):
|
|
383
408
|
@staticmethod
|
384
409
|
def get_dst(year: int, days: int) -> int:
|
385
410
|
"""Get the 2-bit WWVB DST value for the given day"""
|
386
|
-
d0 = datetime.datetime(year, 1, 1) + datetime.timedelta(days - 1)
|
411
|
+
d0 = datetime.datetime(year, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(days - 1)
|
387
412
|
d1 = d0 + datetime.timedelta(1)
|
388
413
|
dst0 = isdst(d0)
|
389
414
|
dst1 = isdst(d1)
|
@@ -405,7 +430,12 @@ class WWVBMinute(_WWVBMinute):
|
|
405
430
|
|
406
431
|
as_datetime = as_datetime_utc
|
407
432
|
|
408
|
-
def as_datetime_local(
|
433
|
+
def as_datetime_local(
|
434
|
+
self,
|
435
|
+
standard_time_offset: int = 7 * 3600,
|
436
|
+
*,
|
437
|
+
dst_observed: bool = True,
|
438
|
+
) -> datetime.datetime:
|
409
439
|
"""Convert to a local datetime according to the DST bits"""
|
410
440
|
u = self.as_datetime_utc()
|
411
441
|
offset = datetime.timedelta(seconds=-standard_time_offset)
|
@@ -426,11 +456,6 @@ class WWVBMinute(_WWVBMinute):
|
|
426
456
|
offset += datetime.timedelta(seconds=3600)
|
427
457
|
return u.astimezone(datetime.timezone(offset))
|
428
458
|
|
429
|
-
def is_ly(self) -> bool:
|
430
|
-
"""Return True if minute is during a leap year"""
|
431
|
-
warnings.warn("Deprecated, use ly property instead", DeprecationWarning)
|
432
|
-
return self.ly
|
433
|
-
|
434
459
|
def _is_end_of_month(self) -> bool:
|
435
460
|
"""Return True if minute is the last minute in a month"""
|
436
461
|
d = self.as_datetime()
|
@@ -449,18 +474,18 @@ class WWVBMinute(_WWVBMinute):
|
|
449
474
|
return 59
|
450
475
|
return 61
|
451
476
|
|
452
|
-
def as_timecode(self) ->
|
477
|
+
def as_timecode(self) -> WWVBTimecode:
|
453
478
|
"""Fill a WWVBTimecode structure representing this minute. Fills both the amplitude and phase codes."""
|
454
479
|
t = WWVBTimecode(self.minute_length())
|
455
480
|
|
456
|
-
self.
|
457
|
-
self.
|
481
|
+
self._fill_am_timecode(t)
|
482
|
+
self._fill_pm_timecode(t)
|
458
483
|
|
459
484
|
return t
|
460
485
|
|
461
486
|
@property
|
462
|
-
def
|
463
|
-
"""Return the 2-bit
|
487
|
+
def _leap_sec(self) -> int:
|
488
|
+
"""Return the 2-bit _leap_sec value used by the PM code"""
|
464
489
|
if not self.ls:
|
465
490
|
return 0
|
466
491
|
if self.ut1 < 0:
|
@@ -477,7 +502,7 @@ class WWVBMinute(_WWVBMinute):
|
|
477
502
|
// 60
|
478
503
|
)
|
479
504
|
|
480
|
-
def
|
505
|
+
def _fill_am_timecode(self, t: WWVBTimecode) -> None:
|
481
506
|
"""Fill the amplitude (AM) portion of a timecode object"""
|
482
507
|
for i in [0, 9, 19, 29, 39, 49]:
|
483
508
|
t.am[i] = AmplitudeModulation.MARK
|
@@ -499,7 +524,7 @@ class WWVBMinute(_WWVBMinute):
|
|
499
524
|
t.am[56] = AmplitudeModulation(self.ls)
|
500
525
|
t._put_am_bcd(self.dst, 57, 58)
|
501
526
|
|
502
|
-
def
|
527
|
+
def _fill_pm_timecode_extended(self, t: WWVBTimecode) -> None:
|
503
528
|
"""During minutes 10..15 and 40..45, the amplitude signal holds 'extended information'"""
|
504
529
|
assert 10 <= self.min < 16 or 40 <= self.min < 46
|
505
530
|
minno = self.min % 10
|
@@ -520,107 +545,99 @@ class WWVBMinute(_WWVBMinute):
|
|
520
545
|
seqno = seqno + 90
|
521
546
|
else:
|
522
547
|
seqno = seqno + 1
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
seqno = seqno + 91
|
548
|
+
elif self.hour < 4:
|
549
|
+
seqno = seqno + 1
|
550
|
+
elif self.hour < 11:
|
551
|
+
seqno = seqno + 91
|
528
552
|
|
529
|
-
info_seq =
|
530
|
-
full_seq = info_seq +
|
553
|
+
info_seq = _lfsr_seq[seqno : seqno + 127]
|
554
|
+
full_seq = info_seq + _ftw + info_seq[::-1]
|
531
555
|
assert len(full_seq) == 360
|
532
556
|
|
533
557
|
offset = minno * 60
|
534
558
|
for i in range(60):
|
535
559
|
t._put_pm_bit(i, full_seq[i + offset])
|
536
560
|
|
537
|
-
def
|
538
|
-
self, t: "WWVBTimecode"
|
539
|
-
) -> None:
|
561
|
+
def _fill_pm_timecode_regular(self, t: WWVBTimecode) -> None: # noqa: PLR0915
|
540
562
|
"""Except during minutes 10..15 and 40..45, the amplitude signal holds 'regular information'"""
|
541
563
|
t._put_pm_bin(0, 13, SYNC_T)
|
542
564
|
|
543
565
|
moc = self.minute_of_century
|
544
|
-
|
566
|
+
_leap_sec = self._leap_sec
|
545
567
|
dst_on = self.dst
|
546
|
-
dst_ls =
|
547
|
-
dst_next =
|
548
|
-
t._put_pm_bin(13, 5,
|
549
|
-
t._put_pm_bit(18,
|
550
|
-
t._put_pm_bit(19,
|
551
|
-
t._put_pm_bit(20,
|
552
|
-
t._put_pm_bit(21,
|
553
|
-
t._put_pm_bit(22,
|
554
|
-
t._put_pm_bit(23,
|
555
|
-
t._put_pm_bit(24,
|
556
|
-
t._put_pm_bit(25,
|
557
|
-
t._put_pm_bit(26,
|
558
|
-
t._put_pm_bit(27,
|
559
|
-
t._put_pm_bit(28,
|
560
|
-
t._put_pm_bit(29, False) # Reserved
|
561
|
-
t._put_pm_bit(30,
|
562
|
-
t._put_pm_bit(31,
|
563
|
-
t._put_pm_bit(32,
|
564
|
-
t._put_pm_bit(33,
|
565
|
-
t._put_pm_bit(34,
|
566
|
-
t._put_pm_bit(35,
|
567
|
-
t._put_pm_bit(36,
|
568
|
-
t._put_pm_bit(37,
|
569
|
-
t._put_pm_bit(38,
|
570
|
-
t._put_pm_bit(39, True) # Reserved
|
571
|
-
t._put_pm_bit(40,
|
572
|
-
t._put_pm_bit(41,
|
573
|
-
t._put_pm_bit(42,
|
574
|
-
t._put_pm_bit(43,
|
575
|
-
t._put_pm_bit(44,
|
576
|
-
t._put_pm_bit(45,
|
577
|
-
t._put_pm_bit(46,
|
578
|
-
t._put_pm_bit(47,
|
579
|
-
t._put_pm_bit(48,
|
580
|
-
t._put_pm_bit(49, True) # Notice
|
581
|
-
t._put_pm_bit(50,
|
582
|
-
t._put_pm_bit(51,
|
583
|
-
t._put_pm_bit(52,
|
584
|
-
t._put_pm_bit(53,
|
585
|
-
t._put_pm_bit(54,
|
586
|
-
t._put_pm_bit(55,
|
587
|
-
t._put_pm_bit(56,
|
588
|
-
t._put_pm_bit(57,
|
589
|
-
t._put_pm_bit(58,
|
568
|
+
dst_ls = _dst_ls_lut[dst_on | (_leap_sec << 2)]
|
569
|
+
dst_next = _get_dst_next(self.as_datetime())
|
570
|
+
t._put_pm_bin(13, 5, _hamming_parity(moc))
|
571
|
+
t._put_pm_bit(18, _extract_bit(moc, 25))
|
572
|
+
t._put_pm_bit(19, _extract_bit(moc, 0))
|
573
|
+
t._put_pm_bit(20, _extract_bit(moc, 24))
|
574
|
+
t._put_pm_bit(21, _extract_bit(moc, 23))
|
575
|
+
t._put_pm_bit(22, _extract_bit(moc, 22))
|
576
|
+
t._put_pm_bit(23, _extract_bit(moc, 21))
|
577
|
+
t._put_pm_bit(24, _extract_bit(moc, 20))
|
578
|
+
t._put_pm_bit(25, _extract_bit(moc, 19))
|
579
|
+
t._put_pm_bit(26, _extract_bit(moc, 18))
|
580
|
+
t._put_pm_bit(27, _extract_bit(moc, 17))
|
581
|
+
t._put_pm_bit(28, _extract_bit(moc, 16))
|
582
|
+
t._put_pm_bit(29, False) # noqa: FBT003 # Reserved
|
583
|
+
t._put_pm_bit(30, _extract_bit(moc, 15))
|
584
|
+
t._put_pm_bit(31, _extract_bit(moc, 14))
|
585
|
+
t._put_pm_bit(32, _extract_bit(moc, 13))
|
586
|
+
t._put_pm_bit(33, _extract_bit(moc, 12))
|
587
|
+
t._put_pm_bit(34, _extract_bit(moc, 11))
|
588
|
+
t._put_pm_bit(35, _extract_bit(moc, 10))
|
589
|
+
t._put_pm_bit(36, _extract_bit(moc, 9))
|
590
|
+
t._put_pm_bit(37, _extract_bit(moc, 8))
|
591
|
+
t._put_pm_bit(38, _extract_bit(moc, 7))
|
592
|
+
t._put_pm_bit(39, True) # noqa: FBT003 # Reserved
|
593
|
+
t._put_pm_bit(40, _extract_bit(moc, 6))
|
594
|
+
t._put_pm_bit(41, _extract_bit(moc, 5))
|
595
|
+
t._put_pm_bit(42, _extract_bit(moc, 4))
|
596
|
+
t._put_pm_bit(43, _extract_bit(moc, 3))
|
597
|
+
t._put_pm_bit(44, _extract_bit(moc, 2))
|
598
|
+
t._put_pm_bit(45, _extract_bit(moc, 1))
|
599
|
+
t._put_pm_bit(46, _extract_bit(moc, 0))
|
600
|
+
t._put_pm_bit(47, _extract_bit(dst_ls, 4))
|
601
|
+
t._put_pm_bit(48, _extract_bit(dst_ls, 3))
|
602
|
+
t._put_pm_bit(49, True) # noqa: FBT003 # Notice
|
603
|
+
t._put_pm_bit(50, _extract_bit(dst_ls, 2))
|
604
|
+
t._put_pm_bit(51, _extract_bit(dst_ls, 1))
|
605
|
+
t._put_pm_bit(52, _extract_bit(dst_ls, 0))
|
606
|
+
t._put_pm_bit(53, _extract_bit(dst_next, 5))
|
607
|
+
t._put_pm_bit(54, _extract_bit(dst_next, 4))
|
608
|
+
t._put_pm_bit(55, _extract_bit(dst_next, 3))
|
609
|
+
t._put_pm_bit(56, _extract_bit(dst_next, 2))
|
610
|
+
t._put_pm_bit(57, _extract_bit(dst_next, 1))
|
611
|
+
t._put_pm_bit(58, _extract_bit(dst_next, 0))
|
590
612
|
if len(t.phase) > 59:
|
591
613
|
t._put_pm_bit(59, PhaseModulation.ZERO)
|
592
614
|
if len(t.phase) > 60:
|
593
615
|
t._put_pm_bit(60, PhaseModulation.ZERO)
|
594
616
|
|
595
|
-
def
|
617
|
+
def _fill_pm_timecode(self, t: WWVBTimecode) -> None:
|
596
618
|
"""Fill the phase portion of a timecode object"""
|
597
619
|
if 10 <= self.min < 16 or 40 <= self.min < 46:
|
598
|
-
self.
|
620
|
+
self._fill_pm_timecode_extended(t)
|
599
621
|
else:
|
600
|
-
self.
|
622
|
+
self._fill_pm_timecode_regular(t)
|
601
623
|
|
602
|
-
def next_minute(self, newut1:
|
624
|
+
def next_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
603
625
|
"""Return an object representing the next minute"""
|
604
626
|
d = self.as_datetime() + datetime.timedelta(minutes=1)
|
605
627
|
return self.from_datetime(d, newut1, newls, self)
|
606
628
|
|
607
|
-
def previous_minute(self, newut1:
|
629
|
+
def previous_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
608
630
|
"""Return an object representing the previous minute"""
|
609
631
|
d = self.as_datetime() - datetime.timedelta(minutes=1)
|
610
632
|
return self.from_datetime(d, newut1, newls, self)
|
611
633
|
|
612
634
|
@classmethod
|
613
|
-
def _get_dut1_info( #
|
614
|
-
cls: type, year: int, days: int, old_time: "Optional[WWVBMinute]" = None
|
615
|
-
) -> Tuple[int, bool]:
|
635
|
+
def _get_dut1_info(cls: type, year: int, days: int, old_time: WWVBMinute | None = None) -> tuple[int, bool]: # noqa: ARG003
|
616
636
|
"""Return the DUT1 information for a given day, possibly propagating information from a previous timestamp"""
|
617
637
|
if old_time is not None:
|
618
638
|
if old_time.minute_length() != 60:
|
619
639
|
newls = False
|
620
|
-
if old_time.ut1 < 0
|
621
|
-
newut1 = old_time.ut1 + 1000
|
622
|
-
else:
|
623
|
-
newut1 = old_time.ut1 - 1000
|
640
|
+
newut1 = old_time.ut1 + 1000 if old_time.ut1 < 0 else old_time.ut1 - 1000
|
624
641
|
else:
|
625
642
|
newls = old_time.ls
|
626
643
|
newut1 = old_time.ut1
|
@@ -628,10 +645,10 @@ class WWVBMinute(_WWVBMinute):
|
|
628
645
|
return 0, False
|
629
646
|
|
630
647
|
@classmethod
|
631
|
-
def fromstring(cls, s: str) ->
|
648
|
+
def fromstring(cls, s: str) -> WWVBMinute:
|
632
649
|
"""Construct a WWVBMinute from a string representation created by print_timecodes"""
|
633
650
|
s = _removeprefix(s, "WWVB timecode: ")
|
634
|
-
d:
|
651
|
+
d: dict[str, int] = {}
|
635
652
|
for part in s.split():
|
636
653
|
k, v = part.split("=")
|
637
654
|
if k == "min":
|
@@ -641,8 +658,8 @@ class WWVBMinute(_WWVBMinute):
|
|
641
658
|
days = d.pop("days")
|
642
659
|
hour = d.pop("hour")
|
643
660
|
minute = d.pop("minute")
|
644
|
-
dst:
|
645
|
-
ut1:
|
661
|
+
dst: int | None = d.pop("dst", None)
|
662
|
+
ut1: int | None = d.pop("ut1", None)
|
646
663
|
ls = d.pop("ls", None)
|
647
664
|
d.pop("ly", None)
|
648
665
|
if d:
|
@@ -653,10 +670,10 @@ class WWVBMinute(_WWVBMinute):
|
|
653
670
|
def from_datetime(
|
654
671
|
cls,
|
655
672
|
d: datetime.datetime,
|
656
|
-
newut1:
|
657
|
-
newls:
|
658
|
-
old_time:
|
659
|
-
) ->
|
673
|
+
newut1: int | None = None,
|
674
|
+
newls: bool | None = None,
|
675
|
+
old_time: WWVBMinute | None = None,
|
676
|
+
) -> WWVBMinute:
|
660
677
|
"""Construct a WWVBMinute from a datetime, possibly specifying ut1/ls data or propagating it from an old time"""
|
661
678
|
u = d.utctimetuple()
|
662
679
|
if newls is None and newut1 is None:
|
@@ -664,9 +681,7 @@ class WWVBMinute(_WWVBMinute):
|
|
664
681
|
return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
|
665
682
|
|
666
683
|
@classmethod
|
667
|
-
def from_timecode_am(
|
668
|
-
cls, t: "WWVBTimecode"
|
669
|
-
) -> Optional["WWVBMinute"]:
|
684
|
+
def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None:
|
670
685
|
"""Construct a WWVBMinute from a WWVBTimecode"""
|
671
686
|
for i in (0, 9, 19, 29, 39, 49, 59):
|
672
687
|
if t.am[i] != AmplitudeModulation.MARK:
|
@@ -700,7 +715,7 @@ class WWVBMinute(_WWVBMinute):
|
|
700
715
|
if days > 366 or (not ly and days > 365):
|
701
716
|
return None
|
702
717
|
ls = bool(t.am[56])
|
703
|
-
dst =
|
718
|
+
dst = _require(t._get_am_bcd(57, 58))
|
704
719
|
return cls(year, days, hour, minute, dst, ut1, ls, ly)
|
705
720
|
|
706
721
|
|
@@ -708,12 +723,12 @@ class WWVBMinuteIERS(WWVBMinute):
|
|
708
723
|
"""A WWVBMinute that uses a database of DUT1 information"""
|
709
724
|
|
710
725
|
@classmethod
|
711
|
-
def _get_dut1_info(cls, year: int, days: int, old_time:
|
712
|
-
d = datetime.datetime(year, 1, 1) + datetime.timedelta(days - 1)
|
726
|
+
def _get_dut1_info(cls, year: int, days: int, old_time: WWVBMinute | None = None) -> tuple[int, bool]: # noqa: ARG003
|
727
|
+
d = datetime.datetime(year, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(days - 1)
|
713
728
|
return int(round(get_dut1(d) * 10)) * 100, isls(d)
|
714
729
|
|
715
730
|
|
716
|
-
def
|
731
|
+
def _bcd_bits(n: int) -> Generator[bool, None, None]:
|
717
732
|
"""Return the bcd representation of n, starting with the least significant bit"""
|
718
733
|
while True:
|
719
734
|
d = n % 10
|
@@ -744,18 +759,23 @@ class PhaseModulation(enum.IntEnum):
|
|
744
759
|
class WWVBTimecode:
|
745
760
|
"""Represent the amplitude and/or phase signal, usually over 1 minute"""
|
746
761
|
|
747
|
-
am:
|
748
|
-
|
762
|
+
am: list[AmplitudeModulation]
|
763
|
+
"""The amplitude modulation data"""
|
764
|
+
|
765
|
+
phase: list[PhaseModulation]
|
766
|
+
"""The phase modulation data"""
|
749
767
|
|
750
768
|
def __init__(self, sz: int) -> None:
|
751
|
-
|
769
|
+
"""Construct a WWVB timecode ``sz`` seconds long"""
|
770
|
+
self.am = [AmplitudeModulation.UNSET] * sz
|
752
771
|
self.phase = [PhaseModulation.UNSET] * sz
|
753
772
|
|
754
|
-
def _get_am_bcd(self, *poslist: int) ->
|
773
|
+
def _get_am_bcd(self, *poslist: int) -> int | None:
|
755
774
|
"""Convert AM data to BCD
|
756
775
|
|
757
776
|
The the bits ``self.am[poslist[i]]`` in MSB order are converted from
|
758
|
-
BCD to integer
|
777
|
+
BCD to integer
|
778
|
+
"""
|
759
779
|
pos = reversed(poslist)
|
760
780
|
val = [bool(self.am[p]) for p in pos]
|
761
781
|
result = 0
|
@@ -775,28 +795,29 @@ class WWVBTimecode:
|
|
775
795
|
|
776
796
|
The bits at ``self.am[poslist[i]]`` in MSB order are filled with
|
777
797
|
the conversion of `v` to BCD
|
778
|
-
Treating 'poslist' as a sequence of indices, update the AM signal with the value as a BCD number
|
798
|
+
Treating 'poslist' as a sequence of indices, update the AM signal with the value as a BCD number
|
799
|
+
"""
|
779
800
|
pos = list(poslist)[::-1]
|
780
|
-
for p, b in zip(pos,
|
801
|
+
for p, b in zip(pos, _bcd_bits(v)):
|
781
802
|
if b:
|
782
803
|
self.am[p] = AmplitudeModulation.ONE
|
783
804
|
else:
|
784
805
|
self.am[p] = AmplitudeModulation.ZERO
|
785
806
|
|
786
|
-
def _put_pm_bit(self, i: int, v:
|
807
|
+
def _put_pm_bit(self, i: int, v: PhaseModulation | int | bool) -> None:
|
787
808
|
"""Update a bit of the Phase Modulation signal"""
|
788
809
|
self.phase[i] = PhaseModulation(v)
|
789
810
|
|
790
811
|
def _put_pm_bin(self, st: int, n: int, v: int) -> None:
|
791
812
|
"""Update an n-digit binary number in the Phase Modulation signal"""
|
792
813
|
for i in range(n):
|
793
|
-
self._put_pm_bit(st + i,
|
814
|
+
self._put_pm_bit(st + i, _extract_bit(v, (n - i - 1)))
|
794
815
|
|
795
816
|
def __str__(self) -> str:
|
796
|
-
"""
|
817
|
+
"""Implement str()"""
|
797
818
|
undefined = [i for i in range(len(self.am)) if self.am[i] == AmplitudeModulation.UNSET]
|
798
819
|
if undefined:
|
799
|
-
warnings.warn(f"am{undefined} is unset")
|
820
|
+
warnings.warn(f"am{undefined} is unset", stacklevel=1)
|
800
821
|
|
801
822
|
def convert_one(am: AmplitudeModulation, phase: PhaseModulation) -> str:
|
802
823
|
if phase is PhaseModulation.UNSET:
|
@@ -808,20 +829,20 @@ class WWVBTimecode:
|
|
808
829
|
return "".join(convert_one(i, j) for i, j in zip(self.am, self.phase))
|
809
830
|
|
810
831
|
def __repr__(self) -> str:
|
811
|
-
"""
|
832
|
+
"""Implement repr()"""
|
812
833
|
return "<WWVBTimecode " + str(self) + ">"
|
813
834
|
|
814
|
-
def to_am_string(self, charset:
|
835
|
+
def to_am_string(self, charset: list[str]) -> str:
|
815
836
|
"""Convert the amplitude signal to a string"""
|
816
837
|
return "".join(charset[i] for i in self.am)
|
817
838
|
|
818
839
|
to_string = to_am_string
|
819
840
|
|
820
|
-
def to_pm_string(self, charset:
|
841
|
+
def to_pm_string(self, charset: list[str]) -> str:
|
821
842
|
"""Convert the phase signal to a string"""
|
822
843
|
return "".join(charset[i] for i in self.phase)
|
823
844
|
|
824
|
-
def to_both_string(self, charset:
|
845
|
+
def to_both_string(self, charset: list[str]) -> str:
|
825
846
|
"""Convert both channels to a string"""
|
826
847
|
return "".join(charset[i + j * 3] for i, j in zip(self.am, self.phase))
|
827
848
|
|
@@ -835,7 +856,6 @@ styles = {
|
|
835
856
|
}
|
836
857
|
|
837
858
|
|
838
|
-
# pylint: disable=too-many-arguments
|
839
859
|
def print_timecodes(
|
840
860
|
w: WWVBMinute,
|
841
861
|
minutes: int,
|
@@ -854,7 +874,7 @@ def print_timecodes(
|
|
854
874
|
if first or all_timecodes:
|
855
875
|
if not first:
|
856
876
|
print(file=file)
|
857
|
-
print(f"WWVB timecode: {
|
877
|
+
print(f"WWVB timecode: {w!s}{channel_text}{style_text}", file=file)
|
858
878
|
first = False
|
859
879
|
pfx = f"{w.year:04d}-{w.days:03d} {w.hour:02d}:{w.min:02d} "
|
860
880
|
tc = w.as_timecode()
|
@@ -873,17 +893,30 @@ def print_timecodes(
|
|
873
893
|
w = w.next_minute()
|
874
894
|
|
875
895
|
|
876
|
-
# pylint: disable=too-many-arguments
|
877
896
|
def print_timecodes_json(
|
878
897
|
w: WWVBMinute,
|
879
898
|
minutes: int,
|
880
899
|
channel: str,
|
881
900
|
file: TextIO,
|
882
901
|
) -> None:
|
883
|
-
"""Print a range of timecodes
|
902
|
+
"""Print a range of timecodes in JSON format.
|
903
|
+
|
904
|
+
The result is a json array of minute data. Each minute data is an object with the following members:
|
905
|
+
|
906
|
+
* year (int)
|
907
|
+
* days (int)
|
908
|
+
* hour (int)
|
909
|
+
* minute (int)
|
910
|
+
* amplitude (string; only if channel is amplitude or both)
|
911
|
+
* phase: (string; only if channel is phase or both)
|
912
|
+
|
913
|
+
The amplitude and phase strings are of length 60 during most minutes, length 61
|
914
|
+
during a minute that includes a (positive) leap second, and theoretically
|
915
|
+
length 59 in the case of a negative leap second.
|
916
|
+
"""
|
884
917
|
result = []
|
885
918
|
for _ in range(minutes):
|
886
|
-
data = {
|
919
|
+
data: dict[str, Any] = {
|
887
920
|
"year": w.year,
|
888
921
|
"days": w.days,
|
889
922
|
"hour": w.hour,
|