source: sasview/src/sas/qtgui/Perspectives/Invariant/InvariantPerspective.py @ a281ab8

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since a281ab8 was a281ab8, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 8 years ago

Prototype DE↔perspective api based on QStandardItem.

  • Property mode set to 100755
File size: 22.8 KB
Line 
1# global
2import sys
3import os
4from PyQt4 import QtCore
5from PyQt4 import QtGui
6from PyQt4 import QtWebKit
7
8from twisted.internet import threads
9
10# sas-global
11from sas.sascalc.invariant import invariant
12from sas.sasgui.guiframe.dataFitting import Data1D
13from sas.qtgui.GuiUtils import Communicate
14from sas.qtgui.GuiUtils import updateModelItem
15
16# local
17from UI.TabbedInvariantUI import tabbedInvariantUI
18from InvariantDetails import DetailsDialog
19from Plotter import Plotter
20from InvariantUtils import WIDGETS
21
22# The minimum q-value to be used when extrapolating
23Q_MINIMUM = 1e-5
24# The maximum q-value to be used when extrapolating
25Q_MAXIMUM = 10
26# the ratio of maximum q value/(qmax of data) to plot the theory data
27Q_MAXIMUM_PLOT = 3
28
29
30class MyModel(object):
31    def __init__(self):
32        self._model = QtGui.QStandardItemModel(self)
33
34    def addItem(self, item):
35        item = QtGui.QStandardItem(str(item))
36        self._model.appendRow(item)
37
38class InvariantWindow(tabbedInvariantUI):
39    # The controller which is responsible for managing signal slots connections
40    # for the gui and providing an interface to the data model.
41    def __init__(self, manager=None, parent=None):
42        super(InvariantWindow, self).__init__(parent)
43        self.setWindowTitle("Invariant Perspective")
44        # initial input params
45        self._background = 0.0
46        self._scale = 1.0
47        self._contrast = 1.0
48        self._porod = 1.0
49        self._npoints_low = 10
50        self._npoints_high = 10
51        self._power_low = 4
52
53        self._manager = manager
54        self._reactor = self._manager.reactor()
55        self._model_item = QtGui.QStandardItem()
56
57        self._helpView = QtWebKit.QWebView()
58        self.detailsDialog = DetailsDialog(self)
59        self._plotter = Plotter(self)
60
61        self._low_extrapolate = False
62        self._low_guinier  = True
63        self._low_fit  = True
64        self._high_extrapolate = False
65        self._high_power_value  = False
66
67        self.communicate = Communicate()
68
69        # Mask file selector
70        ###################################################
71        self._path = "cyl_400_20.txt"
72        from sas.sascalc.dataloader.loader import  Loader
73        loader = Loader()
74        try:
75            self._data = loader.load(self._path)
76        except:
77            raise
78        ###################################################
79
80        self.lineEdit_8.setText(str(Q_MINIMUM))
81        self.lineEdit_9.setText(str(Q_MAXIMUM))
82
83        # Let's choose the Standard Item Model.
84        self.model = QtGui.QStandardItemModel(self)
85
86        # Connect buttons to slots.
87        # Needs to be done early so default values propagate properly.
88        self.setupSlots()
89
90        # Set up the model.
91        self.setupModel()
92
93        # Set up the mapper
94        self.setupMapper()
95
96    def communicator(self):
97        """
98        """
99        return self.communicate
100
101    def updateFromModel(self):
102        """
103        update the globals based on the data in the model
104        """
105        self._background = float(self.model.item(WIDGETS.W_BACKGROUND).text())
106        self._contrast   = float(self.model.item(WIDGETS.W_CONTRAST).text())
107        self._scale      = float(self.model.item(WIDGETS.W_SCALE).text())
108
109        # High extrapolate
110        self._low_extrapolate = ( str(self.model.item(WIDGETS.W_ENABLE_LOWQ).text()) == 'true')
111        self._low_points = float(self.model.item(WIDGETS.W_NPTS_LOWQ).text())
112        self._low_guinier  = ( str(self.model.item(WIDGETS.W_LOWQ_GUINIER).text()) == 'true')
113        self._low_fit  = ( str(self.model.item(WIDGETS.W_LOWQ_FIT).text()) == 'true')
114        self._low_power_value  = float(self.model.item(WIDGETS.W_LOWQ_POWER_VALUE).text())
115
116        # High extrapolating
117        self._high_extrapolate = ( str(self.model.item(WIDGETS.W_ENABLE_HIGHQ).text()) == 'true')
118        self._high_points  = float(self.model.item(WIDGETS.W_NPTS_HIGHQ).text())
119        self._high_fit  = ( str(self.model.item(WIDGETS.W_HIGHQ_FIT).text()) == 'true')
120        self._high_power_value  = float(self.model.item(WIDGETS.W_HIGHQ_POWER_VALUE).text())
121
122    def calculateInvariant(self):
123        """
124        Use twisted to thread the calculations away.
125        """
126        # Find out if extrapolation needs to be used.
127        extrapolation = None
128        if self._low_extrapolate  and not self._high_extrapolate:
129            extrapolation = "low"
130        elif not self._low_extrapolate  and self._high_extrapolate:
131            extrapolation = "high"
132        elif self._low_extrapolate and self._high_extrapolate:
133            extrapolation = "both"
134        try:
135            # modify the Calculate button to indicate background process
136            self.pushButton.setText("Calculating...")
137            self.pushButton.setEnabled(False)
138            self.style = self.pushButton.styleSheet()
139            self.pushButton.setStyleSheet("background-color: rgb(255, 255, 0); color: rgb(0, 0, 0)")
140            # Send the calculations to separate thread.
141            d = threads.deferToThread(self.calculateThread, extrapolation)
142            # Add deferred callback for call return
143            d.addCallback(self.plotResult)
144        except Exception as ex:
145            # Set the button back to available
146            self.pushButton.setEnabled(True)
147            self.pushButton.setText("Calculate")
148            self.pushButton.setStyleSheet(self.style)
149
150
151    def plotResult(self, model):
152        """
153        """
154        if self._low_extrapolate or self._high_extrapolate:
155            self._plotter.show()
156        self.model = model
157        self.mapper.toFirst()
158
159        # Set the button back to available
160        self.pushButton.setEnabled(True)
161        self.pushButton.setText("Calculate")
162        self.pushButton.setStyleSheet(self.style)
163
164        # Send the modified model item to DE for keeping in the model
165        self.communicate.updateModelFromPerspectiveSignal.emit(self._model_item)
166
167
168    def calculateThread(self, extrapolation):
169        """
170        Perform Invariant calculations.
171
172        TODO: Create a dictionary of results to be sent to DE on completion.
173        """
174        self.updateFromModel()
175
176        qstar_low = 0.0
177        qstar_low_err = 0.0
178        qstar_high = 0.0
179        qstar_high_err = 0.0
180        qstar_total = 0.0
181        qstar_total_low_err = 0.0
182
183        # Prepare the invariant object
184        inv = invariant.InvariantCalculator(data=self._data,
185                                            background = self._background,
186                                            scale = self._scale)
187
188        if self._low_extrapolate:
189            function_low = "power_law"
190            if self._low_guinier:
191                function_low = "guinier"
192            if self._low_fit:
193                self._low_power_value = None
194            inv.set_extrapolation(range="low",
195                                  npts=self._low_points,
196                                  function=function_low,
197                                  power=self._low_power_value)
198
199        if self._high_extrapolate:
200            function_low = "power_law"
201            inv.set_extrapolation(range="high",
202                                  npts=self._high_points,
203                                  function=function_low,
204                                  power=self._low_power_value)
205
206        #Compute invariant
207        # TODO: proper exception handling and logic -
208        # display info, update lineedits, don't run extrapolations etc.
209        calculation_failed = False
210        try:
211            qstar_total, qstar_total_error         = inv.get_qstar_with_error()
212        except Exception as ex:
213            calculation_failed = True
214            # Display relevant information
215            item = QtGui.QStandardItem("ERROR")
216            self.model.setItem(WIDGETS.W_INVARIANT, item)
217            item = QtGui.QStandardItem("ERROR")
218            self.model.setItem(WIDGETS.W_INVARIANT_ERR, item)
219        try:
220            volume_fraction, volume_fraction_error = \
221                inv.get_volume_fraction_with_error(self._contrast)
222        except Exception as ex:
223            calculation_failed = True
224            # Display relevant information
225            item = QtGui.QStandardItem("ERROR")
226            self.model.setItem(WIDGETS.W_VOLUME_FRACTION, item)
227            item = QtGui.QStandardItem("ERROR")
228            self.model.setItem(WIDGETS.W_VOLUME_FRACTION_ERR, item)
229        try:
230            surface, surface_error                 = \
231                inv.get_surface_with_error(self._contrast, self._porod)
232        except Exception as ex:
233            calculation_failed = True
234            # Display relevant information
235            item = QtGui.QStandardItem("ERROR")
236            self.model.setItem(WIDGETS.W_SPECIFIC_SURFACE, item)
237            item = QtGui.QStandardItem("ERROR")
238            self.model.setItem(WIDGETS.W_SPECIFIC_SURFACE_ERR, item)
239
240        if(calculation_failed):
241            # TODO: NOTIFY THE GUI MANAGER!!
242            self.mapper.toFirst()
243            return self.model
244
245        self._plotter.clean()
246        self._plotter.x_label("Q(A$^{-1}$)")
247        self._plotter.y_label("Intensity(cm$^{-1}$)")
248
249        if self._low_extrapolate:
250            # for presentation in InvariantDetails
251            qstar_low, qstar_low_err = inv.get_qstar_low()
252            extrapolated_data = inv.get_extra_data_low(self._low_points)
253            power_low = inv.get_extrapolation_power(range='low')
254
255            #inv.data = extrapolated_data
256            #qstar_total, qstar_total_error = inv.get_qstar_with_error()
257
258            # Plot the chart
259            title = "Low-Q extrapolation"
260            self._plotter.data(extrapolated_data)
261            self._plotter.title(title)
262            self._plotter.plot()
263
264            # Add the plot to the model item
265            variant_item = QtCore.QVariant(self._plotter)
266            updateModelItem(self._model_item, variant_item, title)
267
268        if self._high_extrapolate:
269            # for presentation in InvariantDetails
270            qmax_plot = Q_MAXIMUM_PLOT * max(self._data.x)
271            if qmax_plot > Q_MAXIMUM:
272                qmax_plot = Q_MAXIMUM
273            qstar_high, qstar_high_err = inv.get_qstar_high()
274            power_high = inv.get_extrapolation_power(range='high')
275            high_out_data = inv.get_extra_data_high(q_end=qmax_plot, npts=500)
276
277            # find how to add this plot to the existing plot for low_extrapolate
278            # Plot the chart
279            title = "High-Q extrapolation"
280            self._plotter.data(high_out_data)
281            self._plotter.title(title)
282            self._plotter.plot()
283
284            # Add the plot to the model item
285            variant_item = QtCore.QVariant(self._plotter)
286            updateModelItem(self._model_item, variant_item, title)
287
288
289        item = QtGui.QStandardItem(str(float('%.5g'% volume_fraction)))
290        self.model.setItem(WIDGETS.W_VOLUME_FRACTION, item)
291        item = QtGui.QStandardItem(str(float('%.5g'% volume_fraction_error)))
292        self.model.setItem(WIDGETS.W_VOLUME_FRACTION_ERR, item)
293        item = QtGui.QStandardItem(str(float('%.5g'% surface)))
294        self.model.setItem(WIDGETS.W_SPECIFIC_SURFACE, item)
295        item = QtGui.QStandardItem(str(float('%.5g'% surface_error)))
296        self.model.setItem(WIDGETS.W_SPECIFIC_SURFACE_ERR, item)
297        item = QtGui.QStandardItem(str(float('%.5g'% qstar_total)))
298        self.model.setItem(WIDGETS.W_INVARIANT, item)
299        item = QtGui.QStandardItem(str(float('%.5g'% qstar_total_error)))
300        self.model.setItem(WIDGETS.W_INVARIANT_ERR, item)
301
302        #item = QtGui.QStandardItem(str(float('%.5g'% qstar_total)))
303        #self.model.setItem(WIDGETS.D_TOTAL_QSTAR, item)
304        #item = QtGui.QStandardItem(str(float('%.5g'% qstar_total_err)))
305        #self.model.setItem(WIDGETS.D_TOTAL_QSTAR_ERR, item)
306        item = QtGui.QStandardItem(str(float('%.5g'% qstar_low)))
307        self.model.setItem(WIDGETS.D_LOW_QSTAR, item)
308        item = QtGui.QStandardItem(str(float('%.5g'% qstar_low_err)))
309        self.model.setItem(WIDGETS.D_LOW_QSTAR_ERR, item)
310        item = QtGui.QStandardItem(str(float('%.5g'% qstar_high)))
311        self.model.setItem(WIDGETS.D_HIGH_QSTAR, item)
312        item = QtGui.QStandardItem(str(float('%.5g'% qstar_high_err)))
313        self.model.setItem(WIDGETS.D_HIGH_QSTAR_ERR, item)
314
315        self.mapper.toFirst()
316
317        return self.model
318               
319    def title(self):
320        """
321        Perspective name
322        """
323        return "Invariant panel"
324
325    def status(self):
326        """
327        """
328        self.detailsDialog.setModel(self.model)
329        self.detailsDialog.showDialog()
330
331    def help(self):
332        """
333        """
334        _TreeLocation = "html/user/sasgui/perspectives/invariant/invariant_help.html"
335        self._helpView.load(QtCore.QUrl(_TreeLocation))
336        self._helpView.show()
337
338    def setupSlots(self):
339        self.pushButton.clicked.connect(self.calculateInvariant)
340        self.pushButton_2.clicked.connect(self.status)
341        self.pushButton_3.clicked.connect(self.help)
342
343        self.radioButton.toggled.connect(self.lowGuinierAndPowerToggle)
344        self.radioButton_8.toggled.connect(self.hiFitAndFixToggle)
345
346        self.radioButton_3.toggled.connect(self.lowFitAndFixToggle)
347
348        # Bug workaround for QDataWidgetMapper() not reacting to checkbox clicks.
349        # https://bugreports.qt.io/browse/QTBUG-1818
350        self.checkBox.clicked.connect(self.setFocus)
351        self.checkBox_2.clicked.connect(self.setFocus)
352
353        self.model.itemChanged.connect(self.modelChanged)
354
355    def modelChanged(self, item):
356        """
357        """
358        if item.row() == WIDGETS.W_ENABLE_LOWQ:
359            toggle = (str(item.text()) == 'true')
360            self._low_extrapolate = toggle
361            self.lowQToggle(toggle)
362        elif item.row() == WIDGETS.W_ENABLE_HIGHQ:
363            toggle = (str(item.text()) == 'true')
364            self._high_extrapolate = toggle
365            self.highQToggle(toggle)
366       
367    def lowGuinierAndPowerToggle(self, toggle):
368        """
369        """
370        self._low_guinier = toggle
371        toggle = not toggle
372        self.lineEdit_11.setEnabled(toggle)
373        self.radioButton_3.setEnabled(toggle)
374        self.radioButton_4.setEnabled(toggle)
375        self.lineEdit_11.setEnabled(toggle and (not self._low_fit))
376
377    def lowFitAndFixToggle(self, toggle):
378        """
379        """
380        self._low_fit = toggle
381        toggle = not toggle
382        self.lineEdit_11.setEnabled(toggle)
383
384    def hiFitAndFixToggle(self, toggle):
385        """
386        """
387        self.lineEdit_13.setEnabled(toggle)
388
389    def highQToggle(self, clicked):
390        """
391        Disable/enable High Q extrapolation
392        """
393        self.radioButton_7.setEnabled(clicked)
394        self.radioButton_8.setEnabled(clicked)
395        self.lineEdit_12.setEnabled(clicked)
396        self.lineEdit_13.setEnabled(clicked)
397
398    def lowQToggle(self, clicked):
399        """
400        Disable/enable Low Q extrapolation
401        """
402        self.radioButton.setEnabled(clicked)
403        self.radioButton_2.setEnabled(clicked)
404        self.lineEdit_11.setEnabled(clicked and not self._low_fit)
405
406        # Enable subelements
407        self.radioButton_3.setEnabled(clicked and not self._low_guinier)
408        self.radioButton_4.setEnabled(clicked and not self._low_guinier)
409        self.lineEdit_10.setEnabled(clicked and not self._low_guinier)
410
411    def setupModel(self):
412
413        # filename
414        item = QtGui.QStandardItem(self._path)
415        self.model.setItem(WIDGETS.W_FILENAME, item)
416
417        # add Q parameters to the model
418        qmin = min(self._data.x)
419        item = QtGui.QStandardItem(str(qmin))
420        self.model.setItem(WIDGETS.W_QMIN, item)
421        item = QtGui.QStandardItem(str(max(self._data.x)))
422        self.model.setItem(WIDGETS.W_QMAX, item)
423
424        # add custom input params
425        item = QtGui.QStandardItem(str(self._background))
426        self.model.setItem(WIDGETS.W_BACKGROUND, item)
427        item = QtGui.QStandardItem(str(self._contrast))
428        self.model.setItem(WIDGETS.W_CONTRAST, item)
429        item = QtGui.QStandardItem(str(self._scale))
430        self.model.setItem(WIDGETS.W_SCALE, item)
431       
432        # Dialog elements
433        itemf = QtGui.QStandardItem("false")
434        self.model.setItem(WIDGETS.W_ENABLE_HIGHQ, itemf)
435        itemf = QtGui.QStandardItem("false")
436        self.model.setItem(WIDGETS.W_ENABLE_LOWQ, itemf)
437
438        item = QtGui.QStandardItem(str(self._npoints_low))
439        self.model.setItem(WIDGETS.W_NPTS_LOWQ, item)
440        item = QtGui.QStandardItem(str(self._npoints_high))
441        self.model.setItem(WIDGETS.W_NPTS_HIGHQ, item)
442
443        itemt = QtGui.QStandardItem("true")
444        self.model.setItem(WIDGETS.W_LOWQ_GUINIER, itemt)
445
446        itemt = QtGui.QStandardItem("true")
447        self.model.setItem(WIDGETS.W_LOWQ_FIT, itemt)
448        item = QtGui.QStandardItem(str(self._power_low))
449        self.model.setItem(WIDGETS.W_LOWQ_POWER_VALUE, item)
450
451        itemt = QtGui.QStandardItem("true")
452        self.model.setItem(WIDGETS.W_HIGHQ_FIT, itemt)
453        item = QtGui.QStandardItem(str(self._power_low))
454        self.model.setItem(WIDGETS.W_HIGHQ_POWER_VALUE, item)
455
456
457    def setupMapper(self):
458        # Set up the mapper.
459        self.mapper = QtGui.QDataWidgetMapper(self)
460        self.mapper.setOrientation(QtCore.Qt.Vertical)
461        self.mapper.setModel(self.model)
462
463        # Set up the view on the model for testing
464        # self.tableView.setModel(self.model)
465
466        # Filename
467        self.mapper.addMapping(self.lineEdit, WIDGETS.W_FILENAME)
468        # Qmin/Qmax
469        self.mapper.addMapping(self.lineEdit_2, WIDGETS.W_QMIN)
470        self.mapper.addMapping(self.lineEdit_3, WIDGETS.W_QMAX)
471
472        # Background
473        self.mapper.addMapping(self.lineEdit_4, WIDGETS.W_BACKGROUND)
474        # Scale
475        self.mapper.addMapping(self.lineEdit_5, WIDGETS.W_SCALE)
476        # Contrast
477        self.mapper.addMapping(self.lineEdit_6, WIDGETS.W_CONTRAST)
478
479        # Lowq/Highq items
480        self.mapper.addMapping(self.checkBox, WIDGETS.W_ENABLE_LOWQ)
481        self.mapper.addMapping(self.checkBox_2, WIDGETS.W_ENABLE_HIGHQ)
482
483        self.mapper.addMapping(self.lineEdit_10, WIDGETS.W_NPTS_LOWQ)
484        self.mapper.addMapping(self.radioButton, WIDGETS.W_LOWQ_GUINIER)
485
486        self.mapper.addMapping(self.radioButton_3, WIDGETS.W_LOWQ_FIT)
487        self.mapper.addMapping(self.lineEdit_11, WIDGETS.W_LOWQ_POWER_VALUE)
488
489        self.mapper.addMapping(self.radioButton_7, WIDGETS.W_HIGHQ_FIT)
490        self.mapper.addMapping(self.lineEdit_13, WIDGETS.W_HIGHQ_POWER_VALUE)
491   
492        # Output
493        self.mapper.addMapping(self.lineEdit_14, WIDGETS.W_VOLUME_FRACTION)
494        self.mapper.addMapping(self.lineEdit_15, WIDGETS.W_VOLUME_FRACTION_ERR)
495        self.mapper.addMapping(self.lineEdit_16, WIDGETS.W_SPECIFIC_SURFACE)
496        self.mapper.addMapping(self.lineEdit_17, WIDGETS.W_SPECIFIC_SURFACE_ERR)
497        self.mapper.addMapping(self.lineEdit_19, WIDGETS.W_INVARIANT)
498        self.mapper.addMapping(self.lineEdit_18, WIDGETS.W_INVARIANT_ERR)
499
500        self.mapper.toFirst()
501
502    def setData(self, data_item):
503        """
504        Obtain a QStandardItem object and dissect it to get Data1D/2D
505        Pass it over to the calculator
506        """
507        if not isinstance(data_item, list):
508            msg = "Incorrect type passed to the Invariant Perspective"
509            raise AttributeError, msg
510
511        if not isinstance(data_item[0], QtGui.QStandardItem):
512            msg = "Incorrect type passed to the Invariant Perspective"
513            raise AttributeError, msg
514
515        self._model_item = data_item[0]
516
517        # Extract data on 1st child - this is the Data1D/2D component
518        data = self._model_item.child(0).data().toPyObject()
519
520        self.calculate(data_list=[data])
521       
522    def calculate(self, data_list=None):
523        """
524        receive a list of data and compute invariant
525
526        TODO: pass warnings/messages to log
527        """
528        msg = ""
529        data = None
530        if data_list is None:
531            data_list = []
532        if len(data_list) >= 1:
533            if len(data_list) == 1:
534                data = data_list[0]
535            else:
536                data_1d_list = []
537                data_2d_list = []
538                error_msg = ""
539                # separate data into data1d and data2d list
540                for data in data_list:
541                    if data is not None:
542                        if issubclass(data.__class__, Data1D):
543                            data_1d_list.append(data)
544                        else:
545                            error_msg += " %s  type %s \n" % (str(data.name),
546                                                              str(data.__class__.__name__))
547                            data_2d_list.append(data)
548                if len(data_2d_list) > 0:
549                    msg = "Invariant does not support the following data types:\n"
550                    msg += error_msg
551                if len(data_1d_list) == 0:
552                    # remake this as a qt event
553                    #wx.PostEvent(self.parent, StatusEvent(status=msg, info='error'))
554                    return
555
556                # TODO: add msgbox for data choice
557                #msg += "Invariant panel does not allow multiple data!\n"
558                #msg += "Please select one.\n"
559                #if len(data_list) > 1:
560                    #from invariant_widgets import DataDialog
561                    #dlg = DataDialog(data_list=data_1d_list, text=msg)
562                    #if dlg.ShowModal() == wx.ID_OK:
563                    #    data = dlg.get_data()
564                    #else:
565                    #    data = None
566                    #dlg.Destroy()
567
568            if data is None:
569                msg += "invariant receives no data. \n"
570                #wx.PostEvent(self.parent, StatusEvent(status=msg, info='error'))
571                return
572            if not issubclass(data.__class__, Data1D):
573                msg += "invariant cannot be computed for data of "
574                msg += "type %s\n" % (data.__class__.__name__)
575                #wx.PostEvent(self.parent, StatusEvent(status=msg, info='error'))
576                return
577            else:
578                #wx.PostEvent(self.parent, NewPlotEvent(plot=data, title=data.title))
579                try:
580                    self._data = data
581                    self._path = "unique path"
582                    self.calculateInvariant()
583                except:
584                    msg = "Invariant Set_data: " + str(sys.exc_value)
585                    #wx.PostEvent(self.parent, StatusEvent(status=msg, info="error"))
586        else:
587            msg = "invariant cannot be computed for data of "
588            msg += "type %s" % (data.__class__.__name__)
589            #wx.PostEvent(self.parent, StatusEvent(status=msg, info='error'))
590
591    def allowBatch(self):
592        """
593        Tell the caller that we don't accept multiple data instances
594        """
595        return False
596
597if __name__ == "__main__":
598    app = QtGui.QApplication([])
599    import qt4reactor
600    qt4reactor.install()
601    # DO NOT move the following import to the top!
602    # (unless you know what you're doing)
603    from twisted.internet import reactor
604    dlg = InvariantWindow(reactor)
605    dlg.show()
606    reactor.run()
Note: See TracBrowser for help on using the repository browser.