MinimPy Code
Brought to you by:
msaghaei
--- a +++ b/minimpy.py @@ -0,0 +1,1047 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import pickle +import random +import tempfile +import functions +from model import Model +from minimclass import Minim + +try: + import pygtk + pygtk.require("2.0") + import gobject +except: + print("pygtk Not Availible") + sys.exit(1) +try: + import gtk +except: + print("GTK Not Availible") + sys.exit(1) + +class ModelInteface(object): + def close_window(self, widget, data=None): + if self.trial_file_name: + self.save_trial(self.trial_file_name) + self.window.destroy() + if __name__ == "__main__": + sys.exit(0) + + def delete_event(self, widget, event): + if self.trial_file_name: + self.save_trial(self.trial_file_name) + self.window.destroy() + if __name__ == "__main__": + sys.exit(0) + + def group_cell_edited(self, cell, row, new_text, col): + if col == 0: + # names must not be repeated + for r in range(len(self.group_liststore)): + if r != row and new_text == self.group_liststore[r][0]: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Group name already in use! Choose another name!") + return False + if len(new_text) == 0: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Group name can not be empty!") + return False + if col == 1: + if not new_text.isdigit(): + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Allocation ratio must be an integer!") + return False + new_text = int(new_text) + if not new_text: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Allocation ratio must be zero!") + return False + self.group_liststore[row][col] = new_text + self.update_allocations() + + def group_add_button(self, widget, data=None): + self.group_liststore.append(('Group {0}'.format(len(self.group_liststore)+1), 1)) + self.update_allocations() + + def group_delete_button(self, widget, data=None): + if self.group_treeview.get_cursor()[0]: + row = self.group_treeview.get_cursor()[0][0] + self.group_liststore.remove(self.group_liststore.get_iter(row)) + for idx, record in enumerate(self.allocations): + if record['allocation'] == row: + self.ui_pool.append(self.allocations[idx]['UI']) + self.allocations[idx] = False + elif record['allocation'] > row: + record['allocation'] -= 1 + self.allocations = filter(None, self.allocations) + self.update_allocations() + else: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Please select a group first!") + return False + + def variable_cell_edited(self, cell, row, new_text, col): + if col == 0: + for r in range(len(self.variable_liststore)): + if r != row and new_text == self.variable_liststore[r][0]: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: This variable name already in use! Choose another name!") + return False + if len(new_text) == 0: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Variable names can not be empty!") + return False + if col == 1: + new_text = float(new_text) + if not new_text: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Variable weight can not be zero!") + return False + if col == 2: + # check for deleting a level + old_levels = self.variable_liststore[row][col].split(',') + new_levels = new_text.split(',') + if len(new_levels) < 2: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: At least two variable levels should be entered! Separate levels with commas") + return False + if len(old_levels) > len(new_levels): + # which levels has been deleted? + del_levels = list(set(old_levels) - set(new_levels)) + # convert items to indices + del_levels = [old_levels.index(level) for level in del_levels] + del_levels.sort() + for idx in range(len(self.allocations)): + for i, level in enumerate(self.allocations[idx]['levels']): + if level in del_levels: + self.ui_pool.append(self.allocations[idx]['UI']) + self.allocations[idx] = False + # deleting false items + self.allocations = filter(None, self.allocations) + for idx in range(len(self.allocations)): + for i, level in enumerate(self.allocations[idx]['levels']): + d = 0 + for del_level in del_levels: + # for each del_level less than level the level index number should decrement one unit + if level > del_level: + d += 1 + self.allocations[idx]['levels'][i] -= d + self.variable_liststore[row][col] = new_text + self.update_allocations() + + def variable_add_button(self, widget, data=None): + self.variable_liststore.append(('Variable {0}'.format(len(self.variable_liststore)+1), 1.0, 'Level 0, Level 1')) + self.update_allocations() + + def variable_delete_button(self, widget, data=None): + if self.variable_treeview.get_cursor()[0]: + row = self.variable_treeview.get_cursor()[0][0] + self.variable_liststore.remove(self.variable_liststore.get_iter(row)) + for idx in range(len(self.allocations)): + self.allocations[idx]['levels'].pop(row) + self.update_allocations() + + def update_allocations(self): + n = len(self.allocations_treeview.get_columns()) + while n: + n = self.allocations_treeview.remove_column(self.allocations_treeview.get_column(n-1)) + widgets = self.combo_vbox.get_children() + for widget in widgets: + widget.destroy() + table = gtk.Table(3, len(self.variable_liststore)+1) + min_group_old_index = self.min_group_combo.get_active() + if min_group_old_index < 0: + min_group_old_index = 0 + if (min_group_old_index + 1) > len(self.group_liststore): + min_group_old_index = 0 + self.min_group_combo.get_model().clear() + for group in self.group_liststore: + self.min_group_combo.append_text(group[0]) + if len(self.group_liststore): + self.min_group_combo.set_active(min_group_old_index) + self.variable_combos = [] + n = 0 + for variable in self.variable_liststore: + lbl = gtk.Label(variable[0]) + table.attach(lbl, 0, 1, n, n+1) + cbo = gtk.combo_box_new_text() + for level in map(str.strip, variable[2].split(',')): + cbo.append_text(level) + self.variable_combos.append(cbo) + table.attach(cbo, 1, 2, n, n+1) + n += 1 + self.combo_vbox.pack_start(table, False, False) + types = ((str,) * (len(self.variable_liststore) + 2)) + liststore = gtk.ListStore(*types) + + tvcolumn = gtk.TreeViewColumn('UI') + self.allocations_treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', 0) + tvcolumn.set_resizable(True) + tvcolumn.set_expand(True) + + tvcolumn = gtk.TreeViewColumn('Group') + self.allocations_treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', 1) + tvcolumn.set_resizable(True) + tvcolumn.set_expand(True) + + n = 1 + for variable in self.variable_liststore: + n += 1 + tvcolumn = gtk.TreeViewColumn(variable[0]) + self.allocations_treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', n) + tvcolumn.set_resizable(True) + tvcolumn.set_expand(True) + self.allocations_treeview.set_model(liststore) + variable_models = [cbo.get_model() for cbo in self.variable_combos] + for row, record in enumerate(self.allocations): + liststore.append() + liststore[row][0] = record['UI'] + liststore[row][1] = self.group_liststore[record['allocation']][0] + for col, variable_model in enumerate(variable_models): + liststore[row][col+2] = variable_model[record['levels'][col]][0] + self.window.show_all() + + def get_minimize_case(self, new_case): + model = Model() + model.groups = range(len(self.group_liststore)) + model.variables = [range(len(self.variable_liststore[row][2].split(','))) for row in range(len(self.variable_liststore))] + model.variables_weight = [self.variable_liststore[row][1] for row in range(len(self.variable_liststore))] + model.allocation_ratio = [self.group_liststore[row][1] for row in range(len(self.group_liststore))] + model.allocations = self.allocations + model.prob_method = self.get_prob_method() + model.distance_measure = self.get_distance_measure() + model.high_prob = self.high_prob_spin.get_value() + model.min_group = self.min_group_combo.get_active() + m = Minim(model) + m.build_probs(model.high_prob, model.min_group) + m.build_freq_table() + if self.initial_freq_table: + self.add_to_initial(m.freq_table) + return m.enroll(new_case, m.freq_table) + + def add_to_initial(self, freq_table): + for row, group in enumerate(freq_table): + for v, variable in enumerate(group): + for l, level in enumerate(variable): + freq_table[row][v][l] += self.initial_freq_table[row][v][l] + + def allocations_undo_button(self, widget, data=None): + model = self.allocations_treeview.get_model() + if model == None or len(model) == 0: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: No subject allocated!") + if self.trial_file_name: + self.want_trial_unlock() + return False + msg = "Deallocation may introduce errors in randomization!\nAre you sure you want to undo the last allocation?" + dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format = msg) + dialog.set_title("Undo Allocation!") + if dialog.run() == gtk.RESPONSE_YES: + row = len(model)-1 + self.ui_pool.append(model[row][0]) + path = (row,) + iter = model.get_iter(path) + model.remove(iter) + self.allocations.pop() + self.save_trial(self.trial_file_name) + dialog.destroy() + + def allocations_add_button(self, widget, data=None): + model = self.allocations_treeview.get_model() + if not model: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Groups and prognostic factors must be defined first!") + return False + for cbo in self.variable_combos: + if cbo.get_active() == -1: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, 'Error: Some factor levels have not been selected for this case!') + return False + if not self.ui_pool: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, 'Error: Either this trial is finished or has not been saved!') + return False + model.append() + ui = self.ui_pool.pop() + new_case = {'levels': [], 'allocation': -1, 'UI': ui} + row = len(model)-1 + model[row][0] = ui + for col, cbo in enumerate(self.variable_combos): + model[row][col+2] = cbo.get_active_text() + new_case['levels'].append(cbo.get_active()) + m_group = self.get_minimize_case(new_case) + new_case['allocation'] = m_group + model[row][1] = self.group_liststore[m_group][0] + self.allocations.append(new_case) + self.save_trial(self.trial_file_name) + self.report_allocation(model) + if len(self.ui_pool) == 0: + self.end_trial() + + def report_allocation(self, model): + column = self.allocations_treeview.get_columns() + row = len(model)-1 + ret = [] + for col, column in enumerate(column): + ret.append(column.get_title() + ': ' + model[row][col]) + + msg = "New case allocated\n" + '\n'.join(ret) + dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_OK, message_format = msg) + dialog.set_title("New Allocation!") + dialog.run() + dialog.destroy() + + def end_trial(self): + msg = "{0} cases have been minimized!\nDo you like to extend the sample size".format(len(self.allocations)) + dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format = msg) + dialog.set_title("Trial finished!") + if dialog.run() == gtk.RESPONSE_YES: + extra_sample = "" + while not extra_sample.isdigit(): + extra_sample = functions.simple_dialog("Extra Sample", self.window) + ss = len(self.allocations)+int(extra_sample) + self.build_ui_pool(range(len(self.allocations), ss)) + model = self.allocations_treeview.get_model() + for row in range(len(model)): + model[row][0] = model[row][0].zfill(len(str(ss))) + else: + self.ui_pool = None + dialog.destroy() + + def refresh_freq_table(self): + model = Model() + model.groups = range(len(self.group_liststore)) + model.variables = [range(len(self.variable_liststore[row][2].split(','))) for row in range(len(self.variable_liststore))] + model.variables_weight = [self.variable_liststore[row][1] for row in range(len(self.variable_liststore))] + model.allocation_ratio = [self.group_liststore[row][1] for row in range(len(self.group_liststore))] + model.allocations = self.allocations + m = Minim(model) + m.build_freq_table() + if self.initial_freq_table: + self.add_to_initial(m.freq_table) + if not len(m.freq_table): + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: No allocation!") + return False + n = len(self.freq_table_treeview.get_columns()) + while n: + n = self.freq_table_treeview.remove_column(self.freq_table_treeview.get_column(n-1)) + self.initial_table_vbox.hide() + # following for test only + # m.freq_table = [[[18, 25], [6, 3, 3, 4, 3, 6, 4, 5, 5, 4]], [[39, 45], [12, 8, 7, 7, 7, 9, 6, 12, 11, 5]], [[55, 68], [17, 12, 10, 14, 10, 13, 8, 16, 15, 8]]] + cols = 0 + column_names = [] + for variable in self.variable_liststore: + column_names.extend([level for level in map(str.strip, variable[2].split(','))]) + column_names = ['Group'] + column_names + ['Total'] + for var in m.freq_table[0]: + cols += len(var) + col_types = [str] + [int] * (cols + 1) + liststore = gtk.ListStore(*col_types) + for n in range(len(column_names)): + tvcolumn = gtk.TreeViewColumn(column_names[n]) + self.freq_table_treeview.append_column(tvcolumn) + cell = gtk.CellRendererText() + #cell.set_property('editable', True) + #cell.connect("edited", edit_handler, n) + tvcolumn.pack_start(cell, True) + tvcolumn.add_attribute(cell, 'text', n) + tvcolumn.set_resizable(True) + tvcolumn.set_expand(True) + self.freq_table_treeview.set_model(liststore) + for row, group in enumerate(m.freq_table): + liststore.append() + liststore[row][0] = self.group_liststore[row][0] + col = 1 + for variable in group: + for level in variable: + liststore[row][col] = level + col += 1 + liststore[row][col] = sum(variable) + liststore.append() + liststore[len(m.freq_table)][0] = "Total" + total = 0 + for c in range(1, col): + col_total = sum([liststore[row][c] for row in range(len(m.freq_table))]) + total += col_total + liststore[len(m.freq_table)][c] = col_total + liststore[len(m.freq_table)][c+1] = total / len(self.variable_liststore) + if self.freq_table_enable_edit_button.get_label() == "Save as Initial Table": + self.freq_table_enable_edit() + self.window.show_all() + + def notebook_switch_page(self, notebook, page, page_num, data=None): + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, self.notbook_tabs[page_num]) + if page_num < 4: return + self.refresh_freq_table() + self.balance_table_refresh() + + def license_button_clicked(self, widget, data=None): + self.display_about_dialog() + + def display_about_dialog(self): + dialog = gtk.AboutDialog() + dialog.set_name("MinimPy Program") + dialog.set_version("0.1") + dialog.set_copyright("Copyright (c) 2010-2011 Mahmoud Saghaei") + dialog.set_license("Distributed under the GNU GPL v3.\nFor full terms refer to http://www.gnu.org/copyleft/gpl.html.") + dialog.set_website("http://minimpy.sourceforge.net") + dialog.set_authors(["Mahmoud Saghaei"]) + dialog.set_logo(self.pixbuf) + dialog.run() + dialog.destroy() + return False + + dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = +gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_OK, message_format = "XKnight game\nCopyright (c) 2008-2009 Mahmoud Saghaei\nDistributed under the GNU GPL v3.\nFor full terms refer to http://www.gnu.org/copyleft/gpl.html.") + dialog.set_title("About XKnight Game!") + dialog.run() + dialog.destroy() + + def trial_properties_cell_edited(self, cell, row, new_text, col): + self.trial_properties_liststore[row][col] = new_text + + def trial_properties_add_button(self, widget, data=None): + self.trial_properties_liststore.append(('Property Name', 'Property Value')) + + def trial_properties_delete_button(self, widget, data=None): + if self.trial_properties_treeview.get_cursor()[0]: + row = self.trial_properties_treeview.get_cursor()[0][0] + self.trial_properties_liststore.remove(self.trial_properties_liststore.get_iter(row)) + + def __init__(self): + self.trial_file_name = None + self.notbook_tabs = [] + self.ui_pool = None + self.initial_freq_table = False + self.allocations = [] + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.set_title("Minimization Program") + self.window.connect("delete_event", self.delete_event) + self.window.set_border_width(2) + self.window.set_size_request(750, 400) + self.notebook = gtk.Notebook() + self.notebook.set_tab_pos(gtk.POS_LEFT) + self.notebook.popup_enable() + self.notebook.connect("switch-page", self.notebook_switch_page) + img = gtk.Image() + img.set_from_file('logo.png') + img.show() + self.pixbuf = img.get_pixbuf() + + win_vbox = gtk.VBox(False) + win_vbox.pack_start(self.notebook, True, True) + + self.statusbar = gtk.Statusbar() + win_vbox.pack_start(self.statusbar, False, False, 0) + self.sb_context_id = self.statusbar.get_context_id("Statusbar") + + vbx = gtk.VBox(False) + hbx = gtk.HBox(False) + label = gtk.Label("Trial Title") + hbx.pack_start(label, False, False) + self.trial_title_entry = gtk.Entry() + hbx.pack_start(self.trial_title_entry, True, True) + + label = gtk.Label("Sample Size") + hbx.pack_start(label, False, False) + adj = gtk.Adjustment(value=30, lower=5, upper=1000000, step_incr=1, page_incr=10) + self.trial_sample_size_spin = gtk.SpinButton(adj) + hbx.pack_start(self.trial_sample_size_spin, False, False) + vbx.pack_start(hbx, False, False) + + hbx = gtk.HBox(False) + label = gtk.Label("Description") + hbx.pack_start(label, False, False) + vbx.pack_start(hbx, False, False) + sep = gtk.HSeparator() + vbx.pack_start(sep, False, False) + hbx = gtk.HBox(False) + sep = gtk.VSeparator() + hbx.pack_start(sep, False, False) + self.trial_description_text = gtk.TextView() + hbx.pack_start(self.trial_description_text, True, True) + sep = gtk.VSeparator() + hbx.pack_start(sep, False, False) + vbx.pack_start(hbx, True, True) + sep = gtk.HSeparator() + vbx.pack_start(sep, False, False) + + hbox = gtk.HBox(False) + vbx.pack_start(hbox, True, True) + label = gtk.Label("Probability Method") + vbox = gtk.VBox(False) + vbox.pack_start(label, False, False) + radio = gtk.RadioButton(None, label="Biased Coin Minimization") + vbox.pack_start(radio, False, False) + radio = gtk.RadioButton(radio, label="Naive Minimization") + vbox.pack_start(radio, False, False) + # this method returns the radios in revese order + self.prob_method_radios = radio.get_group() + # so we need to invert the returned list + self.prob_method_radios.reverse() + + hb = gtk.HBox(False) + lbl = gtk.Label("Min Group") + hb.pack_start(lbl, False, False) + self.min_group_combo = gtk.combo_box_new_text() + hb.pack_start(self.min_group_combo, False, False) + vbox.pack_start(hb, False, False) + + hb = gtk.HBox(False) + lbl = gtk.Label("High Probability") + hb.pack_start(lbl, False, False) + adj = gtk.Adjustment(value=0.7, lower=0.1, upper=1.0, step_incr=0.01, page_incr=0.1) + self.high_prob_spin = gtk.SpinButton(adj, digits=2) + hb.pack_start(self.high_prob_spin, False, False) + vbox.pack_start(hb, False, False) + hbox.pack_start(vbox, False, False) + sep = gtk.VSeparator() + hbox.pack_start(sep, False, False) + + label = gtk.Label("Distance Measure") + vbox = gtk.VBox(False) + vbox.pack_start(label, False, False) + radio = gtk.RadioButton(None, label="Marginal Balance") + vbox.pack_start(radio, False, False) + radio = gtk.RadioButton(radio, label="Range") + vbox.pack_start(radio, False, False) + radio = gtk.RadioButton(radio, label="Standard Deviation") + vbox.pack_start(radio, False, False) + radio = gtk.RadioButton(radio, label="Variance") + vbox.pack_start(radio, False, False) + hbox.pack_start(vbox, False, False) + self.distance_measure_radios = radio.get_group() + self.distance_measure_radios.reverse() + sep = gtk.VSeparator() + hbox.pack_start(sep, False, False) + + vbox = gtk.VBox(False) + lbl = gtk.Label("Trial Properties") + vbox.pack_start(lbl, False, False) + hb = gtk.HBox(False) + self.trial_properties_liststore, self.trial_properties_treeview = functions.make_treeview( + (str, str), ['Name', 'Value'], + editable=True, edit_handler=self.trial_properties_cell_edited) + hb.pack_start(self.trial_properties_treeview, True, True) + vbuttonbox = gtk.VButtonBox() + vbuttonbox.set_layout(gtk.BUTTONBOX_START) + button = gtk.Button(None, gtk.STOCK_ADD) + button.connect("clicked", self.trial_properties_add_button) + vbuttonbox.pack_start(button, False, False) + button = gtk.Button(None, gtk.STOCK_DELETE) + button.connect("clicked", self.trial_properties_delete_button) + vbuttonbox.pack_start(button, False, False) + hb.pack_start(vbuttonbox, False, False) + vbox.pack_start(hb, False, False) + + hbox.pack_start(vbox, True, True) + sep = gtk.VSeparator() + hbox.pack_start(sep, False, False) + + sep = gtk.HSeparator() + vbx.pack_start(sep, False, False) + + hbuttonbox = gtk.HButtonBox() + hbuttonbox.set_layout(gtk.BUTTONBOX_START) + + self.trial_load_button = gtk.Button(None, gtk.STOCK_OPEN) + self.trial_load_button.connect("clicked", self.load_trial_clicked) + hbuttonbox.pack_start(self.trial_load_button, False, False) + + self.trial_save_button = gtk.Button(None, gtk.STOCK_SAVE) + self.trial_save_button.connect("clicked", self.save_trial_clicked) + hbuttonbox.pack_start(self.trial_save_button, False, False) + + vbx.pack_start(hbuttonbox, False, False) + + lbl = gtk.Label("Settings") + self.notbook_tabs.append("Settings: Define and save a new trial or load a previous trial") + self.notebook.append_page(vbx, lbl) + + hbox = gtk.HBox(False) + self.group_liststore, self.group_treeview = functions.make_treeview( + (str, int), ['Group Name', 'Allocation Ratio'], + editable=True, edit_handler=self.group_cell_edited) + hbox.pack_start(self.group_treeview, True, True) + vbuttonbox = gtk.VButtonBox() + vbuttonbox.set_layout(gtk.BUTTONBOX_START) + button = gtk.Button(None, gtk.STOCK_ADD) + button.connect("clicked", self.group_add_button) + vbuttonbox.pack_start(button, False, False) + button = gtk.Button(None, gtk.STOCK_DELETE) + button.connect("clicked", self.group_delete_button) + vbuttonbox.pack_start(button, False, False) + hbox.pack_start(vbuttonbox, False, False) + lbl = gtk.Label('Groups') + self.notbook_tabs.append("Groups: Define trial groups and their allocation ratios") + self.group_hbox = hbox + self.notebook.append_page(hbox, lbl) + + hbox = gtk.HBox(False) + self.variable_liststore, self.variable_treeview = functions.make_treeview( + (str, float, str), ['Variable Name', 'Weight', 'Levels'], + editable=True, edit_handler=self.variable_cell_edited) + hbox.pack_start(self.variable_treeview, True, True) + vbuttonbox = gtk.VButtonBox() + vbuttonbox.set_layout(gtk.BUTTONBOX_START) + button = gtk.Button(None, gtk.STOCK_ADD) + button.connect("clicked", self.variable_add_button) + vbuttonbox.pack_start(button, False, False) + button = gtk.Button(None, gtk.STOCK_DELETE) + button.connect("clicked", self.variable_delete_button) + vbuttonbox.pack_start(button, False, False) + hbox.pack_start(vbuttonbox, False, False) + lbl = gtk.Label('Variables') + self.notbook_tabs.append("Variables: Define trial prognostic factors, their weights and levels") + self.variable_hbox = hbox + self.notebook.append_page(hbox, lbl) + + hbox = gtk.HBox(False) + vbox = gtk.VBox(False) + + self.allocations_treeview = gtk.TreeView() + hbox.pack_start(self.allocations_treeview, True, True) + + vbuttonbox = gtk.VButtonBox() + vbuttonbox.set_layout(gtk.BUTTONBOX_START) + button = gtk.Button(None, gtk.STOCK_ADD) + button.connect("clicked", self.allocations_add_button) + vbuttonbox.pack_start(button, False, False) + button = gtk.Button(None, gtk.STOCK_UNDO) + button.connect("clicked", self.allocations_undo_button) + vbuttonbox.pack_start(button, False, False) + vbox.pack_start(vbuttonbox, False, False) + self.combo_vbox = gtk.VBox(False) + vbox.pack_start(self.combo_vbox, False, False) + hbox.pack_start(vbox, False, False) + + lbl = gtk.Label('Allocations') + self.notbook_tabs.append('Allocations: Allocate subjects to treatments. To use minimization select "Minimize" as group name') + self.notebook.append_page(hbox, lbl) + + vbox = gtk.VBox(False) + self.freq_table_treeview = gtk.TreeView() + vbox.pack_start(self.freq_table_treeview, False, False) + self.initial_table_vbox = gtk.VBox(False) + self.freq_table_enable_edit_button = gtk.Button("Start Edit") + self.freq_table_enable_edit_button.connect("clicked", self.freq_table_start_edit_clicked) + self.initial_table_vbox.pack_start(self.freq_table_enable_edit_button, False, False) + button = gtk.Button("Delete Initial Table") + button.connect("clicked", self.freq_table_delete_initial_clicked) + self.initial_table_vbox.pack_start(button, False, False) + vbox.pack_start(self.initial_table_vbox, False, False) + lbl = gtk.Label('Table') + self.notbook_tabs.append("Table: Displays frequency table for different levels of prognostic factors") + self.notebook.append_page(vbox, lbl) + + hbox = gtk.HBox(True) + treestore = gtk.TreeStore(str, float, float, float, float) + self.balance_table_treeview = gtk.TreeView(treestore) + col_names = ['Variable/Level', 'Marginal Balance', 'Range', 'Variance', 'SD'] + for i in range(5): + cell = gtk.CellRendererText() + column = gtk.TreeViewColumn(col_names[i], cell, text=i) + self.balance_table_treeview.append_column(column) + hbox.pack_start(self.balance_table_treeview, True, True) + lbl = gtk.Label('Balance') + self.notbook_tabs.append("Balance: Displays current trial balance as four different balance measures") + self.notebook.append_page(hbox, lbl) + + hbox = gtk.HBox(True) + self.about_text = gtk.TextView() + self.about_text.set_wrap_mode(gtk.WRAP_WORD) + self.about_text.set_editable(False) + self.about_text.set_cursor_visible(False) + self.about_text.set_left_margin(20) + self.about_text.set_right_margin(20) + about_text = """ + + +*************************** MinimPy 0.1 *********************************** +Copyright (c) 2010 Mahmoud Saghaei +http://www.saghaei.com +MinimPy is a desktop minimization program for sequential randomization of subject to treatment groups in a clinical trial. +With this program you can define a new minimization model specifying the probability method used for assigning subject to treatment groups, distance measure which is the imbalance score at a the time of allocation considering the levels of prognostic factor for the a new subject which going to be allocated. Main features of MinimPy include total control over choice of probability method, distance measure, defining an initial minimization preload, adding a new method of probability assignment as biased coin minimization and another as marginal balance which has been shown to be superior to other distance measures. + """ + functions.set_text_buffer(self.about_text, about_text) + hbox.pack_start(self.about_text) + button = gtk.Button(None, gtk.STOCK_INFO) + button.connect("clicked", self.license_button_clicked) + functions.textview_add_widget(self.about_text, 0, button) + self.notbook_tabs.append("ABout: Displays information about MinimPy and the developers") + lbl = gtk.Label('ABout') + self.notebook.append_page(hbox, lbl) + self.notebook.set_show_tabs(True) + + self.window.add(win_vbox) + self.window.show_all() + self.initial_table_vbox.hide() + + def balance_table_refresh(self): + liststore = self.freq_table_treeview.get_model() + if not liststore: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: No allocation!") + return False + table = [] + for row in range(len(liststore)-1): + table.append([]) + for col in range(1, len(liststore[0])-1): + table[row].append(liststore[row][col]) + balance = self.get_trial_balances(table) + treestore = self.balance_table_treeview.get_model() + treestore.clear() + variables = [range(len(self.variable_liststore[row][2].split(','))) for row in range(len(self.variable_liststore))] + row = 0 + for idx, variable in enumerate(variables): + var_total = [0] * 4 + level_rows = [] + for level in variable: + for i in range(4): + var_total[i] += balance[row][i] + level_rows.append(balance[row]) + row += 1 + var_total = [self.variable_liststore[idx][0]] + [var_total[i] / len(variable) for i in range(4)] + variable_node = treestore.append(None, var_total) + level_names = map(str.strip, self.variable_liststore[idx][2].split(',')) + for level, level_row in enumerate(level_rows): + treestore.append(variable_node, [level_names[level]] + level_row) + treestore.append(None, ['Mean'] + balance[row]) + treestore.append(None, ['Max'] + balance[row+1]) + self.window.show_all() + + def get_trial_balances(self, table): + model = Model() + m = Minim(model) + levels = [[] for col in table[0]] + balances = [[] for col in table[0]] + [[], []] + for row in table: + for col, value in enumerate(row): + levels[col].append(value) + for row, level_count in enumerate(levels): + allocation_ratio = [self.group_liststore[r][1] for r in range(len(self.group_liststore))] + adj_count = [(1.0 * level_count[i]) / allocation_ratio[i] for i in range(len(level_count))] + balances[row].append(m.get_marginal_balance(adj_count)) + balances[row].append(max(adj_count) - min(adj_count)) + balances[row].append(m.get_variance(adj_count)) + balances[row].append(m.get_standard_deviation(adj_count)) + for col in range(4): + balances[len(levels)].append(1.0 * sum([balances[row][col] for row in range(len(levels))]) / len(levels)) + balances[len(levels)+1].append(max([balances[row][col] for row in range(len(levels))])) + return balances + + def freq_table_disable_edit(self): + cols = self.freq_table_treeview.get_columns() + for n, col in enumerate(cols): + cells = col.get_cell_renderers() + for cell in cells: + cell.set_property('editable', False) + + def freq_table_enable_edit(self): + cols = self.freq_table_treeview.get_columns() + for n, col in enumerate(cols): + cells = col.get_cell_renderers() + for cell in cells: + cell.set_property('editable', True) + cell.connect("edited", self.freq_table_treeview_edit_handler, n, len(cols)) + + def freq_table_start_edit_clicked(self, button, data=False): + if button.get_label() == "Start Edit": + self.freq_table_enable_edit() + button.set_label("Save as Initial Table") + else: + groups = range(len(self.group_liststore)) + variables = [range(len(self.variable_liststore[row][2].split(','))) for row in range(len(self.variable_liststore))] + initial_freq_model = self.freq_table_treeview.get_model() + table = [[[0 for l in v] for v in variables] for g in groups] + for row, group in enumerate(table): + col = 1 + for v, variable in enumerate(group): + for l, level in enumerate(variable): + table[row][v][l] = initial_freq_model[row][col] + col += 1 + self.initial_freq_table = table + button.set_label("Start Edit") + + def freq_table_treeview_edit_handler(self, cell, row, new_text, col, cols): + model = self.freq_table_treeview.get_model() + if col == 0: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: This cell can not be changed!") + return False + if not new_text.isdigit(): + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Only interger values allowed!") + return False + model[row][col] = int(new_text) + + def freq_table_delete_initial_clicked(self, button, data=None): + self.initial_freq_table = False + self.freq_table_disable_edit() + self.freq_table_enable_edit_button.set_label("Start Edit") + + def load_trial_clicked(self, button, data=None): + file_name = self.select_file("Select file", gtk.FILE_CHOOSER_ACTION_OPEN) + if not file_name: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Trial load canceled!") + return False + self.trial_file_name = self.load_trial(file_name) + + def save_trial_clicked(self, button, data=None): + if not self.trial_is_valid(): + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: This trial is not ready to save! Please check the requirements") + return False + file_name = self.select_file("Enter name of the file", gtk.FILE_CHOOSER_ACTION_SAVE) + if not file_name: + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, "Error: Trial save canceled!") + return False + self.build_ui_pool() + self.trial_file_name = self.save_trial(file_name) + + def load_trial(self, file_name, lock=True): + trial_state = self.trial_sample_size_spin.get_property("sensitive") + temp_file = self.save_trial() + try: + fp = open(file_name, 'rb') + data = pickle.load(fp) + fp.close() + self.ui_pool = data['ui_pool'] + self.trial_title_entry.set_text(data['trial_title']) + self.trial_sample_size_spin.set_value(data['sample_size']) + functions.set_text_buffer(self.trial_description_text, data['trial_description']) + self.set_trial_properties(data['trial_properties']) + self.high_prob_spin.set_value(data['high_prob']) + self.initial_freq_table = data['initial_freq_table'] + self.prob_method_radios[data['prob_method']].set_active(True) + self.distance_measure_radios[data['distance_measure']].set_active(True) + self.allocations = data['allocations'] + self.set_groups_data(data['groups']) + self.set_variables_data(data['variables']) + self.update_allocations() + if len(self.group_liststore): + self.min_group_combo.set_active(data['min_group']) + if lock: + self.lock_trial() + return file_name + except Exception as ex: + self.lock_trial(trial_state) + self.load_trial(file_name=temp_file, lock=False) + functions.error_dialog(self.window, 'An error occured while loading the trial!\nThe selected file may not be valid\n' + repr(ex) + '\nTrial restored to its previous state', 'Trial load failed!') + return False + + def set_groups_data(self, groups): + self.group_liststore.clear() + for group in groups: + self.group_liststore.append((group['name'], group['allocation_ratio'])) + + def set_variables_data(self, variables): + self.variable_liststore.clear() + for variable in variables: + self.variable_liststore.append((variable['name'], variable['weight'], variable['levels'])) + + def build_ui_pool(self, lst=None): + if not lst: + ss = self.trial_sample_size_spin.get_value_as_int() + lst = range(ss) + random.shuffle(lst) + self.ui_pool = map(lambda x: x.zfill(len(str(max(lst)))), map(str, lst)) + + def get_trial_properties(self): + table = [['', ''] for row in range(len(self.trial_properties_liststore))] + for row in range(len(table)): + for col in range(len(table[0])): + table[row][col] = self.trial_properties_liststore[row][col] + return table + + def set_trial_properties(self, table): + self.trial_properties_liststore.clear() + for row in range(len(table)): + self.trial_properties_liststore.append(table[row]) + + def trial_is_valid(self): + if not self.trial_title_entry.get_text(): + functions.error_dialog(self.window, "Please specify the trial title first", "Trial title") + self.notebook.set_current_page(0) + self.window.set_focus(self.trial_title_entry) + return False + if not functions.get_text_buffer(self.trial_description_text): + functions.error_dialog(self.window, "Please enter a description for this trial first", "Trial description") + self.notebook.set_current_page(0) + self.window.set_focus(self.trial_description_text) + return False + if len(self.group_liststore) < 2: + functions.error_dialog(self.window, "At least two groups must be defined for the trial", "Trial groups") + self.notebook.set_current_page(1) + self.window.set_focus(self.group_treeview) + return False + if len(self.variable_liststore) < 1: + functions.error_dialog(self.window, "At least one prognostic factor (variable) must be defined for the trial", "Trial variables") + self.notebook.set_current_page(2) + self.window.set_focus(self.variable_treeview) + return False + msg = "Are you sure you want to save the trial?\nOnce you saved a trial no change in trial settings are allowed!\nTrial details:\n" + self.get_trial_info() + dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format = msg) + dialog.set_title("Trial Save!") + if dialog.run() == gtk.RESPONSE_YES: + dialog.destroy() + return True + dialog.destroy() + return False + + def want_trial_unlock(self): + msg = "Do you want to unlock the trial!\nUnlocking the trial make changes of trial setting possible!\nTrial file will be released.\nYou have to save the trial again!" + dialog = gtk.MessageDialog(self.window, flags = gtk.DIALOG_MODAL, type = gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, message_format = msg) + dialog.set_title("Unlock Trial!") + if dialog.run() == gtk.RESPONSE_YES: + self.lock_trial(True) + self.ui_pool = None + self.trial_file_name = None + dialog.destroy() + + def lock_trial(self, state=False): + self.trial_load_button.set_property("sensitive", state) + self.trial_save_button.set_property("sensitive", state) + self.trial_sample_size_spin.set_property("sensitive", state) + for radio in self.prob_method_radios: + radio.set_property("sensitive", state) + self.min_group_combo.set_property("sensitive", state) + self.high_prob_spin.set_property("sensitive", state) + for radio in self.distance_measure_radios: + radio.set_property("sensitive", state) + for child in self.group_hbox.get_children(): + child.set_property("sensitive", state) + for child in self.variable_hbox.get_children(): + child.set_property("sensitive", state) + for child in self.initial_table_vbox.get_children(): + child.set_property("sensitive", state) + self.statusbar.pop(self.sb_context_id) + self.statusbar.push(self.sb_context_id, ("Trial lock!", "Trial unlocked!")[state]) + + def get_trial_info(self): + ret = [] + ret.append("Title: " + self.trial_title_entry.get_text()) + ret.append("Sample Size: " + str(self.trial_sample_size_spin.get_value_as_int())) + ret.append("Description: " + functions.get_text_buffer(self.trial_description_text)) + ret.append("Probability Method: " + self.get_prob_method_name()) + ret.append("Distance Measure: " + self.get_distance_measure_name()) + ret.append("High Probability: " + str(self.high_prob_spin.get_value())) + ret.append("Groups: " + str(self.get_groups_data())) + ret.append("Variables: " + str(self.get_variables_data())) + return '\n'.join(ret) + + def save_trial(self, file_name=None): + if file_name: + # do not lock if this is a temporary save + self.lock_trial() + else: + file_name = tempfile.mkstemp()[1] + try: + data = {} + data['ui_pool'] = self.ui_pool + data['trial_title'] = self.trial_title_entry.get_text() + data['sample_size'] = self.trial_sample_size_spin.get_value_as_int() + data['trial_description'] = functions.get_text_buffer(self.trial_description_text) + data['trial_properties'] = self.get_trial_properties() + data['high_prob'] = self.high_prob_spin.get_value() + data['min_group'] = self.min_group_combo.get_active() + data['initial_freq_table'] = self.initial_freq_table + data['prob_method'] = self.get_prob_method() + data['distance_measure'] = self.get_distance_measure() + data['allocations'] = self.allocations + data['groups'] = self.get_groups_data() + data['variables'] = self.get_variables_data() + fp = open(file_name, 'wb') + pickle.dump(data, fp) + fp.close() + # useful if temp save + return file_name + except Exception as ex: + functions.error_dialog(self.window, "An unknown error occured during trial save!\n" + repr(ex), "Trial save error!") + return False + + def get_distance_measure_name(self): + for distance_measure_radio in self.distance_measure_radios: + if distance_measure_radio.get_active(): + return distance_measure_radio.get_label() + return "Unknown" + + def get_distance_measure(self): + for idx, distance_measure_radio in enumerate(self.distance_measure_radios): + if distance_measure_radio.get_active(): + return idx + return 0 + + def get_prob_method_name(self): + for prob_method_radio in self.prob_method_radios: + if prob_method_radio.get_active(): + return prob_method_radio.get_label() + return "Unknown" + + def get_prob_method(self): + for idx, prob_method_radio in enumerate(self.prob_method_radios): + if prob_method_radio.get_active(): + return idx + return 0 + + def get_variables_data(self): + variables = [] + for row in self.variable_liststore: + variable = {} + variable['name'] = row[0] + variable['weight'] = row[1] + variable['levels'] = row[2] + variables.append(variable) + return variables + + def get_groups_data(self): + groups = [] + for row in self.group_liststore: + group = {} + group['name'] = row[0] + group['allocation_ratio'] = row[1] + groups.append(group) + return groups + + def select_file(self, title, action): + """ + A generic method returning file name + """ + stock = {gtk.FILE_CHOOSER_ACTION_OPEN: gtk.STOCK_OPEN, gtk.FILE_CHOOSER_ACTION_SAVE: gtk.STOCK_SAVE} + buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, stock[action], gtk.RESPONSE_OK) + fdialog = gtk.FileChooserDialog(title, self.window, action, buttons) + if action == gtk.FILE_CHOOSER_ACTION_SAVE: + fdialog.set_do_overwrite_confirmation(True) + fdialog.set_default_response(gtk.RESPONSE_OK) + response = fdialog.run() + if response == gtk.RESPONSE_OK: + file_name = fdialog.get_filename() + else: + file_name = None + fdialog.destroy() + return file_name + +def main(): + gtk.main() + return 0 + +if __name__ == "__main__": + ModelInteface() + main() +