wwvb 4.0.0a0__py3-none-any.whl → 4.1.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.
- leapseconddata/__init__.py +342 -0
- leapseconddata/__main__.py +169 -0
- {wwvb → leapseconddata}/__version__.py +2 -2
- wwvb-4.1.0a0.dist-info/METADATA +60 -0
- wwvb-4.1.0a0.dist-info/RECORD +9 -0
- wwvb-4.1.0a0.dist-info/entry_points.txt +2 -0
- wwvb-4.1.0a0.dist-info/top_level.txt +1 -0
- uwwvb.py +0 -193
- wwvb/__init__.py +0 -935
- wwvb/decode.py +0 -90
- wwvb/dut1table.py +0 -32
- wwvb/gen.py +0 -128
- wwvb/iersdata.py +0 -28
- wwvb/iersdata_dist.py +0 -38
- wwvb/testcli.py +0 -291
- wwvb/testdaylight.py +0 -60
- wwvb/testls.py +0 -63
- wwvb/testpm.py +0 -33
- wwvb/testuwwvb.py +0 -221
- wwvb/testwwvb.py +0 -403
- wwvb/tz.py +0 -13
- wwvb/updateiers.py +0 -199
- wwvb/wwvbtk.py +0 -144
- wwvb-4.0.0a0.dist-info/METADATA +0 -199
- wwvb-4.0.0a0.dist-info/RECORD +0 -23
- wwvb-4.0.0a0.dist-info/entry_points.txt +0 -8
- wwvb-4.0.0a0.dist-info/top_level.txt +0 -2
- {wwvb → leapseconddata}/py.typed +0 -0
- {wwvb-4.0.0a0.dist-info → wwvb-4.1.0a0.dist-info}/WHEEL +0 -0
wwvb/__init__.py
DELETED
@@ -1,935 +0,0 @@
|
|
1
|
-
#!/usr/bin/python3
|
2
|
-
"""A library for WWVB timecodes"""
|
3
|
-
|
4
|
-
# Copyright (C) 2011-2020 Jeff Epler <jepler@gmail.com>
|
5
|
-
# SPDX-FileCopyrightText: 2021 Jeff Epler
|
6
|
-
#
|
7
|
-
# SPDX-License-Identifier: GPL-3.0-only
|
8
|
-
|
9
|
-
from __future__ import annotations
|
10
|
-
|
11
|
-
import datetime
|
12
|
-
import enum
|
13
|
-
import json
|
14
|
-
import warnings
|
15
|
-
from typing import Any, Generator, NamedTuple, TextIO, TypeVar
|
16
|
-
|
17
|
-
from . import iersdata
|
18
|
-
from .tz import Mountain
|
19
|
-
|
20
|
-
HOUR = datetime.timedelta(seconds=3600)
|
21
|
-
SECOND = datetime.timedelta(seconds=1)
|
22
|
-
T = TypeVar("T")
|
23
|
-
|
24
|
-
|
25
|
-
def _require(x: T | None) -> T:
|
26
|
-
"""Check an Optional item is not None."""
|
27
|
-
assert x is not None
|
28
|
-
return x
|
29
|
-
|
30
|
-
|
31
|
-
def _removeprefix(s: str, p: str) -> str:
|
32
|
-
if s.startswith(p):
|
33
|
-
return s[len(p) :]
|
34
|
-
return s
|
35
|
-
|
36
|
-
|
37
|
-
def _date(dt: datetime.date) -> datetime.date:
|
38
|
-
"""Return the date object itself, or the date property of a datetime"""
|
39
|
-
if isinstance(dt, datetime.datetime):
|
40
|
-
return dt.date()
|
41
|
-
return dt
|
42
|
-
|
43
|
-
|
44
|
-
def _maybe_warn_update(dt: datetime.date, stacklevel: int = 1) -> None:
|
45
|
-
"""Maybe print a notice to run updateiers, if it seems useful to do so."""
|
46
|
-
# We already know this date is not covered.
|
47
|
-
# If the date is less than 300 days after today, there should be (possibly)
|
48
|
-
# prospective available now.
|
49
|
-
today = datetime.datetime.now(tz=datetime.timezone.utc).date()
|
50
|
-
print(f"_mwu {today=!r} {dt=!r} {iersdata.end=!r}")
|
51
|
-
if _date(dt) < today + datetime.timedelta(days=330):
|
52
|
-
warnings.warn(
|
53
|
-
"Note: Running `updateiers` may provide better DUT1 and LS information",
|
54
|
-
stacklevel=stacklevel + 1,
|
55
|
-
)
|
56
|
-
|
57
|
-
|
58
|
-
def get_dut1(dt: datetime.date, *, warn_outdated: bool = True) -> float:
|
59
|
-
"""Return the DUT1 number for the given timestamp"""
|
60
|
-
date = _date(dt)
|
61
|
-
i = (date - iersdata.DUT1_DATA_START).days
|
62
|
-
if i < 0:
|
63
|
-
v = iersdata.DUT1_OFFSETS[0]
|
64
|
-
elif i >= len(iersdata.DUT1_OFFSETS):
|
65
|
-
if warn_outdated:
|
66
|
-
_maybe_warn_update(dt, stacklevel=2)
|
67
|
-
v = iersdata.DUT1_OFFSETS[-1]
|
68
|
-
else:
|
69
|
-
v = iersdata.DUT1_OFFSETS[i]
|
70
|
-
return (ord(v) - ord("k")) / 10.0
|
71
|
-
|
72
|
-
|
73
|
-
def isly(year: int) -> bool:
|
74
|
-
"""Return True if the year is a leap year"""
|
75
|
-
d1 = datetime.date(year, 1, 1)
|
76
|
-
d2 = d1 + datetime.timedelta(days=365)
|
77
|
-
return d1.year == d2.year
|
78
|
-
|
79
|
-
|
80
|
-
def isls(t: datetime.date) -> bool:
|
81
|
-
"""Return True if a leap second occurs at the end of this month"""
|
82
|
-
dut1_today = get_dut1(t)
|
83
|
-
month_today = t.month
|
84
|
-
while t.month == month_today:
|
85
|
-
t += datetime.timedelta(1)
|
86
|
-
dut1_next_month = get_dut1(t)
|
87
|
-
return dut1_today * dut1_next_month < 0
|
88
|
-
|
89
|
-
|
90
|
-
def isdst(t: datetime.date, tz: datetime.tzinfo = Mountain) -> bool:
|
91
|
-
"""Return true if daylight saving time is active at the start of the given UTC day"""
|
92
|
-
utc_daystart = datetime.datetime(t.year, t.month, t.day, tzinfo=datetime.timezone.utc)
|
93
|
-
return bool(utc_daystart.astimezone(tz).dst())
|
94
|
-
|
95
|
-
|
96
|
-
def _first_sunday_on_or_after(dt: datetime.date) -> datetime.date:
|
97
|
-
"""Return the first sunday on or after the reference time"""
|
98
|
-
days_to_go = 6 - dt.weekday()
|
99
|
-
if days_to_go:
|
100
|
-
return dt + datetime.timedelta(days_to_go)
|
101
|
-
return dt
|
102
|
-
|
103
|
-
|
104
|
-
def _first_sunday_in_month(y: int, m: int) -> datetime.date:
|
105
|
-
"""Find the first sunday in a given month"""
|
106
|
-
return _first_sunday_on_or_after(datetime.datetime(y, m, 1, tzinfo=datetime.timezone.utc))
|
107
|
-
|
108
|
-
|
109
|
-
def _is_dst_change_day(t: datetime.date, tz: datetime.tzinfo = Mountain) -> bool:
|
110
|
-
"""Return True if the day is a DST change day"""
|
111
|
-
return isdst(t, tz) != isdst(t + datetime.timedelta(1), tz)
|
112
|
-
|
113
|
-
|
114
|
-
def _get_dst_change_hour(t: datetime.date, tz: datetime.tzinfo = Mountain) -> int | None:
|
115
|
-
"""Return the hour when DST changes"""
|
116
|
-
lt0 = datetime.datetime(t.year, t.month, t.day, hour=0, tzinfo=tz)
|
117
|
-
dst0 = lt0.dst()
|
118
|
-
for i in (1, 2, 3):
|
119
|
-
lt1 = (lt0.astimezone(datetime.timezone.utc) + HOUR * i).astimezone(tz)
|
120
|
-
dst1 = lt1.dst()
|
121
|
-
lt2 = lt1 - SECOND
|
122
|
-
dst2 = lt2.dst()
|
123
|
-
if dst0 == dst2 and dst0 != dst1:
|
124
|
-
return i - 1
|
125
|
-
return None
|
126
|
-
|
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]:
|
132
|
-
"""Classify DST information for the WWVB phase modulation signal"""
|
133
|
-
if isdst(d, tz):
|
134
|
-
n = _first_sunday_in_month(d.year, 11)
|
135
|
-
for offset in range(-28, 28, 7):
|
136
|
-
d1 = n + datetime.timedelta(days=offset)
|
137
|
-
if _is_dst_change_day(d1, tz):
|
138
|
-
return d1, (offset + 28) // 7
|
139
|
-
else:
|
140
|
-
m = _first_sunday_in_month(d.year + (d.month > 3), 3)
|
141
|
-
for offset in range(0, 52, 7):
|
142
|
-
d1 = m + datetime.timedelta(days=offset)
|
143
|
-
if _is_dst_change_day(d1, tz):
|
144
|
-
return d1, offset // 7
|
145
|
-
|
146
|
-
return None, None
|
147
|
-
|
148
|
-
|
149
|
-
# "Table 8", likely with transcrption errors
|
150
|
-
_dsttable = [
|
151
|
-
[
|
152
|
-
[
|
153
|
-
0b110001,
|
154
|
-
0b100110,
|
155
|
-
0b100101,
|
156
|
-
0b010101,
|
157
|
-
0b111110,
|
158
|
-
0b010110,
|
159
|
-
0b110111,
|
160
|
-
0b111101,
|
161
|
-
],
|
162
|
-
[
|
163
|
-
0b101010,
|
164
|
-
0b011011,
|
165
|
-
0b001110,
|
166
|
-
0b000001,
|
167
|
-
0b000010,
|
168
|
-
0b001000,
|
169
|
-
0b001101,
|
170
|
-
0b101001,
|
171
|
-
],
|
172
|
-
[
|
173
|
-
0b000100,
|
174
|
-
0b100000,
|
175
|
-
0b110100,
|
176
|
-
0b101100,
|
177
|
-
0b111000,
|
178
|
-
0b010000,
|
179
|
-
0b110010,
|
180
|
-
0b011100,
|
181
|
-
],
|
182
|
-
],
|
183
|
-
[
|
184
|
-
[
|
185
|
-
0b110111,
|
186
|
-
0b010101,
|
187
|
-
0b110001,
|
188
|
-
0b010110,
|
189
|
-
0b100110,
|
190
|
-
0b111110,
|
191
|
-
0b100101,
|
192
|
-
0b111101,
|
193
|
-
],
|
194
|
-
[
|
195
|
-
0b001101,
|
196
|
-
0b000001,
|
197
|
-
0b101010,
|
198
|
-
0b001000,
|
199
|
-
0b011011,
|
200
|
-
0b000010,
|
201
|
-
0b001110,
|
202
|
-
0b101001,
|
203
|
-
],
|
204
|
-
[
|
205
|
-
0b110010,
|
206
|
-
0b101100,
|
207
|
-
0b000100,
|
208
|
-
0b010000,
|
209
|
-
0b100000,
|
210
|
-
0b111000,
|
211
|
-
0b110100,
|
212
|
-
0b011100,
|
213
|
-
],
|
214
|
-
],
|
215
|
-
]
|
216
|
-
|
217
|
-
|
218
|
-
def _lfsr_gen(x: list[int]) -> None:
|
219
|
-
"""Generate the next bit of the 6-minute codes sequence"""
|
220
|
-
x.append(x[-7] ^ x[-6] ^ x[-5] ^ x[-2])
|
221
|
-
|
222
|
-
|
223
|
-
_lfsr_seq = [1] * 7
|
224
|
-
while len(_lfsr_seq) < 255:
|
225
|
-
_lfsr_gen(_lfsr_seq)
|
226
|
-
|
227
|
-
# Table 12 - Fixed 106-bit timing word
|
228
|
-
_ftw = [
|
229
|
-
int(c)
|
230
|
-
for c in "1101000111"
|
231
|
-
"0101100101"
|
232
|
-
"1001101110"
|
233
|
-
"0011000010"
|
234
|
-
"1101001110"
|
235
|
-
"1001010100"
|
236
|
-
"0010111000"
|
237
|
-
"1011010110"
|
238
|
-
"1101111111"
|
239
|
-
"1000000100"
|
240
|
-
"100100"
|
241
|
-
]
|
242
|
-
|
243
|
-
|
244
|
-
def _get_dst_next(d: datetime.date, tz: datetime.tzinfo = Mountain) -> int:
|
245
|
-
"""Find the "dst next" value for the phase modulation signal"""
|
246
|
-
dst_now = isdst(d, tz) # dst_on[1]
|
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)
|
249
|
-
|
250
|
-
if dst_midwinter and dst_midsummer:
|
251
|
-
return 0b101111
|
252
|
-
if not (dst_midwinter or dst_midsummer):
|
253
|
-
return 0b000111
|
254
|
-
|
255
|
-
# Southern hemisphere
|
256
|
-
if dst_midwinter or not dst_midsummer:
|
257
|
-
return 0b100011
|
258
|
-
|
259
|
-
dst_change_date, dst_next_row = _get_dst_change_date_and_row(d, tz)
|
260
|
-
if dst_change_date is None:
|
261
|
-
return 0b100011
|
262
|
-
assert dst_next_row is not None
|
263
|
-
|
264
|
-
dst_change_hour = _get_dst_change_hour(dst_change_date, tz)
|
265
|
-
if dst_change_hour is None:
|
266
|
-
return 0b100011
|
267
|
-
|
268
|
-
return _dsttable[dst_now][dst_change_hour][dst_next_row]
|
269
|
-
|
270
|
-
|
271
|
-
_hamming_weight = [
|
272
|
-
[23, 21, 20, 17, 16, 15, 14, 13, 9, 8, 6, 5, 4, 2, 0],
|
273
|
-
[24, 22, 21, 18, 17, 16, 15, 14, 10, 9, 7, 6, 5, 3, 1],
|
274
|
-
[25, 23, 22, 19, 18, 17, 16, 15, 11, 10, 8, 7, 6, 4, 2],
|
275
|
-
[24, 21, 19, 18, 15, 14, 13, 12, 11, 7, 6, 4, 3, 2, 0],
|
276
|
-
[25, 22, 20, 19, 16, 15, 14, 13, 12, 8, 7, 5, 4, 3, 1],
|
277
|
-
]
|
278
|
-
|
279
|
-
# Identifies the phase data as a time signal (SYNC_T bits present)
|
280
|
-
# or a message signal (SYNC_M bits present); No message signals are defined
|
281
|
-
# by NIST at this time.
|
282
|
-
SYNC_T = 0x768
|
283
|
-
SYNC_M = 0x1A3A
|
284
|
-
|
285
|
-
|
286
|
-
def _extract_bit(v: int, p: int) -> bool:
|
287
|
-
"""Extract bit 'p' from integer 'v' as a bool"""
|
288
|
-
return bool((v >> p) & 1)
|
289
|
-
|
290
|
-
|
291
|
-
def _hamming_parity(value: int) -> int:
|
292
|
-
"""Compute the "hamming parity" of a 26-bit number, such as the minute-of-century
|
293
|
-
|
294
|
-
For more details, see Enhanced WWVB Broadcast Format 4.3
|
295
|
-
"""
|
296
|
-
parity = 0
|
297
|
-
for i in range(4, -1, -1):
|
298
|
-
bit = 0
|
299
|
-
for j in range(15):
|
300
|
-
bit ^= _extract_bit(value, _hamming_weight[i][j])
|
301
|
-
parity = (parity << 1) | bit
|
302
|
-
return parity
|
303
|
-
|
304
|
-
|
305
|
-
_dst_ls_lut = [
|
306
|
-
0b01000,
|
307
|
-
0b10101,
|
308
|
-
0b10110,
|
309
|
-
0b00011,
|
310
|
-
0b01000,
|
311
|
-
0b10101,
|
312
|
-
0b10110,
|
313
|
-
0b00011,
|
314
|
-
0b00100,
|
315
|
-
0b01110,
|
316
|
-
0b10000,
|
317
|
-
0b01101,
|
318
|
-
0b11001,
|
319
|
-
0b11100,
|
320
|
-
0b11010,
|
321
|
-
0b11111,
|
322
|
-
]
|
323
|
-
|
324
|
-
|
325
|
-
class _WWVBMinute(NamedTuple):
|
326
|
-
"""Uniquely identifies a minute of time in the WWVB system.
|
327
|
-
|
328
|
-
To use ut1 and ls information from IERS, create a WWVBMinuteIERS value instead.
|
329
|
-
"""
|
330
|
-
|
331
|
-
year: int
|
332
|
-
"""2-digit year within the WWVB epoch"""
|
333
|
-
|
334
|
-
days: int
|
335
|
-
"""1-based day of year"""
|
336
|
-
|
337
|
-
hour: int
|
338
|
-
"""UTC hour of day"""
|
339
|
-
|
340
|
-
min: int
|
341
|
-
"""Minute of hour"""
|
342
|
-
|
343
|
-
dst: int
|
344
|
-
"""2-bit DST code """
|
345
|
-
|
346
|
-
ut1: int
|
347
|
-
"""UT1 offset in units of 100ms, range -900 to +900ms"""
|
348
|
-
|
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
|
-
"""
|
361
|
-
|
362
|
-
epoch: int = 1970
|
363
|
-
|
364
|
-
def __new__( # noqa: PYI034
|
365
|
-
cls,
|
366
|
-
year: int,
|
367
|
-
days: int,
|
368
|
-
hour: int,
|
369
|
-
minute: int,
|
370
|
-
dst: int | None = None,
|
371
|
-
ut1: int | None = None,
|
372
|
-
ls: bool | None = None,
|
373
|
-
ly: bool | None = None,
|
374
|
-
) -> WWVBMinute:
|
375
|
-
"""Construct a WWVBMinute"""
|
376
|
-
if dst is None:
|
377
|
-
dst = cls.get_dst(year, days)
|
378
|
-
if dst not in (0, 1, 2, 3):
|
379
|
-
raise ValueError("dst value should be 0..3")
|
380
|
-
if ut1 is None and ls is None:
|
381
|
-
ut1, ls = cls._get_dut1_info(year, days)
|
382
|
-
elif ut1 is None or ls is None:
|
383
|
-
raise ValueError("sepecify both ut1 and ls or neither one")
|
384
|
-
year = cls.full_year(year)
|
385
|
-
if ly is None:
|
386
|
-
ly = isly(year)
|
387
|
-
return _WWVBMinute.__new__(cls, year, days, hour, minute, dst, ut1, ls, ly)
|
388
|
-
|
389
|
-
@classmethod
|
390
|
-
def full_year(cls, year: int) -> int:
|
391
|
-
"""Convert a (possibly two-digit) year to a full year.
|
392
|
-
|
393
|
-
If the argument is above 100, it is assumed to be a full year.
|
394
|
-
Otherwise, the intuitive method is followed: Say the epoch is 1970,
|
395
|
-
then 70..99 means 1970..99 and 00..69 means 2000..2069.
|
396
|
-
|
397
|
-
To actually use a different epoch, derive a class from WWVBMinute (or
|
398
|
-
WWVBMinuteIERS) and give it a different epoch property. Then, create
|
399
|
-
instances of that class instead of WWVBMinute.
|
400
|
-
"""
|
401
|
-
century = cls.epoch // 100 * 100
|
402
|
-
if year < (cls.epoch % 100):
|
403
|
-
return year + century + 100
|
404
|
-
if year < 100:
|
405
|
-
return year + century
|
406
|
-
return year
|
407
|
-
|
408
|
-
@staticmethod
|
409
|
-
def get_dst(year: int, days: int) -> int:
|
410
|
-
"""Get the 2-bit WWVB DST value for the given day"""
|
411
|
-
d0 = datetime.datetime(year, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(days - 1)
|
412
|
-
d1 = d0 + datetime.timedelta(1)
|
413
|
-
dst0 = isdst(d0)
|
414
|
-
dst1 = isdst(d1)
|
415
|
-
return dst1 * 2 + dst0
|
416
|
-
|
417
|
-
def __str__(self) -> str:
|
418
|
-
"""Implement str()"""
|
419
|
-
return (
|
420
|
-
f"year={self.year:4d} days={self.days:03d} hour={self.hour:02d} "
|
421
|
-
f"min={self.min:02d} dst={self.dst} ut1={self.ut1} ly={int(self.ly)} "
|
422
|
-
f"ls={int(self.ls)}"
|
423
|
-
)
|
424
|
-
|
425
|
-
def as_datetime_utc(self) -> datetime.datetime:
|
426
|
-
"""Convert to a UTC datetime"""
|
427
|
-
d = datetime.datetime(self.year, 1, 1, tzinfo=datetime.timezone.utc)
|
428
|
-
d += datetime.timedelta(self.days - 1, self.hour * 3600 + self.min * 60)
|
429
|
-
return d
|
430
|
-
|
431
|
-
as_datetime = as_datetime_utc
|
432
|
-
|
433
|
-
def as_datetime_local(
|
434
|
-
self,
|
435
|
-
standard_time_offset: int = 7 * 3600,
|
436
|
-
*,
|
437
|
-
dst_observed: bool = True,
|
438
|
-
) -> datetime.datetime:
|
439
|
-
"""Convert to a local datetime according to the DST bits"""
|
440
|
-
u = self.as_datetime_utc()
|
441
|
-
offset = datetime.timedelta(seconds=-standard_time_offset)
|
442
|
-
d = u - datetime.timedelta(seconds=standard_time_offset)
|
443
|
-
if not dst_observed:
|
444
|
-
dst = False
|
445
|
-
elif self.dst == 0b10:
|
446
|
-
transition_time = u.replace(hour=2)
|
447
|
-
dst = d >= transition_time
|
448
|
-
elif self.dst == 0b11:
|
449
|
-
dst = True
|
450
|
-
elif self.dst == 0b01:
|
451
|
-
transition_time = u.replace(hour=1)
|
452
|
-
dst = d < transition_time
|
453
|
-
else: # self.dst == 0b00
|
454
|
-
dst = False
|
455
|
-
if dst:
|
456
|
-
offset += datetime.timedelta(seconds=3600)
|
457
|
-
return u.astimezone(datetime.timezone(offset))
|
458
|
-
|
459
|
-
def _is_end_of_month(self) -> bool:
|
460
|
-
"""Return True if minute is the last minute in a month"""
|
461
|
-
d = self.as_datetime()
|
462
|
-
e = d + datetime.timedelta(1)
|
463
|
-
return d.month != e.month
|
464
|
-
|
465
|
-
def minute_length(self) -> int:
|
466
|
-
"""Return the length of the minute, 60, 61, or (theoretically) 59 seconds"""
|
467
|
-
if not self.ls:
|
468
|
-
return 60
|
469
|
-
if not self._is_end_of_month():
|
470
|
-
return 60
|
471
|
-
if self.hour != 23 or self.min != 59:
|
472
|
-
return 60
|
473
|
-
if self.ut1 > 0:
|
474
|
-
return 59
|
475
|
-
return 61
|
476
|
-
|
477
|
-
def as_timecode(self) -> WWVBTimecode:
|
478
|
-
"""Fill a WWVBTimecode structure representing this minute. Fills both the amplitude and phase codes."""
|
479
|
-
t = WWVBTimecode(self.minute_length())
|
480
|
-
|
481
|
-
self._fill_am_timecode(t)
|
482
|
-
self._fill_pm_timecode(t)
|
483
|
-
|
484
|
-
return t
|
485
|
-
|
486
|
-
@property
|
487
|
-
def _leap_sec(self) -> int:
|
488
|
-
"""Return the 2-bit _leap_sec value used by the PM code"""
|
489
|
-
if not self.ls:
|
490
|
-
return 0
|
491
|
-
if self.ut1 < 0:
|
492
|
-
return 3
|
493
|
-
return 2
|
494
|
-
|
495
|
-
@property
|
496
|
-
def minute_of_century(self) -> int:
|
497
|
-
"""Return the minute of the century"""
|
498
|
-
century = (self.year // 100) * 100
|
499
|
-
# note: This relies on timedelta seconds never including leapseconds!
|
500
|
-
return (
|
501
|
-
int((self.as_datetime() - datetime.datetime(century, 1, 1, tzinfo=datetime.timezone.utc)).total_seconds())
|
502
|
-
// 60
|
503
|
-
)
|
504
|
-
|
505
|
-
def _fill_am_timecode(self, t: WWVBTimecode) -> None:
|
506
|
-
"""Fill the amplitude (AM) portion of a timecode object"""
|
507
|
-
for i in [0, 9, 19, 29, 39, 49]:
|
508
|
-
t.am[i] = AmplitudeModulation.MARK
|
509
|
-
if len(t.am) > 59:
|
510
|
-
t.am[59] = AmplitudeModulation.MARK
|
511
|
-
if len(t.am) > 60:
|
512
|
-
t.am[60] = AmplitudeModulation.MARK
|
513
|
-
for i in [4, 10, 11, 14, 20, 21, 24, 34, 35, 44, 54]:
|
514
|
-
t.am[i] = AmplitudeModulation.ZERO
|
515
|
-
t._put_am_bcd(self.min, 1, 2, 3, 5, 6, 7, 8)
|
516
|
-
t._put_am_bcd(self.hour, 12, 13, 15, 16, 17, 18)
|
517
|
-
t._put_am_bcd(self.days, 22, 23, 25, 26, 27, 28, 30, 31, 32, 33)
|
518
|
-
ut1_sign = self.ut1 >= 0
|
519
|
-
t.am[36] = t.am[38] = AmplitudeModulation(ut1_sign)
|
520
|
-
t.am[37] = AmplitudeModulation(not ut1_sign)
|
521
|
-
t._put_am_bcd(abs(self.ut1) // 100, 40, 41, 42, 43)
|
522
|
-
t._put_am_bcd(self.year, 45, 46, 47, 48, 50, 51, 52, 53) # Implicitly discards all but lowest 2 digits of year
|
523
|
-
t.am[55] = AmplitudeModulation(self.ly)
|
524
|
-
t.am[56] = AmplitudeModulation(self.ls)
|
525
|
-
t._put_am_bcd(self.dst, 57, 58)
|
526
|
-
|
527
|
-
def _fill_pm_timecode_extended(self, t: WWVBTimecode) -> None:
|
528
|
-
"""During minutes 10..15 and 40..45, the amplitude signal holds 'extended information'"""
|
529
|
-
assert 10 <= self.min < 16 or 40 <= self.min < 46
|
530
|
-
minno = self.min % 10
|
531
|
-
assert minno < 6
|
532
|
-
|
533
|
-
dst = self.dst
|
534
|
-
# Note that these are 1 different than Table 11
|
535
|
-
# because our LFSR sequence is zero-based
|
536
|
-
seqno = (self.min // 30) * 2
|
537
|
-
if dst == 0:
|
538
|
-
pass
|
539
|
-
elif dst == 3:
|
540
|
-
seqno = seqno + 1
|
541
|
-
elif dst == 2:
|
542
|
-
if self.hour < 4:
|
543
|
-
pass
|
544
|
-
elif self.hour < 11:
|
545
|
-
seqno = seqno + 90
|
546
|
-
else:
|
547
|
-
seqno = seqno + 1
|
548
|
-
elif self.hour < 4:
|
549
|
-
seqno = seqno + 1
|
550
|
-
elif self.hour < 11:
|
551
|
-
seqno = seqno + 91
|
552
|
-
|
553
|
-
info_seq = _lfsr_seq[seqno : seqno + 127]
|
554
|
-
full_seq = info_seq + _ftw + info_seq[::-1]
|
555
|
-
assert len(full_seq) == 360
|
556
|
-
|
557
|
-
offset = minno * 60
|
558
|
-
for i in range(60):
|
559
|
-
t._put_pm_bit(i, full_seq[i + offset])
|
560
|
-
|
561
|
-
def _fill_pm_timecode_regular(self, t: WWVBTimecode) -> None: # noqa: PLR0915
|
562
|
-
"""Except during minutes 10..15 and 40..45, the amplitude signal holds 'regular information'"""
|
563
|
-
t._put_pm_bin(0, 13, SYNC_T)
|
564
|
-
|
565
|
-
moc = self.minute_of_century
|
566
|
-
_leap_sec = self._leap_sec
|
567
|
-
dst_on = self.dst
|
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))
|
612
|
-
if len(t.phase) > 59:
|
613
|
-
t._put_pm_bit(59, PhaseModulation.ZERO)
|
614
|
-
if len(t.phase) > 60:
|
615
|
-
t._put_pm_bit(60, PhaseModulation.ZERO)
|
616
|
-
|
617
|
-
def _fill_pm_timecode(self, t: WWVBTimecode) -> None:
|
618
|
-
"""Fill the phase portion of a timecode object"""
|
619
|
-
if 10 <= self.min < 16 or 40 <= self.min < 46:
|
620
|
-
self._fill_pm_timecode_extended(t)
|
621
|
-
else:
|
622
|
-
self._fill_pm_timecode_regular(t)
|
623
|
-
|
624
|
-
def next_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
625
|
-
"""Return an object representing the next minute"""
|
626
|
-
d = self.as_datetime() + datetime.timedelta(minutes=1)
|
627
|
-
return self.from_datetime(d, newut1, newls, self)
|
628
|
-
|
629
|
-
def previous_minute(self, newut1: int | None = None, newls: bool | None = None) -> WWVBMinute:
|
630
|
-
"""Return an object representing the previous minute"""
|
631
|
-
d = self.as_datetime() - datetime.timedelta(minutes=1)
|
632
|
-
return self.from_datetime(d, newut1, newls, self)
|
633
|
-
|
634
|
-
@classmethod
|
635
|
-
def _get_dut1_info(cls: type, year: int, days: int, old_time: WWVBMinute | None = None) -> tuple[int, bool]: # noqa: ARG003
|
636
|
-
"""Return the DUT1 information for a given day, possibly propagating information from a previous timestamp"""
|
637
|
-
if old_time is not None:
|
638
|
-
if old_time.minute_length() != 60:
|
639
|
-
newls = False
|
640
|
-
newut1 = old_time.ut1 + 1000 if old_time.ut1 < 0 else old_time.ut1 - 1000
|
641
|
-
else:
|
642
|
-
newls = old_time.ls
|
643
|
-
newut1 = old_time.ut1
|
644
|
-
return newut1, newls
|
645
|
-
return 0, False
|
646
|
-
|
647
|
-
@classmethod
|
648
|
-
def fromstring(cls, s: str) -> WWVBMinute:
|
649
|
-
"""Construct a WWVBMinute from a string representation created by print_timecodes"""
|
650
|
-
s = _removeprefix(s, "WWVB timecode: ")
|
651
|
-
d: dict[str, int] = {}
|
652
|
-
for part in s.split():
|
653
|
-
k, v = part.split("=")
|
654
|
-
if k == "min":
|
655
|
-
k = "minute"
|
656
|
-
d[k] = int(v)
|
657
|
-
year = d.pop("year")
|
658
|
-
days = d.pop("days")
|
659
|
-
hour = d.pop("hour")
|
660
|
-
minute = d.pop("minute")
|
661
|
-
dst: int | None = d.pop("dst", None)
|
662
|
-
ut1: int | None = d.pop("ut1", None)
|
663
|
-
ls = d.pop("ls", None)
|
664
|
-
d.pop("ly", None)
|
665
|
-
if d:
|
666
|
-
raise ValueError(f"Invalid options: {d}")
|
667
|
-
return cls(year, days, hour, minute, dst, ut1, None if ls is None else bool(ls))
|
668
|
-
|
669
|
-
@classmethod
|
670
|
-
def from_datetime(
|
671
|
-
cls,
|
672
|
-
d: datetime.datetime,
|
673
|
-
newut1: int | None = None,
|
674
|
-
newls: bool | None = None,
|
675
|
-
old_time: WWVBMinute | None = None,
|
676
|
-
) -> WWVBMinute:
|
677
|
-
"""Construct a WWVBMinute from a datetime, possibly specifying ut1/ls data or propagating it from an old time"""
|
678
|
-
u = d.utctimetuple()
|
679
|
-
if newls is None and newut1 is None:
|
680
|
-
newut1, newls = cls._get_dut1_info(u.tm_year, u.tm_yday, old_time)
|
681
|
-
return cls(u.tm_year, u.tm_yday, u.tm_hour, u.tm_min, ut1=newut1, ls=newls)
|
682
|
-
|
683
|
-
@classmethod
|
684
|
-
def from_timecode_am(cls, t: WWVBTimecode) -> WWVBMinute | None:
|
685
|
-
"""Construct a WWVBMinute from a WWVBTimecode"""
|
686
|
-
for i in (0, 9, 19, 29, 39, 49, 59):
|
687
|
-
if t.am[i] != AmplitudeModulation.MARK:
|
688
|
-
return None
|
689
|
-
for i in (4, 10, 11, 14, 20, 21, 24, 34, 35, 44, 54):
|
690
|
-
if t.am[i] != AmplitudeModulation.ZERO:
|
691
|
-
return None
|
692
|
-
if t.am[36] == t.am[37]:
|
693
|
-
return None
|
694
|
-
if t.am[36] != t.am[38]:
|
695
|
-
return None
|
696
|
-
minute = t._get_am_bcd(1, 2, 3, 5, 6, 7, 8)
|
697
|
-
if minute is None:
|
698
|
-
return None
|
699
|
-
hour = t._get_am_bcd(12, 13, 15, 16, 17, 18)
|
700
|
-
if hour is None:
|
701
|
-
return None
|
702
|
-
days = t._get_am_bcd(22, 23, 25, 26, 27, 28, 30, 31, 32, 33)
|
703
|
-
if days is None:
|
704
|
-
return None
|
705
|
-
abs_ut1 = t._get_am_bcd(40, 41, 42, 43)
|
706
|
-
if abs_ut1 is None:
|
707
|
-
return None
|
708
|
-
abs_ut1 *= 100
|
709
|
-
ut1_sign = t.am[38]
|
710
|
-
ut1 = abs_ut1 if ut1_sign else -abs_ut1
|
711
|
-
year = t._get_am_bcd(45, 46, 47, 48, 50, 51, 52, 53)
|
712
|
-
if year is None:
|
713
|
-
return None
|
714
|
-
ly = bool(t.am[55])
|
715
|
-
if days > 366 or (not ly and days > 365):
|
716
|
-
return None
|
717
|
-
ls = bool(t.am[56])
|
718
|
-
dst = _require(t._get_am_bcd(57, 58))
|
719
|
-
return cls(year, days, hour, minute, dst, ut1, ls, ly)
|
720
|
-
|
721
|
-
|
722
|
-
class WWVBMinuteIERS(WWVBMinute):
|
723
|
-
"""A WWVBMinute that uses a database of DUT1 information"""
|
724
|
-
|
725
|
-
@classmethod
|
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)
|
728
|
-
return int(round(get_dut1(d) * 10)) * 100, isls(d)
|
729
|
-
|
730
|
-
|
731
|
-
def _bcd_bits(n: int) -> Generator[bool, None, None]:
|
732
|
-
"""Return the bcd representation of n, starting with the least significant bit"""
|
733
|
-
while True:
|
734
|
-
d = n % 10
|
735
|
-
n = n // 10
|
736
|
-
for i in (1, 2, 4, 8):
|
737
|
-
yield bool(d & i)
|
738
|
-
|
739
|
-
|
740
|
-
@enum.unique
|
741
|
-
class AmplitudeModulation(enum.IntEnum):
|
742
|
-
"""Constants that describe an Amplitude Modulation value"""
|
743
|
-
|
744
|
-
ZERO = 0
|
745
|
-
ONE = 1
|
746
|
-
MARK = 2
|
747
|
-
UNSET = -1
|
748
|
-
|
749
|
-
|
750
|
-
@enum.unique
|
751
|
-
class PhaseModulation(enum.IntEnum):
|
752
|
-
"""Constants that describe a Phase Modulation value"""
|
753
|
-
|
754
|
-
ZERO = 0
|
755
|
-
ONE = 1
|
756
|
-
UNSET = -1
|
757
|
-
|
758
|
-
|
759
|
-
class WWVBTimecode:
|
760
|
-
"""Represent the amplitude and/or phase signal, usually over 1 minute"""
|
761
|
-
|
762
|
-
am: list[AmplitudeModulation]
|
763
|
-
"""The amplitude modulation data"""
|
764
|
-
|
765
|
-
phase: list[PhaseModulation]
|
766
|
-
"""The phase modulation data"""
|
767
|
-
|
768
|
-
def __init__(self, sz: int) -> None:
|
769
|
-
"""Construct a WWVB timecode ``sz`` seconds long"""
|
770
|
-
self.am = [AmplitudeModulation.UNSET] * sz
|
771
|
-
self.phase = [PhaseModulation.UNSET] * sz
|
772
|
-
|
773
|
-
def _get_am_bcd(self, *poslist: int) -> int | None:
|
774
|
-
"""Convert AM data to BCD
|
775
|
-
|
776
|
-
The the bits ``self.am[poslist[i]]`` in MSB order are converted from
|
777
|
-
BCD to integer
|
778
|
-
"""
|
779
|
-
pos = reversed(poslist)
|
780
|
-
val = [bool(self.am[p]) for p in pos]
|
781
|
-
result = 0
|
782
|
-
base = 1
|
783
|
-
for i in range(0, len(val), 4):
|
784
|
-
digit = 0
|
785
|
-
for j, b in enumerate(val[i : i + 4]):
|
786
|
-
digit += b << j
|
787
|
-
if digit > 9:
|
788
|
-
return None
|
789
|
-
result += digit * base
|
790
|
-
base *= 10
|
791
|
-
return result
|
792
|
-
|
793
|
-
def _put_am_bcd(self, v: int, *poslist: int) -> None:
|
794
|
-
"""Insert BCD coded data into the AM signal
|
795
|
-
|
796
|
-
The bits at ``self.am[poslist[i]]`` in MSB order are filled with
|
797
|
-
the conversion of `v` to BCD
|
798
|
-
Treating 'poslist' as a sequence of indices, update the AM signal with the value as a BCD number
|
799
|
-
"""
|
800
|
-
pos = list(poslist)[::-1]
|
801
|
-
for p, b in zip(pos, _bcd_bits(v)):
|
802
|
-
if b:
|
803
|
-
self.am[p] = AmplitudeModulation.ONE
|
804
|
-
else:
|
805
|
-
self.am[p] = AmplitudeModulation.ZERO
|
806
|
-
|
807
|
-
def _put_pm_bit(self, i: int, v: PhaseModulation | int | bool) -> None:
|
808
|
-
"""Update a bit of the Phase Modulation signal"""
|
809
|
-
self.phase[i] = PhaseModulation(v)
|
810
|
-
|
811
|
-
def _put_pm_bin(self, st: int, n: int, v: int) -> None:
|
812
|
-
"""Update an n-digit binary number in the Phase Modulation signal"""
|
813
|
-
for i in range(n):
|
814
|
-
self._put_pm_bit(st + i, _extract_bit(v, (n - i - 1)))
|
815
|
-
|
816
|
-
def __str__(self) -> str:
|
817
|
-
"""Implement str()"""
|
818
|
-
undefined = [i for i in range(len(self.am)) if self.am[i] == AmplitudeModulation.UNSET]
|
819
|
-
if undefined:
|
820
|
-
warnings.warn(f"am{undefined} is unset", stacklevel=1)
|
821
|
-
|
822
|
-
def convert_one(am: AmplitudeModulation, phase: PhaseModulation) -> str:
|
823
|
-
if phase is PhaseModulation.UNSET:
|
824
|
-
return ("0", "1", "2", "?")[am]
|
825
|
-
if phase:
|
826
|
-
return ("⁰", "¹", "²", "¿")[am]
|
827
|
-
return ("₀", "₁", "₂", "⸮")[am]
|
828
|
-
|
829
|
-
return "".join(convert_one(i, j) for i, j in zip(self.am, self.phase))
|
830
|
-
|
831
|
-
def __repr__(self) -> str:
|
832
|
-
"""Implement repr()"""
|
833
|
-
return "<WWVBTimecode " + str(self) + ">"
|
834
|
-
|
835
|
-
def to_am_string(self, charset: list[str]) -> str:
|
836
|
-
"""Convert the amplitude signal to a string"""
|
837
|
-
return "".join(charset[i] for i in self.am)
|
838
|
-
|
839
|
-
to_string = to_am_string
|
840
|
-
|
841
|
-
def to_pm_string(self, charset: list[str]) -> str:
|
842
|
-
"""Convert the phase signal to a string"""
|
843
|
-
return "".join(charset[i] for i in self.phase)
|
844
|
-
|
845
|
-
def to_both_string(self, charset: list[str]) -> str:
|
846
|
-
"""Convert both channels to a string"""
|
847
|
-
return "".join(charset[i + j * 3] for i, j in zip(self.am, self.phase))
|
848
|
-
|
849
|
-
|
850
|
-
styles = {
|
851
|
-
"default": ["0", "1", "2"],
|
852
|
-
"duration": ["2", "5", "8"],
|
853
|
-
"cradek": ["0", "1", "-"],
|
854
|
-
"bar": ["🬍🬎", "🬋🬎", "🬋🬍"],
|
855
|
-
"sextant": ["🬍🬎", "🬋🬎", "🬋🬍", "🬩🬹", "🬋🬹", "🬋🬩"],
|
856
|
-
}
|
857
|
-
|
858
|
-
|
859
|
-
def print_timecodes(
|
860
|
-
w: WWVBMinute,
|
861
|
-
minutes: int,
|
862
|
-
channel: str,
|
863
|
-
style: str,
|
864
|
-
file: TextIO,
|
865
|
-
*,
|
866
|
-
all_timecodes: bool = False,
|
867
|
-
) -> None:
|
868
|
-
"""Print a range of timecodes with a header. This header is in a format understood by WWVBMinute.fromstring"""
|
869
|
-
channel_text = "" if channel == "amplitude" else f" --channel={channel}"
|
870
|
-
style_text = "" if style == "default" else f" --style={style}"
|
871
|
-
style_chars = styles.get(style, ["0", "1", "2"])
|
872
|
-
first = True
|
873
|
-
for _ in range(minutes):
|
874
|
-
if first or all_timecodes:
|
875
|
-
if not first:
|
876
|
-
print(file=file)
|
877
|
-
print(f"WWVB timecode: {w!s}{channel_text}{style_text}", file=file)
|
878
|
-
first = False
|
879
|
-
pfx = f"{w.year:04d}-{w.days:03d} {w.hour:02d}:{w.min:02d} "
|
880
|
-
tc = w.as_timecode()
|
881
|
-
if len(style_chars) == 6:
|
882
|
-
print(f"{pfx} {tc.to_both_string(style_chars)}", file=file)
|
883
|
-
print(file=file)
|
884
|
-
pfx = " " * len(pfx)
|
885
|
-
else:
|
886
|
-
if channel in ("amplitude", "both"):
|
887
|
-
print(f"{pfx} {tc.to_am_string(style_chars)}", file=file)
|
888
|
-
pfx = " " * len(pfx)
|
889
|
-
if channel in ("phase", "both"):
|
890
|
-
print(f"{pfx} {tc.to_pm_string(style_chars)}", file=file)
|
891
|
-
if channel == "both":
|
892
|
-
print(file=file)
|
893
|
-
w = w.next_minute()
|
894
|
-
|
895
|
-
|
896
|
-
def print_timecodes_json(
|
897
|
-
w: WWVBMinute,
|
898
|
-
minutes: int,
|
899
|
-
channel: str,
|
900
|
-
file: TextIO,
|
901
|
-
) -> None:
|
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
|
-
"""
|
917
|
-
result = []
|
918
|
-
for _ in range(minutes):
|
919
|
-
data: dict[str, Any] = {
|
920
|
-
"year": w.year,
|
921
|
-
"days": w.days,
|
922
|
-
"hour": w.hour,
|
923
|
-
"minute": w.min,
|
924
|
-
}
|
925
|
-
|
926
|
-
tc = w.as_timecode()
|
927
|
-
if channel in ("amplitude", "both"):
|
928
|
-
data["amplitude"] = tc.to_am_string(["0", "1", "2"])
|
929
|
-
if channel in ("phase", "both"):
|
930
|
-
data["phase"] = tc.to_pm_string(["0", "1"])
|
931
|
-
|
932
|
-
result.append(data)
|
933
|
-
w = w.next_minute()
|
934
|
-
json.dump(result, file)
|
935
|
-
print(file=file)
|