From 830d9f96a436b15e7ea52e398185f050a2ebbe51 Mon Sep 17 00:00:00 2001 From: Mahmoud Saghaei Date: Mon, 9 Nov 2020 12:51:34 +0330 Subject: [PATCH] First commit --- .gitignore | 3 + .idea/.gitignore | 3 + .idea/inspectionProfiles/Project_Default.xml | 12 + .idea/inspectionProfiles/profiles_settings.xml | 6 + .idea/minimpy2.iml | 10 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + about_minimisation.py | 22 + config.py | 45 + config_dialog.py | 63 ++ config_dialog.ui | 48 + db.py | 796 +++++++++++++++ db/database.sql | 77 ++ db/db.sql | 89 ++ db/minimizer.db | Bin 0 -> 81920 bytes enrol_form.py | 138 +++ fa_IR.ts | 1072 ++++++++++++++++++++ factor_levels.py | 98 ++ factor_levels_dialog.py | 63 ++ factor_levels_dialog.ui | 49 + freq_table.py | 227 +++++ images/about.png | Bin 0 -> 1598 bytes images/add.png | Bin 0 -> 1963 bytes images/config.png | Bin 0 -> 2122 bytes images/delete.png | Bin 0 -> 7891 bytes images/error.png | Bin 0 -> 1405 bytes images/exit.png | Bin 0 -> 6433 bytes images/help.png | Bin 0 -> 13025 bytes images/levels.png | Bin 0 -> 1677 bytes images/logo.png | Bin 0 -> 22636 bytes images/save.png | Bin 0 -> 12191 bytes images/tick_mark.png | Bin 0 -> 2160 bytes locale_sources.ts | 998 +++++++++++++++++++ locales/READ.ME | 11 + locales/fa_IR.qm | Bin 0 -> 41059 bytes locales/languages.lst | 2 + main_window.py | 1254 ++++++++++++++++++++++++ minim.py | 181 ++++ minimisation_en_US.txt | 4 + minimisation_fa_IR.txt | 4 + minimpy2.pro | 3 + mnd.py | 162 +++ model.py | 24 + my_random.py | 46 + run_test.py | 41 + trial.py | 76 ++ ui_about_minimisation.py | 48 + ui_about_minimisation.ui | 35 + ui_main_window.py | 579 +++++++++++ ui_main_window.ui | 705 +++++++++++++ 51 files changed, 7012 insertions(+) create mode 100644 .gitignore create mode 100755 .idea/.gitignore create mode 100755 .idea/inspectionProfiles/Project_Default.xml create mode 100755 .idea/inspectionProfiles/profiles_settings.xml create mode 100755 .idea/minimpy2.iml create mode 100755 .idea/misc.xml create mode 100755 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100755 about_minimisation.py create mode 100755 config.py create mode 100755 config_dialog.py create mode 100755 config_dialog.ui create mode 100755 db.py create mode 100755 db/database.sql create mode 100755 db/db.sql create mode 100755 db/minimizer.db create mode 100755 enrol_form.py create mode 100755 fa_IR.ts create mode 100755 factor_levels.py create mode 100755 factor_levels_dialog.py create mode 100755 factor_levels_dialog.ui create mode 100755 freq_table.py create mode 100755 images/about.png create mode 100755 images/add.png create mode 100755 images/config.png create mode 100755 images/delete.png create mode 100755 images/error.png create mode 100755 images/exit.png create mode 100755 images/help.png create mode 100755 images/levels.png create mode 100644 images/logo.png create mode 100755 images/save.png create mode 100755 images/tick_mark.png create mode 100755 locale_sources.ts create mode 100755 locales/READ.ME create mode 100755 locales/fa_IR.qm create mode 100755 locales/languages.lst create mode 100755 main_window.py create mode 100755 minim.py create mode 100755 minimisation_en_US.txt create mode 100755 minimisation_fa_IR.txt create mode 100755 minimpy2.pro create mode 100755 mnd.py create mode 100755 model.py create mode 100755 my_random.py create mode 100755 run_test.py create mode 100755 trial.py create mode 100755 ui_about_minimisation.py create mode 100755 ui_about_minimisation.ui create mode 100755 ui_main_window.py create mode 100755 ui_main_window.ui diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48d8fdb --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +venv/ +*.pyc diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100755 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100755 index 0000000..fb16d7a --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100755 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/minimpy2.iml b/.idea/minimpy2.iml new file mode 100755 index 0000000..8e81531 --- /dev/null +++ b/.idea/minimpy2.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100755 index 0000000..be317ac --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100755 index 0000000..13d3595 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/about_minimisation.py b/about_minimisation.py new file mode 100755 index 0000000..b33774a --- /dev/null +++ b/about_minimisation.py @@ -0,0 +1,22 @@ +from PySide2 import QtWidgets as qtw +from os import path + +from ui_about_minimisation import Ui_AboutMinimisationDialog + + +class AboutMinimisationDialog(qtw.QDialog): + def __init__(self, parent): + super().__init__(parent) + self.ui = Ui_AboutMinimisationDialog() + self.ui.setupUi(self) + lang = parent.settings.value('language', 'en_US', type=str) + filename = 'minimisation_{}.txt'.format(lang) + if not path.exists(filename): + filename = 'minimisation_en_US.txt' + if not path.exists(filename): + return + text = open(filename).read() + self.ui.minimTextEdit.setPlainText(text) + self.setWindowTitle(parent.tr('What is Minimisation')) + self.ui.closePushButton.setText(parent.tr('Close')) + self.ui.closePushButton.clicked.connect(self.close) diff --git a/config.py b/config.py new file mode 100755 index 0000000..c10ba61 --- /dev/null +++ b/config.py @@ -0,0 +1,45 @@ +import sys + +from PySide2 import QtWidgets as qtw + +from config_dialog import Ui_ConfigDialog + + +class Config(qtw.QDialog): + def __init__(self, parent): + super(Config, self).__init__(parent) + self.parent = parent + self.ui = Ui_ConfigDialog() + self.ui.setupUi(self) + lines = open('locales/languages.lst').readlines() + self.lang_codes = [] + cur_lang = self.parent.settings.value('language', 'en_US', type=str) + cur_index = 0 + for index, line in enumerate(lines): + if line.count(':') != 1: + qtw.QMessageBox.critical(self, parent.tr('Error'), parent.tr('Error in languages.lst file format\nPlease see the READ.ME file in locales folder')) + sys.exit(1) + parts = line.strip().split(":") + self.lang_codes.append(parts[0]) + self.ui.languageComboBox.addItem(parts[1]) + if cur_lang == parts[0]: + cur_index = index + self.ui.languageComboBox.setCurrentIndex(cur_index) + if len(lines) < 2: + self.ui.languageComboBox.setVisible(False) + self.ui.langLabel.setVisible(False) + self.ui.randomEnginComboBox.addItem('Random') + self.ui.randomEnginComboBox.addItem('Secret') + cur_engine = self.parent.settings.value('random_engine', 'random', type=str) + if cur_engine == 'random': + self.ui.randomEnginComboBox.setCurrentIndex(0) + else: + self.ui.randomEnginComboBox.setCurrentIndex(1) + self.ui.savePushButton.clicked.connect(self.on_save_config) + + def on_save_config(self): + lang_index = self.ui.languageComboBox.currentIndex() + self.parent.settings.setValue('language', self.lang_codes[lang_index]) + random_index = self.ui.randomEnginComboBox.currentIndex() + self.parent.settings.setValue('random_engine', ('random', 'secret')[random_index]) + self.close() \ No newline at end of file diff --git a/config_dialog.py b/config_dialog.py new file mode 100755 index 0000000..90d17d6 --- /dev/null +++ b/config_dialog.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'config_dialog.ui' +## +## Created by: Qt User Interface Compiler version 5.14.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject, + QObject, QPoint, QRect, QSize, QTime, QUrl, Qt) +from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, + QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter, + QPixmap, QRadialGradient) +from PySide2.QtWidgets import * + + +class Ui_ConfigDialog(object): + def setupUi(self, ConfigDialog): + if not ConfigDialog.objectName(): + ConfigDialog.setObjectName(u"ConfigDialog") + ConfigDialog.resize(400, 300) + self.formLayout_2 = QFormLayout(ConfigDialog) + self.formLayout_2.setObjectName(u"formLayout_2") + self.langLabel = QLabel(ConfigDialog) + self.langLabel.setObjectName(u"langLabel") + + self.formLayout_2.setWidget(0, QFormLayout.LabelRole, self.langLabel) + + self.languageComboBox = QComboBox(ConfigDialog) + self.languageComboBox.setObjectName(u"languageComboBox") + + self.formLayout_2.setWidget(0, QFormLayout.FieldRole, self.languageComboBox) + + self.randomEnginLabel = QLabel(ConfigDialog) + self.randomEnginLabel.setObjectName(u"randomEnginLabel") + + self.formLayout_2.setWidget(1, QFormLayout.LabelRole, self.randomEnginLabel) + + self.randomEnginComboBox = QComboBox(ConfigDialog) + self.randomEnginComboBox.setObjectName(u"randomEnginComboBox") + + self.formLayout_2.setWidget(1, QFormLayout.FieldRole, self.randomEnginComboBox) + + self.savePushButton = QPushButton(ConfigDialog) + self.savePushButton.setObjectName(u"savePushButton") + + self.formLayout_2.setWidget(2, QFormLayout.FieldRole, self.savePushButton) + + + self.retranslateUi(ConfigDialog) + + QMetaObject.connectSlotsByName(ConfigDialog) + # setupUi + + def retranslateUi(self, ConfigDialog): + ConfigDialog.setWindowTitle(QCoreApplication.translate("ConfigDialog", u"MinimPy2 Config", None)) + self.langLabel.setText(QCoreApplication.translate("ConfigDialog", u"Language", None)) + self.randomEnginLabel.setText(QCoreApplication.translate("ConfigDialog", u"Random engin", None)) + self.savePushButton.setText(QCoreApplication.translate("ConfigDialog", u"Save", None)) + # retranslateUi + diff --git a/config_dialog.ui b/config_dialog.ui new file mode 100755 index 0000000..e227ac0 --- /dev/null +++ b/config_dialog.ui @@ -0,0 +1,48 @@ + + + ConfigDialog + + + + 0 + 0 + 400 + 300 + + + + MinimPy2 Config + + + + + + Language + + + + + + + + + + Random engin + + + + + + + + + + Save + + + + + + + + diff --git a/db.py b/db.py new file mode 100755 index 0000000..5d8f86b --- /dev/null +++ b/db.py @@ -0,0 +1,796 @@ +from PySide2 import QtSql as qts +from PySide2 import QtWidgets as qtw +from PySide2 import QtCore as qtc + +import sys +import os + + +# noinspection PyTypeChecker +class Database(qtc.QObject): + __instance = None + + @staticmethod + def get_instance(mainWindow): + if Database.__instance is None: + Database(mainWindow) + return Database.__instance + + def get_db(self): + return self.db + + def create_db(self): + if not self.db.open(): + self.show_error( + self.tr('Error'), + 'open db: ' + self.db.lastError().text() + ) + sys.exit(1) + all_sqls = open('db/database.sql').read().split(';') + for sql in all_sqls: + query = qts.QSqlQuery(self.db) + query.prepare(sql) + if not query.exec_(): + self.show_error( + self.tr('Error'), + 'creating db: ' + self.db.lastError().text() + ) + sys.exit(1) + + def __init__(self, mainWindow): + super().__init__() + if Database.__instance is not None: + raise Exception("This class is a singleton!") + self.mainWindow = mainWindow + self.db = qts.QSqlDatabase.addDatabase('QSQLITE') + self.db.setDatabaseName('db/minimizer.db') + if not os.path.exists('db/minimizer.db'): + self.create_db() + else: + if not self.db.open(): + self.show_error( + self.tr('DB Connection Error'), + self.tr('Could not open database file {}').format(self.db.lastError().text()) + ) + sys.exit(1) + required_tables = {'trials', 'discarded_identifiers', 'treatments', 'factors', 'levels', 'subjects', + 'subject_levels', 'preloads'} + missing_tables = required_tables - set(self.db.tables()) + if missing_tables: + self.show_error( + self.tr('DB Integrity Error'), + self.tr('Missing tables, please repair DB {}').format(missing_tables) + ) + sys.exit(1) + if not self.checkForeignKeys(): + sys.exit(1) + Database.__instance = self + + def checkForeignKeys(self): + query = qts.QSqlQuery(self.db) + query.prepare('PRAGMA foreign_keys;') + if not query.exec_(): + self.show_query_error(query) + res = query.next() + if not res: + self.show_query_error(query) + if query.value(0): + return True + query.prepare('PRAGMA foreign_keys = ON;') + if not query.exec_(): + self.show_query_error(query) + query.prepare('PRAGMA foreign_keys;') + query.exec_() + if query.next(): + return query.value(0) > 0 + else: + return False + + def get_treatment_title(self, treatment_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT title FROM treatments WHERE id = :treatment_id') + query.bindValue(':treatment_id', treatment_id) + if not query.exec_(): + self.show_query_error(query) + query.next() + return query.value('title') + + def update_subject_treatment_id(self, subject_id, treatment_id): + query = qts.QSqlQuery(self.db) + query.prepare('''UPDATE subjects set treatment_id = :treatment_id, + modified = CURRENT_TIMESTAMP WHERE id = :subject_id''') + query.bindValue(':treatment_id', treatment_id) + query.bindValue(':subject_id', subject_id) + if not query.exec_(): + self.show_query_error(query) + + def get_subject_treatment_id(self, subject_id): + query = qts.QSqlQuery(self.db) + query.prepare("SELECT treatment_id FROM subjects WHERE id = :subject_id") + query.bindValue(':subject_id', subject_id) + if not query.exec_(): + self.show_query_error(query) + if not query.next(): + return None + return query.value('treatment_id') + + def get_subject(self, subject_id): + query = qts.QSqlQuery(self.db) + query.prepare("SELECT * FROM subjects WHERE id = :subject_id") + query.bindValue(':subject_id', subject_id) + if not query.exec_(): + self.show_query_error(query) + if not query.next(): + return None + return query + + def get_subjects(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('''SELECT id, treatment_id, identifier_value, enrolled, + modified FROM subjects WHERE trial_id = :trial_id''') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + subjects = [] + while query.next(): + subject = [ + query.value('id'), + query.value('treatment_id'), + query.value('identifier_value'), + query.value('enrolled'), + query.value('modified') + ] + subjects.append(subject) + return subjects + + def get_subject_level_titles(self, subject_levels): + query = qts.QSqlQuery(self.db) + level_title = [] + for factor_id, level_id in subject_levels: + query.prepare('SELECT title FROM levels WHERE id = :level_id') + query.bindValue(':level_id', level_id) + if not query.exec_(): + self.show_query_error(query) + if not query.next(): + return False + level_title.append(query.value('title')) + return level_title + + def get_subject_levels(self, subject_id): + query = qts.QSqlQuery(self.db) + query.prepare("SELECT factor_id, level_id FROM subject_levels WHERE subject_id = :subject_id ORDER BY id") + query.bindValue(':subject_id', subject_id) + if not query.exec_(): + self.show_query_error(query) + subject_levels = [] + while query.next(): + subject_level = [query.value('factor_id'), query.value('level_id')] + subject_levels.append(subject_level) + return subject_levels + + def get_freq(self, trial_id): + subjects = self.get_subjects(trial_id) + freq = self.get_empty_freq(trial_id) + for subject in subjects: + subject_levels = self.get_subject_levels(subject[0]) + for subject_level in subject_levels: + key = (subject[1], subject_level[0], subject_level[1]) + freq[key] += 1 + return freq + + def get_empty_freq(self, trial_id): + freq = {} + treatments = self.read_treatments(trial_id) + factors = self.read_factors(trial_id) + if not treatments or not factors: + return + for treatment in treatments: + for factor in factors: + levels = self.factor_levels(factor[0]) + for level in levels: + key = (treatment[0], factor[0], level[0]) + if key in freq: + continue + freq[key] = 0 + return freq + + def get_preload(self, trial_id): + query = qts.QSqlQuery(self.db) + preload = self.get_empty_freq(trial_id) + query.prepare("SELECT treatment_id, factor_id, level_id, count FROM preloads WHERE trial_id = :trial_id") + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + rows = [] + while query.next(): + row = [query.value('treatment_id'), query.value('factor_id'), query.value('level_id'), query.value('count')] + rows.append(row) + for row in rows: + key = (row[0], row[1], row[2]) + preload[key] = row[3] + return preload + + def get_preload_with_freq(self, trial_id): + freq = self.get_freq(trial_id) + preload = self.get_preload(trial_id) + if freq is None or preload is None: + return None + preload_with_freq = {} + for key in freq: + preload_with_freq[key] = freq[key] + preload[key] + return preload_with_freq + + def insert_level(self, trial_id, factor_id, title): + query = qts.QSqlQuery(self.db) + query.prepare('INSERT INTO levels (trial_id, factor_id, title) VALUES (:trial_id, :factor_id, :title)') + query.bindValue(':trial_id', trial_id) + query.bindValue(':factor_id', factor_id) + query.bindValue(':title', title) + if not query.exec_(): + self.show_query_error(query) + return query.lastInsertId() + + def insert_factor(self, trial_id, title): + query = qts.QSqlQuery(self.db) + query.prepare('INSERT INTO factors (trial_id, title) VALUES (:trial_id, :title)') + query.bindValue(':title', title) + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + return query.lastInsertId() + + def insert_treatment(self, trial_id, title): + query = qts.QSqlQuery(self.db) + query.prepare('INSERT INTO treatments (trial_id, title) VALUES (:trial_id, :title)') + query.bindValue(':title', title) + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + return query.lastInsertId() + + def insert_trial(self, title): + query = qts.QSqlQuery(self.db) + query.prepare('INSERT INTO trials (title) VALUES (:title)') + query.bindValue(':title', title) + if not query.exec_(): + self.show_query_error(query) + return query.lastInsertId() + + def delete_level(self, level_id): + query = qts.QSqlQuery(self.db) + query.prepare('DELETE FROM levels WHERE id = :id') + query.bindValue(':id', level_id) + if not query.exec_(): + self.show_query_error(query) + + def delete_treatment(self, treatment_id): + query = qts.QSqlQuery(self.db) + query.prepare('DELETE FROM treatments WHERE id = :id') + query.bindValue(':id', treatment_id) + if not query.exec_(): + self.show_query_error(query) + + def delete_factor(self, factor_id): + query = qts.QSqlQuery(self.db) + query.prepare('DELETE FROM factors WHERE id = :id') + query.bindValue(':id', factor_id) + if not query.exec_(): + self.show_query_error(query) + + def delete_trial(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('DELETE FROM trials WHERE id = :id') + query.bindValue(':id', trial_id) + if not query.exec_(): + self.show_query_error(query) + + def get_title(self, row_id, table): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT title FROM {} WHERE id = :row_id'.format(table)) + query.bindValue(':row_id', row_id) + if not query.exec_(): + self.show_query_error(query) + query.next() + return query.value('title') + + def update_level(self, level_id, title): + query = qts.QSqlQuery(self.db) + query.prepare('UPDATE levels set title = :title WHERE id = :id') + query.bindValue(':title', title) + query.bindValue(':id', level_id) + if not query.exec_(): + if self.show_query_error(query): + return self.get_title(level_id, 'levels') + + def update_factor(self, factor_id, column, value): + query = qts.QSqlQuery(self.db) + query.prepare('UPDATE factors set {} = :value WHERE id = :id'.format(column)) + query.bindValue(':value', value) + query.bindValue(':id', factor_id) + if not query.exec_(): + if self.show_query_error(query): + return self.get_title(factor_id, 'factors') + + def update_treatment(self, treatment_id, column, value): + query = qts.QSqlQuery(self.db) + query.prepare('UPDATE treatments set {} = :value WHERE id = :id'.format(column)) + query.bindValue(':value', value) + query.bindValue(':id', treatment_id) + if not query.exec_(): + if self.show_query_error(query): + return self.get_title(treatment_id, 'treatments') + + def update_trial(self, trial_id, column, value): + query = qts.QSqlQuery(self.db) + query.prepare('UPDATE trials SET {} = :value, modified = CURRENT_TIMESTAMP WHERE id = :id'.format(column)) + query.bindValue(':value', value) + query.bindValue(':id', trial_id) + if not query.exec_(): + if self.show_query_error(query): + return self.get_title(trial_id, 'trials') + + def load_levels(self, factor_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT * FROM levels WHERE factor_id = :factor_id ORDER BY id') + query.bindValue(':factor_id', factor_id) + if not query.exec_(): + self.show_query_error(query) + return query + + def load_treatments(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT * FROM treatments WHERE trial_id = :trial_id ORDER BY id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + return query + + def load_factors(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT * FROM factors WHERE trial_id = :trial_id ORDER BY id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + return query + + def get_levels(self, factor_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT title FROM levels WHERE factor_id = :factor_id ORDER BY id') + query.bindValue(':factor_id', factor_id) + if not query.exec_(): + qtw.QMessageBox.critical( + self.parent(), + self.tr('DB read Error'), + query.lastError().text() + ) + return + levels = [] + while query.next(): + levels.append(query.value('title')) + return ', '.join(levels) + + def get_used_identifiers(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT identifier_value FROM subjects WHERE trial_id = :trial_id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + ret = [] + while query.next(): + ret.append(query.value('identifier_value')) + return ret + + def get_discarded_identifiers(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT identifier FROM discarded_identifiers WHERE trial_id = :trial_id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + ret = [] + while query.next(): + ret.append(query.value('identifier')) + return ret + + def get_count_discarded_identifiers(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT COUNT(*) FROM discarded_identifiers WHERE trial_id = :trial_id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + query.next() + return query.value(0) + + def get_num_treatments(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT COUNT(*) FROM treatments WHERE trial_id = :trial_id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + query.next() + return query.value(0) + + def get_factor_level_indices(self, factor_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT COUNT(*) FROM levels WHERE factor_id = :factor_id') + query.bindValue(':factor_id', factor_id) + if not query.exec_(): + self.show_query_error(query) + query.next() + return list(range(query.value(0))) + + def get_factor_weights(self, trial_id): + factors = self.read_factors(trial_id) + if not factors: + return + return [factor[2] for factor in factors] + + def get_factors_level_indices(self, trial_id): + factors = self.read_factors(trial_id) + if not factors: + return + fli = [] + for factor in factors: + fli.append(self.get_factor_level_indices(factor[0])) + return fli + + def get_allocation_ratios(self, trial_id): + treatments = self.read_treatments(trial_id) + if not treatments: + return False + return [treatment[2] for treatment in treatments] + + def get_treatments_id_dict(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT id from treatments WHERE trial_id = :trial_id ORDER BY id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + rows = [] + while query.next(): + rows.append(query.value('id')) + return {row: i for i, row in enumerate(rows)} + + def get_allocations(self, trial_id): + treatment_dict = self.get_treatments_id_dict(trial_id) + allocations = [] + subjects = self.get_subjects(trial_id) + for subject in subjects: + allocation = {'allocation': treatment_dict[subject[1]], 'levels': [], 'UI': subject[2]} + levels = self.get_subject_levels(subject[0]) + for level in levels: + level_dict = self.get_factor_level_dict(level[0]) + allocation['levels'].append(level_dict[level[1]]) + allocations.append(allocation) + return allocations + + def get_min_allocation_ratio_group_index(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT id FROM treatments WHERE trial_id = :trial_id AND ratio = (SELECT min(ratio) FROM treatments);') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + query.next() + result = query.value('id') + treatment_dict = self.get_treatments_id_dict(trial_id) + return treatment_dict[result] + + def get_factor_level_dict(self, factor_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT id FROM levels WHERE factor_id = :factor_id ORDER BY id') + query.bindValue(':factor_id', factor_id) + if not query.exec_(): + self.show_query_error(query) + rows = [] + while query.next(): + rows.append(query.value('id')) + return {row: i for i, row in enumerate(rows)} + + def update_trial_setting(self, trial_id, ui): + title = ui.trialTitleLineEdit.text() + if len(title.strip()) == 0: + return + code = ui.trialCodeLineEdit.text() + prob_method = ui.trialProbMethodComboBox.currentIndex() + base_prob = ui.trialBaseProbabilitySlider.value() / 100.0 + dist_method = ui.trialDistanceMethodComboBox.currentIndex() + identifier_type = ui.trialIdentifierTypeComboBox.currentIndex() + identifier_order = ui.trialIentifierOrderComboBox.currentIndex() + identifier_length = ui.trialIdentifierLengthSpinBox.value() + recycle_ids = 1 if ui.trialRecycleIdsCheckBox.isChecked() else 0 + new_subject_random = 1 if ui.trialNewSubjectRandomCheckBox.isChecked() else 0 + arms_weight = ui.trialArmsWeightDoubleSlider.value() + query = qts.QSqlQuery(self.db) + query.prepare('UPDATE trials set ' + 'title = :title, ' + 'code = :code, ' + 'modified = CURRENT_TIMESTAMP, ' + 'prob_method = :prob_method, ' + 'base_prob = :base_prob, ' + 'dist_method = :dist_method, ' + 'identifier_type = :identifier_type, ' + 'identifier_order = :identifier_order, ' + 'identifier_length = :identifier_length, ' + 'recycle_ids = :recycle_ids, ' + 'new_subject_random = :new_subject_random, ' + 'arms_weight = :arms_weight ' + 'WHERE id = :id' + ) + query.bindValue(':title', title) + query.bindValue(':code', code) + query.bindValue(':prob_method', prob_method) + query.bindValue(':base_prob', base_prob) + query.bindValue(':dist_method', dist_method) + query.bindValue(':identifier_type', identifier_type) + query.bindValue(':identifier_order', identifier_order) + query.bindValue(':identifier_length', identifier_length) + query.bindValue(':recycle_ids', recycle_ids) + query.bindValue(':new_subject_random', new_subject_random) + query.bindValue(':arms_weight', arms_weight) + query.bindValue(':id', trial_id) + if not query.exec_(): + self.show_query_error(query) + + def get_trial_setting(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT * FROM trials WHERE id = :id') + query.bindValue(':id', trial_id) + if not query.exec_(): + self.show_query_error(query) + if not query.next(): + return False + return query + + def clear_trial_setting(self, ui): + ui.trialCreatedValueLabel.setText('') + ui.trialModifiedValueLabel.setText('') + ui.trialTitleLineEdit.setText('') + ui.trialCodeLineEdit.setText('') + ui.trialProbMethodComboBox.setCurrentIndex(0) + ui.trialBaseProbabilitySlider.setValue(0.7 * 100) + ui.trialDistanceMethodComboBox.setCurrentIndex(0) + ui.trialIdentifierTypeComboBox.setCurrentIndex(0) + ui.trialIentifierOrderComboBox.setCurrentIndex(0) + ui.trialIdentifierLengthSpinBox.setValue(3) + ui.trialRecycleIdsCheckBox.setChecked(False) + ui.trialNewSubjectRandomCheckBox.setChecked(False) + ui.trialArmsWeightDoubleSlider.setValue(1.0) + + def map_trial_setting(self, query, ui): + ui.trialCreatedValueLabel.setText(query.value('created')) + ui.trialModifiedValueLabel.setText(query.value('modified')) + ui.trialTitleLineEdit.setText(query.value('title')) + ui.trialCodeLineEdit.setText(query.value('code')) + ui.trialProbMethodComboBox.setCurrentIndex(query.value('prob_method')) + ui.trialBaseProbabilitySlider.setValue(query.value('base_prob') * 100) + ui.trialDistanceMethodComboBox.setCurrentIndex(query.value('dist_method')) + ui.trialIdentifierTypeComboBox.setCurrentIndex(query.value('identifier_type')) + ui.trialIentifierOrderComboBox.setCurrentIndex(query.value('identifier_order')) + ui.trialIdentifierLengthSpinBox.setValue(query.value('identifier_length')) + ui.trialRecycleIdsCheckBox.setChecked(query.value('recycle_ids') != 0) + ui.trialNewSubjectRandomCheckBox.setChecked(query.value('new_subject_random') != 0) + ui.trialArmsWeightDoubleSlider.setValue(query.value('arms_weight')) + + def load_trial_setting(self, trial_id, ui): + self.clear_trial_setting(ui) + query = self.get_trial_setting(trial_id) + if not query: + return + self.map_trial_setting(query, ui) + + def read_factors(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT id, title, weight FROM factors WHERE trial_id = :trial_id ORDER BY id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + factors = [] + while query.next(): + factor = [query.value('id'), query.value('title'), query.value('weight')] + factors.append(factor) + return factors + + def read_treatments(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT id, title, ratio FROM treatments WHERE trial_id = :trial_id ORDER BY id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + treatments = [] + while query.next(): + treatment = [query.value('id'), query.value('title'), query.value('ratio')] + treatments.append(treatment) + return treatments + + def get_factor_level(self, trial_id): + factors = self.read_factors(trial_id) + if not factors: + return + fl = [] + for factor in factors: + query = qts.QSqlQuery(self.db) + query.prepare('SELECT id, title FROM levels WHERE factor_id = :factor_id ORDER BY id') + query.bindValue(':factor_id', factor[0]) + query.exec_() + levels = [] + while query.next(): + levels.append([query.value('id'), query.value('title')]) + fl.append(levels) + return fl + + def factor_levels(self, factor_id): + query = qts.QSqlQuery(self.db) + query.prepare("SELECT id, title FROM levels WHERE factor_id = :factor_id ORDER BY id") + query.bindValue(':factor_id', factor_id) + query.exec_() + levels = [] + while query.next(): + levels.append([query.value('id'), query.value('title')]) + return levels + + def insert_subject(self, identifier_value, treatment_id, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('''INSERT INTO subjects (trial_id, identifier_value, treatment_id) + VALUES (:trial_id, :identifier_value, :treatment_id)''') + query.bindValue(':trial_id', trial_id) + query.bindValue(':identifier_value', identifier_value) + query.bindValue(':treatment_id', treatment_id) + if not query.exec_(): + self.show_query_error(query) + return query.lastInsertId() + + def insert_subject_levels(self, trial_id, subject_id, subject_levels): + query = qts.QSqlQuery(self.db) + for factor_id, level_id in subject_levels: + query.prepare('''INSERT INTO subject_levels (trial_id, subject_id, factor_id, level_id) + VALUES (:trial_id, :subject_id, :factor_id, :level_id);''') + query.bindValue(':trial_id', trial_id) + query.bindValue(':subject_id', subject_id) + query.bindValue(':factor_id', factor_id) + query.bindValue(':level_id', level_id) + if not query.exec_(): + self.show_query_error(query) + return self.get_subject_level_titles(subject_levels) + + def clear_preload(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('DELETE FROM preloads WHERE trial_id = :trial_id') + query.bindValue(':trial_id', trial_id) + query.exec_() + + def delete_subject_levels(self, subject_id): + query = qts.QSqlQuery(self.db) + query.prepare('DELETE FROM subject_levels WHERE subject_id = :subject_id') + query.bindValue(':subject_id', subject_id) + if not query.exec_(): + self.show_query_error(query) + + def delete_subject(self, subject_id, id_value=None, trial_id=None): + query = qts.QSqlQuery(self.db) + query.prepare('DELETE FROM subjects WHERE id = :subject_id;') + query.bindValue(':subject_id', subject_id) + if not query.exec_(): + self.show_query_error(query) + if id_value is None: + return + query.prepare('''INSERT INTO discarded_identifiers (trial_id, identifier) + VALUES (:trial_id, :identifier)''') + query.bindValue(':trial_id', trial_id) + query.bindValue(':identifier', id_value) + if not query.exec_(): + self.show_query_error(query) + + def delete_subjects(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('DELETE FROM subjects WHERE trial_id = :trial_id;') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + query.prepare('DELETE FROM discarded_identifiers WHERE trial_id = :trial_id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + + def save_preload(self, trial_id, preload): + query = qts.QSqlQuery(self.db) + for key in preload: + t, f, lv = key + c = preload[key] + query.prepare('''INSERT INTO preloads (trial_id, treatment_id, factor_id, level_id, count) + VALUES (:trial_id, :treatment_id, :factor_id, :level_id, :count)''') + query.bindValue(':trial_id', trial_id) + query.bindValue(':treatment_id', t) + query.bindValue(':factor_id', f) + query.bindValue(':level_id', lv) + query.bindValue(':count', c) + query.exec_() + + def show_error(self, title, text): + qtw.QMessageBox.critical(self.mainWindow, title, text) + + def show_query_error(self, query): + qtw.QMessageBox.critical( + self.mainWindow, + self.tr('DB Error'), + query.lastError().text() + ) + fatal = 'UNIQUE constraint failed' not in query.lastError().text() + if fatal: + sys.exit(1) + return False + else: + return True + + def has_preload(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT COUNT(*) FROM preloads WHERE trial_id = :trial_id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + if not query.next(): + return False + return query.value(0) > 0 + + def get_subject_count(self, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT COUNT(*) FROM subjects WHERE trial_id = :trial_id') + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + if not query.next(): + return False + return query.value(0) + + def has_subject(self, trial_id): + subject_count = self.get_subject_count(trial_id) + if not subject_count: + return False + return subject_count > 0 + + def get_first_trial_setting(self): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT min(id) FROM trials') + if not query.exec_(): + self.show_query_error(query) + if not query.next(): + return False + trial_id = query.value(0) + return self.get_trial_setting(trial_id) + + def trial_title_exists(self, title): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT COUNT(*) FROM trials WHERE title = :title') + query.bindValue(':title', title) + if not query.exec_(): + self.show_query_error(query) + query.next() + return query.value(0) > 0 + + def treatment_title_exists(self, title, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT COUNT(*) FROM treatments WHERE title = :title and trial_id = :trial_id') + query.bindValue(':title', title) + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + query.next() + return query.value(0) > 0 + + def factor_title_exists(self, title, trial_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT COUNT(*) FROM factors WHERE title = :title and trial_id = :trial_id') + query.bindValue(':title', title) + query.bindValue(':trial_id', trial_id) + if not query.exec_(): + self.show_query_error(query) + query.next() + return query.value(0) > 0 + + def level_title_exists(self, title, factor_id): + query = qts.QSqlQuery(self.db) + query.prepare('SELECT COUNT(*) FROM levels WHERE title = :title and factor_id = :factor_id') + query.bindValue(':title', title) + query.bindValue(':factor_id', factor_id) + if not query.exec_(): + self.show_query_error(query) + query.next() + return query.value(0) > 0 \ No newline at end of file diff --git a/db/database.sql b/db/database.sql new file mode 100755 index 0000000..f543b6a --- /dev/null +++ b/db/database.sql @@ -0,0 +1,77 @@ +CREATE TABLE trials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + code TEXT, + created DATETIME DEFAULT CURRENT_TIMESTAMP, + modified DATETIME DEFAULT CURRENT_TIMESTAMP, + prob_method INTEGER DEFAULT 0, + base_prob FLOAT DEFAULT 0.7, + dist_method INTEGER DEFAULT 0, + identifier_type INTEGER DEFAULT 0, + identifier_order INTEGER DEFAULT 0, + identifier_length INTEGER DEFAULT 3, + recycle_ids INTEGER DEFAULT 0, + new_subject_random INTEGER DEFAULT 0, + arms_weight FLOAT DEFAULT 0, + UNIQUE(title) ON CONFLICT ABORT); +CREATE TABLE discarded_identifiers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + identifier INTEGER NOT NULL, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + UNIQUE (trial_id, identifier) ON CONFLICT ABORT); +CREATE TABLE treatments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + title TEXT NOT NULL, + ratio INTEGER DEFAULT 1, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + UNIQUE (trial_id, title) ON CONFLICT ABORT); +CREATE TABLE factors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + title TEXT NOT NULL, + weight FLOAT DEFAULT 1.0 NOT NULL, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + UNIQUE (trial_id, title) ON CONFLICT ABORT); +CREATE TABLE levels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + factor_id INTEGER NOT NULL, + title TEXT NOT NULL, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + FOREIGN KEY (factor_id) REFERENCES factors (id) ON DELETE CASCADE, + UNIQUE (factor_id, title) ON CONFLICT ABORT); +CREATE TABLE subjects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + identifier_value INTEGER NOT NULL, + treatment_id INTEGER NOT NULL, + enrolled DATETIME DEFAULT CURRENT_TIMESTAMP, + modified DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + FOREIGN KEY (treatment_id) REFERENCES treatments (id) ON DELETE CASCADE, + UNIQUE (trial_id, identifier_value) ON CONFLICT ABORT); +CREATE TABLE subject_levels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + subject_id INTEGER NOT NULL, + factor_id INTEGER NOT NULL, + level_id INTEGER NOT NULL, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + FOREIGN KEY (subject_id) REFERENCES subjects (id) ON DELETE CASCADE, + FOREIGN KEY (factor_id) REFERENCES factors (id) ON DELETE CASCADE, + FOREIGN KEY (level_id) REFERENCES levels (id) ON DELETE CASCADE, + UNIQUE (factor_id, subject_id) ON CONFLICT ABORT); +CREATE TABLE preloads ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + treatment_id INTEGER NOT NULL, + factor_id INTEGER NOT NULL, + level_id INTEGER NOT NULL, + count INTEGER DEFAULT 0, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + FOREIGN KEY (treatment_id) REFERENCES treatments (id) ON DELETE CASCADE, + FOREIGN KEY (factor_id) REFERENCES factors (id) ON DELETE CASCADE, + FOREIGN KEY (level_id) REFERENCES levels (id) ON DELETE CASCADE, + UNIQUE (treatment_id, factor_id, level_id) ON CONFLICT ABORT) diff --git a/db/db.sql b/db/db.sql new file mode 100755 index 0000000..315489e --- /dev/null +++ b/db/db.sql @@ -0,0 +1,89 @@ +BEGIN TRANSACTION; +CREATE TABLE trials ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + code TEXT, + created DATETIME DEFAULT CURRENT_TIMESTAMP, + modified DATETIME DEFAULT CURRENT_TIMESTAMP, + prob_method INTEGER DEFAULT 0, + base_prob FLOAT DEFAULT 0.7, + dist_method INTEGER DEFAULT 0, + identifier_type INTEGER DEFAULT 0, + identifier_order INTEGER DEFAULT 0, + identifier_length INTEGER DEFAULT 3, + recycle_ids INTEGER DEFAULT 0, + new_subject_random INTEGER DEFAULT 0, + arms_weight FLOAT DEFAULT 0); +CREATE TABLE discarded_identifiers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + identifier INTEGER NOT NULL, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + UNIQUE (trial_id, identifier) ON CONFLICT ABORT); +CREATE TABLE treatments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + title TEXT NOT NULL, + ratio INTEGER DEFAULT 1, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + UNIQUE (trial_id, title) ON CONFLICT ABORT); +CREATE TABLE factors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + title TEXT NOT NULL, + weight FLOAT DEFAULT 1.0 NOT NULL, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + UNIQUE (trial_id, title) ON CONFLICT ABORT); +CREATE TABLE levels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + factor_id INTEGER NOT NULL, + title TEXT NOT NULL, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + FOREIGN KEY (factor_id) REFERENCES factors (id) ON DELETE CASCADE, + UNIQUE (factor_id, title) ON CONFLICT ABORT); +CREATE TABLE subjects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + identifier TEXT NOT NULL, + identifier_value INTEGER NOT NULL, + treatment_id INTEGER NOT NULL, + enrolled DATETIME DEFAULT CURRENT_TIMESTAMP, + modified DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + FOREIGN KEY (treatment_id) REFERENCES treatments (id) ON DELETE CASCADE, + UNIQUE (trial_id, identifier) ON CONFLICT ABORT); +CREATE TABLE subject_levels ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + subject_id INTEGER NOT NULL, + factor_id INTEGER NOT NULL, + level_id INTEGER NOT NULL, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + FOREIGN KEY (subject_id) REFERENCES subjects (id) ON DELETE CASCADE, + FOREIGN KEY (factor_id) REFERENCES factors (id) ON DELETE CASCADE, + FOREIGN KEY (level_id) REFERENCES levels (id) ON DELETE CASCADE, + UNIQUE (factor_id, subject_id) ON CONFLICT ABORT); +CREATE TABLE preloads ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trial_id INTEGER NOT NULL, + treatment_id INTEGER NOT NULL, + factor_id INTEGER NOT NULL, + level_id INTEGER NOT NULL, + count INTEGER DEFAULT 0, + FOREIGN KEY (trial_id) REFERENCES trials (id) ON DELETE CASCADE, + FOREIGN KEY (treatment_id) REFERENCES treatments (id) ON DELETE CASCADE, + FOREIGN KEY (factor_id) REFERENCES factors (id) ON DELETE CASCADE, + FOREIGN KEY (level_id) REFERENCES levels (id) ON DELETE CASCADE, + UNIQUE (treatment_id, factor_id, level_id) ON CONFLICT ABORT); +DELETE FROM sqlite_sequence; +CREATE VIEW subject_treatment AS SELECT + subjects.id id, subjects.trial_id trial_id, identifier, treatments.title as treatment + FROM subjects INNER JOIN treatments ON treatments.id = treatment_id; +CREATE VIEW sl_view as SELECT subject_levels.id id, subject_levels.subject_id subject_id, +subject_levels.factor_id factor_id, subject_levels.level_id level_id, factors.title factor, +levels.title level, subjects.treatment_id from subject_levels LEFT JOIN levels ON +subject_levels.level_id = levels.id LEFT JOIN factors ON subject_levels.factor_id = factors.id +LEFT JOIN subjects ON subject_levels.subject_id = subjects.id; +COMMIT; + diff --git a/db/minimizer.db b/db/minimizer.db new file mode 100755 index 0000000000000000000000000000000000000000..56e87eaebad0f4aa213e1d998fabe9c2caf910c6 GIT binary patch literal 81920 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 XTkvj6*+H*euC@-q54-Uom)`#e%=TIM literal 0 HcmV?d00001 diff --git a/enrol_form.py b/enrol_form.py new file mode 100755 index 0000000..a8c2d95 --- /dev/null +++ b/enrol_form.py @@ -0,0 +1,138 @@ +from PySide2 import QtWidgets as qtw +from PySide2 import QtCore as qtc + + +class EnrolForm(qtw.QDialog): + def __init__(self, parent, factors, treatment, edit=False, identifier=None): + super().__init__() + self.m_treatment = None + self.p_treatment = None + self.m_index = None + self.p_index = None + self.selected_probs = None + self.parent = parent + self.setGeometry(100, 100, 400, -1) + self.factors = factors + self.treatment = treatment + form_layout = qtw.QFormLayout() + if edit: + self.setWindowTitle(self.tr('Editing subject "{}"').format(identifier)) + treatment_combo = qtw.QComboBox() + treatment_titles = [] + treatment_ids = [] + selected_treatment_index = -1 + for i, t in enumerate(treatment['treatments']): + treatment_titles.append(t['title']) + treatment_ids.append(t['id']) + if treatment['selected_treatment_id'] == t['id']: + selected_treatment_index = i + treatment_combo.addItems(treatment_titles) + treatment_combo.setCurrentIndex(selected_treatment_index) + treatment_combo.currentIndexChanged.connect(self.get_treatment_combo_events(treatment_ids)) + form_layout.addRow(self.tr('Treatment'), treatment_combo) + else: + self.setWindowTitle(self.tr('Subject enrol')) + + for f, factor in enumerate(factors): + factor_title = factor['title'] + levels_combo = qtw.QComboBox() + level_titles = [] + level_ids = [] + selected_index = -1 + for i, level in enumerate(factor['levels']): + level_titles.append(level['title']) + level_ids.append(level['id']) + if factor['selected_level_id'] == level['id']: + selected_index = i + levels_combo.addItems(level_titles) + levels_combo.setCurrentIndex(selected_index) + levels_combo.currentIndexChanged.connect(self.get_levels_combo_event(f, level_ids)) + form_layout.addRow(factor_title, levels_combo) + self.result_label = qtw.QLabel(self.tr('Select factors, click enrol')) + self.enrol_button = qtw.QPushButton(self.tr('Enrol')) + close_button = qtw.QPushButton(self.tr('Close')) + if edit: + self.enrol_button.setText(self.tr('Save')) + form_layout.addRow(self.enrol_button) + close_button.setText(self.tr('Cancel')) + self.enrol_button.clicked.connect(self.save_subject) + else: + form_layout.addRow(self.enrol_button, self.result_label) + self.enrol_button.clicked.connect(self.enrol_subject) + clickable(self.result_label).connect(self.on_detail) + close_button.clicked.connect(self.close) + form_layout.addRow(close_button) + self.setLayout(form_layout) + + def on_detail(self): + trial_id = self.parent.settings.value('last_trial_id', 0, type=int) + if trial_id == 0: + return + ns = self.parent.database.get_subject_count(trial_id) + if ns == 1: + qtw.QMessageBox.information(self.parent, self.tr('Minimisation detail'), self.tr('Enrolled by randomisation')) + return + final_group = self.tr('preferred') + prob = self.selected_probs[self.p_index] + if self.m_treatment != self.p_treatment: + final_group = self.tr('non-preferred') + prob = self.selected_probs[self.m_index] + msg = self.tr('Preferred treatment = {}\n').format(self.p_treatment) + msg += self.tr('Minimised treatment = {}\n').format(self.m_treatment) + msg += self.tr('Subject assigned to {} treatment with a probability of {:4.2f}').format(final_group, prob) + qtw.QMessageBox.information(self.parent, self.tr('Minimisation detail'), msg) + + def save_subject(self): + self.close() + self.setResult(qtw.QDialog.Accepted) + + def enrol_subject(self): + selected_indices = [] + selected_ids = [] + for factor in self.factors: + if factor['selected_level_id'] == -1: + self.result_label.setText(self.tr('Select {} level').format(factor['title'])) + return + for i, level in enumerate(factor['levels']): + if factor['selected_level_id'] == level['id']: + selected_indices.append(i) + selected_ids.append((factor['id'], factor['selected_level_id'])) + break + mt, mt_index, pt, pt_index, identifier, probs = self.parent.enrol_one(selected_indices, selected_ids) + self.m_index = mt_index + self.p_index = pt_index + self.m_treatment = mt + self.p_treatment = pt + self.selected_probs = probs + style = 'color: blue;' + if mt != pt: + style = 'color: red;' + self.result_label.setStyleSheet(style) + self.result_label.setText(self.tr('Subject "{}", Enrolled to {}').format(identifier, mt)) + self.enrol_button.setEnabled(False) + + + def get_treatment_combo_events(self, treatment_ids): + def on_treatment_combo_index_changed(index): + self.treatment['selected_treatment_id'] = treatment_ids[index] + return on_treatment_combo_index_changed + + def get_levels_combo_event(self, f, level_ids): + def on_levels_combo_index_changed(index): + self.factors[f]['selected_level_id'] = level_ids[index] + return on_levels_combo_index_changed + +def clickable(widget): + class Filter(qtc.QObject): + clicked = qtc.Signal() + def eventFilter(self, obj, event): + if obj == widget: + if event.type() == qtc.QEvent.MouseButtonRelease: + if obj.rect().contains(event.pos()): + self.clicked.emit() + # The developer can opt for .emit(obj) to get the object within the slot. + return True + return False + filter = Filter(widget) + widget.installEventFilter(filter) + return filter.clicked diff --git a/fa_IR.ts b/fa_IR.ts new file mode 100755 index 0000000..effd5a7 --- /dev/null +++ b/fa_IR.ts @@ -0,0 +1,1072 @@ + + + + + ConfigDialog + + + MinimPy2 Config + تنظیمات مینیم‌پای۲ + + + + Language + زبان + + + + Random engin + موتور تصادفی سازی + + + + Save + اندوختن + + + + Database + + + Error + نادرستی + + + + DB Connection Error + بروز نادرستی هنگام پیوستن به بانک داده + + + + Could not open database file: + پرونده داده را نمی توان باز نمود: + + + + DB Integrity Error + بروز ناردستی در ساختار بانک داده + + + + Missing tables, please repair DB: + نشانه ای از جدول های زیر نیست + + + + DB read Error + بروز نادرستی هنگام خوانش بانک داده + + + + DB Error + بروز نادرستی بانک داده + + + + Could not open database file {} + پرونده بانک دادگان باز نمیشود {} + + + + Missing tables, please repair DB {} + برخی حدول ها در بانک دادگان نییست {} + + + + EnrolForm + + + Editing subject "{}" + ویرایش سوژه "{}" + + + + Treatment + درمان + + + + Select factors, click enrol + پس از گزینش فاکتورها بر روی پذیرش کلیک کنید + + + + Enrol + پذیرش + + + + Close + بستن + + + + Save + اندوختن + + + + Cancel + بازگشت + + + + Select {} level + سطح {} را برگزینید + + + + Subject "{}", Enrolled to {} + سوژه "{}", برای {} پذیرش شد + + + + Subject enrol + سوژه جدید + + + + preferred + برگزیده + + + + non-preferred + غیربرگزیده + + + + Preferred treatment = {} + + درمان برگزیده = {} + + + + + Minimised treatment = {} + + درمان مینی مایز شده = {} + + + + + Subject assigned to {} treatment with a probability of {:4.2f} + سوژه به گروه {} با احتمال {:4.2f} افزوده شد + + + + Minimisation detail + تفصیل مینی میزیشن + + + + FactorLevels + + + Level title + عنوان لایه + + + + Title + عنوان + + + + Error! + نادرستی! + + + + Level title '{}' already exist! + لایه ای با نام '{}' هم اکنون در دادگان هست! + + + + Confirm delete + پذیرش پاکسازی + + + + Are you sure you want to delete "%s"? + آیا براستی میخواهدی "%s" پاک کنید؟ + + + + ID + شناسه + + + + Level + لایه + + + + Levels of {} + لایه های {} + + + + FactorLevelsDialog + + + Dialog + دیالوگ + + + + Close + بستن + + + + Delete + پاک کردن + + + + Add + افزودن + + + + MainWindow + + + Start + شروع + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:20pt; font-weight:600;">How to do minimisation using MinimPy2?</span></p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt; font-weight:600;">A quick tutorial</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">1 - Go to Trials tab and select a trial as current to work on it. Define a new trial if there is no trial</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">2 - Go to Setting tab to view / edit trial setting. Usually the default setting is good to go</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">3 - Go to Treatments tab to define at lease two treatments</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">4 - Go to Factors tab to define at lease one factor</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">5 - Define at least two levels for each factor</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">6 - Now the trial is ready for subject enrollment</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">7 - Go to subject tab and click on the Add button in the toolbar</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">8 - The enroll window will appear. Select a level for each factor and click enroll</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">9 - Subject will enroll to one of treatment groups by the minimisation algorhythm</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">10 - If you want to define a preload go to Frequencies tab</p></body></html> + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p align="center" dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:20pt; font-weight:600;">چگونه مینی مایزیشن انجام دهیم</span></p> +<p align="center" dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt; font-weight:600;">یک آموزش سریع</span></p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۱ - به صفحه کارآزمائی ها بروید و یکی را به عنوان جاری انتخاب کنید. اگر هیچ کارآزائی موجود نبود یکی را تعریف کنید</p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۲ - برای دیدن یا ویرایش تنظیمات به صفحه تنظیمات بروید. معمولا تنظیمات اولیه کافی است</p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۳ - به صفحه درمان ها بروید و حداقل دو درمان را تعریف کنید</p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۴ - به صفحه فاکتورها بروید و حداقل یک فاکتور تعریف کنید</p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۵ - برای هر فاکتور حداقل دو لایه تعریف کنید</p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۶ - اکنون کارآزمائی آماده پذیرش سوژه است</p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۷ - به صفحه سوژه ها بروید و بر روی دکمه افزودن در نوار بالای صفحه کلیک کنید</p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۸ - پر پنجره سوژه جدید برای هر فاکتور لایه ای انتخاب کنید و دکمه پذیرش را بزنید</p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۹ - سوژه جدید توسز الگوریتم مینی میزیشن به یک از گروه ها افزوده میشود</p> +<p dir='rtl' style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p dir='rtl' style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">۱۰ اگر میخواهدی برای کارآزمائی پره لود تعریف کنید به صفحه فراوانی بروید</p></body></html> + + + + Current trial exported successfully to "{}" + کارآزمائی جاری با موفقیت در فایل "{}" ذخیره گردید + + + + Error in languages.lst file format\nPlease see the READ.ME file in locales folder + خطا در فایل languages.lst\nلطفا فایل READ.ME در پوشه locales را بدقت بخوانید + + + + Close + بستن + + + + What is Minimisation + درباره مینی‌میزیشن + + + + About + درباره + + + + Factor/Level + فاکتور/لایه + + + + SD + انحراف از معیار + + + + Enrolled + پذیرش شده + + + + Treatment + درمان + + + + Save not available! + اندوختن هم اکنون شدنی نیست! + + + + Clear preload + پاکسازی پره لود + + + + To clear preload, select preload and check edit + برای پاک کردن پره‌لود، پره لود و ویرایش را برگزینید + + + + Total + جمع کل + + + + Error + نادرستی + + + + Sample size consumed! +No further subject enrol possible! +Please convert cases to preload and continue + همه نمونه مصرف شده است. دیگر نمی توان سوژه جدیدی وارد کرد. لطفا سوژه ها را به پره لود تبدیل کنید و ادامه دهید + + + + Filter + فیلتر + + + + Double check! + دوبار چک! + + + + Need your final confirm + تایید نهائی شما را میخواهد + + + + Deleting preload + پاک کردن پره لود + + + + Are you certainly sure you want to convert all subjects into preload? +ALL subjects will be deleted + آیا واقعا میخواهید همه سوژه ها را به پره لود تبدیل کنید؟ توجه کنید! همه سوژه ها حذف خواهند شد + + + + Help! + راهنما! + + + + minimpy2 [{}] + مینیم‌پای۲ [{}] + + + + Factor title + عنوان فاکتور + + + + Title + عنوان + + + + Error! + خطا! + + + + Factor title '{}' already exist! + فاکتور با عنوان '{}' هم اکنون وجود دارد! + + + + Treatment title + عنوان درمان + + + + Treatment title '{}' already exist! + درمانی با عنوان '{}' هم اکنون وجود دارد! + + + + Trial title + عنوان کارآزمائی + + + + A trial title '{}' already exist! + کارآزمائی بالینی با عنوان '{}' هم اکنون وجود دارد! + + + + Confirm delete + تایید عمل حذف + + + + Are you sure you want to delete subject "{}"? + آیا واقعا میخواهید سوژه '{}' را حذف کنید؟ + + + + New factor not allowed! + مجاز به افزودن فاکتور جدید نیستید! + + + + Trial already has subject or preload. + You can not add or delete factors, treatments or levels + این کارآزمائی هم اکنون دارای سوژه و یا پره لود می باشد ولذا شما نمی توانید تغییری در درمانها، فاکتورها و سطوح آنها بدهید + + + + Are you sure you want to delete "%s"? + آیا واقعا میخواهید "%s" حذف کنید؟ + + + + minimpy2 + مینیم‌پای ۲ + + + + version + نسخه + + + + 2.0 + ۲/۰ + + + + Copyright + کپی رایت + + + + 2020 + ۱۳۹۹ + + + + Dr. Mahmoud Saghaei + دکتر محمود سقائی + + + + MinimPy + مینیم‌پای + + + + ID + شناسه + + + + Ratio + نسبت + + + + Factor + فاکتور + + + + Levels + لایه ها + + + + Weight + وزن + + + + Code + کد + + + + Current + کنونی + + + + Mean + میانگین + + + + Max + بیشینه + + + + Balance (mean MB = {:.4f}) + تراز (میانگین ت‌م = {:.4f}) + + + + No subject enrolled! + هنوز سوژه ای وارد نشده است! + + + + Select mnd file + فایل mnd را انتخاب کنید + + + + Warning + هشدار + + + + Do you want to save this trial? + آیا می خواهید این کارآزمائی ذخیره شود؟ + + + + Imported + داده ها وارد شد + + + + Data imported successfully! + داده های با موفقیت وارد شد! + + + + Import error! + بروز نادرستی هنگام درون‌داد داده! + + + + Data are not valid! + داده های بی ارزشند! + + + + Export to file + برون داد به پرونده + + + + Select only preload and edit + تنها پره لود و گزینه ویرایش را برگزینید + + + + Confirm clear + پذیرش پاکسازی + + + + Are you sure you want to clear preload + آیا براستی میخواهید پره لود را پاک کنید + + + + New trial + کارآزمائی جدید + + + + Maximum sample size > {} + بیشترین اندازه نمونه > {} + + + + Are you sure you want to quit? + براستی میخواهید از برنامه بیرون بروید؟ + + + + No current trial! + هیچ کارآزمائی گزینش نشده است! + + + + Fist you have to define and select a trial as current! + نخست باید که یک کارآزمائی را ساخته و انتخاب کنید! + + + + Are you sure you want to edit subject at row "{}" ? + +Editing subject may invalidate your research and the result of minimisation + آیا براستی میخواهید سوژه ردیف "{}" ویرایش نمائید? ویرایش سوژه ها ممکن است پژوهش شما را نادرست نماید و سبب تورش در مینی مایزیشن گردد + + + + MainWindow + پنجره اصلی + + + + Trials + کارآزمائی‌ها + + + + Setting + سازوکار + + + + Created + برپاشده + + + + Modified + ویرایش شده + + + + Trial code + کد کارآزمائی + + + + Probability method + روش احتمال + + + + Biased coin + سکه مخدوش + + + + Naive + خام + + + + Base probability + احتمال پایه + + + + 0.70 + 0.70 + + + + Distance method + روش بازه + + + + Marginal balance + تراز مرزی + + + + Range + دامنه + + + + Standard deviation + انحراف از معیار + + + + Variance + واریانس + + + + Identifier type + گونه شناسه + + + + Numeric + عددی + + + + Alpha + حروفی + + + + Alphanumeric + حروفی عدد + + + + Identifier order + چیدمان شناسه‌ها + + + + Sequential + پشت سر هم + + + + Random + شانسی + + + + Identifier length + درازای شناسه + + + + TextLabel + برچسب متن + + + + Recycle ids + بازیافت شناسه ها + + + + New subject random + سوژه جدید شانسی + + + + Arms weight + وزن یازوهای درمانی + + + + 1.0 + ۱/۰ + + + + Treatments + درمانها + + + + Factors + فاکتورها + + + + Frequencies + فراوانی + + + + Preload + پره‌لود + + + + Edit + ویرایش + + + + Convert to preload + تبدیل به پره لود + + + + I know what I'm doing + می دانم چکار میکنم + + + + Subjects + سوژه‌ها + + + + Balance + تراز + + + + Oveall Balance + تراز فراگیر + + + + Randomness + شاخص شانس + + + + File + پرونده + + + + toolBar + نوار ابزار + + + + Exit + بیرون رفتن + + + + Add + افزودن + + + + Delete + پاک کردن + + + + Save + اندوختن + + + + manage factor levels + اداره لایه های فاکتور + + + + Help + راهنما + + + + displays help + نمایش راهنما + + + + Import + درون داد + + + + Export + برون داد + + + + Config + پیکره بندی + + + + Application config + پیکره بندی برنامه + + + + Restart needed + نیاز به شروع مجدد دارد + + + + Changes will be applied after restart! + تغییرات داده شده تنها پس از شروع مجدد برنامه نشان داده خواهد شد! + + + + Each trial has a uniqe tile and also a code usually in the form of an abreviation of the title. +Last column show if the trial is the active one. Only one trial can be active at any time. Other pages of the application relate to this active trial. To make a treial active check the box in last column. +Click and then type over the trial title or code to edit it + هرکارآزمائی یک عنوان منحصربفرد دارد و همچنین کدی دارد که معمولا حروف اول عنوان کارآزمائی می باشد. +در ستون آخر شما مشخص می کنید که این کارآزمائی به عنوان کارآزمائی فعال یا جاری انتخاب شود. در هر لحظه تنها یک کارآزمائی میتواند کارآزمائی فعال و جاری باشد. دیگر صفحات این برنامه تابع کارآزمائی جاری خواهند بود. برای اینکه یک کارآزمائی را به عنوان جاری انتخاب کنید کافی است که گزینه ستون آخر فعال باشد. +برای ویرایش عنوان و کد کارآزمائی بالینی بر روی آن کلیک کرده و شروع به تایپ نمائید + + + + Creation date of this trial + + تاریخ آفرینش این کارآزمائی + + + + + Modification date of this trial + + تاریخ تغییر این کارآزمائی + + + + + Title of this trial. Title must be unique + + عنوان کارآزمائی. می بایست عنوانی منحصر به فرد باشد + + + + + Code of the trial usually title abbreviation + + کد کارآزمائی که معمولا حروف اول عنوان کارآزمائی می باشد + + + + + The method used for calculating of the assignment probabilities. There are two choices. Biased coin minimization which include the effect of allocation ratios into the computation, and naive method which do not include the effect of allocation ratios. Biased coin is the preffered method. These options only is meaningfull when we have treatments with unequal allocation ratios. Biased Coin method is based on the different probabilities of assignment for subjects to trial groups. The subject is allocated to the preferred treatment with a higher probability and to other groups with lower probabilities. Therefore if treatments with higher allocations ratios are assigned more probabilities this will be considered as biased-coin minimization. In this method a base probability is used for the group with the lowest allocation ratio when that group is selected as the preferred treatment. Probabilities for other groups (PHi) when they selected as preferred treatments are calculated as a function of base probability and their allocation ratios. In Naive method the subject is allocated to the preferred treatment with a higher probability and to other groups with lower probabilities. Here the probabilities are not affected by allocation ratios and usually the same high probability are used for all treatment groups when they are selected as preferred treatments (base probability). This is denoted by Naive Minimization. In this method, probabilities for non-preferred treatments are distributed equally. + + روش محاسبه احتمال های تقسیم سوژه ها بین گروه ها کارآزمائی. در اینجا دو گزینه داریم. سکه مخدوش که تفاوت گروه ها از نظر نسبت انتساب را در نظر میگیرد و روش خام که کاری به این نسبت های انتساب ندارد. روش برتر همانا سکه مخدوش می باشد. البته این گزینه تنها هنگامی معنی دارد که گروه های کارآزمائی از نظر نسبت انتساب تفاوت داشته باشند. پایه روش سکه مخدوش بر احتمال های مختلف برای تقسیم بندی سوژه های بین گروه ها می باشد. در این روش برای انتساب سوژه‌ از روش تصادفی سازی استفاده میشود ولی احتمال داده شدن سوژه به گروه برگزیده بیشتر از سایر گروه‌ها می باشد. گروه برگزیده گروهی است که اگر سوژه به آن گروه داده شود تفاوت گروه های کمتر از قبل میشود. گروه هائی که نسبت انتساب بیشتری دارند احتمال انتساب بیشتری برای آنها در نظر گرفته میشود. در واقع در این روش برای گروهی که کمترین میزان نسبت انتساب دارد مقداری مشخص و از پیش تعیین شده ای از احتمال در نظر گرفته میشود که به آن احتمال پایه می گوئیم. هنگامی که گروه دارای کمینه نسبت انتساب به عنوان گروه برگزیده انتخاب میشود با همین پایه احتمال انتساب وی اعمال میشود. مقدار احتمال برای سایر گروه ها هنگامی که گروه برگزیده میشوند بستگی به مقدار احتمال پایه و نسبت انتساب آنها دارد. در روش خام سوژه ها به درمان برگزیده با احتمال بیشتری و به سایر گروه ها با احتمال کمتری انتساب می یابند و این مقادیر احتمال تحت تاثیر نسبت های انتساب قرار نمی گیرند و معمولا با مقدار احتمال بالای یکسانی برای گروه های برگزیده انتساب می یابند. در این روش میزان احتمال بکار رفته برای سایر گروه های غیر برگزیده بصورت یکنواخت بین گروه ها توزیع میشود. + + + + + The probability given to the treatment with the lowest allocation ratio in biased-coin minimization. In naive minimization it is the probability used for all treatment groups when they are selected as preferred treatments. Use the spin button to specify the value. + + احتمال اختصاص یافته به درمانی که کمترین میزان نسبت انتساب می باشد هنگامی که این گروه به عنوان گروه برگزیده انتخاب میشود. برای تغییر مقدار آن از دکمه های جهتی استفاده کنید. + + + + + Distance measure is used to calculate the imbalance score. There are four options here. Marginal balance computes the cumulative difference between every possible pairs of level counts. This is the default and the one which is recommended. It tends to minimize more accurately when treatment groups are not equal in size. For each factor level marginal balance is calculated as a function of adjusted number of patients present in a factor level for each treatment group. Range calculates the difference between the maximum and the mimimum values in level counts. Standard deviation calculates the standard deviation of level counts and variance calculate their variances. All of these measures uses adjusted level counts. Adjustment performs relative to the values of allocation ratios. + + سنجه فاصله برای محاسبه ضریب عدم ترازی بکار میرود. برای این سنجه چهار گزینه موجود است. تراز مرزی که تفاوت انباشته بین لایه های فاکتور های بصورت تمام حالات ترکیبی زوجی از لایه ها می باشد. این روش پیش فرض و توصیه شده است و بویژه هنگامی که اندازه گروه ها یکسان نباشد خیلی دقیق تر عمل میکند. برای هر لایه فاکتور تراز لبه ای به صورت تابعی از تعداد تعدیل شده سوژه ها در آن لایه برای هر شاخه درمانی محاسبه میشود. گزینه دامنه تفاوت بین بیشترین و کمترین مقدار در شمارش سوژه ها در لایه‌های فاکتور می باشد. انحراف از معیار بصورت محاسبه انحراف از معیار شمار سوژه ها در لایه‌های فاکتور و واریانس عبارت از محاسبه واریانس می باشد. تمام این سنجه ها از شمار تعدیل شده سوژه ها استفاده میکنند. تعدیل شمار سوژه ها بر مبنای نسبت انتساب صورت میگیرد. + + + + + Type of subject identifier. It can be Numeric, Alpha or Alphanumeric. + + نوع شناسه نمایش داده شده برای سوژه ها. گزینه های موجود عددی، حرفی و یا حرفی عددی می باشند.\nهنگامی که اولین سوژه وارده مطالعه شد، دیگر نمی توان نوع شناسه را تغییر دارد + + + + + The order of identifier sequence. It can be Sequential or Random. + + ترتیب شناسه سوژه ها. می تواند ترتیبی و یا شانسی باشد. + + + + + Length of subjects identifier in character. The longer the length the higher will be the possible sample size + + درازی شناسه سوژه ها. شناسه سوژه ها هر چه بلندتر باشد اندازه نمونه ممکن که توسط شناسه ها قابل پوشش هستند بیشتر خواهد شد + + + + + Check to reuse IDs of a deleted subject. The default is to discard + + برای بازیافت شناسه ها چک باکس را فعال کنید. پیشفرض غیر فعال است + + + + + If unchecked (the default), enrol form will show with the level for each factor unselected. If checked, for each factor a random level will be selected. This is only for test purpose to rapidly and randomly enrol a subject + + اگر چک باکس غیرفعال باشد (حالت پیش فرض)، در فرم سوژه جدید، لایه های هر فاکتور بصورت انتخاب نشده می ماند. در غیر این صورت برای هر فاکتور یک لایه بصورت شانسی انتخاب میگردد. این حالت برای انتساب سریع سوژه های تصادفی برای اهداف پژوهشی می باشد و استفاده دیگری ندارد + + + + + This is equalizing factor for arms of trial. The higher the arms weight the higher will be the possibility of having equal group sizes at the end of study + + این عدد ضریب مساوی سازی اندازه گروه های کارآزمائی بالینی است. هرچه این عدد بیشتر باشد، احتمال مساوی شدن نهائی اندازه نمونه گروه ها بیشتر میشود. به هر حاب حتی با بیشترین مقدار این عدد احتمال نامساوی شدن تعداد سوژه ها در گروه های مختلف موجود دارد + + + + + For each treatment a name (unique for the trial) and an integer value for allocation ratio are needed. Allocation ratios are integer numeric values which denote the ratios of the treatment counts. For example if we have three groups a, b and c with their allocation ratios 1, 2 and 3 respectively, this mean that in the final sample nearly 1/6 of subjects will be from treatment a, 1/3 from b and 1/2 from c +You have to define at lease two groups to use minimisation on them. +Click on treatment title and type over it to edit the title of the treatment, and use the spin control to change the allocation ratios + برای هر گروه درمانی یک عنوان (منحصر به فرد در کارآزمائی) و یک عدد صحیح نشان دهنده نسبت انتساب تعریف میشود. نسبت انتساب مشخص کننده نسبت نهائی تعداد سوژه ها در هر گروه کارآزمائی می باشد. به عنوان مثال اگر سه گروه درمانی الف و ب و ج داشته باشیم با نسبت های انتساب ۱، ۲ و ۳، این بدان معنی است که در نمونه نهائی تقریبا یک ششم سوژه ها از گروه الف، یک سوم از گروه ب و نیمی از سوژه ها از گروه ج خواهند بود. +شما باید حداقل دو گروه برای کارآزمائی تان تعریف کنید تا بتوانید بر روی آن دو مینیمایزیشن انجام دهید. +برای ویرایش عنوان درمان ها بر روی عنوان درمان در جدول درمانها کلیک کنید و شروع به تایپ نمائید. برای تغییر مقدار نسبت انتساب از کنترل ستون آخر استفاده کنید + + + + Factors or prognostic factors, are the subject factors which are selected to match subjects based on them. At lease one factor is mandatory for minimisation. Each factor has a name (unique for the trial) and a weight. Weight indicates the relative importance of the factor for minimization. For each factor you must define at least two levels. As an example factor may be gender and its levels may be Male and Female. +Factor title can be edited by clicking and typing over the title of the factor + +Click on the levels button on tool bar or double click on levels in table to manage (add, edit, delete) levels of the selected factor + فاکتورها یا فاکتورهای پروگنوستیک، فاکتور هائی از سوژه ها می باشند که شما برای متعادل ساختن گروه های کارآزمائی از نظر آن فاکتورها انتخاب نموده اید. برای اجرای مینی مایزیشن حداقل می بایست یک فاکتور تعریف کنید و هر فاکتور باید حداقل دو لایه داشته باشد. هر فاکتور دارای عنوانی است که در آن کارآزمائی منحصر به فرد است. همچنین هر فاکتور وزنی نیز دارد که اهمیت نسبی آن فاکتور در مدل مینی مایزیشن را نشان میدهد. هر چه وزن یک فاکتور بیشتر باشد احتما تعادل گروه ها از نظر آن فاکتور بیشتر میگردد. +مثال برای فاکتور می تواند جنس باشد که شما ممکن است دو لایه مؤنث و مذکر برای آن انتخاب کنید. +برای ویرایش عنوان فاکتور بر روی عنوان کلیک کرده و شروع به تایپ کنید. در برنامه کنترل هائی برای تعریف فاکتور جدید حذف فاکتورهای موجود و ویرایش لایه های هر فاکتور وجود دارد. برای ویرایش لایه ها دکمه لایه ها رد نوار ابزار بالای صفحه را کلیک کنید و یا بر روی لایه های یک فاکتور در جدول فاکتورها دبل‌کلیک کنید. در صفحه ویرایش لایه ها می توانید لایه جدید تعریف کنید، لایه ها را خذف کنید و یا عنوان آنها را ویرایش کنید + + + + Manage (add, edit, delete) levels of the selected factor + اداره لایه های فاکتور (یعنی افزودن، ویراستن، و حذف کردن لایه‌ها) + + + + This table shows frequencies of subjects enrolled in the trial or entered via preload table depending on the active checkboxes. When you have already enrolled a number of subjects (using other tools or software) and you want to continue with this minimisation program for the remaining cases, you can enter data of already enrolled subject as counts. You enter counts of different treatments accross all levels of each subject into the preload table. Then the counts from this table will be added to counts of enrolled subject by this application and total counts will be used in minimisation model. +It is as if you have entered the list of subject into your existing list. The effect is exactly the same. When defining preload please note that sum of subjects across different levels of a factor must be equal to thats of other factors, otherwise an error will be generated. +If you have a greal number of subjects and have no need to keep their list, you may convert them into preload table. Select frequencies check box and unselect preload checkbox, then click 'convert to preload' to clear all subjects and add them to preload table. + این جدول فراوانی سوژه ها مستقیما وارد شده در کارآزمائی و یا تعریف شده به عنوان پره لود را نشان میدهد. با انتخاب هرکدام از چک باکس های بالای جدول می توانید فراوانی سوژه های مستقیما وارد شده (فراوانی) و یا تعریف شده به عنوان پره لود و یا مجموع هر دو را مشاهده نمائید. منظور از پره لود چیست؟ وقتی که شما تعدای سوژه را توسط سیستم ها یا برنامه های دیگر وارد کارآزمائی نموده اید و اکنون میخواهید ادامه کار با برنامه مینیم‌پای باشد، چگونه می توانید سوژه های قبلی را به برنامه مینیم‌پای وارد کنید. اگر تعداد سوژه ها خیلی کم باشد (کمتر از ده مورد) وارد کردن آنها بصورت مینی مایز نمودن یک به یک آنها و سپس ویرایش گروه آنها ممکن است خیلی وقت نگیرد ولی اگر تعداد زیادی را از قبل وارد مطالعه کرده اید این روش بسیار وقت گیر و طاقت فرسا و مستعد اشتباه است. در این موارد بهتر است سوژه های را که تا بحال وارد مطالعه نموده اید را بصورت جدولی متشکل از فراوانی سوژه ها در هر لایه فاکتور و برای هر گروه درمانی وارد برنامه کنید. به این جدول که فراوانی سوژه های قبلی را نشان میدهد پره‌لود میگوئیم. پس از این کار، جدول پره لود با سوژه هائی که مستقیما وارد برنامه شده یکجا جمع شده و در مدل مینی مایزیشن بکار خواهد رفت. در واقع از نظر نتیجه مینی مایزیشن هیچ تفاوتی بین جدول پره لود و سوژه هائی که مستقیما وارد شده اند نمی باشد. +برای وارد کردن جدول پره لود، ابتدا چک باکس فراوانی را غیر فعال و سپس جک باکس پره لود را فعال و سرانجام جک باکس ویرایش را فعال کنید. سپس فراوانی ها را در جدول وارد و دست آخر دکمه 'اندوختن' را کلیک کنید. توجه کنید که سرجمع لایه ها برای فاکتور های مختلف الزاما باید برابر باشد در غیر این صورت برنامه از ذخیره کردن خودداری خواهد کرد و پیام خطائی نمایش خواهد داد. همچنین برای پاک کردن جدول پره لود از دکمه پاک کردن در نوار ابراز می توانید استفاده کنید. +چنانچه تعداد سوژه های شما خیلی زیاد شده است و نیازی به داشتن لیست آنها ندارید می توانید سوژه های موجود را به جدول پره‌لود تبدیل کنید. برای این کار چک باکس فراوانی را فعال کنید، پره لود را غیر فعال کنید و سپس بر روی دکمه تبدیل به پره لود کلیک کنید. برنامه دو بار از شما تایید می خواهد و سپس همه سوژه ها را پاک کرده و آنها را به جدول پره‌لود اضافه میکند. + + + + Subjects enrolled in the trial will be listed in subjects table. Each row belongs to one subject and shows subject ID, treatment, levels of factors, dates of enrollment and modification (if any). Subject can be added, edited or deleted from this table, although editing or deleting subject may compromise the minimisation model and therefore are not recommended.To enter a new subject, click plus sign in the tool bar. To edit double click and to delete click on Delete button on toolbar + + سوژه های وارد شده در کارآزمائی در جدول سوژه ها نمایش داده میشوند. هر ردیف مربوط به یک سوژه است و شناسه وی، گروه درمانی وی، لایه انتخاب شده هر فاکتور برای وی، تاریخ وارد شدن در مطالعه و در صورت ویرایش های بعدی تاریخ آخرین تغییرات را نشان میدهد. سوژه ها را می توان به کارآزمائی اضافه نمود و یا حذف و ویرایش کرد، اگر چه این دو اقدام آخر توصیه نمی شود۷ چراکه سبب خدشه دار شدن پژوهش و نتایج مینی مایزیشن میگردد. برای وارد کردن (مینی مایز نمودن) یک سوژه جدید، بر روی دکمه 'افزودن' در نوار بالای صفحه کلیک کنید. در پنجره باز شده لایه هر فاکتور برای آن بیمار را انتخاب و سپس بر روی دکمه 'پذیرش' کلیک کنید تا یک گروه درمانی برای سوژه جدید توسط برنامه تعریف شود (مینی مایزیشن). طی عمل مینی مایزیشن سوژه با احتمال بیشتر به گروه برگزیده افزوده میشود ولی احتمال کمی هم وجود دارد که سوژه به گروه دیگری وارد شود. بنابراین بسته به میزان احتمال پایه انتخاب شده برای کارآزائی، نتیجه نهائی بالانس گروه ها با رعایت اصل تصادفی سازی خواهد بود. اگر سوژه به گروهی برود که سبب افزایش تعادل میشود به اصطلاح می گوئیم سوژه به گروه برگزیده وارد شده است در غیر اینصورت به گروه غیر برگزیده وارد شده است که برنامه با رنگ قرمز نشان میدهد. چنانچه بر روی نتیجه که یا قرمز یا آبی است کلیک کنید، تفصیل پروسه مینی مایزیشن و میزان احتمال مربوطه را می توانید مشاهده کنید. برای ویرایش سوژه ای که قبلا وارد شده است بر روی آن دبل کلیک کنید. برای حذف سوژه بر روی دکمه پاک کردن در نوار بالای صفحه کلیک کنید.\nنواری در بالای جدول سوژه ها میزان پیشرفت کار را بصورت تعداد سوژه های وارد مطالعه شده نسبت به تعداد شناسه های باقیمانده نشان میدهد. + + + + + In this table you can see four different scale related to overall trial balance (marginal balance, range, variance, SD) and randomness. Also the balances for each factor or for each level within each factor are displayed. The last two rows of the table show the mean and max values for each scale. The lower the numeric values of these balance, the more balance we have in our trial. Also a scale of randomness for enrolling subjects is calculated based on run test. + در این جدول چهار سنجه مختلف در باره تراز کارآزمائی دیده میشود: تراز مرزی، دامنه، واریانس و انحراف از معیار. همچنین برای هر فاکتور و برای هر لایه از آن فاکتور مقادیر تراز بصورت جداگانه نگاشته شده است. دو ردیف آخر نشان دهنده میانگین و بیشینه هر ستون می باشد. هر چه مقدار تراز عدد کوچکتری باشد، تراز کارآزمائی بیشتر خواهد بود. به عبارت دیگر تفاوت گروه ها از نظر فاکتور های مختلف کمتر خواهد بود. همچنین سنجه ای از کفایت تصادفی بودن تقسیم بندی سوژه ها بین گروه ها نمایش داده شده است که بر اساس آزمون run می باشد. + + + diff --git a/factor_levels.py b/factor_levels.py new file mode 100755 index 0000000..805c888 --- /dev/null +++ b/factor_levels.py @@ -0,0 +1,98 @@ +from PySide2 import QtWidgets as qtw +from PySide2 import QtCore as qtc + +from db import Database +from factor_levels_dialog import * + + +# noinspection PyTypeChecker +class FactorLevels(qtw.QDialog): + def __init__(self, parent, trial_id, factor_id, factor_title): + super(FactorLevels, self).__init__(parent) + self.parent = parent + self.ui = Ui_FactorLevelsDialog() + self.ui.setupUi(self) + self.database = Database.get_instance(parent) + self.trial_id = trial_id + self.factor_id = factor_id + self.setWindowTitle(self.tr('Levels of {}').format(factor_title)) + self.ui.addLevelPushButton.clicked.connect(self.add_level) + self.ui.deleteLevelPushButton.clicked.connect(self.delete_level) + self.ui.closePushButton.clicked.connect(self.close) + self.ui.levelsTableWidget.cellChanged.connect(self.level_cell_changed) + self.ui.levelsTableWidget.setSelectionBehavior(qtw.QAbstractItemView.SelectRows) + self.ui.levelsTableWidget.setSelectionMode(qtw.QAbstractItemView.SingleSelection) + self.load_levels() + + def add_level(self): + if self.parent.check_trial_subjects_or_preload(): + return + title, ok = qtw.QInputDialog.getText( + self, + self.tr('Level title'), + self.tr('Title'), + qtw.QLineEdit.EchoMode.Normal + ) + if ok and len(title.strip()) != 0: + if self.database.treatment_title_exists(title, self.factor_id): + qtw.QMessageBox.critical(self, + self.tr('Error!'), + self.tr('''Level title '{}' already exist!'''.format(title)) + ) + return + level_id = self.database.insert_level(self.trial_id, self.factor_id, title) + state = self.ui.levelsTableWidget.blockSignals(True) + r = self.ui.levelsTableWidget.rowCount() + self.ui.levelsTableWidget.insertRow(r) + id_item = qtw.QTableWidgetItem(str(level_id)) + id_item.setTextAlignment(qtc.Qt.AlignCenter) + self.ui.levelsTableWidget.setCellWidget(r, 0, qtw.QLabel()) + self.ui.levelsTableWidget.setItem(r, 0, id_item) + title_item = qtw.QTableWidgetItem(title) + self.ui.levelsTableWidget.setItem(r, 1, title_item) + self.ui.levelsTableWidget.blockSignals(state) + + def delete_level(self): + if self.parent.check_trial_subjects_or_preload(): + return + selected = self.ui.levelsTableWidget.selectedIndexes() + if not selected: + return + title = self.ui.levelsTableWidget.item(selected[0].row(), 1).text() + button = qtw.QMessageBox.question(self, self.tr("Confirm delete"), + self.tr('Are you sure you want to delete "%s"?') % title, + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + return + level_id = int(self.ui.levelsTableWidget.item(selected[0].row(), 0).text()) + self.database.delete_level(level_id) + self.ui.levelsTableWidget.blockSignals(True) + self.ui.levelsTableWidget.removeRow(selected[0].row()) + self.ui.levelsTableWidget.blockSignals(False) + + def level_cell_changed(self, row, col): + level_id = int(self.ui.levelsTableWidget.item(row, 0).text()) + title = self.ui.levelsTableWidget.item(row, col).text() + oldValue = self.database.update_level(level_id, title) + if oldValue is not None: + self.parent.abort_table_widget_change(self.ui.levelsTableWidget, row, col, oldValue) + + def load_levels(self): + self.ui.levelsTableWidget.blockSignals(True) + self.ui.levelsTableWidget.setRowCount(0) + self.ui.levelsTableWidget.setColumnCount(2) + header = self.ui.levelsTableWidget.horizontalHeader() + header.setSectionResizeMode(0, qtw.QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, qtw.QHeaderView.Stretch) + self.ui.levelsTableWidget.setHorizontalHeaderLabels([self.tr('ID'), self.tr('Level')]) + query = self.database.load_levels(self.factor_id) + while query.next(): + r = self.ui.levelsTableWidget.rowCount() + self.ui.levelsTableWidget.insertRow(r) + id_item = qtw.QTableWidgetItem(str(query.value('id'))) + id_item.setTextAlignment(qtc.Qt.AlignCenter) + self.ui.levelsTableWidget.setCellWidget(r, 0, qtw.QLabel()) + self.ui.levelsTableWidget.setItem(r, 0, id_item) + title_item = qtw.QTableWidgetItem(query.value('title')) + self.ui.levelsTableWidget.setItem(r, 1, title_item) + self.ui.levelsTableWidget.blockSignals(False) diff --git a/factor_levels_dialog.py b/factor_levels_dialog.py new file mode 100755 index 0000000..1981f8e --- /dev/null +++ b/factor_levels_dialog.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'factor_levels_dialog.ui' +## +## Created by: Qt User Interface Compiler version 5.14.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject, + QObject, QPoint, QRect, QSize, QTime, QUrl, Qt) +from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, + QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter, + QPixmap, QRadialGradient) +from PySide2.QtWidgets import * + + +class Ui_FactorLevelsDialog(object): + def setupUi(self, FactorLevelsDialog): + if not FactorLevelsDialog.objectName(): + FactorLevelsDialog.setObjectName(u"FactorLevelsDialog") + FactorLevelsDialog.resize(358, 300) + self.verticalLayout = QVBoxLayout(FactorLevelsDialog) + self.verticalLayout.setObjectName(u"verticalLayout") + self.gridLayout = QGridLayout() + self.gridLayout.setObjectName(u"gridLayout") + self.levelsTableWidget = QTableWidget(FactorLevelsDialog) + self.levelsTableWidget.setObjectName(u"levelsTableWidget") + + self.gridLayout.addWidget(self.levelsTableWidget, 1, 0, 1, 3) + + self.closePushButton = QPushButton(FactorLevelsDialog) + self.closePushButton.setObjectName(u"closePushButton") + + self.gridLayout.addWidget(self.closePushButton, 0, 2, 1, 1) + + self.deleteLevelPushButton = QPushButton(FactorLevelsDialog) + self.deleteLevelPushButton.setObjectName(u"deleteLevelPushButton") + + self.gridLayout.addWidget(self.deleteLevelPushButton, 0, 1, 1, 1) + + self.addLevelPushButton = QPushButton(FactorLevelsDialog) + self.addLevelPushButton.setObjectName(u"addLevelPushButton") + + self.gridLayout.addWidget(self.addLevelPushButton, 0, 0, 1, 1) + + + self.verticalLayout.addLayout(self.gridLayout) + + + self.retranslateUi(FactorLevelsDialog) + + QMetaObject.connectSlotsByName(FactorLevelsDialog) + # setupUi + + def retranslateUi(self, FactorLevelsDialog): + FactorLevelsDialog.setWindowTitle(QCoreApplication.translate("FactorLevelsDialog", u"Dialog", None)) + self.closePushButton.setText(QCoreApplication.translate("FactorLevelsDialog", u"Close", None)) + self.deleteLevelPushButton.setText(QCoreApplication.translate("FactorLevelsDialog", u"Delete", None)) + self.addLevelPushButton.setText(QCoreApplication.translate("FactorLevelsDialog", u"Add", None)) + # retranslateUi + diff --git a/factor_levels_dialog.ui b/factor_levels_dialog.ui new file mode 100755 index 0000000..534b096 --- /dev/null +++ b/factor_levels_dialog.ui @@ -0,0 +1,49 @@ + + + FactorLevelsDialog + + + + 0 + 0 + 358 + 300 + + + + Dialog + + + + + + + + + + + Close + + + + + + + Delete + + + + + + + Add + + + + + + + + + + diff --git a/freq_table.py b/freq_table.py new file mode 100755 index 0000000..ebd20c9 --- /dev/null +++ b/freq_table.py @@ -0,0 +1,227 @@ +from db import Database +from PySide2 import QtWidgets as qtw +from PySide2 import QtCore as qtc +from PySide2 import QtGui as qtg + + +class FreqTable: + def __init__(self, parent, ui, trial_id): + self.parent = parent + self.num_treatments = None + self.num_levels = None + self.ui = ui + self.grid = ui.freqGridLayout + self.trial_id = trial_id + self.database = Database.get_instance(parent) + + def on_save_preload(self): + can_save = True + if self.ui.frequenciesCheckBox.isChecked(): + can_save = False + if not self.ui.preloadCheckBox.isChecked(): + can_save = False + if not self.ui.editPreloadCheckBox.isChecked(): + can_save = False + if not can_save: + qtw.QMessageBox.information( + self.parent, self.parent.tr('Save not available!'), + self.parent.tr('Select only preload and edit') + ) + return + preload = self.extract_counts() + if self.valid_counts(preload): + pixmap = qtg.QPixmap("images/tick_mark.png") + self.database.clear_preload(self.trial_id) + self.database.save_preload(self.trial_id, preload) + else: + pixmap = qtg.QPixmap("images/error.png") + self.valid_preload_image.setPixmap(pixmap) + + def add_to_preload(self): + preload = self.database.get_preload(self.trial_id) + freq = self.extract_counts() + for key in freq: + freq[key] += preload[key] + self.database.clear_preload(self.trial_id) + self.database.save_preload(self.trial_id, freq) + + def extract_counts(self): + count = {} + for r in range(2, self.num_treatments + 2): + for c in range(1, self.num_levels + 1): + item = self.grid.itemAtPosition(r, c) + entry = item.widget() + key = self.left_top_key[(r, c)] + count[key] = int(entry.text().strip()) + return count + + def valid_counts(self, count): + treatments = self.database.read_treatments(self.trial_id) + factors = self.database.read_factors(self.trial_id) + tfl = [] + f_total = [] + for factor in factors: + levels = self.database.factor_levels(factor[0]) + fl = [[] for i in range(len(levels))] + f_total.append(fl) + valid = True + for treatment in treatments: + s = set() + row = [] + for i, factor in enumerate(factors): + t, f = treatment[0], factor[0] + levels = self.database.factor_levels(f) + lst = [] + for j, level in enumerate(levels): + lv = level[0] + cnt = count[(t, f, lv)] + f_total[i][j].append(cnt) + lst.append(cnt) + row.append(lst) + s.add(sum(lst)) + if len(s) > 1: + valid = False + tfl.append(row) + gt = 0 + for i in range(len(self.total_treatment_buttons)): + sm = sum(tfl[i][0]) + self.total_treatment_buttons[i].setText(str(sm)) + gt += sm + self.grand_total_button.setText(str(gt)) + for i, f_t in enumerate(f_total): + s = [] + for j, f_l in enumerate(f_t): + sm = sum(f_l) + self.total_level_button[i][j].setText(str(sm)) + s.append(sm) + self.total_factorButtons[i].setText(str(sum(s))) + return valid + + def on_clear_preload(self): + err = False + if self.ui.frequenciesCheckBox.isChecked(): + err = True + if not self.ui.preloadCheckBox.isChecked(): + err = True + if not self.ui.editPreloadCheckBox.isChecked(): + err = True + if err: + qtw.QMessageBox.warning(self.parent, self.parent.tr('Clear preload'), + self.parent.tr('To clear preload, select preload and check edit')) + return + button = qtw.QMessageBox.question(self.parent, self.parent.tr("Confirm clear"), + self.parent.tr('Are you sure you want to clear preload'), + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + return + for r in range(2, self.num_treatments + 2): + for c in range(1, self.num_levels + 1): + item = self.grid.itemAtPosition(r, c) + item.widget().setText('0') + self.database.clear_preload(self.trial_id) + + def toggleReadOnly(self): + readOnly = not self.ui.editPreloadCheckBox.isChecked() + if self.num_treatments is None or self.num_levels is None: + return + for r in range(2, self.num_treatments + 2): + for c in range(1, self.num_levels + 1): + item = self.grid.itemAtPosition(r, c) + item.widget().setReadOnly(readOnly) + style = self.entryCountReadOnly if readOnly else self.entryCount + item.widget().setStyleSheet(style) + + def set_counts(self, cur_count): + if self.num_treatments is None or self.num_levels is None: + return + for r in range(2, self.num_treatments + 2): + for c in range(1, self.num_levels + 1): + item = self.grid.itemAtPosition(r, c) + key = self.left_top_key[(r, c)] + item.widget().setText(str(cur_count[key])) + self.valid_counts(self.extract_counts()) + + def build(self): + for i in reversed(range(self.grid.count())): + self.grid.itemAt(i).widget().setParent(None) + factors = self.database.read_factors(self.trial_id) + if len(factors) < 1: + return + treatments = self.database.read_treatments(self.trial_id) + self.num_treatments = len(treatments) + if len(treatments) < 2: + return + flc = self.database.get_factor_level(self.trial_id) + for lvs in flc: + if len(lvs) < 2: + return + self.left_top_key = {} + offset = 1 + self.total_factorButtons = [] + self.total_level_button = [] + fixedStyle = 'background-color: #EEEEEE; color: #000000; border: 1px solid #000000;' + self.entryCountReadOnly = 'color: #999999; border: 1px solid #000000;' + self.entryCount = 'color: blue; border: 1px solid #000000;' + for i in range(len(factors)): + factor = factors[i] + factor_button = qtw.QLabel(factor[1]) + factor_button.setStyleSheet(fixedStyle) + factor_button.setAlignment(qtc.Qt.AlignCenter) + if i > 0: + offset += len(flc[i - 1]) + self.grid.addWidget(factor_button, 0, offset, 1, len(flc[i])) + total_factorButton = qtw.QLabel('0') + total_factorButton.setAlignment(qtc.Qt.AlignCenter) + self.total_factorButtons.append(total_factorButton) + total_factorButton.setStyleSheet(fixedStyle) + self.grid.addWidget(total_factorButton, len(treatments) + 3, offset, 1, len(flc[i])) + level_button_list = [] + for j in range(len(flc[i])): + level = flc[i][j] + level_button = qtw.QLabel(level[1]) + level_button.setAlignment(qtc.Qt.AlignCenter) + level_button.setStyleSheet(fixedStyle) + self.grid.addWidget(level_button, 1, offset + j, 1, 1) + total_level_button = qtw.QLabel('0') + total_level_button.setAlignment(qtc.Qt.AlignCenter) + level_button_list.append(total_level_button) + total_level_button.setStyleSheet(fixedStyle) + self.grid.addWidget(total_level_button, len(treatments) + 2, offset + j, 1, 1) + for x in range(len(treatments)): + treatment = treatments[x] + cell_entry = qtw.QLineEdit() + cell_entry.setMinimumHeight(50) + cell_entry.setStyleSheet(self.entryCountReadOnly) + cell_entry.setText('0') + cell_entry.setAlignment(qtc.Qt.AlignCenter) + cell_entry.setReadOnly(True) + self.grid.addWidget(cell_entry, x + 2, offset + j, 1, 1) + self.left_top_key[(x + 2, offset + j)] = (treatment[0], factor[0], level[0]) + self.total_level_button.append(level_button_list) + total_button = qtw.QLabel(self.parent.tr('Total')) + total_button.setAlignment(qtc.Qt.AlignCenter) + total_button.setStyleSheet(fixedStyle) + offset += len(flc[-1]) + self.num_levels = offset - 1 + self.grid.addWidget(total_button, 0, offset + 2, 2, 1) + self.total_treatment_buttons = [] + for i in range(len(treatments)): + treatment = treatments[i] + treatment_button = qtw.QLabel(treatment[1]) + treatment_button.setStyleSheet(fixedStyle) + self.grid.addWidget(treatment_button, i + 2, 0, 1, 1) + total_treatment_button = qtw.QLabel('0') + total_treatment_button.setAlignment(qtc.Qt.AlignCenter) + self.total_treatment_buttons.append(total_treatment_button) + total_treatment_button.setStyleSheet(fixedStyle) + total_treatment_button.setMinimumWidth(100) + self.grid.addWidget(total_treatment_button, i + 2, offset + 2, 1, 1) + total_button = qtw.QLabel(self.parent.tr('Total')) + total_button.setStyleSheet(fixedStyle) + self.grid.addWidget(total_button, len(treatments) + 2, 0, 2, 1) + self.grand_total_button = qtw.QLabel('0') + self.grand_total_button.setAlignment(qtc.Qt.AlignCenter) + self.grand_total_button.setStyleSheet(fixedStyle) + self.grid.addWidget(self.grand_total_button, len(treatments) + 2, offset + 2, 2, 1) + self.valid_preload_image = qtw.QLabel() + self.grid.addWidget(self.valid_preload_image, 0, 0, 2, 1) diff --git a/images/about.png b/images/about.png new file mode 100755 index 0000000000000000000000000000000000000000..cbadad97f4d2e8be6a7d418336c94db914df5b12 GIT binary patch literal 1598 zcmV-E2EqA>P)U2^%An#7!|u zsjwn?n7yFPLi}KnWnn2Hl^;yXiWJQcT0WvqqGcf(i=_0y2D4aVlUQU~hULWW z-~(Wvt6zo_1t=-z0xN;3F50Vsrh)@Bvalme6#6M^->jAq(tf}_CmVWbZ>+CW1J zP*My6UI$(P+8Y(_hzhU~SQbY5T)hw1tpFv(IN%G+s@_Wc4lE2K{cAlB1@%-`Qp^W_ zqV)z3W~)7 zOXb2fKuPf&W~Cp9EDt07c3y>C1t=*N0NWTCZ$6C*@NgLEEm;*Z6`-V;02~5_c<5M? zKFkTP6SJjG1@6S1())lr!bty;R+y>)CB+cnXJDM8PGs>d@Msw6Ggb0$2fhTZbRc*^_LeZz}oKXtH1X|>YS|Ndx!D@V1tsPz52@}_XATL3eBUxhB~fa#5iDa zssQ`;S2`A$$K)F7%=94l)#R@rso^>JU-*)&u~ua&*63QCRo(B1MC9+!Mu?17(b%Vg}4G-A4Xcm1D?<2 zb8HL+9w=F604xCBjkVlSatB(0c>ry8Mf{i6w)1_Uf#L0#mA}lvciB%bW|6zDWce`y zn9>eR^em_!IDj#3z6SnIy0VoN0<#6L#VDgpCbR=%JvCkdRsoyR44#-U(i+&Qq&Nud z$NvjSGA6)SN;-0d=uaAl+kp|D8h^-slFU(3T#@FONN~zif6^EhU{=Gj7)0@u^BNAXLIjlW@GP3+ z!%FFeAqXgU(a{+HTpl--(2tnD0LYt+%>5? z08W=EaDPGh>{5DhfzttSq9kFxKv5z?S^Iy?i2$f8p56op3+DgTlso?cI%5T7`sW?; zO+k6tZ@(Wn8ra~(k0l7)S5RK|{688-x+gva?e;9}3?}(873-3hp8HVE9=LD{+=1UC z=ywI_Y~yJ>nX*x~gH=EnX@zlG_~KAO`E-x6)94H%-3b@l0U!AkSWte8N7-qdA8S$p zJ1}$A2Squ)zD)8o&H|sr|D0EVbxpnLNobM5)#FKS8t)|&DyvQ5P55z06nT9&U=kfG znEkm6*owOZiI^^6UGj^I6kyK;FFFZcs1Kq%rz|OQYfjtaO1|9?!IhK7E zxHZi~otGvz3v)tr9W*_O$qxGyV}lF^t_AMG#O_^F0p^8~-j!BZlqmT!CSza4L6P9|Yu{jI_Q zOfqy`gNFvgV)j7MfjPAJvLI`r3$s4W3?to;4b$|+NgK_18dyeo)1n)b)4Z;ct8z)4 zU7fXxSd3XLeYv-P!(5%+-0Z+(d|jTN0^A48rTO&RlbA%xZCSTIn!$?#l32rP8s@Iy w7)*Y^NG^>5YRpgjG{!e@3^-D2icO>Z4=d55EU98Jt^fc407*qoM6N<$f)KRYJ^%m! literal 0 HcmV?d00001 diff --git a/images/add.png b/images/add.png new file mode 100755 index 0000000000000000000000000000000000000000..90f7ed93a65b5243e1e3f958995584c66fde37b1 GIT binary patch literal 1963 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 literal 0 HcmV?d00001 diff --git a/images/config.png b/images/config.png new file mode 100755 index 0000000000000000000000000000000000000000..a87e1fe9d477f7410984a5d47c0856f5c69eff19 GIT binary patch literal 2122 zcmV-Q2(|Z#P)EX>4Tx04R}tkv&MmKpe$iQ?(*34rUN>$j~}j6cusQDionYs1;guFdzMbCJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#IXWr2NQwVT3N2zhIPS;0dyl(!0N1D}n$ zITlcb2Fdk<{lV{Ut-|DlmlRF_-7k*w5d%WIK(p>R-^Y&AJOP5wz?I(iR~x|0C+YRJ z7Ci#`w}Ff6wkGcZmpj1VlOdUsD+OtCg#z$?M&FbJ25y1AHLthUK29Hi40V;d0S*p< zks@WU`@Fliv$ucGwEFu2om6tTSIzHX00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00tsSL_t(&-qo60a1>P<$A8^DGd;sNGaqLMr63nc&>|WP zh#D|Vl5vtsCYgJ>dmoah12ai?>|vRIRljtf%lytc-#P!Urv?6f9(Mg5_WhGIJ+obA zVa;mn&OaiGix4Frf>4Nt6Eq7Tj9+F-8@q&21R`kK0IGTkMQJALzcd}h zS1xh=ybrO;mE?=8awXn*AF=*T=~s|H)NjDytR>&%a4w^|{wV+n-+Y!s*CL`FZX0EySJmChdT!|K6`Jzi^r==v%)l8409?NvN8=ecz)%eG4+_*?qS*?!ACvMvaK-RB0UEVHw7Q0H=i zxdn3Wf_<_PYzW1tEVpMk#gNGk*i;aJn9iYAP2Rpu1rf$~r>XQOhlQDT%hdRG>@b>uG0k@cE11>l0f_Y<;<{Vo=_ENx)A1_&O!E$qyV_z zA@TIh1>Aju2T91h1tt`Y`U71wCk-Bm)3NarYQ4kDlG)cyS#BtyplMK`BVp2W2AO8# z0iG+ibK5L^8L~%~bU^^z;&t+p*Ku`rtvZ{?^M3CYnVlJu1lUyL<-Td8yM{rwFDYTB z!$Pj)ktMeIOLGt6l5+}aQX)q&6g*RGySahP7o>!?u+rzk)P-YM)@h6 zNknl@lT9Swk7VP?BQMWgM~2y-v{TcO8*cQO_sO2C4uo(7hi-S(8}si`39X_0v3 zDEY2fmiZawFzR8SMzu_USr3`#F$0-^0NkFk;XiPXynvq z4ZnJ=OI-?m7at^*_%4r@)OYkS zW5!kb@GOd^^`j~Q6t$g1^ay9ZILMAo89zMxAAomFrnQGh2><{907*qoM6N<$f{Xdt AuK)l5 literal 0 HcmV?d00001 diff --git a/images/delete.png b/images/delete.png new file mode 100755 index 0000000000000000000000000000000000000000..6153a232d5201678e502a1c577e31d2da49e4e16 GIT binary patch literal 7891 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 literal 0 HcmV?d00001 diff --git a/images/error.png b/images/error.png new file mode 100755 index 0000000000000000000000000000000000000000..5bce5ccca050d7eaa94b4c94e405c77684418e10 GIT binary patch literal 1405 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 literal 0 HcmV?d00001 diff --git a/images/help.png b/images/help.png new file mode 100755 index 0000000000000000000000000000000000000000..8c531131b4e3b93cf0257adc34ccb520bd8cfc00 GIT binary patch literal 13025 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%0A<*G~&#Bc2IIS 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 X;?$sbdgmRe00000NkvXXu0mjfU~d9! literal 0 HcmV?d00001 diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b7e952c549ad9ee12ac6638318757381d7999ac6 GIT binary patch literal 22636 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 literal 0 HcmV?d00001 diff --git a/images/save.png b/images/save.png new file mode 100755 index 0000000000000000000000000000000000000000..eb24158f4855a8e537a496f3c7a6e39c5e074be7 GIT binary patch literal 12191 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 m#?JO=3F+j8ajp6Pm+gNSt=CS1PhOw^0000 + + + + ConfigDialog + + + MinimPy2 Config + + + + + Language + + + + + Random engin + + + + + Save + + + + + Database + + + Error + + + + + DB Connection Error + + + + + DB Integrity Error + + + + + DB read Error + + + + + DB Error + + + + + Could not open database file {} + + + + + Missing tables, please repair DB {} + + + + + EnrolForm + + + Editing subject "{}" + + + + + Treatment + + + + + Select factors, click enrol + + + + + Enrol + + + + + Close + + + + + Save + + + + + Cancel + + + + + Select {} level + + + + + Subject "{}", Enrolled to {} + + + + + Subject enrol + + + + + preferred + + + + + non-preferred + + + + + Preferred treatment = {} + + + + + + Minimised treatment = {} + + + + + + Subject assigned to {} treatment with a probability of {:4.2f} + + + + + Minimisation detail + + + + + FactorLevels + + + Level title + + + + + Title + + + + + Error! + + + + + Level title '{}' already exist! + + + + + Confirm delete + + + + + Are you sure you want to delete "%s"? + + + + + ID + + + + + Level + + + + + Levels of {} + + + + + FactorLevelsDialog + + + Dialog + + + + + Close + + + + + Delete + + + + + Add + + + + + MainWindow + + + Start + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:20pt; font-weight:600;">How to do minimisation using MinimPy2?</span></p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt; font-weight:600;">A quick tutorial</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">1 - Go to Trials tab and select a trial as current to work on it. Define a new trial if there is no trial</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">2 - Go to Setting tab to view / edit trial setting. Usually the default setting is good to go</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">3 - Go to Treatments tab to define at lease two treatments</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">4 - Go to Factors tab to define at lease one factor</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">5 - Define at least two levels for each factor</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">6 - Now the trial is ready for subject enrollment</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">7 - Go to subject tab and click on the Add button in the toolbar</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">8 - The enroll window will appear. Select a level for each factor and click enroll</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">9 - Subject will enroll to one of treatment groups by the minimisation algorhythm</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">10 - If you want to define a preload go to Frequencies tab</p></body></html> + + + + + Current trial exported successfully to "{}" + + + + + Error in languages.lst file format\nPlease see the READ.ME file in locales folder + + + + + Close + + + + + What is Minimisation + + + + + About + + + + + Factor/Level + + + + + SD + + + + + Enrolled + + + + + Treatment + + + + + Save not available! + + + + + Clear preload + + + + + To clear preload, select preload and check edit + + + + + Total + + + + + Error + + + + + Sample size consumed! +No further subject enrol possible! +Please convert cases to preload and continue + + + + + Filter + + + + + Double check! + + + + + Need your final confirm + + + + + Deleting preload + + + + + Are you certainly sure you want to convert all subjects into preload? +ALL subjects will be deleted + + + + + Help! + + + + + minimpy2 [{}] + + + + + Factor title + + + + + Title + + + + + Error! + + + + + Factor title '{}' already exist! + + + + + Treatment title + + + + + Treatment title '{}' already exist! + + + + + Trial title + + + + + A trial title '{}' already exist! + + + + + Confirm delete + + + + + Are you sure you want to delete subject "{}"? + + + + + New factor not allowed! + + + + + Trial already has subject or preload. + You can not add or delete factors, treatments or levels + + + + + Are you sure you want to delete "%s"? + + + + + minimpy2 + + + + + version + + + + + 2.0 + + + + + Copyright + + + + + 2020 + + + + + Dr. Mahmoud Saghaei + + + + + MinimPy + + + + + ID + + + + + Ratio + + + + + Factor + + + + + Levels + + + + + Weight + + + + + Code + + + + + Current + + + + + Mean + + + + + Max + + + + + Balance (mean MB = {:.4f}) + + + + + No subject enrolled! + + + + + Select mnd file + + + + + Warning + + + + + Do you want to save this trial? + + + + + Imported + + + + + Data imported successfully! + + + + + Import error! + + + + + Data are not valid! + + + + + Export to file + + + + + New trial + + + + + Maximum sample size > {} + + + + + Are you sure you want to quit? + + + + + No current trial! + + + + + Fist you have to define and select a trial as current! + + + + + Are you sure you want to edit subject at row "{}" ? + +Editing subject may invalidate your research and the result of minimisation + + + + + MainWindow + + + + + Trials + + + + + Setting + + + + + Created + + + + + Modified + + + + + Trial code + + + + + Probability method + + + + + Biased coin + + + + + Naive + + + + + Base probability + + + + + 0.70 + + + + + Distance method + + + + + Marginal balance + + + + + Range + + + + + Standard deviation + + + + + Variance + + + + + Identifier type + + + + + Numeric + + + + + Alpha + + + + + Alphanumeric + + + + + Identifier order + + + + + Sequential + + + + + Random + + + + + Identifier length + + + + + TextLabel + + + + + Recycle ids + + + + + New subject random + + + + + Arms weight + + + + + 1.0 + + + + + Treatments + + + + + Factors + + + + + Frequencies + + + + + Preload + + + + + Edit + + + + + Convert to preload + + + + + I know what I'm doing + + + + + Subjects + + + + + Balance + + + + + Oveall Balance + + + + + Randomness + + + + + File + + + + + toolBar + + + + + Exit + + + + + Add + + + + + Delete + + + + + Save + + + + + manage factor levels + + + + + Help + + + + + displays help + + + + + Import + + + + + Export + + + + + Config + + + + + Application config + + + + + Restart needed + + + + + Changes will be applied after restart! + + + + + Each trial has a uniqe tile and also a code usually in the form of an abreviation of the title. +Last column show if the trial is the active one. Only one trial can be active at any time. Other pages of the application relate to this active trial. To make a treial active check the box in last column. +Click and then type over the trial title or code to edit it + + + + + Creation date of this trial + + + + + + Modification date of this trial + + + + + + Title of this trial. Title must be unique + + + + + + Code of the trial usually title abbreviation + + + + + + The method used for calculating of the assignment probabilities. There are two choices. Biased coin minimization which include the effect of allocation ratios into the computation, and naive method which do not include the effect of allocation ratios. Biased coin is the preffered method. These options only is meaningfull when we have treatments with unequal allocation ratios. Biased Coin method is based on the different probabilities of assignment for subjects to trial groups. The subject is allocated to the preferred treatment with a higher probability and to other groups with lower probabilities. Therefore if treatments with higher allocations ratios are assigned more probabilities this will be considered as biased-coin minimization. In this method a base probability is used for the group with the lowest allocation ratio when that group is selected as the preferred treatment. Probabilities for other groups (PHi) when they selected as preferred treatments are calculated as a function of base probability and their allocation ratios. In Naive method the subject is allocated to the preferred treatment with a higher probability and to other groups with lower probabilities. Here the probabilities are not affected by allocation ratios and usually the same high probability are used for all treatment groups when they are selected as preferred treatments (base probability). This is denoted by Naive Minimization. In this method, probabilities for non-preferred treatments are distributed equally. + + + + + + The probability given to the treatment with the lowest allocation ratio in biased-coin minimization. In naive minimization it is the probability used for all treatment groups when they are selected as preferred treatments. Use the spin button to specify the value. + + + + + + Distance measure is used to calculate the imbalance score. There are four options here. Marginal balance computes the cumulative difference between every possible pairs of level counts. This is the default and the one which is recommended. It tends to minimize more accurately when treatment groups are not equal in size. For each factor level marginal balance is calculated as a function of adjusted number of patients present in a factor level for each treatment group. Range calculates the difference between the maximum and the mimimum values in level counts. Standard deviation calculates the standard deviation of level counts and variance calculate their variances. All of these measures uses adjusted level counts. Adjustment performs relative to the values of allocation ratios. + + + + + + Type of subject identifier. It can be Numeric, Alpha or Alphanumeric.\nOnce a subject is enrolled, you can not change identifier type + + + + + + The order of identifier sequence. It can be Sequential or Random. + + + + + + Length of subjects identifier in character. The longer the length the higher will be the possible sample size + + + + + + Check to reuse IDs of a deleted subject. The default is to discard + + + + + + If unchecked (the default), enrol form will show with the level for each factor unselected. If checked, for each factor a random level will be selected. This is only for test purpose to rapidly and randomly enrol a subject + + + + + + This is equalizing factor for arms of trial. The higher the arms weight the higher will be the possibility of having equal group sizes at the end of study + + + + + + For each treatment a name (unique for the trial) and an integer value for allocation ratio are needed. Allocation ratios are integer numeric values which denote the ratios of the treatment counts. For example if we have three groups a, b and c with their allocation ratios 1, 2 and 3 respectively, this mean that in the final sample nearly 1/6 of subjects will be from treatment a, 1/3 from b and 1/2 from c +You have to define at lease two groups to use minimisation on them. +Click on treatment title and type over it to edit the title of the treatment, and use the spin control to change the allocation ratios + + + + + Factors or prognostic factors, are the subject factors which are selected to match subjects based on them. At lease one factor is mandatory for minimisation. Each factor has a name (unique for the trial) and a weight. Weight indicates the relative importance of the factor for minimization. For each factor you must define at least two levels. As an example factor may be gender and its levels may be Male and Female. +Factor title can be edited by clicking and typing over the title of the factor + +Click on the levels button on tool bar or double click on levels in table to manage (add, edit, delete) levels of the selected factor + + + + + Manage (add, edit, delete) levels of the selected factor + + + + + This table shows frequencies of subjects enrolled in the trial or entered via preload table depending on the active checkboxes. When you have already enrolled a number of subjects (using other tools or software) and you want to continue with this minimisation program for the remaining cases, you can enter data of already enrolled subject as counts. You enter counts of different treatments accross all levels of each subject into the preload table. Then the counts from this table will be added to counts of enrolled subject by this application and total counts will be used in minimisation model. +It is as if you have entered the list of subject into your existing list. The effect is exactly the same. When defining preload please note that sum of subjects across different levels of a factor must be equal to thats of other factors, otherwise an error will be generated. +If you have a greal number of subjects and have no need to keep their list, you may convert them into preload table. Select frequencies check box and unselect preload checkbox, then click 'convert to preload' to clear all subjects and add them to preload table. + + + + + Subjects enrolled in the trial will be listed in subjects table. Each row belongs to one subject and shows subject ID, treatment, levels of factors, dates of enrollment and modification (if any). Subject can be added, edited or deleted from this table, although editing or deleting subject may compromise the minimisation model and therefore are not recommended.To enter a new subject, click plus sign in the tool bar. To edit double click and to delete click on Delete button on toolbar.\n +A progressbar above the subject table show the enrollment progress as the number of subject enrolled compared to the remaining counts of identifiers + + + + + In this table you can see four different scale related to overall trial balance (marginal balance, range, variance, SD) and randomness. Also the balances for each factor or for each level within each factor are displayed. The last two rows of the table show the mean and max values for each scale. The lower the numeric values of these balance, the more balance we have in our trial. Also a scale of randomness for enrolling subjects is calculated based on run test. + + + + diff --git a/locales/READ.ME b/locales/READ.ME new file mode 100755 index 0000000..a143f44 --- /dev/null +++ b/locales/READ.ME @@ -0,0 +1,11 @@ +Instruction to translate into another languages + +1 - Find your language code. Each language has a code consists of language code and contry code. Example: en_US for US English and fr_CA for Canadian French +2 - Copy 'locale_sources.ts' file and rename copied file into the language code. Example fr_CA.ts +3 - Open this file with Qt Linguist application (https://www.qt.io/download-qt-installer) +4 - In edit menu click 'Translation file settings...' and set the target language settings +5 - Translate all phrases using the application and save the changes +6 - From File menu select 'Release As...' option and save the compiled language file into 'locales' folder. The compiled language file will have qm extension. +7 - Open 'languages.lst' file in 'locales' folder and add and entry for your language at the end of the file. Example: fr_CA:French. Inplace of language name (French in this example) you may use the locale name (Française) +8 - Copy and translate the file 'minimisation_en_US.txt' and renamee it minimisation_.txt. In place of lang_code please use you language code. For example minimisation_fr_CA.txt +9 - That's it. Now you can change the MinimPy2 application language by clicking 'Config' button in the toolbar and selecting your language from the list. diff --git a/locales/fa_IR.qm b/locales/fa_IR.qm new file mode 100755 index 0000000000000000000000000000000000000000..4a10248d388223233f89497852c586589820f4bd GIT binary patch literal 41059 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 UL_%cB&(@EN3tIRQhT=T`7s-?A2><{9 literal 0 HcmV?d00001 diff --git a/locales/languages.lst b/locales/languages.lst new file mode 100755 index 0000000..700e932 --- /dev/null +++ b/locales/languages.lst @@ -0,0 +1,2 @@ +en_US:English +fa_IR:فارسی diff --git a/main_window.py b/main_window.py new file mode 100755 index 0000000..1fc852f --- /dev/null +++ b/main_window.py @@ -0,0 +1,1254 @@ +from PySide2 import QtWidgets as qtw +from PySide2 import QtCore as qtc +from PySide2 import QtGui as qtg + +from about_minimisation import AboutMinimisationDialog +from config import Config + +from db import Database +from enrol_form import EnrolForm +from factor_levels import FactorLevels +from freq_table import FreqTable +from mnd import * +from ui_main_window import Ui_MainWindow +from minim import Minim +from model import Model +from my_random import Random +from run_test import RunTest +from trial import * +import sys + + +# noinspection PyTypeChecker +class MainWindow(qtw.QMainWindow): + def __init__(self, settings): + super(MainWindow, self).__init__() + self.prev_tab_index = None + self.helpdict = { + ('tialsTableWidget',): self.tr("""Each trial has a uniqe tile and also a code usually in the form of an abreviation of the title.\nLast column show if the trial is the active one. Only one trial can be active at any time. Other pages of the application relate to this active trial. To make a treial active check the box in last column.\nClick and then type over the trial title or code to edit it"""), + ('trialSetting',): self.tr('''In this page you can view and edit different settings for the current trial. Click on blue lable for each setting item to view detailed help information for that setting'''), + ('trialCreatedLabel','trialCreatedValueLabel'): self.tr('''Creation date of this trial\n'''), + ('trialModifiedLabel','trialModifiedValueLabel'): self.tr('''Modification date of this trial\n'''), + ('trialTitleLabel','trialTitleLineEdit'): self.tr('''Title of this trial. Title must be unique\n'''), + ('trialCodeLabel','trialCodeLineEdit'): self.tr('''Code of the trial usually title abbreviation\n'''), + ('trialProbmethodLabel','trialProbMethodComboBox'): self.tr('''The method used for calculating of the assignment probabilities. There are two choices. Biased coin minimization which include the effect of allocation ratios into the computation, and naive method which do not include the effect of allocation ratios. Biased coin is the preffered method. These options only is meaningfull when we have treatments with unequal allocation ratios. Biased Coin method is based on the different probabilities of assignment for subjects to trial groups. The subject is allocated to the preferred treatment with a higher probability and to other groups with lower probabilities. Therefore if treatments with higher allocations ratios are assigned more probabilities this will be considered as biased-coin minimization. In this method a base probability is used for the group with the lowest allocation ratio when that group is selected as the preferred treatment. Probabilities for other groups (PHi) when they selected as preferred treatments are calculated as a function of base probability and their allocation ratios. In Naive method the subject is allocated to the preferred treatment with a higher probability and to other groups with lower probabilities. Here the probabilities are not affected by allocation ratios and usually the same high probability are used for all treatment groups when they are selected as preferred treatments (base probability). This is denoted by Naive Minimization. In this method, probabilities for non-preferred treatments are distributed equally.\n'''), + ('trialBaseProbabilityLabel','trialBaseProbabilitySlider','baseProbLabel'): self.tr('''The probability given to the treatment with the lowest allocation ratio in biased-coin minimization. In naive minimization it is the probability used for all treatment groups when they are selected as preferred treatments. Use the spin button to specify the value.\n'''), + ('trialDistanceMethodLabel','trialDistanceMethodComboBox'): self.tr('''Distance measure is used to calculate the imbalance score. There are four options here. Marginal balance computes the cumulative difference between every possible pairs of level counts. This is the default and the one which is recommended. It tends to minimize more accurately when treatment groups are not equal in size. For each factor level marginal balance is calculated as a function of adjusted number of patients present in a factor level for each treatment group. Range calculates the difference between the maximum and the mimimum values in level counts. Standard deviation calculates the standard deviation of level counts and variance calculate their variances. All of these measures uses adjusted level counts. Adjustment performs relative to the values of allocation ratios.\n'''), + ('trialIdentifierTypeLabel', 'trialIdentifierTypeComboBox'): self.tr('''Type of subject identifier. It can be Numeric, Alpha or Alphanumeric.\nOnce a subject is enrolled, you can not change identifier type'''), + ('trialIdentifierOrderLabel','trialIentifierOrderComboBox'): self.tr('''The order of identifier sequence. It can be Sequential or Random.\n'''), + ('trialIdentifierLengthLabel','max_sample_size_label'): self.tr('''Length of subjects identifier in character. The longer the length the higher will be the possible sample size\n'''), + ('trialRecycleIdsLabel','trialRecycleIdsCheckBox'): self.tr('''Check to reuse IDs of a deleted subject. The default is to discard\n'''), + ('trialNewSubjectRandomLabel','trialNewSubjectRandomCheckBox'): self.tr('''If unchecked (the default), enrol form will show with the level for each factor unselected. If checked, for each factor a random level will be selected. This is only for test purpose to rapidly and randomly enrol a subject\n'''), + ('trialArmsWeightLabel','trialArmsWeightDoubleSlider', 'armWeightLabel'): self.tr(''' This is equalizing factor for arms of trial. The higher the arms weight the higher will be the possibility of having equal group sizes at the end of study\n'''), + ('treatmentTableWidget','Treatments'): self.tr('''For each treatment a name (unique for the trial) and an integer value for allocation ratio are needed. Allocation ratios are integer numeric values which denote the ratios of the treatment counts. For example if we have three groups a, b and c with their allocation ratios 1, 2 and 3 respectively, this mean that in the final sample nearly 1/6 of subjects will be from treatment a, 1/3 from b and 1/2 from c\nYou have to define at lease two groups to use minimisation on them.\nClick on treatment title and type over it to edit the title of the treatment, and use the spin control to change the allocation ratios'''), + ('Factors','factorTableWidget'): self.tr('''Factors or prognostic factors, are the subject factors which are selected to match subjects based on them. At lease one factor is mandatory for minimisation. Each factor has a name (unique for the trial) and a weight. Weight indicates the relative importance of the factor for minimization. For each factor you must define at least two levels. As an example factor may be gender and its levels may be Male and Female.\nFactor title can be edited by clicking and typing over the title of the factor\n\nClick on the levels button on tool bar or double click on levels in table to manage (add, edit, delete) levels of the selected factor'''), + ('factor_levels',): self.tr('''Manage (add, edit, delete) levels of the selected factor'''), + ('Frequencies','freqGridLayout'): self.tr('''This table shows frequencies of subjects enrolled in the trial or entered via preload table depending on the active checkboxes. When you have already enrolled a number of subjects (using other tools or software) and you want to continue with this minimisation program for the remaining cases, you can enter data of already enrolled subject as counts. You enter counts of different treatments accross all levels of each subject into the preload table. Then the counts from this table will be added to counts of enrolled subject by this application and total counts will be used in minimisation model.\nIt is as if you have entered the list of subject into your existing list. The effect is exactly the same. When defining preload please note that sum of subjects across different levels of a factor must be equal to thats of other factors, otherwise an error will be generated.\nIf you have a greal number of subjects and have no need to keep their list, you may convert them into preload table. Select frequencies check box and unselect preload checkbox, then click 'convert to preload' to clear all subjects and add them to preload table.'''), + ('Subjects', 'subjectTableWidget'): self.tr('''Subjects enrolled in the trial will be listed in subjects table. Each row belongs to one subject and shows subject ID, treatment, levels of factors, dates of enrollment and modification (if any). Subject can be added, edited or deleted from this table, although editing or deleting subject may compromise the minimisation model and therefore are not recommended.To enter a new subject, click plus sign in the tool bar. To edit double click and to delete click on Delete button on toolbar\nA progressbar above the subject table show the enrollment progress as the number of subject enrolled compared to the remaining counts of identifiers'''), + ('Balance','balanceTreeWidget'): self.tr('''In this table you can see four different scale related to overall trial balance (marginal balance, range, variance, SD) and randomness. Also the balances for each factor or for each level within each factor are displayed. The last two rows of the table show the mean and max values for each scale. The lower the numeric values of these balance, the more balance we have in our trial. Also a scale of randomness for enrolling subjects is calculated based on run test.'''), + ('start', 'startWidget'): self.tr('''A quick list of steps to start a new minimization.''') + } + + self.settings = settings + + + engine = self.settings.value('random_engine', 'random', type=str) + self.random = Random(engine) + self.ui = Ui_MainWindow() + self.ui.setupUi(self) + self.ui.actionNew.setIcon(qtg.QIcon('images/add.png')) + self.ui.actionDelete.setIcon(qtg.QIcon('images/delete.png')) + self.ui.actionLevels.setIcon(qtg.QIcon('images/levels.png')) + self.ui.actionSave.setIcon(qtg.QIcon('images/save.png')) + self.ui.actionHelp.setIcon(qtg.QIcon('images/help.png')) + self.ui.actionQuit.setIcon(qtg.QIcon('images/exit.png')) + self.ui.actionConfig.setIcon(qtg.QIcon('images/config.png')) + self.ui.actionAbout_MinimPy2.setIcon(qtg.QIcon('images/about.png')) + self.database = Database.get_instance(self) + self.trial = None + + self.ui.tabWidget.currentChanged.connect(self.page_changed) + + self.ui.actionNew.triggered.connect(self.action_new) + self.ui.actionDelete.triggered.connect(self.action_delete) + self.ui.actionSave.triggered.connect(self.action_save) + self.ui.actionLevels.triggered.connect(self.factor_levels) + self.ui.actionQuit.triggered.connect(self.close) + self.ui.actionImport.triggered.connect(self.import_mnd_file) + self.ui.actionExport.triggered.connect(self.export_mnd_file) + self.ui.actionHelp.triggered.connect(self.action_help) + self.ui.actionConfig.triggered.connect(self.on_config) + self.ui.actionAbout_Minimisation.triggered.connect(self.on_about_minimisation) + self.ui.actionAbout_MinimPy2.triggered.connect(self.on_about_minimpy) + self.ui.actionAbout_Qt.triggered.connect(lambda : qtw.QMessageBox.aboutQt(self, '')) + + lst = [self.ui.trialCreatedLabel, self.ui.trialCreatedValueLabel, + self.ui.trialModifiedLabel, self.ui.trialModifiedValueLabel, + self.ui.trialTitleLabel, self.ui.trialCodeLabel, + self.ui.trialProbmethodLabel, self.ui.trialBaseProbabilityLabel, + self.ui.baseProbLabel, self.ui.trialDistanceMethodLabel, + self.ui.trialIdentifierTypeLabel, self.ui.trialIdentifierOrderLabel, + self.ui.trialIdentifierLengthLabel, self.ui.max_sample_size_label, + self.ui.trialRecycleIdsLabel, self.ui.trialNewSubjectRandomLabel, + self.ui.trialArmsWeightLabel, self.ui.armWeightLabel] + for w in lst: + self.set_help_handler(w) + + self.ui.tialsTableWidget.cellChanged.connect(self.trial_cell_changed) + self.ui.treatmentTableWidget.itemChanged.connect(self.treatment_item_changed) + self.ui.factorTableWidget.itemChanged.connect(self.factor_item_changed) + self.ui.factorTableWidget.cellClicked.connect(self.set_help_handler(self.ui.factorTableWidget)) + + self.ui.tialsTableWidget.setSelectionBehavior(qtw.QAbstractItemView.SelectRows) + self.ui.tialsTableWidget.setSelectionMode(qtw.QAbstractItemView.SingleSelection) + + self.ui.treatmentTableWidget.setSelectionBehavior(qtw.QAbstractItemView.SelectRows) + self.ui.treatmentTableWidget.setSelectionMode(qtw.QAbstractItemView.SingleSelection) + + self.ui.factorTableWidget.setSelectionBehavior(qtw.QAbstractItemView.SelectRows) + self.ui.factorTableWidget.setSelectionMode(qtw.QAbstractItemView.SingleSelection) + + self.ui.subjectTableWidget.setSelectionBehavior(qtw.QAbstractItemView.SelectRows) + self.ui.subjectTableWidget.setSelectionMode(qtw.QAbstractItemView.SingleSelection) + + self.ui.editPreloadCheckBox.toggled.connect(self.on_edit_preload) + self.ui.convertPreloadButton.clicked.connect(self.on_convert_preload) + self.ui.factorTableWidget.cellDoubleClicked.connect(self.factor_coloumn_dblclicked) + self.ui.subjectTableWidget.cellDoubleClicked.connect(self.subject_coloumn_dblclicked) + + self.ui.trialIdentifierLengthSpinBox.valueChanged.connect(self.on_identifier_length) + self.ui.trialBaseProbabilitySlider.valueChanged.connect(self.on_base_prob_change) + self.ui.trialArmsWeightDoubleSlider.valueChanged.connect(self.on_arm_weight_change) + + self.freqTable = None + + if self.settings.value('show_tutorial_at_start', True): + self.ui.tabWidget.setCurrentIndex(7) + self.ui.showAtStartCheckBox.setChecked(False) + else: + self.ui.tabWidget.setCurrentIndex(0) + self.ui.showAtStartCheckBox.setChecked(True) + self.ui.balanceTreeWidget.setColumnCount(5) + self.ui.balanceTreeWidget.setHeaderLabels([self.tr('Factor/Level'), self.tr('Marginal balance'), self.tr('Range'), self.tr('Variance'), self.tr('SD')]) + + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id != 0: + trial_setting = self.database.get_trial_setting(last_trial_id) + # when db file accidentally deleted but the last_trial_id persist + if not trial_setting: + trial_setting = self.database.get_first_trial_setting() + if not trial_setting: + last_trial_id = self.database.insert_trial(self.tr('New trial')) + self.settings.setValue('last_trial_id', last_trial_id) + trial_setting = self.database.get_trial_setting(last_trial_id) + else: + self.settings.setValue('last_trial_id', trial_setting.value('id')) + else: + trial_setting = self.database.get_first_trial_setting() + if not trial_setting: + last_trial_id = self.database.insert_trial(self.tr('New trial')) + self.settings.setValue('last_trial_id', last_trial_id) + trial_setting = self.database.get_trial_setting(last_trial_id) + else: + self.settings.setValue('last_trial_id', trial_setting.value('id')) + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id != 0: + trial_setting = self.database.get_trial_setting(last_trial_id) + if trial_setting: + self.trial = Trial(trial_setting) + #if self.database.has_preload(self.trial.id) or self.database.has_subject(self.trial.id): + self.ui.frequenciesCheckBox.toggled.connect(self.set_cur_count) + self.ui.preloadCheckBox.toggled.connect(self.set_cur_count) + self.load_trials() + self.ui.actionSave.setEnabled(False) + self.ui.actionLevels.setEnabled(False) + if self.settings.value('show_tutorial_at_start', True, bool): + self.ui.tabWidget.setCurrentIndex(7) + self.ui.showAtStartCheckBox.setChecked(False) + else: + self.ui.tabWidget.setCurrentIndex(0) + self.ui.showAtStartCheckBox.setChecked(True) + self.ui.showAtStartCheckBox.toggled.connect(self.toggle_show_at_start) + + def toggle_show_at_start(self, check): + self.settings.setValue('show_tutorial_at_start', not check) + + def on_about_minimpy(self): + about = '{}\n{} {} {} ({})\n{} {}\n{}'.format(self.tr('MinimPy'), + self.tr('MinimPy'), + self.tr('version'), + self.tr('2.0'), + self.tr('minimpy2'), + self.tr('Copyright'), + self.tr('2020'), + self.tr('Dr. Mahmoud Saghaei')) + qtw.QMessageBox.about(self, self.tr('minimpy2'), about) + + def on_about_minimisation(self): + ab_min = AboutMinimisationDialog(self) + ab_min.show() + + def set_help_handler(self, w): + def help_handler(): + self.setCursor(qtc.Qt.ArrowCursor) + name = w.objectName() + for key in self.helpdict: + if name in key: + qtw.QMessageBox.information( + self, self.tr('Help!'), + self.tr(self.helpdict[key]) + ) + return True + clickable(w).connect(help_handler) + + def on_config(self): + cur_lang = self.settings.value('language', 'en_US', type=str) + cur_random = self.settings.value('random_engine', 'random', type=str) + try: + config = Config(self) + config.exec_() + except: + qtw.QMessageBox.critical(self, self.tr('Error'), self.tr( + 'Error in languages.lst file format\nPlease see the READ.ME file in locales folder')) + sys.exit(1) + new_lang = self.settings.value('language', 'en_US', type=str) + new_random = self.settings.value('random_engine', 'random', type=str) + if new_lang != cur_lang or new_random != cur_random: + qtw.QMessageBox.information(self, + self.tr('Restart needed'), + self.tr('Changes will be applied after restart!')) + + def action_help(self): + currentIndex = self.ui.tabWidget.currentIndex() + pages = ['tialsTableWidget', 'trialSetting', 'treatmentTableWidget', + 'factorTableWidget', 'freqGridLayout', + 'subjectTableWidget', 'balanceTreeWidget', 'start'] + page = pages[currentIndex] + for key in self.helpdict: + if page in key: + qtw.QMessageBox.information( + self, self.tr('Help!'), + self.tr(self.helpdict[key]) + ) + + def on_arm_weight_change(self, value): + self.ui.armWeightLabel.setText('{}'.format(value)) + + def on_base_prob_change(self, value): + self.ui.baseProbLabel.setText('{:4.2f}'.format(value / 100.0)) + + def on_identifier_length(self, value): + self.trial.identifier_length = value + self.update_sample_size_label() + + def update_sample_size_label(self): + ss, max_sample_size = self.trial.get_max_sample_size() + self.ui.max_sample_size_label.setText(self.tr('Maximum sample size > {}').format(max_sample_size)) + + def closeEvent(self, event): + button = qtw.QMessageBox.question(self, self.tr("Warning"), + self.tr('Are you sure you want to quit?'), + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + event.ignore() + else: + event.accept() + + def page_changed(self, currentTabIndex): + self.setCursor(qtc.Qt.ArrowCursor) + self.ui.actionNew.setEnabled(False) + self.ui.actionDelete.setEnabled(False) + self.ui.actionSave.setEnabled(False) + self.ui.actionLevels.setEnabled(False) + if self.trial == None and currentTabIndex != 0: + qtw.QMessageBox.information( + self, self.tr('No current trial!'), + self.tr('Fist you have to define and select a trial as current!') + ) + self.ui.actionNew.setEnabled(True) + self.ui.actionDelete.setEnabled(True) + self.ui.actionSave.setEnabled(False) + self.ui.actionLevels.setEnabled(False) + self.ui.tabWidget.setCurrentIndex(0) + return + if self.prev_tab_index == 1: + self.save_trial_setting() + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + trial_setting = self.database.get_trial_setting(last_trial_id) + self.trial = Trial(trial_setting) + self.prev_tab_index = currentTabIndex + if currentTabIndex == 0: # trials + self.ui.actionNew.setEnabled(True) + self.ui.actionDelete.setEnabled(True) + self.load_trials() + elif currentTabIndex == 1: # trial settings + self.ui.actionSave.setEnabled(True) + self.load_trial_settings() + elif currentTabIndex == 2: # treatments + self.ui.actionNew.setEnabled(True) + self.ui.actionDelete.setEnabled(True) + self.load_treatments() + elif currentTabIndex == 3: # factors + self.ui.actionNew.setEnabled(True) + self.ui.actionDelete.setEnabled(True) + self.ui.actionLevels.setEnabled(True) + self.load_factors() + elif currentTabIndex == 4: # Frequencies + self.ui.frequenciesCheckBox.setChecked(False) + self.ui.preloadCheckBox.setChecked(False) + self.ui.editPreloadCheckBox.setChecked(False) + self.ui.actionSave.setEnabled(True) + self.ui.actionDelete.setEnabled(True) + self.ui.convertPreloadButton.setEnabled(False) + self.ui.convertWarningCheckBox.setVisible(False) + self.load_frequencies() + elif currentTabIndex == 5: # Subjects + self.ui.actionNew.setEnabled(True) + self.ui.actionDelete.setEnabled(True) + self.load_subjects() + elif currentTabIndex == 6: # Balance + self.load_balance() + + def get_factor_level_dict(self, trial_id): + factor_list = self.database.read_factors(trial_id) + factors = [] + for factor in factor_list: + f = {'id': factor[0], 'title': factor[1], 'weight': factor[2], 'levels': []} + levels = self.database.factor_levels(factor[0]) + for level in levels: + lv = {'id': level[0], 'title': level[1]} + f['levels'].append(lv) + if self.trial.new_subject_random: + index = self.random.randint(0, len(f['levels']) - 1) + f['selected_level_id'] = f['levels'][index]['id'] + else: + f['selected_level_id'] = -1 + factors.append(f) + return factors + + def add_new_subject(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + factors = self.get_factor_level_dict(last_trial_id) + treatments = self.database.read_treatments(last_trial_id) + treatment = {'selected_treatment_id': -1, 'treatments': []} + for t in treatments: + treatment['treatments'].append({'id': t[0], 'title': t[1]}) + enrol_form = EnrolForm(self, factors, treatment, False) + enrol_form.exec_() + if treatment['selected_treatment_id'] == -1: + return + self.update_subjects_progress() + + def subject_coloumn_dblclicked(self, row, col): + button = qtw.QMessageBox.question(self, self.tr("Warning"), + self.tr('''Are you sure you want to edit subject at row "{}" ?\n +Editing subject may invalidate your research and the result of minimisation''').format(row), + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + return + subject_id = int(self.ui.subjectTableWidget.item(row, 0).text()) + identifier = self.ui.subjectTableWidget.item(row, 1).text() + treatment_id = self.database.get_subject_treatment_id(subject_id) + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + factors = self.get_factor_level_dict(last_trial_id) + subject_levels = self.database.get_subject_levels(subject_id) + for factor in factors: + for subject_level in subject_levels: + if factor['id'] == subject_level[0]: + factor['selected_level_id'] = subject_level[1] + break + treatments = self.database.read_treatments(last_trial_id) + treatment = {'selected_treatment_id': treatment_id, 'treatments': []} + for t in treatments: + treatment['treatments'].append({'id': t[0], 'title': t[1]}) + enrol_form = EnrolForm(self, factors, treatment, True, identifier) + enrol_form.exec_() + result = enrol_form.result() + if result != qtw.QDialog.Accepted: + return + treatment_id = treatment['selected_treatment_id'] + for factor in factors: + for subject_level in subject_levels: + if factor['id'] == subject_level[0]: + subject_level[1] = factor['selected_level_id'] + break + self.database.delete_subject_levels(subject_id) + self.database.insert_subject_levels(last_trial_id, subject_id, subject_levels) + self.database.update_subject_treatment_id(subject_id, treatment_id) + self.clear_subject_filters() + self.ui.subjectTableWidget.selectRow(row) + self.ui.subjectTableWidget.setFocus() + + def get_min_free_identifier(self, used_ids): + n = 0 + while n in used_ids: + n += 1 + return n + + def enrol_one(self, selected_indices, selected_ids): + trial_id = self.settings.value('last_trial_id', 0, type=int) + if trial_id == 0: + return + max_sample_size, f = self.trial.get_max_sample_size() + subject_count = self.database.get_subject_count(trial_id) + if subject_count == max_sample_size: + qtw.QMessageBox.critical( + self, + self.tr('Error'), + self.tr( + 'Sample size consumed!\nNo further subject enrol possible!\nPlease convert cases to preload and continue') + ) + return + used_ids = self.database.get_used_identifiers(trial_id) + if not self.trial.recycle_ids: + used_ids.extend(self.database.get_discarded_identifiers(trial_id)) + if not used_ids: + used_ids = [-1] + if self.trial.identifier_order == SEQUENTIAL: + identifier_value = self.get_min_free_identifier(used_ids) + else: + identifier_value = self.get_random_identifier(used_ids, int(max_sample_size)) + new_case = {'levels': selected_indices, 'allocation': -1, 'UI': identifier_value} + # minimised group and preferred group + m_treatment, p_treatment, probs = self.get_minimize_case(new_case, trial_id) + treatments = self.database.read_treatments(trial_id) + treatment_id = treatments[m_treatment][0] + subject_id = self.database.insert_subject(identifier_value, treatment_id, trial_id) + self.database.insert_subject_levels(trial_id, subject_id, selected_ids) + if subject_count == 0: + self.load_subjects() + else: + self.clear_subject_filters() + self.select_row(self.ui.subjectTableWidget, subject_id) + if p_treatment is None: + p_treatment = 0 + return treatments[m_treatment][1], \ + m_treatment, \ + treatments[p_treatment][1], \ + p_treatment, \ + self.trial.format_subject_identifier(identifier_value), probs + + def select_row(self, table_widget, row_id): + for r in range(table_widget.rowCount()): + item = table_widget.item(r, 0) + if item is None: continue + if row_id == int(item.text()): + table_widget.selectRow(r) + return + + def get_minimize_case(self, new_case, trial_id): + model = Model() + num_treatments = self.database.get_num_treatments(trial_id) + model.groups = list(range(num_treatments)) + model.variables = self.database.get_factors_level_indices(trial_id) + model.variables_weight = self.database.get_factor_weights(trial_id) + model.allocation_ratio = self.database.get_allocation_ratios(trial_id) + model.allocations = self.database.get_allocations(trial_id) + model.prob_method = self.trial.prob_method + model.distance_measure = self.trial.dist_method + model.high_prob = self.trial.base_prob + model.min_group = self.database.get_min_allocation_ratio_group_index(trial_id) + model.arms_weight = self.trial.arms_weight + m = Minim(random=self.random, model=model) + m.build_probs(model.high_prob, model.min_group) + m.build_freq_table() + if self.database.has_preload(trial_id): + self.add_to_preload(m.freq_table, trial_id) + minimised_group = m.enroll(new_case, m.freq_table) + preffered_group= m.pref_group + probs = m.selected_probs + return minimised_group, preffered_group, probs + + def add_to_preload(self, freq_table, trial_id): + preload = self.database.get_preload(trial_id) + treatments = self.database.read_treatments(trial_id) + factors = self.database.read_factors(trial_id) + for row, group in enumerate(freq_table): + for v, variable in enumerate(group): + for l, level in enumerate(variable): + factor = factors[v] + levels = self.database.factor_levels(factor[0]) + level = levels[l] + treatment = treatments[row] + key = (treatment[0], factor[0], level[0]) + freq_table[row][v][l] += preload[key] + + def get_random_identifier(self, used_ids, max_sample_size): + if (max_sample_size / len(used_ids) < 100): + pv = list(range(max_sample_size)) + for used_id in used_ids: + if used_id in pv: + pv.remove(used_id) + index = self.random.randint(0, len(pv) - 1) + return pv[index] + else: + v = self.random.randint(0, max_sample_size - 1) + while v in used_ids: + v = self.random.randint(0, max_sample_size - 1) + return v + + def update_subjects_progress(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + ss, ss_label = self.trial.get_max_sample_size() + ss -= self.database.get_count_discarded_identifiers(last_trial_id) + self.ui.subjectProgressBar.setMaximum(ss) + cnt = self.database.get_subject_count(last_trial_id) + self.ui.subjectProgressBar.setValue(cnt) + + def load_subjects(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + self.ui.subjectTableWidget.setRowCount(0) + cnt = self.database.get_subject_count(last_trial_id) + self.update_subjects_progress() + if cnt == 0: + return + factors = self.database.read_factors(last_trial_id) + self.ui.subjectTableWidget.setColumnCount(len(factors) + 5) + headers = ['#', self.tr('ID'), self.tr('Treatment')] + for factor in factors: + headers.append(factor[1]) + headers += [self.tr('Enrolled'), self.tr('Modified')] + self.ui.subjectTableWidget.setHorizontalHeaderLabels(headers) + header = self.ui.subjectTableWidget.horizontalHeader() + header.setSectionResizeMode( + self.ui.subjectTableWidget.columnCount() - 1, + qtw.QHeaderView.ResizeToContents + ) + header.setSectionResizeMode( + self.ui.subjectTableWidget.columnCount() - 2, + qtw.QHeaderView.ResizeToContents + ) + self.ui.subjectTableWidget.insertRow(0) + for i in range(1, len(headers)): + lineEdit = qtw.QLineEdit() + lineEdit.setPlaceholderText(self.tr('Filter')) + lineEdit.editingFinished.connect(self.subject_colum_editing_finished) + self.ui.subjectTableWidget.setCellWidget(0, i, lineEdit) + self.ui.subjectTableWidget.hideColumn(0) + self.load_subject_rows() + + def subject_colum_editing_finished(self): + self.load_subject_rows() + + def clear_subject_filters(self): + for c in range(1, self.ui.subjectTableWidget.columnCount()): + lineEdit = self.ui.subjectTableWidget.cellWidget(0, c) + lineEdit.setText('') + self.load_subject_rows() + + def load_subject_rows(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + subjects = self.database.get_subjects(last_trial_id) + self.ui.subjectTableWidget.setRowCount(1) + fields = [None] + for c in range(1, self.ui.subjectTableWidget.columnCount()): + lineEdit = self.ui.subjectTableWidget.cellWidget(0, c) + if lineEdit is None: + self.load_subjects() + return + text = lineEdit.text().strip() + fields.append(None if len(text) == 0 else text) + # model = self.ui.subjectTableWidget.model() + for subject in subjects: + subject_id = subject[0] + treatment_id = subject[1] + identifier_value = subject[2] + enrolled = subject[3] + modified = subject[4] + treatment = self.database.get_treatment_title(treatment_id) + row = [subject_id, self.trial.format_subject_identifier(identifier_value), treatment] + subject_levels = self.database.get_subject_levels(subject_id) + row.extend(self.database.get_subject_level_titles(subject_levels)) + row.extend([enrolled, modified]) + for c, row_text in enumerate(row): + if fields[c] == None: + continue + if not fields[c] in row_text: + break + else: + r = self.ui.subjectTableWidget.rowCount() + self.ui.subjectTableWidget.insertRow(r) + # data = model.headerData(r, qtc.Qt.Vertical) + for c, row_text in enumerate(row): + item = qtw.QTableWidgetItem(str(row_text)) + item.setFlags(item.flags() ^ qtc.Qt.ItemIsEditable) + self.ui.subjectTableWidget.setItem(r, c, item) + labels = [str(n) for n in range(1, len(subjects) + 1)] + labels.insert(0, '') + self.ui.subjectTableWidget.setVerticalHeaderLabels(labels) + + def set_cur_count(self, checked): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + show_freq = self.ui.frequenciesCheckBox.isChecked() + show_preload = self.ui.preloadCheckBox.isChecked() + if show_preload and not show_freq: + self.ui.editPreloadCheckBox.setEnabled(True) + self.on_edit_preload(self.ui.editPreloadCheckBox.isChecked()) + else: + self.ui.editPreloadCheckBox.setEnabled(False) + self.ui.editPreloadCheckBox.setChecked(False) + self.ui.convertPreloadButton.setEnabled(False) + if show_preload and show_freq: + cur_count = self.database.get_preload_with_freq(last_trial_id) + elif show_preload and not show_freq: + cur_count = self.database.get_preload(last_trial_id) + elif show_freq and not show_preload: + cur_count = self.database.get_freq(last_trial_id) + if self.database.has_subject(last_trial_id): + self.ui.convertPreloadButton.setEnabled(True) + else: + cur_count = self.database.get_empty_freq(last_trial_id) + if cur_count is None: + return + self.freqTable.set_counts(cur_count) + + def on_convert_preload(self): + if not self.ui.convertWarningCheckBox.isChecked(): + qtw.QMessageBox.information( + self, self.tr('Double check!'), + self.tr('Need your final confirm') + ) + self.ui.convertWarningCheckBox.setVisible(True) + return + button = qtw.QMessageBox.question(self, self.tr("Deleting preload"), + self.tr('''Are you certainly sure you want to convert all subjects into preload? +ALL subjects will be deleted'''), + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + self.ui.convertWarningCheckBox.setChecked(False) + self.ui.convertWarningCheckBox.setVisible(False) + return + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + self.ui.convertWarningCheckBox.setChecked(False) + self.ui.convertWarningCheckBox.setVisible(False) + self.ui.convertPreloadButton.setEnabled(False) + self.freqTable.add_to_preload() + self.database.delete_subjects(last_trial_id) + self.ui.frequenciesCheckBox.setChecked(False) + self.ui.preloadCheckBox.setChecked(True) + + def on_edit_preload(self, check): + self.freqTable.toggleReadOnly() + + def load_frequencies(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + self.freqTable = FreqTable(self, self.ui, last_trial_id) + self.freqTable.build() + if self.database.has_subject(self.trial.id): + self.ui.frequenciesCheckBox.setChecked(True) + elif self.database.has_preload(self.trial.id): + self.ui.preloadCheckBox.setChecked(True) + + def factor_levels(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + selected = self.ui.factorTableWidget.selectedIndexes() + if not selected: + return + factor_id = int(self.ui.factorTableWidget.item(selected[0].row(), 0).text()) + factor_title = self.ui.factorTableWidget.item(selected[0].row(), 1).text() + factorLevels = FactorLevels(self, last_trial_id, factor_id, factor_title) + factorLevels.exec_() + self.load_factors() + + def getCurrentTrialFunc(self, trial): + def getToggleFunc(checked): + trial_id = int(trial[0]) + title = trial[1] + if len(title) > 100: + title = title[:97] + '...' + self.setWindowTitle(self.tr('minimpy2 [{}]').format(title)) + self.settings.setValue('last_trial_id', trial_id) + self.trial = Trial(self.database.get_trial_setting(trial_id)) + + return getToggleFunc + + def action_new(self): + self.setCursor(qtc.Qt.ArrowCursor) + currentTabIndex = self.ui.tabWidget.currentIndex() + if currentTabIndex == 0: # trials + self.add_new_trial() + elif currentTabIndex == 2: # treatments + self.add_new_treatment() + elif currentTabIndex == 3: # factors + self.add_new_factor() + elif currentTabIndex == 5: # subjects + self.add_new_subject() + + def action_save(self): + self.setCursor(qtc.Qt.ArrowCursor) + currentTabIndex = self.ui.tabWidget.currentIndex() + if currentTabIndex == 1: # setting + self.save_trial_setting() + elif currentTabIndex == 4: + self.freqTable.on_save_preload() + + def add_new_factor(self): + self.setCursor(qtc.Qt.ArrowCursor) + if self.check_trial_subjects_or_preload(): + return + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + title, ok = qtw.QInputDialog.getText( + self, + self.tr('Factor title'), + self.tr('Title'), + qtw.QLineEdit.EchoMode.Normal + ) + if ok and len(title.strip()) != 0: + if self.database.factor_title_exists(title, last_trial_id): + qtw.QMessageBox.critical(self, + self.tr('Error!'), + self.tr('''Factor title '{}' already exist!'''.format(title)) + ) + return + factor_id = self.database.insert_factor(last_trial_id, title) + state = self.ui.factorTableWidget.blockSignals(True) + factor = [str(factor_id), title, '', 1.0] + self.add_factor_row(factor) + self.ui.factorTableWidget.blockSignals(state) + + def add_new_treatment(self): + if self.check_trial_subjects_or_preload(): + return + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + title, ok = qtw.QInputDialog.getText( + self, + self.tr('Treatment title'), + self.tr('Title'), + qtw.QLineEdit.EchoMode.Normal + ) + if ok and len(title.strip()) != 0: + if self.database.treatment_title_exists(title, last_trial_id): + qtw.QMessageBox.critical(self, + self.tr('Error!'), + self.tr('''Treatment title '{}' already exist!'''.format(title)) + ) + return + treatment_id = self.database.insert_treatment(last_trial_id, title) + state = self.ui.treatmentTableWidget.blockSignals(True) + cols = [str(treatment_id), title, 1] + self.add_treatment_row(cols) + self.ui.tialsTableWidget.blockSignals(state) + + def add_new_trial(self): + title, ok = qtw.QInputDialog.getText( + self, + self.tr('Trial title'), + self.tr('Title'), + qtw.QLineEdit.EchoMode.Normal + ) + if ok and len(title.strip()) != 0: + if self.database.trial_title_exists(title): + qtw.QMessageBox.critical(self, + self.tr('Error!'), + self.tr('''A trial title '{}' already exist!'''.format(title)) + ) + return + trial_id = self.database.insert_trial(title) + self.settings.setValue('last_trial_id', trial_id) + self.trial = Trial(self.database.get_trial_setting(trial_id)) + self.load_trials() + + def action_delete(self): + self.setCursor(qtc.Qt.ArrowCursor) + currentTabIndex = self.ui.tabWidget.currentIndex() + if currentTabIndex == 0: + self.delete_trial() + elif currentTabIndex == 2: # treatments + self.delete_treatment() + elif currentTabIndex == 3: # factors + self.delete_factor() + elif currentTabIndex == 4: # preload + self.freqTable.on_clear_preload() + elif currentTabIndex == 5: # subjects + self.delete_subject() + + def delete_subject(self): + selected = self.ui.subjectTableWidget.selectedIndexes() + if not selected: + return + identifier = self.ui.subjectTableWidget.item(selected[0].row(), 1).text() + button = qtw.QMessageBox.question(self, self.tr("Confirm delete"), + self.tr('Are you sure you want to delete subject "{}"?').format(identifier), + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + return + subject_id = int(self.ui.subjectTableWidget.item(selected[0].row(), 0).text()) + subject = self.database.get_subject(subject_id) + id_value = subject.value('identifier_value') + trial_id = subject.value('trial_id') + if self.trial.recycle_ids: + id_value = None + self.database.delete_subject(subject_id, id_value, trial_id) + self.ui.subjectTableWidget.removeRow(selected[0].row()) + self.update_subjects_progress() + + def check_trial_subjects_or_preload(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + has_subject = self.database.has_subject(last_trial_id) + has_preload = self.database.has_preload(last_trial_id) + if has_subject or has_preload: + qtw.QMessageBox.warning(self, self.tr('New factor not allowed!'), + self.tr('''Trial already has subject or preload. + You can not add or delete factors, treatments or levels''')) + return True + return False + + def delete_treatment(self): + if self.check_trial_subjects_or_preload(): + return + selected = self.ui.treatmentTableWidget.selectedIndexes() + if not selected: + return + title = self.ui.treatmentTableWidget.item(selected[0].row(), 1).text() + button = qtw.QMessageBox.question(self, self.tr("Confirm delete"), + self.tr('Are you sure you want to delete "%s"?') % title, + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + return + treatment_id = int(self.ui.treatmentTableWidget.item(selected[0].row(), 0).text()) + self.database.delete_treatment(treatment_id) + self.ui.treatmentTableWidget.blockSignals(True) + self.ui.treatmentTableWidget.removeRow(selected[0].row()) + self.ui.treatmentTableWidget.blockSignals(False) + + def delete_factor(self): + if self.check_trial_subjects_or_preload(): + return + selected = self.ui.factorTableWidget.selectedIndexes() + if not selected: + return + title = self.ui.factorTableWidget.item(selected[0].row(), 1).text() + button = qtw.QMessageBox.question(self, self.tr("Confirm delete"), + self.tr('Are you sure you want to delete "%s"?') % title, + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + return + factor_id = int(self.ui.factorTableWidget.item(selected[0].row(), 0).text()) + self.database.delete_factor(factor_id) + self.ui.factorTableWidget.blockSignals(True) + self.ui.factorTableWidget.removeRow(selected[0].row()) + self.ui.factorTableWidget.blockSignals(False) + + def delete_trial(self): + selected = self.ui.tialsTableWidget.selectedIndexes() + if not selected: + return + title = self.ui.tialsTableWidget.item(selected[0].row(), 1).text() + button = qtw.QMessageBox.question(self, self.tr("Confirm delete"), + self.tr('Are you sure you want to delete "%s"?') % title, + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + return + trial_id = int(self.ui.tialsTableWidget.item(selected[0].row(), 0).text()) + self.database.delete_trial(trial_id) + self.setWindowTitle(self.tr('minimpy2')) + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == trial_id: + self.settings.setValue('last_trial_id', 0) + self.trial = None + self.ui.tialsTableWidget.removeRow(selected[0].row()) + + def factor_item_changed(self, itemWidget): + row, col = itemWidget.row(), itemWidget.column() + column = 'title' + value = self.ui.factorTableWidget.item(row, col).text() + if col == 3: + column = 'weight' + value = float(value) + factor_id = int(self.ui.factorTableWidget.item(row, 0).text()) + oldValue = self.database.update_factor(factor_id, column, value) + if oldValue is not None: + self.abort_table_widget_change(self.ui.factorTableWidget, row, col, oldValue) + + def abort_table_widget_change(self, table, row, col, oldValue): + table.item(row, col).setText(oldValue) + + def treatment_item_changed(self, itemWidget): + row, col = itemWidget.row(), itemWidget.column() + column = 'title' + value = self.ui.treatmentTableWidget.item(row, col).text() + if col == 2: + column = 'ratio' + value = int(value) + treatment_id = int(self.ui.treatmentTableWidget.item(row, 0).text()) + oldValue = self.database.update_treatment(treatment_id, column, value) + if oldValue is not None: + self.abort_table_widget_change(self.ui.treatmentTableWidget, row, col, oldValue) + + def trial_cell_changed(self, row, col): + trial_id = int(self.ui.tialsTableWidget.item(row, 0).text()) + column = 'title' + value = self.ui.tialsTableWidget.item(row, col).text() + if col == 2: + column = 'code' + oldValue = self.database.update_trial(trial_id, column, value) + if oldValue is not None: + self.abort_table_widget_change(self.ui.tialsTableWidget, row, col, oldValue) + + def load_treatments(self): + self.ui.treatmentTableWidget.blockSignals(True) + self.ui.treatmentTableWidget.setRowCount(0) + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + query = self.database.load_treatments(last_trial_id) + self.ui.treatmentTableWidget.setColumnCount(3) + ID = self.tr('ID') + Title = self.tr('Title') + Ratio = self.tr('Ratio') + self.ui.treatmentTableWidget.setHorizontalHeaderLabels([ID, Title, Ratio]) + header = self.ui.treatmentTableWidget.horizontalHeader() + header.setSectionResizeMode(0, qtw.QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, qtw.QHeaderView.Stretch) + header.setSectionResizeMode(2, qtw.QHeaderView.ResizeToContents) + while query.next(): + cols = [str(query.value('id')), query.value('title'), query.value('ratio')] + self.add_treatment_row(cols) + self.ui.treatmentTableWidget.blockSignals(False) + + def add_treatment_row(self, cols): + r = self.ui.treatmentTableWidget.rowCount() + self.ui.treatmentTableWidget.insertRow(r) + id_item = qtw.QTableWidgetItem(cols[0]) + id_item.setTextAlignment(qtc.Qt.AlignCenter) + self.ui.treatmentTableWidget.setCellWidget(r, 0, qtw.QLabel()) + self.ui.treatmentTableWidget.setItem(r, 0, id_item) + title_item = qtw.QTableWidgetItem(cols[1]) + self.ui.treatmentTableWidget.setItem(r, 1, title_item) + ratio_spin = qtw.QSpinBox() + ratio_spin.setMinimum(1) + ratio_spin.setMaximum(20) + ratio_spin.setValue(cols[2]) + ratio_item = qtw.QTableWidgetItem(str(cols[2])) + self.ui.treatmentTableWidget.setItem(r, 2, ratio_item) + ratio_spin.valueChanged.connect(lambda value: ratio_item.setText(str(value))) + self.ui.treatmentTableWidget.setCellWidget(r, 2, ratio_spin) + + def add_one_trial_cell(self, r, c, trial): + item = trial[c] + oneItem = qtw.QTableWidgetItem(item) + if c == 0: + oneItem.setTextAlignment(qtc.Qt.AlignCenter) + self.ui.tialsTableWidget.setCellWidget(r, c, qtw.QLabel()) + if c == 3: + check = qtw.QRadioButton() + check.setStyleSheet('text-align: center; margin-left:50%; margin-right:50%;') + check.toggled.connect(self.getCurrentTrialFunc(trial)) + check.setChecked(item) + self.ui.tialsTableWidget.setCellWidget(r, c, check) + self.ui.tialsTableWidget.setItem(r, c, oneItem) + + def load_factors(self): + self.ui.factorTableWidget.blockSignals(True) + self.ui.factorTableWidget.setRowCount(0) + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + self.ui.factorTableWidget.setColumnCount(4) + header = self.ui.factorTableWidget.horizontalHeader() + header.setSectionResizeMode(0, qtw.QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, qtw.QHeaderView.Stretch) + header.setSectionResizeMode(2, qtw.QHeaderView.Stretch) + self.ui.factorTableWidget.setHorizontalHeaderLabels([self.tr('ID'), self.tr('Factor'), self.tr('Levels'), + self.tr('Weight')]) + query = self.database.load_factors(last_trial_id) + factors = [] + while query.next(): + factor = [str(query.value('id')), query.value('title'), query.value('weight')] + factors.append(factor) + for factor in factors: + levels = self.database.get_levels(int(factor[0])) + factor.insert(2, levels) + self.add_factor_row(factor) + self.ui.factorTableWidget.blockSignals(False) + + def factor_coloumn_dblclicked(self, row, col): + if col != 2: + return + self.factor_levels() + + def add_factor_row(self, factor): + r = self.ui.factorTableWidget.rowCount() + self.ui.factorTableWidget.insertRow(r) + id_item = qtw.QTableWidgetItem(factor[0]) + id_item.setTextAlignment(qtc.Qt.AlignCenter) + self.ui.factorTableWidget.setCellWidget(r, 0, qtw.QLabel()) + self.ui.factorTableWidget.setItem(r, 0, id_item) + title_item = qtw.QTableWidgetItem(factor[1]) + self.ui.factorTableWidget.setItem(r, 1, title_item) + levels_item = qtw.QTableWidgetItem(factor[2]) + levels_item.setTextAlignment(qtc.Qt.AlignCenter) + self.ui.factorTableWidget.setCellWidget(r, 2, qtw.QLabel()) + self.ui.factorTableWidget.setItem(r, 2, levels_item) + weight_spin = qtw.QDoubleSpinBox() + weight_spin.setMinimum(1.0) + weight_spin.setMaximum(10.0) + weight_spin.setSingleStep(0.10) + weight_spin.setValue(factor[3]) + weight_item = qtw.QTableWidgetItem(str(factor[3])) + self.ui.factorTableWidget.setItem(r, 3, weight_item) + weight_spin.valueChanged.connect(lambda value: weight_item.setText(str(value))) + self.ui.factorTableWidget.setCellWidget(r, 3, weight_spin) + + def load_trials(self): + self.ui.tialsTableWidget.blockSignals(True) + self.ui.tialsTableWidget.setRowCount(0) + query = self.database.get_db().exec_('SELECT id, title, code FROM trials') + trial_list = [] + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + while query.next(): + row = [ + str(query.value('id')), + query.value('title'), + query.value('code'), + query.value('id') == last_trial_id + ] + + trial_list.append(row) + if len(trial_list) == 0: + return + self.ui.tialsTableWidget.setRowCount(len(trial_list)) + self.ui.tialsTableWidget.setColumnCount(len(trial_list[0])) + title = self.tr('Title') + trial_id = self.tr('ID') + code = self.tr('Code') + current = self.tr('Current') + header_lst = [trial_id, title, code, current] + self.ui.tialsTableWidget.setHorizontalHeaderLabels(header_lst) + header = self.ui.tialsTableWidget.horizontalHeader() + header.setSectionResizeMode(0, qtw.QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, qtw.QHeaderView.Stretch) + header.setSectionResizeMode(2, qtw.QHeaderView.ResizeToContents) + header.setSectionResizeMode(3, qtw.QHeaderView.ResizeToContents) + for r in range(len(trial_list)): + for c in range(len(trial_list[r])): + self.add_one_trial_cell(r, c, trial_list[r]) + self.ui.tialsTableWidget.setColumnWidth(1, 400) + self.ui.tialsTableWidget.blockSignals(False) + + def save_trial_setting(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + self.database.update_trial_setting(last_trial_id, self.ui) + self.setWindowTitle(self.tr('minimpy2 [{}]').format(self.ui.trialTitleLineEdit.text())) + self.load_trial_settings() + + def load_trial_settings(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + if self.database.has_subject(last_trial_id): + self.ui.trialIdentifierTypeComboBox.setEnabled(False) + else: + self.ui.trialIdentifierTypeComboBox.setEnabled(True) + self.database.load_trial_setting(last_trial_id, self.ui) + self.on_identifier_length(self.trial.identifier_length) + self.on_base_prob_change(self.trial.base_prob * 100) + + def load_balance(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + if not self.database.has_preload(last_trial_id) and not self.database.has_subject(last_trial_id): + return + table = [] + freq = self.database.get_preload_with_freq(last_trial_id) + treatments = self.database.read_treatments(last_trial_id) + factors = self.database.read_factors(last_trial_id) + for treatment in treatments: + row = [] + for factor in factors: + t = treatment[0] + f = factor[0] + levels = self.database.factor_levels(f) + for level in levels: + l = level[0] + k = (t, f, l) + row.append(freq[k]) + table.append(row) + balance = self.get_trial_balance(table, last_trial_id) + variables = [] + for factor in factors: + f = factor[0] + levels = self.database.factor_levels(f) + variables.append(list(range(len(levels)))) + row = 0 + self.ui.balanceTreeWidget.clear() + for idx, variable in enumerate(variables): + wt = factors[idx][2] + var_total = [0] * 4 + level_rows = [] + for level in variable: + for i in range(4): + balance[row][i] *= float(wt) + var_total[i] += balance[row][i] + level_rows.append(balance[row]) + row += 1 + var_total = [factors[idx][1]] + [var_total[j] / len(variable) for j in range(4)] + var_total = list(map(str, var_total)) + variable_node = qtw.QTreeWidgetItem(self.ui.balanceTreeWidget, var_total) + levels = self.database.factor_levels(factors[idx][0]) + level_names = [level[1] for level in levels] + for level, level_row in enumerate(level_rows): + level_str_list = list(map(str, [level_names[level]] + level_row)) + qtw.QTreeWidgetItem(variable_node, level_str_list) + last_row = row + rows = len(balance) - 2 + for col in range(4): + balance[rows].append(1.0 * sum([balance[row][col] for row in range(rows)]) / rows) + balance[rows + 1].append(max([balance[row][col] for row in range(rows)])) + means = list(map(str, [self.tr('Mean')] + balance[last_row])) + qtw.QTreeWidgetItem(self.ui.balanceTreeWidget, means) + maxes = list(map(str, [self.tr('Max')] + balance[last_row + 1])) + qtw.QTreeWidgetItem(self.ui.balanceTreeWidget, maxes) + self.ui.balanceProgressBar.setValue(100 - 100 * balance[last_row][0]) + self.ui.mean_balance_label.setText(self.tr('Balance (mean MB = {:.4f})').format(balance[last_row][0])) + subjects = self.database.get_subjects(last_trial_id) + if len(subjects) == 0: + self.ui.randomness_label.setText(self.tr('No subject enrolled!')) + self.ui.randomnessProgressBar.setValue(0) + return + else: + self.set_randomness(subjects, last_trial_id) + + def set_randomness(self, subjects, trial_id): + id_dict = self.database.get_treatments_id_dict(trial_id) + seq = [id_dict[subject[1]] for subject in subjects] + mean = sum(seq) / len(seq) + seq = list(map(lambda x: 1 if x > mean else 0, seq)) + rt = RunTest(seq, 0, 1) + z, p = rt.get_p() + if p == None: + return + self.ui.randomnessProgressBar.setValue(p * 100) + self.ui.randomness_label.setText('Randomness (P = {:.4f})'.format(p)) + + def get_trial_balance(self, table, trial_id): + model = Model() + m = Minim(random=self.random, model=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) + treatments = self.database.read_treatments(trial_id) + allocation_ratio = [treatment[2] for treatment in treatments] + for row, level_count in enumerate(levels): + 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)) + return balances + + def import_mnd_file(self): + filename = qtw.QFileDialog.getOpenFileName(self, + self.tr('Select mnd file'), + qtc.QDir.currentPath(), + 'mnd files (*.mnd)') + if filename is None: + return + mnd = Mnd(filename[0]) + if mnd.data_file_valid(): + button = qtw.QMessageBox.question(self, self.tr("Warning"), + mnd.get_detail() + '\n' + self.tr('Do you want to save this trial?'), + qtw.QMessageBox.Yes | qtw.QMessageBox.No) + if button != qtw.QMessageBox.Yes: + return + trial_id = mnd.import_data(self.database) + self.settings.setValue('last_trial_id', trial_id) + self.trial = Trial(self.database.get_trial_setting(trial_id)) + if self.ui.tabWidget.currentIndex() != 0: + self.ui.tabWidget.setCurrentIndex(0) + else: + self.load_trials() + qtw.QMessageBox.information(self, + self.tr('Imported'), + self.tr('''Data imported successfully!''') + ) + else: + qtw.QMessageBox.information( + self, self.tr('Import error!'), + self.tr('Data are not valid!') + ) + + def export_mnd_file(self): + last_trial_id = self.settings.value('last_trial_id', 0, type=int) + if last_trial_id == 0: + return + filename = qtw.QFileDialog.getSaveFileName(self, + self.tr('Export to file'), + qtc.QDir.currentPath(), + 'mnd files (*.mnd)') + if filename is None: + return + filename = filename[0] + if not filename.endswith('.mnd'): + filename += '.mnd' + mnd = Mnd(filename) + if mnd.export_data(last_trial_id, self.database): + qtw.QMessageBox.information(self, self.tr('Export'), self.tr('Current trial exported successfully to "{}"').format(filename)) + + +def clickable(widget): + class Filter(qtc.QObject): + clicked = qtc.Signal() + def eventFilter(self, obj, event): + if obj == widget: + if event.type() == qtc.QEvent.MouseButtonRelease: + if obj.rect().contains(event.pos()): + self.clicked.emit() + # The developer can opt for .emit(obj) to get the object within the slot. + return True + return False + filter = Filter(widget) + widget.installEventFilter(filter) + return filter.clicked + + +def start_app(): + try: + app = qtw.QApplication(sys.argv) + except RuntimeError: + app = qtc.QCoreApplication.instance() + app.setWindowIcon(qtg.QIcon('images/logo.png')) + settings = qtc.QSettings('net.saghaei', 'minimpy2') + lang = settings.value('language', 'en_US', type=str) + if lang != 'en_US': + translator = qtc.QTranslator() + translator.load('locales/{}'.format(lang)) + app.installTranslator(translator) + mw = MainWindow(settings) + app.mainWindow = mw + mw.show() + exit_code = app.exec_() + return exit_code + + +if __name__ == '__main__': + start_app() diff --git a/minim.py b/minim.py new file mode 100755 index 0000000..98b7c65 --- /dev/null +++ b/minim.py @@ -0,0 +1,181 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import math +from model import Model + +class Minim(object): + """ +Main Minimization Class +------------------------------------------------------------------------------- +Instanciation: Minim(allocations=None, groups=None, new_cae=None) + + 'allocations' is a list of cases. + Each 'case' is a dictionairy of 'levels' and 'allocation' and optianlly other info. + Levels element is a list, with ith element is the level of ith var + allocation is the allocation of the case: 0 to n-1, -1 = unalocated + + new_case is one case, with allocation = -1 + """ + + def __init__(self, random, model=None, new_cae=None): + self.pref_group = None + self.selected_probs = None + self.random = random + self._allocations = model.allocations + self._prob_method = model.prob_method + self._distance_measure = model.distance_measure + self._groups = model.groups + self._variables = model.variables + self._variables_weight = model.variables_weight + self._allocation_ratio = model.allocation_ratio + self._high_prob = model.high_prob + self._min_group = model.min_group + self._arms_weight = model.arms_weight + + @property + def allocations(self): + return self._allocations + + @allocations.setter + def allocations(self, allocations): + self._allocations = allocations + + @property + def groups(self): + return self._groups + + @groups.setter + def groups(self, groups): + self._groups = groups + + def build_probs(self, min_high_prob=None, min_group=0): + """ +This is only for bcm method of probability assignment + min_high_prob: The high probability value for the group with + lowest allocation ratio + min_group: index of the group with the lowest probability ratio +for Naive method group rows are similar + """ + if min_high_prob == None: + min_high_prob = self._high_prob + self._probs = [[0] * len(self._groups) for g in range(len(self._groups))] + self._probs[min_group][min_group] = min_high_prob + for r in self._groups: + if r == min_group: + continue + numerator = sum([self._allocation_ratio[row] for row in range(len(self._groups)) if row != r]) + denominator = sum([self._allocation_ratio[row] for row in range(len(self._groups)) if row != min_group]) + if self._prob_method == Model.BCM: + self._probs[r][r] = 1.0 - (1.0 * numerator / denominator) * (1.0 - min_high_prob) + elif self._prob_method == Model.NM: + self._probs[r][r] = min_high_prob + for r in self._groups: + for c in self._groups: + if r == c: + continue + H = self._probs[r][r] + numerator = self._allocation_ratio[c] + denominator = sum([self._allocation_ratio[col] for col in range(len(self._groups)) if col != r]) + if self._prob_method == Model.BCM: + self._probs[r][c] = (1.0 * numerator / denominator) * (1.0 - H) + elif self._prob_method == Model.NM: + self._probs[r][c] = 1.0 * (1.0 - min_high_prob) / (len(self._groups) - 1.0) + + def enroll(self, case, freq_table=None): + if freq_table: + self.freq_table = freq_table + else: + self.build_freq_table() + new_levels = case['levels'] + level_count = [[] for v in self._variables] + # add the freq_table to level count + for row in self.freq_table: + for variable, level in enumerate(new_levels): + level_count[variable].append(row[variable][level]) + scores = [] + adj_count = [sum(self.freq_table[i][0]) / self._allocation_ratio[i] for i in self._groups] + max_list = [] + mx = max(adj_count) + for i, cnt in enumerate(adj_count): + if cnt == mx: + max_list.append(i) + for g in self._groups: + w_arms = self._arms_weight + func = min + if g in max_list: + func = max + scores.append(sum([1.0 * func(self._variables_weight[v], w_arms) * self.get_imbalance_score(level_count[v], g) for v in range(len(self._variables))])) + # indices of minimum score values + min_indices = self.get_min_ties_index(scores) + if len(min_indices) == len(self._groups): + # all treatment have same score + # so build a probs based on allocation ratio + probs = self._allocation_ratio + self.pref_group = None + else: + # indices of prefered treatment(s) + # randomly selecting an index + pt = self.random.choice(min_indices) + self.pref_group = pt + probs = self._probs[pt] + self.selected_probs = probs + case['allocation'] = self.get_rand_biased(probs) + self.build_freq_table() + return case['allocation'] + + def get_rand_biased(self, probs): + p = self.random.uniform(0, sum(probs)) + for i in range(len(probs)): + if p < sum(probs[:i+1]): + return i + + def get_min_ties_index(self, lst): + """get indices of min values of the input list""" + L, ret = min(lst), [] + for idx, item in enumerate(lst): + # this pair is equal + if abs(item - L) < sys.float_info.epsilon: + # so take it + ret.append(idx) + return ret + + def get_marginal_balance(self, count): + numerator = sum([abs(count[i] - count[j]) for i in range(len(count)-1) for j in range(i+1, len(count))]) + denominator = (len(count)-1) * sum(count) + if denominator == 0: return 0.0 + return (1.0 * numerator) / denominator + + def get_imbalance_score(self, count, group, enroll=True): + if enroll: count[group] += 1 + adj_count = [(1.0 * count[i]) / self._allocation_ratio[i] for i in range(len(count))] + if self._distance_measure == Model.MB: + ret = self.get_marginal_balance(adj_count) + elif self._distance_measure == Model.rng: + ret = max(adj_count) - min(adj_count) + elif self._distance_measure == Model.var: + ret = self.get_variance(adj_count) + elif self._distance_measure == Model.SD: + ret = self.get_standard_deviation(adj_count) + if enroll: count[group] -= 1 + return ret + + def get_standard_deviation(self, count): + return math.sqrt(self.get_variance(count)) + + def get_variance(self, count): + mean = 1.0 * sum(count) / len(count) + sq_terms = sum([(i - mean)**2 for i in count]) + return 1.0 * sq_terms / (len(count) - 1.0) + + def build_freq_table(self): + table = [[[0 for l in v] for v in self._variables] for g in self._groups] + self.freq_table = table + if not self._allocations: return + for case in self._allocations: + if not 'allocation' in case: return + group = case['allocation'] + for variable, level in enumerate(case['levels']): + table[group][variable][level] += 1 + self.freq_table = table diff --git a/minimisation_en_US.txt b/minimisation_en_US.txt new file mode 100755 index 0000000..04da1e3 --- /dev/null +++ b/minimisation_en_US.txt @@ -0,0 +1,4 @@ +In clinical trials, best result will be obtained with controlled designs using randomised enrollment. By randomised we mean the process of subject allocation determines only by pure chance, and there is no selection bias in play. Furthermore by randomisation we aim to have balance over background variables such as demographic and other important prognostic factors, among branches of clinical trial. The rational behind having balanced groups is that if there is a significant deffernces among groups with respect to one or more baseline factors, then the results of clinical trial in unreliable which needs complication data analysis with overal loss of study power and validity. For this reason published articles present at the begininng of result sectiob the overal distribution of baseline factors among trial groups to demonstrate the state of balance among them and valifity of results and conclusions. Every study chooses different subsets of baseline factors to for the purpose of balancing thme among groups, though general factors such as gender, age and weight are included in most trials as the baseline factors. It must be emphasized absence of significant difference among groups with respect to baseline factors does not nessecerily equvalent to balanced groups. +Unfortunately is spite of random allocation of subject among groups, sometimes significant differences occure among trial groups with respect to baseline factors which bring considerable diturbances in analysis and interpretation of the results. The probability of this phenomenon is not low, though depends on the number of baseline factors, sample and effect sizes. The probability is high in small samples with large effect sizes and many baseline factors. +A method for preventing this phenomenon is minimisation which largely unknow among clinical researchers. In this method, allocation is not based solely and purely on chance but also the allocation is in the favor of minimising the difference among groups with respect of baseline factors. Therefore the subject in enrolled into a favored group with a more probability that non-favored groups. This process bring increasing balance with continuing enrollment of subjects into arms of trial. +Unfortunately the whole process of minimisation is calculation intensive and requires a lot of data keeping and maintenance which make manual application of minimisation practically impossible. There are several desktop or online application for implementation of minimisation. Some of these include Minim, MinimPy and QMinim. diff --git a/minimisation_fa_IR.txt b/minimisation_fa_IR.txt new file mode 100755 index 0000000..a3a1859 --- /dev/null +++ b/minimisation_fa_IR.txt @@ -0,0 +1,4 @@ +در کارآزمائی های بالینی، بهترین نتیجه از مداخلات کنترل شده و تصادفی بدست می‌آید. منظور از تصادفی سازی یعنی تقسیم سوژه ها بین گروه‌های مختلف مداخله در کارآزمائی بالینی فقط براساس شانس می‌باشد و تورش انتخاب در این پدیده نقشی نداشته ندارد. به علاوه با تصادفی سازی تقسیم سوژه ها بین گروه ها سعی بر این داریم که عوامل مختلف زمینه ای بصورت یک نواخت بین گروه های توزیع گردند و گروه یا گروه هائی از نظر برخی عوامل زمینه ای مانند سن و جنس و غیره با بقیه تفاوت چشمگیری نداشته باشد. چرا که در صورت وقوع تفاوت بارز بین گروه ها از نظر عوامل زمینه ای، آنالیز نتایح حاصله غیر ممکن یا دشوار میگردد، قدرت مطالعه کم میشود و بطور کلی اعتبار کارآزمائی کاهش می یابد. اگر گروه ها از نظر عوامل زمینه مشابه باشند و تنها از نظر نوع مداخله با هم فرق کنند در این صورت هرگونه تفاوت در نتایج کارآزمائی قابل انتساب به نوع مداخله صورت گرفته می باشد. برای همین است که مقالات حاصل از کارآزمائی های بالینی قبل از پرداختن به نتایج اصلی ابتدا شرح در مورد مقایسه عوامل زمینه ای بین گروه های بالینی ارائه می نمایند تا خواننده از مشابهت گروه ها اطمینان حاصل نماید. اینکه این عوامل زمینه ای گزارش شده چه عواملی باید باشند بسته به نوع مداخله ممکن است بسیار متفاوت باشد اگر چه برخی عوامل مانند سن، وزن، قد و جنس معمولا در اکثر مطالعات گزارش میگردند. البته لازم به ذکر است که عدم وجود تفاوت معنی دار بین گروه‌ها از نظر این عوامل زمینه ای لزوما به معنای مشابه بودن آنها نمی باشد. +متاسفانه علی رغم تقسیم بندی تصادفی سوژه ها بین گروه ها گاهی بصورت کاملا شانسی اختلافات بارزی بین برخی گروه ها از نظر پاره‌ای از عوامل زمینه ای بروز میکند. هر چه تعداد این عوامل زمینه ای مورد مقایسه بیشتر باشد احتمال بروز این رویداد ناخواسته و ناجور بیشتر میشود و سبب اختلال کلی در نتایج کارآزمائی و تفسی آنها میگردد. احتمال بروز این پدیده بستگی تام به اندازه نمونه دارد و در نمونه های کوچک این احتمال بالا است. +یکی از روشهای جلوگیری از این پدیده استفاده از مینی میزیشن است که متاسفانه هنوز بخوبی در بین محققان علوم بالینی بخوبی شناخته شده نیست. در این روش تقسیم بندی سوژه ها بین گروه ها فقط بر مبنای شانس خالص نیست، بلکه قدری نیز در جهت متعادل سازی گروه ها از نظر عوامل زمینه ای مهم می باشد، بطوری که با پیشرفت کار کارآزمائی و افزوده شدن هرچه بیشتر سوژه ها اختلاف بین عوامل پایه مشخص شده بتدریج کمتر و کمتر شده و گروه ها از نظر عوامل زمینه ای کاملا قابل مقایسه می گردند. در این روش پژوهشگر از ابتدا مشخص میکند که کدام عوامل زمینه ای از نظر یکسان سازی بین گروه ها مهم می باشند. سپس با استفاده از فرایندی بنام مینی میزیشن تقسیم بندی سوژه ها بین گروه ها بیشتر به گروه‌هائی است که سبب بالانس رو به رشد تعادل بیم آنها گردد. البته در این عمل تقسیم بندی قدری نیز شانس دخالت داده میشود. بدین معنی که سوژه ها با احتمال بیشتری به گروهی داده میشوند که تعادل بیشتری ایجاد میکنند. +متاسفانه فرایند مینی میزیشن پیچیدگی محاسباتی خاصی دارد که مشمول ذخیره داده های قبلی بصورت فراوانی سوژه ها در گروه ها بر حسب لایه های مختلف عوامل زمینه ای می باشد. همچنین پس از ورود هر سوژه به کارآزمائی این داده ها دوباره باید بروزرسانی گشته و برای سوژه بعدی آماده باشند. انجام تمامی این اقدامات بدون استفاده از برنامه های کامپیوتری بسیار مشکل و مستعد خطا می باشد و به همین خاطر برنامه های سرویس های آنلاین گوناگونی تا بحال برای انجام مینی میزیشن ساخته شده است. برخی از این برنامه ها عبارتند از Minim, MinimPy, QMinim و غیره. diff --git a/minimpy2.pro b/minimpy2.pro new file mode 100755 index 0000000..5e63802 --- /dev/null +++ b/minimpy2.pro @@ -0,0 +1,3 @@ +SOURCES = db.py enrol_form.py factor_levels_dialog.py factor_levels.py freq_table.py main_window.py ui_main_window.py minim.py mnd.py model.py my_random.py run_test.py trial.py config.py config_dialog.py +TRANSLATIONS = fa_IR.ts +FORMS = factor_levels_dialog.ui ui_main_window.ui config_dialog.ui diff --git a/mnd.py b/mnd.py new file mode 100755 index 0000000..0c20821 --- /dev/null +++ b/mnd.py @@ -0,0 +1,162 @@ +import pickle + +from trial import Trial + + +class Mnd: + def __init__(self, filename): + self.filename = filename + self.data = None + + def export_data(self, trial_id, db): + trial_setting = db.get_trial_setting(trial_id) + trial = Trial(trial_setting) + subjects = db.get_subjects(trial_id) + ss = 30 + if len(subjects) > 30: + ss = len(subjects) + 10 + ui_pool = list(range(ss)) + for subject in subjects: + if subject[2] in ui_pool: + ui_pool.remove(subject[2]) + while len(ui_pool) > (ss - len(subjects)): + ui_pool.pop() + treatments = db.read_treatments(trial_id) + groups = [] + for treatment in treatments: + group = {'name': treatment[1], 'allocation_ratio': treatment[2]} + groups.append(group) + factors = db.read_factors(trial_id) + variables = [] + for factor in factors: + factor_levels = db.factor_levels(factor[0]) + titles = [factor_level[1] for factor_level in factor_levels] + factor.append([factor_level[0] for factor_level in factor_levels]) + levels = ','.join(titles) + variable = {'name': factor[1], 'weight': factor[2], 'levels': levels} + variables.append(variable) + allocations = [] + for subject in subjects: + allocation = {'UI': subject[2]} + for treatment_index, treatment in enumerate(treatments): + if treatment[0] == subject[1]: + allocation['allocation'] = treatment_index + break + subject_levels = db.get_subject_levels(subject[0]) + levels = [] + for subject_level in subject_levels: + levels.append(self.get_factor_level_index(factors, subject_level)) + allocation['levels'] = levels + allocations.append(allocation) + if not db.has_preload(trial_id): + initial_freq_table = 0 + else: + preload = db.get_preload(trial_id) + initial_freq_table = [[[preload[(t[0], factor[0], level_id)] for level_id in factor[-1]] for factor in factors] for t in treatments] + self.data = {'trial_title': trial.title, + 'trial_description': trial.title, + 'trial_properties': [], + 'high_prob': trial.base_prob, + 'prob_method': trial.prob_method, + 'distance_measure': trial.dist_method, + 'ui_pool': ui_pool, + 'sample_size': ss, + 'groups': groups, + 'variables': variables, + 'allocations': allocations, + 'initial_freq_table': initial_freq_table} + fp = open(self.filename, 'wb') + pickle.dump(self.data, fp, protocol=2) + fp.flush() + fp.close() + return True + + def get_factor_level_index(self, factors, subject_level): + for factor_index, factor in enumerate(factors): + if factor[0] == subject_level[0]: + for factor_level_index, factor_level in enumerate(factor[-1]): + if factor_level == subject_level[1]: + return factor_level_index + + def data_file_valid(self): + try: + fp = open(self.filename, 'rb') + self.data = pickle.load(fp) + self.trial_title = self.data['trial_title'] + self.allocations = self.data['allocations'] + self.high_prob = self.data['high_prob'] + self.initial_freq_table = self.data['initial_freq_table'] + self.prob_method = self.data['prob_method'] + self.distance_measure = self.data['distance_measure'] + self.groups = self.data['groups'] + self.variables = self.data['variables'] + return True + except: + return False + + def get_detail(self): + det = [] + det.append('trial_title: {}'.format(self.trial_title)) + treatments = [] + for group in self.groups: + treatments.append(group['name']) + det.append('Treatments: ({})'.format(', '.join(treatments))) + factors = [] + for variable in self.variables: + factors.append('{}({})'.format(variable['name'], variable['levels'])) + det.append('Factors: [{}]'.format(', '.join(factors))) + det.append('{} Subjects'.format(len(self.allocations))) + if self.initial_freq_table: + det.append('Trial has preload') + return '\n'.join(det) + + def import_data(self, db): + trial_id = db.insert_trial(self.trial_title) + treatments = [] + for group in self.groups: + treatment_id = db.insert_treatment(trial_id, group['name']) + treatments.append(treatment_id) + factors = [] + for variable in self.variables: + factor_id = db.insert_factor(trial_id, variable['name']) + factor = [factor_id] + level_ids = [] + levels = variable['levels'].split(',') + for level in levels: + level_id = db.insert_level(trial_id, factor_id, level) + level_ids.append(level_id) + factor.append(level_ids) + factors.append(factor) + all_numeric = True + for allocation in self.allocations: + if str(allocation['UI']).isnumeric(): + continue + all_numeric = False + break + id_value = 0 + for allocation in self.allocations: + treatment_index = allocation['allocation'] + treatment_id = treatments[treatment_index] + identifier_value = allocation['UI'] if all_numeric else id_value + if not all_numeric: + id_value += 1 + subject_id = db.insert_subject(identifier_value, treatment_id, trial_id) + levels = allocation['levels'] + subject_levels = [] + for factor_index, level_index in enumerate(levels): + factor_id = factors[factor_index][0] + level_id = factors[factor_index][1][level_index] + subject_levels.append((factor_id, level_id)) + db.insert_subject_levels(trial_id, subject_id, subject_levels) + if not self.initial_freq_table: + return trial_id + preload = {} + for treatment_index, row in enumerate(self.initial_freq_table): + t = treatments[treatment_index] + for factor_index, factor in enumerate(row): + f = factors[factor_index][0] + for level_index, count in enumerate(factor): + lv = factors[factor_index][1][level_index] + preload[(t, f, lv)] = count + db.save_preload(trial_id, preload) + return trial_id \ No newline at end of file diff --git a/model.py b/model.py new file mode 100755 index 0000000..922087c --- /dev/null +++ b/model.py @@ -0,0 +1,24 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +class Model(object): + BCM = 0 + NM = 1 + + MB = 0 + rng = 1 + SD = 2 + var = 3 + + def __init__(self): + self.allocations = None + self.prob_method = self.BCM + self.distance_measure = self.MB + self.high_prob = 0.7 + self.min_group = 0 + self.groups = [0, 1, 2] + self.variables = [[0, 1], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + self.variables_weight = [1, 1] + self.allocation_ratio = [1, 2, 3] + self.arms_weight = 1.0 + diff --git a/my_random.py b/my_random.py new file mode 100755 index 0000000..794d3c3 --- /dev/null +++ b/my_random.py @@ -0,0 +1,46 @@ +import random +import secrets + +SECRETS = 'secrets' +RANDOM = 'random' + + +class Random: + __instance = None + + @staticmethod + def get_instance(name): + if Random.__instance is None: + Random(name) + return Random.__instance + + def __init__(self, name): + if Random.__instance is not None: + raise Exception("This class is a singleton!") + else: + if name == RANDOM: + self.engine = random + else: + self.engine = secrets + self.name = name + Random.__instance = self + + def seed(self, seed): + if self.name == SECRETS: + return + self.engine.seed(seed) + + def randint(self, a, b): + a, b = int(a), int(b) + if self.name == RANDOM: + return self.engine.randint(a, b) + return a + self.engine.randbelow(b - a + 1) + + def choice(self, seq): + return self.engine.choice(seq) + + def uniform(self, a, b): + a, b = int(a), int(b) + if self.name == RANDOM: + return self.engine.uniform(a, b) + return self.engine.SystemRandom().uniform(a, b) \ No newline at end of file diff --git a/run_test.py b/run_test.py new file mode 100755 index 0000000..0487797 --- /dev/null +++ b/run_test.py @@ -0,0 +1,41 @@ +from statistics import NormalDist +import sys + + +class RunTest: + def __init__(self, sequence, v1, v2): + self.sequence = sequence + self.v1, self.v2 = v1, v2 + + def get_p(self): + n1 = self.sequence.count(self.v1) + n2 = self.sequence.count(self.v2) + if n1 == 0 or n2 == 0: + return None, None + r_bar = 1.0 + (2.0 * n1 * n2) / (n1 + n2) + sd = (2 * n1 * n2 * (2 * n1 * n2 - n1 -n2)) / (((n1 + n2) ** 2) * (n1 + n2 - 1)) + r = self.get_runs() + z = (r - r_bar) / sd + p = 2 * (1 - NormalDist().cdf(z)) + if p > 1.0: + p = 1.0 + return z, p + + def get_runs(self): + prev_value = None + i = 0 + runs = 0 + while i < len(self.sequence): + cur_vale = self.sequence[i] + if prev_value != cur_vale: + runs += 1 + i += 1 + prev_value = cur_vale + return runs + + +def get_random_sequence(min, max, random): + mn = 10**min + mx = 10**max + n = random.randint(mn, mx) + return bin(n)[2:] diff --git a/trial.py b/trial.py new file mode 100755 index 0000000..215fafb --- /dev/null +++ b/trial.py @@ -0,0 +1,76 @@ +import math + +BIASED_COIN = 0 +NAIVE = 1 +MARGINAL_BALANCE = 0 +RANGE = 1 +SD = 2 +VARIANCE = 3 +NUMERIC = 0 +ALPHA = 1 +MIX = 2 +SEQUENTIAL = 0 +RANDOM = 1 +LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +NUMBER_LETTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + +class Trial: + def __init__(self, setting): + self.id = setting.value('id') + self.title = setting.value('title') + self.code = setting.value('code') + self.created = setting.value('created') + self.modified = setting.value('modified') + self.prob_method = setting.value('prob_method') + self.base_prob = setting.value('base_prob') + self.dist_method = setting.value('dist_method') + self.identifier_type = setting.value('identifier_type') + self.identifier_order = setting.value('identifier_order') + self.identifier_length = setting.value('identifier_length') + self.recycle_ids = setting.value('recycle_ids') + self.new_subject_random = setting.value('new_subject_random') + self.arms_weight = setting.value('arms_weight') + + def get_max_sample_size(self): + base = 10 + if self.identifier_type == ALPHA: + base = 26 + elif self.identifier_type == MIX: + base = 36 + ss = math.pow(base, self.identifier_length) + return ss, self.format_unit(ss) + + def format_unit(self, size): + power = 1000 + n = 0 + power_labels = {0: '', 1: ' K', 2: ' M', 3: ' G', 4: ' T', 5: ' P'} + while size > power: + size //= power + n += 1 + size = int(size) + return '{}{}'.format(size, power_labels[n]) + + def format_subject_identifier(self, value): + length = int(self.identifier_length) + if self.identifier_type == NUMERIC: + return str(value).zfill(length) + base = [10, 26, 36][self.identifier_type] + z = ['0', 'A', '0'][self.identifier_type] + lst = number_to_base(value, base) + w = ['', LETTERS, NUMBER_LETTERS][self.identifier_type] + lst = ''.join(list(map(lambda x: w[x], lst))) + lst = lst.zfill(length) + lst = lst.replace('0', z) + return lst + + +def number_to_base(n, b): + if n == 0: + return [0] + digits = [] + while n: + digits.append(int(n % b)) + n //= b + return digits[::-1] + + diff --git a/ui_about_minimisation.py b/ui_about_minimisation.py new file mode 100755 index 0000000..e56fdba --- /dev/null +++ b/ui_about_minimisation.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'ui_about_minimisation.ui' +## +## Created by: Qt User Interface Compiler version 5.14.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject, + QObject, QPoint, QRect, QSize, QTime, QUrl, Qt) +from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, + QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter, + QPixmap, QRadialGradient) +from PySide2.QtWidgets import * + + +class Ui_AboutMinimisationDialog(object): + def setupUi(self, AboutMinimisationDialog): + if not AboutMinimisationDialog.objectName(): + AboutMinimisationDialog.setObjectName(u"AboutMinimisationDialog") + AboutMinimisationDialog.resize(400, 300) + self.verticalLayout = QVBoxLayout(AboutMinimisationDialog) + self.verticalLayout.setObjectName(u"verticalLayout") + self.minimTextEdit = QPlainTextEdit(AboutMinimisationDialog) + self.minimTextEdit.setObjectName(u"minimTextEdit") + self.minimTextEdit.setReadOnly(True) + + self.verticalLayout.addWidget(self.minimTextEdit) + + self.closePushButton = QPushButton(AboutMinimisationDialog) + self.closePushButton.setObjectName(u"closePushButton") + + self.verticalLayout.addWidget(self.closePushButton) + + + self.retranslateUi(AboutMinimisationDialog) + + QMetaObject.connectSlotsByName(AboutMinimisationDialog) + # setupUi + + def retranslateUi(self, AboutMinimisationDialog): + self.minimTextEdit.setPlainText("") + self.closePushButton.setText(QCoreApplication.translate("AboutMinimisationDialog", u"Close", None)) + pass + # retranslateUi + diff --git a/ui_about_minimisation.ui b/ui_about_minimisation.ui new file mode 100755 index 0000000..e6dbee5 --- /dev/null +++ b/ui_about_minimisation.ui @@ -0,0 +1,35 @@ + + + AboutMinimisationDialog + + + + 0 + 0 + 400 + 300 + + + + + + + true + + + + + + + + + + Close + + + + + + + + diff --git a/ui_main_window.py b/ui_main_window.py new file mode 100755 index 0000000..36e5899 --- /dev/null +++ b/ui_main_window.py @@ -0,0 +1,579 @@ +# -*- coding: utf-8 -*- + +################################################################################ +## Form generated from reading UI file 'ui_main_window.ui' +## +## Created by: Qt User Interface Compiler version 5.14.2 +## +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import (QCoreApplication, QDate, QDateTime, QMetaObject, + QObject, QPoint, QRect, QSize, QTime, QUrl, Qt) +from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, + QFontDatabase, QIcon, QKeySequence, QLinearGradient, QPalette, QPainter, + QPixmap, QRadialGradient) +from PySide2.QtWidgets import * + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + if not MainWindow.objectName(): + MainWindow.setObjectName(u"MainWindow") + MainWindow.resize(802, 600) + self.actionQuit = QAction(MainWindow) + self.actionQuit.setObjectName(u"actionQuit") + self.actionNew = QAction(MainWindow) + self.actionNew.setObjectName(u"actionNew") + self.actionDelete = QAction(MainWindow) + self.actionDelete.setObjectName(u"actionDelete") + self.actionSave = QAction(MainWindow) + self.actionSave.setObjectName(u"actionSave") + self.actionLevels = QAction(MainWindow) + self.actionLevels.setObjectName(u"actionLevels") + self.actionHelp = QAction(MainWindow) + self.actionHelp.setObjectName(u"actionHelp") + self.actionImport = QAction(MainWindow) + self.actionImport.setObjectName(u"actionImport") + self.actionExport = QAction(MainWindow) + self.actionExport.setObjectName(u"actionExport") + self.actionConfig = QAction(MainWindow) + self.actionConfig.setObjectName(u"actionConfig") + self.actionAbout_Minimisation = QAction(MainWindow) + self.actionAbout_Minimisation.setObjectName(u"actionAbout_Minimisation") + self.actionAbout_MinimPy2 = QAction(MainWindow) + self.actionAbout_MinimPy2.setObjectName(u"actionAbout_MinimPy2") + self.actionAbout_Qt = QAction(MainWindow) + self.actionAbout_Qt.setObjectName(u"actionAbout_Qt") + self.centralwidget = QWidget(MainWindow) + self.centralwidget.setObjectName(u"centralwidget") + self.verticalLayout_2 = QVBoxLayout(self.centralwidget) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.tabWidget = QTabWidget(self.centralwidget) + self.tabWidget.setObjectName(u"tabWidget") + self.tab = QWidget() + self.tab.setObjectName(u"tab") + self.verticalLayout = QVBoxLayout(self.tab) + self.verticalLayout.setObjectName(u"verticalLayout") + self.tialsTableWidget = QTableWidget(self.tab) + self.tialsTableWidget.setObjectName(u"tialsTableWidget") + + self.verticalLayout.addWidget(self.tialsTableWidget) + + self.tabWidget.addTab(self.tab, "") + self.tab_2 = QWidget() + self.tab_2.setObjectName(u"tab_2") + self.verticalLayout_3 = QVBoxLayout(self.tab_2) + self.verticalLayout_3.setObjectName(u"verticalLayout_3") + self.trialFormLayout = QFormLayout() + self.trialFormLayout.setObjectName(u"trialFormLayout") + self.trialCreatedLabel = QLabel(self.tab_2) + self.trialCreatedLabel.setObjectName(u"trialCreatedLabel") + self.trialCreatedLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(0, QFormLayout.LabelRole, self.trialCreatedLabel) + + self.trialCreatedValueLabel = QLabel(self.tab_2) + self.trialCreatedValueLabel.setObjectName(u"trialCreatedValueLabel") + + self.trialFormLayout.setWidget(0, QFormLayout.FieldRole, self.trialCreatedValueLabel) + + self.trialModifiedLabel = QLabel(self.tab_2) + self.trialModifiedLabel.setObjectName(u"trialModifiedLabel") + self.trialModifiedLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(1, QFormLayout.LabelRole, self.trialModifiedLabel) + + self.trialModifiedValueLabel = QLabel(self.tab_2) + self.trialModifiedValueLabel.setObjectName(u"trialModifiedValueLabel") + + self.trialFormLayout.setWidget(1, QFormLayout.FieldRole, self.trialModifiedValueLabel) + + self.trialTitleLabel = QLabel(self.tab_2) + self.trialTitleLabel.setObjectName(u"trialTitleLabel") + self.trialTitleLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(2, QFormLayout.LabelRole, self.trialTitleLabel) + + self.trialTitleLineEdit = QLineEdit(self.tab_2) + self.trialTitleLineEdit.setObjectName(u"trialTitleLineEdit") + + self.trialFormLayout.setWidget(2, QFormLayout.FieldRole, self.trialTitleLineEdit) + + self.trialCodeLabel = QLabel(self.tab_2) + self.trialCodeLabel.setObjectName(u"trialCodeLabel") + self.trialCodeLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(3, QFormLayout.LabelRole, self.trialCodeLabel) + + self.trialCodeLineEdit = QLineEdit(self.tab_2) + self.trialCodeLineEdit.setObjectName(u"trialCodeLineEdit") + + self.trialFormLayout.setWidget(3, QFormLayout.FieldRole, self.trialCodeLineEdit) + + self.trialProbmethodLabel = QLabel(self.tab_2) + self.trialProbmethodLabel.setObjectName(u"trialProbmethodLabel") + self.trialProbmethodLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(4, QFormLayout.LabelRole, self.trialProbmethodLabel) + + self.trialProbMethodComboBox = QComboBox(self.tab_2) + self.trialProbMethodComboBox.addItem("") + self.trialProbMethodComboBox.addItem("") + self.trialProbMethodComboBox.setObjectName(u"trialProbMethodComboBox") + + self.trialFormLayout.setWidget(4, QFormLayout.FieldRole, self.trialProbMethodComboBox) + + self.trialBaseProbabilityLabel = QLabel(self.tab_2) + self.trialBaseProbabilityLabel.setObjectName(u"trialBaseProbabilityLabel") + self.trialBaseProbabilityLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(5, QFormLayout.LabelRole, self.trialBaseProbabilityLabel) + + self.horizontalLayout_6 = QHBoxLayout() + self.horizontalLayout_6.setObjectName(u"horizontalLayout_6") + self.baseProbLabel = QLabel(self.tab_2) + self.baseProbLabel.setObjectName(u"baseProbLabel") + self.baseProbLabel.setLineWidth(1) + + self.horizontalLayout_6.addWidget(self.baseProbLabel) + + self.trialBaseProbabilitySlider = QSlider(self.tab_2) + self.trialBaseProbabilitySlider.setObjectName(u"trialBaseProbabilitySlider") + self.trialBaseProbabilitySlider.setMinimum(10) + self.trialBaseProbabilitySlider.setOrientation(Qt.Horizontal) + + self.horizontalLayout_6.addWidget(self.trialBaseProbabilitySlider) + + + self.trialFormLayout.setLayout(5, QFormLayout.FieldRole, self.horizontalLayout_6) + + self.trialDistanceMethodLabel = QLabel(self.tab_2) + self.trialDistanceMethodLabel.setObjectName(u"trialDistanceMethodLabel") + self.trialDistanceMethodLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(6, QFormLayout.LabelRole, self.trialDistanceMethodLabel) + + self.trialDistanceMethodComboBox = QComboBox(self.tab_2) + self.trialDistanceMethodComboBox.addItem("") + self.trialDistanceMethodComboBox.addItem("") + self.trialDistanceMethodComboBox.addItem("") + self.trialDistanceMethodComboBox.addItem("") + self.trialDistanceMethodComboBox.setObjectName(u"trialDistanceMethodComboBox") + + self.trialFormLayout.setWidget(6, QFormLayout.FieldRole, self.trialDistanceMethodComboBox) + + self.trialIdentifierTypeLabel = QLabel(self.tab_2) + self.trialIdentifierTypeLabel.setObjectName(u"trialIdentifierTypeLabel") + self.trialIdentifierTypeLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(7, QFormLayout.LabelRole, self.trialIdentifierTypeLabel) + + self.trialIdentifierTypeComboBox = QComboBox(self.tab_2) + self.trialIdentifierTypeComboBox.addItem("") + self.trialIdentifierTypeComboBox.addItem("") + self.trialIdentifierTypeComboBox.addItem("") + self.trialIdentifierTypeComboBox.setObjectName(u"trialIdentifierTypeComboBox") + + self.trialFormLayout.setWidget(7, QFormLayout.FieldRole, self.trialIdentifierTypeComboBox) + + self.trialIdentifierOrderLabel = QLabel(self.tab_2) + self.trialIdentifierOrderLabel.setObjectName(u"trialIdentifierOrderLabel") + self.trialIdentifierOrderLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(8, QFormLayout.LabelRole, self.trialIdentifierOrderLabel) + + self.trialIentifierOrderComboBox = QComboBox(self.tab_2) + self.trialIentifierOrderComboBox.addItem("") + self.trialIentifierOrderComboBox.addItem("") + self.trialIentifierOrderComboBox.setObjectName(u"trialIentifierOrderComboBox") + + self.trialFormLayout.setWidget(8, QFormLayout.FieldRole, self.trialIentifierOrderComboBox) + + self.trialIdentifierLengthLabel = QLabel(self.tab_2) + self.trialIdentifierLengthLabel.setObjectName(u"trialIdentifierLengthLabel") + self.trialIdentifierLengthLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(9, QFormLayout.LabelRole, self.trialIdentifierLengthLabel) + + self.horizontalLayout_5 = QHBoxLayout() + self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") + self.trialIdentifierLengthSpinBox = QSpinBox(self.tab_2) + self.trialIdentifierLengthSpinBox.setObjectName(u"trialIdentifierLengthSpinBox") + self.trialIdentifierLengthSpinBox.setMinimum(3) + self.trialIdentifierLengthSpinBox.setMaximum(10) + + self.horizontalLayout_5.addWidget(self.trialIdentifierLengthSpinBox) + + self.max_sample_size_label = QLabel(self.tab_2) + self.max_sample_size_label.setObjectName(u"max_sample_size_label") + + self.horizontalLayout_5.addWidget(self.max_sample_size_label) + + + self.trialFormLayout.setLayout(9, QFormLayout.FieldRole, self.horizontalLayout_5) + + self.trialRecycleIdsLabel = QLabel(self.tab_2) + self.trialRecycleIdsLabel.setObjectName(u"trialRecycleIdsLabel") + self.trialRecycleIdsLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(10, QFormLayout.LabelRole, self.trialRecycleIdsLabel) + + self.trialRecycleIdsCheckBox = QCheckBox(self.tab_2) + self.trialRecycleIdsCheckBox.setObjectName(u"trialRecycleIdsCheckBox") + + self.trialFormLayout.setWidget(10, QFormLayout.FieldRole, self.trialRecycleIdsCheckBox) + + self.trialNewSubjectRandomLabel = QLabel(self.tab_2) + self.trialNewSubjectRandomLabel.setObjectName(u"trialNewSubjectRandomLabel") + self.trialNewSubjectRandomLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(11, QFormLayout.LabelRole, self.trialNewSubjectRandomLabel) + + self.trialNewSubjectRandomCheckBox = QCheckBox(self.tab_2) + self.trialNewSubjectRandomCheckBox.setObjectName(u"trialNewSubjectRandomCheckBox") + + self.trialFormLayout.setWidget(11, QFormLayout.FieldRole, self.trialNewSubjectRandomCheckBox) + + self.trialArmsWeightLabel = QLabel(self.tab_2) + self.trialArmsWeightLabel.setObjectName(u"trialArmsWeightLabel") + self.trialArmsWeightLabel.setStyleSheet(u"color: blue;") + + self.trialFormLayout.setWidget(12, QFormLayout.LabelRole, self.trialArmsWeightLabel) + + self.horizontalLayout_9 = QHBoxLayout() + self.horizontalLayout_9.setObjectName(u"horizontalLayout_9") + self.armWeightLabel = QLabel(self.tab_2) + self.armWeightLabel.setObjectName(u"armWeightLabel") + + self.horizontalLayout_9.addWidget(self.armWeightLabel) + + self.trialArmsWeightDoubleSlider = QSlider(self.tab_2) + self.trialArmsWeightDoubleSlider.setObjectName(u"trialArmsWeightDoubleSlider") + self.trialArmsWeightDoubleSlider.setMinimum(1) + self.trialArmsWeightDoubleSlider.setOrientation(Qt.Horizontal) + + self.horizontalLayout_9.addWidget(self.trialArmsWeightDoubleSlider) + + + self.trialFormLayout.setLayout(12, QFormLayout.FieldRole, self.horizontalLayout_9) + + + self.verticalLayout_3.addLayout(self.trialFormLayout) + + self.tabWidget.addTab(self.tab_2, "") + self.tab_3 = QWidget() + self.tab_3.setObjectName(u"tab_3") + self.verticalLayout_4 = QVBoxLayout(self.tab_3) + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.treatmentTableWidget = QTableWidget(self.tab_3) + self.treatmentTableWidget.setObjectName(u"treatmentTableWidget") + + self.verticalLayout_4.addWidget(self.treatmentTableWidget) + + self.tabWidget.addTab(self.tab_3, "") + self.tab_4 = QWidget() + self.tab_4.setObjectName(u"tab_4") + self.verticalLayout_5 = QVBoxLayout(self.tab_4) + self.verticalLayout_5.setObjectName(u"verticalLayout_5") + self.factorTableWidget = QTableWidget(self.tab_4) + self.factorTableWidget.setObjectName(u"factorTableWidget") + + self.verticalLayout_5.addWidget(self.factorTableWidget) + + self.tabWidget.addTab(self.tab_4, "") + self.tab_5 = QWidget() + self.tab_5.setObjectName(u"tab_5") + self.verticalLayout_6 = QVBoxLayout(self.tab_5) + self.verticalLayout_6.setObjectName(u"verticalLayout_6") + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.frequenciesCheckBox = QCheckBox(self.tab_5) + self.frequenciesCheckBox.setObjectName(u"frequenciesCheckBox") + self.frequenciesCheckBox.setChecked(False) + + self.horizontalLayout.addWidget(self.frequenciesCheckBox) + + self.preloadCheckBox = QCheckBox(self.tab_5) + self.preloadCheckBox.setObjectName(u"preloadCheckBox") + + self.horizontalLayout.addWidget(self.preloadCheckBox) + + self.editPreloadCheckBox = QCheckBox(self.tab_5) + self.editPreloadCheckBox.setObjectName(u"editPreloadCheckBox") + self.editPreloadCheckBox.setEnabled(False) + + self.horizontalLayout.addWidget(self.editPreloadCheckBox) + + self.convertPreloadButton = QPushButton(self.tab_5) + self.convertPreloadButton.setObjectName(u"convertPreloadButton") + + self.horizontalLayout.addWidget(self.convertPreloadButton) + + self.convertWarningCheckBox = QCheckBox(self.tab_5) + self.convertWarningCheckBox.setObjectName(u"convertWarningCheckBox") + + self.horizontalLayout.addWidget(self.convertWarningCheckBox) + + + self.verticalLayout_6.addLayout(self.horizontalLayout) + + self.scrollArea = QScrollArea(self.tab_5) + self.scrollArea.setObjectName(u"scrollArea") + self.scrollArea.setWidgetResizable(True) + self.scrollAreaWidgetContents = QWidget() + self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") + self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 760, 422)) + self.horizontalLayout_2 = QHBoxLayout(self.scrollAreaWidgetContents) + self.horizontalLayout_2.setSpacing(0) + self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) + self.freqGridLayout = QGridLayout() + self.freqGridLayout.setSpacing(1) + self.freqGridLayout.setObjectName(u"freqGridLayout") + + self.horizontalLayout_2.addLayout(self.freqGridLayout) + + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + + self.verticalLayout_6.addWidget(self.scrollArea) + + self.tabWidget.addTab(self.tab_5, "") + self.tab_6 = QWidget() + self.tab_6.setObjectName(u"tab_6") + self.verticalLayout_7 = QVBoxLayout(self.tab_6) + self.verticalLayout_7.setObjectName(u"verticalLayout_7") + self.subjectProgressBar = QProgressBar(self.tab_6) + self.subjectProgressBar.setObjectName(u"subjectProgressBar") + self.subjectProgressBar.setValue(0) + + self.verticalLayout_7.addWidget(self.subjectProgressBar) + + self.subjectTableWidget = QTableWidget(self.tab_6) + self.subjectTableWidget.setObjectName(u"subjectTableWidget") + self.subjectTableWidget.setSortingEnabled(True) + + self.verticalLayout_7.addWidget(self.subjectTableWidget) + + self.tabWidget.addTab(self.tab_6, "") + self.tab_7 = QWidget() + self.tab_7.setObjectName(u"tab_7") + self.verticalLayout_8 = QVBoxLayout(self.tab_7) + self.verticalLayout_8.setObjectName(u"verticalLayout_8") + self.horizontalLayout_3 = QHBoxLayout() + self.horizontalLayout_3.setObjectName(u"horizontalLayout_3") + self.mean_balance_label = QLabel(self.tab_7) + self.mean_balance_label.setObjectName(u"mean_balance_label") + + self.horizontalLayout_3.addWidget(self.mean_balance_label) + + self.balanceProgressBar = QProgressBar(self.tab_7) + self.balanceProgressBar.setObjectName(u"balanceProgressBar") + self.balanceProgressBar.setMaximum(100) + self.balanceProgressBar.setValue(0) + self.balanceProgressBar.setTextVisible(True) + + self.horizontalLayout_3.addWidget(self.balanceProgressBar) + + + self.verticalLayout_8.addLayout(self.horizontalLayout_3) + + self.balanceTreeWidget = QTreeWidget(self.tab_7) + __qtreewidgetitem = QTreeWidgetItem() + __qtreewidgetitem.setText(0, u"1"); + self.balanceTreeWidget.setHeaderItem(__qtreewidgetitem) + self.balanceTreeWidget.setObjectName(u"balanceTreeWidget") + + self.verticalLayout_8.addWidget(self.balanceTreeWidget) + + self.horizontalLayout_4 = QHBoxLayout() + self.horizontalLayout_4.setObjectName(u"horizontalLayout_4") + self.randomness_label = QLabel(self.tab_7) + self.randomness_label.setObjectName(u"randomness_label") + + self.horizontalLayout_4.addWidget(self.randomness_label) + + self.randomnessProgressBar = QProgressBar(self.tab_7) + self.randomnessProgressBar.setObjectName(u"randomnessProgressBar") + self.randomnessProgressBar.setValue(0) + + self.horizontalLayout_4.addWidget(self.randomnessProgressBar) + + + self.verticalLayout_8.addLayout(self.horizontalLayout_4) + + self.tabWidget.addTab(self.tab_7, "") + self.tab_8 = QWidget() + self.tab_8.setObjectName(u"tab_8") + self.verticalLayout_9 = QVBoxLayout(self.tab_8) + self.verticalLayout_9.setObjectName(u"verticalLayout_9") + self.textEdit = QTextEdit(self.tab_8) + self.textEdit.setObjectName(u"textEdit") + + self.verticalLayout_9.addWidget(self.textEdit) + + self.showAtStartCheckBox = QCheckBox(self.tab_8) + self.showAtStartCheckBox.setObjectName(u"showAtStartCheckBox") + + self.verticalLayout_9.addWidget(self.showAtStartCheckBox) + + self.tabWidget.addTab(self.tab_8, "") + + self.verticalLayout_2.addWidget(self.tabWidget) + + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QMenuBar(MainWindow) + self.menubar.setObjectName(u"menubar") + self.menubar.setGeometry(QRect(0, 0, 802, 22)) + self.menuFile = QMenu(self.menubar) + self.menuFile.setObjectName(u"menuFile") + self.menuHelp = QMenu(self.menubar) + self.menuHelp.setObjectName(u"menuHelp") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QStatusBar(MainWindow) + self.statusbar.setObjectName(u"statusbar") + MainWindow.setStatusBar(self.statusbar) + self.toolBar = QToolBar(MainWindow) + self.toolBar.setObjectName(u"toolBar") + self.toolBar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) + MainWindow.addToolBar(Qt.TopToolBarArea, self.toolBar) + + self.menubar.addAction(self.menuFile.menuAction()) + self.menubar.addAction(self.menuHelp.menuAction()) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionImport) + self.menuFile.addAction(self.actionExport) + self.menuFile.addSeparator() + self.menuFile.addAction(self.actionConfig) + self.menuFile.addAction(self.actionQuit) + self.menuHelp.addAction(self.actionHelp) + self.menuHelp.addSeparator() + self.menuHelp.addAction(self.actionAbout_Minimisation) + self.menuHelp.addAction(self.actionAbout_MinimPy2) + self.menuHelp.addAction(self.actionAbout_Qt) + self.toolBar.addAction(self.actionNew) + self.toolBar.addAction(self.actionDelete) + self.toolBar.addAction(self.actionLevels) + self.toolBar.addAction(self.actionSave) + self.toolBar.addAction(self.actionConfig) + self.toolBar.addAction(self.actionHelp) + self.toolBar.addAction(self.actionAbout_MinimPy2) + self.toolBar.addAction(self.actionQuit) + + self.retranslateUi(MainWindow) + + self.tabWidget.setCurrentIndex(7) + + + QMetaObject.connectSlotsByName(MainWindow) + # setupUi + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) + self.actionQuit.setText(QCoreApplication.translate("MainWindow", u"Exit", None)) + self.actionNew.setText(QCoreApplication.translate("MainWindow", u"Add", None)) + self.actionDelete.setText(QCoreApplication.translate("MainWindow", u"Delete", None)) + self.actionSave.setText(QCoreApplication.translate("MainWindow", u"Save", None)) +#if QT_CONFIG(tooltip) + self.actionSave.setToolTip(QCoreApplication.translate("MainWindow", u"Save", None)) +#endif // QT_CONFIG(tooltip) + self.actionLevels.setText(QCoreApplication.translate("MainWindow", u"Levels", None)) +#if QT_CONFIG(tooltip) + self.actionLevels.setToolTip(QCoreApplication.translate("MainWindow", u"manage factor levels", None)) +#endif // QT_CONFIG(tooltip) + self.actionHelp.setText(QCoreApplication.translate("MainWindow", u"Help", None)) +#if QT_CONFIG(tooltip) + self.actionHelp.setToolTip(QCoreApplication.translate("MainWindow", u"displays help", None)) +#endif // QT_CONFIG(tooltip) + self.actionImport.setText(QCoreApplication.translate("MainWindow", u"Import", None)) + self.actionExport.setText(QCoreApplication.translate("MainWindow", u"Export", None)) + self.actionConfig.setText(QCoreApplication.translate("MainWindow", u"Config", None)) +#if QT_CONFIG(tooltip) + self.actionConfig.setToolTip(QCoreApplication.translate("MainWindow", u"Application config", None)) +#endif // QT_CONFIG(tooltip) + self.actionAbout_Minimisation.setText(QCoreApplication.translate("MainWindow", u"What is Minimisation", None)) + self.actionAbout_MinimPy2.setText(QCoreApplication.translate("MainWindow", u"About", None)) + self.actionAbout_Qt.setText(QCoreApplication.translate("MainWindow", u"About Qt", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), QCoreApplication.translate("MainWindow", u"Trials", None)) + self.trialCreatedLabel.setText(QCoreApplication.translate("MainWindow", u"Created", None)) + self.trialModifiedLabel.setText(QCoreApplication.translate("MainWindow", u"Modified", None)) + self.trialTitleLabel.setText(QCoreApplication.translate("MainWindow", u"Trial title", None)) + self.trialCodeLabel.setText(QCoreApplication.translate("MainWindow", u"Trial code", None)) + self.trialProbmethodLabel.setText(QCoreApplication.translate("MainWindow", u"Probability method", None)) + self.trialProbMethodComboBox.setItemText(0, QCoreApplication.translate("MainWindow", u"Biased coin", None)) + self.trialProbMethodComboBox.setItemText(1, QCoreApplication.translate("MainWindow", u"Naive", None)) + + self.trialBaseProbabilityLabel.setText(QCoreApplication.translate("MainWindow", u"Base probability", None)) + self.baseProbLabel.setText(QCoreApplication.translate("MainWindow", u"0.70", None)) + self.trialDistanceMethodLabel.setText(QCoreApplication.translate("MainWindow", u"Distance method", None)) + self.trialDistanceMethodComboBox.setItemText(0, QCoreApplication.translate("MainWindow", u"Marginal balance", None)) + self.trialDistanceMethodComboBox.setItemText(1, QCoreApplication.translate("MainWindow", u"Range", None)) + self.trialDistanceMethodComboBox.setItemText(2, QCoreApplication.translate("MainWindow", u"Standard deviation", None)) + self.trialDistanceMethodComboBox.setItemText(3, QCoreApplication.translate("MainWindow", u"Variance", None)) + + self.trialIdentifierTypeLabel.setText(QCoreApplication.translate("MainWindow", u"Identifier type", None)) + self.trialIdentifierTypeComboBox.setItemText(0, QCoreApplication.translate("MainWindow", u"Numeric", None)) + self.trialIdentifierTypeComboBox.setItemText(1, QCoreApplication.translate("MainWindow", u"Alpha", None)) + self.trialIdentifierTypeComboBox.setItemText(2, QCoreApplication.translate("MainWindow", u"Alphanumeric", None)) + + self.trialIdentifierOrderLabel.setText(QCoreApplication.translate("MainWindow", u"Identifier order", None)) + self.trialIentifierOrderComboBox.setItemText(0, QCoreApplication.translate("MainWindow", u"Sequential", None)) + self.trialIentifierOrderComboBox.setItemText(1, QCoreApplication.translate("MainWindow", u"Random", None)) + + self.trialIdentifierLengthLabel.setText(QCoreApplication.translate("MainWindow", u"Identifier length", None)) + self.max_sample_size_label.setText(QCoreApplication.translate("MainWindow", u"TextLabel", None)) + self.trialRecycleIdsLabel.setText(QCoreApplication.translate("MainWindow", u"Recycle ids", None)) + self.trialNewSubjectRandomLabel.setText(QCoreApplication.translate("MainWindow", u"New subject random", None)) + self.trialArmsWeightLabel.setText(QCoreApplication.translate("MainWindow", u"Arms weight", None)) + self.armWeightLabel.setText(QCoreApplication.translate("MainWindow", u"1.0", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), QCoreApplication.translate("MainWindow", u"Setting", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), QCoreApplication.translate("MainWindow", u"Treatments", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), QCoreApplication.translate("MainWindow", u"Factors", None)) + self.frequenciesCheckBox.setText(QCoreApplication.translate("MainWindow", u"Frequencies", None)) + self.preloadCheckBox.setText(QCoreApplication.translate("MainWindow", u"Preload", None)) + self.editPreloadCheckBox.setText(QCoreApplication.translate("MainWindow", u"Edit", None)) + self.convertPreloadButton.setText(QCoreApplication.translate("MainWindow", u"Convert to preload", None)) + self.convertWarningCheckBox.setText(QCoreApplication.translate("MainWindow", u"I know what I'm doing", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_5), QCoreApplication.translate("MainWindow", u"Frequencies", None)) + self.subjectProgressBar.setFormat(QCoreApplication.translate("MainWindow", u"%v/%m", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_6), QCoreApplication.translate("MainWindow", u"Subjects", None)) + self.mean_balance_label.setText(QCoreApplication.translate("MainWindow", u"Oveall Balance", None)) + self.randomness_label.setText(QCoreApplication.translate("MainWindow", u"Randomness", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_7), QCoreApplication.translate("MainWindow", u"Balance", None)) + self.textEdit.setHtml(QCoreApplication.translate("MainWindow", u"\n" +"\n" +"

