diff --git a/src/firefly/application.py b/src/firefly/application.py
index 9c4dd6c7..724a646c 100644
--- a/src/firefly/application.py
+++ b/src/firefly/application.py
@@ -257,7 +257,9 @@ def setup_window_actions(self):
plans = [
# (plan_name, text, display file)
("count", "&Count", "count.py"),
+ ("move_motor", "&Move motor", "move_motor_window.py"),
("line_scan", "&Line scan", "line_scan.py"),
+ ("grid_scan", "&Grid scan", "grid_scan.py"),
("xafs_scan", "&XAFS Scan", "xafs_scan.py"),
]
self.plan_actions = []
diff --git a/src/firefly/plans/grid_scan.py b/src/firefly/plans/grid_scan.py
new file mode 100644
index 00000000..aa7fed02
--- /dev/null
+++ b/src/firefly/plans/grid_scan.py
@@ -0,0 +1,188 @@
+import logging
+
+from bluesky_queueserver_api import BPlan
+from qtpy import QtWidgets
+from qtpy.QtGui import QDoubleValidator
+
+from firefly.application import FireflyApplication
+from firefly.component_selector import ComponentSelector
+from firefly.plans.line_scan import LineScanDisplay
+
+log = logging.getLogger()
+
+
+class TitleRegion:
+ def __init__(self):
+ self.setup_ui()
+
+ def setup_ui(self):
+ self.layout = QtWidgets.QGridLayout()
+ labels = ["Priority axis", "Motor", "Start", "Stop", "Snake", "Fly"]
+ Qlabels_all = {}
+
+ # add labels in the first row
+ for i, label_i in enumerate(labels):
+ Qlabel_i = QtWidgets.QLabel(label_i)
+ self.layout.addWidget(Qlabel_i, 0, i)
+ Qlabels_all[label_i] = Qlabel_i
+
+ # fix widths so the labels are aligned with GridScanRegions
+ Qlabels_all["Priority axis"].setFixedWidth(70)
+ Qlabels_all["Motor"].setFixedWidth(100)
+ Qlabels_all["Snake"].setFixedWidth(53)
+ Qlabels_all["Fly"].setFixedWidth(43)
+
+ # add labels in the second row
+ label = QtWidgets.QLabel("fast -> slow")
+ self.layout.addWidget(label, 1, 0)
+
+
+class GridScanRegion:
+ def __init__(self):
+ self.setup_ui()
+
+ def setup_ui(self):
+ self.layout = QtWidgets.QHBoxLayout()
+
+ # First item, motor No.
+ self.motor_label = QtWidgets.QLCDNumber()
+ self.motor_label.setStyleSheet(
+ "QLCDNumber { background-color: white; color: red; }"
+ )
+ self.layout.addWidget(self.motor_label)
+
+ # Second item, ComponentSelector
+ self.motor_box = ComponentSelector()
+ self.layout.addWidget(self.motor_box)
+
+ # Third item, 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
+ 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, snake checkbox
+ self.snake_checkbox = QtWidgets.QCheckBox()
+ self.snake_checkbox.setText("Snake")
+ self.snake_checkbox.setEnabled(True)
+ self.layout.addWidget(self.snake_checkbox)
+
+ # Sixth item, 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)
+
+
+class GridScanDisplay(LineScanDisplay):
+ default_num_regions = 2
+
+ def __init__(self, parent=None, args=None, macros=None, ui_filename=None, **kwargs):
+ super().__init__(parent, args, macros, ui_filename, **kwargs)
+
+ def customize_ui(self):
+ super().customize_ui()
+ # add title layout
+ self.title_region = TitleRegion()
+ self.ui.title_layout.addLayout(self.title_region.layout)
+ # reset button
+ self.ui.reset_pushButton.clicked.connect(self.reset_default_regions)
+
+ def add_regions(self, num=1):
+ for i in range(num):
+ region = GridScanRegion()
+ self.ui.regions_layout.addLayout(region.layout)
+ # Save it to the list
+ self.regions.append(region)
+
+ # the num of motor
+ num_motor_i = len(self.regions)
+ # region.motor_label.setText(str(num_motor_i)) # when using label
+ region.motor_label.display(num_motor_i)
+
+ def time_calculate_method(self, detector_time):
+ num_points = self.ui.scan_pts_spin_box.value()
+ num_regions = len(self.regions)
+ total_time_per_scan = num_regions * detector_time * num_points
+ return total_time_per_scan
+
+ def update_regions(self):
+ super().update_regions()
+
+ # disable snake for the last region and enable the previous regions
+ self.regions[-1].snake_checkbox.setEnabled(False)
+ for region_i in self.regions[:-1]:
+ region_i.snake_checkbox.setEnabled(True)
+
+ def queue_plan(self, *args, **kwargs):
+ """Execute this plan on the queueserver."""
+ detectors, num_points, motor_args, repeat_scan_num, md = (
+ self.get_scan_parameters()
+ )
+
+ # get snake axes, if all unchecked, set it None
+ snake_axes = [
+ region_i.motor_box.current_component().name
+ for i, region_i in enumerate(self.regions)
+ if region_i.snake_checkbox.isChecked()
+ ]
+
+ if snake_axes == []:
+ snake_axes = False
+
+ if self.ui.relative_scan_checkbox.isChecked():
+ scan_type = "rel_grid_scan"
+ else:
+ scan_type = "grid_scan"
+
+ # # Build the queue item
+ item = BPlan(
+ scan_type,
+ detectors,
+ *motor_args,
+ num=num_points,
+ snake_axes=snake_axes,
+ md=md,
+ )
+
+ # Submit the item to the queueserver
+ app = FireflyApplication.instance()
+ log.info("Added line scan() plan to queue.")
+ # repeat scans
+ for i in range(repeat_scan_num):
+ app.add_queue_item(item)
+
+ def ui_filename(self):
+ return "plans/grid_scan.ui"
+
+
+# -----------------------------------------------------------------------------
+# :author: Juanjuan Huang
+# :email: juanjuan.huang@anl.gov
+# :copyright: Copyright © 2024, UChicago Argonne, LLC
+#
+# Distributed under the terms of the 3-Clause BSD License
+#
+# The full license is in the file LICENSE, distributed with this software.
+#
+# DISCLAIMER
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# -----------------------------------------------------------------------------
diff --git a/src/firefly/plans/grid_scan.ui b/src/firefly/plans/grid_scan.ui
new file mode 100644
index 00000000..10656fdc
--- /dev/null
+++ b/src/firefly/plans/grid_scan.ui
@@ -0,0 +1,588 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 787
+ 366
+
+
+
+ 2D Scan
+
+
+ -
+
+
-
+
+
-
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 377
+ 162
+
+
+
+
-
+
+
-
+
+
-
+
+
+ 1
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 20
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Start...
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Stop...
+
+
+
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ -
+
+
+ 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
+
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Experiment purpose
+
+
+
+ -
+
+
+ Notes
+
+
+
+ -
+
+
+ Type this sample's name
+
+
+ Sample name
+
+
+
+ -
+
+
+ Sample name
+
+
+
+ -
+
+
-
+
+
+ total exposure time for a single scan
+
+
+ Exposure time each scan
+
+
+
+ -
+
+
+ total exposure time for a single scan
+
+
+ Total exposure time
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ h
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ min
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ s
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+
-
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ h
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ min
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ s
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+ -
+
+
+ Type the experimental purpose note for this scan
+
+
+ Purpose
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 60
+
+
+
+ 1
+
+
+ 14
+
+
+ 80
+
+
+ Additional notes about the sample
+
+
+
+ -
+
+
+ Run
+
+
+
+ -
+
+
+ Do this scan by multiple times
+
+
+ Num. of scans
+
+
+
+ -
+
+
+ Do this scan by multiple times
+
+
+ 1
+
+
+ 999
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Num. Motors
+
+
+
+ -
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+ QAbstractSpinBox::CorrectToNearestValue
+
+
+ 1
+
+
+ 1
+
+
+ 10
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Relative
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Scan Points
+
+
+
+ -
+
+
+ 1
+
+
+ 10000
+
+
+ 2
+
+
+ 10
+
+
+
+ -
+
+
+ Reset to default values
+
+
+
+
+
+ Reset Defaults
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+
+
+
+
+
+
+ QueueButton
+ QPushButton
+
+
+
+ DetectorListView
+ QListWidget
+
+
+
+ ComponentSelector
+ QWidget
+ firefly.component_selector
+ 1
+
+
+
+
+
diff --git a/src/firefly/plans/line_scan.py b/src/firefly/plans/line_scan.py
index 309109e1..e837071a 100644
--- a/src/firefly/plans/line_scan.py
+++ b/src/firefly/plans/line_scan.py
@@ -2,9 +2,12 @@
from bluesky_queueserver_api import BPlan
from qtpy import QtWidgets
+from qtpy.QtGui import QDoubleValidator
from firefly import display
+from firefly.application import FireflyApplication
from firefly.component_selector import ComponentSelector
+from firefly.plans.util import is_valid_value, time_converter
log = logging.getLogger()
@@ -16,30 +19,28 @@ def __init__(self):
def setup_ui(self):
self.layout = QtWidgets.QHBoxLayout()
- # First item, motor No.
- # self.motor_label = QtWidgets.QLabel()
- # self.motor_label.setText("1")
- # self.layout.addWidget(self.motor_label)
-
- # Second item, ComponentSelector
+ # First item, ComponentSelector
self.motor_box = ComponentSelector()
- # self.stop_line_edit.setPlaceholderText("Stop…")
self.layout.addWidget(self.motor_box)
- # Third item, start point
+ # Second item, 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
+ # Third item, 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)
class LineScanDisplay(display.FireflyDisplay):
+ default_num_regions = 1
+
def customize_ui(self):
- # Remove the default XAFS layout from .ui file
+ # Remove the default layout from .ui file
self.clearLayout(self.ui.region_template_layout)
self.reset_default_regions()
@@ -50,6 +51,13 @@ def customize_ui(self):
self.ui.run_button.setEnabled(True) # for testing
self.ui.run_button.clicked.connect(self.queue_plan)
+ # 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)
+ self.ui.scan_pts_spin_box.valueChanged.connect(self.update_total_time)
+
def clearLayout(self, layout):
if layout is not None:
while layout.count():
@@ -58,11 +66,10 @@ def clearLayout(self, layout):
item.widget().deleteLater()
def reset_default_regions(self):
- default_num_regions = 1
if not hasattr(self, "regions"):
self.regions = []
- self.add_regions(default_num_regions)
- self.ui.num_motor_spin_box.setValue(default_num_regions)
+ self.add_regions(self.default_num_regions)
+ self.ui.num_motor_spin_box.setValue(self.default_num_regions)
self.update_regions()
def add_regions(self, num=1):
@@ -92,13 +99,49 @@ def update_regions(self):
elif diff_region_num > 0:
self.add_regions(diff_region_num)
- def queue_plan(self, *args, **kwargs):
- """Execute this plan on the queueserver."""
+ def update_total_time(self):
+ # get default detector time
+ app = FireflyApplication.instance()
+ detectors = self.ui.detectors_list.selected_detectors()
+ detectors = [app.registry[name] for name in detectors]
+ detectors = [det for det in detectors if hasattr(det, "default_time_signal")]
+
+ # to prevent detector list is empty
+ try:
+ detector_time = max([det.default_time_signal.get() for det in detectors])
+ except ValueError:
+ detector_time = float("nan")
+
+ # get scan num points to calculate total time
+ total_time_per_scan = self.time_calculate_method(detector_time)
+
+ # calculate time for each scan
+ hrs, mins, secs = time_converter(total_time_per_scan)
+ self.ui.label_hour_scan.setText(str(hrs))
+ self.ui.label_min_scan.setText(str(mins))
+ self.ui.label_sec_scan.setText(str(secs))
+
+ # calculate time for entire plan
+ num_scan_repeat = self.ui.spinBox_repeat_scan_num.value()
+ total_time = num_scan_repeat * total_time_per_scan
+ hrs_total, mins_total, secs_total = time_converter(total_time)
+
+ self.ui.label_hour_total.setText(str(hrs_total))
+ self.ui.label_min_total.setText(str(mins_total))
+ self.ui.label_sec_total.setText(str(secs_total))
+
+ def time_calculate_method(self, detector_time):
+ num_points = self.ui.scan_pts_spin_box.value()
+ total_time_per_scan = detector_time * num_points
+ return total_time_per_scan
+
+ def get_scan_parameters(self):
# Get scan parameters from widgets
detectors = self.ui.detectors_list.selected_detectors()
num_points = self.ui.scan_pts_spin_box.value()
+ repeat_scan_num = int(self.ui.spinBox_repeat_scan_num.value())
- # get paramters from each rows of line regions:
+ # Get paramters from each rows of line regions:
motor_lst, start_lst, stop_lst = [], [], []
for region_i in self.regions:
motor_lst.append(region_i.motor_box.current_component().name)
@@ -111,6 +154,23 @@ def queue_plan(self, *args, **kwargs):
for values in motor_i
]
+ # Get meta data info
+ md = {
+ "sample": self.ui.lineEdit_sample.text(),
+ "purpose": self.ui.lineEdit_purpose.text(),
+ "notes": self.ui.textEdit_notes.toPlainText(),
+ }
+ # Only include metadata that isn't an empty string
+ md = {key: val for key, val in md.items() if is_valid_value(val)}
+
+ return detectors, num_points, motor_args, repeat_scan_num, md
+
+ def queue_plan(self, *args, **kwargs):
+ """Execute this plan on the queueserver."""
+ detectors, num_points, motor_args, repeat_scan_num, md = (
+ self.get_scan_parameters()
+ )
+
if self.ui.relative_scan_checkbox.isChecked():
if self.ui.log_scan_checkbox.isChecked():
scan_type = "rel_log_scan"
@@ -122,11 +182,6 @@ def queue_plan(self, *args, **kwargs):
else:
scan_type = "scan"
- md = {
- "sample": self.ui.lineEdit_sample.text(),
- "purpose": self.ui.lineEdit_purpose.text(),
- }
-
# # Build the queue item
item = BPlan(
scan_type,
@@ -138,11 +193,37 @@ def queue_plan(self, *args, **kwargs):
)
# Submit the item to the queueserver
- from firefly.application import FireflyApplication
-
app = FireflyApplication.instance()
log.info("Added line scan() plan to queue.")
- app.add_queue_item(item)
+ # repeat scans
+ for i in range(repeat_scan_num):
+ app.add_queue_item(item)
def ui_filename(self):
return "plans/line_scan.ui"
+
+
+# -----------------------------------------------------------------------------
+# :author: Juanjuan Huang
+# :email: juanjuan.huang@anl.gov
+# :copyright: Copyright © 2024, UChicago Argonne, LLC
+#
+# Distributed under the terms of the 3-Clause BSD License
+#
+# The full license is in the file LICENSE, distributed with this software.
+#
+# DISCLAIMER
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# -----------------------------------------------------------------------------
diff --git a/src/firefly/plans/line_scan.ui b/src/firefly/plans/line_scan.ui
index 814551e7..8d503dfc 100644
--- a/src/firefly/plans/line_scan.ui
+++ b/src/firefly/plans/line_scan.ui
@@ -6,8 +6,8 @@
0
0
- 660
- 366
+ 770
+ 318
@@ -16,6 +16,132 @@
-
+
-
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
-
+
+
+
+ 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
+
+
+
+
+
-
-
@@ -28,8 +154,8 @@
0
0
- 372
- 234
+ 377
+ 114
@@ -136,6 +262,9 @@
<html><head/><body><p>Use <span style=" font-weight:600;">ctrl</span> to select multiple detectors</p></body></html>
+
+ QAbstractItemView::MultiSelection
+
@@ -143,15 +272,39 @@
-
-
-
-
-
+
+
-
+
- Sample name
+ Notes
- -
+
-
+
+
-
+
+
+ total exposure time for a single scan
+
+
+ Exposure time each scan
+
+
+
+ -
+
+
+ total exposure time for a single scan
+
+
+ Total exposure time
+
+
+
+
+
+ -
Type this sample's name
@@ -161,129 +314,202 @@
- -
-
-
- Experiment purpose
-
-
-
- -
-
-
- Type the experimental purpose note for this scan
-
-
- Purpose
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
+
-
+
+
-
+
+
-
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ h
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ min
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ s
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+
-
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ h
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ min
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ s
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
- -
-
-
- Run
+
-
+
+
+ true
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- -
-
-
-
-
-
+
0
0
-
- Num. Motors
-
-
-
- -
-
-
- true
-
-
- false
-
-
- false
-
-
- QAbstractSpinBox::CorrectToNearestValue
+
+
+ 16777215
+ 60
+
-
+
1
-
- 1
+
+ 14
-
- 10
+
+ 80
-
-
- -
-
-
- Qt::Vertical
+
+ Additional notes about the sample
- -
-
-
- Relative
+
-
+
+
+ Type the experimental purpose note for this scan
+
+
+ Purpose
- -
-
-
- Qt::Vertical
+
-
+
+
+ Run
- -
-
+
-
+
- Log
+ Sample name
- -
-
-
- Qt::Vertical
+
-
+
+
+ Experiment purpose
- -
-
+
-
+
Qt::Horizontal
@@ -295,32 +521,26 @@
- -
-
-
-
- 0
- 0
-
+
-
+
+
+ Do this scan by multiple times
- Scan Points
+ Num. of scans
- -
-
+
-
+
+
+ Do this scan by multiple times
+
1
- 10000
-
-
- 2
-
-
- 10
+ 999
diff --git a/src/firefly/plans/move_motor_window.py b/src/firefly/plans/move_motor_window.py
new file mode 100644
index 00000000..866cb75b
--- /dev/null
+++ b/src/firefly/plans/move_motor_window.py
@@ -0,0 +1,163 @@
+import logging
+
+from bluesky_queueserver_api import BPlan
+from qtpy import QtWidgets
+from qtpy.QtGui import QDoubleValidator
+
+from firefly import display
+from firefly.application import FireflyApplication
+from firefly.component_selector import ComponentSelector
+
+log = logging.getLogger()
+
+
+class MotorRegion:
+ def __init__(self):
+ self.setup_ui()
+
+ def setup_ui(self):
+ self.layout = QtWidgets.QHBoxLayout()
+
+ # First item, ComponentSelector
+ self.motor_box = ComponentSelector()
+ self.layout.addWidget(self.motor_box)
+
+ # Second item, position point
+ self.position_line_edit = QtWidgets.QLineEdit()
+ self.position_line_edit.setValidator(QDoubleValidator()) # only takes floats
+ self.position_line_edit.setPlaceholderText("Position…")
+ self.layout.addWidget(self.position_line_edit)
+
+
+class MoveMotorDisplay(display.FireflyDisplay):
+ default_num_regions = 1
+
+ def customize_ui(self):
+ # Remove the default layout from .ui file
+ self.clearLayout(self.ui.region_template_layout)
+ self.reset_default_regions()
+
+ # disable the line edits in spin box
+ self.ui.num_motor_spin_box.lineEdit().setReadOnly(True)
+ self.ui.num_motor_spin_box.valueChanged.connect(self.update_regions)
+
+ self.ui.run_button.setEnabled(True) # for testing
+ self.ui.run_button.clicked.connect(self.queue_plan)
+
+ def time_converter(self, total_seconds):
+ hours = round(total_seconds // 3600)
+ minutes = round((total_seconds % 3600) // 60)
+ seconds = round(total_seconds % 60)
+ if total_seconds == -1:
+ hours, minutes, seconds = "N/A", "N/A", "N/A"
+ return hours, minutes, seconds
+
+ def clearLayout(self, layout):
+ if layout is not None:
+ while layout.count():
+ item = layout.takeAt(0)
+ if item.widget():
+ item.widget().deleteLater()
+
+ def reset_default_regions(self):
+ if not hasattr(self, "regions"):
+ self.regions = []
+ self.add_regions(self.default_num_regions)
+ self.ui.num_motor_spin_box.setValue(self.default_num_regions)
+ self.update_regions()
+
+ def add_regions(self, num=1):
+ for i in range(num):
+ region = MotorRegion()
+ self.ui.regions_layout.addLayout(region.layout)
+ # Save it to the list
+ self.regions.append(region)
+
+ def remove_regions(self, num=1):
+ for i in range(num):
+ layout = self.regions[-1].layout
+ # iterate/wait, and delete all widgets in the layout in the end
+ while layout.count():
+ item = layout.takeAt(0)
+ if item.widget():
+ item.widget().deleteLater()
+ self.regions.pop()
+
+ def update_regions(self):
+ new_region_num = self.ui.num_motor_spin_box.value()
+ old_region_num = len(self.regions)
+ diff_region_num = new_region_num - old_region_num
+
+ if diff_region_num < 0:
+ self.remove_regions(abs(diff_region_num))
+ elif diff_region_num > 0:
+ self.add_regions(diff_region_num)
+
+ def get_scan_parameters(self):
+ # get paramters from each rows of line regions:
+ motor_lst, position_lst = [], []
+ for region_i in self.regions:
+ motor_lst.append(region_i.motor_box.current_component().name)
+ position_lst.append(float(region_i.position_line_edit.text()))
+
+ motor_args = [
+ values for motor_i in zip(motor_lst, position_lst) for values in motor_i
+ ]
+
+ # get meta data info
+ md = {
+ "sample": self.ui.lineEdit_sample.text(),
+ "purpose": self.ui.lineEdit_purpose.text(),
+ }
+
+ return motor_args, md
+
+ def queue_plan(self, *args, **kwargs):
+ """Execute this plan on the queueserver."""
+ motor_args, md = self.get_scan_parameters()
+
+ if self.ui.relative_scan_checkbox.isChecked():
+ scan_type = "mvr"
+ else:
+ scan_type = "mv"
+
+ # # Build the queue item
+ item = BPlan(
+ scan_type,
+ *motor_args,
+ md=md,
+ )
+
+ # Submit the item to the queueserver
+ app = FireflyApplication.instance()
+ log.info("Added line scan() plan to queue.")
+ app.add_queue_item(item)
+
+ def ui_filename(self):
+ return "plans/move_motor_window.ui"
+
+
+# -----------------------------------------------------------------------------
+# :author: Juanjuan Huang
+# :email: juanjuan.huang@anl.gov
+# :copyright: Copyright © 2024, UChicago Argonne, LLC
+#
+# Distributed under the terms of the 3-Clause BSD License
+#
+# The full license is in the file LICENSE, distributed with this software.
+#
+# DISCLAIMER
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# -----------------------------------------------------------------------------
diff --git a/src/firefly/plans/move_motor_window.ui b/src/firefly/plans/move_motor_window.ui
new file mode 100644
index 00000000..c7a400ff
--- /dev/null
+++ b/src/firefly/plans/move_motor_window.ui
@@ -0,0 +1,286 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 660
+ 366
+
+
+
+ Move motor
+
+
+ -
+
+
-
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Num. Motors
+
+
+
+ -
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+ QAbstractSpinBox::CorrectToNearestValue
+
+
+ 1
+
+
+ 1
+
+
+ 10
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Relative
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
-
+
+
+ Sample name
+
+
+
+ -
+
+
+ Type this sample's name
+
+
+ Sample name
+
+
+
+ -
+
+
+ Experiment purpose
+
+
+
+ -
+
+
+ Type the experimental purpose note for this scan
+
+
+ Purpose
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Run
+
+
+
+
+
+ -
+
+
-
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 640
+ 234
+
+
+
+
-
+
+
-
+
+
-
+
+
+ 1
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 20
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Positioner
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 20
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ Value
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ QueueButton
+ QPushButton
+
+
+
+ ComponentSelector
+ QWidget
+ firefly.component_selector
+ 1
+
+
+
+
+
diff --git a/src/firefly/plans/util.py b/src/firefly/plans/util.py
new file mode 100644
index 00000000..9c601113
--- /dev/null
+++ b/src/firefly/plans/util.py
@@ -0,0 +1,72 @@
+import numpy as np
+
+__all__ = ["time_converter"]
+
+
+def time_converter(total_seconds):
+ """
+ Convert time (in seconds) to a tuple of hours, minutes, seconds
+
+ Parameters
+ ==========
+ total_seconds (float/int)
+
+ Returns
+ ==========
+ tuple: hours, minutes, seconds
+ """
+
+ # use np.round instead round to prevent errors in round(float("nan"))
+ hours = np.round(total_seconds // 3600)
+ minutes = np.round((total_seconds % 3600) // 60)
+ seconds = np.round(total_seconds % 60)
+ return hours, minutes, seconds
+
+
+def is_valid_value(value):
+ """
+ Check if the value is considered valid for inclusion in metadata.
+ Valid values are non-None, and if they are str/list/tuple/dict, they should have a positive length.
+
+ Parameters
+ ==========
+ value (any): The value to check.
+
+ Returns
+ ==========
+ bool: True if the value is valid, False otherwise.
+ """
+ # Check if the value is None
+ if value is None:
+ return False
+ # Check if the value is a collection with length
+ if isinstance(value, (str, list, tuple, dict)):
+ return len(value) > 0
+ # All other non-None values are considered valid
+ return True
+
+
+# -----------------------------------------------------------------------------
+# :author: Juanjuan Huang
+# :email: juanjuan.huang@anl.gov
+# :copyright: Copyright © 2024, UChicago Argonne, LLC
+#
+# Distributed under the terms of the 3-Clause BSD License
+#
+# The full license is in the file LICENSE, distributed with this software.
+#
+# DISCLAIMER
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# -----------------------------------------------------------------------------
diff --git a/src/firefly/plans/xafs_scan.py b/src/firefly/plans/xafs_scan.py
index 465a68a0..4adc6aa0 100644
--- a/src/firefly/plans/xafs_scan.py
+++ b/src/firefly/plans/xafs_scan.py
@@ -4,11 +4,13 @@
import numpy as np
from bluesky_queueserver_api import BPlan
from qtpy import QtWidgets
+from qtpy.QtCore import QObject, Signal
from qtpy.QtGui import QDoubleValidator
from xraydb.xraydb import XrayDB
from firefly import display
from firefly.application import FireflyApplication
+from firefly.plans.util import is_valid_value, time_converter
from haven.energy_ranges import (
E_step_to_k_step,
ERange,
@@ -61,9 +63,30 @@ def setup_ui(self):
Qlabels_all["Exposure [s]"].setFixedWidth(68)
-class XafsScanRegion:
+class XafsScanRegion(QObject):
+ time_calculation_signal = Signal()
+
def __init__(self):
+ super().__init__()
self.setup_ui()
+ self.kErange = None
+ self.xafs_region_time = (
+ 0 # flag for whether time is calculated correctly, if not, will set to -1
+ )
+
+ # List of widgets and their signals to connect to update_total_time
+ widgets_signals = [
+ (self.start_line_edit, "textChanged"),
+ (self.stop_line_edit, "textChanged"),
+ (self.step_line_edit, "textChanged"),
+ (self.weight_spinbox, "valueChanged"),
+ (self.exposure_time_spinbox, "valueChanged"),
+ (self.k_space_checkbox, "stateChanged"),
+ ]
+
+ # Connect all signals to the update_total_time method
+ for widget, signal_name in widgets_signals:
+ getattr(widget, signal_name).connect(self.update_total_time)
def setup_ui(self):
self.layout = QtWidgets.QHBoxLayout()
@@ -88,8 +111,9 @@ def setup_ui(self):
# Energy step box
self.step_line_edit = QtWidgets.QLineEdit()
self.step_line_edit.setValidator(
- QDoubleValidator(0.0, float("inf"), 2)
- ) # only takes positive floats
+ QDoubleValidator(0.0, float("inf"), 2) # the step is always bigger than 0
+ )
+ # only takes positive floats
self.step_line_edit.setPlaceholderText("Step…")
self.layout.addWidget(self.step_line_edit)
@@ -97,6 +121,7 @@ def setup_ui(self):
self.weight_spinbox = QtWidgets.QDoubleSpinBox()
self.layout.addWidget(self.weight_spinbox)
self.weight_spinbox.setDecimals(1)
+ self.weight_spinbox.setEnabled(False)
# K-space checkbox
self.k_space_checkbox = QtWidgets.QCheckBox()
@@ -118,8 +143,9 @@ def update_line_edit_value(self, line_edit, conversion_func):
converted_value = conversion_func(round(float(text), float_accuracy))
line_edit.setText(f"{converted_value:.6g}")
- def update_wavenumber_energy(self):
- is_k_checked = self.k_space_checkbox.isChecked()
+ def update_wavenumber_energy(self, is_k_checked):
+ # disable weight box when k is not selected
+ self.weight_spinbox.setEnabled(is_k_checked)
# Define conversion functions
conversion_funcs = {
@@ -151,6 +177,42 @@ def update_wavenumber_energy(self):
else:
self.update_line_edit_value(line_edit, func)
+ def update_total_time(self):
+ weight = self.weight_spinbox.value()
+ exposure_time = self.exposure_time_spinbox.value()
+
+ # prevent invalid inputs such as nan
+ try:
+ start = round(float(self.start_line_edit.text()), float_accuracy)
+ stop = round(float(self.stop_line_edit.text()), float_accuracy)
+ step = round(float(self.step_line_edit.text()), float_accuracy)
+
+ # when the round doesn't work for nan values
+ except ValueError:
+ self.kErange = []
+ start, stop, step = float("nan"), float("nan"), float("nan")
+
+ if self.k_space_checkbox.isChecked():
+ self.kErange = KRange(
+ k_min=start,
+ k_max=stop,
+ k_step=step,
+ k_weight=weight,
+ exposure=exposure_time,
+ )
+
+ else:
+ self.kErange = ERange(
+ E_min=start,
+ E_max=stop,
+ E_step=step,
+ weight=weight,
+ exposure=exposure_time,
+ )
+
+ # Emit the signal regardless of success or failure
+ self.time_calculation_signal.emit()
+
class XafsScanDisplay(display.FireflyDisplay):
min_energy = 4000
@@ -202,6 +264,11 @@ def customize_ui(self):
self.title_region.regions_all_checkbox.stateChanged.connect(
self.on_regions_all_checkbox
)
+ # connect is_standard with a warning box
+ self.ui.checkBox_is_standard.clicked.connect(self.on_is_standard)
+
+ # connect num. of scans with total_time
+ self.ui.spinBox_repeat_scan_num.valueChanged.connect(self.update_total_time)
def on_region_checkbox(self):
for region_i in self.regions:
@@ -276,6 +343,9 @@ def reset_default_regions(self):
region_i.stop_line_edit.setText(str(default_regions[i][1]))
region_i.step_line_edit.setText(str(default_regions[i][2]))
+ # reset scan repeat num to 1
+ self.ui.spinBox_repeat_scan_num.setValue(1)
+
def add_regions(self, num=1):
for i in range(num):
region = XafsScanRegion()
@@ -283,6 +353,9 @@ def add_regions(self, num=1):
self.regions.append(region)
# disable/enabale regions when selected
region.region_checkbox.stateChanged.connect(self.on_region_checkbox)
+ region.region_checkbox.stateChanged.connect(self.update_total_time)
+ # receive time signals from XafsRegion
+ region.time_calculation_signal.connect(self.update_total_time)
def remove_regions(self, num=1):
for i in range(num):
@@ -304,6 +377,49 @@ def update_regions(self):
elif diff_region_num > 0:
self.add_regions(diff_region_num)
+ def update_total_time(self):
+ # Summing total_time for all checked regions directly within the sum function using a generator expression
+ kEranges_all = [
+ region_i.kErange
+ for region_i in self.regions
+ if region_i.region_checkbox.isChecked()
+ ]
+
+ # prevent end points are smaller than start points
+ try:
+ _, exposures = merge_ranges(*kEranges_all, sort=True)
+ total_time_per_scan = exposures.sum()
+ except ValueError:
+ total_time_per_scan = float("nan")
+
+ # calculate time for each scan
+ hr, min, sec = time_converter(total_time_per_scan)
+ self.ui.label_hour_scan.setText(str(hr))
+ self.ui.label_min_scan.setText(str(min))
+ self.ui.label_sec_scan.setText(str(sec))
+
+ # calculate time for entire planf
+ num_scan_repeat = self.ui.spinBox_repeat_scan_num.value()
+ total_time = num_scan_repeat * total_time_per_scan
+ hr_total, min_total, sec_total = time_converter(total_time)
+
+ self.ui.label_hour_total.setText(str(hr_total))
+ self.ui.label_min_total.setText(str(min_total))
+ self.ui.label_sec_total.setText(str(sec_total))
+
+ def on_is_standard(self, is_checked):
+ # if is_standard checked, warn that the data will be used for public
+ if is_checked:
+ response = QtWidgets.QMessageBox.warning(
+ self,
+ "Notice",
+ "When checking this option, this data will be used by public.",
+ QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
+ QtWidgets.QMessageBox.No,
+ )
+ if response != QtWidgets.QMessageBox.Yes:
+ self.ui.checkBox_is_standard.setChecked(False)
+
def queue_plan(self, *args, **kwargs):
"""Execute this plan on the queueserver."""
# Get parameters from each rows of line regions:
@@ -354,11 +470,15 @@ def queue_plan(self, *args, **kwargs):
energies = list(np.round(energies, float_accuracy))
exposures = list(np.round(exposures, float_accuracy))
detectors = self.ui.detectors_list.selected_detectors()
+ repeat_scan_num = int(self.ui.spinBox_repeat_scan_num.value())
md = {
"sample": self.ui.lineEdit_sample.text(),
"purpose": self.ui.lineEdit_purpose.text(),
"is_standard": self.ui.checkBox_is_standard.isChecked(),
+ "notes": self.ui.textEdit_notes.toPlainText(),
}
+ # Only include metadata that isn't an empty string
+ md = {key: val for key, val in md.items() if is_valid_value(val)}
# Check that an absorption edge was selected
if self.use_edge_checkbox.isChecked():
@@ -366,13 +486,14 @@ def queue_plan(self, *args, **kwargs):
match = re.findall(r"\d+\.?\d*", self.edge_combo_box.currentText())
self.edge_value = round(float(match[-1]), float_accuracy)
- except:
+ except IndexError:
QtWidgets.QMessageBox.warning(
self, "Error", "Please select an absorption edge."
)
return None
else:
self.edge_value = 0
+
# Build the queue item
item = BPlan(
"energy_scan",
@@ -385,7 +506,9 @@ def queue_plan(self, *args, **kwargs):
# Submit the item to the queueserver
app = FireflyApplication.instance()
log.info(f"Adding XAFS scan to queue.")
- app.add_queue_item(item)
+ # repeat scans
+ for i in range(repeat_scan_num):
+ app.add_queue_item(item)
def ui_filename(self):
return "plans/xafs_scan.ui"
@@ -393,7 +516,7 @@ def ui_filename(self):
# -----------------------------------------------------------------------------
# :author: Juanjuan Huang
-# :email: wolfman@anl.gov
+# :email: juanjuan.huang@anl.gov
# :copyright: Copyright © 2024, UChicago Argonne, LLC
#
# Distributed under the terms of the 3-Clause BSD License
diff --git a/src/firefly/plans/xafs_scan.ui b/src/firefly/plans/xafs_scan.ui
index 2446a1aa..c988f749 100644
--- a/src/firefly/plans/xafs_scan.ui
+++ b/src/firefly/plans/xafs_scan.ui
@@ -6,8 +6,8 @@
0
0
- 840
- 302
+ 901
+ 365
@@ -109,6 +109,19 @@
+ -
+
+
+ Reset to default values
+
+
+
+
+
+ Reset Defaults
+
+
+
-
@@ -131,7 +144,7 @@
0
0
700
- 165
+ 156
@@ -155,6 +168,19 @@
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
-
@@ -292,62 +318,73 @@
-
-
-
-
-
-
- Reset to default values
-
-
-
-
+
+
-
+
- Reset Defaults
+ Sample name
- -
-
+
-
+
- Sample name
+ Notes
- -
-
+
-
+
- Type this sample's name
+ Type the experimental purpose note for this scan
- Sample name
+ Purpose
- -
-
-
- Is standard
+
-
+
+
+ Type this sample's name
+
+
+ Sample name
- -
+
-
Experiment purpose
- -
-
-
- Type the experimental purpose note for this scan
-
-
- Purpose
-
-
+
-
+
+
-
+
+
+ total exposure time for a single scan
+
+
+ Exposure time each scan
+
+
+
+ -
+
+
+ total exposure time for a single scan
+
+
+ Total exposure time
+
+
+
+
- -
+
-
Qt::Horizontal
@@ -360,10 +397,203 @@
- -
+
-
+
+
+ Do this scan by multiple times
+
+
+ 1
+
+
+ 999
+
+
+
+ -
+
+
+ Do this scan by multiple times
+
+
+ Num. of scans
+
+
+
+ -
+
+
-
+
+
-
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ h
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ min
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ s
+
+
+ Qt::AlignCenter
+
+
+
+
+
+ -
+
+
-
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ h
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ min
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ 0
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ s
+
+
+ Qt::AlignCenter
+
+
+
+
+
+
+
+ -
+
+
+ Is standard
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 60
+
+
+
+ 1
+
+
+ 14
+
+
+ 80
+
+
+ Additional notes about the sample
+
+
+
+ -
- Add to the queue
+ Run/Add to the queue
# background-color: #28a745
diff --git a/src/firefly/tests/test_grid_scan_window.py b/src/firefly/tests/test_grid_scan_window.py
new file mode 100644
index 00000000..1fc51828
--- /dev/null
+++ b/src/firefly/tests/test_grid_scan_window.py
@@ -0,0 +1,121 @@
+from unittest import mock
+
+import pytest
+from bluesky_queueserver_api import BPlan
+from ophyd.sim import make_fake_device
+from qtpy import QtCore
+
+from firefly.application import FireflyApplication
+from firefly.plans.grid_scan import GridScanDisplay
+from haven.instrument import motor
+
+
+@pytest.fixture
+def fake_motors(sim_registry):
+ motor_names = ["motorA_m1", "motorA_m2"]
+ motors = []
+ for name in motor_names:
+ this_motor = make_fake_device(motor.HavenMotor)(name=name, labels={"motors"})
+ sim_registry.register(this_motor)
+ motors.append(this_motor)
+ return motors
+
+
+def test_time_calculator(qtbot, sim_registry, fake_motors, dxp, I0):
+ app = FireflyApplication.instance()
+ display = GridScanDisplay()
+ qtbot.addWidget(display)
+
+ # set up motor num
+ display.ui.num_motor_spin_box.setValue(2)
+
+ # set up num of repeat scans
+ display.ui.spinBox_repeat_scan_num.setValue(6)
+
+ # set up scan num of points
+ display.ui.scan_pts_spin_box.setValue(10)
+
+ # set up detectors
+ display.ui.detectors_list.selected_detectors = mock.MagicMock(
+ return_value=["vortex_me4", "I0"]
+ )
+
+ # set up default timing for the detector
+ detectors = display.ui.detectors_list.selected_detectors()
+ detectors = {name: app.registry[name] for name in detectors}
+ detectors["I0"].default_time_signal.set(1).wait(2)
+ detectors["vortex_me4"].default_time_signal.set(0.5).wait(2)
+
+ # Create empty QItemSelection objects
+ selected = QtCore.QItemSelection()
+ deselected = QtCore.QItemSelection()
+
+ # emit the signal so that the time calculator is triggered
+ display.ui.detectors_list.selectionModel().selectionChanged.emit(
+ selected, deselected
+ )
+
+ # Check whether time is calculated correctly for a single scan
+ assert int(display.ui.label_hour_scan.text()) == 0
+ assert int(display.ui.label_min_scan.text()) == 0
+ assert int(display.ui.label_sec_scan.text()) == 20
+
+ # Check whether time is calculated correctly including the repeated scan
+ assert int(display.ui.label_hour_total.text()) == 0
+ assert int(display.ui.label_min_total.text()) == 2
+ assert int(display.ui.label_sec_total.text()) == 0
+
+
+def test_grid_scan_plan_queued(ffapp, qtbot, sim_registry, fake_motors):
+ display = GridScanDisplay()
+ display.ui.run_button.setEnabled(True)
+ display.ui.num_motor_spin_box.setValue(2)
+ display.update_regions()
+
+ # set up a test motor 1
+ display.regions[0].motor_box.combo_box.setCurrentText("motorA_m1")
+ display.regions[0].start_line_edit.setText("1")
+ display.regions[0].stop_line_edit.setText("111")
+ # select snake for the first motor
+ display.regions[0].snake_checkbox.setChecked(True)
+
+ # set up a test motor 2
+ display.regions[1].motor_box.combo_box.setCurrentText("motorA_m2")
+ display.regions[1].start_line_edit.setText("2")
+ display.regions[1].stop_line_edit.setText("222")
+
+ # set up scan num of points
+ display.ui.scan_pts_spin_box.setValue(10)
+
+ # set up detector list
+ display.ui.detectors_list.selected_detectors = mock.MagicMock(
+ return_value=["vortex_me4", "I0"]
+ )
+
+ # set up meta data
+ display.ui.lineEdit_sample.setText("sam")
+ display.ui.lineEdit_purpose.setText("test")
+ display.ui.textEdit_notes.setText("notes")
+
+ expected_item = BPlan(
+ "grid_scan",
+ ["vortex_me4", "I0"],
+ "motorA_m1",
+ 1,
+ 111,
+ "motorA_m2",
+ 2,
+ 222,
+ num=10,
+ snake_axes=["motorA_m1"],
+ md={"sample": "sam", "purpose": "test", "notes": "notes"},
+ )
+
+ def check_item(item):
+ return item.to_dict() == expected_item.to_dict()
+
+ # Click the run button and see if the plan is queued
+ with qtbot.waitSignal(
+ ffapp.queue_item_added, timeout=1000, check_params_cb=check_item
+ ):
+ qtbot.mouseClick(display.ui.run_button, QtCore.Qt.LeftButton)
diff --git a/src/firefly/tests/test_line_scan_window.py b/src/firefly/tests/test_line_scan_window.py
index e3e6867d..30e4a570 100644
--- a/src/firefly/tests/test_line_scan_window.py
+++ b/src/firefly/tests/test_line_scan_window.py
@@ -5,6 +5,7 @@
from ophyd.sim import make_fake_device
from qtpy import QtCore
+from firefly.application import FireflyApplication
from firefly.plans.line_scan import LineScanDisplay
from haven.instrument import motor
@@ -19,10 +20,58 @@ def fake_motors(sim_registry):
return motors
-def test_line_scan_plan_queued(ffapp, qtbot, sim_registry, fake_motors):
+def test_time_calculator(qtbot, sim_registry, fake_motors, dxp, I0):
+ app = FireflyApplication.instance()
+ display = LineScanDisplay()
+ qtbot.addWidget(display)
+
+ # set up motor num
+ display.ui.num_motor_spin_box.setValue(2)
+
+ # set up num of repeat scans
+ display.ui.spinBox_repeat_scan_num.setValue(6)
+
+ # set up scan num of points
+ display.ui.scan_pts_spin_box.setValue(10)
+
+ # set up detectors
+ display.ui.detectors_list.selected_detectors = mock.MagicMock(
+ return_value=["vortex_me4", "I0"]
+ )
+
+ # set up default timing for the detector
+ detectors = display.ui.detectors_list.selected_detectors()
+ detectors = {name: app.registry[name] for name in detectors}
+ detectors["I0"].default_time_signal.set(1).wait(2)
+ detectors["vortex_me4"].default_time_signal.set(0.5).wait(2)
+
+ # Create empty QItemSelection objects
+ selected = QtCore.QItemSelection()
+ deselected = QtCore.QItemSelection()
+
+ # emit the signal so that the time calculator is triggered
+ display.ui.detectors_list.selectionModel().selectionChanged.emit(
+ selected, deselected
+ )
+
+ # Check whether time is calculated correctly for a single scan
+ assert int(display.ui.label_hour_scan.text()) == 0
+ assert int(display.ui.label_min_scan.text()) == 0
+ assert int(display.ui.label_sec_scan.text()) == 10
+
+ # Check whether time is calculated correctly including the repeated scan
+ assert int(display.ui.label_hour_total.text()) == 0
+ assert int(display.ui.label_min_total.text()) == 1
+ assert int(display.ui.label_sec_total.text()) == 0
+
+
+def test_line_scan_plan_queued(ffapp, qtbot, sim_registry, fake_motors, dxp, I0):
display = LineScanDisplay()
display.ui.run_button.setEnabled(True)
+
+ # set up motor num
display.ui.num_motor_spin_box.setValue(2)
+
display.update_regions()
# set up a test motor 1
@@ -38,7 +87,7 @@ def test_line_scan_plan_queued(ffapp, qtbot, sim_registry, fake_motors):
# set up scan num of points
display.ui.scan_pts_spin_box.setValue(10)
- # set up detector list
+ # time is calculated when the selection is changed
display.ui.detectors_list.selected_detectors = mock.MagicMock(
return_value=["vortex_me4", "I0"]
)
@@ -46,18 +95,19 @@ def test_line_scan_plan_queued(ffapp, qtbot, sim_registry, fake_motors):
# set up meta data
display.ui.lineEdit_sample.setText("sam")
display.ui.lineEdit_purpose.setText("test")
+ display.ui.textEdit_notes.setText("notes")
expected_item = BPlan(
"scan",
["vortex_me4", "I0"],
"motorA_m1",
- 1,
- 111,
+ 1.0,
+ 111.0,
"motorA_m2",
- 2,
- 222,
+ 2.0,
+ 222.0,
num=10,
- md={"sample": "sam", "purpose": "test"},
+ md={"sample": "sam", "purpose": "test", "notes": "notes"},
)
def check_item(item):
diff --git a/src/firefly/tests/test_move_motor_window.py b/src/firefly/tests/test_move_motor_window.py
new file mode 100644
index 00000000..217f317b
--- /dev/null
+++ b/src/firefly/tests/test_move_motor_window.py
@@ -0,0 +1,67 @@
+import pytest
+from bluesky_queueserver_api import BPlan
+from ophyd.sim import make_fake_device
+from qtpy import QtCore
+
+from firefly.application import FireflyApplication
+from firefly.plans.move_motor_window import MoveMotorDisplay
+from haven.instrument import motor
+
+
+@pytest.fixture
+def fake_motors(sim_registry):
+ motor_names = ["motorA_m1", "motorA_m2"]
+ motors = []
+ for name in motor_names:
+ this_motor = make_fake_device(motor.HavenMotor)(name=name, labels={"motors"})
+ motors.append(this_motor)
+ return motors
+
+
+def test_move_motor_plan_queued(ffapp, qtbot, sim_registry, fake_motors):
+ app = FireflyApplication.instance()
+ display = MoveMotorDisplay()
+ display.ui.run_button.setEnabled(True)
+
+ # set up motor num
+ display.ui.num_motor_spin_box.setValue(2)
+
+ # uncheck relative
+ display.ui.relative_scan_checkbox.setChecked(False)
+
+ display.update_regions()
+
+ # set up a test motor 1
+ display.regions[0].motor_box.combo_box.setCurrentText("motorA_m1")
+ display.regions[0].position_line_edit.setText("111")
+
+ # set up a test motor 2
+ display.regions[1].motor_box.combo_box.setCurrentText("motorA_m2")
+ display.regions[1].position_line_edit.setText("222")
+
+ # set up meta data
+ display.ui.lineEdit_sample.setText("sam")
+ display.ui.lineEdit_purpose.setText("test")
+
+ expected_item = BPlan(
+ "mv",
+ "motorA_m1",
+ 111.0,
+ "motorA_m2",
+ 222.0,
+ md={
+ "sample": "sam",
+ "purpose": "test",
+ },
+ )
+
+ # print(item.to_dict())
+
+ def check_item(item):
+ return item.to_dict() == expected_item.to_dict()
+
+ # Click the run button and see if the plan is queued
+ with qtbot.waitSignal(
+ ffapp.queue_item_added, timeout=1000, check_params_cb=check_item
+ ):
+ qtbot.mouseClick(display.ui.run_button, QtCore.Qt.LeftButton)
diff --git a/src/firefly/tests/test_xafs_scan.py b/src/firefly/tests/test_xafs_scan.py
index 4031ab6e..07bd852d 100644
--- a/src/firefly/tests/test_xafs_scan.py
+++ b/src/firefly/tests/test_xafs_scan.py
@@ -107,6 +107,8 @@ def test_xafs_scan_plan_queued_energies(ffapp, qtbot):
display.ui.lineEdit_sample.setText("sam")
display.ui.lineEdit_purpose.setText("test")
display.ui.checkBox_is_standard.setChecked(True)
+ display.ui.lineEdit_purpose.setText("test")
+ display.ui.textEdit_notes.setText("sam_notes")
expected_item = BPlan(
"energy_scan",
@@ -114,7 +116,12 @@ def test_xafs_scan_plan_queued_energies(ffapp, qtbot):
exposure=exposures,
E0=11500.8,
detectors=["vortex_me4", "I0"],
- md={"sample": "sam", "purpose": "test", "is_standard": True},
+ md={
+ "sample": "sam",
+ "purpose": "test",
+ "is_standard": True,
+ "notes": "sam_notes",
+ },
)
def check_item(item):
@@ -154,7 +161,6 @@ def check_item(item):
qtbot.mouseClick(display.ui.run_button, QtCore.Qt.LeftButton)
-# TODO K end point should not include step
def test_xafs_scan_plan_queued_energies_k_mixed(ffapp, qtbot):
display = XafsScanDisplay()
display.ui.regions_spin_box.setValue(2)
@@ -178,6 +184,10 @@ def test_xafs_scan_plan_queued_energies_k_mixed(ffapp, qtbot):
display.ui.detectors_list.selected_detectors = mock.MagicMock(
return_value=["vortex_me4", "I0"]
)
+
+ # set repeat scan num to 2
+ display.ui.spinBox_repeat_scan_num.setValue(3)
+
energies = np.array(
[
-20,
@@ -195,9 +205,11 @@ def test_xafs_scan_plan_queued_energies_k_mixed(ffapp, qtbot):
exposures = np.array(
[1, 1, 1, 1, 1, 1, 1, 1, 5.665, 14.141]
) # k exposures kmin 3.62263
+
# set up meta data
display.ui.lineEdit_sample.setText("sam")
- display.ui.lineEdit_purpose.setText("test")
+ display.ui.lineEdit_purpose.setText("") # invalid input should be removed from md
+ display.ui.textEdit_notes.setText("sam_notes")
expected_item = BPlan(
"energy_scan",
@@ -205,7 +217,11 @@ def test_xafs_scan_plan_queued_energies_k_mixed(ffapp, qtbot):
exposure=exposures,
E0=11500.8,
detectors=["vortex_me4", "I0"],
- md={"sample": "sam", "purpose": "test", "is_standard": False},
+ md={
+ "sample": "sam",
+ "is_standard": False,
+ "notes": "sam_notes",
+ },
)
def check_item(item):
@@ -213,6 +229,16 @@ def check_item(item):
expected_dict = expected_item.to_dict()["kwargs"]
try:
+ # Check whether time is calculated correctly for a single scan
+ assert int(float(display.ui.label_hour_scan.text())) == 0
+ assert int(float(display.ui.label_min_scan.text())) == 0
+ assert int(float(display.ui.label_sec_scan.text())) == 28
+
+ # Check whether time is calculated correctly including the repeated scan
+ assert int(float(display.ui.label_hour_total.text())) == 0
+ assert int(float(display.ui.label_min_total.text())) == 1
+ assert int(float(display.ui.label_sec_total.text())) == 23
+
# Check energies & exposures within 3 decimals
np.testing.assert_array_almost_equal(
item_dict["energies"], expected_dict["energies"], decimal=2
@@ -231,6 +257,7 @@ def check_item(item):
assert item_dict == expected_dict, "Non-array items do not match."
except AssertionError as e:
+ print(e)
return False
return True
diff --git a/src/haven/energy_ranges.py b/src/haven/energy_ranges.py
index 7bf88ec9..556ee9b8 100644
--- a/src/haven/energy_ranges.py
+++ b/src/haven/energy_ranges.py
@@ -105,7 +105,9 @@ def energies(self):
def exposures(self):
"""Convert the range to a sequence of exposure times, in seconds."""
- return self.exposure * self.energies() ** self.weight
+ # disable weight for now
+ # return self.exposure * self.energies() ** self.weight
+ return self.exposure * self.energies() ** 0 # do not consider weights for now
@dataclass