1 """Auto-fit of Hill equation.
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 os
25 import sys
26 import traceback
27 import numpy
28 import gobject
29 import cairo
30 import gtk
31 import qubx.GTK
32 import qubx.pyenv
33 import qubx.fast.data
34 import qubx.fit_space
35 import qubx.faces
36 import qubx.notebook
37 import qubx.notebookGTK
38 import qubx.settings
39 import qubx.task
40 import qubx.dataGTK
41
42 from gtk import gdk, keysyms
43 from itertools import izip, count, chain
44 from qubx.util_types import *
45 from qubx.accept import acceptFloatGreaterThan
46 from qubx.GTK import pack_item, pack_space, pack_hsep, pack_vsep, pack_label, pack_button, pack_check, pack_radio, pack_scrolled
47 from qubx.tree import node_data_or_set_def
48
49 HILL_MAX_RESULTS = 100
50
51 HILL_MEASURE_PEAK = 0
52 HILL_MEASURE_MEAN = 1
53 HILL_MEASURE_EQUIL = 2
54
56 """Measures, displays, updates the dose-response curve for frontmost data, and fits to the Hill equation.
57
58 @ivar fit: L{qubx.fit_space.FitSpace}
59 @ivar controls: L{qubx.faces.Face} for QubX.About
60 @ivar measure: method for finding response; one of HILL_MEASURE_PEAK, HILL_MEASURE_MEAN, HILL_MEASURE_EQUIL
61 @ivar filter_kHz: if measure == HILL_MEASURE_PEAK, applies this filter to minimize measurement noise effect on the one-point peak
62 @ivar mean_dur: if measure == HILL_MEASURE_MEAN, number of milliseconds of data to average each segment
63 @ivar mean_from: if measure == HILL_MEASURE_MEAN, number of milliseconds to skip at the beginning of each segment
64 """
65
66 __explore_featured = ['profile', 'fit', 'layNotebook', 'subNotebook', 'nbChart', 'nbPicture', 'nbParams', 'controls',
67 'measure', 'filter_kHz', 'mean_dur', 'mean_from']
68
69 - def __init__(self, name='Hill', global_name='QubX.Figures.Hill'):
70 qubx.faces.Face.__init__(self, name, global_name)
71 self.__ref = Reffer()
72 cat = qubx.settings.SettingsMgr['Hill']
73 self.profile = cat.active
74 cat.OnSet = self.__ref(self.__onUpdateProfile)
75 self.__measure = node_data_or_set_def(self.profile, 'Measure', HILL_MEASURE_PEAK)
76 self.__filter_kHz = node_data_or_set_def(self.profile, 'Filter_kHz', 1.0)
77 self.__mean_dur = node_data_or_set_def(self.profile, 'Mean_dur', 10.0)
78 self.__mean_from = node_data_or_set_def(self.profile, 'Mean_from', 500.0)
79 self.__dose = self.__model_Peq = None
80 self.fit = pack_item(qubx.fit_space.FitSpace(label=name, global_name='%s.fit' % global_name), self, expand=True)
81 self.fit.caption = "log10(Ligand) v. fraction open, fitted to the Hill equation"
82 self.fit.controls.robot.OnStats += self.__ref(self.__onStats)
83 self.fit.controls.robot.OnException += self.__ref(self.__onRobotException)
84 self.fit.controls.robot.OnEndFit += self.__ref(self.__onEndFit)
85 self.fit.controls.robot.set_expr('L**Hill / (L**Hill + EC50**Hill)')
86 self.fit.controls.robot.set_data(numpy.arange(10, dtype='float32'), numpy.zeros(shape=(10,), dtype='float32'))
87 self.fit.OnOverlay += self.__ref(self.__onOverlayFit)
88 self.layNotebook = qubx.toolspace.Layer(x=1, y=-5.5-qubx.fit_space.LINE_EMS, w=2, h=2, cBG=qubx.toolspace.COLOR_CLEAR)
89 self.subNotebook = qubx.notebookGTK.SubLayer_Notebook(x=0, y=0, w=2, h=2)
90 self.layNotebook.add_sublayer(self.subNotebook)
91 self.fit.add_layer(self.layNotebook)
92 self.nbChart = qubx.notebook.NbChart('Hill chart', 'QubX.Figures.Hill.nbChart', lambda: 'Hill',
93 self.fit.controls.nb_get_shape, self.fit.controls.nb_get_headers,
94 get_col=self.fit.controls.nb_get_col, get_type=lambda: float,
95 get_series=self.fit.nb_get_series)
96 self.subNotebook.items.append(self.nbChart)
97 self.nbPicture = qubx.notebookGTK.NbPicture('Hill picture', 'QubX.Figures.Hill.nbPicture', self.fit.nb_picture_get_shape,
98 self.fit.nb_picture_draw)
99 self.subNotebook.items.append(self.nbPicture)
100 self.nbParams = qubx.notebook.NbDynText('Parameters', 'QubX.Figures.Hill.nbParams', self.fit.controls.nb_get_param_text)
101 self.nbParams.std_err_est = None
102 self.subNotebook.items.append(self.nbParams)
103 self.__nb_next_stats = False
104 qubx.notebook.Notebook.register_auto('Hill_fit.Parameters', 'Hill Parameters, on Fit')
105 qubx.notebook.Notebook.register_auto('Hill_fit.Results', 'Hill Results, on Fit')
106 qubx.notebook.Notebook.register_auto('Hill_fit.Chart', 'Hill chart, on Fit')
107 qubx.notebook.Notebook.register_auto('Hill_fit.Picture', 'Hill picture, on Fit')
108
109 self.QubX = qubx.pyenv.env.globals['QubX']
110 self.QubX.OnQuit += self.__ref(lambda: self.fit.dispose())
111 self.controls = qubx.faces.Face(name, global_name='QubX.About.%s' % name)
112 self.controls.set_size_request(100, 100)
113 self.controls.show()
114 pack_label('Dose-Response:', self.controls)
115 self.lblVs = pack_label('log10(Ligand) v. fraction open', self.controls)
116 pack_label('Measure each level at:', self.controls)
117 line = pack_item(gtk.HBox(), self.controls)
118 self.chkPeak = pack_radio('Peak filtered at ', line, active=False, on_toggle=bind_with_args_before(self.__onToggleMeasure, HILL_MEASURE_PEAK))
119 self.txtFilter = pack_item(qubx.GTK.NumEntry(self.__filter_kHz, acceptFloatGreaterThan(0.0), width_chars=4), line)
120 self.txtFilter.OnChange += self.__ref(self.__onChangeFilter)
121 pack_label('kHz', line)
122 line = pack_item(gtk.HBox(), self.controls)
123 self.chkMean = pack_radio('Mean of ', line, group=self.chkPeak, active=False, on_toggle=bind_with_args_before(self.__onToggleMeasure, HILL_MEASURE_MEAN))
124 self.txtMeanDur = pack_item(qubx.GTK.NumEntry(self.__mean_dur, acceptFloatGreaterThan(0.0), width_chars=5), line)
125 self.txtMeanDur.OnChange += self.__ref(self.__onChangeMeanDur)
126 pack_label('ms', line)
127 line = pack_item(gtk.HBox(), self.controls)
128 pack_label('ms', line, at_end=True)
129 self.txtMeanFrom = pack_item(qubx.GTK.NumEntry(self.__mean_from, acceptFloatGreaterThan(0.0), width_chars=5), line, at_end=True)
130 self.txtMeanFrom.OnChange += self.__ref(self.__onChangeMeanFrom)
131 pack_label('starting at ', line, at_end=True)
132 self.chkEquil = pack_radio('Equilibrium', self.controls, group=self.chkPeak, active=False, on_toggle=bind_with_args_before(self.__onToggleMeasure, HILL_MEASURE_EQUIL))
133 pack_label('The blue trace is occupancy prob.', pack_item(gtk.HBox(), self.controls))
134 pack_label('of class 1 (red) calculated from', pack_item(gtk.HBox(), self.controls))
135 pack_label('the model.', pack_item(gtk.HBox(), self.controls))
136 self.datasrc = self.QubX.DataSource
137 self.datasrc.OnChangeSamples += self.__ref(self.__onChangeSamples)
138 self.models = ModelTracker()
139 for evt in (self.models.OnSwitch, self.models.OnChange, self.models.OnSetRate):
140 evt += self.__ref(bind(self.__onChangeModel))
141 gobject.idle_add(self.__onChangeSamples)
142 self.__serial_data = 0
143 self.set_measure(self.__measure)
154 measure = property(lambda self: self.__measure, lambda self, x: self.set_measure(x))
160 filter_kHz = property(lambda self: self.__filter_kHz, lambda self, x: self.set_filter_kHz(x))
166 mean_dur = property(lambda self: self.__mean_dur, lambda self, x: self.set_mean_dur(x))
172 mean_from = property(lambda self: self.__mean_from, lambda self, x: self.set_mean_from(x))
195
217 traceback.print_exception(typ, val, trace)
219 self.__nb_next_stats = True
220 - def __onStats(self, correlation, is_pseudo, std_err_est, ssr, r2, runs_prob):
248 if serial < self.__serial_data: return
249 with self.fit.controls.robot.main_hold:
250 rates = self.models.model.rates
251 ligands = [rates.get(i, 'Ligand') for i in xrange(rates.size)]
252 volts = [rates.get(i, 'Voltage') for i in xrange(rates.size)]
253 names = [nm for nm in chain(ligands, volts) if nm]
254 data = self.QubX.Data.view
255 series = [data.file.signals.get(i, 'Name') for i in xrange(data.file.signals.size)]
256 self.stim_ix = None
257 self.is_log = True
258 for i,nm in enumerate(series):
259 if nm in ligands:
260 self.stim_ix = i
261 self.is_log = True
262 break
263 elif nm in volts:
264 self.stim_ix = i
265 self.is_log = False
266 break
267 if not (self.stim_ix is None):
268 gobject.idle_add(self.lblVs.set_label, '%s v. fraction open' % ((self.is_log and 'log10(%s)' or '%s') % series[self.stim_ix]))
269 gobject.idle_add(self.fit.set_caption, '%s v. fraction open, fitted to the Hill equation' % ((self.is_log and 'log10(%s)' or '%s') % series[self.stim_ix]))
270 measure = self.__measure
271 filter_kHz = self.__filter_kHz
272 segments = self.datasrc.get_segmentation()
273 Nseg = len(segments)
274 Ntail = 1
275 if segments:
276 if self.measure == HILL_MEASURE_MEAN:
277 Ntail = segments[0].n
278 n_sub = int(round(self.mean_dur * 1e-3 / data.file.sampling))
279 off_sub = int(round(self.mean_from * 1e-3 / data.file.sampling))
280 segments = [data.get_segmentation_indexed(seg.f + off_sub, seg.f + off_sub + n_sub - 1, signal=seg.signal)[0]
281 for seg in segments]
282 else:
283 Ntail = min(100, segments[0].n / 5)
284 sign = 1
285 if self.models.model:
286 classes = self.models.model.classes
287 sign = classes.get(1, 'Amp') - classes.get(0, 'Amp')
288 if (self.stim_ix is None) or (not Nseg):
289 self.__dose = None
290 return
291 if measure == HILL_MEASURE_PEAK:
292 dr = qubx.fast.data.DoseResponse_Peak(segments[0].sampling, filter_kHz, sign)
293 else:
294 dr = qubx.fast.data.DoseResponse_Equil(Ntail)
295 for seg in reversed(segments):
296 for dose, response in izip(self.datasrc.gen_samples(reversed(seg.chunks), self.fit.controls.robot.main_hold, self.stim_ix),
297 self.datasrc.gen_samples(reversed(seg.chunks), self.fit.controls.robot.main_hold)):
298 if dose.included:
299 dr.add_data(dose.samples, response.samples)
300 if measure == HILL_MEASURE_PEAK:
301 dose, response = dr.result(HILL_MAX_RESULTS)
302 else:
303 dose, response, resp_std = dr.result(HILL_MAX_RESULTS)
304 small_doses = dose[dose>0.0]
305 zero_ish = min(small_doses) / numpy.e if len(small_doses) else 1e-8
306 if self.is_log:
307 logdose = numpy.log10(dose)
308 logdose[dose <= 0.0] = numpy.log10(zero_ish)
309 else:
310 logdose = dose
311 if sign > 0:
312 response -= numpy.min(response)
313 max_resp = numpy.max(response)
314 response /= max_resp
315 else:
316 response -= numpy.max(response)
317 max_resp = numpy.min(response)
318 response /= max_resp
319 if measure != HILL_MEASURE_PEAK:
320 resp_std /= max_resp
321 self.fit.controls.robot.set_data(logdose, response, [response, dose], ['I', 'L'])
322 self.fit.controls.robot.set_param('EC50', numpy.mean(dose))
323 self.__dose = dose
324 self.__dose_name = series[self.stim_ix]
325 gobject.idle_add(self.__onChangeModel)
326 gobject.idle_add(self.fit.controls.robot.fit)
327
328
330 """Follows the frontmost L{qubx.model.QubModel} and L{qubx.modelGTK.ModelView},
331 with events when anything changes.
332
333 @ivar OnSwitch: L{WeakEvent}(QubX.Models, model) when a new file is brought to front
334 @ivar OnChange: L{WeakEvent}(model) when something in the model changes (except a rate constant)
335 @ivar OnSetRate: L{WeakEvent}(rate_index, field_name, value, prev, is_undoing)
336 """
348 if model == self.__model: return
349 if self.__model:
350 for event in (self.__model.OnSetChannelCount,
351 self.__model.OnSetIeqFv,
352 self.__model.OnSetVRev,
353 self.__model.states.OnSet,
354 self.__model.states.OnInsert,
355 self.__model.states.OnRemoved,
356 self.__model.classes.OnSet,
357 self.__model.rates.OnInsert,
358 self.__model.rates.OnRemoved,
359 self.__model.constraints_kin.OnEdit):
360 event -= self.__ref(self.__onChangeModel)
361 self.__model.rates.OnSet -= self.__ref(self.__onSetRate)
362 self.__model = model
363 self.__view = models.view
364 if self.__model:
365 for event in (self.__model.OnSetChannelCount,
366 self.__model.OnSetIeqFv,
367 self.__model.OnSetVRev,
368 self.__model.states.OnSet,
369 self.__model.states.OnInsert,
370 self.__model.states.OnRemoved,
371 self.__model.classes.OnSet,
372 self.__model.rates.OnInsert,
373 self.__model.rates.OnRemoved,
374 self.__model.constraints_kin.OnEdit):
375 event += self.__ref(self.__onChangeModel)
376 self.__model.rates.OnSet += self.__ref(self.__onSetRate)
377 self.OnSwitch(model)
378 model = property(lambda self: self.__model)
379 view = property(lambda self: self.__view)
383 self.OnSetRate(r, field, val, prev, undoing)
384