How to do minimisation using MinimPy2?

\n" +"

A quick tutorial

\n" +"


\n" +"

1 - Go to Trials tab and select a trial as current to work on it. Define a new trial if there is no trial

\n" +"


\n" +"

2 - Go to Setting tab to view / edit trial setting. Usually the default setting is good to go

\n" +"


\n" +"

3 - Go to Treatments tab to define at lease two treatments

\n" +"


\n" +"

4 - Go to Factors tab to define at lease one factor

\n" +"


\n" +"

5 - Define at least two levels for each factor

\n" +"


\n" +"

6 - Now the trial is ready for subject enrollment

\n" +"


\n" +"

7 - Go to subject tab and click on the Add button in the toolbar

\n" +"


\n" +"

8 - The enroll window will appear. Select a level for each factor and click enroll

\n" +"


\n" +"

9 - Subject will enroll to one of treatment groups by the minimisation algorhythm

\n" +"


\n" +"

10 - If you want to define a preload go to Frequencies tab

", None)) + self.showAtStartCheckBox.setText(QCoreApplication.translate("MainWindow", u"Don't show at startup", None)) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_8), QCoreApplication.translate("MainWindow", u"Start", None)) + self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None)) + self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None)) + self.toolBar.setWindowTitle(QCoreApplication.translate("MainWindow", u"toolBar", None)) + # retranslateUi + diff --git a/ui_main_window.ui b/ui_main_window.ui new file mode 100755 index 0000000..0b2a2f1 --- /dev/null +++ b/ui_main_window.ui @@ -0,0 +1,705 @@ + + + MainWindow + + + + 0 + 0 + 802 + 600 + + + + MainWindow + + + + + + + 7 + + + + Trials + + + + + + + + + + Setting + + + + + + + + color: blue; + + + Created + + + + + + + + + + color: blue; + + + Modified + + + + + + + + + + color: blue; + + + Trial title + + + + + + + + + + color: blue; + + + Trial code + + + + + + + + + + color: blue; + + + Probability method + + + + + + + + Biased coin + + + + + Naive + + + + + + + + color: blue; + + + Base probability + + + + + + + + + 1 + + + 0.70 + + + + + + + 10 + + + Qt::Horizontal + + + + + + + + + color: blue; + + + Distance method + + + + + + + + Marginal balance + + + + + Range + + + + + Standard deviation + + + + + Variance + + + + + + + + color: blue; + + + Identifier type + + + + + + + + Numeric + + + + + Alpha + + + + + Alphanumeric + + + + + + + + color: blue; + + + Identifier order + + + + + + + + Sequential + + + + + Random + + + + + + + + color: blue; + + + Identifier length + + + + + + + + + 3 + + + 10 + + + + + + + TextLabel + + + + + + + + + color: blue; + + + Recycle ids + + + + + + + + + + color: blue; + + + New subject random + + + + + + + + + + color: blue; + + + Arms weight + + + + + + + + + 1.0 + + + + + + + 1 + + + Qt::Horizontal + + + + + + + + + + + + Treatments + + + + + + + + + + Factors + + + + + + + + + + Frequencies + + + + + + + + Frequencies + + + false + + + + + + + Preload + + + + + + + false + + + Edit + + + + + + + Convert to preload + + + + + + + I know what I'm doing + + + + + + + + + true + + + + + 0 + 0 + 760 + 422 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + + + + + + + + + + Subjects + + + + + + 0 + + + %v/%m + + + + + + + true + + + + + + + + Balance + + + + + + + + Oveall Balance + + + + + + + 100 + + + 0 + + + true + + + + + + + + + + 1 + + + + + + + + + + Randomness + + + + + + + 0 + + + + + + + + + + Start + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:20pt; font-weight:600;">How to do minimisation using MinimPy2?</span></p> +<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt; font-weight:600;">A quick tutorial</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">1 - Go to Trials tab and select a trial as current to work on it. Define a new trial if there is no trial</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">2 - Go to Setting tab to view / edit trial setting. Usually the default setting is good to go</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">3 - Go to Treatments tab to define at lease two treatments</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">4 - Go to Factors tab to define at lease one factor</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">5 - Define at least two levels for each factor</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">6 - Now the trial is ready for subject enrollment</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">7 - Go to subject tab and click on the Add button in the toolbar</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">8 - The enroll window will appear. Select a level for each factor and click enroll</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">9 - Subject will enroll to one of treatment groups by the minimisation algorhythm</p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">10 - If you want to define a preload go to Frequencies tab</p></body></html> + + + + + + + Don't show at startup + + + + + + + + + + + + + 0 + 0 + 802 + 22 + + + + + File + + + + + + + + + + + Help + + + + + + + + + + + + + + toolBar + + + Qt::ToolButtonTextUnderIcon + + + TopToolBarArea + + + false + + + + + + + + + + + + + Exit + + + + + Add + + + + + Delete + + + + + Save + + + Save + + + + + Levels + + + manage factor levels + + + + + Help + + + displays help + + + + + Import + + + + + Export + + + + + Config + + + Application config + + + + + What is Minimisation + + + + + About + + + + + About Qt + + + + + + -- 2.11.0