PyFT8 3.1.0__tar.gz → 3.3.1__tar.gz
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.
- {pyft8-3.1.0 → pyft8-3.3.1}/PKG-INFO +5 -3
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8/databases.py +47 -56
- pyft8-3.3.1/PyFT8/gui.py +310 -0
- pyft8-3.3.1/PyFT8/osd.py +88 -0
- pyft8-3.3.1/PyFT8/pyft8.py +219 -0
- pyft8-3.3.1/PyFT8/qso_manager.py +119 -0
- pyft8-3.3.1/PyFT8/receiver.py +613 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8/time_utils.py +20 -7
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8/transmitter.py +61 -55
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8.egg-info/PKG-INFO +5 -3
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8.egg-info/SOURCES.txt +14 -5
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8.egg-info/requires.txt +3 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/README.md +1 -2
- {pyft8-3.1.0 → pyft8-3.3.1}/pyproject.toml +5 -2
- pyft8-3.3.1/tests/misc/animation.py +36 -0
- pyft8-3.3.1/tests/misc/bits_loopback.py +204 -0
- pyft8-3.3.1/tests/misc/bits_loopback_short.py +169 -0
- pyft8-3.3.1/tests/misc/symbols_loopback.py +81 -0
- {pyft8-3.1.0/tests/dev → pyft8-3.3.1/tests/misc}/test_loopback_performance.py +2 -10
- pyft8-3.3.1/tests/pipeline/Fine tuning compare IOS_PC.py +373 -0
- pyft8-3.3.1/tests/pipeline/Targetted tests.py +396 -0
- pyft8-3.3.1/tests/pipeline/test_01.wav +0 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/tests/plot_baseline.py +10 -9
- pyft8-3.3.1/tests/spare.py +52 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/tests/test_batch_and_live.py +60 -49
- pyft8-3.1.0/PyFT8/gui.py +0 -227
- pyft8-3.1.0/PyFT8/pyft8.py +0 -404
- pyft8-3.1.0/PyFT8/receiver.py +0 -422
- pyft8-3.1.0/tests/spare.py +0 -18
- {pyft8-3.1.0 → pyft8-3.3.1}/LICENSE +0 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/MANIFEST.in +0 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8/__init__.py +0 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8/pskreporter.py +0 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8/rigctrl.py +0 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8.egg-info/dependency_links.txt +0 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8.egg-info/entry_points.txt +0 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/PyFT8.egg-info/top_level.txt +0 -0
- {pyft8-3.1.0 → pyft8-3.3.1}/setup.cfg +0 -0
- {pyft8-3.1.0/tests/dev → pyft8-3.3.1/tests/misc}/CQ AAAA.py +0 -0
- {pyft8-3.1.0/tests/dev → pyft8-3.3.1/tests/misc}/osd.py +0 -0
- {pyft8-3.1.0/tests/dev → pyft8-3.3.1/tests/misc}/test_generate_wav.py +0 -0
- {pyft8-3.1.0/tests/dev → pyft8-3.3.1/tests/misc}/view_worked_before.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyFT8
|
|
3
|
-
Version: 3.1
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: FT8 Decoding and Encoding in Python with test/loopback code
|
|
5
5
|
Author-email: G1OJS <g1ojs@yahoo.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -15,6 +15,9 @@ License-File: LICENSE
|
|
|
15
15
|
Requires-Dist: numpy>=1.24.0
|
|
16
16
|
Requires-Dist: matplotlib
|
|
17
17
|
Requires-Dist: pyaudio
|
|
18
|
+
Requires-Dist: pyserial
|
|
19
|
+
Requires-Dist: paho-mqtt
|
|
20
|
+
Requires-Dist: psutil
|
|
18
21
|
Dynamic: license-file
|
|
19
22
|
|
|
20
23
|
# PyFT8 [](https://pepy.tech/projects/pyft8) [](https://pepy.tech/projects/pyft8) [PyPI link](https://pypi.org/project/PyFT8/)
|
|
@@ -112,8 +115,7 @@ upload = Y
|
|
|
112
115
|
|
|
113
116
|
The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in NORM mode, and FT8_lib, using the same 10 minutes of busy 20m audio that is used to test ft8_lib.
|
|
114
117
|
|
|
115
|
-
<img width="
|
|
116
|
-
|
|
118
|
+
<img width="900" height="898" alt="batch test performance" src="https://github.com/user-attachments/assets/513d4121-5483-4e7b-81df-785dc4db3d37" />
|
|
117
119
|
|
|
118
120
|
## Limitations
|
|
119
121
|
PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import threading, os, pickle, json, time
|
|
1
2
|
from PyFT8.pskreporter import PSKR_MQTT_listener
|
|
2
|
-
|
|
3
|
+
from PyFT8.time_utils import time_utils
|
|
3
4
|
|
|
4
5
|
call_hashes, hashes_for_calls = {}, {}
|
|
5
6
|
|
|
@@ -56,9 +57,9 @@ class DiskDict:
|
|
|
56
57
|
threading.Thread(target = self._autosave, args=(autosave_t0,), daemon = True).start()
|
|
57
58
|
|
|
58
59
|
def _autosave(self, autosave_t0, autosave_period = 15):
|
|
59
|
-
|
|
60
|
+
time_utils.sleep(autosave_t0)
|
|
60
61
|
while True:
|
|
61
|
-
|
|
62
|
+
time_utils.sleep(autosave_period)
|
|
62
63
|
self.save()
|
|
63
64
|
|
|
64
65
|
def load(self):
|
|
@@ -84,38 +85,31 @@ class DiskDict:
|
|
|
84
85
|
return
|
|
85
86
|
|
|
86
87
|
class History:
|
|
87
|
-
def __init__(self, config_folder, my_call, home_square, pskr_refresh_mins
|
|
88
|
+
def __init__(self, config_folder, my_call, home_square, pskr_refresh_mins):
|
|
88
89
|
self.pskr_refresh_mins = pskr_refresh_mins
|
|
89
|
-
self.
|
|
90
|
+
self.log_cache = None
|
|
90
91
|
self.my_call = my_call
|
|
91
92
|
self.home_square = home_square
|
|
92
93
|
self.home_square_lev4 = home_square[:4]
|
|
93
94
|
self.freqs_to_bands = {}
|
|
94
95
|
self.dist_brg_cache = {}
|
|
95
|
-
self.hearing_me = DiskDict(f"{self.config_folder}/hearing_me.json", 3) # all-time record of hearing me
|
|
96
|
-
self.heard_by_me = DiskDict(f"{self.config_folder}/heard_by_me.json", 5) # all-time record of heard by me
|
|
97
96
|
self.hearing_me_new = {}
|
|
98
97
|
self.heard_by_me_new = {}
|
|
99
|
-
self.call_to_grid = DiskDict(f"{self.config_folder}/call_to_grid.json", 7) # all time cache call -> fine locator
|
|
100
|
-
self.band_TxRx_homecall_recent_l4grid = DiskDict(f"{self.config_folder}/recent_l4grid.json", 9) # last 20 mins data -> per band tx/rx & current band detail
|
|
101
98
|
self.home_activity = {}
|
|
102
99
|
self.home_most_remotes = {}
|
|
103
100
|
self.lock = threading.Lock()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
else:
|
|
110
|
-
mqtt = PSKR_MQTT_listener(self.home_square_lev4, self.add_mqtt_spot)
|
|
111
|
-
threading.Thread(target = self.count_activity, daemon = True).start()
|
|
101
|
+
self.all_file = f"{config_folder}/ALL.txt"
|
|
102
|
+
self.hearing_me = DiskDict(f"{config_folder}/hearing_me.json", 3) # all-time record of hearing me
|
|
103
|
+
self.heard_by_me = DiskDict(f"{config_folder}/heard_by_me.json", 5) # all-time record of heard by me
|
|
104
|
+
self.call_to_grid = DiskDict(f"{config_folder}/call_to_grid.json", 7) # all time cache call -> fine locator
|
|
105
|
+
self.band_TxRx_homecall_recent_L4grid = DiskDict(f"{config_folder}/recent_l4grid.json", 9) # last 20 mins data -> per band tx/rx & current band detail
|
|
112
106
|
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
107
|
+
def start_collect_new(self):
|
|
108
|
+
mqtt = PSKR_MQTT_listener(self.home_square_lev4, self.add_mqtt_spot)
|
|
109
|
+
threading.Thread(target = self.count_activity, daemon = True).start()
|
|
110
|
+
|
|
111
|
+
def incorporate_log_data(self, log_cache):
|
|
112
|
+
self.log_cache = log_cache
|
|
119
113
|
for key in log_cache:
|
|
120
114
|
key_parts = key.split('_')
|
|
121
115
|
if len(key_parts) > 1:
|
|
@@ -127,9 +121,13 @@ class History:
|
|
|
127
121
|
else:
|
|
128
122
|
print(m)
|
|
129
123
|
|
|
130
|
-
def load_hearing_heard_from_all_file(self,
|
|
131
|
-
recs = self.parse_all_txt(
|
|
132
|
-
if not any(recs):
|
|
124
|
+
def load_hearing_heard_from_all_file(self, bands):
|
|
125
|
+
recs = self.parse_all_txt()
|
|
126
|
+
if not any(recs):
|
|
127
|
+
return
|
|
128
|
+
for b in bands:
|
|
129
|
+
f = float(bands[b])
|
|
130
|
+
self.freqs_to_bands[round(f,1)] = b
|
|
133
131
|
for r in recs:
|
|
134
132
|
if r['md'] == 'FT8':
|
|
135
133
|
band = self.freqs_to_bands.get(round(r['fMHz'], 1), None)
|
|
@@ -140,10 +138,10 @@ class History:
|
|
|
140
138
|
data = self.heard_by_me.data if TxRx == 'Rx' else self.hearing_me.data
|
|
141
139
|
self.add_myspots_record(data, None, band, call, 0, 0)
|
|
142
140
|
|
|
143
|
-
def parse_all_txt(self
|
|
141
|
+
def parse_all_txt(self):
|
|
144
142
|
rows, recs = None, []
|
|
145
|
-
if os.path.exists(all_file):
|
|
146
|
-
with open(all_file, 'r') as f:
|
|
143
|
+
if os.path.exists(self.all_file):
|
|
144
|
+
with open(self.all_file, 'r') as f:
|
|
147
145
|
rows = f.readlines()
|
|
148
146
|
if rows is not None:
|
|
149
147
|
for r in rows:
|
|
@@ -157,14 +155,13 @@ class History:
|
|
|
157
155
|
return recs
|
|
158
156
|
|
|
159
157
|
def write_all_txt_row(self, cyclestart_string, fMHz, TxRx, mode, snr, dt, fHz, msg):
|
|
160
|
-
|
|
161
|
-
filemode = 'w' if not os.path.exists(all_file) else 'a'
|
|
158
|
+
filemode = 'w' if not os.path.exists(self.all_file) else 'a'
|
|
162
159
|
row = f"{cyclestart_string} {fMHz:8.3f} {TxRx} {mode} {snr:+03d} {dt:4.1f} {fHz:4.0f} {msg}"
|
|
163
|
-
with open(all_file, filemode) as f:
|
|
160
|
+
with open(self.all_file, filemode) as f:
|
|
164
161
|
f.write(f"{row}\n")
|
|
165
162
|
|
|
166
163
|
def add_mqtt_spot(self, d):
|
|
167
|
-
tnow = int(
|
|
164
|
+
tnow = int(time_utils.time())
|
|
168
165
|
sc, rc = (d['sc'], d['sl']), (d['rc'], d['rl'])
|
|
169
166
|
for iTxRx, call_grid in enumerate([sc, rc]):
|
|
170
167
|
call, grid = call_grid
|
|
@@ -183,8 +180,8 @@ class History:
|
|
|
183
180
|
self.call_to_grid.data[call] = grid
|
|
184
181
|
|
|
185
182
|
def add_homespots_record(self, key, t):
|
|
186
|
-
self.
|
|
187
|
-
self.
|
|
183
|
+
self.band_TxRx_homecall_recent_L4grid.data.setdefault(key, [])
|
|
184
|
+
self.band_TxRx_homecall_recent_L4grid.data[key].append(int(t))
|
|
188
185
|
|
|
189
186
|
def add_myspots_record(self, historic_data, new_alert_data, band, call, t, rp):
|
|
190
187
|
self._update_new_alert(band, call, historic_data, new_alert_data)
|
|
@@ -210,7 +207,7 @@ class History:
|
|
|
210
207
|
result = call in new_alert_data[band]
|
|
211
208
|
return result
|
|
212
209
|
|
|
213
|
-
def is_hearing_me(self, band, call, since_seconds):
|
|
210
|
+
def is_hearing_me(self, band, call, since_seconds = 120):
|
|
214
211
|
result = False
|
|
215
212
|
if band in self.hearing_me.data:
|
|
216
213
|
if call in self.hearing_me.data[band]:
|
|
@@ -220,7 +217,7 @@ class History:
|
|
|
220
217
|
def count_activity(self):
|
|
221
218
|
import numpy as np
|
|
222
219
|
while True:
|
|
223
|
-
|
|
220
|
+
time_utils.sleep(5)
|
|
224
221
|
self.home_activity = {}
|
|
225
222
|
self.home_most_remotes = {}
|
|
226
223
|
with self.lock:
|
|
@@ -231,29 +228,29 @@ class History:
|
|
|
231
228
|
self.home_most_remotes[b] = [('Nobody',0), ('Nobody',0)]
|
|
232
229
|
|
|
233
230
|
# keep only the remote spots that happened in the self.pskr_refresh_mins window
|
|
234
|
-
for band_TxRx_homecall in self.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
self.
|
|
231
|
+
for band_TxRx_homecall in self.band_TxRx_homecall_recent_L4grid.data:
|
|
232
|
+
band_TxRx_homecall_recent_L4grid = self.band_TxRx_homecall_recent_L4grid.data[band_TxRx_homecall]
|
|
233
|
+
band_TxRx_homecall_recent_L4grid = [t for t in band_TxRx_homecall_recent_L4grid if (time_utils.time() - t) < 60*self.pskr_refresh_mins]
|
|
234
|
+
self.band_TxRx_homecall_recent_L4grid.data[band_TxRx_homecall] = band_TxRx_homecall_recent_L4grid
|
|
238
235
|
|
|
239
236
|
# count number of local Tx and Rx, and identify the local Tx and Rx with most remote spots
|
|
240
|
-
for band_TxRx_homecall in self.
|
|
241
|
-
|
|
242
|
-
if len(
|
|
237
|
+
for band_TxRx_homecall in self.band_TxRx_homecall_recent_L4grid.data:
|
|
238
|
+
band_TxRx_homecall_recent_L4grid = self.band_TxRx_homecall_recent_L4grid.data[band_TxRx_homecall]
|
|
239
|
+
if len(band_TxRx_homecall_recent_L4grid):
|
|
243
240
|
b, iTxRx, c = band_TxRx_homecall.split('_')
|
|
244
241
|
iTxRx = int(iTxRx)
|
|
245
242
|
self.home_activity.setdefault(b, [0, 0])
|
|
246
243
|
self.home_activity[b][iTxRx] +=1
|
|
247
244
|
self.home_most_remotes.setdefault(b, [('Nobody',0), ('Nobody',0)])
|
|
248
|
-
nremotes = len(
|
|
245
|
+
nremotes = len(band_TxRx_homecall_recent_L4grid)
|
|
249
246
|
current_winner = self.home_most_remotes[b][iTxRx]
|
|
250
247
|
if nremotes > current_winner[1]:
|
|
251
248
|
if c != self.my_call:
|
|
252
249
|
self.home_most_remotes[b][iTxRx] = (c, nremotes)
|
|
253
250
|
|
|
254
251
|
def get_spot_counts(self, band, call):
|
|
255
|
-
tx_reports = self.
|
|
256
|
-
rx_reports = self.
|
|
252
|
+
tx_reports = self.band_TxRx_homecall_recent_L4grid.data.get(f"{band}_0_{call}", [])
|
|
253
|
+
rx_reports = self.band_TxRx_homecall_recent_L4grid.data.get(f"{band}_1_{call}", [])
|
|
257
254
|
n_spotting = len(tx_reports) if tx_reports else 0
|
|
258
255
|
n_spotted = len(rx_reports) if rx_reports else 0
|
|
259
256
|
return n_spotted, n_spotting
|
|
@@ -285,20 +282,14 @@ class ADIF:
|
|
|
285
282
|
ensure_file_exists(self.adif_log_file, header = "header <eoh>\n")
|
|
286
283
|
self.cache = self._build_cache()
|
|
287
284
|
|
|
288
|
-
def log(self,
|
|
289
|
-
log_dict = {'call':oStation['c'], 'gridsquare':oStation['g'], 'mode':'FT8',
|
|
290
|
-
'operator':mStation['c'], 'station_callsign':mStation['c'], 'my_gridsquare':mStation['g'],
|
|
291
|
-
'rst_sent':rpts['sent'], 'rst_rcvd':rpts['rcvd'],
|
|
292
|
-
'qso_date':time.strftime("%Y%m%d", times['time_on']), 'qso_date_off':time.strftime("%Y%m%d", times['time_off']),
|
|
293
|
-
'time_on':time.strftime("%H%M%S", times['time_on']), 'time_off':time.strftime("%H%M%S", times['time_on']),
|
|
294
|
-
'band':band_info['b'], 'freq':band_info['fMHz']}
|
|
285
|
+
def log(self, log_dict):
|
|
295
286
|
with open(self.adif_log_file,'a') as f:
|
|
296
287
|
for k, v in log_dict.items():
|
|
297
288
|
v = str(v)
|
|
298
289
|
f.write(f"<{k}:{len(v)}>{v} ")
|
|
299
290
|
f.write(f"<eor>\n")
|
|
300
291
|
cbm = log_dict['call'] + "_" + log_dict['band'] + "_FT8"
|
|
301
|
-
tm =
|
|
292
|
+
tm = time_utils.time()
|
|
302
293
|
self.cache[log_dict['call']] = tm
|
|
303
294
|
self.cache[cbm] = tm
|
|
304
295
|
|
|
@@ -318,7 +309,7 @@ class ADIF:
|
|
|
318
309
|
with open(self.adif_log_file, 'r') as f:
|
|
319
310
|
for l in f.readlines():
|
|
320
311
|
if parse(l, 'mode') == "FT8":
|
|
321
|
-
c, b, d, t = parse(l, 'call'), parse(l, 'band'), parse(l, 'qso_date'), parse(l, 'time_on')
|
|
312
|
+
c, b, d, t = parse(l, 'call:'), parse(l, 'band'), parse(l, 'qso_date'), parse(l, 'time_on')
|
|
322
313
|
if c and b and d and t:
|
|
323
314
|
time_tuple = time.strptime(d+t, "%Y%m%d%H%M%S")
|
|
324
315
|
tm = calendar.timegm(time_tuple)
|
pyft8-3.3.1/PyFT8/gui.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from matplotlib import rcParams
|
|
4
|
+
import queue
|
|
5
|
+
from matplotlib.animation import FuncAnimation
|
|
6
|
+
from matplotlib.widgets import Slider, Button
|
|
7
|
+
from PyFT8.time_utils import time_utils
|
|
8
|
+
|
|
9
|
+
rcParams['toolbar'] = 'None'
|
|
10
|
+
MAIN_TEXT_COLOR = '#f0f9fa'
|
|
11
|
+
TEXT_BACKGROUND_COLOR = '#2a2b2b'
|
|
12
|
+
INFO_TEXT_COLOR = 'white'
|
|
13
|
+
BUTTONCOLOR = 'grey'
|
|
14
|
+
HOVERCOLOR = 'darkgreen'
|
|
15
|
+
ACTIVE_BUTTON_COLOR = 'cyan'
|
|
16
|
+
INACTIVE_BUTTON_COLOR = '#edeef0'
|
|
17
|
+
MAX_FONT_SIZE_MAIN = 10
|
|
18
|
+
L = {'pmargin':0.04, 'sidebar_width': 0.17, 'banner_height':0.1, 'vsep1':0.01, 'hsep1':0.02}
|
|
19
|
+
|
|
20
|
+
# ================== WATERFALL ======================================================
|
|
21
|
+
|
|
22
|
+
class Scrollbox:
|
|
23
|
+
def __init__(self, fig, box, nlines = 5, monospace = False, default_text = '', fontsize = None):
|
|
24
|
+
self.fig = fig
|
|
25
|
+
self.ax = fig.add_axes(box)
|
|
26
|
+
self.default_text = default_text
|
|
27
|
+
bbox = self.ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
|
|
28
|
+
self.fontsize = np.min([0.5 * bbox.height * fig.dpi / nlines, MAX_FONT_SIZE_MAIN]) if fontsize is None else fontsize
|
|
29
|
+
self.nlines = nlines
|
|
30
|
+
self.line_height = 0.9 / nlines
|
|
31
|
+
self.lines = []
|
|
32
|
+
self.lineartists = []
|
|
33
|
+
for i in range(self.nlines):
|
|
34
|
+
self.lineartists.append(self.ax.text(0.03,1 - self.line_height * (i+1),
|
|
35
|
+
'', color = MAIN_TEXT_COLOR, fontsize = self.fontsize, clip_on = True))
|
|
36
|
+
if monospace:
|
|
37
|
+
self.lineartists[-1].set_fontfamily('monospace')
|
|
38
|
+
self.ax.set_xticks([])
|
|
39
|
+
self.ax.set_yticks([])
|
|
40
|
+
self.ax.set_facecolor(TEXT_BACKGROUND_COLOR)
|
|
41
|
+
|
|
42
|
+
def scroll_print(self, text, color = MAIN_TEXT_COLOR):
|
|
43
|
+
self.lines = self.lines[-(self.nlines-1):]
|
|
44
|
+
self.lines.append({'text':text, 'color':color})
|
|
45
|
+
for i, line in enumerate(self.lines):
|
|
46
|
+
self.lineartists[i].set_text(line['text'])
|
|
47
|
+
self.lineartists[i].set_color(line['color'])
|
|
48
|
+
|
|
49
|
+
def clear(self):
|
|
50
|
+
self.lines = []
|
|
51
|
+
for i in range(self.nlines):
|
|
52
|
+
self.lineartists[i].set_text(self.default_text)
|
|
53
|
+
|
|
54
|
+
def list_print(self, lst, colors = None):
|
|
55
|
+
for i, txt in enumerate(lst[:self.nlines]):
|
|
56
|
+
if txt != self.lineartists[i].get_text():
|
|
57
|
+
self.lineartists[i].set_text(txt)
|
|
58
|
+
col = colors[i] if colors is not None and i < len(colors) else 'white'
|
|
59
|
+
self.lineartists[i].set_color(col)
|
|
60
|
+
for i in range(len(lst), self.nlines):
|
|
61
|
+
if self.lineartists[i].get_text() != '':
|
|
62
|
+
self.lineartists[i].set_text('')
|
|
63
|
+
|
|
64
|
+
class ButtonBox:
|
|
65
|
+
def __init__(self, fig, box, btn_pc = 30, onclick = None, clickargs=None, btn_label = ''):
|
|
66
|
+
btnbox, infobox = box.copy(), box.copy()
|
|
67
|
+
btnbox[2] = box[2] * btn_pc /100
|
|
68
|
+
infobox[2] = box[2] * (100-btn_pc) /100
|
|
69
|
+
infobox[0] = box[0] + box[2] * (btn_pc /100)
|
|
70
|
+
self.btn_axs = fig.add_axes(btnbox)
|
|
71
|
+
self.info_axs = fig.add_axes(infobox)
|
|
72
|
+
self.info_axs.set_xticks([])
|
|
73
|
+
self.info_axs.set_yticks([])
|
|
74
|
+
self.info_axs.set_facecolor(TEXT_BACKGROUND_COLOR)
|
|
75
|
+
self.label2 = self.info_axs.text(0.03, 0.5, '', color = INFO_TEXT_COLOR, verticalalignment = 'center', clip_on = True)
|
|
76
|
+
self.btn_widg = Button(self.btn_axs, btn_label, color = BUTTONCOLOR, hovercolor = HOVERCOLOR)
|
|
77
|
+
self.label = self.btn_widg.label
|
|
78
|
+
self.label.set_color(MAIN_TEXT_COLOR)
|
|
79
|
+
self.clickargs = clickargs
|
|
80
|
+
self.active = False
|
|
81
|
+
self.btn_widg.on_clicked(lambda x: onclick(clickargs))
|
|
82
|
+
|
|
83
|
+
def set_active(self, active: bool):
|
|
84
|
+
if self.active != active:
|
|
85
|
+
self.active = active
|
|
86
|
+
color = ACTIVE_BUTTON_COLOR if self.active else INACTIVE_BUTTON_COLOR
|
|
87
|
+
self.label.set_color(color)
|
|
88
|
+
self.label2.set_color(color)
|
|
89
|
+
|
|
90
|
+
def set_info_text(self, text):
|
|
91
|
+
if text != self.label2:
|
|
92
|
+
self.label2.set_text(text)
|
|
93
|
+
|
|
94
|
+
MESSAGE_TYPES = {'generic':{'bg':'blue', 'fg':'white'}, 'CQ':{'bg':'green', 'fg':'white'},'from_me': {'bg':'yellow', 'fg':'white'}, 'to_me':{'bg':'red', 'fg':'white'}}
|
|
95
|
+
class Msg_box:
|
|
96
|
+
def __init__(self, fig, ax, w, h, onclick):
|
|
97
|
+
from matplotlib.patches import Rectangle
|
|
98
|
+
self.onclick = onclick
|
|
99
|
+
self.fig, self.ax = fig, ax
|
|
100
|
+
rect = Rectangle((0, 0), w, h, alpha=0.5, edgecolor='lime', lw=1)
|
|
101
|
+
self.patch = self.ax.add_patch(rect)
|
|
102
|
+
self.text_inst = self.ax.text(0, 0, '', fontsize='small', fontweight = 'bold' )
|
|
103
|
+
self.cid = fig.canvas.mpl_connect('button_press_event', self._onclick)
|
|
104
|
+
self.visible = False
|
|
105
|
+
|
|
106
|
+
def set_properties(self, x, y, message):
|
|
107
|
+
self.patch.set_xy((x, y))
|
|
108
|
+
self.text_inst.set_position((x, y+1))
|
|
109
|
+
self.text_inst.set_text(message['display_text'])
|
|
110
|
+
message_type_params = MESSAGE_TYPES[message['message_type']]
|
|
111
|
+
self.text_inst.set_color(message_type_params['fg'])
|
|
112
|
+
self.patch.set_facecolor(message_type_params['bg'])
|
|
113
|
+
self.message = message
|
|
114
|
+
self.cycle = message['origin']['odd_even']
|
|
115
|
+
self.patch.set_visible(True)
|
|
116
|
+
self.text_inst.set_visible(True)
|
|
117
|
+
self.visible = True
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def hide(self):
|
|
121
|
+
self.patch.set_visible(False)
|
|
122
|
+
self.text_inst.set_visible(False)
|
|
123
|
+
self.visible = False
|
|
124
|
+
|
|
125
|
+
def _onclick(self, event):
|
|
126
|
+
if self.visible:
|
|
127
|
+
b, _ = self.patch.contains(event)
|
|
128
|
+
if(b):
|
|
129
|
+
self.onclick({'action': 'MESSAGE_CLICK', 'message':self.message})
|
|
130
|
+
|
|
131
|
+
class Gui:
|
|
132
|
+
def __init__(self, config, rig, history, console_print, wf_data, hearing_me_since_mins = 5):
|
|
133
|
+
self.rig = rig
|
|
134
|
+
self.on_click = None
|
|
135
|
+
self.plt = plt
|
|
136
|
+
self.fig = plt.figure(figsize = (10,10), facecolor=(.18, .71, .71, 0.4))
|
|
137
|
+
self.fig.canvas.manager.set_window_title('PyFT8 by G1OJS')
|
|
138
|
+
self.config, self.history = config, history
|
|
139
|
+
self.console_print = console_print
|
|
140
|
+
self.wf_top = 1-L['pmargin']-L['banner_height']-L['vsep1']
|
|
141
|
+
self.wf_left = L['pmargin']+L['sidebar_width']+L['hsep1']
|
|
142
|
+
self.hearing_me_since_mins = hearing_me_since_mins
|
|
143
|
+
self.wf_data = wf_data
|
|
144
|
+
self.sidebars_last_update = 0
|
|
145
|
+
self.sidebars_page = 0
|
|
146
|
+
self._make_axes()
|
|
147
|
+
self.gui_queue = queue.Queue()
|
|
148
|
+
self.active_msg_boxes = []
|
|
149
|
+
self.inactive_msg_boxes = []
|
|
150
|
+
self.image = self.ax_wf.imshow(self.wf_data['data'],vmax=120,vmin=90,origin='lower',interpolation='none', aspect = 'auto')
|
|
151
|
+
self._make_buttons()
|
|
152
|
+
self.tlast = 0
|
|
153
|
+
self.band_info = {'current_band': None, 'fMHz':0, 'time_set':0}
|
|
154
|
+
self.sidebars_artists = [*[bb.label for bb in self.button_boxes], *[bb.label2 for bb in self.button_boxes],
|
|
155
|
+
*self.band_stats.lineartists, *self.console.lineartists, *self.hm.lineartists]
|
|
156
|
+
|
|
157
|
+
self.ani = FuncAnimation(self.fig, self._animate, interval = 100, frames=(100000), blit = False)
|
|
158
|
+
|
|
159
|
+
def register_onclick(self, on_click):
|
|
160
|
+
self.on_click = on_click
|
|
161
|
+
|
|
162
|
+
def get_band_info(self):
|
|
163
|
+
return self.band_info
|
|
164
|
+
|
|
165
|
+
def _animate(self, frames):
|
|
166
|
+
#t = time_utils.time()
|
|
167
|
+
#print(t - self.tlast)
|
|
168
|
+
#self.tlast = t
|
|
169
|
+
|
|
170
|
+
if self.gui_queue.empty():
|
|
171
|
+
self.image.set_data(self.wf_data['data'])
|
|
172
|
+
else:
|
|
173
|
+
t0 = time_utils.time()
|
|
174
|
+
while (not self.gui_queue.empty()) and (time_utils.time()-t0 < 0.1):
|
|
175
|
+
action, data = self.gui_queue.get()
|
|
176
|
+
if action == "clear_cycle":
|
|
177
|
+
curr_cycle = data
|
|
178
|
+
self._clear_msg_boxes(curr_cycle)
|
|
179
|
+
else:
|
|
180
|
+
x, y, message = data
|
|
181
|
+
mb = self._get_msg_box()
|
|
182
|
+
self.active_msg_boxes.append(mb)
|
|
183
|
+
mb.set_properties(x, y, message)
|
|
184
|
+
|
|
185
|
+
#p = [mb.patch for mb in self.active_msg_boxes]
|
|
186
|
+
#t = [mb.text_inst for mb in self.active_msg_boxes]
|
|
187
|
+
|
|
188
|
+
#return [self.image, *p, *t, *self.sidebars_artists]
|
|
189
|
+
|
|
190
|
+
def _on_click_local(self, clickargs):
|
|
191
|
+
btn_action = clickargs['action']
|
|
192
|
+
if(btn_action == 'SET_BAND'):
|
|
193
|
+
current_band, freqMHz = clickargs['band'], clickargs['freq']
|
|
194
|
+
self.band_info = {'current_band':current_band, 'fMHz':freqMHz, 'time_set':time_utils.time()}
|
|
195
|
+
self.rig.set_freq_Hz(int(1000000*float(self.band_info['fMHz'])))
|
|
196
|
+
self.console_print(f"[PyFT8] Set band: {self.band_info['current_band']} {self.band_info['fMHz']}")
|
|
197
|
+
if clickargs['action'] == "MESSAGE_CLICK":
|
|
198
|
+
self.console_print(f"[GUI] Clicked on message '{clickargs['message']['msg_tuple']}'")
|
|
199
|
+
self.on_click(clickargs)
|
|
200
|
+
self._refresh_sidebars()
|
|
201
|
+
|
|
202
|
+
def _clear_msg_boxes(self, curr_cycle):
|
|
203
|
+
self._refresh_sidebars()
|
|
204
|
+
to_hide = [mb for mb in self.active_msg_boxes if mb.cycle == curr_cycle]
|
|
205
|
+
for mb in to_hide:
|
|
206
|
+
self.inactive_msg_boxes.append(mb)
|
|
207
|
+
self.active_msg_boxes.remove(mb)
|
|
208
|
+
mb.hide()
|
|
209
|
+
|
|
210
|
+
def _get_msg_box(self):
|
|
211
|
+
if len(self.inactive_msg_boxes) == 0:
|
|
212
|
+
mb = Msg_box(self.fig, self.ax_wf, self.wf_data['sig_w'], self.wf_data['sig_h'], onclick = self._on_click_local)
|
|
213
|
+
else:
|
|
214
|
+
mb = self.inactive_msg_boxes.pop()
|
|
215
|
+
return mb
|
|
216
|
+
|
|
217
|
+
def _make_axes(self):
|
|
218
|
+
self.ax_wf = self.fig.add_axes([self.wf_left, L['pmargin'], 1-self.wf_left-L['pmargin'], self.wf_top-L['pmargin']])
|
|
219
|
+
self.ax_wf.set_xticks([])
|
|
220
|
+
self.ax_wf.set_yticks([])
|
|
221
|
+
self.band_stats = Scrollbox(self.fig, [L['pmargin'], self.wf_top+L['vsep1'], L['sidebar_width'], L['banner_height']], nlines = 4, monospace = True)
|
|
222
|
+
self.band_stats.ax.text(-0.2,0.75,'Tx')
|
|
223
|
+
self.band_stats.ax.text(-0.2,0.25,'Rx')
|
|
224
|
+
self.console = Scrollbox(self.fig, [self.wf_left, self.wf_top+L['vsep1'], 1-self.wf_left-L['pmargin'], L['banner_height']])
|
|
225
|
+
|
|
226
|
+
def _make_buttons(self):
|
|
227
|
+
self.button_boxes = []
|
|
228
|
+
bh, bs = 0.02, 0.002
|
|
229
|
+
bb = ButtonBox(self.fig, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
|
|
230
|
+
btn_label = "CQ", onclick = self._on_click_local, clickargs = {'action':'CQ'})
|
|
231
|
+
self.button_boxes.append(bb)
|
|
232
|
+
bb = ButtonBox(self.fig, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
|
|
233
|
+
btn_label = "Repeat last", onclick = self._on_click_local, clickargs = {'action':'RPT_LAST'})
|
|
234
|
+
self.button_boxes.append(bb)
|
|
235
|
+
bb = ButtonBox(self.fig, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 100,
|
|
236
|
+
btn_label = "Tx off", onclick = self._on_click_local, clickargs = {'action':'TX_OFF'})
|
|
237
|
+
self.button_boxes.append(bb)
|
|
238
|
+
for band, freq in self.config['bands'].items():
|
|
239
|
+
bb = ButtonBox(self.fig, [L['pmargin'], self.wf_top - (len(self.button_boxes)+1) * bh + bs, L['sidebar_width'], bh-bs], btn_pc = 30,
|
|
240
|
+
btn_label = band, onclick = self._on_click_local, clickargs = {'action':'SET_BAND', 'band':band, 'freq':freq})
|
|
241
|
+
self.button_boxes.append(bb)
|
|
242
|
+
|
|
243
|
+
self.hm = Scrollbox(self.fig, [L['pmargin'], L['pmargin'], L['sidebar_width'], self.wf_top - (len(self.button_boxes)+2) * bh + bs - L['vsep1']],
|
|
244
|
+
nlines = 30, monospace = True, fontsize = 8)
|
|
245
|
+
|
|
246
|
+
def _refresh_sidebars(self):
|
|
247
|
+
current_band = self.band_info['current_band']
|
|
248
|
+
|
|
249
|
+
# band stats
|
|
250
|
+
grd = self.config['station']['grid'][:4]
|
|
251
|
+
for bb in self.button_boxes:
|
|
252
|
+
button_band = bb.clickargs.get('band','')
|
|
253
|
+
if button_band:
|
|
254
|
+
if current_band is not None:
|
|
255
|
+
bb.set_active(button_band == current_band)
|
|
256
|
+
if button_band in self.history.home_activity:
|
|
257
|
+
cnts = self.history.home_activity[button_band]
|
|
258
|
+
bb.set_info_text(f"{cnts[0]}Tx, {cnts[1]}Rx")
|
|
259
|
+
|
|
260
|
+
if current_band is not None and self.history is not None:
|
|
261
|
+
# home square counts
|
|
262
|
+
if current_band in self.history.home_most_remotes:
|
|
263
|
+
tx_lead, rx_lead = self.history.home_most_remotes[current_band]
|
|
264
|
+
myCall = self.config['station']['call']
|
|
265
|
+
n_spotted, n_spotting = self.history.get_spot_counts(current_band, myCall)
|
|
266
|
+
self.band_stats.scroll_print(f"{myCall:<7} {tx_lead[0]:<7}", color = '#ff756b')
|
|
267
|
+
self.band_stats.scroll_print(f"{n_spotting:<7} {tx_lead[1]:<7}", color = '#ff756b')
|
|
268
|
+
self.band_stats.scroll_print(f"{myCall:<7} {rx_lead[0]:<7}", color = '#b6f0c6')
|
|
269
|
+
self.band_stats.scroll_print(f"{n_spotted:<7} {rx_lead[1]:<7}", color = '#b6f0c6')
|
|
270
|
+
|
|
271
|
+
#refresh hearing me / heard by me panel
|
|
272
|
+
historic_data = self.history.hearing_me.data if self.sidebars_page == 1 else self.history.heard_by_me.data
|
|
273
|
+
new_calls_data = self.history.hearing_me_new if self.sidebars_page == 1 else self.history.heard_by_me_new
|
|
274
|
+
timewindow_str = f"<{self.hearing_me_since_mins:.0f} mins"
|
|
275
|
+
title_txt = f"Hearing me {timewindow_str}" if self.sidebars_page==1 else f"Heard by me {timewindow_str}"
|
|
276
|
+
display_rows = [(title_txt, 2e40, 'white')]
|
|
277
|
+
tnow = time_utils.time()
|
|
278
|
+
if current_band in historic_data:
|
|
279
|
+
band_rpts = historic_data[current_band]
|
|
280
|
+
calls_now = [call for call in band_rpts if (tnow - band_rpts[call]['t']) < 60*self.hearing_me_since_mins]
|
|
281
|
+
subtitle_txt = f"{len(calls_now)}/{len(band_rpts)} now/ever"
|
|
282
|
+
display_rows.append((subtitle_txt, 1e40, 'white'))
|
|
283
|
+
for remote_call in calls_now:
|
|
284
|
+
rpt = band_rpts[remote_call]
|
|
285
|
+
snr, geo_text, timestamp = int(rpt['rp']), self.history.get_geo_text(remote_call, self.config['gui']['loc']), rpt['t']
|
|
286
|
+
color = 'white' if self.history.is_in_new_alert(current_band, remote_call, new_calls_data) else 'lime'
|
|
287
|
+
display_rows.append((f"{remote_call:<7} {snr:+03d} {geo_text:<12}", timestamp, color))
|
|
288
|
+
display_rows.sort(key = lambda row: row[1], reverse = True)
|
|
289
|
+
self.hm.list_print([row[0] for row in display_rows], [row[2] for row in display_rows])
|
|
290
|
+
self.sidebars_page = (self.sidebars_page +1 )%2
|
|
291
|
+
|
|
292
|
+
def set_bandstats_title(self, txt):
|
|
293
|
+
self.band_stats.ax.set_title(txt, fontsize = 10)
|
|
294
|
+
|
|
295
|
+
def after_new_search(self, curr_cycle):
|
|
296
|
+
self.gui_queue.put(('clear_cycle', curr_cycle))
|
|
297
|
+
|
|
298
|
+
def set_message(self, message):
|
|
299
|
+
o = message['origin']
|
|
300
|
+
x = int(o['t0'] / self.wf_data['dt'] + o['odd_even'] * self.wf_data['pixels_per_cycle'])
|
|
301
|
+
y = int(o['f0'] / self.wf_data['df'])
|
|
302
|
+
self.gui_queue.put(('add_message', (x, y, message)))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
pyft8-3.3.1/PyFT8/osd.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from itertools import combinations
|
|
3
|
+
|
|
4
|
+
generator_matrix_rows = ["8329ce11bf31eaf509f27fc", "761c264e25c259335493132", "dc265902fb277c6410a1bdc", "1b3f417858cd2dd33ec7f62", "09fda4fee04195fd034783a", "077cccc11b8873ed5c3d48a", "29b62afe3ca036f4fe1a9da", "6054faf5f35d96d3b0c8c3e", "e20798e4310eed27884ae90", "775c9c08e80e26ddae56318", "b0b811028c2bf997213487c", "18a0c9231fc60adf5c5ea32", "76471e8302a0721e01b12b8", "ffbccb80ca8341fafb47b2e", "66a72a158f9325a2bf67170", "c4243689fe85b1c51363a18", "0dff739414d1a1b34b1c270", "15b48830636c8b99894972e", "29a89c0d3de81d665489b0e", "4f126f37fa51cbe61bd6b94", "99c47239d0d97d3c84e0940", "1919b75119765621bb4f1e8", "09db12d731faee0b86df6b8", "488fc33df43fbdeea4eafb4", "827423ee40b675f756eb5fe", "abe197c484cb74757144a9a", "2b500e4bc0ec5a6d2bdbdd0", "c474aa53d70218761669360", "8eba1a13db3390bd6718cec", "753844673a27782cc42012e", "06ff83a145c37035a5c1268", "3b37417858cc2dd33ec3f62", "9a4a5a28ee17ca9c324842c", "bc29f465309c977e89610a4", "2663ae6ddf8b5ce2bb29488", "46f231efe457034c1814418", "3fb2ce85abe9b0c72e06fbe", "de87481f282c153971a0a2e", "fcd7ccf23c69fa99bba1412", "f0261447e9490ca8e474cec", "4410115818196f95cdd7012", "088fc31df4bfbde2a4eafb4", "b8fef1b6307729fb0a078c0", "5afea7acccb77bbc9d99a90", "49a7016ac653f65ecdc9076", "1944d085be4e7da8d6cc7d0", "251f62adc4032f0ee714002", "56471f8702a0721e00b12b8", "2b8e4923f2dd51e2d537fa0", "6b550a40a66f4755de95c26", "a18ad28d4e27fe92a4f6c84", "10c2e586388cb82a3d80758", "ef34a41817ee02133db2eb0", "7e9c0c54325a9c15836e000", "3693e572d1fde4cdf079e86", "bfb2cec5abe1b0c72e07fbe", "7ee18230c583cccc57d4b08", "a066cb2fedafc9f52664126", "bb23725abc47cc5f4cc4cd2", "ded9dba3bee40c59b5609b4", "d9a7016ac653e6decdc9036", "9ad46aed5f707f280ab5fc4", "e5921c77822587316d7d3c2", "4f14da8242a8b86dca73352", "8b8b507ad467d4441df770e", "22831c9cf1169467ad04b68", "213b838fe2ae54c38ee7180", "5d926b6dd71f085181a4e12", "66ab79d4b29ee6e69509e56", "958148682d748a38dd68baa", "b8ce020cf069c32a723ab14", "f4331d6d461607e95752746", "6da23ba424b9596133cf9c8", "a636bcbc7b30c5fbeae67fe", "5cb0d86a07df654a9089a20", "f11f106848780fc9ecdd80a", "1fbb5364fb8d2c9d730d5ba", "fcb86bc70a50c9d02a5d034", "a534433029eac15f322e34c", "c989d9c7c3d3b8c55d75130", "7bb38b2f0186d46643ae962", "2644ebadeb44b9467d1f42c", "608cc857594bfbb55d69600"]
|
|
5
|
+
kGEN = np.array([int(row,16)>>1 for row in generator_matrix_rows])
|
|
6
|
+
A = np.zeros((83, 91), dtype=np.uint8)
|
|
7
|
+
for i, row in enumerate(kGEN):
|
|
8
|
+
for j in range(91):
|
|
9
|
+
A[i, 90 - j] = (row >> j) & 1
|
|
10
|
+
G = np.concatenate([np.eye(91, dtype=np.uint8), A.T],axis=1)
|
|
11
|
+
|
|
12
|
+
def gf2_systematic_from_reliability(G, reliab_order):
|
|
13
|
+
G = (G.copy() & 1).astype(np.uint8)
|
|
14
|
+
k, n = G.shape
|
|
15
|
+
colperm = np.array(reliab_order, dtype=np.int64)
|
|
16
|
+
inv = np.empty(n, dtype=np.int64)
|
|
17
|
+
inv[colperm] = np.arange(n)
|
|
18
|
+
G = G[:, colperm]
|
|
19
|
+
# Gauss-Jordan:
|
|
20
|
+
row = 0
|
|
21
|
+
for col in range(n):
|
|
22
|
+
if row >= k:
|
|
23
|
+
break
|
|
24
|
+
pivot_rows = np.where(G[row:, col] == 1)[0]
|
|
25
|
+
if pivot_rows.size == 0:
|
|
26
|
+
continue
|
|
27
|
+
piv = row + pivot_rows[0]
|
|
28
|
+
if piv != row:
|
|
29
|
+
G[[row, piv], :] = G[[piv, row], :]
|
|
30
|
+
ones = np.where(G[:, col] == 1)[0]
|
|
31
|
+
for r in ones:
|
|
32
|
+
if r != row:
|
|
33
|
+
G[r, :] ^= G[row, :]
|
|
34
|
+
if col != row:
|
|
35
|
+
G[:, [row, col]] = G[:, [col, row]]
|
|
36
|
+
colperm[[row, col]] = colperm[[col, row]]
|
|
37
|
+
row += 1
|
|
38
|
+
if row < k:
|
|
39
|
+
raise ValueError("Could not find k independent columns to form a systematic generator.")
|
|
40
|
+
return G, colperm
|
|
41
|
+
|
|
42
|
+
def encode_gf2(u, Gsys):
|
|
43
|
+
u = (u.astype(np.uint8) & 1)
|
|
44
|
+
return (u @ Gsys) & 1
|
|
45
|
+
|
|
46
|
+
def weighted_distance_bits(c, r_hard, w):
|
|
47
|
+
diff = c ^ r_hard
|
|
48
|
+
return float(np.sum(w * diff))
|
|
49
|
+
|
|
50
|
+
def osd_decode_minimal(llr_channel, reliab_order, Ls = [55,10]):
|
|
51
|
+
global G
|
|
52
|
+
r = (llr_channel > 0).astype(np.uint8)
|
|
53
|
+
w = np.abs(llr_channel).astype(np.float32)
|
|
54
|
+
k = G.shape[0]
|
|
55
|
+
n = G.shape[1]
|
|
56
|
+
Gsys, colperm = gf2_systematic_from_reliability(G, reliab_order)
|
|
57
|
+
r_sys = r[colperm]
|
|
58
|
+
w_sys = w[colperm]
|
|
59
|
+
u0 = r_sys[:k].copy()
|
|
60
|
+
c0_sys = encode_gf2(u0, Gsys)
|
|
61
|
+
best_c_sys = c0_sys.copy()
|
|
62
|
+
best_m = weighted_distance_bits(best_c_sys, r_sys, w_sys)
|
|
63
|
+
info_reliab = w_sys[:k]
|
|
64
|
+
for t in range(1, len(Ls) + 1):
|
|
65
|
+
flip_pool = np.argsort(info_reliab)[:min(Ls[t-1], k)]
|
|
66
|
+
for comb in combinations(flip_pool, t):
|
|
67
|
+
u = u0.copy()
|
|
68
|
+
u[list(comb)] ^= 1
|
|
69
|
+
c_sys = encode_gf2(u, Gsys)
|
|
70
|
+
m = weighted_distance_bits(c_sys, r_sys, w_sys)
|
|
71
|
+
if m < best_m:
|
|
72
|
+
best_m = m
|
|
73
|
+
best_c_sys = c_sys
|
|
74
|
+
inv = np.empty(n, dtype=np.int64)
|
|
75
|
+
inv[colperm] = np.arange(n)
|
|
76
|
+
best_c_orig = best_c_sys[inv]
|
|
77
|
+
return best_c_orig.astype(np.uint8)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def check_G():
|
|
81
|
+
from PyFT8.cycle_manager import Candidate
|
|
82
|
+
u = np.random.randint(0, 2, size=91, dtype=np.uint8)
|
|
83
|
+
c = (u @ G) & 1
|
|
84
|
+
cand = Candidate()
|
|
85
|
+
cand.llr = np.where(c == 1, +1.0, -1.0)
|
|
86
|
+
assert cand.ldpc.calc_ncheck(self.llr) == 0
|
|
87
|
+
|
|
88
|
+
|