PyFT8 3.6.6__tar.gz → 3.7.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.
Files changed (42) hide show
  1. {pyft8-3.6.6 → pyft8-3.7.1}/PKG-INFO +24 -2
  2. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/gui.py +36 -47
  3. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/message_broker.py +1 -2
  4. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/pyft8.py +2 -2
  5. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/receiver.py +1 -1
  6. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/PKG-INFO +24 -2
  7. {pyft8-3.6.6 → pyft8-3.7.1}/README.md +23 -1
  8. {pyft8-3.6.6 → pyft8-3.7.1}/pyproject.toml +1 -1
  9. {pyft8-3.6.6 → pyft8-3.7.1}/LICENSE +0 -0
  10. {pyft8-3.6.6 → pyft8-3.7.1}/MANIFEST.in +0 -0
  11. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/__init__.py +0 -0
  12. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/databases.py +0 -0
  13. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/osd.py +0 -0
  14. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/pskreporter.py +0 -0
  15. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/qso_manager.py +0 -0
  16. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/rigctrl.py +0 -0
  17. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/time_utils.py +0 -0
  18. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8/transmitter.py +0 -0
  19. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/SOURCES.txt +0 -0
  20. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/dependency_links.txt +0 -0
  21. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/entry_points.txt +0 -0
  22. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/requires.txt +0 -0
  23. {pyft8-3.6.6 → pyft8-3.7.1}/PyFT8.egg-info/top_level.txt +0 -0
  24. {pyft8-3.6.6 → pyft8-3.7.1}/setup.cfg +0 -0
  25. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/CQ AAAA.py +0 -0
  26. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/animation.py +0 -0
  27. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/bits_loopback.py +0 -0
  28. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/bits_loopback_short.py +0 -0
  29. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/osd.py +0 -0
  30. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/symbols_loopback.py +0 -0
  31. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/test_generate_wav.py +0 -0
  32. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/test_loopback_performance.py +0 -0
  33. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/text.py +0 -0
  34. {pyft8-3.6.6 → pyft8-3.7.1}/tests/misc/view_worked_before.py +0 -0
  35. {pyft8-3.6.6 → pyft8-3.7.1}/tests/pipeline/Fine tuning compare IOS_PC.py +0 -0
  36. {pyft8-3.6.6 → pyft8-3.7.1}/tests/pipeline/Targetted tests.py +0 -0
  37. {pyft8-3.6.6 → pyft8-3.7.1}/tests/pipeline/test_01.wav +0 -0
  38. {pyft8-3.6.6 → pyft8-3.7.1}/tests/play_wavs.py +0 -0
  39. {pyft8-3.6.6 → pyft8-3.7.1}/tests/plot_decode_times.py +0 -0
  40. {pyft8-3.6.6 → pyft8-3.7.1}/tests/spare.py +0 -0
  41. {pyft8-3.6.6 → pyft8-3.7.1}/tests/spare2.py +0 -0
  42. {pyft8-3.6.6 → pyft8-3.7.1}/tests/test_batch_and_live.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 3.6.6
3
+ Version: 3.7.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
@@ -23,7 +23,7 @@ Dynamic: license-file
23
23
  # PyFT8 [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyft8?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/pyft8) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyft8?period=weekly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads/wk)](https://pepy.tech/projects/pyft8) [PyPI link](https://pypi.org/project/PyFT8/)
24
24
  # All-Python FT8 Transceiver GUI / Command Line Modem
25
25
 
26
- This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is about 70% of that achieved by WSJT-x in NORM mode, but (tbc) slightly above ft8_lib.
26
+ This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is very close to that of WSJT-x 2.7.0 running in FAST mode.
27
27
 
28
28
  PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Python, but can be used as a standalone replacement for WSJT-x and other software.
29
29
 
@@ -116,6 +116,28 @@ The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in
116
116
 
117
117
  <img width="900" height="900" alt="live playback vs wsjtx_v2 7 0" src="https://github.com/user-attachments/assets/853e98b2-5b9b-4241-b5f3-181fe193642e" />
118
118
 
