Updated license from GPL version 2 or later to GPL version 3 or later.
[debian/gnuradio] / gr-radio-astronomy / src / python / ra_fftsink.py
1 #!/usr/bin/env python
2 #
3 # Copyright 2003,2004,2005 Free Software Foundation, Inc.
4
5 # This file is part of GNU Radio
6
7 # GNU Radio is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3, or (at your option)
10 # any later version.
11
12 # GNU Radio is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16
17 # You should have received a copy of the GNU General Public License
18 # along with GNU Radio; see the file COPYING.  If not, write to
19 # the Free Software Foundation, Inc., 51 Franklin Street,
20 # Boston, MA 02110-1301, USA.
21
22
23 from gnuradio import gr, gru, window
24 from gnuradio.wxgui import stdgui
25 import wx
26 import gnuradio.wxgui.plot as plot
27 import Numeric
28 import threading
29 import math    
30 import random
31
32 default_ra_fftsink_size = (640,140)
33
34
35
36 class ra_fft_sink_base(object):
37     def __init__(self, input_is_real=False, baseband_freq=0, y_per_div=10, sc_y_per_div=0.5, ref_level=50, sc_ref_level=20,
38                  sample_rate=1, fft_size=512, fft_rate=15,
39                  average=False, avg_alpha=None, title='', peak_hold=False, ofunc=None, xydfunc=None):
40
41         # initialize common attributes
42         self.baseband_freq = baseband_freq
43         self.y_divs = 8
44         self.y_per_div=y_per_div
45         self.sc_y_per_div=sc_y_per_div
46         self.ref_level = ref_level
47         self.autoscale = False
48         self.sc_ref_level = sc_ref_level
49         self.sample_rate = sample_rate
50         self.fft_size = fft_size
51         self.fft_rate = fft_rate
52         self.binwidth = float(sample_rate/fft_size)
53         self.average = average
54         self.ofunc = ofunc
55         self.xydfunc = xydfunc
56         self.ofunc = ofunc
57         if avg_alpha is None:
58             self.avg_alpha = 2.0 / fft_rate
59         else:
60             self.avg_alpha = avg_alpha
61         self.title = title
62         self.peak_hold = peak_hold
63         self.input_is_real = input_is_real
64         self.msgq = gr.msg_queue(2)         # queue that holds a maximum of 2 messages
65
66     def set_y_per_div(self, y_per_div):
67         self.y_per_div = y_per_div
68
69
70     def set_ref_level(self, ref_level):
71         self.ref_level = ref_level
72
73     def set_average(self, average):
74         self.average = average
75         if average:
76             self.avg.set_taps(self.avg_alpha)
77             self.set_peak_hold(False)
78         else:
79             self.avg.set_taps(1.0)
80
81     def set_peak_hold(self, enable):
82         self.peak_hold = enable
83         if enable:
84             self.set_average(False)
85         self.win.set_peak_hold(enable)
86
87     def set_autoscale(self, auto):
88         self.autoscale = auto
89
90     def set_avg_alpha(self, avg_alpha):
91         self.avg_alpha = avg_alpha
92
93     def set_baseband_freq(self, baseband_freq):
94         self.baseband_freq = baseband_freq
95         
96
97 class ra_fft_sink_f(gr.hier_block, ra_fft_sink_base):
98     def __init__(self, fg, parent, baseband_freq=0,
99                  y_per_div=10, sc_y_per_div=0.5, sc_ref_level=40, ref_level=50,                  sample_rate=1, fft_size=512,
100                  fft_rate=15, average=False, avg_alpha=None, title='',
101                  size=default_ra_fftsink_size, peak_hold=False, ofunc=None,
102                  xydfunc=None):
103         ra_fft_sink_base.__init__(self, input_is_real=True, baseband_freq=baseband_freq,
104                                y_per_div=y_per_div, sc_y_per_div=sc_y_per_div,
105                                sc_ref_level=sc_ref_level, ref_level=ref_level,
106                                sample_rate=sample_rate, fft_size=fft_size,
107                                fft_rate=fft_rate,
108                                average=average, avg_alpha=avg_alpha, title=title,
109                                peak_hold=peak_hold, ofunc=ofunc, 
110                                xydfunc=xydfunc)
111                                
112         self.binwidth = float(sample_rate/2.0)/float(fft_size)
113         s2p = gr.serial_to_parallel(gr.sizeof_float, fft_size)
114         one_in_n = gr.keep_one_in_n(gr.sizeof_float * fft_size,
115                                     max(1, int(sample_rate/fft_size/fft_rate)))
116         mywindow = window.blackmanharris(fft_size)
117         fft = gr.fft_vfc(fft_size, True, mywindow)
118         c2mag = gr.complex_to_mag(fft_size)
119         self.avg = gr.single_pole_iir_filter_ff(1.0, fft_size)
120         log = gr.nlog10_ff(20, fft_size, -20*math.log10(fft_size))
121         sink = gr.message_sink(gr.sizeof_float * fft_size, self.msgq, True)
122
123         fg.connect (s2p, one_in_n, fft, c2mag, self.avg, log, sink)
124         gr.hier_block.__init__(self, fg, s2p, sink)
125
126         self.win = fft_window(self, parent, size=size)
127         self.set_average(self.average)
128
129 class ra_fft_sink_c(gr.hier_block, ra_fft_sink_base):
130     def __init__(self, fg, parent, baseband_freq=0,
131                  y_per_div=10, sc_y_per_div=0.5, sc_ref_level=40,
132                  ref_level=50, sample_rate=1, fft_size=512,
133                  fft_rate=15, average=False, avg_alpha=None, title='',
134                  size=default_ra_fftsink_size, peak_hold=False, ofunc=None, xydfunc=None):
135
136         ra_fft_sink_base.__init__(self, input_is_real=False, baseband_freq=baseband_freq,
137                                y_per_div=y_per_div, sc_y_per_div=sc_y_per_div,
138                                sc_ref_level=sc_ref_level, ref_level=ref_level,
139                                sample_rate=sample_rate, fft_size=fft_size,
140                                fft_rate=fft_rate,
141                                average=average, avg_alpha=avg_alpha, 
142                                title=title,
143                                peak_hold=peak_hold, ofunc=ofunc, 
144                                xydfunc=xydfunc)
145
146         s2p = gr.serial_to_parallel(gr.sizeof_gr_complex, fft_size)
147         one_in_n = gr.keep_one_in_n(gr.sizeof_gr_complex * fft_size,
148                                     max(1, int(sample_rate/fft_size/fft_rate)))
149         mywindow = window.blackmanharris(fft_size)
150         fft = gr.fft_vcc(fft_size, True, mywindow)
151         c2mag = gr.complex_to_mag(fft_size)
152         self.avg = gr.single_pole_iir_filter_ff(1.0, fft_size)
153         log = gr.nlog10_ff(20, fft_size, -20*math.log10(fft_size))
154         sink = gr.message_sink(gr.sizeof_float * fft_size, self.msgq, True)
155
156         fg.connect(s2p, one_in_n, fft, c2mag, self.avg, log, sink)
157         gr.hier_block.__init__(self, fg, s2p, sink)
158
159         self.win = fft_window(self, parent, size=size)
160         self.set_average(self.average)
161
162
163 # ------------------------------------------------------------------------
164
165 myDATA_EVENT = wx.NewEventType()
166 EVT_DATA_EVENT = wx.PyEventBinder (myDATA_EVENT, 0)
167
168
169 class DataEvent(wx.PyEvent):
170     def __init__(self, data):
171         wx.PyEvent.__init__(self)
172         self.SetEventType (myDATA_EVENT)
173         self.data = data
174
175     def Clone (self): 
176         self.__class__ (self.GetId())
177
178
179 class input_watcher (threading.Thread):
180     def __init__ (self, msgq, fft_size, event_receiver, **kwds):
181         threading.Thread.__init__ (self, **kwds)
182         self.setDaemon (1)
183         self.msgq = msgq
184         self.fft_size = fft_size
185         self.event_receiver = event_receiver
186         self.keep_running = True
187         self.start ()
188
189     def run (self):
190         while (self.keep_running):
191             msg = self.msgq.delete_head()  # blocking read of message queue
192             itemsize = int(msg.arg1())
193             nitems = int(msg.arg2())
194
195             s = msg.to_string()            # get the body of the msg as a string
196
197             # There may be more than one FFT frame in the message.
198             # If so, we take only the last one
199             if nitems > 1:
200                 start = itemsize * (nitems - 1)
201                 s = s[start:start+itemsize]
202
203             complex_data = Numeric.fromstring (s, Numeric.Float32)
204             de = DataEvent (complex_data)
205             wx.PostEvent (self.event_receiver, de)
206             del de
207     
208
209 class fft_window (plot.PlotCanvas):
210     def __init__ (self, ra_fftsink, parent, id = -1,
211                   pos = wx.DefaultPosition, size = wx.DefaultSize,
212                   style = wx.DEFAULT_FRAME_STYLE, name = ""):
213         plot.PlotCanvas.__init__ (self, parent, id, pos, size, style, name)
214
215         self.y_range = None
216         self.ra_fftsink = ra_fftsink
217         self.peak_hold = False
218         self.peak_vals = None
219
220         self.SetEnableGrid (True)
221         # self.SetEnableZoom (True)
222         # self.SetBackgroundColour ('black')
223         
224         self.build_popup_menu()
225         
226         EVT_DATA_EVENT (self, self.set_data)
227         wx.EVT_CLOSE (self, self.on_close_window)
228         self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
229         self.Bind(wx.EVT_MOTION, self.on_motion)
230
231         self.input_watcher = input_watcher(ra_fftsink.msgq, ra_fftsink.fft_size, self)
232
233
234     def on_close_window (self, event):
235         print "fft_window:on_close_window"
236         self.keep_running = False
237
238             
239     def set_data (self, evt):
240         calc_min = 99e10
241         calc_max = -99e10
242         dB = evt.data
243         L = len (dB)
244
245         calc_min = min(dB)
246         calc_max = max(dB)
247
248         if (self.ra_fftsink.ofunc != None):
249             self.ra_fftsink.ofunc(evt.data,L)
250         
251         if self.peak_hold:
252             if self.peak_vals is None:
253                 self.peak_vals = dB
254             else:
255                 self.peak_vals = Numeric.maximum(dB, self.peak_vals)
256                 dB = self.peak_vals
257
258         x = max(abs(self.ra_fftsink.sample_rate), abs(self.ra_fftsink.baseband_freq))
259         if x >= 1e9:
260             sf = 1e-9
261             units = "GHz"
262         elif x >= 1e6:
263             sf = 1e-6
264             units = "MHz"
265         elif x >= 1e3:
266             sf = 1e-3
267             units = "kHz"
268         else:
269             sf = 1.0
270             units = "Hz"
271
272         if self.ra_fftsink.input_is_real:     # only plot 1/2 the points
273             x_vals = ((Numeric.arrayrange (L/2)
274                        * (self.ra_fftsink.sample_rate * sf / L))
275                       + self.ra_fftsink.baseband_freq * sf)
276             points = Numeric.zeros((len(x_vals), 2), Numeric.Float64)
277             points[:,0] = x_vals
278             points[:,1] = dB[0:L/2]
279         else:
280             # the "negative freqs" are in the second half of the array
281             x_vals = ((Numeric.arrayrange (-L/2, L/2)
282                        * (self.ra_fftsink.sample_rate * sf / L))
283                       + self.ra_fftsink.baseband_freq * sf)
284             points = Numeric.zeros((len(x_vals), 2), Numeric.Float64)
285             points[:,0] = x_vals
286             points[:,1] = Numeric.concatenate ((dB[L/2:], dB[0:L/2]))
287
288         lines = plot.PolyLine (points, colour='BLUE')
289         graphics = plot.PlotGraphics ([lines],
290                                       title=self.ra_fftsink.title,
291                                       xLabel = units, yLabel = "dB")
292
293         self.Draw (graphics, xAxis=None, yAxis=self.y_range)
294         d = calc_max - calc_min
295         d = d * 0.1
296         if self.ra_fftsink.autoscale == True:
297             self.y_range = self._axisInterval ('min', calc_min-d, calc_max+d)
298         else:
299             self.update_y_range ()
300
301     def set_peak_hold(self, enable):
302         self.peak_hold = enable
303         self.peak_vals = None
304
305     def update_y_range (self):
306         ymax = self.ra_fftsink.ref_level
307         ymin = self.ra_fftsink.ref_level - self.ra_fftsink.y_per_div * self.ra_fftsink.y_divs
308         self.y_range = self._axisInterval ('min', ymin, ymax)
309
310     def on_average(self, evt):
311         # print "on_average"
312         self.ra_fftsink.set_average(evt.IsChecked())
313
314     def on_peak_hold(self, evt):
315         # print "on_peak_hold"
316         self.ra_fftsink.set_peak_hold(evt.IsChecked())
317
318     def on_autoscale(self, evt):
319         self.ra_fftsink.set_autoscale(evt.IsChecked())
320
321     def on_incr_ref_level(self, evt):
322         # print "on_incr_ref_level"
323         self.ra_fftsink.set_ref_level(self.ra_fftsink.ref_level
324                                    + self.ra_fftsink.y_per_div)
325
326     def on_decr_ref_level(self, evt):
327         # print "on_decr_ref_level"
328         self.ra_fftsink.set_ref_level(self.ra_fftsink.ref_level
329                                    - self.ra_fftsink.y_per_div)
330
331     def on_incr_y_per_div(self, evt):
332         # print "on_incr_y_per_div"
333         self.ra_fftsink.set_y_per_div(next_up(self.ra_fftsink.y_per_div, (0.5,1,2,5,10)))
334
335     def on_decr_y_per_div(self, evt):
336         # print "on_decr_y_per_div"
337         self.ra_fftsink.set_y_per_div(next_down(self.ra_fftsink.y_per_div, (0.5,1,2,5,10)))
338
339     def on_y_per_div(self, evt):
340         # print "on_y_per_div"
341         Id = evt.GetId()
342         if Id == self.id_y_per_div_1:
343             self.ra_fftsink.set_y_per_div(0.5)
344         elif Id == self.id_y_per_div_2:
345             self.ra_fftsink.set_y_per_div(1.0)
346         elif Id == self.id_y_per_div_5:
347             self.ra_fftsink.set_y_per_div(2.0)
348         elif Id == self.id_y_per_div_10:
349             self.ra_fftsink.set_y_per_div(5.0)
350         elif Id == self.id_y_per_div_20:
351             self.ra_fftsink.set_y_per_div(10)
352
353         
354     def on_right_click(self, event):
355         menu = self.popup_menu
356         for id, pred in self.checkmarks.items():
357             item = menu.FindItemById(id)
358             item.Check(pred())
359         self.PopupMenu(menu, event.GetPosition())
360
361     def on_motion(self, event):
362         if not self.ra_fftsink.xydfunc == None:
363             xy = self.GetXY(event)
364             self.ra_fftsink.xydfunc (xy)
365
366     def build_popup_menu(self):
367         self.id_incr_ref_level = wx.NewId()
368         self.id_decr_ref_level = wx.NewId()
369         self.id_autoscale = wx.NewId()
370         self.id_incr_y_per_div = wx.NewId()
371         self.id_decr_y_per_div = wx.NewId()
372         self.id_y_per_div_1 = wx.NewId()
373         self.id_y_per_div_2 = wx.NewId()
374         self.id_y_per_div_5 = wx.NewId()
375         self.id_y_per_div_10 = wx.NewId()
376         self.id_y_per_div_20 = wx.NewId()
377         self.id_average = wx.NewId()
378         self.id_peak_hold = wx.NewId()
379
380         self.Bind(wx.EVT_MENU, self.on_average, id=self.id_average)
381         self.Bind(wx.EVT_MENU, self.on_peak_hold, id=self.id_peak_hold)
382         self.Bind(wx.EVT_MENU, self.on_autoscale, id=self.id_autoscale)
383         self.Bind(wx.EVT_MENU, self.on_incr_ref_level, id=self.id_incr_ref_level)
384         self.Bind(wx.EVT_MENU, self.on_decr_ref_level, id=self.id_decr_ref_level)
385         self.Bind(wx.EVT_MENU, self.on_incr_y_per_div, id=self.id_incr_y_per_div)
386         self.Bind(wx.EVT_MENU, self.on_decr_y_per_div, id=self.id_decr_y_per_div)
387         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_1)
388         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_2)
389         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_5)
390         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_10)
391         self.Bind(wx.EVT_MENU, self.on_y_per_div, id=self.id_y_per_div_20)
392
393
394         # make a menu
395         menu = wx.Menu()
396         self.popup_menu = menu
397         menu.AppendCheckItem(self.id_average, "Average")
398         menu.AppendCheckItem(self.id_peak_hold, "Peak Hold")
399         menu.Append(self.id_incr_ref_level, "Incr Ref Level")
400         menu.Append(self.id_decr_ref_level, "Decr Ref Level")
401         # menu.Append(self.id_incr_y_per_div, "Incr dB/div")
402         # menu.Append(self.id_decr_y_per_div, "Decr dB/div")
403         menu.AppendSeparator()
404         # we'd use RadioItems for these, but they're not supported on Mac
405         menu.AppendCheckItem(self.id_autoscale, "Autoscale")
406         menu.AppendCheckItem(self.id_y_per_div_1, "0.5 dB/div")
407         menu.AppendCheckItem(self.id_y_per_div_2, "1.0 dB/div")
408         menu.AppendCheckItem(self.id_y_per_div_5, "2.0 dB/div")
409         menu.AppendCheckItem(self.id_y_per_div_10, "5.0 dB/div")
410         menu.AppendCheckItem(self.id_y_per_div_20, "10.0 dB/div")
411
412         self.checkmarks = {
413             self.id_average : lambda : self.ra_fftsink.average,
414             self.id_peak_hold : lambda : self.ra_fftsink.peak_hold,
415             self.id_autoscale : lambda : self.ra_fftsink.autoscale,
416             self.id_y_per_div_1 : lambda : self.ra_fftsink.y_per_div == 0.5,
417             self.id_y_per_div_2 : lambda : self.ra_fftsink.y_per_div == 1.0,
418             self.id_y_per_div_5 : lambda : self.ra_fftsink.y_per_div == 2.0,
419             self.id_y_per_div_10 : lambda : self.ra_fftsink.y_per_div == 5.0,
420             self.id_y_per_div_20 : lambda : self.ra_fftsink.y_per_div == 10.0,
421             }
422
423
424 def next_up(v, seq):
425     """
426     Return the first item in seq that is > v.
427     """
428     for s in seq:
429         if s > v:
430             return s
431     return v
432
433 def next_down(v, seq):
434     """
435     Return the last item in seq that is < v.
436     """
437     rseq = list(seq[:])
438     rseq.reverse()
439
440     for s in rseq:
441         if s < v:
442             return s
443     return v
444
445
446 # ----------------------------------------------------------------
447 #                     Deprecated interfaces
448 # ----------------------------------------------------------------
449
450 # returns (block, win).
451 #   block requires a single input stream of float
452 #   win is a subclass of wxWindow
453
454 def make_ra_fft_sink_f(fg, parent, title, fft_size, input_rate, ymin = 0, ymax=50):
455     
456     block = ra_fft_sink_f(fg, parent, title=title, fft_size=fft_size, sample_rate=input_rate,
457                        y_per_div=(ymax - ymin)/8, ref_level=ymax)
458     return (block, block.win)
459
460 # returns (block, win).
461 #   block requires a single input stream of gr_complex
462 #   win is a subclass of wxWindow
463
464 def make_ra_fft_sink_c(fg, parent, title, fft_size, input_rate, ymin=0, ymax=50):
465     block = ra_fft_sink_c(fg, parent, title=title, fft_size=fft_size, sample_rate=input_rate,
466                        y_per_div=(ymax - ymin)/8, ref_level=ymax)
467     return (block, block.win)
468
469
470 # ----------------------------------------------------------------
471 # Standalone test app
472 # ----------------------------------------------------------------
473
474 class test_app_flow_graph (stdgui.gui_flow_graph):
475     def __init__(self, frame, panel, vbox, argv):
476         stdgui.gui_flow_graph.__init__ (self, frame, panel, vbox, argv)
477
478         fft_size = 256
479
480         # build our flow graph
481         input_rate = 20.000e3
482
483         # Generate a complex sinusoid
484         src1 = gr.sig_source_c (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
485         #src1 = gr.sig_source_c (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
486
487         # We add these throttle blocks so that this demo doesn't
488         # suck down all the CPU available.  Normally you wouldn't use these.
489         thr1 = gr.throttle(gr.sizeof_gr_complex, input_rate)
490
491         sink1 = ra_fft_sink_c (self, panel, title="Complex Data", fft_size=fft_size,
492                             sample_rate=input_rate, baseband_freq=100e3,
493                             ref_level=60, y_per_div=10)
494         vbox.Add (sink1.win, 1, wx.EXPAND)
495         self.connect (src1, thr1, sink1)
496
497         src2 = gr.sig_source_f (input_rate, gr.GR_SIN_WAVE, 5.75e3, 1000)
498         #src2 = gr.sig_source_f (input_rate, gr.GR_CONST_WAVE, 5.75e3, 1000)
499         thr2 = gr.throttle(gr.sizeof_float, input_rate)
500         sink2 = ra_fft_sink_f (self, panel, title="Real Data", fft_size=fft_size*2,
501                             sample_rate=input_rate, baseband_freq=100e3,
502                             ref_level=60, y_per_div=10)
503         vbox.Add (sink2.win, 1, wx.EXPAND)
504         self.connect (src2, thr2, sink2)
505
506 def main ():
507     app = stdgui.stdapp (test_app_flow_graph,
508                          "FFT Sink Test App")
509     app.MainLoop ()
510
511 if __name__ == '__main__':
512     main ()