#! /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()