119
+ ## Decoding approach
120
+ The approach to decoding, as at v3.7.0, is as follows.
121
+
122
+ - As audio arrives
123
+ - FFT to a spectrum 4 times per symbol duration & assemble a time-frequency grid with 2 bins per tone.
124
+ - Collect all audio samples for later full cycle FFT
125
+ - Search the time frequency grid for candidates based on Costas array sync score (using middle Costas block)
126
+ - Send candidates for decode using symbol & tone points drawn directly from the search grid to get early decodes using limited LDPC cycles.
127
+
128
+ This often decodes around three quarters of the candidates decoded by WSJT-x, with the steps below getting most of the rest:
129
+
130
+ - For candidates that haven't decoded:
131
+ - FFT the full cycle audio, downsample to 200 samples per second & shift the signal to f = 0
132
+ - Explore fine time and frequency offsets to maximise the Costas sync score
133
+ - Check for bad candidates (count maxima of Costas tones)
134
+ - Extract the LLR & wait for decoder to pick up the candidate
135
+ - For candidates awaiting decode from above:
136
+ - Try more extensive LDPC
137
+ - Try several 'A Priori' masks and input to more LDPC attempts, recording the output LLR on failures
138
+ - Try Ordered Statistics Decoding on the base LLR
139
+ - Try Ordered Statistics Decoding on the LLRs output from the AP+LDPC runs above
140
+
119
141
  ## Limitations
120
142
  PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
121
143
 
@@ -25,13 +25,12 @@ def add_my_axes(fig, pos):
25
25
  ax.set_facecolor(TEXT_BACKGROUND_COLOR)
26
26
  return ax
27
27
 
28
- MESSAGE_TYPES = {'generic':{'bg':'blue', 'fg':'white', 'alpha':0.5}, 'CQ':{'bg':'green', 'fg':'white', 'alpha':0.8},
29
- 'from_me': {'bg':'yellow', 'fg':'black', 'alpha':0.95}, 'to_me':{'bg':'red', 'fg':'white', 'alpha':0.9}}
28
+ MESSAGE_TYPES = {'generic':{'bg':'blue', 'fg':'white', 'alpha':0.4}, 'CQ':{'bg':'green', 'fg':'white', 'alpha':0.7},
29
+ 'from_me': {'bg':'yellow', 'fg':'black', 'alpha':0.9}, 'to_me':{'bg':'red', 'fg':'white', 'alpha':0.9}}
30
30
  class Msg_box:
31
- def __init__(self, fig, ax, x, y, w, h, message):
31
+ def __init__(self, fig, ax, w, h):
32
32
  from matplotlib.patches import Rectangle
33
33
  self.fig, self.ax = fig, ax
34
- message_type_params = MESSAGE_TYPES[message['message_type']]
35
34
  rect = Rectangle((0, 0), w, h, edgecolor='lime', lw=2)
36
35
  self.patch = self.ax.add_patch(rect)
37
36
  self.patch.set_visible(False)
@@ -39,7 +38,6 @@ class Msg_box:
39
38
  self.text_inst.set_visible(False)
40
39
  self.text_inst.set_fontfamily('monospace')
41
40
  self.artists = [self.patch, self.text_inst]
42
- self.set_props(x, y, message)
43
41
 
44
42
  def set_props(self, x, y, message):
45
43
  self.xy = (x, y)
@@ -177,10 +175,10 @@ class Gui:
177
175
  self.cid = self.fig.canvas.mpl_connect('button_press_event', self._oncanvasclick)
178
176
 
179
177
  def main_loop(self):
180
- last_ptr = 0
181
178
  self.plt.show(block = False)
182
- target_update_time = 0.25
179
+ target_update_time = 0.15
183
180
  while True:
181
+ self.fig.canvas.flush_events() # for clicks
184
182
  tstart_wf_redraw = time_utils.time()
185
183
  self.image.set_data(self.waterfall_data['data'])
186
184
  if self.needs_redraw:
@@ -188,42 +186,29 @@ class Gui:
188
186
  plt.pause(0.01)
189
187
  else:
190
188
  self.ax_wf.draw_artist(self.image)
191
- t = time_utils.time()
189
+ self.fig.canvas.flush_events() # for clicks
192
190
  for mb in self.msg_boxes:
193
- mb.draw()
194
- #print(f"Drew {len(self.msg_boxes)} in {time_utils.time() - t:6.2f}s")
195
- self.fig.canvas.blit(self.ax_wf.bbox)
196
- #self.fig.canvas.update()
191
+ if mb.patch.get_visible():
192
+ mb.draw()
193
+ self.fig.canvas.flush_events() # for clicks
194
+ self.fig.canvas.blit(self.ax_wf.bbox) #self.fig.canvas.update()
197
195
  self.fig.canvas.flush_events()
198
- #print(f"Updated at {time_utils.cycle_time():6.2f}s")
199
196
  tdelay = target_update_time - (time_utils.time() - tstart_wf_redraw)
