1 """Widget to display and control L{qubx.fit_robot}.
2
3 Copyright 2008-2013 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
23 import cStringIO
24 import gc
25 import gobject
26 import gtk
27 import numpy
28 import optparse
29 import os
30 import re
31 import sys
32 import traceback
33 import qubx.fit
34 import qubx.fit_robots
35 import qubx.GTK
36 import qubx.notebook
37 import qubx.pyenv
38 import qubx.pyenvGTK
39 import qubx.settings
40 import qubx.toolspace
41 import qubx.tree
42
43 from itertools import izip, count, repeat
44 from numpy import random
45 from math import *
46 from gtk import gdk, keysyms
47 from qubx.accept import acceptFloatOrUnset, acceptString, acceptIntGreaterThan, acceptFloatGreaterThan, acceptODEorF
48 from qubx.GTK import pack_item, pack_space, pack_hsep, pack_vsep, pack_label, pack_button, pack_check, pack_radio, pack_scrolled, build_menuitem
49 from qubx.toolspace import ColorInfo
50 from qubx.util_types import WeakEvent, Reffer, ScaleToColor, UNSET_VALUE, bind, bind_with_args, scrolled_float, Anon
51
52 COLOR_FITSPACE_BG = ('fit.bg', (1,1,1,1))
53 ColorInfo[COLOR_FITSPACE_BG[0]].label = 'Chart background'
54 COLOR_FITSPACE_GRID = ('fit.grid', (.3,1,.3,.5))
55 ColorInfo[COLOR_FITSPACE_GRID[0]].label = 'Chart grid lines'
56 COLOR_FITSPACE_GRIDZERO = ('fit.gridzero', (1,.3,.3,.5))
57 ColorInfo[COLOR_FITSPACE_GRIDZERO[0]].label = 'Chart grid zero line'
58 COLOR_FITSPACE_GRIDLBL = ('fit.gridlbl', (0,0,0,1))
59 ColorInfo[COLOR_FITSPACE_GRIDLBL[0]].label = 'Chart grid labels'
60 COLOR_FITSPACE_DATA = ('fit.data', (0,0,0,1))
61 ColorInfo[COLOR_FITSPACE_DATA[0]].label = 'Chart data'
62 COLOR_FITSPACE_FILL = ('fit.fill', (.5,.5,.5,1))
63 ColorInfo[COLOR_FITSPACE_FILL[0]].label = 'Chart fill (histogram bars)'
64 COLOR_FITSPACE_EXTRA = ('fit.extra', (0,0,1,.8))
65 ColorInfo[COLOR_FITSPACE_EXTRA[0]].label = 'Chart data secondary series'
66 COLOR_FITSPACE_CURVE = ('fit.curve', (1,0,0,.8))
67 ColorInfo[COLOR_FITSPACE_CURVE[0]].label = 'Chart fit curve'
68 COLOR_FITSPACE_LAYER = ('fit.layer', (1,.8,.33,.8))
69 ColorInfo[COLOR_FITSPACE_LAYER[0]].label = 'Chart layer background'
70 COLOR_FITSPACE_TEXT = ('fit.text', (0,.4,0,1))
71 ColorInfo[COLOR_FITSPACE_TEXT[0]].label = 'Chart text'
72 COLOR_FITSPACE_NUMBER = ('fit.number', (0,0,0,1))
73 ColorInfo[COLOR_FITSPACE_NUMBER[0]].label = 'Chart numbers'
74 COLOR_FITSPACE_BUTTON = ('fit.button', (.7,0,0,1))
75 ColorInfo[COLOR_FITSPACE_BUTTON[0]].label = 'Chart buttons'
76 COLOR_CORREL_LBL = ('fit.correl.lbl', (1,1,1,1))
77 ColorInfo[COLOR_CORREL_LBL[0]].label = 'Chart correlation numbers'
78 COLOR_FITSPACE_BACKBUTTON = ('fit.backbutton', (.2,.3,.2,1))
79 ColorInfo[COLOR_FITSPACE_BACKBUTTON[0]].label = 'Chart "back" from fitting'
80
81 LINE_EMS = 1.5
82
83 FITFMT = '%.4g'
84
85 ZOOM_SCROLL_FACTOR = .12
86
87 -def make_frame(w, h, appearance, controls_showing):
88 """Returns (left, top, right, bottom) pixel coords, telling where to draw the graph given that the whole space has dimensions (w,h)."""
89 em = appearance.emsize
90 line = em * LINE_EMS
91 if controls_showing:
92 aspect = 2.0
93 ah = h - 5*line
94 aw = w - 4*line
95 if aw > ah*aspect:
96 aw = ah*aspect
97 elif ah > aw/aspect:
98 ah = aw/aspect
99 cx = w*.55
100 x0 = max(line, min(w-2*line-aw, cx-aw/2))
101 y0 = h/2 - ah/2
102 else:
103 x0, y0 = 5*em, 3*line
104 aw = w - x0 - 6*em
105 ah = h - 2*y0
106 return x0, y0, x0+aw, y0+ah
107
115
123
124 -def gen_grid(u0, u1, z0, z1, appearance):
125 """Yields successive gridline u-coordinates along one dimension.
126
127 @param u0: lowest value showing
128 @param u1: highest value showing
129 @param z0: lowest x- or y- coord
130 @param z1: highest x- or y- coord
131 @param appearance: L{qubx.toolspace.TS_Appearance}
132 """
133 target_ppd = 7*appearance.emsize
134 target_u = abs(target_ppd * (u1-u0)/(z1-z0))
135 scale = pow(10, floor(log10(abs(target_u or 1.0))))
136 msd = target_u / scale
137 if msd < 1.6:
138 upg = scale
139 elif msd < 3.6:
140 upg = 2*scale
141 elif msd < 8:
142 upg = 5*scale
143 else:
144 upg = 10*scale
145 u = upg * floor(u0/upg)
146 if u < u0:
147 u += upg
148 while u <= u1:
149 yield u
150 u += upg
151
153 w = x1 - x0
154 return (x0 + w*p0, x0 + w*p1)
155
157 w = min(1.0, (p1 - p0)/factor)
158 hw = 0.5 * w
159 c = min(1.0 - hw, max(hw, (p0 + p1) / 2.0))
160 return (c-hw, c+hw)
161
163 p_in_frame = (p - p0) / (p1 - p0)
164 w = min(1.0, (p1 - p0)/factor)
165 return (max(0.0, p - p_in_frame*w), min(1.0, p + (1.0 - p_in_frame)*w))
166
168 dp = max(-p0, min(1.0 - p1, dp))
169 return (p0 + dp, p1 + dp)
170
172 """Toolspace layers to control a L{qubx.fit_robots.FitSession}.
173
174 @ivar robot: L{qubx.fit_robots.FitSession}
175 @ivar OnClickFit: L{WeakEvent}() when Fit button is clicked, to override default behavior (self.robot.fit())
176 """
180 """Creates a new layerset to control a L{qubx.fit_robot.FitRobot}, creating the robot too if None provided.
181
182 @param label: string to identify the robot, if one is created
183 @param cLayer: colorref for floating panels
184 @param cText: colorref for static text in floating panels
185 @param cNumber: colorref for variable/editable text in floating panels
186 @param cButton: colorref for clickable text in floating panels
187 @param global_name: how self might be called globally, for scripting
188 """
189 qubx.toolspace.LayerSet.__init__(self)
190 self.robot = qubx.fit_robots.FitSession(label)
191 self.my_robot = True
192 if global_name:
193 self.robot.global_name = '%s.robot' % global_name
194 elif not ('global_name' in self.robot.__dict__):
195 self.robot.global_name = None
196 self.sync = self.robot.sync
197 self.label = label
198 self.cLayer = cLayer
199 self.cText = cText
200 self.cNumber = cNumber
201 self.cButton = cButton
202 self.global_name = global_name
203 self.__ref = Reffer()
204 self.defer_scriptable_scroll = qubx.pyenv.DeferredScriptableScroll()
205 self.OnClickFit = WeakEvent()
206 cat = qubx.settings.SettingsMgr['qubx.fit_space.%s' % label]
207 self.profileCat = cat
208 self.profile = cat.active
209 cat.OnSet = self.__ref(self.update_profile)
210 self.presets = qubx.settings.SettingsMgr['qubx.fit_space.curves']
211 self.robot.OnData += self.__ref(self.__onData)
212 self.robot.OnWeight += self.__ref(self.__onWeight)
213 self.robot.OnExpr += self.__ref(self.__onExpr)
214 self.robot.OnParam += self.__ref(self.__onParam)
215 self.robot.OnMaxIter += self.__ref(self.__onMaxIter)
216 self.robot.OnToler += self.__ref(self.__onToler)
217 self.robot.OnStrategy += self.__ref(self.__onStrategy)
218 self.robot.OnFitter += self.__ref(self.__onFitter)
219 self.robot.OnStats += self.__ref(self.__onStats)
220 self.robot.OnStartFit += self.__ref(self.__onStartFit)
221 self.robot.OnIteration += self.__ref(self.__onIteration)
222 self.robot.OnEndFit += self.__ref(self.__onEndFit)
223
224 self.layExpr = qubx.toolspace.Layer(x=0, y=0, w=-.1, h=LINE_EMS, cBG=cLayer)
225 self.subExprLbl = qubx.toolspace.SubLayer_Label('y = ', -1, 0, x=0, y=0, w=3, h=LINE_EMS, color=cText,
226 action=self.__ref(self.__onClickExpr))
227 self.layExpr.add_sublayer(self.subExprLbl)
228 self.subExpr = qubx.toolspace.SubLayer_Label('', -1, 0, x=3, y=0, w=-32, h=LINE_EMS, color=cNumber,
229 action=self.__ref(self.__onClickExpr))
230 self.layExpr.add_sublayer(self.subExpr)
231 self.subWeightLbl = qubx.toolspace.SubLayer_Label('Weights: ', 1, 0, x=-32, y=0, w=6, h=LINE_EMS, color=cText,
232 action=self.__ref(self.__onClickWeight))
233 self.layExpr.add_sublayer(self.subWeightLbl)
234 self.subWeight = qubx.toolspace.SubLayer_Label('1.0', -1, 0, x=-25, y=0, w=13, h=LINE_EMS, color=cNumber,
235 action=self.__ref(self.__onClickWeight))
236 self.layExpr.add_sublayer(self.subWeight)
237 self.subPresets = qubx.toolspace.SubLayer_Popup(color=cText, on_popup=self.__ref(self.__onPresetsPopup), caption="Presets...",
238 x=-11, y=0, w=10, h=LINE_EMS)
239 self.subPresets.popup = gtk.Menu()
240 self.layExpr.add_sublayer(self.subPresets)
241 self.layers.append(self.layExpr)
242
243 self.layParams = qubx.toolspace.Layer(x=0, y=LINE_EMS, w=27, h=-1.3*LINE_EMS, cBG=cLayer)
244 self.layers.append(self.layParams)
245 self.sub_check = self.sub_label = self.sub_value = self.sub_pm = self.sub_err = self.sub_more = []
246
247 self.layStats = qubx.toolspace.Layer(x=-16, y=-7*LINE_EMS, w=15, h=4*LINE_EMS, cBG=cLayer)
248 for i, lbl in enumerate(['SSR: ', 'r2: ', 'P(runs): ', 'Correl: ']):
249 sub = qubx.toolspace.SubLayer_Label(lbl, 1, 0, x=0, y=i*LINE_EMS, w=6, h=LINE_EMS, color=cText)
250 self.layStats.add_sublayer(sub)
251 self.subSSR = qubx.toolspace.SubLayer_Label('', -1, 0, x=7, y=0, w=8, h=LINE_EMS, color=cNumber)
252 self.layStats.add_sublayer(self.subSSR)
253 self.subR2 = qubx.toolspace.SubLayer_Label('', -1, 0, x=7, y=LINE_EMS, w=8, h=LINE_EMS, color=cNumber)
254 self.layStats.add_sublayer(self.subR2)
255 self.subRunsProb = qubx.toolspace.SubLayer_Label('', -1, 0, x=7, y=2*LINE_EMS, w=8, h=LINE_EMS, color=cNumber)
256 self.layStats.add_sublayer(self.subRunsProb)
257 self.subCorrel = SubLayer_Correl(x=9, y=3*LINE_EMS, w=LINE_EMS, h=LINE_EMS, action=self.__ref(self.__onClickCorrel))
258 self.layStats.add_sublayer(self.subCorrel)
259 self.layers.append(self.layStats)
260
261 self.layBottom = qubx.toolspace.Layer(x=0, y=-LINE_EMS, w=-.1, h=LINE_EMS, cBG=cLayer)
262 self.subN = qubx.toolspace.SubLayer_Label('', -1, 0, x=-40, y=0, w=8, h=LINE_EMS, color=cText)
263 self.layBottom.add_sublayer(self.subN)
264 self.subStrategy = qubx.toolspace.SubLayer_Label('Strategy...', 0, 0, x=-18, y=0, w=9, h=LINE_EMS, color=cButton,
265 action=self.__ref(self.__onClickStrategy))
266 self.layBottom.add_sublayer(self.subStrategy)
267 self.subFit = qubx.toolspace.SubLayer_Label('Fit', 0, 0, x=-9, y=0, w=9, h=LINE_EMS, color=cButton,
268 action=self.__ref(self.__onClickFit))
269 self.layBottom.add_sublayer(self.subFit)
270 self.layers.append(self.layBottom)
271
272 self.layFitting = qubx.toolspace.Layer(x=1, y=-7*LINE_EMS, w=-17, h=4*LINE_EMS, cBG=cLayer)
273 self.subFitting = qubx.toolspace.SubLayer_Label('Fitting...', x=1, y=LINE_EMS, w=-1, h=LINE_EMS, color=cButton)
274 self.layFitting.add_sublayer(self.subFitting)
275 self.subProgress = qubx.toolspace.SubLayer_Progress(color=cButton, x=4, y=2.5*LINE_EMS, w=-4, h=LINE_EMS)
276 self.layFitting.add_sublayer(self.subProgress)
277
278
279 self.layMore = Layer_Param(cLayer, cText, cNumber, cButton, x=28, y=2*LINE_EMS)
280 self.layCorrel = Layer_Correl(cLayer, cText, cNumber, cButton, x=28, y=2*LINE_EMS)
281
282
283 self.winStrategy = StrategyDialog(label, self.robot)
284 self.winStrategy.OnClickFit += self.__ref(self.__onClickFit)
285
286 self.__xx = self.__yy = self.__vvv = self.__v_names = []
287 self.__weight = self.__curve = '1.0'
288 self.__curve_obj = qubx.fit.Curve(self.__curve, locals=self.robot.locals.__dict__)
289 self.__params = self.__vals = self.__lo = self.__hi = self.__can_fit = []
290 self.__max_iter = self.__toler = 1.0
291 self.__correlation = self.__is_pseudo = self.__std_err_est = self.__ssr = self.__r2 = self.__runs_prob = None
292 self.__iteration = 0
293 self.__fitting = self.__modal = False
294 self.__fit_receiver = None
295
296 self.update_profile(cat, self.profile)
297 self.robot.request_data()
298 self.robot.request_curve()
299 self.robot.request_fitparams()
300 self.robot.request_stats()
301
302 param_names = property(lambda self: self.__params)
303 param_vals = property(lambda self: self.__vals)
304 param_lo = property(lambda self: self.__lo)
305 param_hi = property(lambda self: self.__hi)
306 param_can_fit = property(lambda self: self.__can_fit)
307 weight_expr = property(lambda self: self.__weight)
308 curve_expr = property(lambda self: self.__curve)
309 curve = property(lambda self: self.__curve_obj, None, "copy of robot's qubx.fit.Curve, for computing fit curves on gui thread")
310 xx = property(lambda self: self.__xx)
311 yy = property(lambda self: self.__yy)
312 vvv = property(lambda self: self.__vvv)
313 v_names = property(lambda self: self.__v_names)
314 max_iter = property(lambda self: self.__max_iter)
315 toler = property(lambda self: self.__toler)
316 correlation = property(lambda self: self.__correlation)
317 correlation_is_pseudo = property(lambda self: self.__is_pseudo)
318 std_err_est = property(lambda self: self.__std_err_est)
319 ssr = property(lambda self: self.__ssr)
320 r2 = property(lambda self: self.__r2)
321 runs_prob = property(lambda self: self.__runs_prob)
322 fitting = property(lambda self: self.__fitting)
323
324 - def update_profile(self, settings=None, updates=None, curve_only=False):
349 self.subPresets.popup.foreach(lambda item: self.subPresets.popup.remove(item))
350 for name in sorted(qubx.fit.Curves.keys()):
351 build_menuitem(name, self.__ref(bind(self.__use_curve_class, name)), menu=self.subPresets.popup)
352 for name in self.presets.listNames():
353 build_menuitem(name, self.__ref(bind(self.__use_preset, name)), menu=self.subPresets.popup)
354 build_menuitem('Manage...', self.__ref(self.__onClickManagePresets), menu=self.subPresets.popup)
355 build_menuitem('Add to menu...', self.__ref(self.__onClickAddPreset), menu=self.subPresets.popup)
356 return True
393 """If you didn't provide a robot to the constructor, call this when you're done to stop the robot thread."""
394 if self.my_robot:
395 self.robot.dispose()
396 self.__ref.clear()
397 self.winStrategy.destroy()
399 """Waits for any previous robot commands to take effect before returning."""
400 qubx.pyenv.call_async_wait(self.robot.do)
402 """Waits for any previous robot commands to take effect before returning."""
403 qubx.pyenv.call_async_wait(self.robot.do)
404
405 - def __onData(self, xx, yy, vvv, v_names):
417 - def __onExpr(self, curve_name, curve_expr, params, param_vals, lo, hi, can_fit):
418 self.__curve = curve_expr
419 self.subExpr.label = curve_expr
420 self.__curve_obj = qubx.fit.Curves[curve_name](curve_expr, locals=self.robot.locals.__dict__)
421 self.__curve_obj.set_vars(self.__v_names)
422 self.__params = params[:]
423 self.__vals = param_vals[:]
424 self.__lo = lo[:]
425 self.__hi = hi[:]
426 self.__can_fit = can_fit[:]
427 self.layParams.clear_sublayers()
428 self.sub_check = []
429 self.sub_label = []
430 self.sub_value = []
431 self.sub_pm = []
432 self.sub_err = []
433 self.sub_more = []
434 self.__ref_expr = Reffer()
435 for i in xrange(len(params)):
436 sub = qubx.toolspace.SubLayer_Check(color=self.cButton, x=.5, y=i*LINE_EMS, w=1, h=LINE_EMS)
437 sub.set_active(can_fit[i])
438 sub.OnToggle += self.__ref_expr(bind(self.__onToggle, i))
439 self.sub_check.append(sub)
440 self.layParams.add_sublayer(sub)
441 sub = qubx.toolspace.SubLayer_Label('%i: %s' % (i+1, params[i]), -1, 0, x=2, y=i*LINE_EMS, w=6, h=LINE_EMS, color=self.cText,
442 action=self.__ref_expr(bind(self.__onToggle, i)))
443 self.sub_label.append(sub)
444 self.layParams.add_sublayer(sub)
445 sub = qubx.toolspace.SubLayer_Label(FITFMT%param_vals[i], 0, 0, x=8, y=i*LINE_EMS, w=7, h=LINE_EMS, color=self.cNumber,
446 action=self.__ref_expr(bind(self.__onClickValue, i)), scroll=self.__ref_expr(bind_with_args(self.__onScrollValue, i)))
447 self.sub_value.append(sub)
448 self.layParams.add_sublayer(sub)
449 sub = qubx.toolspace.SubLayer_Label('+/-', 0, 0, x=15, y=i*LINE_EMS, w=2, h=LINE_EMS, color=self.cText)
450 self.sub_pm.append(sub)
451 self.layParams.add_sublayer(sub)
452 sub = qubx.toolspace.SubLayer_Label('', 0, 0, x=17, y=i*LINE_EMS, w=7, h=LINE_EMS, color=self.cNumber)
453 self.sub_err.append(sub)
454 self.layParams.add_sublayer(sub)
455 sub = qubx.toolspace.SubLayer_Label('...', 0, 0, x=24, y=i*LINE_EMS, w=2, h=LINE_EMS, color=self.cButton,
456 action=self.__ref_expr(bind(self.__onClickMore, i)))
457 self.sub_more.append(sub)
458 self.layParams.add_sublayer(sub)
459 self.layParams.h = len(params) * LINE_EMS
460 self.profile['Name'].data = curve_name
461 self.profile['Eqn'].data = curve_expr
462 self.profile.remove(self.profile['Params'])
463 paramsNode = self.profile['Params']
464 for i, name in enumerate(params):
465 paramNode = paramsNode.append(name)
466 paramNode.data = [param_vals[i], float(can_fit[i]), lo[i], hi[i]]
467 self.robot.request_stats()
468 - def __onParam(self, index, name, value, lo, hi, can_fit):
492 self.profile['Fitter'].data = fitter_name
493 - def __onStats(self, correlation, is_pseudo, std_err_est, ssr, r2, runs_prob):
512 self.__fitting = True
513 self.space.add_layer(self.layFitting)
514 self.subFit.label = 'Stop'
521 self.__fitting = False
522 self.space.remove_layer(self.layFitting)
523 self.subFit.label = 'Fit'
524 for paramNode in qubx.tree.children(self.profile['Params']):
525 if paramNode.name in self.__params:
526 ix = self.__params.index(paramNode.name)
527 paramNode.data = [self.__vals[ix], float(self.__can_fit[ix]), self.__lo[ix], self.__hi[ix]]
529 def get_val(name):
530 name0 = name+'0'
531 pp = [i for i,p in enumerate(self.__params) if ((p == name) or (p == name0))]
532 if pp:
533 return self.__vals[pp[0]]
534 else:
535 return 1.0
536 if fObj.f:
537 return 'f(%s)' % ', '.join(('%s=%f'%(x, get_val(x)) for x in fObj.args))
538 else:
539 return 'dy(%s)' % ', '.join(('%s=%f'%(x, get_val(x)) for x in (fObj.args + fObj.ynames)))
546 if self.__fitting or self.__modal: return
547 dlg = qubx.pyenvGTK.HelpTestDialog(acceptODEorF(static=["x"], avail=self.__v_names, custom=True, locals=self.robot.locals.__dict__),
548 caption=self.__expr_caption(),
549 parent=None,
550 label="f(x) = ",
551 help_msg=FIT_SPACE_HELP,
552 bind=lambda val: qubx.pyenv.env.globals.__setitem__(val.f and 'f' or 'dy', val.f and val or val.dy),
553 write_test=self.write_test)
554 if gtk.RESPONSE_ACCEPT == dlg.run(self.__curve):
555 if self.global_name:
556 qubx.pyenv.env.scriptable_if_matching('%s.robot.set_expr(%s)' % (self.global_name, repr(dlg.expr)),
557 [(self.global_name, self)])
558 self.robot.set_expr(dlg.expr)
559 dlg.destroy()
580 if self.__fitting or self.__modal: return
581 dlg = qubx.GTK.NumEntryDialog('Edit parameter value', None, '%s = ' % self.__params[i],
582 self.__vals[i], float)
583 if gtk.RESPONSE_ACCEPT == dlg.run():
584 if self.global_name:
585 qubx.pyenv.env.scriptable_if_matching('%s.robot.set_param(%s, value=%s)' %
586 (self.global_name, repr(self.__params[i]), repr(dlg.value)),
587 [(self.global_name, self)])
588 self.robot.set_param(self.__params[i], value=dlg.value)
589 dlg.destroy()
600 if self.__fitting: return
601 if self.__modal:
602 self.layMore.click_ok()
603 gobject.idle_add(self.__onClickMore, i)
604 else:
605 self.__modal = True
606 self.layMore.run(self.space, self.__params[i], self.__vals[i], self.__lo[i], self.__hi[i], self.__std_err_est[i], self.__can_fit[i],
607 self.__onMoreOK, self.__onModalClose)
608 - def __onMoreOK(self, nm, value, lo, hi, can_fit):
609 if self.global_name:
610 qubx.pyenv.env.scriptable_if_matching('%s.robot.set_param(%s, value=%s, lo=%s, hi=%s, can_fit=%s)' %
611 (self.global_name, repr(nm),
612 repr(value), repr(lo), repr(hi), repr(can_fit)),
613 [(self.global_name, self)])
614 self.robot.set_param(nm, value, lo, hi, can_fit)
618 if self.__fitting or self.__modal: return
619 self.__modal = True
620 self.layCorrel.run(self.space, self.subCorrel.correlation, self.subCorrel.is_pseudo, self.__onModalClose)
622 if self.__modal or self.__fitting: return
623 self.winStrategy.hide()
624 self.winStrategy.show()
636 - def fit(self, wait=True, receiver=None):
637 if wait:
638 qubx.pyenv.call_async_wait(lambda rcv: self.fit(wait=False, receiver=rcv))
639 return
640 self.__fit_receiver = receiver
641 if self.OnClickFit:
642 self.OnClickFit()
643 else:
644 self.robot.fit()
646 if not (self.__std_err_est is None):
647 return "%s parameters:\n\t"%self.label + "\n\t".join(["%s:\t%.6g%s" % (name, val, (not active) and " (constant)" or (" +/- %.6g" % err)) for name, val, active, err in izip(self.__params, self.__vals, self.__can_fit, self.__std_err_est[:len(self.__params)])])
648 else:
649 return "%s parameters:\n\t"%self.label + "\n\t".join(["%s:\t%.6g%s" % (name, val, (not active) and " (constant)" or "") for name, val, active in izip(self.__params, self.__vals, self.__can_fit)])
651 if not len(self.__xx):
652 return 0, 0
653 return len(self.__xx), 3+len(self.__vvv)
655 return ['X', 'Y', 'Fit'] + self.__v_names
657 if c == 0:
658 return self.__xx
659 elif c == 1:
660 return self.__yy
661 elif c == 2:
662 return self.__curve_obj.eval(self.__vals, self.__xx, self.__vvv)
663 else:
664 return self.__vvv[c-3]
667
668
669 -class FitSpace(qubx.toolspace.ToolSpace):
670 """Mirrors and controls a L{qubx.fit_robot.FitRobot}.
671
672 @ivar controls: L{FitControls}
673 @ivar controlsHidden: minimal L{LayerSet}
674 @ivar draw_data: True (default) to draw data and fit curve
675 @ivar data_width: line width of data trace, in ems
676 @ivar data_rad: radius of data points, in ems
677 @ivar extra_width: line width of additional series
678 @ivar extra_rad: radius of additional series' data points
679 @ivar curve_width: line width of curve
680 @ivar caption: text across the top, when controls are hidden
681 @ivar global_name: how self might be called globally, for scripting
682 @ivar t2x: f(x) -> t
683 @ivar x2t: f(t) -> x
684 @ivar d2y: f(data value) -> y
685 @ivar y2d: f(y) -> data value
686 @ivar u2y: list of f(extra series value) -> y
687 @ivar y2u: list of f(y) -> extra series value
688 """
689
690 __explore_featured = ['controls', 'controlsHidden', 'draw_data', 'data_width', 'data_rad', 'extra_width', 'extra_rad',
691 'curve_width', 'caption', 'global_name', 't2x', 'x2t', 'd2y', 'y2d', 'u2y', 'y2u',
692 'cBG', 'cGrid', 'cData', 'cExtra', 'cCurve', 'cFill', 'cGridZero', 'cGridLbl', 'hist_width',
693 'draw_hist', 'draw_fit_when_hidden', 'xzoom', 'yzoom', 't0', 't1', 'd0', 'd1',
694 'dispose', 'click_show_fit_controls', 'click_hide_fit_controls', 'update_coords_from_mouse',
695 'nb_get_series', 'nb_picture_get_shape', 'nb_picture_draw', 'set_bounds']
696
697 - def __init__(self, label='Fit',
698 cBG=COLOR_FITSPACE_BG, cGrid=COLOR_FITSPACE_GRID, cData=COLOR_FITSPACE_DATA, cExtra=COLOR_FITSPACE_EXTRA, cCurve=COLOR_FITSPACE_CURVE,
699 cLayer=COLOR_FITSPACE_LAYER, cText=COLOR_FITSPACE_TEXT, cNumber=COLOR_FITSPACE_NUMBER, cButton=COLOR_FITSPACE_BUTTON,
700 cFill=COLOR_FITSPACE_FILL, cGridZero=COLOR_FITSPACE_GRIDZERO, cGridLbl=COLOR_FITSPACE_GRIDLBL, global_name=""):
701 """Creates a new view on a L{qubx.fit_robot.FitRobot}, creating the robot too if None provided.
702
703 @param label: string to identify the robot, if one is created
704 @param cBG: L{qubx.toolspace}-style colorref for the background
705 @param cGrid: colorref for grid lines
706 @param cData: colorref for data series
707 @param cExtra: colorref for additional series
708 @param cCurve: colorref for trend line
709 @param cLayer: colorref for floating panels
710 @param cText: colorref for static text in floating panels
711 @param cNumber: colorref for variable/editable text in floating panels
712 @param cButton: colorref for clickable text in floating panels
713 @param cFill: colorref for histogram bar interior
714 @param cGridZero: colorref for gridlines at zero
715 @param cGridLbl: colorref for grid labels
716 """
717 qubx.toolspace.ToolSpace.__init__(self, can_focus=True)
718 self.set_size_request(100, 40)
719 self.controls = FitControls(label, cLayer, cText, cNumber, cButton,
720 global_name=(global_name and ("%s.controls" % global_name) or ""))
721 self.label = label
722 self.cBG = cBG
723 self.cGrid = cGrid
724 self.cData = cData
725 self.cExtra = cExtra
726 self.cCurve = cCurve
727 self.cFill = cFill
728 self.cGridZero = cGridZero
729 self.cGridLbl = cGridLbl
730 self.__global_name = global_name
731 self.hist_width = 0.95
732 self.__ref = Reffer()
733 self.OnDraw += self.__ref(self.__onDraw)
734 self.controls.robot.OnData += self.__ref(self.__onData)
735 self.controls.robot.OnExpr += self.__ref(self.__onExpr)
736 self.controls.robot.OnParam += self.__ref(self.__onParam)
737 self.controls.robot.OnIteration += self.__ref(self.__onIteration)
738 self.controls.robot.OnStatus += self.__ref(self.__onStatus)
739 self.controls.robot.OnStats += self.__ref(self.__onStats)
740
741 self.tool = self.cursor = FitSpaceCursor(self)
742
743 self.controlsHidden = qubx.toolspace.LayerSet()
744 self.layCaption = qubx.toolspace.Layer(x=0, y=0, w=-.1, h=LINE_EMS, cBG=cLayer)
745 self.subCaption = qubx.toolspace.SubLayer_Label("", 0, 0, x=0, y=0, w=-11, h=LINE_EMS, color=cText)
746 self.layCaption.add_sublayer(self.subCaption)
747 self.controlsHidden.layers.append(self.layCaption)
748
749 self.layShowFit = qubx.toolspace.Layer(x=1, y=-3-LINE_EMS, w=2, h=2, cBG=qubx.toolspace.COLOR_CLEAR)
750 self.subShowFit = SubLayer_FitIcon(action=self.__ref(self.__onClickShowFit))
751 self.subShowFit.tooltip = 'Curve fitting...'
752 self.layShowFit.add_sublayer(self.subShowFit)
753 self.controlsHidden.layers.append(self.layShowFit)
754
755 self.layBottomHidden = qubx.toolspace.Layer(x=0, y=-LINE_EMS, w=-.1, h=LINE_EMS, cBG=cLayer)
756 self.subCoordsHidden = qubx.toolspace.SubLayer_Label('', -1, 0, x=0, y=0, w=-40, h=LINE_EMS, color=cText)
757 self.layBottomHidden.add_sublayer(self.subCoordsHidden)
758 self.subEditBounds = qubx.toolspace.SubLayer_Label('Edit Bounds...', 1, 0, x=-10, y=0, w=9, h=LINE_EMS, color=cButton,
759 action=self.__ref(self.__onClickEditBounds))
760 self.layBottomHidden.add_sublayer(self.subEditBounds)
761 self.controlsHidden.layers.append(self.layBottomHidden)
762
763 self.subCoords = qubx.toolspace.SubLayer_Label('', -1, 0, x=0, y=0, w=-40, h=LINE_EMS, color=cText)
764 self.controls.layBottom.add_sublayer(self.subCoords)
765
766 self.layHideControls = qubx.toolspace.Layer(x=1, y=-3-LINE_EMS, w=2, h=2, cBG=qubx.toolspace.COLOR_CLEAR)
767 self.subHideControls = qubx.toolspace.SubLayer_Arrow(color=COLOR_FITSPACE_BACKBUTTON, angle=pi, w=2, h=2,
768 action=self.__ref(self.__onClickHideControls))
769 self.subHideControls.tooltip = 'Hide fitting controls'
770 self.layHideControls.add_sublayer(self.subHideControls)
771 self.controls.layers.append(self.layHideControls)
772
773 self.layerset = self.controls
774
775 self.layZoom = qubx.toolspace.Layer(x=-2*LINE_EMS-0.5, y=2, w=2*LINE_EMS, h=LINE_EMS, cBG=qubx.toolspace.COLOR_WHITE)
776 self.add_layer(self.layZoom)
777 self.subZoomOut = qubx.toolspace.SubLayer_SmoothZoom('-', zoom_out=True, x=0, y=0, w=LINE_EMS, h=LINE_EMS)
778 self.subZoomOut.OnZoom += self.__ref(self.__onZoom)
779 self.layZoom.add_sublayer(self.subZoomOut)
780 self.subZoomIn = qubx.toolspace.SubLayer_SmoothZoom('+', x=LINE_EMS, y=0, w=LINE_EMS, h=LINE_EMS)
781 self.subZoomIn.OnZoom += self.__ref(self.__onZoom)
782 self.layZoom.add_sublayer(self.subZoomIn)
783
784 self.__xx = self.__yy = self.__vvv = self.__v_names = []
785 self.__params = self.__vals = self.__lo = self.__hi = self.__can_fit = []
786
787 self.__data_width = .3
788 self.__data_rad = .5
789 self.__extra_width = .2
790 self.__extra_rad = 0
791 self.__curve_width = .5
792 self.__draw_data = True
793 self.__draw_hist = False
794 self.__draw_fit_when_hidden = True
795 self.__caption = ""
796
797 self.__x0 = self.__t0 = self.__y0 = self.__d0 = 0
798 self.__x1 = self.__t1 = self.__y1 = self.__d1 = 1
799 self.__u0 = self.__u1 = []
800 self.__xzoom = (0.0, 1.0)
801 self.__yzoom = (0.0, 1.0)
802 self.__t0z, self.__t1z, self.__d0z, self.__d1z = self.__x0, self.__x1, self.__d0, self.__d1
803 self.t2x = self.x2t = self.d2y = self.y2d = lambda x: x
804 self.u2y = self.y2u = []
805
806 data_width = property(lambda self: self.__data_width, lambda self, x: self.set_data_width(x))
807 data_rad = property(lambda self: self.__data_rad, lambda self, x: self.set_data_rad(x))
808 extra_width = property(lambda self: self.__extra_width, lambda self, x: self.set_extra_width(x))
809 extra_rad = property(lambda self: self.__extra_rad, lambda self, x: self.set_extra_rad(x))
810 curve_width = property(lambda self: self.__curve_width, lambda self, x: self.set_curve_width(x))
811 caption = property(lambda self: self.__caption, lambda self, x: self.set_caption(x))
812 draw_data = property(lambda self: self.__draw_data, lambda self, x: self.set_draw_data(x))
813 draw_hist = property(lambda self: self.__draw_hist, lambda self, x: self.set_draw_hist(x))
814 draw_fit_when_hidden = property(lambda self: self.__draw_fit_when_hidden,
815 lambda self, x: self.set_draw_fit_when_hidden(x))
816 xzoom = property(lambda self: self.__xzoom, lambda self, x: self.set_xzoom(x))
817 yzoom = property(lambda self: self.__yzoom, lambda self, x: self.set_yzoom(x))
818 global_name = property(lambda self: self.__global_name, lambda self, x: self.set_global_name(x))
819 t0 = property(lambda self: self.__t0)
820 t1 = property(lambda self: self.__t1)
821 d0 = property(lambda self: self.__d0)
822 d1 = property(lambda self: self.__d1)
823
855 self.__caption = x
856 self.subCaption.label = x
858 """Override to update your NbItems global_names; remember to call FitSpace.set_global_name(x)."""
859 self.__global_name = x
861 """If you didn't provide a robot to the constructor, call this when you're done to stop the robot thread."""
862 self.controls.dispose()
863 self.__ref.clear()
865 x0, x1 = apply_zoom_axis(self.t0, self.t1, *self.xzoom)
866 y0, y1 = apply_zoom_axis(self.d0, self.d1, *self.yzoom)
867 x0, y0, x1, y1 = PromptChartBounds(x0, y0, x1, y1, self.global_name, self.t0, self.d0, self.t1, self.d1)
868 if not (x0 is None):
869 self.set_bounds(x0, y0, x1, y1)
871 w = self.t1 - self.t0
872 h = self.d1 - self.d0
873 self.xzoom = ((x0 - self.t0) / w, (x1 - self.t0) / w)
874 self.yzoom = ((y0 - self.d0) / h, (y1 - self.d0) / h)
890 """Updates the lower-left display of data-coords under the mouse, given mouse-coords x and y."""
891 if self.extra_rad or self.extra_width:
892 extras = lambda: ''.join([' %s: %10.6g' % (self.__v_names[i], self.y2u[i](y)) for i in xrange(1,len(self.__v_names))])
893 else:
894 extras = lambda: ''
895 lbl = 'X: %10.6g Y: %10.6g%s' % (self.x2t(x), self.y2d(y), extras())
896 self.subCoords.label = lbl
897 self.subCoordsHidden.label = lbl
902 d0a = self.__d0 if (d0 is None) else d0
903 d1a = self.__d1 if (d1 is None) else d1
904 d0z, d1z = apply_zoom_axis(d0a, d1a, *self.__yzoom)
905 y2d, d2y = make_y_transform(d0z, d1z, self.__y0, self.__y1)
906 if self.extra_rad or self.extra_width:
907 context.set_source_rgba(*self.appearance.color(self.cExtra))
908 context.set_line_width(self.extra_width * self.appearance.emsize * self.appearance.line_width)
909 draw_line(context, self.appearance, self.__xx, arr, self.t2x, d2y, self.extra_rad)
911 self.__x0, self.__y0, self.__x1, self.__y1 = make_frame(w, h, self.appearance, not (self.layerset == self.controlsHidden))
912 self.__t0z, self.__t1z = apply_zoom_axis(self.__t0, self.__t1, *self.__xzoom)
913 self.__d0z, self.__d1z = apply_zoom_axis(self.__d0, self.__d1, *self.__yzoom)
914 self.x2t, self.t2x = make_x_transform(self.__t0z, self.__t1z, self.__x0, self.__x1)
915 self.y2d, self.d2y = make_y_transform(self.__d0z, self.__d1z, self.__y0, self.__y1)
916 self.y2u = []; self.u2y = []
917 for s0, s1 in izip(self.__u0, self.__u1):
918 s0z, s1z = apply_zoom_axis(s0, s1, *self.__yzoom)
919 y2s, s2y = make_y_transform(s0z, s1z, self.__y0, self.__y1)
920 self.y2u.append(y2s)
921 self.u2y.append(s2y)
922
923 context.set_source_rgba( *self.appearance.color(self.cBG) )
924 context.rectangle(0, 0, w, h)
925 context.fill()
926
927 if not self.__draw_data:
928 return
929
930
931
932
933
934
935
936 context.set_line_width(self.data_width * self.appearance.emsize * self.appearance.line_width)
937 if self.__draw_hist:
938 draw_hist(context, self.appearance, self.__xx, self.__yy, self.t2x, self.d2y, self.cData, self.cFill, self.hist_width)
939 else:
940 context.set_source_rgba(*self.appearance.color(self.cData))
941 draw_line(context, self.appearance, self.__xx, self.__yy, self.t2x, self.d2y, self.data_rad)
942
943
944 if (self.layerset == self.controls) or self.__draw_fit_when_hidden:
945 if not self.controls.curve.can_resample:
946 xx = self.__xx
947 ff = self.controls.robot.curve.last_fit
948 else:
949 try:
950
951 x0 = max(0, int(floor(self.t2x(self.__t0))))
952 x1 = min(w-1, int(floor(self.t2x(self.__t1))))
953 xx = numpy.array([self.x2t(x) for x in xrange(x0, x1+1)])
954 ff = self.controls.curve.eval(self.__vals, xx)
955 except:
956
957 xx = self.__xx
958 ff = self.controls.curve.eval(self.__vals, self.__xx, self.__vvv)
959 context.set_source_rgba(*self.appearance.color(self.cCurve))
960 context.set_line_width(self.curve_width * self.appearance.emsize * self.appearance.line_width)
961 draw_line(context, self.appearance, xx, ff, self.t2x, self.d2y, 0)
962
963
964 set_grid_color = lambda v: context.set_source_rgba( *self.appearance.color((abs(v)>1e-7) and self.cGrid or self.cGridZero) )
965 context.set_line_width(self.appearance.line_width)
966 for t in gen_grid(self.x2t(0), self.x2t(w), 0, w, self.appearance):
967 set_grid_color(t)
968 x = self.t2x(t)
969 context.move_to(x, 0)
970 context.line_to(x, h)
971 context.stroke()
972 context.set_source_rgba(*self.appearance.color(self.cGridLbl))
973 context.save()
974 context.translate(x-self.appearance.emsize/5, h-LINE_EMS*self.appearance.emsize)
975 context.scale(0.7, 0.7)
976 context.rotate(-pi/2)
977 context.move_to(0, 0)
978 context.show_text((abs(t)<=1e-7) and "0" or ('%.2g' % t))
979 context.restore()
980 for d in gen_grid(self.y2d(h), self.y2d(0), h, 0, self.appearance):
981 set_grid_color(d)
982 y = self.d2y(d)
983 context.move_to(0, y)
984 context.line_to(w, y)
985 context.stroke()
986 context.set_source_rgba(*self.appearance.color(self.cGridLbl))
987 context.save()
988 context.translate(self.appearance.emsize, y-self.appearance.emsize/5)
989 context.scale(0.7, 0.7)
990 context.move_to(0, 0)
991 context.show_text((abs(d)<=1e-7) and "0" or ('%.2g' % d))
992 context.restore()
993
994
995 - def __onData(self, xx, yy, vvv, v_names):
996 self.__xx = xx
997 self.__yy = yy
998 self.__vvv = vvv
999 self.__v_names = v_names
1000 if len(xx):
1001 self.__t0 = min(xx)
1002 self.__t1 = max(xx)
1003 self.__d0 = min(yy)
1004 self.__d1 = max(yy)
1005 self.__u0 = [min(vv) for vv in vvv]
1006 self.__u1 = [max(vv) for vv in vvv]
1007 else:
1008 self.__t0 = 0.0
1009 self.__t1 = 1.0
1010 self.__d0 = 0.0
1011 self.__d1 = 1.0
1012 self.__u0 = [0.0] * len(v_names)
1013 self.__u1 = [1.0] * len(v_names)
1014 if self.draw_hist and (len(xx) > 1):
1015 self.__t0 -= (xx[1] - xx[0])/2
1016 self.__t1 += (xx[-1] - xx[-2])/2
1017 self.redraw_canvas(True)
1018 - def __onExpr(self, curve_name, curve_expr, params, param_vals, lo, hi, can_fit):
1025 - def __onParam(self, index, name, value, lo, hi, can_fit):
1037 - def __onStats(self, correlation, is_pseudo, std_err_est, ssr, r2, runs_prob):
1046
1047
1048 -def draw_line(context, appearance, tt, uu, t2x, u2y, rad):
1049 if (len(tt) == 0) or (len(uu) == 0): return
1050 rad = rad * appearance.emsize;
1051 x = t2x(tt[0])
1052 y = u2y(uu[0])
1053 context.move_to(x, y)
1054 for t,u in izip(tt, uu):
1055 x = t2x(t)
1056 y = u2y(u)
1057 context.line_to(x, y)
1058 context.stroke()
1059 if rad:
1060 context.arc(x, y, rad, 0, 2*pi)
1061 context.fill()
1062 context.move_to(x, y)
1063
1064 -def draw_hist(context, appearance, tt, uu, t2x, u2y, color, fill_color, hist_width=0.8):
1065 N = min(len(tt), len(uu))
1066 if N <= 1:
1067 return
1068 y0 = u2y(0.0)
1069 for i in xrange(N):
1070 t, u = tt[i], uu[i]
1071 if i == 0:
1072 dt = tt[1] - t
1073 else:
1074 dt = t - tt[i-1]
1075 x_left = t2x(t - 0.5*dt*hist_width)
1076 x_right = t2x(t + 0.5*dt*hist_width)
1077 y1 = u2y(u)
1078 context.set_source_rgba(*appearance.color(fill_color))
1079 context.rectangle(x_left, y0, x_right-x_left, y1-y0)
1080 context.fill_preserve()
1081 context.set_source_rgba(*appearance.color(color))
1082 context.stroke()
1083
1084
1087 gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
1088 self.__ref = Reffer()
1089 self.OnClickFit = WeakEvent()
1090 self.robot = robot
1091 self.robot.OnFitter += self.__ref(self.__onFitter)
1092 self.robot.OnMaxIter += self.__ref(self.__onMaxIter)
1093 self.robot.OnToler += self.__ref(self.__onToler)
1094 self.robot.OnStrategy += self.__ref(self.__onStrategy)
1095 self.robot.OnStartFit += self.__ref(self.__onStartFit)
1096 self.robot.OnEndFit += self.__ref(self.__onEndFit)
1097 self.set_title('%s - %s Strategy' % (qubx.pyenv.env.globals['QubX'].appname, label))
1098 self.connect('delete-event', self.__onDelete)
1099 self.set_size_request(400, 300)
1100 self.vbox = gtk.VBox()
1101 self.vbox.show()
1102 self.add(self.vbox)
1103
1104 line = pack_item(gtk.HBox(), self.vbox)
1105 pack_label('Fitter:', line)
1106 self.mnuFitter = pack_item(qubx.GTK.DynamicComboBox(), line, expand=True)
1107 self.mnuFitter.OnPopulate += self.__ref(self.__onPopulateFitter)
1108 self.mnuFitter.OnChanged += self.__ref(self.__onChangeFitter)
1109
1110 line = pack_item(gtk.HBox(), self.vbox)
1111 pack_label('Max iterations:', line)
1112 self.txtMaxIter = pack_item(qubx.GTK.NumEntry(1, acceptIntGreaterThan(0), width_chars=5), line)
1113 self.txtMaxIter.OnChange += self.__ref(self.__onChangeMaxIter)
1114 pack_label('Toler:', line)
1115 self.txtToler = pack_item(qubx.GTK.NumEntry(1e-5, acceptFloatGreaterThan(0.0), width_chars=8), line)
1116 self.txtToler.OnChange += self.__ref(self.__onChangeToler)
1117 line = pack_item(gtk.HBox(), self.vbox)
1118 pack_label('Strategy:', line)
1119 self.btnFit = pack_button('Fit', line, at_end=True, on_click=self.__onClickFit)
1120 self.btnAdd = pack_button('Add a step', line, at_end=True, on_click=self.__onClickAdd)
1121 self.btnReset = pack_button('Reset', line, at_end=True, on_click=self.__onClickReset)
1122
1123 self.vsplit = pack_item(gtk.VPaned(), self.vbox, expand=True)
1124 self.txtStrategy = gtk.TextView()
1125 qubx.GTK.SetFixedWidth(self.txtStrategy)
1126 scroll = pack_scrolled(self.txtStrategy, None, size_request=(100, 70))
1127 self.vsplit.pack1(scroll, True, True)
1128 self.txtOutput = qubx.pyenvGTK.PythonOut(self.robot.OnOutput)
1129 self.txtOutput.show()
1130 self.vsplit.pack2(self.txtOutput, True, True)
1131
1132 self.robot.request_fitparams()
1133 self.robot.request_strategy()
1134 self.robot.request_fitter_name()
1135
1149 self.mnuFitter.choose(fitter_name)
1151 self.txtMaxIter.value = maxiter
1157 self.txtStrategy.set_editable(False)
1158 self.btnAdd.set_sensitive(False)
1159 self.btnFit.set_sensitive(False)
1160 self.mnuFitter.set_sensitive(False)
1161 self.txtMaxIter.set_sensitive(False)
1162 self.txtToler.set_sensitive(False)
1164 self.txtStrategy.set_editable(True)
1165 self.btnAdd.set_sensitive(True)
1166 self.btnFit.set_sensitive(True)
1167 self.mnuFitter.set_sensitive(True)
1168 self.txtMaxIter.set_sensitive(True)
1169 self.txtToler.set_sensitive(True)
1205
1206
1207 TheFitCopyDialog = qubx.GTK.CopyDialog('Copy fit curve image')
1208
1210 """Tracks mouse coordinates in a L{FitSpace}."""
1220 self.press_t, self.press_d = self.space.x2t(x), self.space.y2d(y)
1221 self.press_x2t, self.press_y2d = self.space.x2t, self.space.y2d
1222 self.press_xzoom, self.press_yzoom = self.space.xzoom, self.space.yzoom
1243
1249 if self.space.layerset != self.space.controls:
1250 if e.keyval in (ord('f'), ord('F')):
1251 self.space.click_show_fit_controls()
1252 gobject.idle_add(self.space.grab_focus)
1253 return
1254 else:
1255 if e.keyval in (ord('f'), ord('F')):
1256 self.space.click_hide_fit_controls()
1257 gobject.idle_add(self.space.grab_focus)
1258 return
1259 if e.keyval in (keysyms.Return, keysyms.KP_Enter, keysyms.space):
1260 self.space.controls.click_fit()
1261 return
1262 kmap = [('Y', self.space.controls.subExpr),
1263 ('W', self.space.controls.subWeight),
1264 ('P', self.space.controls.subPresets),
1265 ('S', self.space.controls.subStrategy),
1266 ('C', self.space.controls.subCorrel)]
1267 kmap.extend([(str(i+1), sub) for i, sub in enumerate(self.space.controls.sub_more)])
1268 for ltr, sub in kmap:
1269 if e.keyval in (ord(ltr), ord(ltr.lower())):
1270 sub.action(0, 0, Anon(state=e.state, time=e.time, button=0, x=0, y=0))
1271 return
1272
1273
1274
1275
1276
1277 BaseFitSpaceCursor.onKeyPress(self, e)
1278
1280 - def __init__(self, space, layer, ok, cancel, clickable=[]):
1330
1331
1333 """Displays a square matrix that has all entries -1 <= x <= 1, as a square subdivided into squares of color; see L{ScaleToColor}.
1334
1335 @ivar correlation: a square matrix with all entries -1 <= x <= 1, such as the cross-correlation of fit parameters.
1336 @ivar is_pseudo: True if correlation could not be calculated strictly (covariance was not positive definite); ignored for now.
1337 """
1345 correlation = property(lambda self: self.__correlation, lambda self, x: self.set_correlation(x))
1346 - def draw(self, context, w, h, appearance):
1347 qubx.toolspace.SubLayer.draw(self, context, w, h, appearance)
1348 context.rectangle(0, 0, self.w, self.h)
1349 context.set_source_rgba(0,0,0,1)
1350 context.fill()
1351 if self.__correlation is None: return
1352 N = min(self.__correlation.shape)
1353 if N == 0: return
1354 dx = float(self.w) / N
1355 dy = float(self.h) / N
1356 for i in xrange(N):
1357 for j in xrange(N):
1358 context.set_source_rgb( *ScaleToColor(self.__correlation[i,j]) )
1359 context.rectangle(i*dx, j*dy, dx, dy)
1360 context.fill()
1361
1362
1364 """Displays one square from a matrix that has all entries -1 <= x <= 1, as a color and number; see L{ScaleToColor}."""
1370 - def draw(self, context, w, h, appearance):
1371 qubx.toolspace.SubLayer.draw(self, context, w, h, appearance)
1372 context.rectangle(0, 0, self.w, self.h)
1373 context.set_source_rgba(*self.color)
1374 context.fill()
1375
1376
1377 context.save()
1378 context.scale(0.5, 0.5)
1379 fascent, fdescent, fheight, fxadvance, fyadvance = context.font_extents()
1380 xbearing, ybearing, width, height, xadvance, yadvance = context.text_extents(self._label)
1381 vtotal = fascent + fdescent
1382 by = (2*self.h - vtotal)/2 + fascent
1383 bx = (2*self.w - width)/2 - xbearing
1384 context.set_source_rgba(*appearance.color(self.cNumber))
1385 context.move_to(bx, by)
1386 context.show_text(self._label)
1387 context.restore()
1388
1389
1391 """Displays a large-size, labeled cross-correlation matrix, modally."""
1392 - def __init__(self, cBG, cText, cNumber, cButton, *args, **kw):
1393 if not ('h' in kw):
1394 kw['h'] = 10*LINE_EMS
1395 kw['w'] = 14*LINE_EMS
1396 qubx.toolspace.Layer.__init__(self, cBG=cBG, *args, **kw)
1397 self.__ref = Reffer()
1398 self.subX = qubx.toolspace.SubLayer_Label('X', 0, 1, x=-LINE_EMS, y=0, w=LINE_EMS, h=LINE_EMS, color=cButton,
1399 action=self.__ref(self.__onClickOK))
1400 self.add_sublayer(self.subX)
1401 self.subTitle = qubx.toolspace.SubLayer_Label('Cross-Correlation', 0, 1, x=1, y=0, w=-1, h=LINE_EMS, color=cText)
1402 self.add_sublayer(self.subTitle)
1403 self.subDisclaimer = qubx.toolspace.SubLayer_Label('', 0, 1, x=1, y=-2.5*LINE_EMS, w=-1, h=LINE_EMS, color=cText)
1404 self.add_sublayer(self.subDisclaimer)
1405 self.subOK = qubx.toolspace.SubLayer_Label('OK', 0, 1, x=-10, y=-1.5*LINE_EMS, w=9, h=1.5*LINE_EMS, color=cButton,
1406 action=self.__ref(self.__onClickOK))
1407 self.add_sublayer(self.subOK)
1408 self.sub_cells = []
1413 - def run(self, space, correlation, is_pseudo, on_close):
1426 - def __layout(self, space, correlation):
1427 M, N = correlation.shape
1428 em = space.appearance.emsize
1429 px = py = min((self.w - 2*em), (self.h - 4*LINE_EMS*em))
1430 x0, y0 = (self.w - px) / (2*em), (self.h - 2*LINE_EMS*em - py) / (2*em)
1431 dx, dy = px / (em*M), py / (em*N)
1432 for i in xrange(M):
1433 for j in xrange(N):
1434 sub = SubLayer_CorrelSquare(correlation[i,j], x=x0+i*dx, y=y0+j*dy, w=dx, h=dy)
1435 self.sub_cells.append(sub)
1436 self.add_sublayer(sub)
1437
1438
1439
1441 """Presents details about one parameter, as when the user clicks "..." in the upper-left param list; you run() it like a dialog."""
1442 - def __init__(self, cBG, cText, cNumber, cButton, *args, **kw):
1443 if not ('h' in kw):
1444 kw['h'] = 9*LINE_EMS
1445 if not ('w' in kw):
1446 kw['w'] = 14*LINE_EMS
1447 qubx.toolspace.Layer.__init__(self, cBG=cBG, *args, **kw)
1448 self.__ref = Reffer()
1449 for i,lbl in enumerate(['Name:', 'Value:', 'Low:', 'High:', '+/-']):
1450 sub = qubx.toolspace.SubLayer_Label(lbl, 1, 0, x=0, y=i*LINE_EMS, w=6, h=LINE_EMS, color=cText)
1451 self.add_sublayer(sub)
1452 self.subName = qubx.toolspace.SubLayer_Label('', -1, 0, x=7, y=0, w=7, h=LINE_EMS, color=cNumber)
1453 self.add_sublayer(self.subName)
1454 self.subValue = qubx.toolspace.SubLayer_Label('', -1, 0, x=7, y=LINE_EMS, w=7, h=LINE_EMS, color=cNumber,
1455 action=self.__ref(self.__onClickValue), scroll=self.__ref(self.__onScrollValue))
1456 self.add_sublayer(self.subValue)
1457 self.subLo = qubx.toolspace.SubLayer_Label('', -1, 0, x=7, y=2*LINE_EMS, w=7, h=LINE_EMS, color=cNumber,
1458 action=self.__ref(self.__onClickLow))
1459 self.add_sublayer(self.subLo)
1460 self.subHi = qubx.toolspace.SubLayer_Label('', -1, 0, x=7, y=3*LINE_EMS, w=7, h=LINE_EMS, color=cNumber,
1461 action=self.__ref(self.__onClickHigh))
1462 self.add_sublayer(self.subHi)
1463 self.subErr = qubx.toolspace.SubLayer_Label('', -1, 0, x=7, y=4*LINE_EMS, w=7, h=LINE_EMS, color=cNumber)
1464 self.add_sublayer(self.subErr)
1465
1466 self.subChk = qubx.toolspace.SubLayer_Check(color=cButton, x=2, y=5*LINE_EMS, w=1.5, h=LINE_EMS)
1467 self.subChk.OnToggle += self.__ref(self.__onToggle)
1468 self.add_sublayer(self.subChk)
1469 self.subChkLbl = qubx.toolspace.SubLayer_Label('fit this parameter', -1, 0, x=4, y=5*LINE_EMS, w=-1, h=LINE_EMS, color=cText,
1470 action=self.__ref(self.__onToggle))
1471 self.add_sublayer(self.subChkLbl)
1472
1473 self.subCancel = qubx.toolspace.SubLayer_Label('Cancel', 0, 1, x=-20, y=7*LINE_EMS, w=9, h=1.5*LINE_EMS, color=cButton,
1474 action=self.__ref(self.__onClickCancel))
1475 self.add_sublayer(self.subCancel)
1476 self.subOK = qubx.toolspace.SubLayer_Label('OK', 0, 1, x=-10, y=7*LINE_EMS, w=9, h=1.5*LINE_EMS, color=cButton,
1477 action=self.__ref(self.__onClickOK))
1478 self.add_sublayer(self.subOK)
1479 self.subX = qubx.toolspace.SubLayer_Label(' X ', 0, 1, x=-2, y=0, w=2, h=1.5*LINE_EMS, color=cButton,
1480 action=self.__ref(self.__onClickOK))
1481 self.add_sublayer(self.subX)
1482
1507 self.can_fit = not self.can_fit
1508 self.subChk.set_active(self.can_fit)
1518 if self.space:
1519 self.space.tool = self.prev_tool
1520 self.space.remove_layer(self)
1521 gobject.idle_add(self.on_ok, self.nm, self.val, self.lo, self.hi, self.can_fit)
1522 gobject.idle_add(self.on_close)
1523 - def run(self, space, nm, val, lo, hi, err, can_fit, on_ok, on_close):
1524 """Displays this layer on the space; fills the blanks with (nm, val, lo, hi, err, can_fit); returns immediately.
1525
1526 @param on_ok: when the user clicks OK, this layer is hidden, then it calls on_ok(nm, val, lo, hi, can_fit)
1527 @param on_close: when the user clicks Cancel, or after on_ok, it calls on_close()
1528 """
1529 self.nm = nm
1530 self.val = val
1531 self.lo = lo
1532 self.hi = hi
1533 self.can_fit = can_fit
1534 self.on_ok = on_ok
1535 self.on_close = on_close
1536
1537 self.prev_tool = space.tool
1538 space.tool = FitSpaceModalCursor(space, self, self.click_ok, self.click_cancel,
1539 [self.subValue, self.subLo, self.subHi, self.subChk, self.subCancel, self.subOK])
1540
1541 self.subName.label = nm
1542 self.subValue.label = FITFMT%val
1543 self.subLo.label = (lo != UNSET_VALUE) and (FITFMT%lo) or ''
1544 self.subHi.label = (hi != UNSET_VALUE) and (FITFMT%hi) or ''
1545 self.subErr.label = FITFMT%err
1546 self.subChk.set_active(can_fit)
1547
1548 space.add_layer(self)
1549
1550
1551
1553 w = int(w)
1554 h = int(h)
1555 margin = min(w, h)/6
1556 cr.save()
1557 cr.set_line_width(4.0)
1558 cr.set_source_rgba(.5,.5,.5,.5)
1559 cr.rectangle(margin, margin, w-2*margin, h-2*margin)
1560 cr.stroke_preserve()
1561 cr.set_source_rgba(0,0,0,.8)
1562 cr.fill()
1563 margin += 2
1564 cr.translate(margin, margin)
1565 ptrad = max(.25, (min(w,h) - 2*margin)/10.0)
1566 dx = max(1e-6, 1.0 / (w - 2*margin))
1567 dy = max(1e-6, 1.0 / (h - 2*margin))
1568 f = lambda x: h*(-.5*(2*x-1)**2 + .5)
1569 cr.set_source_rgba(1, 1, 1, .6)
1570 for i in xrange(w/2):
1571 x = random.uniform()
1572 cr.arc(x/dx, ((h-2*margin) - f(x) + ptrad*random.normal()), ptrad, 0, 2*pi)
1573 cr.fill()
1574 cr.set_source_rgba(1, 0, 0, .5)
1575 cr.set_line_width(2.0*ptrad)
1576 cr.move_to(0, (h-2*margin)-f(0))
1577 for i in xrange(1, w-2*margin):
1578 cr.line_to(i, (h-2*margin)-f(i*dx))
1579 cr.stroke()
1580 cr.restore()
1581
1583 - def draw(self, context, w, h, appearance):
1586
1587
1588
1589 MAX_SERIES_NAME_LEN = 32
1590
1592 """Returns nm, modified to be a legal python identifier."""
1593 alnum = re.sub(r"[^a-zA-Z0-9_]", '_', nm)
1594 safe = re.sub(r"(^[0-9])", r"_\1", alnum)
1595 if safe == 'x': safe = '_x_'
1596 return safe[:MAX_SERIES_NAME_LEN]
1597
1599 """Returns (series, names) read from file-like object f.
1600 The file should be tab- or comma-separated text. The first line can have column labels.
1601 Returns C{series}, a list of numpy.array(dtype='float32'), one per column,
1602 and C{names}, a list of str identifiers. Blank names are replaced with Y%i. If there's
1603 only one column, an ordinal series 'X' is prepended.
1604 """
1605 names = []
1606 series = []
1607 N = 0
1608 splitter = re.compile(r"(?:, ?)|(?:\t)")
1609 for line in f:
1610 toks = splitter.split(line.strip(' \r\n'))
1611
1612 while toks and toks[0] == '':
1613 toks = toks[1:]
1614 while toks and toks[-1] == '':
1615 toks = toks[:-1]
1616
1617 toks = [x or 0.0 for x in toks]
1618 if not toks: continue
1619 for i in xrange(len(series), len(toks)):
1620 series.append([0.0]*N)
1621 names.append('')
1622 try:
1623 nums = map(float, toks)
1624 for i, num in enumerate(nums):
1625 series[i].append(num)
1626 for i in xrange(len(toks), len(series)):
1627 series[i].append(0.0)
1628 N += 1
1629 except ValueError, e:
1630 for i, tok in enumerate(toks):
1631 if (not names[i]) and tok:
1632 names[i] = SafeName(tok)
1633
1634 if len(series) == 1:
1635 series.insert(0, [float(x) for x in xrange(N)])
1636 names.insert(0, 'X')
1637 elif not names[0]:
1638 names[0] = 'X'
1639 for i in xrange(len(series)):
1640 if not names[i]:
1641 names[i] = 'Y%d' % i
1642 series = [numpy.array(ser, dtype='float32') for ser in series]
1643 return series, names
1644
1645 -def pick_xy(series, names, x_name, y_name):
1646 """Given (series, names) from L{read_data}, and preferred x- and y- series names, returns the most appropriate 2 series.
1647
1648 @return: (x_series, y_series, index of x_series, index of y_series)
1649 """
1650 if x_name and (x_name in series):
1651 xi = names.index(x_name)
1652 else:
1653 xi = 0
1654 if y_name and (y_name in series):
1655 yi = names.index(y_name)
1656 else:
1657 yi = 1
1658 return series[xi], series[yi], xi, yi
1659
1661 usage = "usage: %prog [options] file.txt"
1662 parser = optparse.OptionParser(usage)
1663 parser.add_option('-e', '--expr', dest='expr', default='m*x + b', help='fit curve expression')
1664 parser.add_option('-x', '--x-name', dest='x_name', default='', help='x column header or index')
1665 parser.add_option('-y', '--y-name', dest='y_name', default='', help='y column header or index')
1666 options, args = parser.parse_args()
1667 expr = options.expr
1668 x_name = options.x_name
1669 y_name = options.y_name
1670
1671 if len(args) > 1:
1672 print 'usage: %prog [options] {file.txt or -} (- reads stdin until eof)'
1673 sys.exit(1)
1674 if (len(args) == 0) or (args[0] == '-'):
1675 series, names = read_data(sys.stdin)
1676 else:
1677 series, names = read_data(open(args[0], 'r'))
1678
1679 if (len(series) == 0) or (len(series[0]) == 0):
1680 print 'no data'
1681 sys.exit(1)
1682 xx, yy, xi, yi = pick_xy(series, names, x_name, y_name)
1683 yname = names[yi]
1684 for i in reversed(sorted([xi, yi])):
1685 del series[i]
1686 del names[i]
1687 series = [yy] + series
1688 names = [yname] + names
1689
1690 gobject.threads_init()
1691 win = gtk.Window(gtk.WINDOW_TOPLEVEL)
1692 win.set_title('qubx.fit_space %s' % os.path.split(args[0])[1])
1693 win.connect('destroy', lambda w: gtk.main_quit())
1694
1695 fitspace = FitSpace()
1696 win.add(fitspace)
1697 fitspace.show()
1698 win.show()
1699
1700
1701 qubx.toolspace.Appearance.set_font_size(13)
1702
1703 fitspace.robot.set_data(xx, yy, series, names)
1704 fitspace.robot.set_expr(expr)
1705
1706 try:
1707 gtk.main()
1708 except KeyboardInterrupt:
1709 pass
1710 except:
1711 traceback.print_exc()
1712 fitspace.robot.stop()
1713
1714
1715 FIT_SPACE_HELP = """You can enter either a function, such as
1716 amp * exp(-x / tau)
1717 or a system of differential equations, such as
1718 y1' = a*y2; y2' = b*y1
1719 The first integrated variable (e.g. y1) will be used for fitting.
1720
1721 You can use the minimum/maximum values of any variable, e.g.
1722 hi("y") - lo("y")
1723 x - lo("x")
1724 hi("OtherSeriesByName")
1725
1726 Expressions use Python syntax, except that the single quote (') is reserved for differential equations.
1727 Full documentation is available at www.python.org.
1728
1729 Parameter names must begin with a letter, and contain only letters, numbers, and _ (underscore). They are case sensitive.
1730
1731 Construct piecewise functions using "and" and "or"
1732 ((x<0.0) and (-1e21)) or ((x==0) and -1e20) or log(x)
1733 """ + qubx.pyenv.PYTHON_HELP
1734
1735
1736 -def PromptChartBounds(x0, y0, x1, y1, title=None, t0=None, d0=None, t1=None, d1=None):
1737 dlg = gtk.Dialog('Edit chart bounds%s' % (title and (" (%s)"%title) or ""), qubx.GTK.get_active_window(), gtk.DIALOG_MODAL)
1738 line = pack_item(gtk.HBox(True), dlg.vbox)
1739 pack_label('x0:', line)
1740 txtX0 = pack_item(qubx.GTK.NumEntry(x0, format='%.5g'), line)
1741 line = pack_item(gtk.HBox(True), dlg.vbox)
1742 pack_label('x1:', line)
1743 txtX1 = pack_item(qubx.GTK.NumEntry(x1, format='%.5g'), line)
1744 line = pack_item(gtk.HBox(True), dlg.vbox)
1745 pack_label('y0:', line)
1746 txtY0 = pack_item(qubx.GTK.NumEntry(y0, format='%.5g'), line)
1747 line = pack_item(gtk.HBox(True), dlg.vbox)
1748 pack_label('y1:', line)
1749 txtY1 = pack_item(qubx.GTK.NumEntry(y1, format='%.5g'), line)
1750 def on_click_reset(btn):
1751 if d1 is None:
1752 txtX0.value, txtX1.value = x0, x1
1753 txtY0.value, txtY1.value = y0, y1
1754 else:
1755 txtX0.value, txtX1.value = t0, t1
1756 txtY0.value, txtY1.value = d0, d1
1757 pack_button('Reset', dlg.action_area, on_click_reset)
1758 pack_button('Cancel', dlg.action_area, bind(dlg.response, gtk.RESPONSE_REJECT))
1759 pack_button('OK', dlg.action_area, bind(dlg.response, gtk.RESPONSE_ACCEPT))
1760 response = dlg.run()
1761 x0, x1, y0, y1 = txtX0.value, txtX1.value, txtY0.value, txtY1.value
1762 dlg.destroy()
1763 if response == gtk.RESPONSE_ACCEPT:
1764 return x0, y0, x1, y1
1765 else:
1766 return None, None, None, None
1767
1768 if __name__ == '__main__':
1769 main()
1770
1771
1772
1773
1774