1 """Histograms panel with amplitude histogram.
2
3 Copyright 2008-2014 Research Foundation State University of New York
4 This file is part of QUB Express.
5
6 QUB Express is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 QUB Express is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License,
17 named LICENSE.txt, in the QUB Express program directory. If not, see
18 <http://www.gnu.org/licenses/>.
19
20 """
21
22 from __future__ import with_statement
23
24 import cStringIO
25 import itertools
26 import gc
27 import os
28 import sys
29 import time
30 import traceback
31 import gobject
32 import cairo
33 import gtk
34 import traceback
35 import qubx.settings
36 import qubx.GTK
37 import qubx.pyenv
38 import qubx.tree
39 import qubx.faces
40 import qubx.fit_space
41 import qubx.task
42 import qubx.dataGTK
43 import qubx.notebook
44 import qubx.notebookGTK
45 import qubx.toolspace
46
47 from gtk import gdk
48 from gtk import keysyms
49 from itertools import izip, count
50 from qubx.util_types import *
51 from qubx.accept import *
52 from qubx.GTK import pack_item, pack_space, pack_hsep, pack_vsep, pack_label, pack_button, pack_check, pack_radio, pack_scrolled
53 from qubx.settings import Property, Propertied
54 from numpy import arange, array, zeros
55 from qubx.task import Tasks
58 - def __init__(self, caption, nb_caption, bins, bars, pdf=None, components=[],
59 nb_headers=[], nb_xlabel="", nb_ylabel="",
60 data_colorref=qubx.toolspace.COLOR_BLACK, pdf_color=(0,0,0), global_name="", **kw):
61 Anon.__init__(self, caption=caption, nb_caption=nb_caption, bins=bins, bars=bars, pdf=pdf, components=components,
62 nb_headers=nb_headers, nb_xlabel=nb_xlabel, nb_ylabel=nb_ylabel,
63 data_colorref=data_colorref, pdf_color=pdf_color, **kw)
64 self.nbChart = qubx.notebook.NbChart(caption, global_name=global_name,
65 get_caption=lambda: self.nb_caption,
66 get_shape=self.__nb_get_shape, get_headers=self.__nb_get_headers,
67 get_col=self.__nb_get_col, get_type=lambda: float,
68 get_xlabel=lambda: self.nb_xlabel, get_ylabel=lambda: self.nb_ylabel,
69 get_series=self.__nb_get_series)
71 return (len(self.bins), 2+((not (self.pdf is None)) and 1 or 0)+len(self.components))
81 if c == 0:
82 return self.bins
83 if c == 1:
84 return self.bars
85 if (c == 2) and not (self.pdf is None):
86 return self.pdf
87 return self.components[c - 2 - ((not (self.pdf is None)) and 1 or 0)]
89 series = [qubx.notebook.NbChartSeries(0, 1, 0, len(self.bins), qubx.notebook.HISTOGRAM, qubx.toolspace.Appearance.color(self.data_colorref))]
90 if not (self.pdf is None):
91 series.append(qubx.notebook.NbChartSeries(0, 2, 0, len(self.bins), qubx.notebook.LINES, self.pdf_color))
92 for c, com in enumerate(self.components):
93 series.append(qubx.notebook.NbChartSeries(0, len(series)+1, 0, len(self.bins), qubx.notebook.LINES, (0,0,0), .3))
94 return series
95
97 """
98
99 Base class for panel showing one or more pages of histograms which are updated on a background thread (robot).
100
101 When the histograms are outdated, call update(). On the worker thread, it will call robot_update().
102 Communicate the new histogram(s) back to the GUI via gobject.idle_add().
103
104 @ivar QubX: the top-level L{qubx.express.Cube} object
105 @ivar robot: a worker thread servicing a queue (L{qubx.task.Robot})
106 @ivar plots: list of L{HistoPlot} displays
107 @ivar plot_count: number of plots showing
108 @ivar page: index of page showing
109 @ivar page_count: number of pages
110 @ivar OnAddPlot: L{WeakEvent}(index) for subclass customization
111 @ivar OnRemovingPlot: L{WeakEvent}(index)
112 @ivar OnChangePage: L{WeakEvent}(index)
113 """
114
115 __explore_featured = ['robot', 'plots', 'plot_count', 'page', 'page_count', 'OnAddPlot', 'OnRemovingPlot', 'OnChangePage',
116 'sync', 'update_plots', 'robot_update']
117
118 - def __init__(self, name, global_name=""):
119 """@param name: caption for this panel's tab"""
120 qubx.faces.Face.__init__(self, name, global_name)
121 self.__ref = Reffer()
122 self.set_size_request(100, 70)
123 self.QubX = qubx.pyenv.env.globals['QubX']
124 self.QubX.OnQuit += self.__ref(lambda: self.robot.stop())
125 self.robot = qubx.task.Robot(name, self.__ref(lambda: Tasks.add_task(self.robot)),
126 self.__ref(lambda: Tasks.remove_task(self.robot)))
127 self.robot.OnException += self.__ref(self.__onException)
128 self.sync = self.robot.sync
129 self.serial = 0
130
131 self.OnAddPlot = WeakEvent()
132 self.OnRemovingPlot = WeakEvent()
133 self.plots = []
134 self.plotBox = pack_item(qubx.GTK.AspectGrid(), self, expand=True)
135
136 self.OnChangePage = WeakEvent()
137 self.__page = 0
138 self.__page_count = 1
139 self.__plot_fit = -1
140 self.adjPage = gtk.Adjustment(value=0, lower=0, upper=0, step_incr=1, page_incr=1, page_size=0)
141 self.scroll = pack_item(gtk.HScrollbar(self.adjPage), self, show=False)
142 self.scroll.connect('value_changed', self.__onChangePage)
143
144 self.histrec = [ [] ]
145 self.nbCharts = qubx.notebook.NbItems("All charts", self.global_name and '%s.nbCharts'%self.global_name or '')
146 self.nbPictures = qubx.notebook.NbItems("All pictures", self.global_name and '%s.nbPictures'%self.global_name or '')
147 - def set_page_count(self, n):
148 if self.__page >= n:
149 self.page = n - 1
150 while self.__page_count > n:
151 self.__page_count -= 1
152 while self.__page_count < n:
153 self.__page_count += 1
154 if len(self.histrec) < self.__page_count:
155 self.histrec.append([])
156 self.adjPage.upper = n - 1
157 self.adjPage.emit('changed')
158 if n <= 1:
159 self.scroll.hide()
160 else:
161 self.scroll.show()
162 page_count = property(lambda self: self.__page_count, lambda self, x: self.set_page_count(x))
163 - def set_page(self, x):
164 if self.__page == x:
165 return
166 self.__page = x
167 self.adjPage.set_value(self.__page)
168 self.OnChangePage(x)
169 self.update_plots()
170 page = property(lambda self: self.__page, lambda self, x: self.set_page(x))
172 while x < len(self.plots):
173 self.plots[-1].dispose()
174 self.OnRemovingPlot(len(self.plots)-1)
175 del self.nbPictures.items[len(self.plots)-1]
176 if self.__plot_fit == (len(self.plots)-1):
177 self.plots[-1].layerset = self.plots[-1].controlsHidden
178 self.plots[-1].OnChangeLayerset -= self.__ref(self.__onChangeLayerset)
179 if self.__plot_fit == -1:
180 self.plotBox.remove(self.plots[-1])
181 del self.plots[-1]
182 if len(self.plots) == 1:
183 self.plots[0].subNotebook.items.remove(self.nbCharts)
184 self.plots[0].subNotebook.items.remove(self.nbPictures)
185 while x > len(self.plots):
186 plot = HistoPlot(label=self.face_name, global_name='%s.plots[%i]'%(self.global_name, len(self.plots)))
187 plot.OnChangeLayerset += self.__ref(self.__onChangeLayerset)
188 if len(self.plots) == 1:
189 self.plots[0].subNotebook.items.append(self.nbCharts)
190 self.plots[0].subNotebook.items.append(self.nbPictures)
191 plot.subNotebook.items.append(self.nbCharts)
192 plot.subNotebook.items.append(self.nbPictures)
193 self.plots.append(plot)
194 plot.show()
195 if (self.__plot_fit == -1):
196 self.plotBox.pack_aspect(plot)
197 self.OnAddPlot(len(self.plots)-1)
198 try:
199 self.nbPictures.items.insert(len(self.plots)-1, plot.nbPicture)
200 except:
201 self.nbPictures.items.insert(len(self.plots)-1, None)
202 plot_count = property(lambda self: len(self.plots), lambda self, x: self.set_plot_count(x))
214
216 """Re-render all plots from self.histrec[self.page]."""
217 if not self.page_count: return
218 hists = self.histrec[self.page]
219 self.plot_count = len(hists)
220 for h, hist in enumerate(hists):
221 self.plots[h].set_rec(hist)
222
223
224 items = []
225 for hists in self.histrec:
226 for hist in hists:
227 items.append(hist.nbChart)
228 self.nbCharts.items[:] = items
229
230 self.OnChangePage(self.__page)
233 - def __onChangePage(self, scroll):
234 self.page = int(self.adjPage.value)
236 traceback.print_exception(typ, val, trace)
237 - def update(self, force=False):
238 """Requests that robot_update() be called in the robot thread."""
239 self.serial += 1
240 if self.showing or force:
241 self.robot.do(self.robot_try_update, self.serial)
243 """In robot thread, filters out duplicate (stale serial) requests and calls robot_update()"""
244
245 if serial == self.serial:
246 self.robot_update()
248 """Override this method to calculate something in the robot thread."""
249 pass
250
252 """Base class for a histogram panel which updates when the sampled data source changes."""
253 __explore_featured = ['on_change_data']
254 - def __init__(self, name, global_name=""):
263 """Override this method to do something when the sampled data changes."""
264 pass
265
266
267 @Propertied(Property('bins', 128, "number of bins"),
268 Property('auto', True, "False to manually set lo and hi bounds"),
269 Property('lo', 0.0, "left bound"),
270 Property('hi', 1.0, 'right bound'),
271 Property('multi_file', False, 'True to work on a Group of files'),
272 Property('multi_file_group', 0, 'multi_file: work on Data table entries with this Group number')
273 )
275 """Panel showing the all-points amplitude histogram of one signal.
276
277 @ivar signal: signal (channel) index, 1-based
278 @ivar controls: L{qubx.faces.Face} with settings, to go in QubX.About
279 @ivar path: last save-as location
280 """
281
282 __explore_featured = ['controls', 'path', 'copy_to_clipboard', 'copy_image_to_clipboard', 'save_to']
283
284 - def __init__(self, name='AmpHist', global_name=""):
285 DataHistFace.__init__(self, name, global_name)
286 self.propertied_connect_settings('AmpHist')
287 self.controls = qubx.faces.Face("AmpHist", global_name='QubX.About.AmpHist')
288 self.controls.set_size_request(100, 100)
289 self.controls.show()
290 line = pack_item(gtk.HBox(True), self.controls)
291 pack_label('Bins:', line)
292 self.txtBins = pack_item(qubx.GTK.NumEntry(self.bins, acceptIntGreaterThan(1)), line)
293 self.propertied_connect_NumEntry('bins', self.txtBins)
294 line = pack_item(gtk.HBox(), self.controls)
295 self.chkAuto = pack_check('auto', line)
296 self.propertied_connect_check('auto', self.chkAuto)
297 pack_label('lo:', line)
298 self.txtLo = pack_item(qubx.GTK.NumEntry(self.lo, width_chars=4), line)
299 self.propertied_connect_NumEntry('lo', self.txtLo)
300 pack_label('hi:', line)
301 self.txtHi = pack_item(qubx.GTK.NumEntry(self.hi, width_chars=4), line)
302 self.propertied_connect_NumEntry('hi', self.txtHi)
303 line = pack_item(gtk.HBox(), self.controls)
304 self.chkMulti = pack_check('Multi-file: Group', line)
305 self.propertied_connect_check('multi_file', self.chkMulti)
306 self.txtGroup = pack_item(qubx.GTK.NumEntry(self.multi_file_group, acceptInt, width_chars=4), line)
307 self.propertied_connect_NumEntry('multi_file_group', self.txtGroup)
308 line = pack_item(gtk.HBox(), self.controls)
309 self.btnSave = pack_button('Save...', line, on_click=self.__onClickSave)
310 self.btnCopy = pack_button('Copy', line, on_click=self.__onClickCopy)
311 self.btnCopyImage = pack_button('Image...', line, on_click=self.__onClickCopyImage)
312 self.plot_count = 1
313 self.path = None
326 dlg = gtk.FileChooserDialog('Save as...', None, gtk.FILE_CHOOSER_ACTION_SAVE,
327 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))
328 dlg.set_default_response(gtk.RESPONSE_OK)
329 filter = gtk.FileFilter()
330 filter.set_name("text files")
331 filter.add_pattern("*.txt")
332 dlg.add_filter(filter)
333 if self.path: dlg.set_current_folder(self.path)
334 dlg.set_filename('AmpHist ')
335 response = dlg.run()
336 if response == gtk.RESPONSE_OK:
337 try:
338 fname = add_ext_if_none(dlg.get_filename(), '.txt')
339 if self.global_name:
340 qubx.pyenv.env.scriptable_if_matching('%s.save_to(open(%s, "w"))' % (self.global_name, repr(fname)),
341 [(self.global_name, self)])
342 self.save_to(open(fname, 'w'))
343 except Exception, e:
344 print "Saving %s: %s" % (fname, e)
345 self.path = dlg.get_current_folder()
346 dlg.destroy()
353 buf = cStringIO.StringIO()
354 self.save_to(buf)
355 gtk.clipboard_get('CLIPBOARD').set_text(buf.getvalue())
366 pixmap = gdk.Pixmap(self.window, w, h, -1)
367 ctx = pixmap.cairo_create()
368 ctx.set_source_rgb(1,1,1)
369 ctx.paint()
370 self.plots[0].draw_to_context(ctx, w, h)
371 del ctx
372 pixbuf = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, w, h)
373 pixbuf.get_from_drawable(pixmap, gdk.colormap_get_system(), 0, 0, 0, 0, -1, -1)
374 clipboard = gtk.clipboard_get('CLIPBOARD')
375 clipboard.set_image(pixbuf)
376 del pixmap
377 del pixbuf
378 qubx.pyenv.env.gc_collect_on_idle()
379 self.plots[0].redraw_canvas()
388 """Writes the histogram as tab-separated text to a.file-like object"""
389 f.write(self.plots[0].nbChart.to_txt())
391 """Determines if update() is needed"""
392 if self.QubX.Data.view:
393 self.update()
394
396 """Calculates a new amp hist."""
397 with self.robot.main_hold:
398 if self.multi_file:
399 segs = []
400 for v, ss in self.QubX.DataSource.get_segmentation_files(group=self.multi_file_group):
401 segs.extend(ss)
402 else:
403 segs = self.dataSource.get_segmentation()
404 n = sum(seg.n for seg in segs)
405 auto, lo, hi = self.auto, self.lo, self.hi
406 if n == 0: return
407 finished = 0
408 if auto:
409 lo, hi = 1e30, -1e30
410 for seg in segs:
411 for chunk in self.dataSource.gen_samples(seg.chunks, self.robot.main_hold):
412 if chunk.included:
413 lo = min(lo, min(chunk.samples))
414 hi = max(hi, max(chunk.samples))
415 finished += len(chunk.samples)
416 self.robot.progress = finished * 50.0 / n
417 if lo != self.lo:
418 gobject.idle_add(self.propertied_set, lo, 'lo')
419 if hi != self.hi:
420 gobject.idle_add(self.propertied_set, hi, 'hi')
421 else:
422 self.robot.progress = 50.0
423 if lo >= hi: return
424 bins, binw = make_bins(lo, hi, self.bins)
425 bars = zeros(shape=(self.bins,))
426 one = 1.0 / n
427 last = self.bins-1
428 finished = 0
429 for seg in segs:
430 for chunk in self.dataSource.gen_samples(seg.chunks, self.robot.main_hold):
431 if chunk.included:
432 samples = array(chunk.samples[(lo <= chunk.samples) & (chunk.samples <= hi)])
433 samples -= lo
434 samples /= binw
435 samples = samples.astype('int32')
436 samples[samples == self.bins] = self.bins-1
437 for x in xrange(len(bins)):
438 bars[x] += one * len(samples[samples == x])
439 finished += chunk.n
440 self.robot.progress = 50.0 + finished * 50.0 / n
441 with self.robot.main_hold:
442 self.histrec[0][:] = [HistRec('All-points amplitude histogram of %s'%segs[0].file.signals.get(segs[0].signal, 'Name'),
443 'AmpHist', bins, bars, nb_headers=['bin_mid', 'count / n'],
444 nb_xlabel='bin_mid', nb_ylabel='count / n')]
445 gobject.idle_add(self.update_plots)
446
448 """Returns (bins, bin_width) as (numpy.array[n], float)."""
449 d = (hi - lo)
450 one = d/n
451 bins = arange(n, dtype='float64')
452 bins *= one
453 bins += lo + one/2
454 return bins, one
455
456
457 -class HistoPlot(qubx.fit_space.FitSpace):
458 """
459 fit_space for a histogram
460 """
461 - def __init__(self, bins=[], bars=[], caption="", label="Hist", **kw):
462 """
463 @param bins: array of upper bounds
464 @param bars: array of count/total
465 """
466 qubx.fit_space.FitSpace.__init__(self, label=label, **kw)
467 self.draw_hist = True
468 self.layerset = self.controlsHidden
469 self.caption = caption
470 self.set_size_request(40, 40)
471 self.lines = []
472 self.__ref = Reffer()
473 self.OnOverlay += self.__ref(self._onOverlay)
474 self.OnChangeLayerset += self.__ref(self.__onChangeLayerset)
475 self.controls.robot.OnExpr += self.__ref(self.__onExpr)
476 self.controls.robot.OnParam += self.__ref(self.__onParam)
477 self.controls.robot.OnStats += self.__ref(self.__onStats)
478 self.controls.robot.OnIteration += self.__ref(self.__onIteration)
479 self.controls.robot.OnEndFit += self.__ref(self.__onEndFit)
480 self.layNotebook = qubx.toolspace.Layer(x=1, y=-5.5-qubx.fit_space.LINE_EMS, w=2, h=2, cBG=qubx.toolspace.COLOR_CLEAR)
481 self.subNotebook = qubx.notebookGTK.SubLayer_Notebook(x=0, y=0, w=2, h=2)
482 self.layNotebook.add_sublayer(self.subNotebook)
483 self.add_layer(self.layNotebook)
484 self.nbChartGiven = qubx.notebook.NbItems('Chart', '')
485 self.nbChartFit = qubx.notebook.NbChart('Chart', '', lambda: self.caption,
486 self.controls.nb_get_shape, self.controls.nb_get_headers,
487 get_col=self.controls.nb_get_col, get_type=lambda: float,
488 get_series=self.nb_get_series)
489 self.nbChart = self.nbChartGiven
490 self.subNotebook.items.append(self.nbChart)
491 self.nbPicture = qubx.notebookGTK.NbPicture('Picture', '', self.nb_picture_get_shape,
492 self.nb_picture_draw)
493 self.subNotebook.items.append(self.nbPicture)
494 self.nbParams = qubx.notebook.NbDynText('Parameters', '', self.controls.nb_get_param_text)
495 self.nbParams.std_err_est = None
496 self.subNotebook.items.append(self.nbParams)
497 qubx.notebook.Notebook.register_auto('%s.Parameters'%label, '%s Parameters, on Fit'%label, True)
498 qubx.notebook.Notebook.register_auto('%s.Results'%label, '%s Results, on Fit'%label, True)
499 qubx.notebook.Notebook.register_auto('%s.Chart'%label, '%s Chart, on Fit'%label)
500 qubx.notebook.Notebook.register_auto('%s.Picture'%label, '%s Picture, on Fit'%label, True)
501 self.__nb_next_stats = False
502 self.global_name = self.global_name
503 self.set_data(bins, bars)
505 self.bins = array(bins, copy=True)
506 self.bars = array(bars, copy=True)
507 self.pdf = None
508 self.components = []
509 self.controls.robot.set_data(bins, bars)
517 self._dim = (w, h)
518 if self.layerset == self.controls:
519 return
520 for c in self.components:
521 context.set_source_rgba(*self.appearance.color(self.cBG))
522 context.set_line_width(0.2*self.appearance.emsize * self.appearance.line_width + 0.15 * self.appearance.emsize)
523 qubx.fit_space.draw_line(context, self.appearance, self.bins, c, self.t2x, self.d2y, 0.0)
524 context.set_source_rgba(.5,.5,.5,.8)
525 context.set_line_width(0.2*self.appearance.emsize * self.appearance.line_width)
526 qubx.fit_space.draw_line(context, self.appearance, self.bins, c, self.t2x, self.d2y, 0.0)
527 if not (self.pdf is None):
528 context.set_source_rgba(*self.appearance.color(self.cBG))
529 context.set_line_width(0.6*self.appearance.emsize * self.appearance.line_width + 0.2*self.appearance.emsize)
530 qubx.fit_space.draw_line(context, self.appearance, self.bins, self.pdf, self.t2x, self.d2y, 0.0)
531 context.set_source_rgba(*(list(self.dataColor2)+[.6]))
532 context.set_line_width(0.6*self.appearance.emsize * self.appearance.line_width)
533 qubx.fit_space.draw_line(context, self.appearance, self.bins, self.pdf, self.t2x, self.d2y, 0.0)
534 - def set_pdf(self, pdf, components):
535 """Sets the pdf and its components (fit curves).
536 @param pdf: numpy.array with sum fit curve
537 @param components: list of numpy arrays with partial fit curves
538 """
539 self.pdf = pdf
540 self.components = components
541 self.draw_fit_when_hidden = False
542 self.redraw_canvas()
554 if (layerset == self.controlsHidden) and not (self.pdf is None):
555 self.nbChart = self.nbChartGiven
556 else:
557 self.nbChart = self.nbChartFit
558 self.subNotebook.items[0] = self.nbChart
559 - def __onExpr(self, curve_name, expr, params, param_vals, lo, hi, can_fit):
560 self.__param_names = params
561 self.__param_vals = param_vals
562 self.__param_active = can_fit
563 - def __onParam(self, index, name, value, lo, hi, can_fit):
570 self.__nb_next_stats = True
571 - def __onStats(self, correlation, is_pseudo, std_err_est, ssr, r2, runs_prob):
589
590
591 TheAmpHistCopyDialog = qubx.GTK.CopyDialog('Copy histogram image')
592