200
- #print(tdelay)
201
197
  if tdelay > 0.01:
202
198
  time_utils.sleep(tdelay)
203
199
 
204
- def before_search(self, curr_cycle):
205
- self._hide_msg_boxes(curr_cycle) # hide boxes overlapping new decodes
200
+ def before_search(self, curr_cycle): # would be better with a local trigger
201
+ #self._hide_msg_boxes(curr_cycle) # hide boxes overlapping new decodes
206
202
  self._hide_msg_boxes(curr_cycle = None) # hide all boxes
207
-
208
- def after_search(self, curr_cycle):
209
- self._refresh_hearing()
210
- self._refresh_band_buttons()
211
203
  self._refresh_home_panel()
212
- #self._clear_msg_boxes(curr_cycle)
204
+ self._refresh_band_buttons()
205
+ self._refresh_hearing()
213
206
 
214
207
  def display_message(self, message):
208
+ mb = self._get_message_box()
215
209
  x = int(message['t0'] / self.waterfall_data['dt'] + message['their_tx_cycle'] * self.waterfall_data['pixels_per_cycle'])
216
210
  y = int(message['fHz'] / self.waterfall_data['df'])
217
- mb = None
218
- for mb1 in self.msg_boxes:
219
- if not mb1.patch.get_visible():
220
- mb1.set_props(x, y, message)
221
- mb = mb1
222
- break
223
- if not mb:
224
- mb = Msg_box(self.fig, self.ax_wf, x, y, self.waterfall_data['sig_w'], self.waterfall_data['sig_h'], message)
225
- self.msg_boxes.append(mb)
226
- #print(f"{len(self.msg_boxes)} message boxes")
211
+ mb.set_props(x, y, message)
227
212
 
228
213
  def update_message(self, display_text, update_dict):
229
214
  for mb in self.msg_boxes:
@@ -252,17 +237,35 @@ class Gui:
252
237
  color = row_text['color'])
253
238
  self.needs_redraw = True
254
239
 
240
+ def _get_message_box(self):
241
+ mb = None
242
+ for mb1 in self.msg_boxes:
243
+ if not mb1.patch.get_visible():
244
+ mb = mb1
245
+ break
246
+ if not mb:
247
+ mb = Msg_box(self.fig, self.ax_wf, self.waterfall_data['sig_w'], self.waterfall_data['sig_h'])
248
+ self.msg_boxes.append(mb)
249
+ return mb
250
+
255
251
  def _set_band(self, band):
256
252
  if band in self.configured_bands:
257
253
  self.band_info = {'current_band':band, 'fMHz':self.configured_bands[band], 'time_set':time_utils.time()}
258
254
  self.console_print(f"[PyFT8] Set band: {self.band_info['current_band']} {self.band_info['fMHz']}")
259
- self._clear_msg_boxes()
255
+ for mb in self.msg_boxes:
256
+ mb.hide()
257
+ mb.remove()
258
+ self.msg_boxes = []
259
+ self._refresh_home_panel()
260
+ self._refresh_band_buttons()
261
+ self._refresh_hearing()
262
+ self.needs_redraw = True
260
263
 
261
264
  def _oncanvasclick(self, clickargs):
262
265
  if clickargs.inaxes is self.ax_wf:
263
266
  for mb in self.msg_boxes:
264
267
  if mb.contains(clickargs.x, clickargs.y):
265
- self.console_print(f"[GUI] Clicked on message '{mb.message['msg_text']}'")
268
+ self.console_print(f"[GUI] Clicked on message '{mb.message['msg_text']}")
266
269
  self.qso_manager.on_click({'action':"MESSAGE_CLICK", 'message':mb.message})
267
270
  return
268
271
  for bb in self.button_boxes:
@@ -271,7 +274,6 @@ class Gui:
271
274
  self.needs_redraw = True
272
275
  self._set_band(bb.id)
273
276
  self.qso_manager.on_click({'action':"SET_BAND", 'fMHz':self.configured_bands[bb.id]})
274
- self.after_search(curr_cycle = None)
275
277
  else:
276
278
  action = ["CQ", "RPT_LAST", "TX_OFF"][['CQ', 'Repeat last', 'Tx off'].index(bb.id)]
277
279
  self.qso_manager.on_click({'action':action})
@@ -281,19 +283,6 @@ class Gui:
281
283
  for mb in to_hide:
282
284
  mb.hide()
283
285
  self.needs_redraw = True
