From b6ebf0a35c58fa7073ffeb65c24949555ec35411 Mon Sep 17 00:00:00 2001 From: i-jey Date: Mon, 7 Apr 2025 14:43:45 -0700 Subject: [PATCH 01/11] Add date, time, and storage temperature inputs to the experiment form. --- ulc_mm_package/QtGUI/form_gui.py | 61 +++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/ulc_mm_package/QtGUI/form_gui.py b/ulc_mm_package/QtGUI/form_gui.py index 95a34f1be..84c7f2b69 100644 --- a/ulc_mm_package/QtGUI/form_gui.py +++ b/ulc_mm_package/QtGUI/form_gui.py @@ -6,7 +6,6 @@ import sys - from PyQt5.QtWidgets import ( QApplication, QDialog, @@ -19,10 +18,11 @@ QDesktopWidget, ) from PyQt5.QtGui import QIcon -from PyQt5.QtCore import pyqtSignal +from PyQt5.QtCore import pyqtSignal, QDate, QTime from ulc_mm_package.scope_constants import EXPERIMENT_METADATA_KEYS from ulc_mm_package.image_processing.processing_constants import TARGET_FLOWRATE +from PyQt5.QtWidgets import QDateEdit, QTimeEdit from ulc_mm_package.QtGUI.gui_constants import ( ICON_PATH, SITE_LIST, @@ -48,6 +48,15 @@ def closeEvent(self, event): else: event.accept() + def _validate_temperature(self): + text = self.sample_storage_temp.text() + if not text.endswith(("C", "F")) or not text[:-1].isdigit(): + self.sample_storage_temp.setStyleSheet("border: 1px solid red;") + self.start_btn.setEnabled(False) + else: + self.sample_storage_temp.setStyleSheet("") + self.start_btn.setEnabled(True) + def _load_ui(self): self.setWindowTitle("Experiment form") @@ -78,6 +87,9 @@ def _load_ui(self): # Labels self.operator_lbl = QLabel("Operator ID") self.participant_lbl = QLabel("Non-identifying participant ID") + self.sample_collection_date_lbl = QLabel("Sample collection date") + self.sample_collection_time_lbl = QLabel("Sample collection time") + self.sample_storage_temp_lbl = QLabel("Sample storage temperature (°C or °F)") self.flowcell_lbl = QLabel("Flowcell ID") self.notes_lbl = QLabel("Other notes") self.site_lbl = QLabel("Site") @@ -87,6 +99,22 @@ def _load_ui(self): # Text boxes self.operator_val = QLineEdit() self.participant_val = QLineEdit() + + # Sample collection date + self.sample_collection_date = QDateEdit(QDate.currentDate()) + self.sample_collection_date.setCalendarPopup(True) + self.sample_collection_date.setDisplayFormat("yyyy-MMM-dd") + + # Sample collection time + self.sample_collection_time = QTimeEdit(QTime.currentTime()) + self.sample_collection_time.setDisplayFormat("hh:mm AP") + self.sample_collection_time.setCalendarPopup(True) + + # Sample storage temperature + self.sample_storage_temp = QLineEdit() + self.sample_storage_temp.setPlaceholderText("Enter temperature (e.g. 25C or 77F)") + self.sample_storage_temp.textChanged.connect(self._validate_temperature) + self.flowcell_val = QLineEdit() self.notes_val = QPlainTextEdit() @@ -107,21 +135,27 @@ def _load_ui(self): # Place widgets self.main_layout.addWidget(self.operator_lbl, 0, 0) self.main_layout.addWidget(self.participant_lbl, 1, 0) - self.main_layout.addWidget(self.flowcell_lbl, 2, 0) - self.main_layout.addWidget(self.site_lbl, 4, 0) - self.main_layout.addWidget(self.sample_lbl, 5, 0) - self.main_layout.addWidget(self.notes_lbl, 6, 0) - self.main_layout.addWidget(self.exit_btn, 8, 0) + self.main_layout.addWidget(self.sample_collection_date_lbl, 2, 0) + self.main_layout.addWidget(self.sample_collection_time_lbl, 3, 0) + self.main_layout.addWidget(self.sample_storage_temp_lbl, 4, 0) + self.main_layout.addWidget(self.flowcell_lbl, 5, 0) + self.main_layout.addWidget(self.site_lbl, 6, 0) + self.main_layout.addWidget(self.sample_lbl, 7, 0) + self.main_layout.addWidget(self.notes_lbl, 8, 0) + self.main_layout.addWidget(self.exit_btn, 9, 0) self.main_layout.addWidget(self.operator_val, 0, 1) self.main_layout.addWidget(self.participant_val, 1, 1) - self.main_layout.addWidget(self.flowcell_val, 2, 1) - self.main_layout.addWidget(self.site_val, 4, 1) - self.main_layout.addWidget(self.sample_val, 5, 1) - self.main_layout.addWidget(self.notes_val, 6, 1) - self.main_layout.addWidget(self.start_btn, 8, 1) + self.main_layout.addWidget(self.sample_collection_date, 2, 1) + self.main_layout.addWidget(self.sample_collection_time, 3, 1) + self.main_layout.addWidget(self.sample_storage_temp, 4, 1) + self.main_layout.addWidget(self.flowcell_val, 5, 1) + self.main_layout.addWidget(self.site_val, 6, 1) + self.main_layout.addWidget(self.sample_val, 7, 1) + self.main_layout.addWidget(self.notes_val, 8, 1) + self.main_layout.addWidget(self.start_btn, 9, 1) - self.main_layout.addWidget(self.msg_lbl, 7, 0, 1, 2) + self.main_layout.addWidget(self.msg_lbl, 10, 0, 1, 2) # Set the focus order self.operator_val.setFocus() @@ -156,6 +190,7 @@ def reset_parameters(self) -> None: if __name__ == "__main__": app = QApplication(sys.argv) gui = FormGUI() + gui.exit_btn.clicked.connect(gui.close) print(gui.get_form_input()) From 0963a23f2029458fc5e5a362e47d9b457b23ddb0 Mon Sep 17 00:00:00 2001 From: i-jey Date: Mon, 7 Apr 2025 14:45:16 -0700 Subject: [PATCH 02/11] formatting --- ulc_mm_package/QtGUI/form_gui.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ulc_mm_package/QtGUI/form_gui.py b/ulc_mm_package/QtGUI/form_gui.py index 84c7f2b69..0abda34fc 100644 --- a/ulc_mm_package/QtGUI/form_gui.py +++ b/ulc_mm_package/QtGUI/form_gui.py @@ -56,7 +56,7 @@ def _validate_temperature(self): else: self.sample_storage_temp.setStyleSheet("") self.start_btn.setEnabled(True) - + def _load_ui(self): self.setWindowTitle("Experiment form") @@ -112,7 +112,9 @@ def _load_ui(self): # Sample storage temperature self.sample_storage_temp = QLineEdit() - self.sample_storage_temp.setPlaceholderText("Enter temperature (e.g. 25C or 77F)") + self.sample_storage_temp.setPlaceholderText( + "Enter temperature (e.g. 25C or 77F)" + ) self.sample_storage_temp.textChanged.connect(self._validate_temperature) self.flowcell_val = QLineEdit() From 81dfcee54625fe55f6fb872c5397f5cef81c9ddb Mon Sep 17 00:00:00 2001 From: i-jey Date: Mon, 7 Apr 2025 16:11:06 -0700 Subject: [PATCH 03/11] Make the temperature unit case insensitive --- ulc_mm_package/QtGUI/form_gui.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ulc_mm_package/QtGUI/form_gui.py b/ulc_mm_package/QtGUI/form_gui.py index 0abda34fc..706b277e5 100644 --- a/ulc_mm_package/QtGUI/form_gui.py +++ b/ulc_mm_package/QtGUI/form_gui.py @@ -50,7 +50,7 @@ def closeEvent(self, event): def _validate_temperature(self): text = self.sample_storage_temp.text() - if not text.endswith(("C", "F")) or not text[:-1].isdigit(): + if not text.endswith(("C", "F", "c", "f")) or not text[:-1].isdigit(): self.sample_storage_temp.setStyleSheet("border: 1px solid red;") self.start_btn.setEnabled(False) else: @@ -168,6 +168,9 @@ def get_form_input(self) -> dict: "operator_id": self.operator_val.text(), "participant_id": self.participant_val.text(), "flowcell_id": self.flowcell_val.text(), + "sample_collection_date": self.sample_collection_date.text(), + "sample_collection_time": self.sample_collection_time.text(), + "sample_storage_temp": self.sample_storage_temp.text(), "target_flowrate": ( TARGET_FLOWRATE.name.capitalize(), TARGET_FLOWRATE.value, From a5b0d6eb01dd046bade610e4df1170c40d33212d Mon Sep 17 00:00:00 2001 From: i-jey Date: Mon, 7 Apr 2025 16:11:25 -0700 Subject: [PATCH 04/11] Add new form entries to the experiment metadata --- ulc_mm_package/scope_constants.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ulc_mm_package/scope_constants.py b/ulc_mm_package/scope_constants.py index 7db87fffd..3f1f7af8e 100644 --- a/ulc_mm_package/scope_constants.py +++ b/ulc_mm_package/scope_constants.py @@ -145,6 +145,9 @@ def IMG_HEIGHT(self) -> int: EXPERIMENT_METADATA_KEYS = [ "operator_id", "participant_id", + "sample_collection_date", + "sample_collection_time", + "sample_storage_temp", "flowcell_id", "target_flowrate", "site", From 7aaa9ccf0cce438f30f1412a110778ef0c6a1ff4 Mon Sep 17 00:00:00 2001 From: i-jey Date: Wed, 9 Apr 2025 10:42:52 -0700 Subject: [PATCH 05/11] A small convenience: automatically calculate the sample age based on the entered sample collection time and the current time that the form is submitted. --- ulc_mm_package/QtGUI/form_gui.py | 13 +++++++++++++ ulc_mm_package/scope_constants.py | 1 + 2 files changed, 14 insertions(+) diff --git a/ulc_mm_package/QtGUI/form_gui.py b/ulc_mm_package/QtGUI/form_gui.py index 706b277e5..4c87cbac0 100644 --- a/ulc_mm_package/QtGUI/form_gui.py +++ b/ulc_mm_package/QtGUI/form_gui.py @@ -164,12 +164,25 @@ def _load_ui(self): self.start_btn.setDefault(True) def get_form_input(self) -> dict: + # Determine the sample age from the current time and the sample collection time + current_date = QDate.currentDate() + current_time = QTime.currentTime() + + sample_date = self.sample_collection_date.date() + sample_time = self.sample_collection_time.time() + + date_diff_in_hours = sample_date.daysTo(current_date) * 24 + time_diff_in_hours = sample_time.secsTo(current_time) / 3600 + + sample_age_hours = date_diff_in_hours + time_diff_in_hours + form_metadata = { "operator_id": self.operator_val.text(), "participant_id": self.participant_val.text(), "flowcell_id": self.flowcell_val.text(), "sample_collection_date": self.sample_collection_date.text(), "sample_collection_time": self.sample_collection_time.text(), + "sample_age_hours": sample_age_hours, "sample_storage_temp": self.sample_storage_temp.text(), "target_flowrate": ( TARGET_FLOWRATE.name.capitalize(), diff --git a/ulc_mm_package/scope_constants.py b/ulc_mm_package/scope_constants.py index 3f1f7af8e..699606753 100644 --- a/ulc_mm_package/scope_constants.py +++ b/ulc_mm_package/scope_constants.py @@ -147,6 +147,7 @@ def IMG_HEIGHT(self) -> int: "participant_id", "sample_collection_date", "sample_collection_time", + "sample_age_hours", "sample_storage_temp", "flowcell_id", "target_flowrate", From 5842f08816e79f7a7274e0bdb052eada944130ea Mon Sep 17 00:00:00 2001 From: i-jey Date: Wed, 9 Apr 2025 10:49:53 -0700 Subject: [PATCH 06/11] Round sample collection age to 2 decimal points --- ulc_mm_package/QtGUI/form_gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ulc_mm_package/QtGUI/form_gui.py b/ulc_mm_package/QtGUI/form_gui.py index 4c87cbac0..60aee5b6e 100644 --- a/ulc_mm_package/QtGUI/form_gui.py +++ b/ulc_mm_package/QtGUI/form_gui.py @@ -174,7 +174,7 @@ def get_form_input(self) -> dict: date_diff_in_hours = sample_date.daysTo(current_date) * 24 time_diff_in_hours = sample_time.secsTo(current_time) / 3600 - sample_age_hours = date_diff_in_hours + time_diff_in_hours + sample_age_hours = round(date_diff_in_hours + time_diff_in_hours, 2) form_metadata = { "operator_id": self.operator_val.text(), From f846bc4afef0f08aca90a284e5b1627172cef67f Mon Sep 17 00:00:00 2001 From: i-jey Date: Wed, 9 Apr 2025 10:50:23 -0700 Subject: [PATCH 07/11] Remove msg label --- ulc_mm_package/QtGUI/form_gui.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ulc_mm_package/QtGUI/form_gui.py b/ulc_mm_package/QtGUI/form_gui.py index 60aee5b6e..044701b5c 100644 --- a/ulc_mm_package/QtGUI/form_gui.py +++ b/ulc_mm_package/QtGUI/form_gui.py @@ -94,7 +94,6 @@ def _load_ui(self): self.notes_lbl = QLabel("Other notes") self.site_lbl = QLabel("Site") self.sample_lbl = QLabel("Sample type") - self.msg_lbl = QLabel("Please fill out experiment data.") # Text boxes self.operator_val = QLineEdit() @@ -157,7 +156,6 @@ def _load_ui(self): self.main_layout.addWidget(self.notes_val, 8, 1) self.main_layout.addWidget(self.start_btn, 9, 1) - self.main_layout.addWidget(self.msg_lbl, 10, 0, 1, 2) # Set the focus order self.operator_val.setFocus() From d4383b8849cf451007ba463f825a6573be749891 Mon Sep 17 00:00:00 2001 From: i-jey Date: Wed, 9 Apr 2025 10:57:18 -0700 Subject: [PATCH 08/11] reduce unnecessary precision when writing to per image metadata file --- ulc_mm_package/QtGUI/scope_op.py | 37 +++++++++++++++---------------- ulc_mm_package/scope_constants.py | 1 - 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/ulc_mm_package/QtGUI/scope_op.py b/ulc_mm_package/QtGUI/scope_op.py index 59e46eb40..c4705e069 100644 --- a/ulc_mm_package/QtGUI/scope_op.py +++ b/ulc_mm_package/QtGUI/scope_op.py @@ -869,7 +869,7 @@ def run_experiment(self, img, timestamp) -> None: return # Record timestamp before running routines - self.img_metadata["timestamp"] = timestamp + self.img_metadata["timestamp"] = round(timestamp, 2) self.img_metadata["im_counter"] = f"{self.frame_count:0{self.digits}d}" t0 = perf_counter() @@ -1002,24 +1002,23 @@ def run_experiment(self, img, timestamp) -> None: self.img_metadata["motor_pos"] = self.mscope.motor.getCurrentPosition() try: pressure, status = self.mscope.pneumatic_module.getPressure() - ( - self.img_metadata["pressure_hpa"], - self.img_metadata["pressure_status_flag"], - ) = (pressure, status) + self.img_metadata["pressure_hpa"] = round(pressure, 2) except PressureSensorStaleValue as e: ## TODO??? self.logger.error(f"Stale pressure sensor value - {e}") - self.img_metadata["led_pwm_val"] = self.mscope.led.pwm_duty_cycle - self.img_metadata[ - "syringe_pos" - ] = self.mscope.pneumatic_module.getCurrentDutyCycle() - self.img_metadata["flowrate"] = self.flowrate - self.img_metadata["focus_error"] = raw_focus_err - self.img_metadata["filtered_focus_error"] = filtered_focus_err + self.img_metadata["led_pwm_val"] = round(self.mscope.led.pwm_duty_cycle, 4) + self.img_metadata["syringe_pos"] = round( + self.mscope.pneumatic_module.getCurrentDutyCycle(), 4 + ) + self.img_metadata["flowrate"] = round(self.flowrate, 4) + self.img_metadata["focus_error"] = round(raw_focus_err, 4) + self.img_metadata["filtered_focus_error"] = round(filtered_focus_err, 4) self.img_metadata["focus_adjustment"] = focus_adjustment - self.img_metadata["classic_sharpness_ratio"] = sharpness_ratio_rel_peak - self.img_metadata["mean_pixel_val"] = curr_mean_pixel_val + self.img_metadata["classic_sharpness_ratio"] = round( + sharpness_ratio_rel_peak, 4 + ) + self.img_metadata["mean_pixel_val"] = round(curr_mean_pixel_val, 4) if self.frame_count % TH_PERIOD_NUM == 0: try: @@ -1027,11 +1026,11 @@ def run_experiment(self, img, timestamp) -> None: temperature, humidity, ) = self.mscope.ht_sensor.get_temp_and_humidity() - self.img_metadata["humidity"] = humidity - self.img_metadata["temperature"] = temperature - self.img_metadata[ - "camera_temperature" - ] = self.mscope.camera._getTemperature() + self.img_metadata["humidity"] = round(humidity, 4) + self.img_metadata["temperature"] = round(temperature, 2) + self.img_metadata["camera_temperature"] = round( + self.mscope.camera._getTemperature(), 2 + ) except Exception as e: # some error has occurred, but the TH sensor isn't critical, so just warn # and move on diff --git a/ulc_mm_package/scope_constants.py b/ulc_mm_package/scope_constants.py index 3f1f7af8e..95d51b7ee 100644 --- a/ulc_mm_package/scope_constants.py +++ b/ulc_mm_package/scope_constants.py @@ -169,7 +169,6 @@ def IMG_HEIGHT(self) -> int: "timestamp", "motor_pos", "pressure_hpa", - "pressure_status_flag", "led_pwm_val", "syringe_pos", "flowrate", From cd54457455ce06f4ff759c7e442c96f1fa012d42 Mon Sep 17 00:00:00 2001 From: i-jey Date: Wed, 9 Apr 2025 11:14:01 -0700 Subject: [PATCH 09/11] Account for potential none values before rounding --- ulc_mm_package/QtGUI/scope_op.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/ulc_mm_package/QtGUI/scope_op.py b/ulc_mm_package/QtGUI/scope_op.py index c4705e069..4edadaca4 100644 --- a/ulc_mm_package/QtGUI/scope_op.py +++ b/ulc_mm_package/QtGUI/scope_op.py @@ -1002,7 +1002,9 @@ def run_experiment(self, img, timestamp) -> None: self.img_metadata["motor_pos"] = self.mscope.motor.getCurrentPosition() try: pressure, status = self.mscope.pneumatic_module.getPressure() - self.img_metadata["pressure_hpa"] = round(pressure, 2) + self.img_metadata["pressure_hpa"] = ( + round(pressure, 2) if pressure is not None else pressure + ) except PressureSensorStaleValue as e: ## TODO??? self.logger.error(f"Stale pressure sensor value - {e}") @@ -1011,12 +1013,22 @@ def run_experiment(self, img, timestamp) -> None: self.img_metadata["syringe_pos"] = round( self.mscope.pneumatic_module.getCurrentDutyCycle(), 4 ) - self.img_metadata["flowrate"] = round(self.flowrate, 4) - self.img_metadata["focus_error"] = round(raw_focus_err, 4) - self.img_metadata["filtered_focus_error"] = round(filtered_focus_err, 4) + self.img_metadata["flowrate"] = ( + round(self.flowrate, 4) if self.flowrate is not None else self.flowrate + ) + self.img_metadata["focus_error"] = ( + round(raw_focus_err, 4) if raw_focus_err is not None else raw_focus_err + ) + self.img_metadata["filtered_focus_error"] = ( + round(filtered_focus_err, 4) + if filtered_focus_err is not None + else filtered_focus_err + ) self.img_metadata["focus_adjustment"] = focus_adjustment - self.img_metadata["classic_sharpness_ratio"] = round( - sharpness_ratio_rel_peak, 4 + self.img_metadata["classic_sharpness_ratio"] = ( + round(sharpness_ratio_rel_peak, 4) + if sharpness_ratio_rel_peak is not None + else sharpness_ratio_rel_peak ) self.img_metadata["mean_pixel_val"] = round(curr_mean_pixel_val, 4) From b159beff73e15552db32314b9527dbabb9f63a70 Mon Sep 17 00:00:00 2001 From: i-jey Date: Wed, 9 Apr 2025 12:21:47 -0700 Subject: [PATCH 10/11] Formatting --- ulc_mm_package/QtGUI/form_gui.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ulc_mm_package/QtGUI/form_gui.py b/ulc_mm_package/QtGUI/form_gui.py index 044701b5c..89c040766 100644 --- a/ulc_mm_package/QtGUI/form_gui.py +++ b/ulc_mm_package/QtGUI/form_gui.py @@ -156,7 +156,6 @@ def _load_ui(self): self.main_layout.addWidget(self.notes_val, 8, 1) self.main_layout.addWidget(self.start_btn, 9, 1) - # Set the focus order self.operator_val.setFocus() self.start_btn.setDefault(True) From f2ba275739c430c81743f3e58a5741116ade66a3 Mon Sep 17 00:00:00 2001 From: i-jey Date: Wed, 9 Apr 2025 13:14:58 -0700 Subject: [PATCH 11/11] minor bugfix --- ulc_mm_package/QtGUI/scope_op.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ulc_mm_package/QtGUI/scope_op.py b/ulc_mm_package/QtGUI/scope_op.py index 4edadaca4..ec9c4a7ca 100644 --- a/ulc_mm_package/QtGUI/scope_op.py +++ b/ulc_mm_package/QtGUI/scope_op.py @@ -1030,7 +1030,11 @@ def run_experiment(self, img, timestamp) -> None: if sharpness_ratio_rel_peak is not None else sharpness_ratio_rel_peak ) - self.img_metadata["mean_pixel_val"] = round(curr_mean_pixel_val, 4) + self.img_metadata["mean_pixel_val"] = ( + round(curr_mean_pixel_val, 4) + if curr_mean_pixel_val is not None + else curr_mean_pixel_val + ) if self.frame_count % TH_PERIOD_NUM == 0: try: