diff --git a/src/firefly/plans/grid_scan.py b/src/firefly/plans/grid_scan.py index fec696a8..0c540a84 100644 --- a/src/firefly/plans/grid_scan.py +++ b/src/firefly/plans/grid_scan.py @@ -23,7 +23,8 @@ def setup_ui(self): "Motor", "Start", "Stop", - "Scan points", + "N. points", + "Size", "Snake", "Fly", ] @@ -38,7 +39,7 @@ def setup_ui(self): # fix widths so the labels are aligned with GridScanRegions Qlabels_all["Priority axis"].setFixedWidth(70) Qlabels_all["Motor"].setFixedWidth(100) - Qlabels_all["Scan points"].setFixedWidth(68) + Qlabels_all["N. points"].setFixedWidth(68) Qlabels_all["Snake"].setFixedWidth(53) Qlabels_all["Fly"].setFixedWidth(43) @@ -52,7 +53,7 @@ class GridScanRegion(regions_display.RegionBase): def setup_ui(self): self.layout = QtWidgets.QHBoxLayout() - # First item, motor No. + # motor No. self.motor_label = QtWidgets.QLCDNumber() self.motor_label.setStyleSheet( "QLCDNumber { background-color: white; color: red; }" @@ -60,40 +61,76 @@ def setup_ui(self): self.motor_label.display(self.line_label) self.layout.addWidget(self.motor_label) - # Second item, ComponentSelector + # ComponentSelector self.motor_box = ComponentSelector() self.layout.addWidget(self.motor_box) - # Third item, start point + # Start point self.start_line_edit = QtWidgets.QLineEdit() self.start_line_edit.setValidator(QDoubleValidator()) # only takes floats self.start_line_edit.setPlaceholderText("Start…") self.layout.addWidget(self.start_line_edit) - # Forth item, stop point + # Stop point self.stop_line_edit = QtWidgets.QLineEdit() self.stop_line_edit.setValidator(QDoubleValidator()) # only takes floats self.stop_line_edit.setPlaceholderText("Stop…") self.layout.addWidget(self.stop_line_edit) - # Fifth item, number of scan point + # Number of scan point self.scan_pts_spin_box = QtWidgets.QSpinBox() self.scan_pts_spin_box.setMinimum(1) self.scan_pts_spin_box.setMaximum(99999) self.layout.addWidget(self.scan_pts_spin_box) - # Sixth item, snake checkbox + # Step size (non-editable) + self.step_size_line_edit = QtWidgets.QLineEdit() + self.step_size_line_edit.setReadOnly(True) + self.step_size_line_edit.setDisabled(True) + self.step_size_line_edit.setPlaceholderText("Step Size…") + self.layout.addWidget(self.step_size_line_edit) + + # Snake checkbox self.snake_checkbox = QtWidgets.QCheckBox() self.snake_checkbox.setText("Snake") self.snake_checkbox.setEnabled(True) self.layout.addWidget(self.snake_checkbox) - # Seventh item, fly checkbox # not available right now + # Fly checkbox # not available right now self.fly_checkbox = QtWidgets.QCheckBox() self.fly_checkbox.setText("Fly") self.fly_checkbox.setEnabled(False) self.layout.addWidget(self.fly_checkbox) + # Connect signals + self.start_line_edit.textChanged.connect(self.update_step_size) + self.stop_line_edit.textChanged.connect(self.update_step_size) + self.scan_pts_spin_box.valueChanged.connect(self.update_step_size) + + def update_step_size(self): + try: + # Get Start and Stop values + start_text = self.start_line_edit.text().strip() + stop_text = self.stop_line_edit.text().strip() + if not start_text or not stop_text: + self.step_size_line_edit.setText("N/A") + return + + start = float(start_text) + stop = float(stop_text) + + # Ensure num_points is an integer + num_points = int(self.scan_pts_spin_box.value()) # Corrected method call + + # Calculate step size + if num_points > 1: + step_size = (stop - start) / (num_points - 1) + self.step_size_line_edit.setText(f"{step_size}") + else: + self.step_size_line_edit.setText("N/A") + except ValueError: + self.step_size_line_edit.setText("N/A") + class GridScanDisplay(regions_display.RegionsDisplay): Region = GridScanRegion @@ -108,10 +145,6 @@ def customize_ui(self): # add title layout self.title_region = TitleRegion() self.ui.title_layout.addLayout(self.title_region.layout) - # When selections of detectors changed update_total_time - # self.ui.detectors_list.selectionModel().selectionChanged.connect( - # self.update_total_time - # ) self.ui.spinBox_repeat_scan_num.valueChanged.connect(self.update_total_time) # Connect scan points change to update total time for region in self.regions: diff --git a/src/firefly/plans/grid_scan.ui b/src/firefly/plans/grid_scan.ui index fa6c5761..847b9fe5 100644 --- a/src/firefly/plans/grid_scan.ui +++ b/src/firefly/plans/grid_scan.ui @@ -6,8 +6,8 @@ 0 0 - 983 - 432 + 800 + 385 @@ -65,9 +65,15 @@ + + background-color: rgb(255, 85, 127); + Relative + + true + @@ -86,93 +92,97 @@ - + - - - true + + + Qt::Horizontal - - - - 0 - 0 - 471 - 215 - + + + + 500 + 0 + - - - - - 3 - - - 3 - - - + + true + + + + + 0 + 0 + 498 + 207 + + + + + + + 3 + + + 3 + + + + + + + 3 + + + 3 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + - - - 3 + + + + 0 + 0 + - - 3 + + Detectors - + - - - Qt::Vertical + + + <html><head/><body><p>Use <span style=" font-weight:600;">ctrl</span> to select multiple detectors</p></body></html> - - - 20 - 40 - + + QAbstractItemView::MultiSelection - + - - - - Qt::Vertical - - - - - - - - - - 0 - 0 - - - - Detectors - - - - - - - <html><head/><body><p>Use <span style=" font-weight:600;">ctrl</span> to select multiple detectors</p></body></html> - - - QAbstractItemView::MultiSelection - - - - - @@ -182,6 +192,13 @@ + + + + Qt::Horizontal + + + @@ -190,7 +207,7 @@ Experiment purpose: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter @@ -235,7 +252,7 @@ Exposure time each scan: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter @@ -248,7 +265,7 @@ Total exposure time: - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter @@ -502,13 +519,6 @@ - - - - Qt::Horizontal - - - diff --git a/src/firefly/plans/line_scan.py b/src/firefly/plans/line_scan.py index 8e3d8a94..afb450e4 100644 --- a/src/firefly/plans/line_scan.py +++ b/src/firefly/plans/line_scan.py @@ -32,6 +32,41 @@ def setup_ui(self): self.stop_line_edit.setPlaceholderText("Stop…") self.layout.addWidget(self.stop_line_edit) + # Step size (non-editable) + self.step_size_line_edit = QtWidgets.QLineEdit() + self.step_size_line_edit.setReadOnly(True) + self.step_size_line_edit.setDisabled(True) + self.step_size_line_edit.setPlaceholderText("Step Size…") + self.layout.addWidget(self.step_size_line_edit) + + # Connect signals + self.start_line_edit.textChanged.connect(self.update_step_size) + self.stop_line_edit.textChanged.connect(self.update_step_size) + + def update_step_size(self, num_points=None): + try: + # Get Start and Stop values + start_text = self.start_line_edit.text().strip() + stop_text = self.stop_line_edit.text().strip() + if not start_text or not stop_text: + self.step_size_line_edit.setText("N/A") + return + + start = float(start_text) + stop = float(stop_text) + + # Ensure num_points is an integer + num_points = int(num_points) if num_points is not None else 2 + + # Calculate step size + if num_points > 1: + step_size = (stop - start) / (num_points - 1) + self.step_size_line_edit.setText(f"{step_size}") + else: + self.step_size_line_edit.setText("N/A") + except ValueError: + self.step_size_line_edit.setText("N/A") + class LineScanDisplay(regions_display.RegionsDisplay): Region = LineScanRegion @@ -55,6 +90,14 @@ def customize_ui(self): ) self.ui.spinBox_repeat_scan_num.valueChanged.connect(self.update_total_time) + # Connect scan_pts_spin_box value change to regions + self.ui.scan_pts_spin_box.valueChanged.connect(self.update_regions_step_size) + + def update_regions_step_size(self, num_points): + """Update the step size for all regions.""" + for region in self.regions: + region.update_step_size(num_points) + def queue_plan(self, *args, **kwargs): """Execute this plan on the queueserver.""" detectors, motor_args, repeat_scan_num = self.get_scan_parameters() diff --git a/src/firefly/plans/line_scan.ui b/src/firefly/plans/line_scan.ui index f21e7b6b..cbe938a0 100644 --- a/src/firefly/plans/line_scan.ui +++ b/src/firefly/plans/line_scan.ui @@ -7,7 +7,7 @@ 0 0 795 - 389 + 384 @@ -16,83 +16,128 @@ - - + + + + + + + 0 + 0 + + + + Num. Motors + + + - - + + true - - - - 0 - 0 - 377 - 215 - - - - - - - 3 - - - 3 - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + false + + + false + + + QAbstractSpinBox::CorrectToNearestValue + + + 1 + + + 1 + + + 10 + - + Qt::Vertical - - - - - - 0 - 0 - - - - Detectors - - - - - - - <html><head/><body><p>Use <span style=" font-weight:600;">ctrl</span> to select multiple detectors</p></body></html> - - - QAbstractItemView::MultiSelection - - - - + + + background-color: rgb(255, 85, 127); + + + Relative + + + true + + + + + + + Qt::Vertical + + + + + + + Log + + + + + + + Qt::Vertical + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Scan Points + + + + + + + 1 + + + 10000 + + + 2 + + + 10 + + @@ -346,147 +391,32 @@ - - - - Do this scan by multiple times - - - Num. of scans - - - - - - Do this scan by multiple times - - - 1 - - - 999 - - - - - - - - - - - - 0 - 0 - - - - Num. Motors - - - - - - - true - - - false - - - false - - - QAbstractSpinBox::CorrectToNearestValue - - - 1 - - - 1 - - - 10 - - - - - - - Qt::Vertical - - - - - - - Relative - - - - - - - Qt::Vertical - - - - - - - Log - - - - - - - Qt::Vertical - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - Scan Points - - - - - - - 1 - - - 10000 - - - 2 - - - 10 - - + + + + + Do this scan by multiple times + + + Num. of scans: + + + + + + + Do this scan by multiple times + + + 1 + + + 999 + + + + @@ -497,6 +427,89 @@ + + + + Qt::Horizontal + + + + + 500 + 0 + + + + true + + + + + 0 + 0 + 498 + 208 + + + + + + + 3 + + + 3 + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 0 + 10 + + + + + + + + + + + + + + 0 + 0 + + + + Detectors + + + + + + + <html><head/><body><p>Use <span style=" font-weight:600;">ctrl</span> to select multiple detectors</p></body></html> + + + QAbstractItemView::MultiSelection + + + + + + + diff --git a/src/firefly/plans/regions_display.py b/src/firefly/plans/regions_display.py index da2a3893..9e2b7755 100644 --- a/src/firefly/plans/regions_display.py +++ b/src/firefly/plans/regions_display.py @@ -56,7 +56,6 @@ def customize_ui(self): # Disable the line edits in spin box (use up/down buttons instead) self.ui.num_motor_spin_box.lineEdit().setReadOnly(True) - # Create the initial (blank) regions self.regions = [] self.ui.num_motor_spin_box.setValue(self.default_num_regions) @@ -64,6 +63,23 @@ def customize_ui(self): # Set up the mechanism for changing region number self.ui.num_motor_spin_box.valueChanged.connect(self.update_regions_slot) self.ui.run_button.clicked.connect(self.queue_plan) + # Color highlights for relative checkbox + if hasattr(self, "relative_scan_checkbox"): + self.ui.relative_scan_checkbox.stateChanged.connect(self.change_background) + + def change_background(self, state): + """ + Change the background color of the relative scan checkbox based on its state. + """ + if state: # Checked + self.ui.relative_scan_checkbox.setStyleSheet( + "background-color: rgb(255, 85, 127);" + ) + + else: # Unchecked + self.ui.relative_scan_checkbox.setStyleSheet( + "background-color: rgb(0, 170, 255);" + ) @asyncSlot(object) async def update_devices(self, registry): diff --git a/src/firefly/tests/test_grid_scan_window.py b/src/firefly/tests/test_grid_scan_window.py index 4d1efb54..dc440715 100644 --- a/src/firefly/tests/test_grid_scan_window.py +++ b/src/firefly/tests/test_grid_scan_window.py @@ -59,7 +59,58 @@ async def test_time_calculator(display, sim_registry, ion_chamber): @pytest.mark.asyncio -async def test_grid_scan_plan_queued(display, qtbot, sim_registry, ion_chamber): +async def test_step_size_calculation(display): + # Set up the display with 2 regions + await display.update_regions(2) + + # Region 0: Set Start, Stop, and Points + region_0 = display.regions[0] + region_0.start_line_edit.setText("0") + region_0.stop_line_edit.setText("10") + region_0.scan_pts_spin_box.setValue(5) + + # Trigger step size calculation + region_0.update_step_size() + + # Check step size calculation for Region 0 + assert region_0.step_size_line_edit.text() == "2.5" + + # Region 1: Set Start, Stop, and Points + region_1 = display.regions[1] + region_1.start_line_edit.setText("5") + region_1.stop_line_edit.setText("15") + region_1.scan_pts_spin_box.setValue(3) + + # Trigger step size calculation + region_1.update_step_size() + + # Check step size calculation for Region 1 + assert region_1.step_size_line_edit.text() == "5.0" + + # Test invalid input for Region 0 + region_0.start_line_edit.setText("invalid") + region_0.update_step_size() + assert region_0.step_size_line_edit.text() == "N/A" + + # Test edge case: num_points = 1 for Region 1 + region_1.scan_pts_spin_box.setValue(1) + region_1.update_step_size() + assert region_1.step_size_line_edit.text() == "N/A" + + # Reset valid values for Region 0 + region_0.start_line_edit.setText("10") + region_0.stop_line_edit.setText("30") + region_0.scan_pts_spin_box.setValue(4) + region_0.update_step_size() + assert ( + region_0.step_size_line_edit.text() == "6.666666666666667" + ) # Expect float precision + + +@pytest.mark.asyncio +async def test_grid_scan_plan_queued( + display, sim_registry, ion_chamber, monkeypatch, qtbot +): await display.update_regions(2) # set up a test motor 1 @@ -87,7 +138,7 @@ async def test_grid_scan_plan_queued(display, qtbot, sim_registry, ion_chamber): display.ui.textEdit_notes.setText("notes") expected_item = BPlan( - "grid_scan", + "rel_grid_scan", ["vortex_me4", "I00"], "async_motor_1", 2.0, diff --git a/src/firefly/tests/test_line_scan_window.py b/src/firefly/tests/test_line_scan_window.py index 693d4cae..dde258ca 100644 --- a/src/firefly/tests/test_line_scan_window.py +++ b/src/firefly/tests/test_line_scan_window.py @@ -71,7 +71,42 @@ async def test_time_calculator(display, sim_registry, ion_chamber, qtbot, qapp): @pytest.mark.asyncio -async def test_line_scan_plan_queued(qtbot, display): +async def test_step_size_calculation(display, qtbot): + await display.update_regions(1) + region = display.regions[0] + region.start_line_edit.setText("0") + region.stop_line_edit.setText("10") + + # Set num_points and emit the signal + display.ui.scan_pts_spin_box.setValue(5) + region.update_step_size(5) # Emit the signal with the new num_points value + assert region.step_size_line_edit.text() == "2.5" + + # Change the number of points and verify step size updates + display.ui.scan_pts_spin_box.setValue(3) + region.update_step_size(3) + assert region.step_size_line_edit.text() == "5.0" + + # Test invalid input + region.start_line_edit.setText("Start..") + region.update_step_size(3) + assert region.step_size_line_edit.text() == "N/A" + + # Test edge case: num_points = 1 + display.ui.scan_pts_spin_box.setValue(1) + region.update_step_size(1) + assert region.step_size_line_edit.text() == "N/A" + + # Reset to a valid state and verify + region.start_line_edit.setText("0") + region.stop_line_edit.setText("10") + display.ui.scan_pts_spin_box.setValue(6) + region.update_step_size(6) + assert region.step_size_line_edit.text() == "2.0" + + +@pytest.mark.asyncio +async def test_line_scan_plan_queued(display, monkeypatch, qtbot): # set up motor num await display.update_regions(2) @@ -99,7 +134,7 @@ async def test_line_scan_plan_queued(qtbot, display): display.ui.textEdit_notes.setText("notes") expected_item = BPlan( - "scan", + "rel_scan", ["vortex_me4", "I0"], "async_motor_1", 1.0,