284
-
285
- def _clear_msg_boxes(self, curr_cycle = None):
286
- to_remove = [mb for mb in self.msg_boxes if (mb.cycle == curr_cycle) or (curr_cycle is None)]
287
- self.msg_boxes = [mb for mb in self.msg_boxes if (mb.cycle != curr_cycle) and not (curr_cycle is None)]
288
- for mb in to_remove:
289
- mb.hide()
290
- mb.remove()
291
- self.needs_redraw = True
292
-
293
- def _text_row(self, ax, x, y, text = '', color = 'white', **args):
294
- art = ax.text(x, y, text, color = color)
295
- art.set_fontfamily('monospace')
296
- return art
297
286
 
298
287
  def _refresh_home_panel(self):
299
288
  current_band = self.band_info['current_band']
@@ -34,7 +34,7 @@ class Broker():
34
34
  self.on_decode(message_dict)
35
35
  m = message_dict
36
36
  screen_format = f"{cyclestart_string} {their_snr} {m['dt']:4.1f} {m['fHz']:4.0f} ~ {hail} {their_call} {grid_rpt}"
37
- print(f"{screen_format:50s} decoded@ {m['decode_completed']:5.1f}s, dec = {m['decode_status']}")
37
+ print(f"{screen_format:50s} decoded@ {m['decode_completed'] %15:5.1f}s, dec = {m['decode_status']}")
38
38
  self.message_queue_non_time_critical.put(message_dict)
39
39
 
40
40
  def _process_message_ntc(self):
@@ -55,7 +55,6 @@ class Broker():
55
55
  new_display_text = f"{m['display_text']} {hearing_me} {wb_text} {geo_text}"
56
56
  self.gui.update_message( m['display_text'], {'hearing_me':hearing_me, 'wb_text':wb_text,
57
57
  'geo_text':geo_text, 'display_text':new_display_text } )
58
-
59
58
  if band_info:
60
59
  if m['their_call'] != 'not':
61
60
  if self.history:
@@ -11,7 +11,7 @@ from PyFT8.databases import ADIF
11
11
  from PyFT8.qso_manager import QSO_manager
12
12
  from PyFT8.message_broker import Broker
13
13
 
14
- VER = '3.6.6'
14
+ VER = '3.7.1'
15
15
  PSKR_REFRESH_MINS = 20
16
16
  message_broker = None
17
17
 
@@ -136,7 +136,7 @@ def cli():
136
136
  qso_manager = QSO_manager(message_broker, rig_control, console_print)
137
137
  message_broker.gui.register_qso_manager(qso_manager)
138
138
  message_broker.rx.register_before_search(message_broker.gui.before_search)
139
- message_broker.rx.register_after_search(message_broker.gui.after_search)
139
+ #message_broker.rx.register_after_search(message_broker.gui.after_search)
140
140
  message_broker.history.incorporate_log_data(qso_manager.adif_logging.cache)
141
141
  message_broker.history.start_collect_new()
142
142
 
@@ -592,7 +592,7 @@ class Receiver():
592
592
  'msg_text':f"{hail} {their_call} {grid_rpt}",
593
593
  'their_snr':f"{c.snr:+03d}", 'their_tx_cycle':c.origin['odd_even'],
594
594
  't0':c.origin['t0'], 'dt':c.origin['t0'] - 0.5, 'fHz':c.origin['f0'],
595
- 'decode_status':c.decode_status, 'decode_completed':time_utils.cycle_time()}
595
+ 'decode_status':c.decode_status, 'decode_completed':time_utils.time()}
596
596
  self.process_message(message_dict)
597
597
 
598
598
  if len(to_decode):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyFT8
3
- Version: 3.6.6
3
+ Version: 3.7.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
@@ -23,7 +23,7 @@ Dynamic: license-file
23
23
  # PyFT8 [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyft8?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/pyft8) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyft8?period=weekly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads/wk)](https://pepy.tech/projects/pyft8) [PyPI link](https://pypi.org/project/PyFT8/)
24
24
  # All-Python FT8 Transceiver GUI / Command Line Modem
25
25
 
26
- This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is about 70% of that achieved by WSJT-x in NORM mode, but (tbc) slightly above ft8_lib.
26
+ This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is very close to that of WSJT-x 2.7.0 running in FAST mode.
27
27
 
28
28
  PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Python, but can be used as a standalone replacement for WSJT-x and other software.
29
29
 
@@ -116,6 +116,28 @@ The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in
116
116
 
117
117
  <img width="900" height="900" alt="live playback vs wsjtx_v2 7 0" src="https://github.com/user-attachments/assets/853e98b2-5b9b-4241-b5f3-181fe193642e" />
118
118
 
119
+ ## Decoding approach
120
+ The approach to decoding, as at v3.7.0, is as follows.
121
+
122
+ - As audio arrives
123
+ - FFT to a spectrum 4 times per symbol duration & assemble a time-frequency grid with 2 bins per tone.
124
+ - Collect all audio samples for later full cycle FFT
125
+ - Search the time frequency grid for candidates based on Costas array sync score (using middle Costas block)
126
+ - Send candidates for decode using symbol & tone points drawn directly from the search grid to get early decodes using limited LDPC cycles.
127
+
128
+ This often decodes around three quarters of the candidates decoded by WSJT-x, with the steps below getting most of the rest:
129
+
130
+ - For candidates that haven't decoded:
131
+ - FFT the full cycle audio, downsample to 200 samples per second & shift the signal to f = 0
132
+ - Explore fine time and frequency offsets to maximise the Costas sync score
133
+ - Check for bad candidates (count maxima of Costas tones)
134
+ - Extract the LLR & wait for decoder to pick up the candidate
135
+ - For candidates awaiting decode from above:
136
+ - Try more extensive LDPC
137
+ - Try several 'A Priori' masks and input to more LDPC attempts, recording the output LLR on failures
138
+ - Try Ordered Statistics Decoding on the base LLR
139
+ - Try Ordered Statistics Decoding on the LLRs output from the AP+LDPC runs above
140
+
119
141
  ## Limitations
120
142
  PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
121
143
 
@@ -1,7 +1,7 @@
1
1
  # PyFT8 [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyft8?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/pyft8) [![PyPI Downloads](https://static.pepy.tech/personalized-badge/pyft8?period=weekly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads/wk)](https://pepy.tech/projects/pyft8) [PyPI link](https://pypi.org/project/PyFT8/)
2
2
  # All-Python FT8 Transceiver GUI / Command Line Modem
3
3
 
4
- This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is about 70% of that achieved by WSJT-x in NORM mode, but (tbc) slightly above ft8_lib.
4
+ This repository contains the source code for PyFT8, an all-Python open source FT8 transceiver that you can run as a basic GUI or from the command line to receive and transmit. Decoding performance (number of decodes) is very close to that of WSJT-x 2.7.0 running in FAST mode.
5
5
 
6
6
  PyFT8 is somewhat experimental, with a focus on demonstrating FT8 written in Python, but can be used as a standalone replacement for WSJT-x and other software.
7
7
 
@@ -94,6 +94,28 @@ The image below shows the number of decodes from PyFT8, WSJT-x V2.7.0 running in
94
94
 
95
95
  <img width="900" height="900" alt="live playback vs wsjtx_v2 7 0" src="https://github.com/user-attachments/assets/853e98b2-5b9b-4241-b5f3-181fe193642e" />
96
96
 
97
+ ## Decoding approach
98
+ The approach to decoding, as at v3.7.0, is as follows.
99
+
100
+ - As audio arrives
101
+ - FFT to a spectrum 4 times per symbol duration & assemble a time-frequency grid with 2 bins per tone.
102
+ - Collect all audio samples for later full cycle FFT
103
+ - Search the time frequency grid for candidates based on Costas array sync score (using middle Costas block)
104
+ - Send candidates for decode using symbol & tone points drawn directly from the search grid to get early decodes using limited LDPC cycles.
105
+
106
+ This often decodes around three quarters of the candidates decoded by WSJT-x, with the steps below getting most of the rest:
107
+
108
+ - For candidates that haven't decoded:
109
+ - FFT the full cycle audio, downsample to 200 samples per second & shift the signal to f = 0
110
+ - Explore fine time and frequency offsets to maximise the Costas sync score
111
+ - Check for bad candidates (count maxima of Costas tones)
112
+ - Extract the LLR & wait for decoder to pick up the candidate
113
+ - For candidates awaiting decode from above:
114
+ - Try more extensive LDPC
115
+ - Try several 'A Priori' masks and input to more LDPC attempts, recording the output LLR on failures
116
+ - Try Ordered Statistics Decoding on the base LLR
117
+ - Try Ordered Statistics Decoding on the LLRs output from the AP+LDPC runs above
118
+
97
119
  ## Limitations
98
120
  PyFT8 doesn't decode / encode *all* message types. The table below shows which are handled.
99
121
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "PyFT8"
3
- version = "3.6.6"
3
+ version = "3.7.1"
4
4
  license = "GPL-3.0-or-later"
5
5
 
6
6
  authors = [
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes