#!/usr/bin/env python import sys, math, os, random from PyQt4 import QtGui, QtCore pattern_count_filename = os.environ.get("PATTERN_COUNT_FILE") def parse_counts_from_fn(pattern_count_filename): print "Going to try reading", pattern_count_filename, "..." inp = open(pattern_count_filename, 'rU') pcd = [float(i.strip()) for i in inp if i.strip()] assert len(pcd) > 7 for i in pcd: assert i >= 0.0 return pcd if pattern_count_filename: try: pattern_count_data = parse_counts_from_fn(pattern_count_filename) except: sys.exit("Expecting %s to be a file of pattern counts (one-per line)." % pattern_count_filename) else: pattern_count_data = [0]*8 class BranchEnum: A, B, INTERNAL, C, D = 0, 1, 2, 3, 4 names = ["A", "B", "Internal", "C", "D"] # ratios of dimensions for 45 degree lines HYP_OVER_HORIZ = math.sqrt(2) HORIZ_OVER_HYP = 1/HYP_OVER_HORIZ HYP_OVER_VERT = math.sqrt(2) VERT_OVER_HYP = 1/HYP_OVER_VERT TOL = 0.000001 NUM_STATES = 2 MAX_BRANCH_LEN = (NUM_STATES - 1.0)/NUM_STATES AS_CENTRAL_WIDGET = False if AS_CENTRAL_WIDGET: TreeWindowBase = QtGui.QWorkspace else: TreeWindowBase = QtGui.QDialog class TopologyEnum: AB, AC, AD = 0, 1, 2 names = ["AB | CD", "AC | BD", "AD | BC"] colors = [QtCore.Qt.red, QtCore.Qt.blue, QtCore.Qt.green] txt_fmt = ['%s', '%s', '%s'] best_txt_fmt = ['%s', '%s', '%s'] SHOW_DATA_BARS = [True, True, True, True] def randomly_choose_indices(p, n): """Treats `p` as a list of category probabilities. Returns as list of `n` indices that were randomly drawn from this discrete probability distribution. Does not verify that sum(p) == 1.0. All rounding error is "given" to the list category. """ last_ind = len(p) - 1 c = [0]*len(p) for i in xrange(n): u = random.random() ind = 0 while ind < last_ind: u -= p[ind] if u < 0.0: break ind += 1 c[ind] = c[ind] + 1 return c class MultiBarWidget(QtGui.QWidget): def __init__(self, parent): self.h = [] QtGui.QWidget.__init__(self, parent) self.max_dim = 300 self.max_prob = 1.0 self.px_per_unit_prob = self.max_dim/self.max_prob self.bar_dim = 10 self.bar_skip = 0 self.setMinimumHeight(4.5*(self.bar_dim+ self.bar_skip)) self.setMinimumWidth(self.max_dim) def paintEvent(self, event): paint = QtGui.QPainter() paint.begin(self) colors = [QtCore.Qt.black, QtCore.Qt.red, QtCore.Qt.blue, QtCore.Qt.green, ] y = 0 for n, b in enumerate(self.h): if SHOW_DATA_BARS[n]: w = b*self.px_per_unit_prob r = QtCore.QRect(0, y, w, self.bar_dim) curr_color = colors[n] paint.fillRect(r, curr_color) #print 0, y, w, self.bar_dim y += self.bar_dim + self.bar_skip paint.end() class LnLWorkspace(TreeWindowBase): """Internally the tree calculations are done as if we have a AB|CD tree. Then patterns and branch lengths are swapped into the correct order. """ def __init__(self, prob_sources, parent=None): QtGui.QWidget.__init__(self, parent) assert(len(prob_sources) == 3) self.prob_sources = prob_sources abp, acp, adp = [i.calc_pat_probs() for i in self.prob_sources] self.setWindowTitle("Data and Likelihoods") gridLayout = QtGui.QGridLayout() dheader = QtGui.QLabel("Data (counts)") gridLayout.addWidget(dheader, 0, 0) tax_labels = QtGui.QLabel("A B C D") gridLayout.addWidget(tax_labels, 0, 1) self.showDataChkBox = QtGui.QCheckBox("Data") self.showAB = QtGui.QCheckBox("AB") self.showAC = QtGui.QCheckBox("AC") self.showAD = QtGui.QCheckBox("AD") self.showBarsBoxes = [self.showDataChkBox, self.showAB, self.showAC, self.showAD] for el in self.showBarsBoxes: el.setChecked(True) self.connect(el, QtCore.SIGNAL('stateChanged(int)'), self.show_bars_changed) gridLayout.addWidget(self.showDataChkBox, 0, 2) gridLayout.addWidget(self.showAB, 0, 3) gridLayout.addWidget(self.showAC, 0, 4) gridLayout.addWidget(self.showAD, 0, 5) patternCountValidator = QtGui.QDoubleValidator(self) patternCountValidator.setBottom(0.0) self.data_labels = [] self.data_counts = [] self.plot_widget_list = [] for i, bits in enumerate(['000', '001', '010', '011', '100', '101', '110', '111']): lab = QtGui.QLabel("0 %s" % " ".join(bits)) vlab = QtGui.QLineEdit() vlab.setMaximumWidth(100) vlab.setText("0") vlab.setValidator(patternCountValidator) self.data_labels.append(lab) self.data_counts.append(vlab) plot_widget = MultiBarWidget(self) plot_widget.h = [0.0, abp[i], acp[i], adp[i]] self.plot_widget_list.append(plot_widget) gridLayout.addWidget(vlab, 1 + i, 0) gridLayout.addWidget(lab, 1 + i, 1) gridLayout.addWidget(plot_widget, 1 + i, 2, 1, 3) for el in self.data_counts: self.connect(el, QtCore.SIGNAL('textChanged(QString)'), self.counts_changed) w = QtGui.QLabel('ln(Likelihood)') gridLayout.addWidget(w, 9, 0, 1, 2) w = QtGui.QLabel('Parsimony score') gridLayout.addWidget(w, 10, 0, 1, 2) # font for the text boxes for the lnL self.notBestFont = QtGui.QFont() self.bestFont = QtGui.QFont() self.bestFont.setUnderline(True) self.lnL_labels = [QtGui.QLabel(''), QtGui.QLabel(''), QtGui.QLabel('')] for n, el in enumerate(self.lnL_labels): gridLayout.addWidget(el, 9, 2 + n) self.pars_labels = [QtGui.QLabel(''), QtGui.QLabel(''), QtGui.QLabel('')] for n, el in enumerate(self.pars_labels): gridLayout.addWidget(el, 10, 2 + n) self.setLayout(gridLayout) self.resize(570, 500) def show_bars_changed(self): for n, el in enumerate(self.showBarsBoxes): if el.isChecked(): SHOW_DATA_BARS[n] = True else: SHOW_DATA_BARS[n] = False self.repaint() def counts_changed(self): abp, acp, adp = [i.calc_pat_probs() for i in self.prob_sources] c = [float(i.text() or 0) for i in self.data_counts] #print c t = sum(c) have_data = True if t == 0.0: t = 1.0 have_data = False for n, el in enumerate(c): self.plot_widget_list[n].h = [el/t, abp[n], acp[n], adp[n]] self.plot_widget_list[n].repaint() if have_data: lnL_values = self.calc_ln_L() pars_scores = self.calc_pars_scores() #print pars_scores highest_index = lnL_values.index(max(lnL_values)) most_pars_index = pars_scores.index(min(pars_scores)) as_str = ["%6.2f" % i for i in lnL_values] for n, el in enumerate(self.lnL_labels): if n == highest_index: el.setFont(self.bestFont) else: el.setFont(self.notBestFont) fmt_str = TopologyEnum.txt_fmt[n] txt = TopologyEnum.txt_fmt[n] % as_str[n] el.setText(txt) el.show() as_str = ["%6.2f" % i for i in pars_scores] for n, el in enumerate(self.pars_labels): if n == most_pars_index: el.setFont(self.bestFont) else: el.setFont(self.notBestFont) fmt_str = TopologyEnum.txt_fmt[n] txt = TopologyEnum.txt_fmt[n] % as_str[n] el.setText(txt) el.show() self.repaint() def probs_changed(self): self.counts_changed() self.repaint() def get_counts(self): c = [] for i in self.data_counts: try: x = float(i.text()) except: x = 0.0 c.append(x) return c def calc_pars_scores(self): c = self.get_counts() non_const = sum(c[1:]) homoplasy_ab = c[5] + c[6] homoplasy_ac = c[3] + c[6] homoplasy_ad = c[3] + c[5] return [non_const + homoplasy_ab, non_const + homoplasy_ac, non_const + homoplasy_ad] def calc_ln_L(self): c = self.get_counts() freqs = [i.calc_pat_probs() for i in self.prob_sources] return [self.calc_ln_L_from_counts(c, f) for f in freqs] def calc_ln_L_from_counts(self, counts, freqs): sum = 0.0 for n, c in enumerate(counts): if c > 0: f = freqs[n] try: char_ln_l = c*math.log(f) except ValueError: return float('-inf') sum += char_ln_l return sum def set_counts(self, pattern_count_data): for n, x in enumerate(pattern_count_data): try: self.data_counts[n].setText(str(int(x))) except: self.data_counts[n].setText(str(x)) if n == 7: break self.repaint() def crop_branch_len(x): if x < 0.0: return 0.0 if x > MAX_BRANCH_LEN: return MAX_BRANCH_LEN return x class TreeWorkspace(TreeWindowBase): """Internally the tree calculations are done as if we have a AB|CD tree. Then patterns and branch lengths are swapped into the correct order. """ def __init__(self, parent=None, topology=TopologyEnum.AB): QtGui.QWidget.__init__(self, parent) self.topology = topology self.lnLPanel = None self.setWindowTitle(TopologyEnum.names[self.topology]) gridLayout = QtGui.QGridLayout() self.labels = [] self.spinboxes = [] self.opt_buttons = [] for i in range(5): lab = QtGui.QLabel("length of '%s'" % BranchEnum.names[i]) sb = QtGui.QDoubleSpinBox() sb.setRange(0.0, MAX_BRANCH_LEN) sb.setDecimals(4) sb.setSingleStep(0.005) sb.setValue(0.05) gridLayout.addWidget(lab, i, 2) gridLayout.addWidget(sb, i, 1) self.labels.append(lab) self.spinboxes.append(sb) self.connect(sb, QtCore.SIGNAL('valueChanged(double)'), self.brlen_changed) opt = QtGui.QPushButton("Optimize") self.opt_buttons.append(opt) gridLayout.addWidget(opt, i, 0) self.connect(opt, QtCore.SIGNAL('clicked()'), getattr(self, "opt_" + BranchEnum.names[i])) opt = QtGui.QPushButton("Optimize all") self.opt_buttons.append(opt) gridLayout.addWidget(opt, 5, 0) self.connect(opt, QtCore.SIGNAL('clicked()'), self.opt_all) pat_pr = self.calc_pat_probs() self.pr_labels = [] self.pr_values = [] for i, bits in enumerate(['000', '001', '010', '011', '100', '101', '110', '111']): lab = QtGui.QLabel("Pr(0%s) =" % bits) vlab = QtGui.QLabel("%0.5f" % pat_pr[i]) self.pr_labels.append(lab) self.pr_values.append(vlab) #gridLayout.addWidget(lab, i, 3) #gridLayout.addWidget(vlab, i, 4) #gridLayout.setRowStretch(i, 1) #gridLayout.setColumnStretch(0, .5) #gridLayout.setColumnStretch(1, .5) self.treeCanvas = QtGui.QFrame() gridLayout.setRowMinimumHeight(6, 250) gridLayout.addWidget(self.treeCanvas, 6, 0, 1, 5) self.treePaintX, self.treePaintY = (50, 300) self.treePaintScaler = 300 self.treePen = QtGui.QPen(TopologyEnum.colors[self.topology], 2, QtCore.Qt.SolidLine) sim = QtGui.QPushButton("Simulate...") self.load_data_button = QtGui.QPushButton("Load Data") gridLayout.addWidget(sim, 9, 0) gridLayout.addWidget(self.load_data_button, 10, 0) self.num_chars_edit = QtGui.QLineEdit() num_char_validator = QtGui.QIntValidator(self) num_char_validator.setBottom(0) self.num_chars_edit.setText("500") self.num_chars_edit.setValidator(num_char_validator) gridLayout.addWidget(self.num_chars_edit, 9, 1) lab = QtGui.QLabel("... characters") gridLayout.addWidget(lab, 9, 2) self.connect(sim, QtCore.SIGNAL('clicked()'), self.simulate) self.connect(self.load_data_button, QtCore.SIGNAL('clicked()'), self.load_data) self.setLayout(gridLayout) self.resize(570, 400) def simulate(self): if not self.lnLPanel: return try: t = self.num_chars_edit.text() n = int(t) except: raise p = self.calc_pat_probs() c = randomly_choose_indices(p, n) print "simulated counts = ", c self.lnLPanel.set_counts(c) def load_data(self): if not self.lnLPanel: return load_data_filedialog = QtGui.QFileDialog(self) #sys.stderr.write("\n".join(dir(load_data_filedialog))) if load_data_filedialog.exec_(): f = load_data_filedialog.selectedFiles() if f: pattern_count_filename= f[0] try: c = parse_counts_from_fn(pattern_count_filename) self.lnLPanel.set_counts(c) except: sys.stderr.write("Error reading data from %s" % pattern_count_filename) def do_opt(self, value_holder, curr_step=0.04): calc = self.lnLPanel if not calc: return data = calc.get_counts() if sum(data) == 0.0: return curr_pat_p = self.calc_pat_probs() curr_v = value_holder.value() curr_lnL = calc.calc_ln_L_from_counts(data, curr_pat_p) best_v, best_lnL = curr_v, curr_lnL lower_lnL = None lower_v = curr_v higher_lnL = None higher_v = curr_v while True: if lower_lnL is not None and lower_lnL - TOL > curr_lnL: curr_v, curr_lnL = lower_v, lower_lnL value_holder.setValue(curr_v) elif higher_lnL is not None and higher_lnL - TOL > curr_lnL: curr_v, curr_lnL = higher_v, higher_lnL else: if (higher_lnL is not None) and (lower_lnL is not None): if (abs(lower_lnL - curr_lnL) < TOL) and (abs(lower_lnL - curr_lnL) < TOL): value_holder.setValue(curr_v) self.repaint() return curr_v, curr_lnL if curr_lnL == float('-inf'): return curr_v, curr_lnL curr_step /= 2 best_v, best_lnL = curr_v, curr_lnL lower_v = crop_branch_len(curr_v - curr_step) value_holder.setValue(lower_v) p = self.calc_pat_probs() lower_lnL = calc.calc_ln_L_from_counts(data, p) self.repaint() higher_v = crop_branch_len(curr_v + curr_step) value_holder.setValue(higher_v) p = self.calc_pat_probs() higher_lnL = calc.calc_ln_L_from_counts(data, p) self.repaint() #print lower_v, "=>", lower_lnL #print curr_v, "=>", curr_lnL #print higher_v, "=>", higher_lnL def opt_all(self): curr_step = 0.04 v, prev_lnl = self.opt_A(curr_step=curr_step) same_score_count = 0 while True: self.opt_Internal(curr_step=curr_step) self.opt_A(curr_step=curr_step) self.opt_B(curr_step=curr_step) self.opt_C(curr_step=curr_step) v, curr_lnl = self.opt_D(curr_step=curr_step) #print "prev_lnl, curr_lnl = ", prev_lnl, curr_lnl if abs(prev_lnl - curr_lnl) < TOL: same_score_count += 1 else: same_score_count = 0 if same_score_count > 1: return prev_lnl = curr_lnl curr_step /= 10 def opt_A(self,curr_step=0.04): return self.do_opt(self.spinboxes[BranchEnum.A], curr_step=curr_step) def opt_B(self,curr_step=0.04): return self.do_opt(self.spinboxes[BranchEnum.B], curr_step=curr_step) def opt_Internal(self,curr_step=0.04): return self.do_opt(self.spinboxes[BranchEnum.INTERNAL], curr_step=curr_step) def opt_C(self,curr_step=0.04): return self.do_opt(self.spinboxes[BranchEnum.C], curr_step=curr_step) def opt_D(self,curr_step=0.04): return self.do_opt(self.spinboxes[BranchEnum.D], curr_step=curr_step) def get_br_lens(self): return [i.value() for i in self.spinboxes] def get_funky_ordered_br_lens(self): if self.topology == TopologyEnum.AB: return self.get_br_lens() a_ch, b_ch, int_ch, c_ch, d_ch = self.get_br_lens() if self.topology == TopologyEnum.AC: b_ch, c_ch = c_ch, b_ch else: b_ch, c_ch, d_ch = d_ch, b_ch, c_ch return a_ch, b_ch, int_ch, c_ch, d_ch def calc_pat_p_scores(self): """Pattern parsimony scores returned in order: A 00000000 B 00001111 C 00110011 D 01010101 """ if self.topology == TopologyEnum.AB: return (0, 1, 1, 1, 1, 2, 2, 1) elif self.topology == TopologyEnum.AC: return (0, 1, 1, 2, 1, 1, 2, 1) return (0, 1, 1, 2, 1, 2, 1, 1) def calc_pat_probs(self): """Pattern likelihoods returned in order (assumes CFN with A=0), A 00000000 B 00001111 C 00110011 D 01010101 """ b = self.get_funky_ordered_br_lens() a_ch, b_ch, int_ch, c_ch, d_ch = b a_no_ch, b_no_ch, int_no_ch, c_no_ch, d_no_ch = [1.0 - i for i in b] # print "b = ", b # ab anc ab0, ab1= a_no_ch, a_ch # ab anc and b b0ab0 = ab0*b_no_ch b1ab0 = ab0*b_ch b0ab1 = ab1*b_ch b1ab1 = ab1*b_no_ch # print b0ab0, b1ab0, b0ab1, b1ab1 # ab anc, b and cd anc b0ab0cd0 = b0ab0*int_no_ch b0ab0cd1 = b0ab0*int_ch b1ab0cd0 = b1ab0*int_no_ch b1ab0cd1 = b1ab0*int_ch b0ab1cd0 = b0ab1*int_ch b0ab1cd1 = b0ab1*int_no_ch b1ab1cd0 = b1ab1*int_ch b1ab1cd1 = b1ab1*int_no_ch # b and cd anc b0cd0 = b0ab0cd0 + b0ab1cd0 b1cd0 = b1ab0cd0 + b1ab1cd0 b0cd1 = b0ab0cd1 + b0ab1cd1 b1cd1 = b1ab0cd1 + b1ab1cd1 # b, c and cd anc b0cd0c0 = b0cd0*c_no_ch b0cd0c1 = b0cd0*c_ch b1cd0c0 = b1cd0*c_no_ch b1cd0c1 = b1cd0*c_ch b0cd1c0 = b0cd1*c_ch b0cd1c1 = b0cd1*c_no_ch b1cd1c0 = b1cd1*c_ch b1cd1c1 = b1cd1*c_no_ch # b, c, d and cd anc b0cd0c0d0 = b0cd0c0*d_no_ch b0cd0c0d1 = b0cd0c0*d_ch b0cd0c1d0 = b0cd0c1*d_no_ch b0cd0c1d1 = b0cd0c1*d_ch b1cd0c0d0 = b1cd0c0*d_no_ch b1cd0c0d1 = b1cd0c0*d_ch b1cd0c1d0 = b1cd0c1*d_no_ch b1cd0c1d1 = b1cd0c1*d_ch b0cd1c0d0 = b0cd1c0*d_ch b0cd1c0d1 = b0cd1c0*d_no_ch b0cd1c1d0 = b0cd1c1*d_ch b0cd1c1d1 = b0cd1c1*d_no_ch b1cd1c0d0 = b1cd1c0*d_ch b1cd1c0d1 = b1cd1c0*d_no_ch b1cd1c1d0 = b1cd1c1*d_ch b1cd1c1d1 = b1cd1c1*d_no_ch # b, c, d b0c0d0 = b0cd0c0d0 + b0cd1c0d0 b0c0d1 = b0cd0c0d1 + b0cd1c0d1 b0c1d0 = b0cd0c1d0 + b0cd1c1d0 b0c1d1 = b0cd0c1d1 + b0cd1c1d1 b1c0d0 = b1cd0c0d0 + b1cd1c0d0 b1c0d1 = b1cd0c0d1 + b1cd1c0d1 b1c1d0 = b1cd0c1d0 + b1cd1c1d0 b1c1d1 = b1cd0c1d1 + b1cd1c1d1 # swap elements if self.topology == TopologyEnum.AC: b0c0d0 , b0c0d1 , b0c1d0 , b0c1d1 , b1c0d0 , b1c0d1 , b1c1d0 , b1c1d1 = (b0c0d0 , b0c0d1 , b1c0d0 , b1c0d1 , b0c1d0 , b0c1d1 , b1c1d0 , b1c1d1) elif self.topology == TopologyEnum.AD: b0c0d0 , b0c0d1 , b0c1d0 , b0c1d1 , b1c0d0 , b1c0d1 , b1c1d0 , b1c1d1 = (b0c0d0 , b1c0d0 , b0c0d1 , b1c0d1 , b0c1d0 , b1c1d0 , b0c1d1 , b1c1d1) # return with d = ones bit, c = 2's bit, d = 4's bit return (b0c0d0 , b0c0d1 , b0c1d0 , b0c1d1 , b1c0d0 , b1c0d1 , b1c1d0 , b1c1d1) def brlen_changed(self): self.repaint() if self.lnLPanel: self.lnLPanel.probs_changed() def refresh_pat_prob_GUI(self): pat_pr = self.calc_pat_probs() for i, item in enumerate(self.pr_values): t = "%0.5f" % pat_pr[i] item.setText(t) # print i, t def paintEvent(self, event): self.refresh_pat_prob_GUI() paint = QtGui.QPainter() paint.begin(self) scaler = self.treePaintScaler abAncX = (self.treePaintX + MAX_BRANCH_LEN*scaler*HORIZ_OVER_HYP) abAncY = (self.treePaintY+ MAX_BRANCH_LEN*scaler*VERT_OVER_HYP) a_len, b_len, int_len, c_len, d_len = self.get_funky_ordered_br_lens() cdAncX = abAncX + scaler*int_len cdAncY = abAncY aX = abAncX - scaler*HORIZ_OVER_HYP*a_len aY = abAncY - scaler*VERT_OVER_HYP*a_len bX = abAncX - scaler*HORIZ_OVER_HYP*b_len bY = abAncY + scaler*VERT_OVER_HYP*b_len cX = cdAncX + scaler*HORIZ_OVER_HYP*c_len cY = cdAncY - scaler*VERT_OVER_HYP*c_len dX = cdAncX + scaler*HORIZ_OVER_HYP*d_len dY = cdAncY + scaler*VERT_OVER_HYP*d_len font_x_offset = 10 font_y_offset = 5 if self.topology == TopologyEnum.AB: b_text, c_text, d_text = "B", "C", "D" elif self.topology == TopologyEnum.AC: b_text, c_text, d_text = "C", "B", "D" else: b_text, c_text, d_text = "D", "B", "C" paint.setPen(self.treePen) paint.drawLine(abAncX, abAncY, cdAncX, cdAncY) paint.drawLine(abAncX, abAncY, aX, aY) paint.drawText(aX - font_x_offset, aY + font_y_offset, "A") paint.drawLine(abAncX, abAncY, bX, bY) paint.drawText(bX - font_x_offset, bY+ font_y_offset, b_text) paint.drawLine(cdAncX, cdAncY, cX, cY) paint.drawText(cX+4, cY + font_y_offset, c_text) paint.drawLine(cdAncX, cdAncY, dX, dY) paint.drawText(dX+4, dY+ font_y_offset, d_text) paint.end() class LikelihoodApp(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) self.setWindowTitle('tree likelihood visualizer') sb = self.statusBar() self.abTree = TreeWorkspace(topology=TopologyEnum.AB) if AS_CENTRAL_WIDGET: self.setCentralWidget = self.abTree self.resize(self.abTree.width(), self.abTree.height() + sb.height()) else: self.acTree = TreeWorkspace(topology=TopologyEnum.AC) self.adTree = TreeWorkspace(topology=TopologyEnum.AD) self.lnL = LnLWorkspace(prob_sources=[self.abTree, self.acTree, self.adTree]) self.abTree.lnLPanel = self.lnL self.acTree.lnLPanel = self.lnL self.adTree.lnLPanel = self.lnL self.lnL.move(0,0) self.abTree.move(500,20) self.acTree.move(520,40) self.adTree.move(530,50) def show(self): if not AS_CENTRAL_WIDGET: self.acTree.show() self.abTree.show() self.adTree.show() self.lnL.show() QtGui.QMainWindow.show(self) app = QtGui.QApplication(sys.argv) qb = LikelihoodApp() qb.show() qb.lnL.set_counts(pattern_count_data) sys.exit(app.exec_())