copied over new macros from pxii #38
No files matched your search
@@ -44,5 +44,5 @@ if _args.session.lower() == "alignment":
|
||||
bec._ip.prompts.username = _session_name
|
||||
bec._ip.prompts.status = 1
|
||||
|
||||
d, planner = init_beamline_environment()
|
||||
#d, planner = init_beamline_environment()
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
[BECDockArea]
|
||||
acceptDrops=false
|
||||
accessibleDescription=
|
||||
accessibleIdentifier=
|
||||
accessibleName=
|
||||
autoFillBackground=false
|
||||
baseSize=@Size(0 0)
|
||||
contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1)
|
||||
cursor=@Variant(\0\0\0J\0\0)
|
||||
enabled=true
|
||||
focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0)
|
||||
geometry=@Rect(0 23 1670 1505)
|
||||
inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0)
|
||||
layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0)
|
||||
maximumSize=@Size(16777215 16777215)
|
||||
minimumSize=@Size(0 0)
|
||||
mode=creator
|
||||
mouseTracking=false
|
||||
sizeIncrement=@Size(0 0)
|
||||
sizePolicy=@Variant(\0\0\0K\0\0\0U)
|
||||
statusTip=
|
||||
tabletTracking=false
|
||||
toolTip=
|
||||
toolTipDuration=-1
|
||||
whatsThis=
|
||||
windowFilePath=
|
||||
windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0)
|
||||
windowModified=false
|
||||
windowOpacity=1
|
||||
windowTitle=BEC - TestLayout4
|
||||
workspace_is_locked=false
|
||||
|
||||
[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Info.dockWidgetScrollArea.qt_scrollarea_viewport.Info]
|
||||
acceptDrops=false
|
||||
accessibleDescription=
|
||||
accessibleIdentifier=
|
||||
accessibleName=
|
||||
autoFillBackground=true
|
||||
baseSize=@Size(0 0)
|
||||
contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1)
|
||||
cursor=@Variant(\0\0\0J\0\0)
|
||||
enabled=true
|
||||
focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0)
|
||||
geometry=@Rect(0 0 816 1426)
|
||||
html_text="<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n<html><head><meta name=\"qrichtext\" content=\"1\" /><meta charset=\"utf-8\" /><style type=\"text/css\">\np, li { white-space: pre-wrap; }\nhr { height: 1px; border-width: 0; }\nli.unchecked::marker { content: \"\\2610\"; }\nli.checked::marker { content: \"\\2612\"; }\n</style></head><body style=\" font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;\">\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Starting daily beamline setup</p>\n<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>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Machine current OK (401.8 mA)</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Beamline is not offline, (operation mode is attended)</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ FE shutter is open</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Hutch shutter is open</p>\n<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>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ SS Xray eye is OUT</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Cryostream temperature: 99.96 K</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Detector height: 62.0 mm</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Beamstop blocking beam: I2 = 8.76e-09</p>\n<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>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Beamline energy set to 12400 eV</p>\n<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>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Beam aligned</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Displacement: Beam X: -0.1 microns, Beam Y: -0.3 microns</p>\n<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>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Flux measured</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Intensity at current transmission: 8.62e+11 ph/s</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Intensity at 100% transmission: 8.53e+12 ph/s</p>\n<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>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Total duration: 39.7 s</p></body></html>"
|
||||
inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0)
|
||||
layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0)
|
||||
maximumSize=@Size(16777215 16777215)
|
||||
minimumSize=@Size(640 310)
|
||||
mouseTracking=false
|
||||
plain_text="Starting daily beamline setup\n\n✓ Machine current OK (401.8 mA)\n✓ Beamline is not offline, (operation mode is attended)\n✓ FE shutter is open\n✓ Hutch shutter is open\n\n✓ SS Xray eye is OUT\n✓ Cryostream temperature: 99.96 K\n✓ Detector height: 62.0 mm\n✓ Beamstop blocking beam: I2 = 8.76e-09\n\n✓ Beamline energy set to 12400 eV\n\n✓ Beam aligned\nDisplacement: Beam X: -0.1 microns, Beam Y: -0.3 microns\n\n✓ Flux measured\nIntensity at current transmission: 8.62e+11 ph/s\nIntensity at 100% transmission: 8.53e+12 ph/s\n\nTotal duration: 39.7 s"
|
||||
sizeIncrement=@Size(0 0)
|
||||
sizePolicy=@Variant(\0\0\0K\0\0\0U)
|
||||
statusTip=
|
||||
tabletTracking=false
|
||||
toolTip=
|
||||
toolTipDuration=-1
|
||||
whatsThis=
|
||||
windowFilePath=
|
||||
windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0)
|
||||
windowModified=false
|
||||
windowOpacity=1
|
||||
windowTitle=
|
||||
|
||||
[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_1.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_1]
|
||||
acceptDrops=false
|
||||
accessibleDescription=
|
||||
accessibleIdentifier=
|
||||
accessibleName=
|
||||
autoFillBackground=true
|
||||
auto_range_x=true
|
||||
auto_range_y=false
|
||||
baseSize=@Size(0 0)
|
||||
color_palette=plasma
|
||||
contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1)
|
||||
cursor=@Variant(\0\0\0J\0\0)
|
||||
curve_json=[]
|
||||
enable_fps_monitor=false
|
||||
enable_popups=true
|
||||
enable_side_panel=false
|
||||
enable_toolbar=true
|
||||
enabled=true
|
||||
focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0)
|
||||
geometry=@Rect(0 0 575 341)
|
||||
inner_axes=true
|
||||
inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0)
|
||||
invert_x=false
|
||||
invert_y=false
|
||||
layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0)
|
||||
legend_label_size=9
|
||||
lock_aspect_ratio=false
|
||||
max_dataset_size_mb=10
|
||||
maximumSize=@Size(16777215 16777215)
|
||||
minimal_crosshair_precision=3
|
||||
minimumSize=@Size(384 200)
|
||||
mouseTracking=false
|
||||
outer_axes=false
|
||||
signal_x=dcm_pitch
|
||||
sizeIncrement=@Size(0 0)
|
||||
sizePolicy=@Variant(\0\0\0K\0\0\0U)
|
||||
skip_large_dataset_check=false
|
||||
skip_large_dataset_warning=false
|
||||
statusTip=
|
||||
tabletTracking=false
|
||||
title=Scan: Current
|
||||
toolTip=
|
||||
toolTipDuration=-1
|
||||
whatsThis=
|
||||
windowFilePath=
|
||||
windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0)
|
||||
windowModified=false
|
||||
windowOpacity=1
|
||||
windowTitle=
|
||||
x_grid=false
|
||||
x_label=dcm_pitch
|
||||
x_limits=@Variant(\0\0\0\x1a\xc0\x1a\xb0\xb0)(\x17v\xc0\x15\xa0\xc4\x15n&N)
|
||||
x_log=false
|
||||
x_mode=dcm_pitch
|
||||
y_grid=false
|
||||
y_label=lu_bpmsum
|
||||
y_limits=@Variant(\0\0\0\x1a\xc0g\xe2\xe0\x3|_\x2@\x9d\xf0\x41\x91k\xed@)
|
||||
y_log=false
|
||||
|
||||
[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_2.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_2]
|
||||
acceptDrops=false
|
||||
accessibleDescription=
|
||||
accessibleIdentifier=
|
||||
accessibleName=
|
||||
autoFillBackground=true
|
||||
auto_range_x=true
|
||||
auto_range_y=true
|
||||
baseSize=@Size(0 0)
|
||||
color_palette=plasma
|
||||
contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1)
|
||||
cursor=@Variant(\0\0\0J\0\0)
|
||||
curve_json=[]
|
||||
enable_fps_monitor=false
|
||||
enable_popups=true
|
||||
enable_side_panel=false
|
||||
enable_toolbar=true
|
||||
enabled=true
|
||||
focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0)
|
||||
geometry=@Rect(0 0 575 304)
|
||||
inner_axes=true
|
||||
inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0)
|
||||
invert_x=false
|
||||
invert_y=false
|
||||
layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0)
|
||||
legend_label_size=9
|
||||
lock_aspect_ratio=false
|
||||
max_dataset_size_mb=10
|
||||
maximumSize=@Size(16777215 16777215)
|
||||
minimal_crosshair_precision=3
|
||||
minimumSize=@Size(384 200)
|
||||
mouseTracking=false
|
||||
outer_axes=false
|
||||
signal_x=
|
||||
sizeIncrement=@Size(0 0)
|
||||
sizePolicy=@Variant(\0\0\0K\0\0\0U)
|
||||
skip_large_dataset_check=false
|
||||
skip_large_dataset_warning=false
|
||||
statusTip=
|
||||
tabletTracking=false
|
||||
title=
|
||||
toolTip=
|
||||
toolTipDuration=-1
|
||||
whatsThis=
|
||||
windowFilePath=
|
||||
windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0)
|
||||
windowModified=false
|
||||
windowOpacity=1
|
||||
windowTitle=
|
||||
x_grid=false
|
||||
x_label=
|
||||
x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0)
|
||||
x_log=false
|
||||
x_mode=auto
|
||||
y_grid=false
|
||||
y_label=
|
||||
y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0)
|
||||
y_log=false
|
||||
|
||||
[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_3.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_3]
|
||||
acceptDrops=false
|
||||
accessibleDescription=
|
||||
accessibleIdentifier=
|
||||
accessibleName=
|
||||
autoFillBackground=true
|
||||
auto_range_x=true
|
||||
auto_range_y=true
|
||||
baseSize=@Size(0 0)
|
||||
color_palette=plasma
|
||||
contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1)
|
||||
cursor=@Variant(\0\0\0J\0\0)
|
||||
curve_json=[]
|
||||
enable_fps_monitor=false
|
||||
enable_popups=true
|
||||
enable_side_panel=false
|
||||
enable_toolbar=true
|
||||
enabled=true
|
||||
focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0)
|
||||
geometry=@Rect(0 0 575 358)
|
||||
inner_axes=true
|
||||
inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0)
|
||||
invert_x=false
|
||||
invert_y=false
|
||||
layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0)
|
||||
legend_label_size=9
|
||||
lock_aspect_ratio=false
|
||||
max_dataset_size_mb=10
|
||||
maximumSize=@Size(16777215 16777215)
|
||||
minimal_crosshair_precision=3
|
||||
minimumSize=@Size(384 200)
|
||||
mouseTracking=false
|
||||
outer_axes=false
|
||||
signal_x=
|
||||
sizeIncrement=@Size(0 0)
|
||||
sizePolicy=@Variant(\0\0\0K\0\0\0U)
|
||||
skip_large_dataset_check=false
|
||||
skip_large_dataset_warning=false
|
||||
statusTip=
|
||||
tabletTracking=false
|
||||
title=
|
||||
toolTip=
|
||||
toolTipDuration=-1
|
||||
whatsThis=
|
||||
windowFilePath=
|
||||
windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0)
|
||||
windowModified=false
|
||||
windowOpacity=1
|
||||
windowTitle=
|
||||
x_grid=false
|
||||
x_label=
|
||||
x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0)
|
||||
x_log=false
|
||||
x_mode=auto
|
||||
y_grid=false
|
||||
y_label=
|
||||
y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0)
|
||||
y_log=false
|
||||
|
||||
[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Plot_4.dockWidgetScrollArea.qt_scrollarea_viewport.Plot_4]
|
||||
acceptDrops=false
|
||||
accessibleDescription=
|
||||
accessibleIdentifier=
|
||||
accessibleName=
|
||||
autoFillBackground=true
|
||||
auto_range_x=true
|
||||
auto_range_y=true
|
||||
baseSize=@Size(0 0)
|
||||
color_palette=plasma
|
||||
contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1)
|
||||
cursor=@Variant(\0\0\0J\0\0)
|
||||
curve_json=[]
|
||||
enable_fps_monitor=false
|
||||
enable_popups=true
|
||||
enable_side_panel=false
|
||||
enable_toolbar=true
|
||||
enabled=true
|
||||
focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0)
|
||||
geometry=@Rect(0 0 575 294)
|
||||
inner_axes=true
|
||||
inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0)
|
||||
invert_x=false
|
||||
invert_y=false
|
||||
layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0)
|
||||
legend_label_size=9
|
||||
lock_aspect_ratio=false
|
||||
max_dataset_size_mb=10
|
||||
maximumSize=@Size(16777215 16777215)
|
||||
minimal_crosshair_precision=3
|
||||
minimumSize=@Size(384 200)
|
||||
mouseTracking=false
|
||||
outer_axes=false
|
||||
signal_x=
|
||||
sizeIncrement=@Size(0 0)
|
||||
sizePolicy=@Variant(\0\0\0K\0\0\0U)
|
||||
skip_large_dataset_check=false
|
||||
skip_large_dataset_warning=false
|
||||
statusTip=
|
||||
tabletTracking=false
|
||||
title=
|
||||
toolTip=
|
||||
toolTipDuration=-1
|
||||
whatsThis=
|
||||
windowFilePath=
|
||||
windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0)
|
||||
windowModified=false
|
||||
windowOpacity=1
|
||||
windowTitle=
|
||||
x_grid=false
|
||||
x_label=
|
||||
x_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0)
|
||||
x_log=false
|
||||
x_mode=auto
|
||||
y_grid=false
|
||||
y_label=
|
||||
y_limits=@Variant(\0\0\0\x1a\0\0\0\0\0\0\0\0?\xf0\0\0\0\0\0\0)
|
||||
y_log=false
|
||||
|
||||
[BECDockArea.ads%3A%3ACDockManager.ads%3A%3ACDockSplitter.ads%3A%3ACDockSplitter.ads%3A%3ACDockAreaWidget.Progress.dockWidgetScrollArea.qt_scrollarea_viewport.Progress]
|
||||
acceptDrops=false
|
||||
accessibleDescription=
|
||||
accessibleIdentifier=
|
||||
accessibleName=
|
||||
autoFillBackground=true
|
||||
baseSize=@Size(0 0)
|
||||
contextMenuPolicy=@Variant(\0\0\0\x7f\0\0\0\x16Qt::ContextMenuPolicy\0\0\0\0\x1)
|
||||
cursor=@Variant(\0\0\0J\0\0)
|
||||
enabled=true
|
||||
focusPolicy=@Variant(\0\0\0\x7f\0\0\0\x10Qt::FocusPolicy\0\0\0\0\0)
|
||||
geometry=@Rect(0 0 256 1416)
|
||||
html_text="<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n<html><head><meta name=\"qrichtext\" content=\"1\" /><meta charset=\"utf-8\" /><style type=\"text/css\">\np, li { white-space: pre-wrap; }\nhr { height: 1px; border-width: 0; }\nli.unchecked::marker { content: \"\\2610\"; }\nli.checked::marker { content: \"\\2612\"; }\n</style></head><body style=\" font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;\">\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Pre checks</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Set energy</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Move beam</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">– Align beamstop</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">– Align collimator</p>\n<p style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">✓ Measure flux</p></body></html>"
|
||||
inputMethodHints=@Variant(\0\0\0\x7f\0\0\0\x14Qt::InputMethodHint\0\0\0\0\0)
|
||||
layoutDirection=@Variant(\0\0\0\x7f\0\0\0\x14Qt::LayoutDirection\0\0\0\0\0)
|
||||
maximumSize=@Size(16777215 16777215)
|
||||
minimumSize=@Size(256 310)
|
||||
mouseTracking=false
|
||||
plain_text=✓ Pre checks\n✓ Set energy\n✓ Move beam\n– Align beamstop\n– Align collimator\n✓ Measure flux
|
||||
sizeIncrement=@Size(0 0)
|
||||
sizePolicy=@Variant(\0\0\0K\0\0\0U)
|
||||
statusTip=
|
||||
tabletTracking=false
|
||||
toolTip=
|
||||
toolTipDuration=-1
|
||||
whatsThis=
|
||||
windowFilePath=
|
||||
windowModality=@Variant(\0\0\0\x7f\0\0\0\x13Qt::WindowModality\0\0\0\0\0)
|
||||
windowModified=false
|
||||
windowOpacity=1
|
||||
windowTitle=
|
||||
|
||||
[BECDockArea.dockSettingsAction]
|
||||
autoRepeat=true
|
||||
checkable=false
|
||||
checked=false
|
||||
enabled=true
|
||||
iconText=Dock settings
|
||||
iconVisibleInMenu=true
|
||||
menuRole=@Variant(\0\0\0\x7f\0\0\0\x12QAction::MenuRole\0\0\0\0\x1)
|
||||
priority=@Variant(\0\0\0\x7f\0\0\0\x12QAction::Priority\0\0\0\0\x80)
|
||||
shortcut=
|
||||
shortcutContext=@Variant(\0\0\0\x7f\0\0\0\x14Qt::ShortcutContext\0\0\0\0\x1)
|
||||
shortcutVisibleInContextMenu=true
|
||||
statusTip=
|
||||
text=Dock settings
|
||||
toolTip=Dock settings
|
||||
whatsThis=
|
||||
|
||||
[Perspectives]
|
||||
1\Name=BEC - TestLayout4
|
||||
1\State="@ByteArray(\0\0\x3:x\xda\x95\x92\xdfO\x83\x30\x10\xc7\xdf\xfd+.}\x9f@\xcb\x94%\xc0\xb2`\x96\xf8\xe2\x8f\xb0\xe9\xa3\x41\x38I#\xb4\xa6\xed\x16\x35\xfe\xf1\x16\xb6\x10\xa3\x65\x91\x87\xa6\xbd\xef]?\xed\xf7r\xf1\xf2\xbdm`\x8fJs)\x12\x12\x9c\xfb\x4P\x94\xb2\xe2\xa2N\xc8v\xb3\x9e\x45\x64\x99\xc6\xf7\x66U\xed\vQbu%\xcbW\x9b\xcb?\xb4\xc1\x16\x1e\x86\x8b\x4\xb6\x1a\xd5\x10[L&\x85)\xb8\xb0J\x97N\xe3!\x86u#\v\xd3?\xe0[=\x7fk\xb8\x31V\xbeU\x1cm\x89\xe9\x1_\x1d`'LB\xe8?kV\n\v\xd8\x14\xcf\xfds\x90\xed\x94\xc2.u\xa7\x64\xadPk[\xf1\xc8\xab\x1a\r\xdc\x14-\xfe\xd0!k\xa4\xc6\xaa\xfb\x8c\x97\xc6^\x87\x19\x85]\x8b\x17\xf9\x1b\xd4kNH\xce?Q\xa7t\x1e@D)\xc4\xde!\xb6\xfb\xd1\xcd\x98\xaf\xd9\xe0+<\xe1\xab\x91\xe6)\xf8\xe3\xea\xa0N\xf1\xd4_\xa1N\x10\x9d\xe\x62N\x10\x9b\xe\n\x9d\xa0\xf0T\xa7\xd9\xe5\x5\x30\xb6\0\xb6`\xc0\xe8\xc2\xdd\xf1^\t\xfc(\x80\xb9]\xae\x12o\x98T{\x1e\x99\xfc\xf4\xec\x1b\xd0\xbb\tD)"
|
||||
size=1
|
||||
|
||||
[mainWindow]
|
||||
DockingState="@ByteArray(\0\0\x3:x\xda\x95\x92\xdfO\x83\x30\x10\xc7\xdf\xfd+.}\x9f@\xcb\x94%\xc0\xb2`\x96\xf8\xe2\x8f\xb0\xe9\xa3\x41\x38I#\xb4\xa6\xed\x16\x35\xfe\xf1\x16\xb6\x10\xa3\x65\x91\x87\xa6\xbd\xef]?\xed\xf7r\xf1\xf2\xbdm`\x8fJs)\x12\x12\x9c\xfb\x4P\x94\xb2\xe2\xa2N\xc8v\xb3\x9e\x45\x64\x99\xc6\xf7\x66U\xed\vQbu%\xcbW\x9b\xcb?\xb4\xc1\x16\x1e\x86\x8b\x4\xb6\x1a\xd5\x10[L&\x85)\xb8\xb0J\x97N\xe3!\x86u#\v\xd3?\xe0[=\x7fk\xb8\x31V\xbeU\x1cm\x89\xe9\x1_\x1d`'LB\xe8?kV\n\v\xd8\x14\xcf\xfds\x90\xed\x94\xc2.u\xa7\x64\xadPk[\xf1\xc8\xab\x1a\r\xdc\x14-\xfe\xd0!k\xa4\xc6\xaa\xfb\x8c\x97\xc6^\x87\x19\x85]\x8b\x17\xf9\x1b\xd4kNH\xce?Q\xa7t\x1e@D)\xc4\xde!\xb6\xfb\xd1\xcd\x98\xaf\xd9\xe0+<\xe1\xab\x91\xe6)\xf8\xe3\xea\xa0N\xf1\xd4_\xa1N\x10\x9d\xe\x62N\x10\x9b\xe\n\x9d\xa0\xf0T\xa7\xd9\xe5\x5\x30\xb6\0\xb6`\xc0\xe8\xc2\xdd\xf1^\t\xfc(\x80\xb9]\xae\x12o\x98T{\x1e\x99\xfc\xf4\xec\x1b\xd0\xbb\tD)"
|
||||
Geometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\x17\0\0\x6\x85\0\0\x5\xf7\0\0\0\0\0\0\0\0\xff\xff\xff\xff\xff\xff\xff\xff\0\0\0\0\0\0\0\0\xf\0\0\0\0\0\0\0\0\x17\0\0\x6\x85\0\0\x5\xf7)
|
||||
State=@Variant(\0\0\0\x7f\0\0\0\tPyObject\0\0\0\0\xf\x80\x4\x95\x4\0\0\0\0\0\0\0\x43\0\x94.)
|
||||
|
||||
[manifest]
|
||||
widgets\1\closable=true
|
||||
widgets\1\floatable=true
|
||||
widgets\1\floating=false
|
||||
widgets\1\floating_abs_h=0
|
||||
widgets\1\floating_abs_w=0
|
||||
widgets\1\floating_abs_x=0
|
||||
widgets\1\floating_abs_y=0
|
||||
widgets\1\floating_rel_h=0
|
||||
widgets\1\floating_rel_w=0
|
||||
widgets\1\floating_rel_x=0
|
||||
widgets\1\floating_rel_y=0
|
||||
widgets\1\floating_screen=
|
||||
widgets\1\movable=true
|
||||
widgets\1\object_name=Info
|
||||
widgets\1\widget_class=TextBox
|
||||
widgets\2\closable=true
|
||||
widgets\2\floatable=true
|
||||
widgets\2\floating=false
|
||||
widgets\2\floating_abs_h=0
|
||||
widgets\2\floating_abs_w=0
|
||||
widgets\2\floating_abs_x=0
|
||||
widgets\2\floating_abs_y=0
|
||||
widgets\2\floating_rel_h=0
|
||||
widgets\2\floating_rel_w=0
|
||||
widgets\2\floating_rel_x=0
|
||||
widgets\2\floating_rel_y=0
|
||||
widgets\2\floating_screen=
|
||||
widgets\2\movable=true
|
||||
widgets\2\object_name=Plot_1
|
||||
widgets\2\widget_class=Waveform
|
||||
widgets\3\closable=true
|
||||
widgets\3\floatable=true
|
||||
widgets\3\floating=false
|
||||
widgets\3\floating_abs_h=0
|
||||
widgets\3\floating_abs_w=0
|
||||
widgets\3\floating_abs_x=0
|
||||
widgets\3\floating_abs_y=0
|
||||
widgets\3\floating_rel_h=0
|
||||
widgets\3\floating_rel_w=0
|
||||
widgets\3\floating_rel_x=0
|
||||
widgets\3\floating_rel_y=0
|
||||
widgets\3\floating_screen=
|
||||
widgets\3\movable=true
|
||||
widgets\3\object_name=Plot_2
|
||||
widgets\3\widget_class=Waveform
|
||||
widgets\4\closable=true
|
||||
widgets\4\floatable=true
|
||||
widgets\4\floating=false
|
||||
widgets\4\floating_abs_h=0
|
||||
widgets\4\floating_abs_w=0
|
||||
widgets\4\floating_abs_x=0
|
||||
widgets\4\floating_abs_y=0
|
||||
widgets\4\floating_rel_h=0
|
||||
widgets\4\floating_rel_w=0
|
||||
widgets\4\floating_rel_x=0
|
||||
widgets\4\floating_rel_y=0
|
||||
widgets\4\floating_screen=
|
||||
widgets\4\movable=true
|
||||
widgets\4\object_name=Plot_3
|
||||
widgets\4\widget_class=Waveform
|
||||
widgets\5\closable=true
|
||||
widgets\5\floatable=true
|
||||
widgets\5\floating=false
|
||||
widgets\5\floating_abs_h=0
|
||||
widgets\5\floating_abs_w=0
|
||||
widgets\5\floating_abs_x=0
|
||||
widgets\5\floating_abs_y=0
|
||||
widgets\5\floating_rel_h=0
|
||||
widgets\5\floating_rel_w=0
|
||||
widgets\5\floating_rel_x=0
|
||||
widgets\5\floating_rel_y=0
|
||||
widgets\5\floating_screen=
|
||||
widgets\5\movable=true
|
||||
widgets\5\object_name=Plot_4
|
||||
widgets\5\widget_class=Waveform
|
||||
widgets\6\closable=true
|
||||
widgets\6\floatable=true
|
||||
widgets\6\floating=false
|
||||
widgets\6\floating_abs_h=0
|
||||
widgets\6\floating_abs_w=0
|
||||
widgets\6\floating_abs_x=0
|
||||
widgets\6\floating_abs_y=0
|
||||
widgets\6\floating_rel_h=0
|
||||
widgets\6\floating_rel_w=0
|
||||
widgets\6\floating_rel_x=0
|
||||
widgets\6\floating_rel_y=0
|
||||
widgets\6\floating_screen=
|
||||
widgets\6\movable=true
|
||||
widgets\6\object_name=Progress
|
||||
widgets\6\widget_class=TextBox
|
||||
widgets\size=6
|
||||
|
||||
[profile]
|
||||
created_at=2026-09-27T09:27:43Z
|
||||
quick_select=true
|
||||
screenshot="@ByteArray(\x89PNG\r\n\x1a\n\0\0\0\rIHDR\0\0\x6\x86\0\0\x5\xe1\b\x6\0\0\0\xb7\x5(\x97\0\0\0\tpHYs\0\0\xe\xc4\0\0\xe\xc4\x1\x95+\xe\x1b\0\0 \0IDATx\x9c\xec\xddut\x14\xd7\xfb\xc7\xf1w\x1c\x12\x92\xe0n\xc5\xdd\x83\xb6Xp\xb7\xe0RZ\xac\xb8\x43)R\\\x8a\xbbK\xb1\xe0.-\x85\x1f-\x14)\x14)\xe\xa5XB\bNB\x2\x44v\x7f\x7f\xe4\xcb\x96\x94\x18\xc9\xc6\xe0\xf3:'\xe7$3w\xef}\xb2wv\xf6\xce<3w,\xfc_\x5\x1a\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x91\x8f\x9e\x65|\a \"\"\"\"\"\"\"\"\"\"\"\"qC\x89!\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x91O\x84\x12\x43\"\"\"\"\"\"\"\"\"\"\"\"\x9f\b%\x86\x12\x90{\xf7\x1fq\xef\xfe\xa3\xf8\xe\x43$\xca\xb4\xcd&N\xea\xb7O\x97\xfa^DD\xe4\xc3\xe8\xbbSDDD\x12\x13\x8d]$\xaa\x94\x18\x12\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\xf9\x44(1$\"\"\"\"\"\"\"\"\"\"\"\xf2\x89\xb0\x8e\xef\0\x44\x44\x44\x44\x44\x44\x44\x12;?\xbf\xd7\xf8\xbc\xf4#(\xd8\x10\xdf\xa1\x88\x88\x88\x98\x95\xad\xad\rI\xeclqv\xb4\x8f\xefPD\xc4L\x94\x18\x12\x11\x11\x11\x11\x11\x11\x89\x1\xaf\x87OIjkC\xd6\xcci\xb0\xb6\xd2\xc4\x1c\"\"\xf2qy\xf3&\x90\x17>\xaf\xf0\xb8\xff\x98\xcc\x19S\xc7w8\"1r\xef\xfe#\xb2\x64L\x13\xdf\x61\xc4;%\x86\x44\x44\x44\x44\x44\x44\x44\xa2\xc9\xcf\xef\x35I\xecl\xc8\x90.E|\x87\"\"\"\x12+\xec\xeclH\x9b\xc6\x6+k\xf0\xf3\x7f\x83\x83\xbd]|\x87$\"1\xa4K\x99\x44\x44\x44\x44\x44\x44\x44\xa2\xc9\xe7\xa5\x1fiS;\xc7w\x18\"\"\xf2\t\xbb\xfe\x8fW\x9c\xb4\xe3\xec\xe4\xc0\v\x1f\xbf\x38iKDb\x97\x12\x43\"\"\"\"\"\"\"\xd1\x14\x14l\xd0\xf4q\"\"\xf2I\xb0\xb6\xb2\"\xd8\x10\x1c\xdf\x61\x88\x88\x19h\xf4*\"\"\"\"\"\"\"\"\"\"\xf2\x89\x33\x1a\x8d\x18\x8d\xc6\x38{\x9d\xc4\x1f=cHDDDDDDDDDD\xcc\xc2}\xc3\x66N\x9d>\x13\xe5\xf2\xe5\xca\xba\xd0\xaci\xe3X\x8cH\xa2\xc2h42m\xfal\x8c\x46#\x3\a\xf4\xc1`00x\xe8\b\xdc\x9a\x35\xa6l\x19\x97PeO\x9c<\xc5\xa6\xcd\xdb\x98\x34\x61\f\xd6\xd6VL\x9e\x32\x9d\x37\x1o\x18\x39\xfc[,,,\xcc\x1a\xd3\xac\x39\v\xb8w\xcf#\xd4\xf2,Y2\xd3\xa7\xd7\x37\x66m\xebS\xa3\xc4\x90\x88\x88\x88\x88\x88\x88\x88\x88\x88\x88\x98\xc5\xa9\xd3g\xd8\xbdg\x1f\xb9s\xe5\x8c\xb4\xec\xcb\x97/\xd9\xbc\x65\x1b\xaf\xfc_\xd3\xae]\xabX\x89\xe7\xfa\xb5\x1b\x1c<\xfc+\xdft\xed\x14+\xf5G\xe6\xce\x9d{l\xd8\xb8\x19\xef\x87\x8f(X ?\xad[\xb9\x91$I\x92x\x89%*\xe6\xce_\x84\xd1h\xa4_\xdf^l\xde\xb2\r\x97R%\xdeK\f\xdd\xbe}\x87\xcd[\xb6\x31~\xecH\xa6\xcf\x98\xcd\x82\x45K\xe9\xf1M\x17\xb3\xc7\x32{\xee\x42\x66\xcc\x9c\x43\x81\xfc\xf9\xb0\xb2\xb2\x2\xc0`0\xb0y\xcb\xb6\x90\x18\xfb\xf4\x30{\x9bq-((\x18\x83!\x18[[\xdb\x38mW\x89!\x11\x11\x11\x11\x11\x11\x11\x11\x11\x11\x31\x9b\xdc\xb9r\xb2s\xfb\x46\x9e<y\x12n\x19'''\xac\xac\xac\x19\x30p(\xc3G\x8d\x1\x30{r\xe8\xfa\xb5\x1b\xb4h\xf3%\xa9\xd3\xa4\xa2\x43\xbb\x36\xd8\xdb'5k\xfd\x91\xb9x\xe9\x32M\xdd\xda\x90\xc4\xce\x8e\xcc\x99\x33\xb1}\xc7N\xb6l\xdd\xce\xb6\xad\xee\xd8\xda\xd8\xc4i,\x91\xb1\xb0\xb0`@\xff\xde\0\xcc\x99\xb7\x90\xc0\xa0\xa0H_3u\xda,\x96,[I\xcf\xee]\x19\x38\xa0O\x8c\xee\xe0\xd9\xbc\x65\x1bK\x97\xaf\x32\xfdm4\xc2\xd5\xab\xd7\xe8\xdd\xb3\x1b\x3\xfa\xf7\tUv\xfa\x8c\xd9\xcc\x9c\x35\x97}?\xfd\x8c\xe5;mv\xfc\xb2=-\xdc\x9a\x44;\x86\xb8\x12\x18\x18\xc4\xa2%\xcb\xd8\xbcy\x1b\xb7\xef\xdc\x65\xd9\xe2\xf9\xb8\xbaV\x8e\xd3\x18\xe2\xed\x19\x43w\xef\xde%[\x8e\xfc\xdc\xbd{7Z\xeb#\xb2s\xd7.j\xd6\xaa\x13\xd3\x10\xe5\x7f\xcaW\xa8\xc4\xfe\xfd\xfb\xa3\xbd^bG\x81\x42\xc5\x39t\xe8P|\x87\x91 \xc5\xf7\x36\xab}\xd0\x87\xd3w\x82\x88\x88\x88\x88\x88\x88\xc8\xc7\xe5\xc9\x93'\x94*S1\xdc\x9f\xbd\xfb~\xc2\xda\xda\x8aiS'\xd1\xa8\x41=\x86\x8f\x1a\xc3\xe6-\xdb\xcc\xd6\xfe\xb5\xab\xd7i\xde\xba\x3\xa9\xd3\xa4\xc2}\xcd\x8a\x38O\n\x1\xfc\x30m\x16\x19\x33\xa4\xe7\xe8\xaf\a\xd8\xb3k\vkW\xaf\xe0\xe2\xa5\xcbl\xdb\xbe+\xce\x63\x89\x8a\xa0\xa0`z\xf6\xe8J\x97N\x1dY\xbc\x64y\xa4\xe5\x97,[I\x97N\x1d\xe9\xd5\xb3\x1b\x41\x41\xc1\x31j\xfb\xe9\xd3g\\\xbbv\x83\xa2\x85\vQ\xb4p!\x8a\x15)D\xbf\xbe\xbd\xe8\xdf\xaf\xf7{e\xfb\xf5\xed\x45\xbf\xbe\xbd(^\xa4\xb0\xa9\xfc\xb5k7\"LD&\x14w\xee\xdc\xa3Q\x93\x16l\xdf\xbe\x93\x9c\x39sP\xa6t\xa9\x38O\n\x81\x99\x13\x43\xfe\xfe\xfe\x64\xcb\x91\x9f\xdcy\v\xf3\xe6\xcd\x1bsV\x1d\xca\xe2\xc5K\xc8\x96#?\xd9r\xe4\xa7@\xa1\xe2|\xf5U'~\xfa\xe9'\f\x6\x43\xac\xb5\xf9\xb1\xe9\xd1\xa3\x17\xd9r\xe4\xe7\xea\xd5\xab\xb1\xd6\xc6\xc9\x93'M\xfd\x94-G~\xea\xd5o\xc4\x88\x11#9{\xf6\xacY\xea\xdf\xb7\x7f?\xed;t$ \xc0,\xf5\xbd\x15\x1c\x1c\xcc\x9e={\x19\x32\x64(\x95*\xbbR\xaf~#\xc6\x8c\x1d\xc7\xcd\x9b\x37\xcd\xda\x8e|\x98\xb8\xd8\x66\xe7\xcc\x99K\xef\xde}\xdf[~\xe4\xc8Q\xb2\xe5\xc8\x1f\xee>\xe6\xd5\xeb\xd7\xb4j\xddV\x89\xba\xff\xf8X\xbf\x13&N\x9c\xc4\x8c\x19\x33\x63\xa5n\x11\x11\x11\xf9\xf8\x5\x4\x4\x90\x36\x63n\xf6\xff\xf4s\xa4\x65\x83\x82\x82\xf9\xea\xeb\xae\xacZ\xb5\x1a\0ooo\xd2\x66\xcc\xcd\xe9\xd3\x7f\xc6v\x98\x11\x32\x18\f<\x7f\xfe\xdc\xec\xc7\x62\xaf_\xbf\x61\xed\xda\xf5|\xdd\xa9\x1b\xc5J\x94\xa7S\xe7o\xf8\xe3\x8fSfm\xe3\x43\xfc\xf7\xfd\x17\x11\x91\xd8\xf1\x36\x39\xf4Y\xf6l\x1c?a\x9e\xfd\xfe\xd5\xab\xd7h\xd1\xe6K\x9e={Fi\x97\x92\xac^\xb3\x9e\x99\xb3\xe6\x86\xfaY\xef\xbe\x89\v\x17/a4\x1a\xcd\xd2\x66Xn\\\xff\x9bj\xaeUptr\x4\xa0l\x19\x17\xd2\xa7K\xc7\xdf\x37\xfe\x8e\xb5\x36\x63\x62\xe8\xb0\x91\xe4-P\x9c\xc5KWD\xf9\x35\x8b\x97\xae o\x81\Line truncated
|
||||
screenshot_at=2026-09-27T09:27:43Z
|
||||
@@ -193,7 +193,7 @@ states:
|
||||
bl_bright: 'on'
|
||||
bs_pos: in
|
||||
bs_z: safe
|
||||
coll_y: intermediate
|
||||
coll_y: in
|
||||
cryo_pos: in
|
||||
det_cov: 'close'
|
||||
diag_y: out
|
||||
|
||||
Regular → Executable
+52
-6
@@ -55,8 +55,8 @@ aerotech:
|
||||
deviceConfig:
|
||||
prefix: http://mx-x06da-queue-01:5234
|
||||
deviceTags:
|
||||
- motors
|
||||
- aerotech
|
||||
- motors
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readoutPriority: baseline
|
||||
@@ -66,12 +66,12 @@ aerotech:
|
||||
work: 0
|
||||
work: null
|
||||
x:
|
||||
in: 0
|
||||
in: -0.0
|
||||
out: -10
|
||||
safe: -50
|
||||
y:
|
||||
mount: 0
|
||||
work: 0.026
|
||||
work: -0.014
|
||||
z:
|
||||
mount: 0
|
||||
work: 0.022
|
||||
@@ -246,6 +246,43 @@ bcu_sl_yt:
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readoutPriority: baseline
|
||||
beam_profile_centroid:
|
||||
description: Beam location and size based on AD sample camera analysis with centroid
|
||||
analysis
|
||||
deviceClass: pxiii_bec.devices.CentroidBeamProfile
|
||||
deviceConfig:
|
||||
prefix: 'X06DA-ES-MS:Stats5:'
|
||||
deviceTags:
|
||||
- beam
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
beam_profile_gaussian:
|
||||
description: Beam location and size based on AD sample camera analysis with gaussiant
|
||||
fitting
|
||||
deviceClass: pxiii_bec.devices.GaussianBeamProfile
|
||||
deviceConfig:
|
||||
prefix: 'X06DA-ES-MS:XYP1:'
|
||||
deviceTags:
|
||||
- beam
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
beam_steering:
|
||||
description: Beam location and size based on AD sample camera analysis
|
||||
deviceClass: pxiii_bec.devices.BeamSteerer
|
||||
deviceConfig:
|
||||
hfm_step_per_x_px: -0.00013
|
||||
prefix: ''
|
||||
px_tolerance: 3
|
||||
sample_loc_x_px: 1020
|
||||
sample_loc_y_px: 1010
|
||||
step_fraction: 0.7
|
||||
vfm_step_per_y_px: -0.00033
|
||||
deviceTags:
|
||||
- beam
|
||||
enabled: true
|
||||
readoutPriority: on_request
|
||||
userParameter:
|
||||
profile_provider: beam_profile_centroid
|
||||
bl_bright:
|
||||
description: Backlight Brightness
|
||||
deviceClass: ophyd.EpicsSignal
|
||||
@@ -401,7 +438,7 @@ coll_y:
|
||||
onFailure: buffer
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
in: 25.42
|
||||
in: 25.4
|
||||
intermediate: 15
|
||||
out: 4.901
|
||||
park: 0
|
||||
@@ -616,8 +653,8 @@ det_z:
|
||||
onFailure: buffer
|
||||
readoutPriority: baseline
|
||||
userParameter:
|
||||
vis: 150
|
||||
mse: 800
|
||||
vis: 150
|
||||
diag_y:
|
||||
description: Scintillator/diode Y
|
||||
deviceClass: ophyd_devices.EpicsMotor
|
||||
@@ -741,6 +778,15 @@ fl_bright:
|
||||
'on': 3.0
|
||||
type: continuous
|
||||
"\u201Ctol\u201D": 0.01
|
||||
fluorimeter:
|
||||
description: fluorimeter
|
||||
deviceClass: pxiii_bec.devices.Fluorimeter
|
||||
deviceConfig:
|
||||
prefix: 'X06DA-ES-XRF:'
|
||||
deviceTags:
|
||||
- xrf
|
||||
enabled: true
|
||||
readoutPriority: on_request
|
||||
hfm_bd:
|
||||
description: HFM Downstream Bender
|
||||
deviceClass: ophyd_devices.EpicsMotorEC
|
||||
@@ -1019,8 +1065,8 @@ smargon:
|
||||
deviceConfig:
|
||||
prefix: http://x06da-smargopolo.psi.ch:3000
|
||||
deviceTags:
|
||||
- smargon
|
||||
- motors
|
||||
- smargon
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readoutPriority: baseline
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
beam_profile_gaussian:
|
||||
readoutPriority: baseline
|
||||
description: Beam location and size based on AD sample camera analysis with gaussiant fitting
|
||||
deviceClass: pxiii_bec.devices.GaussianBeamProfile
|
||||
deviceConfig:
|
||||
prefix: "X06DA-ES-MS:XYP1:"
|
||||
deviceTags:
|
||||
- beam
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
|
||||
beam_profile_centroid:
|
||||
readoutPriority: baseline
|
||||
description: Beam location and size based on AD sample camera analysis with centroid analysis
|
||||
deviceClass: pxiii_bec.devices.CentroidBeamProfile
|
||||
deviceConfig:
|
||||
prefix: "X06DA-ES-MS:Stats5:"
|
||||
deviceTags:
|
||||
- beam
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
|
||||
beam_steering:
|
||||
readoutPriority: on_request
|
||||
description: Beam location and size based on AD sample camera analysis
|
||||
deviceClass: pxiii_bec.devices.BeamSteerer
|
||||
deviceConfig:
|
||||
prefix: ""
|
||||
hfm_step_per_x_px: -0.00013
|
||||
vfm_step_per_y_px: -0.00033
|
||||
sample_loc_x_px: 1020
|
||||
sample_loc_y_px: 1010
|
||||
px_tolerance: 3
|
||||
step_fraction: 0.7
|
||||
userParameter:
|
||||
profile_provider: "beam_profile_centroid" # "centroid" or "gaussian"
|
||||
deviceTags:
|
||||
- beam
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
@@ -0,0 +1,11 @@
|
||||
fluorimeter:
|
||||
readoutPriority: on_request
|
||||
description: fluorimeter
|
||||
deviceClass: pxiii_bec.devices.Fluorimeter
|
||||
deviceConfig:
|
||||
prefix: "X06DA-ES-XRF:"
|
||||
deviceTags:
|
||||
- xrf
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: false
|
||||
@@ -0,0 +1,213 @@
|
||||
states:
|
||||
robot_sample_exchange:
|
||||
allow_modifiers: true
|
||||
bl_pos: in
|
||||
bl_bright: 'off'
|
||||
bs_pos: out
|
||||
bs_z: safe
|
||||
coll_y: out
|
||||
cryo_pos: in
|
||||
det_cov: 'close'
|
||||
diag_y: out
|
||||
fl_bright: 'off'
|
||||
aerotech_x: in
|
||||
aerotech_y: mount
|
||||
aerotech_z: mount
|
||||
aerotech_u: mount
|
||||
smargon_x: mount
|
||||
smargon_y: mount
|
||||
smargon_chi: mount
|
||||
smargon_phi: mount
|
||||
xrf_pos: out
|
||||
|
||||
sample_alignment:
|
||||
allow_modifiers: true
|
||||
bl_pos: in
|
||||
bl_bright: 'on'
|
||||
bs_pos: in
|
||||
bs_z: safe
|
||||
coll_y: out
|
||||
cryo_pos: in
|
||||
det_cov: 'close'
|
||||
diag_y: out
|
||||
fl_bright: 'on'
|
||||
aerotech_x: in
|
||||
aerotech_y: work
|
||||
aerotech_z: work
|
||||
# aerotech_u: mount
|
||||
# smargon_x: mount
|
||||
# smargon_y: mount
|
||||
# smargon_chi: mount
|
||||
# smargon_phi: mount
|
||||
xrf_pos: out
|
||||
|
||||
data_collection:
|
||||
allow_modifiers: true
|
||||
bl_pos: out
|
||||
bl_bright: 'off'
|
||||
bs_pos: in
|
||||
bs_z: safe
|
||||
coll_y: in
|
||||
cryo_pos: in
|
||||
det_cov: 'open'
|
||||
diag_y: out
|
||||
fl_bright: 'on'
|
||||
aerotech_x: in
|
||||
aerotech_y: work
|
||||
aerotech_z: work
|
||||
# aerotech_u: mount
|
||||
# smargon_x: mount
|
||||
# smargon_y: mount
|
||||
# smargon_chi: mount
|
||||
# smargon_phi: mount
|
||||
xrf_pos: out
|
||||
|
||||
DC_XRF:
|
||||
allow_modifiers: true
|
||||
# bl_pos: out
|
||||
bl_bright: 'off'
|
||||
bs_pos: in
|
||||
bs_z: safe
|
||||
coll_y: in
|
||||
cryo_pos: in
|
||||
det_cov: 'close'
|
||||
diag_y: out
|
||||
fl_bright: 'on'
|
||||
aerotech_x: in
|
||||
aerotech_y: work
|
||||
aerotech_z: work
|
||||
aerotech_u: mount
|
||||
# smargon_x: mount
|
||||
# smargon_y: mount
|
||||
# smargon_chi: mount
|
||||
# smargon_phi: mount
|
||||
xrf_pos: in
|
||||
|
||||
manual_sample_exchange:
|
||||
allow_modifiers: true
|
||||
bl_pos: out
|
||||
bl_bright: 'off'
|
||||
bs_pos: out
|
||||
bs_z: safe
|
||||
coll_y: park
|
||||
cryo_pos: in
|
||||
det_cov: 'close'
|
||||
diag_y: park
|
||||
fl_bright: 'off'
|
||||
aerotech_x: in
|
||||
aerotech_y: mount
|
||||
aerotech_z: mount
|
||||
aerotech_u: mount
|
||||
smargon_x: mount
|
||||
smargon_y: mount
|
||||
smargon_chi: mount
|
||||
smargon_phi: mount
|
||||
xrf_pos: out
|
||||
det_z: 'mse'
|
||||
|
||||
beam_visualisation:
|
||||
bl_pos: out
|
||||
bl_bright: 'off'
|
||||
bs_pos: in
|
||||
bs_z: safe
|
||||
coll_y: in
|
||||
cryo_pos: out
|
||||
det_cov: 'close'
|
||||
det_z: 'vis'
|
||||
diag_y: scint
|
||||
fl_bright: 'off'
|
||||
aerotech_x: out
|
||||
aerotech_y: mount
|
||||
aerotech_z: mount
|
||||
aerotech_u: mount
|
||||
smargon_x: mount
|
||||
smargon_y: mount
|
||||
smargon_chi: mount
|
||||
smargon_phi: mount
|
||||
xrf_pos: out
|
||||
|
||||
flux_measurement:
|
||||
bl_pos: in
|
||||
bl_bright: 'off'
|
||||
bs_pos: in
|
||||
bs_z: safe
|
||||
coll_y: out
|
||||
cryo_pos: out
|
||||
det_cov: 'close'
|
||||
diag_y: i1
|
||||
fl_bright: 'off'
|
||||
aerotech_x: out
|
||||
aerotech_y: mount
|
||||
aerotech_z: mount
|
||||
aerotech_u: mount
|
||||
smargon_x: mount
|
||||
smargon_y: mount
|
||||
smargon_chi: mount
|
||||
smargon_phi: mount
|
||||
xrf_pos: out
|
||||
|
||||
beamstop_alignment:
|
||||
bl_pos: out
|
||||
bl_bright: 'off'
|
||||
bs_pos: in
|
||||
bs_z: samp
|
||||
coll_y: out
|
||||
cryo_pos: out
|
||||
det_cov: 'close'
|
||||
diag_y: out
|
||||
fl_bright: 'on'
|
||||
aerotech_x: out
|
||||
aerotech_y: mount
|
||||
aerotech_z: mount
|
||||
aerotech_u: mount
|
||||
smargon_x: mount
|
||||
smargon_y: mount
|
||||
smargon_chi: mount
|
||||
smargon_phi: mount
|
||||
xrf_pos: out
|
||||
|
||||
maintenance:
|
||||
allow_modifiers: true
|
||||
bl_pos: out
|
||||
bl_bright: 'off'
|
||||
bs_pos: out
|
||||
bs_z: safe
|
||||
coll_y: park
|
||||
cryo_pos: in
|
||||
det_cov: 'close'
|
||||
diag_y: park
|
||||
fl_bright: 'off'
|
||||
aerotech_x: out
|
||||
aerotech_y: mount
|
||||
aerotech_z: mount
|
||||
aerotech_u: mount
|
||||
smargon_x: mount
|
||||
smargon_y: mount
|
||||
smargon_chi: mount
|
||||
smargon_phi: mount
|
||||
xrf_pos: out
|
||||
|
||||
xtal_snapshot:
|
||||
allow_modifiers: true
|
||||
bl_pos: in
|
||||
bl_bright: 'on'
|
||||
bs_pos: in
|
||||
bs_z: safe
|
||||
coll_y: in
|
||||
cryo_pos: in
|
||||
det_cov: 'close'
|
||||
diag_y: out
|
||||
fl_bright: 'on'
|
||||
#aerotech_x: in
|
||||
#aerotech_y: work
|
||||
#aerotech_z: work
|
||||
#aerotech_u: mount
|
||||
#smargon_x: mount
|
||||
#smargon_y: mount
|
||||
#smargon_chi: mount
|
||||
#smargon_phi: mount
|
||||
xrf_pos: out
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File renamed without changes.
@@ -0,0 +1,5 @@
|
||||
base_config:
|
||||
- !include ./pxiii-devices.yaml
|
||||
#states_config:
|
||||
# - !include ./pxiii-state-devices.yaml
|
||||
|
||||
@@ -4,11 +4,16 @@ Ophyd devices for the PX III beamline, including the MX specific Aerotech A3200
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
|
||||
from mx_bec.devices.beam_profile import CentroidBeamProfile, GaussianBeamProfile
|
||||
from mx_bec.devices.beam_steering import BeamSteerer
|
||||
|
||||
from .A3200 import AerotechAbrStage
|
||||
from .A3200utils import A3200Axis
|
||||
from .NDArrayPreview import NDArrayPreview
|
||||
from .PneumaticValve import PneumaticValve
|
||||
from .SamCamDetector import SamCamDetector
|
||||
from .SmarGonA import SmarGonAxis as SmarGonAxisA
|
||||
from .SmarGonB import SmarGonAxis as SmarGonAxisB
|
||||
from .StdDaqPreview import StdDaqPreviewDetector
|
||||
from .NDArrayPreview import NDArrayPreview
|
||||
from .SamCamDetector import SamCamDetector
|
||||
from .PneumaticValve import PneumaticValve
|
||||
from .fluorimeter import Fluorimeter
|
||||
@@ -0,0 +1,30 @@
|
||||
from ophyd import DeviceStatus, EpicsSignal, Component as Cpt
|
||||
|
||||
from ophyd_devices import PSIDeviceBase
|
||||
|
||||
class Fluorimeter(PSIDeviceBase):
|
||||
|
||||
start = Cpt(EpicsSignal, suffix="mca1Start")
|
||||
stop_signal = Cpt(EpicsSignal, suffix="mca1Stop")
|
||||
erase_and_start = Cpt(EpicsSignal, suffix="mca1EraseStart")
|
||||
erase = Cpt(EpicsSignal, suffix="mca1Erase")
|
||||
preset_mode = Cpt(EpicsSignal, suffix="dxp1:PresetMode")
|
||||
status = Cpt(EpicsSignal, suffix="mca1.ACQG")
|
||||
real_time = Cpt(EpicsSignal, suffix="mca1.PRTM")
|
||||
live_time = Cpt(EpicsSignal, suffix="mca1.PLTM")
|
||||
elapsed_real_time = Cpt(EpicsSignal, suffix="mca1.ERTM")
|
||||
elapsed_live_time = Cpt(EpicsSignal, suffix="mca1.ELTM")
|
||||
elapsed_trigger_live_time = Cpt(EpicsSignal, suffix="dxp1:ElapsedTriggerLiveTime")
|
||||
instant_dead_time = Cpt(EpicsSignal, suffix="mca1.IDTIM")
|
||||
average_dead_time = Cpt(EpicsSignal, suffix="mca1.DTIM")
|
||||
poll_time_rbv = Cpt(EpicsSignal, suffix="PollTime_RBV")
|
||||
poll_time = Cpt(EpicsSignal, suffix="PollTime")
|
||||
mca = Cpt(EpicsSignal, suffix="mca1")
|
||||
data = Cpt(EpicsSignal, suffix="mca1.VAL")
|
||||
background = Cpt(EpicsSignal, suffix="mca1.BG")
|
||||
saveFile = Cpt(EpicsSignal, suffix="SaveSystemFile")
|
||||
saveFile_name = Cpt(EpicsSignal, suffix="SaveSystem")
|
||||
saveFile_rbv = Cpt(EpicsSignal, suffix="SaveSystem_RBV")
|
||||
calibration_offset = Cpt(EpicsSignal, suffix="mca1.CALO")
|
||||
calibration_slope = Cpt(EpicsSignal, suffix="mca1.CALS")
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
"""
|
||||
``Aerotech`` --- Aerotech control software
|
||||
******************************************
|
||||
|
||||
This module provides an object to control the Aerotech Abr rotational stage.
|
||||
|
||||
Methods in the Abr class
|
||||
========================
|
||||
|
||||
Standard bluesky interface:
|
||||
AerotechAbrStage.configure(d={...})
|
||||
AerotechAbrStage.kickoff()
|
||||
AerotechAbrStage.stop()
|
||||
Additional bluesky functionality:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Aerotech.is_homed()
|
||||
Aerotech.do_homing(wait=True)
|
||||
Aerotech.get_ready(ostart=None, orange=None, etime=None, wait=True)
|
||||
Aerotech.is_done()
|
||||
Aerotech.is_ready()
|
||||
Aerotech.is_busy()
|
||||
Aerotech.start_exposure()
|
||||
Aerotech.wait_status(status)
|
||||
Aerotech.move(angle, wait=False, speed=None)
|
||||
Aerotech.set_shutter(state)
|
||||
|
||||
|
||||
Returns the axis mode: ``-ES-DF1:AXES-MODE``
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
abr = AerotechAbrStage(prefix="X06DA-ES-DF1", name="abr")
|
||||
|
||||
# move omega to 270.0 degrees
|
||||
abr.omega = 270.0
|
||||
|
||||
# move omega to 180 degrees and wait for movement to finish
|
||||
abr.move(180, wait=True)
|
||||
|
||||
# move omega to 3000 degrees at 360 degrees/s and wait for movement to finish
|
||||
abr.move(3000, velocity=360, wait=True)
|
||||
|
||||
# stop any movement
|
||||
abr.stop() # this function only returns after the STATUS is back to OK
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
from ophyd import Device, Component, EpicsSignal, EpicsSignalRO, Kind
|
||||
from ophyd.status import SubscriptionStatus
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
try:
|
||||
from .A3200enums import AbrCmd, AbrMode
|
||||
except ImportError:
|
||||
from A3200enums import AbrCmd, AbrMode
|
||||
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
# pylint: disable=logging-fstring-interpolation
|
||||
class AerotechAbrStage(PSIDeviceBase, Device):
|
||||
"""Standard PX stage on A3200 controller
|
||||
|
||||
This is the wrapper class for the standard rotation stage layout for the PX
|
||||
beamlines at SLS. It wraps the main rotation axis OMEGA (Aerotech ABR)and
|
||||
the associated motion axes GMX, GMY and GMZ. The ophyd class associates to
|
||||
the general PX measurement procedure, which is that the actual scan script
|
||||
is running as an AeroBasic program on the controller and we communicate to
|
||||
it via 10+1 global variables.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["reset", "kickoff", "complete", "set_axis_mode", "arm", "disarm"]
|
||||
|
||||
taskStop = Component(EpicsSignal, "-AERO:TSK-STOP", put_complete=True, kind=Kind.omitted)
|
||||
status = Component(EpicsSignal, "-AERO:STAT", put_complete=True, kind=Kind.omitted)
|
||||
clear = Component(EpicsSignal, "-AERO:CTRL-CLFT", put_complete=True, kind=Kind.omitted)
|
||||
|
||||
# Enable/disable motor movement via the IOC (i.e. make it task-only)
|
||||
axisModeLocked = Component(EpicsSignal, "-DF1:LOCK", put_complete=True, kind=Kind.omitted)
|
||||
axisModeDirect = Component(
|
||||
EpicsSignal, "-DF1:MODE-DIRECT", put_complete=True, kind=Kind.omitted
|
||||
)
|
||||
axisAxesMode = Component(EpicsSignal, "-DF1:AXES-MODE", put_complete=True, kind=Kind.omitted)
|
||||
|
||||
# Shutter box is missing readback so the -GET signal is installed on the VME
|
||||
# _shutter = Component(
|
||||
# EpicsSignal, "-PH1:GET", write_pv="-PH1:SET", put_complete=True, kind=Kind.config,
|
||||
# )
|
||||
|
||||
# Status flags for all axes
|
||||
omega_done = Component(EpicsSignalRO, "-DF1:OMEGA-DONE", kind=Kind.normal)
|
||||
gmx_done = Component(EpicsSignalRO, "-DF1:GMX-DONE", kind=Kind.normal)
|
||||
gmy_done = Component(EpicsSignalRO, "-DF1:GMY-DONE", kind=Kind.normal)
|
||||
gmz_done = Component(EpicsSignalRO, "-DF1:GMZ-DONE", kind=Kind.normal)
|
||||
|
||||
# For some reason the task interface is called PSO...
|
||||
scan_command = Component(EpicsSignal, "-PSO:CMD", put_complete=True, kind=Kind.omitted)
|
||||
start_command = Component(
|
||||
EpicsSignal, "-PSO:START-TEST.PROC", put_complete=True, kind=Kind.omitted
|
||||
)
|
||||
stop_command = Component(
|
||||
EpicsSignal, "-PSO:STOP-TEST.PROC", put_complete=True, kind=Kind.omitted
|
||||
)
|
||||
|
||||
# Global variables to controll AeroBasic scripts
|
||||
_var_1 = Component(EpicsSignal, "-PSO:VAR-1", put_complete=True, kind=Kind.omitted)
|
||||
_var_2 = Component(EpicsSignal, "-PSO:VAR-2", put_complete=True, kind=Kind.omitted)
|
||||
_var_3 = Component(EpicsSignal, "-PSO:VAR-3", put_complete=True, kind=Kind.omitted)
|
||||
_var_4 = Component(EpicsSignal, "-PSO:VAR-4", put_complete=True, kind=Kind.omitted)
|
||||
_var_5 = Component(EpicsSignal, "-PSO:VAR-5", put_complete=True, kind=Kind.omitted)
|
||||
_var_6 = Component(EpicsSignal, "-PSO:VAR-6", put_complete=True, kind=Kind.omitted)
|
||||
_var_7 = Component(EpicsSignal, "-PSO:VAR-7", put_complete=True, kind=Kind.omitted)
|
||||
_var_8 = Component(EpicsSignal, "-PSO:VAR-8", put_complete=True, kind=Kind.omitted)
|
||||
_var_9 = Component(EpicsSignal, "-PSO:VAR-9", put_complete=True, kind=Kind.omitted)
|
||||
_var_10 = Component(EpicsSignal, "-PSO:VAR-10", put_complete=True, kind=Kind.omitted)
|
||||
|
||||
# Task status PVs (programs always run on task 1)
|
||||
task1 = Component(EpicsSignalRO, "-AERO:TSK1-DONE", auto_monitor=True)
|
||||
task2 = Component(EpicsSignalRO, "-AERO:TSK2-DONE", auto_monitor=True)
|
||||
task3 = Component(EpicsSignalRO, "-AERO:TSK3-DONE", auto_monitor=True)
|
||||
task4 = Component(EpicsSignalRO, "-AERO:TSK4-DONE", auto_monitor=True)
|
||||
scan_done = Component(EpicsSignal, "-GRD:SCAN-DONE", kind=Kind.config)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
scan_info=None,
|
||||
**kwargs,
|
||||
):
|
||||
# super() will call the mixin class
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
scan_info=scan_info,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def set_axis_mode(self, mode: str, settle_time=0.1) -> None:
|
||||
"""Set axis mode to direct/measurement mode.
|
||||
|
||||
Measurement ensures that the scrips run undisturbed by blocking axis
|
||||
motion commands from the IOC (i.e. internal only).
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
mode : str
|
||||
Valid values are 'direct' and 'measuring'.
|
||||
"""
|
||||
if mode == "direct":
|
||||
self.axisModeDirect.set(37, settle_time=settle_time).wait()
|
||||
if mode == "measuring":
|
||||
self.axisAxesMode.set(AbrMode.MEASURING, settle_time=settle_time).wait()
|
||||
|
||||
def on_stage(self):
|
||||
"""
|
||||
|
||||
NOTE: Zac's request is that stage is essentially ARM, i.e. get ready and don't do anything.
|
||||
"""
|
||||
d = {}
|
||||
# FIXME: I don't care about how we fish out config parameters from scan info
|
||||
scan_args = {
|
||||
**self.scan_info.msg.request_inputs["inputs"],
|
||||
**self.scan_info.msg.request_inputs["kwargs"],
|
||||
**self.scan_info.msg.scan_parameters,
|
||||
}
|
||||
scanname = self.scan_info.msg.scan_name
|
||||
|
||||
if scanname in (
|
||||
"standardscan",
|
||||
"helicalscan",
|
||||
"helicalscan1",
|
||||
"helicalscan2",
|
||||
"helicalscan3",
|
||||
):
|
||||
d["scan_command"] = AbrCmd.MEASURE_STANDARD
|
||||
d["var_1"] = scan_args["start"]
|
||||
d["var_2"] = scan_args["range"]
|
||||
d["var_3"] = scan_args["move_time"]
|
||||
d["var_4"] = scan_args.get("ready_rate", 500)
|
||||
d["var_5"] = 0
|
||||
d["var_6"] = 0
|
||||
d["var_7"] = 0
|
||||
# d["var_8"] = 0
|
||||
# d["var_9"] = 0
|
||||
if scanname in ("verticallinescan", "vlinescan"):
|
||||
d["scan_command"] = AbrCmd.VERTICAL_LINE_SCAN
|
||||
d["var_1"] = scan_args["range"] / scan_args["steps"]
|
||||
d["var_2"] = scan_args["steps"]
|
||||
d["var_3"] = scan_args["exp_time"]
|
||||
d["var_4"] = 0
|
||||
d["var_5"] = 0
|
||||
d["var_6"] = 0
|
||||
d["var_7"] = 0
|
||||
# d["var_8"] = 0
|
||||
# d["var_9"] = 0
|
||||
if scanname in ("screeningscan"):
|
||||
d["scan_command"] = AbrCmd.SCREENING
|
||||
d["var_1"] = scan_args["start"]
|
||||
d["var_2"] = scan_args["oscrange"]
|
||||
d["var_3"] = scan_args["exp_time"]
|
||||
d["var_4"] = scan_args["range"] / scan_args["steps"]
|
||||
d["var_5"] = scan_args["steps"]
|
||||
d["var_6"] = scan_args.get("delta", 0.5)
|
||||
d["var_7"] = 0
|
||||
# d["var_8"] = 0
|
||||
# d["var_9"] = 0
|
||||
if scanname in ("rasterscan", "rastersimplescan"):
|
||||
d["scan_command"] = AbrCmd.RASTER_SCAN_SIMPLE
|
||||
d["var_1"] = scan_args["exp_time"]
|
||||
d["var_2"] = scan_args["range_x"] / scan_args["steps_x"]
|
||||
d["var_3"] = scan_args["range_y"] / scan_args["steps_y"]
|
||||
d["var_4"] = scan_args["steps_x"]
|
||||
d["var_5"] = scan_args["steps_y"]
|
||||
d["var_6"] = 0
|
||||
d["var_7"] = 0
|
||||
# d["var_8"] = 0
|
||||
# d["var_9"] = 0
|
||||
|
||||
# Reconfigure if got a valid scan config
|
||||
if len(d) > 0:
|
||||
self.configure(d)
|
||||
|
||||
# Stage the ABR stage
|
||||
self.arm()
|
||||
|
||||
def on_unstage(self):
|
||||
"""Unstage the ABR controller"""
|
||||
self.disarm()
|
||||
|
||||
def configure(self, d: dict) -> tuple:
|
||||
""" " Configure the exposure scripts
|
||||
|
||||
Script execution at the PX beamlines is based on scripts that are always
|
||||
running on the controller that execute commands when commanded by
|
||||
setting a pre-defined set of global variables. This method performs the
|
||||
configuration of the exposure scrips by setting the required global
|
||||
variables.
|
||||
|
||||
Parameters in d: dict
|
||||
---------------------
|
||||
scan_command: int
|
||||
The index of the desired AeroBasic program to be executed.
|
||||
Usually supported values are taken from an Enum.
|
||||
var_1:
|
||||
var_2:
|
||||
var_3:
|
||||
var_4:
|
||||
var_5:
|
||||
var_6:
|
||||
var_7:
|
||||
var_8:
|
||||
var_9:
|
||||
var_10:
|
||||
"""
|
||||
old = self.read_configuration()
|
||||
|
||||
# ToDo: Check if idle before reconfiguring
|
||||
self.scan_command.set(d["scan_command"]).wait()
|
||||
# Set the corresponding global variables
|
||||
if "var_1" in d and d["var_1"] is not None:
|
||||
self._var_1.set(d["var_1"]).wait()
|
||||
if "var_2" in d and d["var_2"] is not None:
|
||||
self._var_2.set(d["var_2"]).wait()
|
||||
if "var_3" in d and d["var_3"] is not None:
|
||||
self._var_3.set(d["var_3"]).wait()
|
||||
if "var_4" in d and d["var_4"] is not None:
|
||||
self._var_4.set(d["var_4"]).wait()
|
||||
if "var_5" in d and d["var_5"] is not None:
|
||||
self._var_5.set(d["var_5"]).wait()
|
||||
if "var_6" in d and d["var_6"] is not None:
|
||||
self._var_6.set(d["var_6"]).wait()
|
||||
if "var_7" in d and d["var_7"] is not None:
|
||||
self._var_7.set(d["var_7"]).wait()
|
||||
if "var_8" in d and d["var_8"] is not None:
|
||||
self._var_8.set(d["var_8"]).wait()
|
||||
if "var_9" in d and d["var_9"] is not None:
|
||||
self._var_9.set(d["var_9"]).wait()
|
||||
if "var_10" in d and d["var_10"] is not None:
|
||||
self._var_10.set(d["var_10"]).wait()
|
||||
|
||||
new = self.read_configuration()
|
||||
return old, new
|
||||
|
||||
def arm(self):
|
||||
"""Bluesky-style stage
|
||||
|
||||
Since configuration synchronization is not guaranteed, this does
|
||||
nothing. The script launched by kickoff().
|
||||
"""
|
||||
|
||||
def on_kickoff(self, timeout=1) -> SubscriptionStatus:
|
||||
"""Kick off the set program"""
|
||||
self.start_command.set(1).wait()
|
||||
|
||||
# Define wait until the busy flag goes high
|
||||
def is_busy(*, value, **_):
|
||||
return bool(value == 0)
|
||||
|
||||
# Subscribe and wait for update
|
||||
status = SubscriptionStatus(self.scan_done, is_busy, timeout=timeout, settle_time=0.1)
|
||||
status.wait()
|
||||
# return status
|
||||
|
||||
def disarm(self, settle_time=0.1):
|
||||
"""Stops current script and releases the axes"""
|
||||
# Disarm commands
|
||||
self.scan_command.set(AbrCmd.NONE, settle_time=settle_time).wait()
|
||||
self.stop_command.set(1).wait()
|
||||
# Go to direct mode
|
||||
self.set_axis_mode("direct", settle_time=settle_time)
|
||||
|
||||
def complete(self, timeout=None) -> SubscriptionStatus:
|
||||
"""Waits for task execution
|
||||
|
||||
NOTE: Original complete was raster scanner complete...
|
||||
"""
|
||||
|
||||
# Define wait until the busy flag goes down (excluding initial update)
|
||||
def is_idle(*, value, **_):
|
||||
return bool(value == 1)
|
||||
|
||||
# Subscribe and wait for update
|
||||
# status = SubscriptionStatus(self.task1, is_idle, timeout=timeout, settle_time=0.5)
|
||||
status = SubscriptionStatus(self.scan_done, is_idle, timeout=timeout, settle_time=0.5)
|
||||
return status
|
||||
|
||||
def reset(self, settle_time=0.1, wait_after_reload=1) -> None:
|
||||
"""Resets the Aerotech controller state
|
||||
|
||||
Attempts to reset the currently running measurement task on the A3200
|
||||
by stopping current motions, reloading aerobasic programs and going
|
||||
to DIRECT mode.
|
||||
|
||||
This will stop movements in both DIRECT and MEASURING modes. During the
|
||||
stop the `status` temporarely goes to ERROR but reverts to OK after a
|
||||
couple of seconds.
|
||||
"""
|
||||
# Disarm commands
|
||||
self.scan_command.set(AbrCmd.NONE, settle_time=settle_time).wait()
|
||||
self.stop_command.set(1, settle_time=settle_time)
|
||||
# Reload tasks
|
||||
self.taskStop.set(1, settle_time=wait_after_reload).wait()
|
||||
# Go to direct mode
|
||||
self.set_axis_mode("direct", settle_time=settle_time)
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
def stop(self, settle_time=1.0) -> None:
|
||||
"""Stops current motions"""
|
||||
# Disarm commands
|
||||
self.scan_command.set(AbrCmd.NONE, settle_time=settle_time).wait()
|
||||
self.stop_command.set(1).wait()
|
||||
# Go to direct mode
|
||||
self.set_axis_mode("direct", settle_time=settle_time)
|
||||
|
||||
def is_ioc_ok(self):
|
||||
"""Checks execution status"""
|
||||
return 0 == self.status.get()
|
||||
|
||||
@property
|
||||
def axis_mode(self):
|
||||
"""Read axis mode"""
|
||||
return self.axisAxesMode.get()
|
||||
|
||||
# @property
|
||||
# def shutter(self):
|
||||
# return self._shutter.get()
|
||||
|
||||
# @shutter.setter
|
||||
# def shutter(self, value):
|
||||
# if self.axisAxesMode.get():
|
||||
# print("ABR is not in direct mode; cannot manipulate shutter")
|
||||
# return False
|
||||
# state = str(state).lower()
|
||||
# if state not in ["1", "0", "closed", "open"]:
|
||||
# print("unknown shutter state requested")
|
||||
# return None
|
||||
# elif state in ["1", "open"]:
|
||||
# state = 1
|
||||
# elif state == ["0", "closed"]:
|
||||
# state = 0
|
||||
# self._shutter.set(state).wait()
|
||||
# return state == self._shutter.get()
|
||||
|
||||
def wait_for_movements(self, timeout=60.0):
|
||||
"""Waits for all motor movements"""
|
||||
t_start = time.time()
|
||||
t_elapsed = 0
|
||||
|
||||
while self.is_moving() and t_elapsed < timeout:
|
||||
t_elapsed = time.time() - t_start
|
||||
if timeout is not None and t_elapsed > timeout:
|
||||
raise TimeoutError("Timeout waiting for all axis to stop moving")
|
||||
time.sleep(0.5)
|
||||
|
||||
def is_moving(self):
|
||||
"""Chechs if all axes are DONE"""
|
||||
return not (
|
||||
self.omega_done.get()
|
||||
and self.gmx_done.get()
|
||||
and self.gmy_done.get()
|
||||
and self.gmz_done.get()
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
abr = AerotechAbrStage(prefix="X06DA-ES", name="abr")
|
||||
abr.wait_for_connection()
|
||||
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Enumerations for the MX specific Aerotech A3200 stage.
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
class AbrStatus:
|
||||
"""ABR measurement task status"""
|
||||
|
||||
DONE = 0
|
||||
READY = 1
|
||||
BUSY = 2
|
||||
|
||||
|
||||
class AbrGridStatus:
|
||||
"""ABR grid scan status"""
|
||||
|
||||
BUSY = 0
|
||||
DONE = 1
|
||||
|
||||
|
||||
class AbrMode:
|
||||
"""ABR mode status"""
|
||||
|
||||
DIRECT = 0
|
||||
MEASURING = 1
|
||||
|
||||
|
||||
class AbrShutterStatus:
|
||||
"""ABR shutter status"""
|
||||
|
||||
CLOSE = 0
|
||||
OPEN = 1
|
||||
|
||||
|
||||
class AbrCmd:
|
||||
"""ABR command table"""
|
||||
|
||||
NONE = 0
|
||||
RASTER_SCAN_SIMPLE = 1
|
||||
MEASURE_STANDARD = 2
|
||||
VERTICAL_LINE_SCAN = 3
|
||||
SCREENING = 4
|
||||
SUPER_FAST_OMEGA = 5
|
||||
STILL_WEDGE = 6
|
||||
STILLS = 7
|
||||
REPEAT_SINGLE_OSCILLATION = 8
|
||||
SINGLE_OSCILLATION = 9
|
||||
OLD_FASHIONED = 10
|
||||
RASTER_SCAN = 11
|
||||
JET_ROTATION = 12
|
||||
X_HELICAL = 13
|
||||
X_RUNSEQ = 14
|
||||
JUNGFRAU = 15
|
||||
MSOX = 16
|
||||
SLIT_SCAN = 17
|
||||
RASTER_SCAN_STILL = 18
|
||||
SCAN_SASTT = 19
|
||||
SCAN_SASTT_V2 = 20
|
||||
SCAN_SASTT_V3 = 21
|
||||
|
||||
|
||||
class AbrAxis:
|
||||
"""ABR axis index"""
|
||||
|
||||
OMEGA = 1
|
||||
GMX = 2
|
||||
GMY = 3
|
||||
GMZ = 4
|
||||
STY = 5
|
||||
STZ = 6
|
||||
@@ -0,0 +1,259 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Tue Jun 11 11:28:38 2024
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
import types
|
||||
from collections import OrderedDict
|
||||
from ophyd import Component, PVPositioner, Signal, EpicsSignal, EpicsSignalRO, Kind, PositionerBase
|
||||
from ophyd.status import Status, MoveStatus
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
from .A3200enums import AbrMode
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
# ABR_DONE = 0
|
||||
# ABR_READY = 1
|
||||
# ABR_BUSY = 2
|
||||
|
||||
# GRID_SCAN_BUSY = 0
|
||||
# GRID_SCAN_DONE = 1
|
||||
|
||||
# DIRECT_MODE = 0
|
||||
# MEASURING_MODE = 1
|
||||
|
||||
|
||||
class A3200Axis(PVPositioner):
|
||||
"""Positioner wrapper for A3200 axes
|
||||
|
||||
|
||||
Positioner wrapper for motors on the Aerotech A3200 controller. As the IOC
|
||||
does not provide a motor record, this class simply wraps axes into a
|
||||
standard Ophyd positioner for the BEC. It also has some additional
|
||||
functionality for error checking and diagnostics.
|
||||
|
||||
Examples
|
||||
--------
|
||||
omega = A3200Axis('X06DA-ES-DF1:OMEGA', base_pv='X06DA-ES')
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prefix : str
|
||||
Axis PV name root.
|
||||
base_pv : str (situational)
|
||||
IOC PV name root, i.e. X06DA-ES if standalone class.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["omove"]
|
||||
|
||||
abr_mode_direct = Component(
|
||||
EpicsSignal, "-DF1:MODE-DIRECT", put_complete=True, kind=Kind.omitted
|
||||
)
|
||||
abr_mode = Component(
|
||||
EpicsSignal, "-DF1:AXES-MODE", auto_monitor=True, put_complete=True, kind=Kind.omitted
|
||||
)
|
||||
|
||||
# Basic PV positioner interface
|
||||
done = Component(EpicsSignalRO, "-DONE", auto_monitor=True, kind=Kind.config)
|
||||
readback = Component(EpicsSignalRO, "-RBV", auto_monitor=True, kind=Kind.hinted)
|
||||
# Setpoint is one of the two...
|
||||
setpoint = Component(EpicsSignal, "-SETP", kind=Kind.config)
|
||||
# setpoint = Component(EpicsSignal, "-VAL", kind=Kind.config)
|
||||
|
||||
velocity = Component(EpicsSignal, "-SETV", kind=Kind.config)
|
||||
status = Component(EpicsSignalRO, "-STAT", auto_monitor=True, kind=Kind.config)
|
||||
# PV to issue native relative movements on the A3200
|
||||
relmove = Component(EpicsSignal, "-INCP", put_complete=True, kind=Kind.config)
|
||||
# PV to home axis
|
||||
home = Component(EpicsSignal, "-HOME", kind=Kind.config)
|
||||
ishomed = Component(EpicsSignal, "-AS00", kind=Kind.config)
|
||||
|
||||
# HW status words
|
||||
dshw = Component(EpicsSignalRO, "-DSHW", auto_monitor=True, kind=Kind.normal)
|
||||
ashw = Component(EpicsSignalRO, "-ASHW", auto_monitor=True, kind=Kind.normal)
|
||||
fslw = Component(EpicsSignalRO, "-FSLW", auto_monitor=True, kind=Kind.normal)
|
||||
|
||||
# Rock movement
|
||||
_rock = Component(EpicsSignal, "-ROCK", put_complete=True, kind=Kind.config)
|
||||
_rock_dist = Component(EpicsSignal, "-RINCP", put_complete=True, kind=Kind.config)
|
||||
_rock_velo = Component(EpicsSignal, "-RSETV", put_complete=True, kind=Kind.config)
|
||||
_rock_count = Component(EpicsSignal, "-COUNT", put_complete=True, kind=Kind.config)
|
||||
# _rock_accel = Component(EpicsSignal, "-RRATE", put_complete=True, kind=Kind.config)
|
||||
|
||||
hlm = Component(Signal, kind=Kind.config)
|
||||
llm = Component(Signal, kind=Kind.config)
|
||||
vmin = Component(Signal, kind=Kind.config)
|
||||
vmax = Component(Signal, kind=Kind.config)
|
||||
offset = Component(EpicsSignal, "-OFF", put_complete=True, kind=Kind.config)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
base_pv="",
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
llm=0,
|
||||
hlm=0,
|
||||
vmin=0,
|
||||
vmax=0,
|
||||
**kwargs,
|
||||
):
|
||||
"""__init__ MUST have a full argument list"""
|
||||
|
||||
# Patching the parent's PVs into the axis class to check for direct/locked mode
|
||||
if parent is None:
|
||||
|
||||
def maybe_add_prefix(self, _, kw, suffix):
|
||||
# Patched not to enforce parent prefix when no parent
|
||||
if kw in self.add_prefix:
|
||||
return suffix
|
||||
return suffix
|
||||
|
||||
self.__class__.__dict__["abr_mode"].maybe_add_prefix = types.MethodType(
|
||||
maybe_add_prefix, self.__class__.__dict__["abr_mode"]
|
||||
)
|
||||
self.__class__.__dict__["abr_mode_direct"].maybe_add_prefix = types.MethodType(
|
||||
maybe_add_prefix, self.__class__.__dict__["abr_mode_direct"]
|
||||
)
|
||||
logger.info(self.__class__.__dict__["abr_mode"].kwargs)
|
||||
self.__class__.__dict__["abr_mode"].suffix = base_pv + "-DF1:AXES-MODE"
|
||||
self.__class__.__dict__["abr_mode_direct"].suffix = base_pv + "-DF1:MODE-DIRECT"
|
||||
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.llm.set(llm).wait()
|
||||
self.hlm.set(hlm).wait()
|
||||
self.vmin.set(vmin).wait()
|
||||
self.vmax.set(vmax).wait()
|
||||
|
||||
def omove(
|
||||
self,
|
||||
position,
|
||||
wait=True,
|
||||
timeout=None,
|
||||
# moved_cb=None,
|
||||
velocity=None,
|
||||
relative=False,
|
||||
direct=False,
|
||||
**kwargs,
|
||||
) -> MoveStatus:
|
||||
"""Native absolute/relative movement on the A3200"""
|
||||
|
||||
# Check if we're in direct movement mode
|
||||
if self.abr_mode.value not in (AbrMode.DIRECT, "DIRECT"):
|
||||
if direct:
|
||||
self.abr_mode_direct.set(1).wait()
|
||||
else:
|
||||
raise RuntimeError(f"ABR axis not in direct mode: {self.abr_mode.value}")
|
||||
|
||||
# Before moving, ensure we can stop (if a stop_signal is configured).
|
||||
if self.stop_signal is not None:
|
||||
self.stop_signal.wait_for_connection()
|
||||
|
||||
# Set velocity if provided
|
||||
if velocity is not None:
|
||||
self.velocity.set(velocity).wait()
|
||||
|
||||
# This is adapted from pv_positioner.py
|
||||
self.check_value(position)
|
||||
|
||||
# Get MoveStatus from parent of parent
|
||||
status = PositionerBase.move(self, position=position, timeout=timeout, **kwargs)
|
||||
|
||||
has_done = self.done is not None
|
||||
if not has_done:
|
||||
moving_val = 1 - self.done_value
|
||||
self._move_changed(value=self.done_value)
|
||||
self._move_changed(value=moving_val)
|
||||
|
||||
try:
|
||||
if relative:
|
||||
# Relative movement instead of setpoint
|
||||
self.relmove.put(position, wait=True)
|
||||
else:
|
||||
# Standard absolute movement
|
||||
self.setpoint.put(position, wait=True)
|
||||
if wait:
|
||||
status.wait()
|
||||
except KeyboardInterrupt:
|
||||
self.stop()
|
||||
raise
|
||||
return status
|
||||
|
||||
def describe(self):
|
||||
"""Workaround to schema expected by the BEC"""
|
||||
d = super().describe()
|
||||
d[str(self.name)] = d[f"{self.name}_readback"]
|
||||
return d
|
||||
|
||||
def read(self) -> OrderedDict[str, dict]:
|
||||
"""Workaround to schema expected by the BEC"""
|
||||
d = super().read()
|
||||
d[str(self.name)] = d[f"{self.name}_readback"]
|
||||
return d
|
||||
|
||||
def move(self, position, wait=True, timeout=None, moved_cb=None, **kwargs) -> MoveStatus:
|
||||
"""Exposes the ophyd move command through BEC abstraction"""
|
||||
return self.omove(position, wait=wait, timeout=timeout, moved_cb=moved_cb, **kwargs)
|
||||
|
||||
def rock(self, distance, counts: int, velocity=None, wait=True) -> Status:
|
||||
"""Repeated single axis zigzag scan I guess PSO should be configured for this"""
|
||||
|
||||
self._rock_dist.put(distance)
|
||||
self._rock_count.put(counts)
|
||||
if velocity is not None:
|
||||
self._rock_velo.put(velocity)
|
||||
# if acceleration is not None:
|
||||
# self._rock_accel.put(acceleration)
|
||||
self._rock.put(1)
|
||||
|
||||
status = super().move(position=distance)
|
||||
if wait:
|
||||
status.wait()
|
||||
return status
|
||||
|
||||
# def is_omega_ok(self):
|
||||
# """Checks omega axis status"""
|
||||
# return 0 == self.self.omega.status.get()
|
||||
|
||||
# def is_homed(self):
|
||||
# """Checks if omega is homed"""
|
||||
# return 1 == self.omega.is_homed.get()
|
||||
|
||||
# def do_homing(self, wait=True):
|
||||
# """Execute the homing procedure.
|
||||
|
||||
# Executes the homing procedure on omega and waits (default) until it is completed.
|
||||
|
||||
# TODO: Return a status object to do this wwith futures and monitoring.
|
||||
|
||||
# PARAMETERS
|
||||
# `wait` true / false if the routine is to wait for the homing to finish.
|
||||
# """
|
||||
# self.omega.home.set(1, settle_time=1).wait()
|
||||
# if not wait:
|
||||
# return
|
||||
# while not self.omega.is_homed():
|
||||
# time.sleep(0.2)
|
||||
|
||||
|
||||
# Automatically start an axis if directly invoked
|
||||
if __name__ == "__main__":
|
||||
omega = A3200Axis(prefix="X06DA-ES-DF1:OMEGA", base_pv="X06DA-ES", name="omega")
|
||||
omega.wait_for_connection()
|
||||
@@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
``NDArrayPreview`` --- Standalone Preview for ImagePlugin
|
||||
*********************************************************
|
||||
|
||||
This module provides a standalone object to receive images to ophyd from the
|
||||
AreaDetector's ImagePlugin.
|
||||
|
||||
Created on Wed Jan 29 2025
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
import numpy as np
|
||||
from ophyd import Device, Component, EpicsSignal, EpicsSignalWithRBV, Kind, Staged
|
||||
from ophyd.areadetector import NDDerivedSignal
|
||||
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class SilentNDDerivedSignal(NDDerivedSignal):
|
||||
"""Silent version of NDDerivedSignal, it does not spam the terminal on
|
||||
every defective frame (shit happens, ok?)."""
|
||||
|
||||
def _array_shape_callback(self, **kwargs):
|
||||
try:
|
||||
super()._array_shape_callback(**kwargs)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
|
||||
class NDArrayPreview(Device):
|
||||
"""Wrapper class around AreaDetector's NDStdArray plugins
|
||||
|
||||
This is a standalone class to display images from AreaDetector's
|
||||
ImagePlugin without using a parent device. It also offers BEC exposed
|
||||
methods to transfer image and change image array Kind-ness.
|
||||
|
||||
NOTE: As an explicit request, it can toggle data recording
|
||||
"""
|
||||
|
||||
# Subscriptions for plotting image
|
||||
USER_ACCESS = ["image", "savemode"]
|
||||
SUB_MONITOR = "device_monitor_2d"
|
||||
_default_sub = SUB_MONITOR
|
||||
|
||||
# Status attributes
|
||||
min_callback_time = Component(
|
||||
EpicsSignalWithRBV, "MinCallbackTime", kind=Kind.config, put_complete=True
|
||||
)
|
||||
array_size_x = Component(EpicsSignal, "ArraySize0_RBV", kind=Kind.config)
|
||||
array_size_y = Component(EpicsSignal, "ArraySize1_RBV", kind=Kind.config)
|
||||
array_size_z = Component(EpicsSignal, "ArraySize2_RBV", kind=Kind.config)
|
||||
ndimensions = Component(EpicsSignal, "NDimensions_RBV", kind=Kind.config)
|
||||
array_data = Component(EpicsSignal, "ArrayData", kind=Kind.omitted)
|
||||
shaped_image = Component(
|
||||
SilentNDDerivedSignal,
|
||||
derived_from="array_data",
|
||||
shape=("array_size_z", "array_size_y", "array_size_x"),
|
||||
num_dimensions="ndimensions",
|
||||
kind=Kind.omitted,
|
||||
)
|
||||
|
||||
def read(self):
|
||||
"""Stream out data on every read()"""
|
||||
if self._staged == Staged.yes:
|
||||
image = self.shaped_image.get()
|
||||
self._run_subs(sub_type=self.SUB_MONITOR, value=image)
|
||||
return super().read()
|
||||
|
||||
def savemode(self, save=False):
|
||||
"""Toggle save mode for the shaped image"""
|
||||
# pylint: disable=protected-access
|
||||
if save:
|
||||
self.shaped_image._kind = Kind.normal
|
||||
else:
|
||||
self.shaped_image._kind = Kind.omitted
|
||||
|
||||
def image(self):
|
||||
"""Fallback method in case image streaming fills up the BEC"""
|
||||
array_size = (self.array_size_z.get(), self.array_size_y.get(), self.array_size_x.get())
|
||||
if array_size == (0, 0, 0):
|
||||
raise RuntimeError("Invalid image; ensure array_callbacks are on")
|
||||
|
||||
if array_size[-1] == 0:
|
||||
array_size = array_size[:-1]
|
||||
|
||||
image = self.array_data.get()
|
||||
return np.array(image).reshape(array_size)
|
||||
|
||||
|
||||
# Automatically connect to SAMCAM at PXIII if directly invoked
|
||||
if __name__ == "__main__":
|
||||
img = NDArrayPreview("X06DA-SAMCAM:image1:", name="samimg")
|
||||
img.wait_for_connection()
|
||||
@@ -0,0 +1,47 @@
|
||||
from ophyd import EpicsSignal
|
||||
from ophyd.status import SubscriptionStatus
|
||||
|
||||
|
||||
class PneumaticValve(EpicsSignal):
|
||||
"""Wrapper around EpicsSignal to wait until reaching target. Use the
|
||||
status returned by set() to wait until movement is finished. Do NOT
|
||||
use put if you want to wait, that's a low-level PV write op.
|
||||
|
||||
NOTE: The SET and GET states do not match exactly
|
||||
"""
|
||||
|
||||
def set(self, value, *, timeout=5, settle_time=0.1):
|
||||
"""Overloaded setter that waits for target state
|
||||
|
||||
NOTE: The SubscriptionStatus callback does not run in put()
|
||||
"""
|
||||
# Lazy hardcoded state lookup
|
||||
target = 1 if value in (1, "Measure") else 2
|
||||
|
||||
# Define wait until an end state is reached
|
||||
def on_target(*, value, **_):
|
||||
return bool(value == target)
|
||||
|
||||
# Subscribe a monitor in advance and wait for update
|
||||
status = SubscriptionStatus(self, on_target, timeout=timeout, settle_time=0.1)
|
||||
# Set value to start movement
|
||||
super().set(value, settle_time=settle_time).wait()
|
||||
# Return the monitor
|
||||
return status
|
||||
|
||||
def check_value(self, value):
|
||||
"""Input validation"""
|
||||
if value not in (0, 1, "Measure", "Park"):
|
||||
raise ValueError(f"Unsupported pneumatic valve target {value}")
|
||||
return super().check_value(value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pneum = PneumaticValve(
|
||||
read_pv="X06DA-ES-BS:GET-POS",
|
||||
write_pv="X06DA-ES-BS:SET-POS",
|
||||
auto_monitor=True,
|
||||
put_complete=True,
|
||||
name="bspump",
|
||||
)
|
||||
pneum.wait_for_connection()
|
||||
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
``SamCam`` --- Sample Camera control software
|
||||
*********************************************
|
||||
|
||||
This module provides an object to control the sample camera at the PX III
|
||||
beamline. The camera should run continously and stream data via ZMQ for
|
||||
the GUI and alignment scripts.
|
||||
|
||||
Created on Thu Jan 30 2025
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
from ophyd import ADComponent
|
||||
from ophyd_devices.devices.areadetector.cam import GenICam
|
||||
|
||||
# from ophyd_devices.devices.areadetector.plugins import ImagePlugin_V35
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
PSIDetectorBase,
|
||||
CustomDetectorMixin,
|
||||
)
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class SamCamSetup(CustomDetectorMixin):
|
||||
"""Simple camera mixin class, the SAMCAM is usually streaming"""
|
||||
|
||||
def on_stage(self):
|
||||
"""Just make sure it's running continously"""
|
||||
self.parent.cam.acquire.put(1, wait=True)
|
||||
|
||||
def on_unstage(self):
|
||||
"""Should run continously"""
|
||||
|
||||
def on_stop(self):
|
||||
"""Should run continously"""
|
||||
|
||||
|
||||
class SamCamDetector(PSIDetectorBase):
|
||||
"""Sample camera device
|
||||
|
||||
The SAMCAM continously streams images to the GUI and sample alignment
|
||||
scripts via ZMQ.
|
||||
"""
|
||||
|
||||
custom_prepare_cls = SamCamSetup
|
||||
|
||||
cam = ADComponent(GenICam, "cam1:")
|
||||
# image = ADComponent(ImagePlugin_V35, "image1:")
|
||||
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
``SmarGon`` --- SmarGon control software
|
||||
******************************************
|
||||
|
||||
The module provides an object to control the SmarGon goniometer axes at PX III.
|
||||
The SmarGon axes are interfaced as positioners.
|
||||
"""
|
||||
|
||||
import time
|
||||
from threading import Thread, Lock
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
from collections import OrderedDict
|
||||
from ophyd import Component, Kind, Signal, PVPositioner
|
||||
from ophyd.status import SubscriptionStatus
|
||||
|
||||
try:
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
except ModuleNotFoundError:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("SmarGon")
|
||||
|
||||
|
||||
# SmarGon contoller can't really handle multiple connections
|
||||
# Use this mutex to ensure one access at a time
|
||||
mutex = Lock()
|
||||
|
||||
|
||||
class SmarGonSignal(Signal):
|
||||
"""SmarGonSignal (R/W)
|
||||
|
||||
Small helper class to read/write parameters from SmarGon. As there is no
|
||||
motion status readback from smargopolo, this should be substituted with
|
||||
setting with 'settle_time'.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, write_addr="targetSCS", low_limit=None, high_limit=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.write_addr = write_addr
|
||||
self.addr = self.parent.name
|
||||
self._limits = (low_limit, high_limit)
|
||||
# self.get()
|
||||
|
||||
def put(self, value, *, timestamp=None, **kwargs):
|
||||
"""Overriden put to add communication with smargopolo"""
|
||||
# Validate new value and get timestamp
|
||||
self.check_value(value)
|
||||
if timestamp is None:
|
||||
timestamp = time.time()
|
||||
|
||||
# Perform the actual write to SmargoPolo
|
||||
# pylint: disable=protected-access
|
||||
r = self.parent._go_n_put(f"{self.write_addr}?{self.addr.upper()}={value}")
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
old_value = self._readback
|
||||
self._timestamp = timestamp
|
||||
self._readback = r[self.addr.upper()]
|
||||
self._value = r[self.addr.upper()]
|
||||
|
||||
# Notify subscribers
|
||||
self._run_subs(
|
||||
sub_type=self.SUB_VALUE, old_value=old_value, value=value, timestamp=self._timestamp
|
||||
)
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return self._limits
|
||||
|
||||
def check_value(self, value, **kwargs):
|
||||
"""Check if value falls within limits"""
|
||||
lol = self.limits[0]
|
||||
if lol is not None:
|
||||
if value < lol:
|
||||
raise ValueError(f"Target {value} outside of limits {self.limits}")
|
||||
hil = self.limits[1]
|
||||
if hil is not None:
|
||||
if value > hil:
|
||||
raise ValueError(f"Target {value} outside of limits {self.limits}")
|
||||
|
||||
def get(self, **kwargs):
|
||||
# pylint: disable=protected-access
|
||||
r = self.parent._go_n_get(self.write_addr)
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self._value = r[self.addr.upper()] if isinstance(r, dict) else r
|
||||
return super().get(**kwargs)
|
||||
|
||||
|
||||
class SmarGonSignalRO(Signal):
|
||||
"""Small helper class for read-only parameters PVs from SmarGon.
|
||||
|
||||
Reads and optionally monitors a variable on the SmarGon.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, read_addr="readbackSCS", auto_monitor=False, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._metadata["write_access"] = False
|
||||
self.read_addr = read_addr
|
||||
self.addr = self.parent.name
|
||||
|
||||
if auto_monitor:
|
||||
self._mon = Thread(target=self.poll, daemon=True)
|
||||
self._mon.start()
|
||||
|
||||
def get(self, **kwargs):
|
||||
# pylint: disable=protected-access
|
||||
r = self.parent._go_n_get(self.read_addr)
|
||||
|
||||
if isinstance(r, dict):
|
||||
self.put(r[self.addr.upper()], force=True)
|
||||
else:
|
||||
self.put(r, force=True)
|
||||
return self._readback
|
||||
|
||||
def poll(self):
|
||||
"""Fooo"""
|
||||
time.sleep(2)
|
||||
while True:
|
||||
time.sleep(0.25)
|
||||
try:
|
||||
self.get()
|
||||
except requests.ConnectTimeout as ex:
|
||||
logger.error(f"[{self.name}] {ex}")
|
||||
|
||||
|
||||
class SmarGonAxis(PVPositioner):
|
||||
"""SmarGon client deice
|
||||
|
||||
This class controls the SmarGon goniometer via the REST interface. All
|
||||
SmarGon axes share a common mutex to manage actual HW access.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["omove"]
|
||||
|
||||
# Status attributes
|
||||
sg_url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
|
||||
corr = Component(SmarGonSignalRO, read_addr="corr_type", kind=Kind.config)
|
||||
mode = Component(SmarGonSignalRO, read_addr="mode", kind=Kind.config)
|
||||
|
||||
# Axis parameters
|
||||
readback = Component(SmarGonSignalRO, kind=Kind.hinted, auto_monitor=True)
|
||||
setpoint = Component(SmarGonSignal, kind=Kind.normal)
|
||||
done = Component(Signal, value=1, kind=Kind.normal)
|
||||
# moving = Component(SmarGonMovingSignalRO, kind=Kind.config)
|
||||
_tol = 0.001
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
prefix="SCS",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
sg_url: str = "http://x06da-smargopolo.psi.ch:3000",
|
||||
low_limit=None,
|
||||
high_limit=None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.__class__.__dict__["readback"].kwargs["read_addr"] = f"readback{prefix}"
|
||||
self.__class__.__dict__["setpoint"].kwargs["write_addr"] = f"target{prefix}"
|
||||
self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit
|
||||
self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit
|
||||
self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url
|
||||
# Fine-tune HTTP connection behavior
|
||||
# NOTE: SmarGon has a few failed requests every one in a while
|
||||
self._s = requests.Session()
|
||||
retries = Retry(total=5, backoff_factor=0.05, status_forcelist=[500, 502, 503, 504])
|
||||
self._s.mount("http://", HTTPAdapter(max_retries=retries))
|
||||
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def initialize(self):
|
||||
"""Helper function for initial readings"""
|
||||
# self.corr.get()
|
||||
# self.mode.get()
|
||||
r = self._go_n_get("corr_type")
|
||||
print(r)
|
||||
|
||||
def move(self, position, wait=True, timeout=None, moved_cb=None):
|
||||
"""Move command that's masked by BEC"""
|
||||
return self.omove(position, wait, timeout, moved_cb)
|
||||
|
||||
def omove(self, position, wait=True, timeout=2.0, moved_cb=None):
|
||||
"""Original move command without the BEC wrappers"""
|
||||
status = self.setpoint.set(position, settle_time=0.1)
|
||||
status.wait()
|
||||
if not wait:
|
||||
return status
|
||||
|
||||
def on_target(*, value, **_):
|
||||
distance = abs(value - self.setpoint._value)
|
||||
print(f"[self.name] Distance: {distance}")
|
||||
return bool(distance < self._tol)
|
||||
|
||||
status = SubscriptionStatus(self.readback, on_target, timeout=timeout, settle_time=0.1)
|
||||
return status
|
||||
|
||||
def describe(self):
|
||||
"""Workaround to schema expected by the BEC"""
|
||||
d = super().describe()
|
||||
d[str(self.name)] = d[f"{self.name}_readback"]
|
||||
return d
|
||||
|
||||
def read(self) -> OrderedDict[str, dict]:
|
||||
"""Workaround to schema expected by the BEC"""
|
||||
d = super().read()
|
||||
d[str(self.name)] = d[f"{self.name}_readback"]
|
||||
return d
|
||||
|
||||
def _pos_changed(self, timestamp=None, value=None, **kwargs):
|
||||
pass
|
||||
|
||||
def _go_n_get(self, address, **kwargs):
|
||||
"""Helper function to connect to smargopolo"""
|
||||
cmd = f"{self.sg_url.get()}/{address}"
|
||||
try:
|
||||
with mutex:
|
||||
r = self._s.get(cmd, timeout=1, **kwargs)
|
||||
except TimeoutError:
|
||||
try:
|
||||
time.sleep(0.05)
|
||||
with mutex:
|
||||
r = self._s.get(cmd, timeout=0.5, **kwargs)
|
||||
except TimeoutError:
|
||||
time.sleep(0.05)
|
||||
with mutex:
|
||||
r = self._s.get(cmd, timeout=0.5, **kwargs)
|
||||
if not r.ok:
|
||||
raise RuntimeError(
|
||||
f"[{self.name}] Error getting {address}; reply was {r.status_code} => {r.reason}"
|
||||
)
|
||||
return r.json()
|
||||
|
||||
def _go_n_put(self, address, **kwargs):
|
||||
"""Helper function to connect to smargopolo"""
|
||||
cmd = f"{self.sg_url.get()}/{address}"
|
||||
try:
|
||||
with mutex:
|
||||
r = self._s.put(cmd, timeout=1, **kwargs)
|
||||
except TimeoutError:
|
||||
try:
|
||||
time.sleep(0.05)
|
||||
with mutex:
|
||||
r = self._s.put(cmd, timeout=0.5, **kwargs)
|
||||
except TimeoutError:
|
||||
time.sleep(0.05)
|
||||
with mutex:
|
||||
r = self._s.put(cmd, timeout=0.5, **kwargs)
|
||||
if not r.ok:
|
||||
raise RuntimeError(
|
||||
f"[{self.name}] Error putting {address}; reply was {r.status_code} => {r.reason}"
|
||||
)
|
||||
return r.json()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
shx = SmarGonAxis(prefix="SCS", name="shx", sg_url="http://x06da-smargopolo.psi.ch:3000")
|
||||
shy = SmarGonAxis(prefix="SCS", name="shy", sg_url="http://x06da-smargopolo.psi.ch:3000")
|
||||
shz = SmarGonAxis(
|
||||
prefix="SCS",
|
||||
name="shz",
|
||||
low_limit=10,
|
||||
high_limit=22,
|
||||
sg_url="http://x06da-smargopolo.psi.ch:3000",
|
||||
)
|
||||
shx.wait_for_connection()
|
||||
shy.wait_for_connection()
|
||||
shz.wait_for_connection()
|
||||
@@ -0,0 +1,249 @@
|
||||
"""
|
||||
``SmarGon`` --- SmarGon control software
|
||||
******************************************
|
||||
|
||||
The module provides an object to control the SmarGon goniometer axes at PX III.
|
||||
The SmarGon axes are interfaced as positioners.
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter, Retry
|
||||
from ophyd import Component, Kind, Signal, PVPositioner
|
||||
from ophyd.status import SubscriptionStatus
|
||||
|
||||
try:
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
except ModuleNotFoundError:
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("SmarGon")
|
||||
|
||||
|
||||
# SmarGon contoller can't really handle multiple connections
|
||||
# Use this mutex to ensure one access at a time
|
||||
mutex = threading.Lock()
|
||||
|
||||
|
||||
class LimitedSmarGonSignal(Signal):
|
||||
"""SmarGonSignal (R/W)
|
||||
|
||||
Small helper class to read/write parameters from SmarGon. As there is no
|
||||
motion status readback from smargopolo, this should be substituted with
|
||||
setting with 'settle_time'.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, write_addr="targetSCS", low_limit=None, high_limit=None, **kwargs):
|
||||
self._limits = (low_limit, high_limit)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.write_addr = write_addr
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return self._limits
|
||||
|
||||
def check_value(self, value, **kwargs):
|
||||
"""Check if value falls within limits"""
|
||||
lol = self.limits[0]
|
||||
if lol is not None:
|
||||
if value < lol:
|
||||
raise ValueError(f"Target {value} outside of limits {self.limits}")
|
||||
hil = self.limits[1]
|
||||
if hil is not None:
|
||||
if value > hil:
|
||||
raise ValueError(f"Target {value} outside of limits {self.limits}")
|
||||
|
||||
def put(self, value, *, timestamp=None, force=False, metadata=None, **kwargs):
|
||||
"""Overriden put to add communication with smargopolo"""
|
||||
# Validate new value and get timestamp
|
||||
if not force:
|
||||
self.check_value(value)
|
||||
if timestamp is None:
|
||||
timestamp = time.time()
|
||||
|
||||
# Perform the actual write to SmargoPolo
|
||||
# pylint: disable=protected-access
|
||||
r = self.parent._go_n_put(f"{self.write_addr}?{self.parent.name.upper()}={value}")
|
||||
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
old_value = self._readback
|
||||
self._timestamp = timestamp
|
||||
self._readback = r[self.parent.name.upper()]
|
||||
self._value = r[self.parent.name.upper()]
|
||||
|
||||
# Notify subscribers
|
||||
self._run_subs(
|
||||
sub_type=self.SUB_VALUE, old_value=old_value, value=value, timestamp=self._timestamp
|
||||
)
|
||||
|
||||
|
||||
class SmarGonAxis(PVPositioner):
|
||||
"""SmarGon client deice
|
||||
|
||||
This class controls the SmarGon goniometer via the REST interface. All
|
||||
SmarGon axes share a common mutex to manage actual HW access.
|
||||
"""
|
||||
|
||||
USER_ACCESS = ["omove", "oldmove"]
|
||||
|
||||
# Status attributes
|
||||
sg_url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
|
||||
|
||||
# Axis parameters
|
||||
readback = Component(Signal, kind=Kind.hinted, metadata={"write_access": False})
|
||||
setpoint = Component(LimitedSmarGonSignal, kind=Kind.normal)
|
||||
done = Component(Signal, value=1, kind=Kind.normal, metadata={"write_access": False})
|
||||
_tol = 0.001
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(
|
||||
self,
|
||||
prefix="SCS",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
sg_url: str = "http://x06da-smargopolo.psi.ch:3000",
|
||||
low_limit=None,
|
||||
high_limit=None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
# self.__class__.__dict__["setpoint"].kwargs["write_addr"] = f"target{prefix}"
|
||||
self.__class__.__dict__["setpoint"].kwargs["low_limit"] = low_limit
|
||||
self.__class__.__dict__["setpoint"].kwargs["high_limit"] = high_limit
|
||||
self.__class__.__dict__["sg_url"].kwargs["value"] = sg_url
|
||||
# Fine-tune HTTP connection behavior
|
||||
# NOTE: SmarGon has a few failed requests every one in a while
|
||||
self._s = requests.Session()
|
||||
retries = Retry(total=5, backoff_factor=0.05, status_forcelist=[500, 502, 503, 504])
|
||||
self._s.mount("http://", HTTPAdapter(max_retries=retries))
|
||||
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def on_target():
|
||||
"""Monitors the setpoint and readback and calculates the on_target flag"""
|
||||
time.sleep(2)
|
||||
while True:
|
||||
# Read back target and setpoint values
|
||||
# pylint: disable=protected-access
|
||||
r = self._go_n_get("readbackSCS")
|
||||
rb = r[self.name.upper()]
|
||||
self.readback.set(rb, force=True).wait()
|
||||
r = self._go_n_get("targetSCS")
|
||||
sp = r[self.name.upper()]
|
||||
self.setpoint._value = sp
|
||||
# print(f"Readback: {rb}\tSetpoint: {sp}")
|
||||
# Check if they're within tolerance
|
||||
distance = abs(rb - sp)
|
||||
done = 1 if distance < self._tol else 0
|
||||
self.done.put(done, force=True)
|
||||
time.sleep(0.2)
|
||||
|
||||
self._mon = threading.Thread(target=on_target, daemon=True)
|
||||
self._mon.start()
|
||||
|
||||
def omove(self, position, wait=True, timeout=None, moved_cb=None):
|
||||
"""Move command that's masked by BEC"""
|
||||
self.done.put(0, force=True)
|
||||
return self.move(position, wait, timeout, moved_cb)
|
||||
|
||||
def oldmove(self, position, wait=True, timeout=2.0, moved_cb=None):
|
||||
"""Original move command without the BEC wrappers"""
|
||||
status = self.setpoint.set(position, settle_time=0.1).wait()
|
||||
if not wait:
|
||||
return status
|
||||
|
||||
def on_target(*, value, **_):
|
||||
distance = abs(value - self.setpoint._value)
|
||||
print(f"[self.name] Distance: {distance}")
|
||||
return bool(distance < self._tol)
|
||||
|
||||
status = SubscriptionStatus(self.readback, on_target, timeout=timeout, settle_time=0.1)
|
||||
return status
|
||||
|
||||
def describe(self):
|
||||
"""Workaround to schema expected by the BEC"""
|
||||
d = super().describe()
|
||||
d[str(self.name)] = d[f"{self.name}_readback"]
|
||||
return d
|
||||
|
||||
def read(self) -> OrderedDict[str, dict]:
|
||||
"""Workaround to schema expected by the BEC"""
|
||||
d = super().read()
|
||||
d[str(self.name)] = d[f"{self.name}_readback"]
|
||||
return d
|
||||
|
||||
def _pos_changed(self, timestamp=None, value=None, **kwargs):
|
||||
"""Remove EPICS dependency"""
|
||||
pass
|
||||
|
||||
def _go_n_get(self, address, **kwargs):
|
||||
"""Helper function to connect to smargopolo"""
|
||||
cmd = f"{self.sg_url.get()}/{address}"
|
||||
try:
|
||||
with mutex:
|
||||
r = self._s.get(cmd, timeout=1, **kwargs)
|
||||
except TimeoutError:
|
||||
try:
|
||||
time.sleep(0.05)
|
||||
with mutex:
|
||||
r = self._s.get(cmd, timeout=0.5, **kwargs)
|
||||
except TimeoutError:
|
||||
time.sleep(0.05)
|
||||
with mutex:
|
||||
r = self._s.get(cmd, timeout=0.5, **kwargs)
|
||||
if not r.ok:
|
||||
raise RuntimeError(
|
||||
f"[{self.name}] Error getting {address}; reply was {r.status_code} => {r.reason}"
|
||||
)
|
||||
return r.json()
|
||||
|
||||
def _go_n_put(self, address, **kwargs):
|
||||
"""Helper function to connect to smargopolo"""
|
||||
cmd = f"{self.sg_url.get()}/{address}"
|
||||
try:
|
||||
with mutex:
|
||||
r = self._s.put(cmd, timeout=1, **kwargs)
|
||||
except TimeoutError:
|
||||
try:
|
||||
time.sleep(0.05)
|
||||
with mutex:
|
||||
r = self._s.put(cmd, timeout=0.5, **kwargs)
|
||||
except TimeoutError:
|
||||
time.sleep(0.05)
|
||||
with mutex:
|
||||
r = self._s.put(cmd, timeout=0.5, **kwargs)
|
||||
if not r.ok:
|
||||
raise RuntimeError(
|
||||
f"[{self.name}] Error putting {address}; reply was {r.status_code} => {r.reason}"
|
||||
)
|
||||
return r.json()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
shx = SmarGonAxis(prefix="SCS", name="shx", sg_url="http://x06da-smargopolo.psi.ch:3000")
|
||||
shy = SmarGonAxis(prefix="SCS", name="shy", sg_url="http://x06da-smargopolo.psi.ch:3000")
|
||||
shz = SmarGonAxis(
|
||||
prefix="SCS",
|
||||
name="shz",
|
||||
low_limit=10,
|
||||
high_limit=22,
|
||||
sg_url="http://x06da-smargopolo.psi.ch:3000",
|
||||
)
|
||||
shx.wait_for_connection()
|
||||
shy.wait_for_connection()
|
||||
shz.wait_for_connection()
|
||||
@@ -0,0 +1,393 @@
|
||||
# #!/usr/bin/env python3
|
||||
|
||||
# from time import sleep, time
|
||||
# from typing import Tuple
|
||||
|
||||
# from requests import get, put
|
||||
|
||||
# from beamline import beamline
|
||||
# from mx_redis import SMARGON
|
||||
|
||||
# try:
|
||||
# from mx_preferences import get_config
|
||||
|
||||
# host = get_config(beamline)["smargon"]["host"]
|
||||
# port = get_config(beamline)["smargon"]["port"]
|
||||
# except Exception:
|
||||
# host = "x06da-smargopolo.psi.ch"
|
||||
# port = 3000
|
||||
# base = f"http://{host}:{port}"
|
||||
|
||||
|
||||
# def gonget(thing: str, **kwargs) -> dict:
|
||||
# """issue a GET for some API component on the smargopolo server"""
|
||||
# cmd = f"{base}/{thing}"
|
||||
# if kwargs.get("verbose", False):
|
||||
# print(cmd)
|
||||
# r = get(cmd)
|
||||
# if not r.ok:
|
||||
# raise Exception(f"error getting {thing}; server returned {r.status_code} => {r.reason}")
|
||||
# return r.json()
|
||||
|
||||
|
||||
# def gonput(thing: str, **kwargs):
|
||||
# """issue a PUT for some API component on the smargopolo server"""
|
||||
# cmd = f"{base}/{thing}"
|
||||
# if kwargs.get("verbose", False):
|
||||
# print(cmd)
|
||||
# put(cmd)
|
||||
|
||||
|
||||
# def scsput(**kwargs):
|
||||
# """
|
||||
# Issue a new absolute target in the SH coordinate system.
|
||||
|
||||
# The key "verbose" may be passed in kwargs with any true
|
||||
# value for verbose behaviour.
|
||||
|
||||
|
||||
# :param kwargs: a dict containing keys ("shx", "shy", "shz", "chi", "phi")
|
||||
# :type kwargs: dict
|
||||
# :return:
|
||||
# :rtype:
|
||||
# """
|
||||
# xyz = {
|
||||
# k.upper(): v for k, v in kwargs.items() if k.lower() in ("shx", "shy", "shz", "chi", "phi")
|
||||
# }
|
||||
# thing = "&".join([f"{k.upper()}={float(v):.5f}" for k, v in xyz.items()])
|
||||
# cmd = f"{base}/targetSCS?{thing}"
|
||||
# if kwargs.get("verbose", False):
|
||||
# print(cmd)
|
||||
# put(cmd)
|
||||
|
||||
|
||||
# def bcsput(**kwargs):
|
||||
# """
|
||||
# Issue a new absolute target in the beamline coordinate system.
|
||||
|
||||
# The key "verbose" may be passed in kwargs with any true
|
||||
# value for verbose behaviour.
|
||||
|
||||
|
||||
# :param kwargs: a dict containing keys ("bx", "by", "bz", "chi", "phi")
|
||||
# :return:
|
||||
# :rtype:
|
||||
# """
|
||||
# xyz = {k.upper(): v for k, v in kwargs.items() if k.lower() in ("bx", "by", "bz", "chi", "phi")}
|
||||
# thing = "&".join([f"{k.upper()}={float(v):.5f}" for k, v in xyz.items()])
|
||||
# cmd = f"{base}/targetBCS?{thing}"
|
||||
# if kwargs.get("verbose", False):
|
||||
# print(cmd)
|
||||
# put(cmd)
|
||||
|
||||
|
||||
# def scsrelput(**kwargs) -> None:
|
||||
# """
|
||||
# Issue relative increments to current SH coordinate system.
|
||||
|
||||
# The key "verbose" may be passed in kwargs with any true
|
||||
# value for verbose behaviour.
|
||||
|
||||
|
||||
# :param kwargs: a dict containing keys ("shx", "shy", "shz", "chi", "phi")
|
||||
# :type kwargs: dict
|
||||
# :return:
|
||||
# :rtype:
|
||||
# """
|
||||
# xyz = {
|
||||
# k.upper(): v for k, v in kwargs.items() if k.lower() in ("shx", "shy", "shz", "chi", "phi")
|
||||
# }
|
||||
# thing = "&".join([f"{k.upper()}={float(v):.5f}" for k, v in xyz.items()])
|
||||
# cmd = f"{base}/targetSCS_rel?{thing}"
|
||||
# if kwargs.get("verbose", False):
|
||||
# print(cmd)
|
||||
# put(cmd)
|
||||
|
||||
|
||||
# def bcsrelput(**kwargs):
|
||||
# """
|
||||
# Issue relative increments to current beamline coordinate system.
|
||||
|
||||
# The key "verbose" may be passed in kwargs with any true
|
||||
# value for verbose behaviour.
|
||||
|
||||
# :param kwargs: a dict containing keys ("bx", "by", "bz")
|
||||
# :type kwargs: dict
|
||||
# :return:
|
||||
# :rtype:
|
||||
# """
|
||||
# xyz = {k.upper(): v for k, v in kwargs.items() if k.lower() in ("bx", "by", "bz")}
|
||||
# thing = "&".join([f"{k.upper()}={float(v):.5f}" for k, v in xyz.items()])
|
||||
# cmd = f"{base}/targetBCS_rel?{thing}"
|
||||
# if kwargs.get("verbose", False):
|
||||
# print(cmd)
|
||||
# put(cmd)
|
||||
|
||||
|
||||
# # url_redis = f"{beamline}-cons-705.psi.ch"
|
||||
# # print(f"connecting to redis DB #3 on host: {url_redis}")
|
||||
# # redis_handle = redis.StrictRedis(host=url_redis, db=3)
|
||||
# # pubsub = redis_handle.pubsub()
|
||||
|
||||
# MODE_UNINITIALIZED = 0
|
||||
# MODE_INITIALIZING = 1
|
||||
# MODE_READY = 2
|
||||
# MODE_ERROR = 99
|
||||
|
||||
|
||||
# class SmarGon(object):
|
||||
# def __init__(self):
|
||||
# super(SmarGon, self).__init__()
|
||||
# self.__dict__.update(target=None)
|
||||
# self.__dict__.update(bookmarks={})
|
||||
# self.__dict__.update(_latest_message={})
|
||||
# # pubsub.psubscribe(**{f"__keyspace@{SMARGON.value}__:*": self._cb_readbackSCS})
|
||||
# # pubsub.run_in_thread(sleep_time=0.5, daemon=True)
|
||||
|
||||
# def __repr__(self):
|
||||
# BX, BY, BZ, OMEGA, CHI, PHI, a, b, c = self.readback_bcs().values()
|
||||
# return f"<{self.__class__.__name__} X={BX:.3f}, Y={BY:.3f}, Z={BZ:.3f}, CHI={CHI:.3f}, PHI={PHI:.3f}, OMEGA={OMEGA:.3f}>"
|
||||
|
||||
# def _cb_readbackSCS(self, msg):
|
||||
# if msg["data"] in ["hset"]:
|
||||
# self._latest_message = msg
|
||||
|
||||
# def move_home(self, wait=False) -> None:
|
||||
# """move to beamline coordinate system X, Y, Z, Chi, Phi = 0 0 0 0 0"""
|
||||
# self.apply_bookmark_sh({"shx": 0.0, "shy": 0.0, "shz": 18.0, "chi": 0.0, "phi": 0.0})
|
||||
# if wait:
|
||||
# self.wait_home()
|
||||
|
||||
# def xyz(self, coords: Tuple[float, float, float], wait: bool = True) -> None:
|
||||
# """
|
||||
# Move smargon in absolute beamline coordinates
|
||||
|
||||
# :param coords: a tuple of floats representing X, Y, Z coordinates
|
||||
# :type coords:
|
||||
# :param wait:
|
||||
# :type wait:
|
||||
# :return:
|
||||
# :rtype:
|
||||
# """
|
||||
# x, y, z = coords
|
||||
# # the two steps below are necessary otherwise the control system
|
||||
# # remembers *a* previous CHI
|
||||
# bcs = self.bcs
|
||||
# bcs.update({"BX": x, "BY": y, "BZ": z})
|
||||
# self.bcs = bcs
|
||||
# if wait:
|
||||
# self.wait()
|
||||
|
||||
# def wait_home(self, timeout: float = 20.0) -> None:
|
||||
# """
|
||||
# wait for the smargon to reach its home position:
|
||||
# SHX = 0.0
|
||||
# SHY = 0.0
|
||||
# SHZ = 18.0
|
||||
# CHI = 0.0
|
||||
# PHI = 0.0
|
||||
|
||||
# :param timeout: time to wait for positions to be reached raises TimeoutError if timeout reached
|
||||
# :type timeout: float
|
||||
# :return:
|
||||
# :rtype:
|
||||
# """
|
||||
# tout = timeout + time()
|
||||
# in_place = [False, False]
|
||||
# rbv = -999.0
|
||||
# while not all(in_place) and time() < tout:
|
||||
# rbv = self.readback_scs()
|
||||
# in_place = []
|
||||
# for k, v in {"SHX": 0.0, "SHY": 0.0, "SHZ": 18.0, "CHI": 0.0, "PHI": 0.0}.items():
|
||||
# in_place.append(abs(rbv[k] - v) < 0.01)
|
||||
# if time() > tout:
|
||||
# raise TimeoutError(f"timeout waiting for smargon to reach home position: {rbv}")
|
||||
|
||||
# def push_bookmark(self):
|
||||
# """
|
||||
# save current absolute coordinates in FIFO stack
|
||||
# :return:
|
||||
# :rtype:
|
||||
# """
|
||||
# t = round(time())
|
||||
# self.bookmarks[t] = self.readback_scs()
|
||||
|
||||
# def pop_bookmark(self):
|
||||
# return self.bookmarks.popitem()[1]
|
||||
|
||||
# def apply_bookmark_sh(self, scs):
|
||||
# scsput(**scs)
|
||||
|
||||
# def apply_last_bookmark_sh(self):
|
||||
# scs = self.pop_bookmark()
|
||||
# scsput(**scs)
|
||||
|
||||
# def readback_mcs(self):
|
||||
# """current motor positions of the smargon sliders"""
|
||||
# return gonget("readbackMCS")
|
||||
|
||||
# def readback_scs(self):
|
||||
# """current SH coordinates of the smargon model"""
|
||||
# return gonget("readbackSCS")
|
||||
|
||||
# def readback_bcs(self):
|
||||
# """current beamline coordinates of the smargon"""
|
||||
# return gonget("readbackBCS")
|
||||
|
||||
# def target_scs(self):
|
||||
# """currently assigned targets for the smargon control system"""
|
||||
# return gonget("targetSCS")
|
||||
|
||||
# def initialize(self):
|
||||
# """initialize the smargon"""
|
||||
# self.set_mode(MODE_UNINITIALIZED)
|
||||
# sleep(0.1)
|
||||
# self.set_mode(MODE_INITIALIZING)
|
||||
|
||||
# def set_mode(self, mode: int):
|
||||
# """put smargon control system in a given mode
|
||||
# MODE_UNINITIALIZED = 0
|
||||
# MODE_INITIALIZING = 1
|
||||
# MODE_READY = 2
|
||||
# MODE_ERROR = 99
|
||||
# """
|
||||
# gonput(f"mode?mode={mode}")
|
||||
|
||||
# def enable_correction(self):
|
||||
# """enable calibration based corrections"""
|
||||
# gonput("corr_type?corr_type=1")
|
||||
|
||||
# def disable_correction(self):
|
||||
# """disable calibration based corrections"""
|
||||
# gonput("corr_type?corr_type=0")
|
||||
|
||||
# def chi(self, val=None, wait=False):
|
||||
# if val is None:
|
||||
# return self.readback_scs()["CHI"]
|
||||
# scsput(CHI=val)
|
||||
# if wait:
|
||||
# timeout = 10 + time()
|
||||
# while time() < timeout:
|
||||
# if abs(val - self.readback_scs()["CHI"]) < 0.1:
|
||||
# break
|
||||
# if time() > timeout:
|
||||
# raise RuntimeError(f"SmarGon CHI did not reach requested target {val} in time")
|
||||
|
||||
# def phi(self, val=None, wait=False):
|
||||
# if val is None:
|
||||
# return self.readback_scs()["PHI"]
|
||||
# scsput(PHI=val)
|
||||
# if wait:
|
||||
# timeout = 70 + time()
|
||||
# while time() < timeout:
|
||||
# if abs(val - self.readback_scs()["PHI"]) < 0.1:
|
||||
# break
|
||||
# if time() > timeout:
|
||||
# raise RuntimeError(f"SmarGon PHI did not reach requested target {val} in time")
|
||||
|
||||
# def wait(self, timeout=60.0):
|
||||
# """waits up to `timeout` seconds for smargon to reach target"""
|
||||
# target = {
|
||||
# k.upper(): v
|
||||
# for k, v in self.target_scs().items()
|
||||
# if k.lower() in ("shx", "shy", "shz", "chi", "phi")
|
||||
# }
|
||||
|
||||
# timeout = timeout + time()
|
||||
# while time() < timeout:
|
||||
# s = {
|
||||
# k: (abs(v - target[k]) < 0.01)
|
||||
# for k, v in self.readback_scs().items()
|
||||
# if k.upper() in ("SHX", "SHY", "SHZ", "CHI", "PHI")
|
||||
# }
|
||||
# if all(list(s.values())):
|
||||
# break
|
||||
# if time() > timeout:
|
||||
# raise TimeoutError("timed out waiting for smargon to reach target")
|
||||
|
||||
# def __setattr__(self, key, value):
|
||||
# key = key.lower()
|
||||
# if key == "mode":
|
||||
# self.set_mode(value)
|
||||
# elif key == "correction":
|
||||
# assert value in (
|
||||
# 0,
|
||||
# 1,
|
||||
# False,
|
||||
# True,
|
||||
# ), "correction is either 1 or True (enabled) or 0 (disabled)"
|
||||
# gonput(f"corr_type?corr_type?{value}")
|
||||
# elif key == "scs":
|
||||
# scsput(**value)
|
||||
# elif key == "bcs":
|
||||
# bcsput(**value)
|
||||
# elif key == "target":
|
||||
# if not isinstance(value, dict):
|
||||
# raise Exception(
|
||||
# f"expected a dict with target axis and values got something else: {value}"
|
||||
# )
|
||||
# for k in value.keys():
|
||||
# if k.lower() not in "shx shy shz chi phi ox oy oz".split():
|
||||
# raise Exception(f'unknown axis in target "{k}"')
|
||||
# scsput(**value)
|
||||
# elif key in "shx shy shz chi phi ox oy oz".split():
|
||||
# scsput(**{key: value})
|
||||
# elif key in "bx by bz".split():
|
||||
# bcs = self.readback_bcs()
|
||||
# bcs[key] = value
|
||||
# bcsput(**bcs)
|
||||
# else:
|
||||
# self.__dict__[key].update(value)
|
||||
|
||||
# def __getattr__(self, key):
|
||||
# key = key.lower()
|
||||
# if key == "mode":
|
||||
# return self.readback_mcs()["mode"]
|
||||
# elif key == "correction":
|
||||
# return gonget("corr_type")
|
||||
# elif key == "bcs":
|
||||
# return self.readback_bcs()
|
||||
# elif key == "mcs":
|
||||
# return self.readback_mcs()
|
||||
# elif key == "scs":
|
||||
# return self.readback_scs()
|
||||
# elif key in "shx shy shz chi phi ox oy oz".split():
|
||||
# return self.readback_scs()[key.upper()]
|
||||
# elif key in "bx by bz".split():
|
||||
# return self.readback_bcs()[key.upper()]
|
||||
# else:
|
||||
# return self.__getattribute__(key)
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# import argparse
|
||||
|
||||
# parser = argparse.ArgumentParser(description="SmarGon client")
|
||||
# parser.add_argument("-i", "--initialize", help="initialize smargon", action="store_true")
|
||||
# args = parser.parse_args()
|
||||
|
||||
# smargon = SmarGon()
|
||||
|
||||
# if args.initialize:
|
||||
# print("initializing smargon device")
|
||||
# import Aerotech
|
||||
|
||||
# print("moving aerotech back by 50mm")
|
||||
# abr = Aerotech.Abr()
|
||||
|
||||
# abr.incr_x(-50.0, wait=True, velo=100.0)
|
||||
|
||||
# print("issuing init command to smargon")
|
||||
# smargon.initialize()
|
||||
# sleep(0.5)
|
||||
|
||||
# print("waiting for init routine to complete")
|
||||
# while MODE_READY != smargon.mode:
|
||||
# sleep(0.5)
|
||||
|
||||
# print("moving smargon to HOME position")
|
||||
# smargon.move_home()
|
||||
|
||||
# print("moving aerotech to its previous position")
|
||||
# abr.incr_x(50.0, wait=True, velo=100.0)
|
||||
# exit(0)
|
||||
@@ -0,0 +1,206 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Standard DAQ preview image stream module
|
||||
|
||||
Created on Thu Jun 27 17:28:43 2024
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
import json
|
||||
from time import sleep, time
|
||||
from threading import Thread
|
||||
import zmq
|
||||
import numpy as np
|
||||
from ophyd import Device, Signal, Component, Kind
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin,
|
||||
PSIDetectorBase,
|
||||
)
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
ZMQ_TOPIC_FILTER = b""
|
||||
|
||||
|
||||
class StdDaqPreviewMixin(CustomDetectorMixin):
|
||||
"""Setup class for the standard DAQ preview stream
|
||||
|
||||
Parent class: CustomDetectorMixin
|
||||
"""
|
||||
|
||||
_mon = None
|
||||
|
||||
def on_stage(self):
|
||||
"""Start listening for preview data stream"""
|
||||
if self._mon is not None:
|
||||
self.parent.unstage()
|
||||
sleep(0.5)
|
||||
|
||||
logger.info(f"[{self.parent.name}] Attaching monitor to {self.parent.url.get()}")
|
||||
self.parent.connect()
|
||||
self._stop_polling = False
|
||||
self._mon = Thread(target=self.poll, daemon=True)
|
||||
self._mon.start()
|
||||
|
||||
def on_unstage(self):
|
||||
"""Stop a running preview"""
|
||||
if self._mon is not None:
|
||||
self._stop_polling = True
|
||||
# Might hang on recv_multipart
|
||||
self._mon.join(timeout=1)
|
||||
# So also disconnect the socket
|
||||
try:
|
||||
# pylint: disable=protected-access
|
||||
self.parent._socket.disconnect(self.parent.url.get())
|
||||
except zmq.error.ZMQError:
|
||||
# Might be already closed
|
||||
pass
|
||||
|
||||
def on_stop(self):
|
||||
"""Stop a running preview"""
|
||||
self.on_unstage()
|
||||
|
||||
def poll(self):
|
||||
"""Collect streamed updates"""
|
||||
try:
|
||||
t_last = time()
|
||||
while True:
|
||||
try:
|
||||
# Exit loop and finish monitoring
|
||||
if self._stop_polling:
|
||||
logger.info(f"[{self.parent.name}]\tDetaching monitor")
|
||||
break
|
||||
|
||||
# pylint: disable=no-member
|
||||
# pylint: disable=protected-access
|
||||
r = self.parent._socket.recv_multipart(flags=zmq.NOBLOCK)
|
||||
|
||||
# Length and throtling checks
|
||||
if len(r) != 2:
|
||||
logger.warning(
|
||||
f"[{self.parent.name}] Received malformed array of length {len(r)}"
|
||||
)
|
||||
t_curr = time()
|
||||
t_elapsed = t_curr - t_last
|
||||
if t_elapsed < self.parent.throttle.get():
|
||||
sleep(0.1)
|
||||
continue
|
||||
|
||||
# Unpack the Array V1 reply to metadata and array data
|
||||
meta, data = r
|
||||
|
||||
# Update image and update subscribers
|
||||
header = json.loads(meta)
|
||||
image = np.frombuffer(data, dtype=header["type"])
|
||||
if image.size != np.prod(header["shape"]):
|
||||
err = f"Unexpected array size of {image.size} for header: {header}"
|
||||
raise ValueError(err)
|
||||
image = image.reshape(header["shape"])
|
||||
|
||||
# Update image and update subscribers
|
||||
self.parent.array_counter.put(header["frame"], force=True)
|
||||
self.parent.ndimensions.put(len(header["shape"]), force=True)
|
||||
self.parent.array_size.put(header["shape"], force=True)
|
||||
# self.parent.array_data.put(data, force=True)
|
||||
self.parent.shaped_image.put(image, force=True)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.parent._last_image = image
|
||||
self.parent._run_subs(sub_type=self.parent.SUB_MONITOR, value=image)
|
||||
t_last = t_curr
|
||||
logger.info(
|
||||
f"[{self.parent.name}] Updated frame {header['frame']}\t"
|
||||
f"Shape: {header['shape']}\tMean: {np.mean(image):.3f}"
|
||||
)
|
||||
except ValueError:
|
||||
# Happens when ZMQ partially delivers the multipart message
|
||||
pass
|
||||
except zmq.error.Again:
|
||||
# Happens when receive queue is empty
|
||||
sleep(0.1)
|
||||
except Exception as ex:
|
||||
logger.info(f"[{self.parent.name}]\t{str(ex)}")
|
||||
raise
|
||||
finally:
|
||||
self._mon = None
|
||||
logger.info(f"[{self.parent.name}]\tDetaching monitor")
|
||||
|
||||
|
||||
class StdDaqPreviewDetector(PSIDetectorBase):
|
||||
"""Detector wrapper class around the StdDaq preview image stream.
|
||||
|
||||
This was meant to provide live image stream directly from the StdDAQ but
|
||||
also works with other ARRAY v1 streamers, like the AreaDetector ZMQ plugin.
|
||||
Note that the preview stream must be already throtled in order to cope with
|
||||
the incoming data and the python class might throttle it further.
|
||||
|
||||
NOTE: As an explicit request, it does not record the image data.
|
||||
|
||||
You can add a preview widget to the dock by:
|
||||
cam_widget = gui.add_dock('cam_dock1').add_widget('BECFigure').image('daq_stream1')
|
||||
"""
|
||||
|
||||
# Subscriptions for plotting image
|
||||
USER_ACCESS = ["image", "savemode"]
|
||||
SUB_MONITOR = "device_monitor_2d"
|
||||
_default_sub = SUB_MONITOR
|
||||
|
||||
custom_prepare_cls = StdDaqPreviewMixin
|
||||
|
||||
# Configuration attributes
|
||||
url = Component(Signal, kind=Kind.config, metadata={"write_access": False})
|
||||
throttle = Component(Signal, value=0.25, kind=Kind.config)
|
||||
# Streamed data status
|
||||
array_counter = Component(Signal, kind=Kind.hinted, metadata={"write_access": False})
|
||||
ndimensions = Component(Signal, kind=Kind.normal, metadata={"write_access": False})
|
||||
array_size = Component(Signal, kind=Kind.normal, metadata={"write_access": False})
|
||||
# array_data = Component(Signal, kind=Kind.omitted, metadata={"write_access": False})
|
||||
shaped_image = Component(Signal, kind=Kind.omitted, metadata={"write_access": False})
|
||||
_last_image = None
|
||||
|
||||
def __init__(
|
||||
self, *args, url: str = "tcp://129.129.95.38:20000", parent: Device = None, **kwargs
|
||||
) -> None:
|
||||
super().__init__(*args, parent=parent, **kwargs)
|
||||
self.url.set(url, force=True).wait()
|
||||
# Connect to the DAQ
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
"""Connect to te StDAQs PUB-SUB streaming interface
|
||||
|
||||
StdDAQ may reject connection for a few seconds when it restarts,
|
||||
so if it fails, wait a bit and try to connect again.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
# Socket to talk to server
|
||||
context = zmq.Context()
|
||||
self._socket = context.socket(zmq.SUB)
|
||||
self._socket.setsockopt(zmq.SUBSCRIBE, ZMQ_TOPIC_FILTER)
|
||||
try:
|
||||
self._socket.connect(self.url.get())
|
||||
except ConnectionRefusedError:
|
||||
sleep(1)
|
||||
self._socket.connect(self.url.get())
|
||||
|
||||
def savemode(self, save=False):
|
||||
"""Toggle save mode for the shaped image"""
|
||||
# pylint: disable=protected-access
|
||||
if save:
|
||||
self.shaped_image._kind = Kind.normal
|
||||
else:
|
||||
self.shaped_image._kind = Kind.omitted
|
||||
|
||||
def image(self):
|
||||
"""
|
||||
Gets the last image as an attribute in case image must be abandoned
|
||||
due to some caching on the BEC.
|
||||
"""
|
||||
return self._last_image
|
||||
|
||||
|
||||
# Automatically connect to MicroSAXS testbench if directly invoked
|
||||
if __name__ == "__main__":
|
||||
daq = StdDaqPreviewDetector(url="tcp://129.129.95.111:20000", name="preview")
|
||||
daq.wait_for_connection()
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Ophyd devices for the PX III beamline, including the MX specific Aerotech A3200 stage.
|
||||
|
||||
@author: mohacsi_i
|
||||
"""
|
||||
from .A3200 import AerotechAbrStage
|
||||
from .A3200utils import A3200Axis
|
||||
from .SmarGonA import SmarGonAxis as SmarGonAxisA
|
||||
from .SmarGonB import SmarGonAxis as SmarGonAxisB
|
||||
from .StdDaqPreview import StdDaqPreviewDetector
|
||||
from .NDArrayPreview import NDArrayPreview
|
||||
from .SamCamDetector import SamCamDetector
|
||||
from .PneumaticValve import PneumaticValve
|
||||
@@ -0,0 +1,43 @@
|
||||
from ophyd import Component as Cpt
|
||||
|
||||
from .http import TIMESTAMP_ID, HttpDeviceController, HttpDeviceSignal, HttpOphydDevice
|
||||
|
||||
|
||||
class AerotechController(HttpDeviceController):
|
||||
_readback_endpoint = "/status"
|
||||
_target_endpoint = "/position"
|
||||
|
||||
def __init__(self, *, prefix, **kwargs):
|
||||
self._readbacks: dict[str, dict[str, float | bool]] = {}
|
||||
super().__init__(prefix=prefix, **kwargs)
|
||||
|
||||
def put(self, axis: str, val: float):
|
||||
self._rest_post(body={axis: val})
|
||||
|
||||
def get_readback(self, axis_id: str) -> tuple[float, float] | None:
|
||||
with self._readback_lock:
|
||||
if axis_id not in self._readbacks or TIMESTAMP_ID not in self._readbacks:
|
||||
return None
|
||||
return self._readbacks.get(axis_id)["pos"], self._readbacks.get(TIMESTAMP_ID) # type: ignore
|
||||
|
||||
|
||||
class Aerotech(HttpOphydDevice):
|
||||
controller_class = AerotechController
|
||||
|
||||
x = Cpt(HttpDeviceSignal, axis_identifier="x", tolerance=0.01)
|
||||
y = Cpt(HttpDeviceSignal, axis_identifier="y", tolerance=0.01)
|
||||
z = Cpt(HttpDeviceSignal, axis_identifier="z", tolerance=0.01)
|
||||
u = Cpt(HttpDeviceSignal, axis_identifier="u", tolerance=0.01)
|
||||
vel_u_deg_s = Cpt(HttpDeviceSignal, axis_identifier="vel_u_deg_s", tolerance=0.01)
|
||||
|
||||
|
||||
def _test():
|
||||
a = Aerotech(name="aerotech", prefix="http://mx-x06da-queue-01:5234")
|
||||
a.wait_for_connection()
|
||||
return a
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
aerotech = _test()
|
||||
print(aerotech.read())
|
||||
aerotech.stop()
|
||||
@@ -0,0 +1,178 @@
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from threading import Event, RLock, Thread
|
||||
from typing import Any
|
||||
|
||||
from ophyd import OphydObject
|
||||
from ophyd_devices import PSIDeviceBase
|
||||
from ophyd_devices.utils.socket import SocketSignal
|
||||
from requests import Response, Session
|
||||
|
||||
TIMESTAMP_ID = "__timestamp"
|
||||
_POLL_INTERVAL_SLOW = 0.1
|
||||
|
||||
|
||||
class HttpRestError(Exception):
|
||||
"""Error for rest calls from a HttpRestSignal."""
|
||||
|
||||
def __init__(self, resp: Response, *args: object, value: Any | None = None) -> None:
|
||||
method, url = resp.request.method, resp.request.url
|
||||
data = f"{str(value)} to " if value is not None else ""
|
||||
super().__init__(
|
||||
f"Could not {method} {data}{url}. Code: {resp.status_code}. Reason: {resp.reason}.",
|
||||
*args,
|
||||
)
|
||||
|
||||
|
||||
class HttpDeviceController(OphydObject, ABC):
|
||||
"""Controller to consolidate polling loops and other REST calls for devices which communicate
|
||||
with HTTP REST interfaces"""
|
||||
|
||||
_readback_endpoint: str
|
||||
_target_endpoint: str
|
||||
|
||||
def __init__(self, *, prefix, **kwargs):
|
||||
self._readbacks: dict
|
||||
self._session = Session()
|
||||
self._prefix = prefix
|
||||
self._targets = {}
|
||||
self._signal_registry: set[str] = set()
|
||||
self._readback_poll_interval: float = _POLL_INTERVAL_SLOW
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self._setup_readback()
|
||||
|
||||
def _setup_readback(self):
|
||||
self._stop_monitor_readback_event = Event()
|
||||
self._readback_lock = RLock()
|
||||
self._monitor_readback_thread = Thread(
|
||||
target=self._monitor,
|
||||
args=[
|
||||
self._readback_endpoint,
|
||||
self._stop_monitor_readback_event,
|
||||
self._readback_lock,
|
||||
self._readbacks,
|
||||
],
|
||||
)
|
||||
|
||||
def manual_update(self):
|
||||
self._update_reading(self._readback_endpoint, self._readback_lock, self._readbacks)
|
||||
|
||||
def _update_reading(self, endpoint: str, lock: RLock, buffer: dict):
|
||||
data = self._rest_get(endpoint)
|
||||
timestamp = time.monotonic()
|
||||
with lock:
|
||||
buffer.update(data)
|
||||
buffer["__timestamp"] = timestamp
|
||||
|
||||
def _monitor(self, endpoint: str, event: Event, lock: RLock, buffer: dict):
|
||||
while not event.is_set():
|
||||
self._update_reading(endpoint, lock, buffer)
|
||||
time.sleep(self._readback_poll_interval)
|
||||
|
||||
def _clean_monitor(self):
|
||||
if self._monitor_readback_thread.is_alive():
|
||||
self._stop_monitor_readback_event.set()
|
||||
self._monitor_readback_thread.join(timeout=2)
|
||||
if self._monitor_readback_thread.is_alive():
|
||||
raise RuntimeError("Failed to clean up Aerotech monitor thread.")
|
||||
|
||||
def register(self, axis_id: str):
|
||||
self._signal_registry.add(axis_id)
|
||||
|
||||
def _rest_get(self, endpoint):
|
||||
resp = self._session.get(self._prefix + endpoint)
|
||||
if not resp.ok:
|
||||
raise HttpRestError(resp)
|
||||
return resp.json()
|
||||
|
||||
def _rest_put(self, params: dict | None = None, body: dict | None = None):
|
||||
resp = self._session.put(self._prefix + self._target_endpoint, params=params, json=body)
|
||||
if not resp.ok:
|
||||
raise HttpRestError(resp, value=params)
|
||||
|
||||
def _rest_post(self, params: dict | None = None, body: dict | None = None):
|
||||
resp = self._session.post(self._prefix + self._target_endpoint, params=params, json=body)
|
||||
if not resp.ok:
|
||||
raise HttpRestError(resp, value=params)
|
||||
|
||||
def start_monitor(self):
|
||||
"""Start or restart the automonitor thread."""
|
||||
self._clean_monitor()
|
||||
self._setup_readback()
|
||||
self._monitor_readback_thread.start()
|
||||
|
||||
def monitor_stopped(self):
|
||||
return not self._monitor_readback_thread.is_alive()
|
||||
|
||||
def put(self, axis: str, val: float):
|
||||
self._rest_put({axis: val})
|
||||
|
||||
@abstractmethod
|
||||
def get_readback(self, axis_id: str) -> tuple[float, float] | None:
|
||||
"""Return a tuple (reading, timestamp) if the axis_id exists"""
|
||||
|
||||
def stop(self):
|
||||
# There doesn't appear to be a stop endpoint on the server
|
||||
# Best effort: set the target to the current position
|
||||
pass
|
||||
# TODO: self._rest_put(self._readbacks)
|
||||
|
||||
|
||||
class HttpDeviceSignal(SocketSignal):
|
||||
"""Ophyd signal which gets and puts to a REST API rather than EPICS PVs, mediated through the Aerotech
|
||||
Controller"""
|
||||
|
||||
def __init__(self, *args, axis_identifier: str, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
controller: HttpDeviceController | None = getattr(self.root, "controller", None)
|
||||
if controller is None:
|
||||
raise TypeError("HttpDeviceSignal must be used in a device with a HttpDeviceController")
|
||||
self._controller = controller
|
||||
self._axis_id = axis_identifier
|
||||
self._controller.register(self._axis_id)
|
||||
|
||||
def _socket_get(self): # type: ignore
|
||||
self._readback, self.metadata["timestamp"] = self._controller.get_readback(
|
||||
self._axis_id
|
||||
) or (0.0, 0.0)
|
||||
return self._readback
|
||||
|
||||
def _socket_set(self, val: float):
|
||||
self._controller.put(self._axis_id, val)
|
||||
|
||||
def get(self, **kwargs):
|
||||
if self._controller.monitor_stopped():
|
||||
self._controller.start_monitor()
|
||||
return super().get(**kwargs)
|
||||
|
||||
|
||||
class HttpOphydDevice(PSIDeviceBase):
|
||||
controller_class: type[HttpDeviceController]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
prefix: str = "",
|
||||
scan_info=None,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
self.controller = self.controller_class(prefix=prefix)
|
||||
super().__init__(
|
||||
name=name,
|
||||
prefix=prefix,
|
||||
scan_info=scan_info,
|
||||
device_manager=device_manager,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def wait_for_connection(self, **kwargs): # type: ignore
|
||||
self.controller.start_monitor()
|
||||
self.controller.manual_update()
|
||||
return super().wait_for_connection(**kwargs)
|
||||
|
||||
def stop(self, *, success: bool = False) -> None:
|
||||
self.controller.stop()
|
||||
return super().stop(success=success)
|
||||
@@ -0,0 +1,42 @@
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd_devices import PSIDeviceBase
|
||||
|
||||
from .http import HttpDeviceController, HttpDeviceSignal, HttpOphydDevice
|
||||
|
||||
_TIMESTAMP_ID = "__timestamp"
|
||||
_POLL_INTERVAL_SLOW = 0.1
|
||||
|
||||
|
||||
class SmargonController(HttpDeviceController):
|
||||
"""Controller to consolidate polling loops and other REST calls for the smargon"""
|
||||
|
||||
_readback_endpoint = "/readbackSCS"
|
||||
_target_endpoint = "/targetSCS"
|
||||
|
||||
def __init__(self, *, prefix, **kwargs):
|
||||
self._readbacks: dict[str, float] = {}
|
||||
super().__init__(prefix=prefix, **kwargs)
|
||||
|
||||
def get_readback(self, axis_id: str) -> tuple[float, float] | None:
|
||||
with self._readback_lock:
|
||||
if axis_id not in self._readbacks or _TIMESTAMP_ID not in self._readbacks:
|
||||
return None
|
||||
return self._readbacks.get(axis_id), self._readbacks.get(_TIMESTAMP_ID) # type: ignore
|
||||
|
||||
def put(self, axis: str, val: float):
|
||||
self._rest_put(params={axis: val})
|
||||
|
||||
def stop(self):
|
||||
# There doesn't appear to be a stop endpoint on the server
|
||||
# Best effort: set the target to the current position
|
||||
self._rest_put(params=self._readbacks)
|
||||
|
||||
|
||||
class Smargon(HttpOphydDevice):
|
||||
controller_class = SmargonController
|
||||
|
||||
x = Cpt(HttpDeviceSignal, axis_identifier="SHX", tolerance=0.01)
|
||||
y = Cpt(HttpDeviceSignal, axis_identifier="SHY", tolerance=0.01)
|
||||
z = Cpt(HttpDeviceSignal, axis_identifier="SHZ", tolerance=0.01)
|
||||
phi = Cpt(HttpDeviceSignal, axis_identifier="PHI", tolerance=0.01)
|
||||
chi = Cpt(HttpDeviceSignal, axis_identifier="CHI", tolerance=0.01)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,22 +0,0 @@
|
||||
"""Enums for beamline states"""
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class BeamlineState(str, Enum):
|
||||
"""List of beamline states"""
|
||||
ROBOT_SAMPLE_EXCHANGE = "robot_sample_exchange"
|
||||
SAMPLE_ALIGNMENT = "sample_alignment"
|
||||
DATA_COLLECTION = "data_collection"
|
||||
DC_XRF = "DC_XRF"
|
||||
MANUAL_SAMPLE_EXCHANGE = "manual_sample_exchange"
|
||||
BEAM_VISUALISATION = "beam_visualisation"
|
||||
FLUX_MEASUREMENT = "flux_measurement"
|
||||
BEAMSTOP_ALIGNMENT = "beamstop_alignment"
|
||||
MAINTENANCE = "maintenance"
|
||||
XTAL_SNAPSHOT = "xtal_snapshot"
|
||||
|
||||
|
||||
class TemperatureMode(str, Enum):
|
||||
"""List of temperature modes"""
|
||||
CRYO = "cryo"
|
||||
ROOM_TEMP = "room_temp"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,252 +0,0 @@
|
||||
"""Get data from an h5 file or BEC history and perform fitting."""
|
||||
|
||||
import numpy as np
|
||||
from lmfit.models import (
|
||||
GaussianModel,
|
||||
LorentzianModel,
|
||||
VoigtModel,
|
||||
ConstantModel,
|
||||
LinearModel,
|
||||
)
|
||||
from scipy.ndimage import gaussian_filter1d
|
||||
import h5py
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
def create_fit_parameters(
|
||||
deriv: bool = False,
|
||||
model: str = "Voigt",
|
||||
baseline: str = "Linear",
|
||||
smoothing: None = None,
|
||||
):
|
||||
"""Store the fit parameters in a dictionary."""
|
||||
# map input model to lmfit model name
|
||||
model_mappings = {
|
||||
"Gaussian": GaussianModel,
|
||||
"Lorentzian": LorentzianModel,
|
||||
"Voigt": VoigtModel,
|
||||
"Constant": ConstantModel,
|
||||
"Linear": LinearModel,
|
||||
}
|
||||
return {
|
||||
"deriv": deriv,
|
||||
"model": model_mappings[model],
|
||||
"baseline": model_mappings[baseline],
|
||||
"smoothing": smoothing,
|
||||
}
|
||||
|
||||
|
||||
def get_data_from_h5(signal_name: str = "lu_bpmsum"):
|
||||
"""Get data from an h5 file."""
|
||||
with h5py.File("scan_676.h5", "r") as f:
|
||||
entry = f["entry"]["collection"]
|
||||
y_data = entry["devices"][signal_name][signal_name]["value"][:]
|
||||
motor_data = entry["metadata"]["bec"]
|
||||
motor_name = motor_data["scan_motors"][0].decode()
|
||||
scan_number = motor_data["scan_number"][()]
|
||||
x_data = entry["devices"][motor_name][motor_name]["value"][:]
|
||||
return {
|
||||
"x_data": x_data,
|
||||
"y_data": y_data,
|
||||
"signal_name": signal_name,
|
||||
"motor_name": motor_name,
|
||||
"scan_number": str(scan_number),
|
||||
}
|
||||
|
||||
|
||||
def get_data_from_history(
|
||||
history_index: int,
|
||||
signal_name: str = "lu_bpmsum",
|
||||
):
|
||||
"""Read data from the BEC history and return the X and Y data as arrays."""
|
||||
scan = bec.history[history_index]
|
||||
md = scan.metadata["bec"]
|
||||
motor_name = md["scan_motors"][0].decode()
|
||||
scan_number = md["scan_number"]
|
||||
x_data = scan.devices[motor_name][motor_name].read()["value"]
|
||||
y_data = scan.devices[signal_name][signal_name].read()["value"]
|
||||
return {
|
||||
"signal_name": signal_name,
|
||||
"x_data": x_data,
|
||||
"y_data": y_data,
|
||||
"motor_name": motor_name,
|
||||
"scan_number": scan_number,
|
||||
}
|
||||
|
||||
|
||||
def process_data(data, fit_params):
|
||||
"""
|
||||
Process the signal data for fitting based on derivative or smoothing.
|
||||
"""
|
||||
smoothing, deriv = fit_params["smoothing"], fit_params["deriv"]
|
||||
signal_name = data["signal_name"]
|
||||
y_data = data["y_data"]
|
||||
|
||||
if deriv:
|
||||
if smoothing:
|
||||
y_smooth = gaussian_filter1d(y_data, smoothing)
|
||||
fitting_data = np.gradient(y_smooth)
|
||||
signal_name = f"Derivative of smoothed {signal_name}"
|
||||
else:
|
||||
fitting_data = np.gradient(y_data)
|
||||
signal_name = f"Derivative of {signal_name}"
|
||||
elif smoothing and smoothing > 0.01:
|
||||
fitting_data = gaussian_filter1d(y_data, smoothing)
|
||||
signal_name = f"Smoothed {signal_name}"
|
||||
else:
|
||||
fitting_data = y_data
|
||||
|
||||
updated_data = {
|
||||
"y_to_fit": fitting_data,
|
||||
"signal_name": signal_name,
|
||||
}
|
||||
data.update(updated_data)
|
||||
return data
|
||||
|
||||
|
||||
def fit(data, fit_params):
|
||||
"""Fit a signal to a model and return the fitting results."""
|
||||
# Create the model
|
||||
peak_model = fit_params["model"](prefix="peak_")
|
||||
baseline_model = fit_params["baseline"](prefix="base_")
|
||||
full_model = peak_model + baseline_model
|
||||
|
||||
# Prepare data
|
||||
processed_data = process_data(data, fit_params)
|
||||
params = full_model.make_params()
|
||||
y_min = np.min(processed_data["y_to_fit"])
|
||||
|
||||
# Configure baseline parameters
|
||||
if fit_params["baseline"] == ConstantModel:
|
||||
params["base_c"].set(value=y_min)
|
||||
elif fit_params["baseline"] == LinearModel:
|
||||
params["base_intercept"].set(value=y_min)
|
||||
params["base_slope"].set(value=0)
|
||||
|
||||
# Add peak-specific parameters
|
||||
params.update(
|
||||
peak_model.guess(processed_data["y_to_fit"], x=processed_data["x_data"])
|
||||
)
|
||||
|
||||
# Perform the fitting
|
||||
lmfit_result = full_model.fit(
|
||||
processed_data["y_to_fit"], params, x=processed_data["x_data"]
|
||||
)
|
||||
|
||||
# Find the X that gives the max Y
|
||||
max_index = np.argmax(processed_data["y_to_fit"])
|
||||
x_max = processed_data["x_data"][max_index]
|
||||
|
||||
# Generate data for a smoothed fit curve
|
||||
fit_xdata = np.linspace(np.min(data["x_data"]), np.max(data["x_data"]), 500)
|
||||
fit_ydata = lmfit_result.eval(x=fit_xdata, params=lmfit_result.params)
|
||||
|
||||
# Collect results
|
||||
return {
|
||||
"model": fit_params["model"].__name__,
|
||||
"fwhm": lmfit_result.params["peak_fwhm"].value,
|
||||
"centre": lmfit_result.best_values["peak_center"],
|
||||
"height": lmfit_result.params["peak_height"].value,
|
||||
"chi_sq": lmfit_result.chisqr,
|
||||
"lmfit_result": lmfit_result,
|
||||
"x_max": x_max,
|
||||
"fit_xdata": fit_xdata,
|
||||
"fit_ydata": fit_ydata,
|
||||
}
|
||||
|
||||
|
||||
def plot_fitted_data(data, fit_result):
|
||||
"""Plot the original data and the fitted model."""
|
||||
plt.plot(data["x_data"], data["y_to_fit"], label="Data")
|
||||
plt.plot(fit_result['fit_xdata'], fit_result['fit_ydata'], label="Fit")
|
||||
plt.xlabel(data["motor_name"])
|
||||
plt.ylabel(data["signal_name"])
|
||||
plt.title(f"Scan {data['scan_number']}, fitted with {fit_result['model']}")
|
||||
plt.grid(True)
|
||||
plt.legend()
|
||||
plt.show()
|
||||
|
||||
|
||||
def select_bec_window(dock_area_name="Fitting"):
|
||||
"""Check to see if the fitting results dock is already open and re-create it if not"""
|
||||
open_docks = bec.gui.windows
|
||||
if open_docks.get(dock_area_name) is None:
|
||||
dock_area = bec.gui.new(dock_area_name)
|
||||
# wf = dock_area.new("Plot").new(bec.gui.available_widgets.Waveform)
|
||||
wf = dock_area.new(widget='Waveform', object_name='Plot')
|
||||
text_box = dock_area.new(widget='TextBox', object_name="Results", where="bottom")
|
||||
else:
|
||||
wf = bec.gui.Fitting.Plot
|
||||
text_box = bec.gui.Fitting.Results
|
||||
return wf, text_box
|
||||
|
||||
|
||||
def plot_live_data_bec(
|
||||
motor_name,
|
||||
signal_name,
|
||||
window_name="Fitting"
|
||||
):
|
||||
"""
|
||||
Plotting live data for motor and signal using BEC.
|
||||
|
||||
This function plots live data from a specified motor and signal.
|
||||
It clears the current plot window, sets its title, labels the axes
|
||||
with the provided motor and signal names, and initializes live plotting
|
||||
on the given signal against the motor.
|
||||
|
||||
Args:
|
||||
motor_name (str): The name of the motor to be used as the x-axis.
|
||||
signal_name (str): The name of the signal to be used as the y-axis.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
wf, text_box = select_bec_window(window_name)
|
||||
text_box.set_plain_text("Plotting live data")
|
||||
wf.clear_all()
|
||||
wf.title = "Scan: Live scan"
|
||||
wf.x_label = motor_name
|
||||
wf.y_label = signal_name
|
||||
wf.plot(device_x=motor_name, device_y=signal_name)
|
||||
|
||||
|
||||
def plot_fitted_data_bec(
|
||||
data,
|
||||
fit_result,
|
||||
):
|
||||
"""
|
||||
Plot fitted data and display fitting parameters in the specified window.
|
||||
|
||||
This function selects a BEC window and plots the original data along with the
|
||||
fitted function. Additionally, it displays the fitting results in a text
|
||||
box within the same window for better visualization of the fit results.
|
||||
|
||||
Parameters:
|
||||
data : dict
|
||||
Dictionary containing the original dataset, where 'x_data' and 'y_to_fit'
|
||||
hold the independent variable and the dependent variable, respectively,
|
||||
'scan_number' represents the scan number, 'motor_name' and 'signal_name'
|
||||
provide axis labels.
|
||||
fit_result : dict
|
||||
Dictionary containing the results of the fit, including parameters such
|
||||
as 'centre', 'fwhm', 'height', and the fitted model stored under
|
||||
'lmfit_result', with its 'best_fit' attribute representing the fitted data.
|
||||
"""
|
||||
wf, text_box = select_bec_window()
|
||||
fit_text = (
|
||||
f"Fit parameters: Centre = {fit_result['centre']:.4f}, "
|
||||
f"FWHM = {fit_result['fwhm']:.3f}, "
|
||||
f"Height = {fit_result['height']:.4f}\n"
|
||||
f"Model = {fit_result['model']}\n"
|
||||
f"Chi sq = {fit_result['chi_sq']:.3g}"
|
||||
)
|
||||
text_box.set_plain_text(fit_text)
|
||||
wf.clear_all()
|
||||
wf.title = f"Scan: {data['scan_number']}"
|
||||
wf.x_label = data["motor_name"]
|
||||
wf.y_label = data["signal_name"]
|
||||
wf.plot(x=data["x_data"], y=data["y_to_fit"], label="Data")
|
||||
wf.plot(x=fit_result["fit_xdata"], y=fit_result["fit_ydata"], label="Fit")
|
||||
# wf.Fit.set(symbol_size = 0)
|
||||
wf.get_curve('Fit').set(symbol_size=0)
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
"""Use the methods in mx_basics to perform:
|
||||
1) a go_to_peak scan, that scans a motor, finds the peak position and moves to peak
|
||||
2) fits data from a bec history file
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
# from pxiii_parameters import FitDefaults, BPMScans, MirrorConfig
|
||||
|
||||
# from mx_basics import (
|
||||
# create_fit_parameters,
|
||||
# get_data_from_history,
|
||||
# fit,
|
||||
# plot_fitted_data_bec,
|
||||
# plot_live_data_bec,
|
||||
# )
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FitDefaults:
|
||||
"""Default values for fitting routines"""
|
||||
|
||||
# Constants for default models, baselines, and parameters
|
||||
MODEL = "Voigt"
|
||||
BASELINE = "Linear"
|
||||
SETTLE_TIME = 0.1
|
||||
RELATIVE_MODE = True
|
||||
|
||||
|
||||
# Method functions
|
||||
def calculate_step_size(start: float, stop: float, steps: int) -> float:
|
||||
"""
|
||||
Provides the function to calculate the step size for dividing a specified range
|
||||
into a given number of steps.
|
||||
|
||||
Args:
|
||||
start: The starting value of the range.
|
||||
stop: The stopping value of the range.
|
||||
steps: The number of steps to divide the range into. Must be at least 1.
|
||||
|
||||
Raises:
|
||||
ValueError: If the steps value is less than 1.
|
||||
|
||||
Returns:
|
||||
The calculated step size as a float, rounded to three decimal places.
|
||||
"""
|
||||
if steps < 1:
|
||||
raise ValueError("Number of steps must be at least 1.")
|
||||
return round((stop - start) / steps, 3)
|
||||
|
||||
|
||||
def move_to_position(motor_device, motor_name: str, position: float, data: dict):
|
||||
"""
|
||||
Function to move a specified motor device to a given position.
|
||||
|
||||
The function verifies if the requested position is within the scan range of the
|
||||
motor device provided. If the position is outside the range, the motor is
|
||||
moved to the center of its scan range, an error message is raised, and the
|
||||
operation is halted. If the position is valid, the motor is moved to the
|
||||
specified position.
|
||||
|
||||
Parameters:
|
||||
motor_device: The motor device to be moved.
|
||||
motor_name: str
|
||||
The name of the motor as a string
|
||||
position: float
|
||||
The desired position to move the motor to. Position should be within
|
||||
the scan range of the motor determined by the provided data.
|
||||
data: dict
|
||||
A dictionary containing "x_data", which is used to determine the
|
||||
scan range of the motor.
|
||||
|
||||
Raises:
|
||||
ValueError: Raised if the specified position is outside the valid scan
|
||||
range determined by "x_data" in the data dictionary. The motor will
|
||||
return to the center of its scan range in this case.
|
||||
"""
|
||||
|
||||
motor_min = np.min(data["x_data"])
|
||||
motor_max = np.max(data["x_data"])
|
||||
motor_centre = (motor_max + motor_min) / 2
|
||||
|
||||
if not motor_min <= position <= motor_max:
|
||||
scans.umv(motor_device, motor_centre, relative=False)
|
||||
msg = (
|
||||
f"Position {position: .2f} is outside the scan range of "
|
||||
f"{motor_min: .2f} to {motor_max: .2f}. "
|
||||
f"Returning to centre of scan range {motor_centre: .3f}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
motor_position = round(position, 4)
|
||||
scans.umv(motor_device, motor_position, relative=False)
|
||||
print(f"\n Moving {motor_name} to position {motor_position: .3f}")
|
||||
|
||||
|
||||
# @dataclass(frozen=True)
|
||||
# class FitDefaults:
|
||||
# """Default values for fitting routines"""
|
||||
|
||||
# # Constants for default models, baselines, and parameters
|
||||
# MODEL = "Voigt"
|
||||
# BASELINE = "Linear"
|
||||
# SETTLE_TIME = 0.1
|
||||
# RELATIVE_MODE = True
|
||||
|
||||
|
||||
def go_to_peak(
|
||||
motor_device,
|
||||
signal_device,
|
||||
start: float,
|
||||
stop: float,
|
||||
steps: int,
|
||||
relative: bool = FitDefaults.RELATIVE_MODE,
|
||||
plot: bool = True,
|
||||
settle: float = FitDefaults.SETTLE_TIME,
|
||||
confirm: bool = True,
|
||||
gomax: bool = False,
|
||||
):
|
||||
"""
|
||||
Go to the peak of a signal by scanning a motor within a specified range and
|
||||
identifying the optimal position based on signal peak data.
|
||||
|
||||
Parameters:
|
||||
motor_device: The motor device to be scanned.
|
||||
signal_device: The signal device to monitor during the scan.
|
||||
start (float): The starting position of the scan. Ignored if `relative` is True.
|
||||
stop (float): The ending position of the scan. Ignored if `relative` is True.
|
||||
steps (int): The number of steps to divide the scan range into.
|
||||
relative (bool, optional): If True, interpret `start` and `stop` as relative to
|
||||
the current motor position. Defaults to RELATIVE_MODE constant.
|
||||
plot (bool, optional): If True, plot the scan data and the fitted results.
|
||||
Defaults to True.
|
||||
settle (float, optional): The time in seconds to wait after each step for the
|
||||
signal to stabilize. Defaults to DEFAULT_SETTLE_TIME constant.
|
||||
confirm (bool, optional): If True, ask for user confirmation before starting
|
||||
the scan. Defaults to True.
|
||||
|
||||
Raises:
|
||||
Exception: Raises exceptions potentially raised by dependent functions or
|
||||
operations such as plotting, fitting, or motor movement.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
motor_name = motor_device.name
|
||||
signal_name = signal_device.name
|
||||
# wf.plot(x_name=motor_name, y_name=signal_name)
|
||||
if plot:
|
||||
plot_live_data_bec(motor_name, signal_name)
|
||||
|
||||
# Validate and calculate step size
|
||||
step_size = calculate_step_size(start, stop, steps)
|
||||
|
||||
# Confirm the scan range
|
||||
# current_motor_position = motor_device.user_readback.get()
|
||||
current_motor_position = motor_device.read()[motor_name]["value"]
|
||||
if confirm:
|
||||
if relative:
|
||||
scan_start = current_motor_position + start
|
||||
scan_end = current_motor_position + stop
|
||||
print(
|
||||
f"\nScanning from {scan_start: .6g} to {scan_end: .6g} in "
|
||||
f"{steps} steps of size {step_size}"
|
||||
)
|
||||
print(f"Relative mode = {relative}")
|
||||
else:
|
||||
print(
|
||||
f"\nScanning from {start: .5g} to {stop: .5g} in {steps} steps of size {step_size}"
|
||||
)
|
||||
print(f"Relative mode = {relative}")
|
||||
input("Press Enter to continue...")
|
||||
# Perform the scan
|
||||
scan_result = scans.line_scan(
|
||||
motor_device, start, stop, steps=steps, relative=relative, settling_time=settle
|
||||
)
|
||||
motor_data = scan_result.scan.live_data[motor_name][motor_name].val
|
||||
signal_data = scan_result.scan.live_data[signal_name][signal_name].val
|
||||
scan_number = "Current"
|
||||
|
||||
data = {
|
||||
"x_data": np.array(motor_data),
|
||||
"y_data": np.array(signal_data),
|
||||
"motor_name": motor_name,
|
||||
"signal_name": signal_name,
|
||||
"motor_device": motor_device,
|
||||
"scan_number": scan_number,
|
||||
}
|
||||
|
||||
# Define and fit model to scan data
|
||||
fit_params = create_fit_parameters(False, FitDefaults.MODEL, FitDefaults.BASELINE)
|
||||
fit_result = fit(data, fit_params)
|
||||
|
||||
# Plot the fitted data if plot = True
|
||||
if plot:
|
||||
plot_fitted_data_bec(data, fit_result)
|
||||
|
||||
# If gomax is set then move to the maximum value, rather than the fit centre
|
||||
if gomax:
|
||||
value = fit_result["x_max"]
|
||||
print(f"Max position is at {value}")
|
||||
move_to_position(data["motor_device"], data["motor_name"], fit_result["x_max"], data)
|
||||
else:
|
||||
# Safely move the motor to the peak position
|
||||
move_to_position(data["motor_device"], data["motor_name"], fit_result["centre"], data)
|
||||
|
||||
|
||||
def fit_history(
|
||||
history_index: int,
|
||||
signal_name: str,
|
||||
deriv: bool = False,
|
||||
model: str = FitDefaults.MODEL,
|
||||
move_to_peak: bool = False,
|
||||
):
|
||||
"""
|
||||
Retrieve and analyze historical data by fitting a model, optionally moving to
|
||||
a peak position.
|
||||
|
||||
Parameters:
|
||||
history_index (int): Index of the historical data set to retrieve.
|
||||
signal_name (str): Name of the signal to fit.
|
||||
deriv (bool, optional): Whether to include the derivative in the fitting
|
||||
procedure. Defaults to False.
|
||||
model (str, optional): Name of the model to use for fitting. Defaults to
|
||||
DEFAULT_MODEL.
|
||||
move_to_peak (bool, optional): Whether to move the motor to the peak position
|
||||
after fitting. Defaults to False.
|
||||
|
||||
Raises:
|
||||
KeyError: If required keys are not found in the retrieved data dictionary.
|
||||
ValueError: If the fitting process fails or produces invalid results.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Retrieve historical data
|
||||
data = get_data_from_history(history_index, signal_name)
|
||||
|
||||
# Define fitting parameters
|
||||
fit_params = create_fit_parameters(deriv, model, FitDefaults.BASELINE)
|
||||
|
||||
# Perform fit and plot the data
|
||||
fit_result = fit(data, fit_params)
|
||||
plot_fitted_data_bec(data, fit_result)
|
||||
|
||||
# Optionally move the motor to the peak position
|
||||
if move_to_peak:
|
||||
move_to_position(data["motor_device"], data["motor_name"], fit_result["centre"], data)
|
||||
|
||||
|
||||
def scan_bpm(bpmname):
|
||||
"""
|
||||
Runs a grid scan of a BPM in x and y, and plots each channel
|
||||
as a heatmap.
|
||||
|
||||
Parameters:
|
||||
bpmname: the name of the bpm to be scanned e.g. "fe"
|
||||
|
||||
"""
|
||||
|
||||
# Open a dock area and set up the heatmaps
|
||||
dock_area = bec.gui.new("XBPM_Scan")
|
||||
wf5 = dock_area.new("Sum").new(bec.gui.available_widgets.Heatmap)
|
||||
wf1 = dock_area.new("Ch1", relative_to="Sum", position="bottom").new(
|
||||
bec.gui.available_widgets.Heatmap
|
||||
)
|
||||
wf3 = dock_area.new("Ch3", relative_to="Ch1", position="right").new(
|
||||
bec.gui.available_widgets.Heatmap
|
||||
)
|
||||
wf4 = dock_area.new("Ch4", relative_to="Ch3", position="bottom").new(
|
||||
bec.gui.available_widgets.Heatmap
|
||||
)
|
||||
wf2 = dock_area.new("Ch2", relative_to="Ch1", position="bottom").new(
|
||||
bec.gui.available_widgets.Heatmap
|
||||
)
|
||||
wfscan = dock_area.new("ScanControl").new(bec.gui.available_widgets.ScanControl)
|
||||
|
||||
cfg = getattr(BPMScans, bpmname)
|
||||
|
||||
wf1.x_label = cfg["x_name"]
|
||||
wf1.y_label = cfg["y_name"]
|
||||
wf1.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z1_name"], color_map="plasma")
|
||||
|
||||
wf2.x_label = cfg["x_name"]
|
||||
wf2.y_label = cfg["y_name"]
|
||||
wf2.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z2_name"], color_map="plasma")
|
||||
|
||||
wf3.x_label = cfg["x_name"]
|
||||
wf3.y_label = cfg["y_name"]
|
||||
wf3.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z3_name"], color_map="plasma")
|
||||
|
||||
wf4.x_label = cfg["x_name"]
|
||||
wf4.y_label = cfg["y_name"]
|
||||
wf4.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z4_name"], color_map="plasma")
|
||||
|
||||
wf5.x_label = cfg["x_name"]
|
||||
wf5.y_label = cfg["y_name"]
|
||||
wf5.plot(x_name=cfg["x_name"], y_name=cfg["y_name"], z_name=cfg["z5_name"], color_map="plasma")
|
||||
# Run the scan
|
||||
x_mot = cfg["x_device"]
|
||||
y_mot = cfg["y_device"]
|
||||
# scans.grid_scan(x_mot, -0.5, 0.5, 20, y_mot, -0.5, 0.5, 20,
|
||||
# exp_time=0.5, relative=False, snaked=True)
|
||||
|
||||
|
||||
def optimise_kb(mirror):
|
||||
"""
|
||||
Runs a grid scan of a the upstream and downstream benders,
|
||||
and plots a heatmap of the sample camera x or y sigma.
|
||||
|
||||
Parameters:
|
||||
mirror: either "hfm" or :vfm"
|
||||
|
||||
"""
|
||||
|
||||
# Open a dock area and set up the heatmaps
|
||||
dock_area = bec.gui.new(mirror)
|
||||
wf1 = dock_area.new("Heatmap").new(bec.gui.available_widgets.Heatmap)
|
||||
|
||||
wfscan = dock_area.new("ScanControl").new(bec.gui.available_widgets.ScanControl)
|
||||
|
||||
cfg = getattr(MirrorConfig, mirror)
|
||||
|
||||
wf1.x_label = cfg["bu_name"]
|
||||
wf1.y_label = cfg["bd_name"]
|
||||
wf1.plot(x_name=cfg["bu_name"], y_name=cfg["bd_name"], z_name=cfg["z_name"], color_map="plasma")
|
||||
|
||||
# Run the scan
|
||||
x_mot = cfg["x_device"]
|
||||
y_mot = cfg["y_device"]
|
||||
# scans.grid_scan(x_mot, -0.02, 0.02, 11, y_mot, -0.02, 0.02, 11,
|
||||
# exp_time=0.5, relative=True, snaked=True)
|
||||
Binary file not shown.
@@ -0,0 +1,320 @@
|
||||
"""Functions for fitting scan data"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
from lmfit.models import GaussianModel, LorentzianModel, VoigtModel, ConstantModel, LinearModel
|
||||
from scipy.ndimage import gaussian_filter1d
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FitDefaults:
|
||||
"""Default values for fitting routines"""
|
||||
|
||||
# Constants for default models, baselines, and parameters
|
||||
MODEL = "Voigt"
|
||||
BASELINE = "Linear"
|
||||
|
||||
|
||||
def create_fit_parameters(
|
||||
deriv: bool = False,
|
||||
negative: bool = False,
|
||||
model: str = "Voigt",
|
||||
baseline: str = "Linear",
|
||||
smoothing: None = None,
|
||||
):
|
||||
"""Store the fit parameters in a dictionary
|
||||
|
||||
Parameters: optional derivative, negative peak, fit model,
|
||||
baseline model, optional smoothing
|
||||
Returns: dictionary of fit parameters
|
||||
"""
|
||||
# map input model to lmfit model name
|
||||
model_mappings = {
|
||||
"Gaussian": GaussianModel,
|
||||
"Lorentzian": LorentzianModel,
|
||||
"Voigt": VoigtModel,
|
||||
"Constant": ConstantModel,
|
||||
"Linear": LinearModel,
|
||||
}
|
||||
return {
|
||||
"deriv": deriv,
|
||||
"negative": negative,
|
||||
"model": model_mappings[model],
|
||||
"baseline": model_mappings[baseline],
|
||||
"smoothing": smoothing,
|
||||
}
|
||||
|
||||
|
||||
def process_data(data, fit_params):
|
||||
"""
|
||||
Process the signal data for fitting based on derivative or smoothing
|
||||
|
||||
Parameters: data and fitting parameter dictionary
|
||||
Returns: processed data
|
||||
"""
|
||||
smoothing = fit_params["smoothing"]
|
||||
deriv = fit_params["deriv"]
|
||||
negative = fit_params["negative"]
|
||||
|
||||
signal_name = data["signal_name"]
|
||||
y_data = data["y_data"]
|
||||
|
||||
if smoothing and smoothing > 0.01:
|
||||
fitting_data = gaussian_filter1d(y_data, smoothing)
|
||||
signal_name = f"Smoothed {signal_name}"
|
||||
else:
|
||||
fitting_data = y_data.copy()
|
||||
|
||||
if deriv:
|
||||
fitting_data = np.gradient(fitting_data)
|
||||
signal_name = f"Derivative of {signal_name}"
|
||||
|
||||
if negative:
|
||||
fitting_data = -fitting_data
|
||||
signal_name = f"Negative {signal_name}"
|
||||
|
||||
data.update({"y_to_fit": fitting_data, "signal_name": signal_name})
|
||||
return data
|
||||
|
||||
|
||||
def fit(data, fit_params):
|
||||
"""Fit a signal to a model and return the fitting results
|
||||
|
||||
Parameters: data and fitting parameters dictionary
|
||||
Returns: Fit results and quality indicators
|
||||
"""
|
||||
|
||||
# Create the model
|
||||
peak_model = fit_params["model"](prefix="peak_")
|
||||
baseline_model = fit_params["baseline"](prefix="base_")
|
||||
full_model = peak_model + baseline_model
|
||||
|
||||
# Prepare data
|
||||
processed_data = process_data(data, fit_params)
|
||||
params = full_model.make_params()
|
||||
y_min = np.min(processed_data["y_to_fit"])
|
||||
|
||||
# Configure baseline parameters
|
||||
if fit_params["baseline"] == ConstantModel:
|
||||
params["base_c"].set(value=y_min)
|
||||
elif fit_params["baseline"] == LinearModel:
|
||||
params["base_intercept"].set(value=y_min)
|
||||
params["base_slope"].set(value=0)
|
||||
|
||||
# Add peak-specific parameters
|
||||
params.update(peak_model.guess(processed_data["y_to_fit"], x=processed_data["x_data"]))
|
||||
|
||||
# Perform the fitting
|
||||
lmfit_result = full_model.fit(processed_data["y_to_fit"], params, x=processed_data["x_data"])
|
||||
|
||||
# Find the X that gives the max Y
|
||||
max_index = np.argmax(processed_data["y_to_fit"])
|
||||
x_max = processed_data["x_data"][max_index]
|
||||
|
||||
# Generate data for a smoothed fit curve
|
||||
fit_xdata = np.linspace(np.min(data["x_data"]), np.max(data["x_data"]), 500)
|
||||
fit_ydata = lmfit_result.eval(x=fit_xdata, params=lmfit_result.params)
|
||||
|
||||
# Fit diagnostics
|
||||
residuals = lmfit_result.residual
|
||||
noise = np.std(residuals)
|
||||
|
||||
height = lmfit_result.params["peak_height"].value
|
||||
centre = lmfit_result.best_values["peak_center"]
|
||||
fwhm = lmfit_result.params["peak_fwhm"].value
|
||||
|
||||
centre_stderr = lmfit_result.params["peak_center"].stderr
|
||||
height_stderr = lmfit_result.params["peak_height"].stderr
|
||||
|
||||
snr = height / noise if noise > 0 else np.inf
|
||||
|
||||
# Collect results
|
||||
return {
|
||||
"model": fit_params["model"].__name__,
|
||||
"fwhm": fwhm,
|
||||
"centre": centre,
|
||||
"height": height,
|
||||
# Fit quality
|
||||
"chi_sq": lmfit_result.chisqr,
|
||||
"redchi": lmfit_result.redchi,
|
||||
"aic": lmfit_result.aic,
|
||||
"bic": lmfit_result.bic,
|
||||
"noise": noise,
|
||||
"snr": snr,
|
||||
"centre_stderr": centre_stderr,
|
||||
"height_stderr": height_stderr,
|
||||
# Existing information
|
||||
"lmfit_result": lmfit_result,
|
||||
"x_max": x_max,
|
||||
"fit_xdata": fit_xdata,
|
||||
"fit_ydata": fit_ydata,
|
||||
}
|
||||
|
||||
|
||||
def check_peak_fit(x_data, result, min_fwhm_steps=3):
|
||||
"""Check whether a fitted peak is reliable
|
||||
Parameters: X data, results from the fit step
|
||||
Returns: success or problems"""
|
||||
|
||||
x_data = np.asarray(x_data)
|
||||
|
||||
x_min = np.min(x_data)
|
||||
x_max = np.max(x_data)
|
||||
scan_range = x_max - x_min
|
||||
step = np.median(np.diff(np.sort(x_data)))
|
||||
|
||||
centre = result["centre"]
|
||||
fwhm = result["fwhm"]
|
||||
|
||||
step_size = np.mean(np.diff(x_data))
|
||||
fwhm_steps = result["fwhm"] / abs(step_size)
|
||||
|
||||
problems = []
|
||||
|
||||
if not x_min + 0.1 * scan_range < centre < x_max - 0.1 * scan_range:
|
||||
problems.append("peak centre is too close to scan edge")
|
||||
|
||||
if fwhm < min_fwhm_steps * step:
|
||||
problems.append(
|
||||
f"Peak FWHM spans only {fwhm_steps:.2f} scan steps " f"(minimum {min_fwhm_steps})"
|
||||
)
|
||||
|
||||
if fwhm > 0.7 * scan_range:
|
||||
problems.append("peak is too broad for scan range")
|
||||
|
||||
if result["snr"] < 5:
|
||||
problems.append(f"low SNR ({result['snr']:.1f})")
|
||||
|
||||
centre_stderr = result["centre_stderr"]
|
||||
|
||||
if centre_stderr is None:
|
||||
problems.append("centre uncertainty could not be estimated")
|
||||
elif centre_stderr > 0.2 * fwhm:
|
||||
problems.append(f"centre uncertainty is large " f"({centre_stderr:.3g})")
|
||||
|
||||
return {"success": not problems, "problems": problems}
|
||||
|
||||
|
||||
def find_edge_offset(x, y, fraction=0.5, offset=0.0):
|
||||
"""
|
||||
Find a single signal edge and calculate a target position
|
||||
at a known offset from that edge.
|
||||
Parameters: xdata, ydata, fraction of edge, description, offset
|
||||
Returns: edge, centre, threshold
|
||||
"""
|
||||
|
||||
x = np.asarray(x)
|
||||
y = np.asarray(y)
|
||||
|
||||
y_min = np.min(y)
|
||||
y_max = np.max(y)
|
||||
|
||||
threshold = y_min + fraction * (y_max - y_min)
|
||||
|
||||
# Find threshold crossings
|
||||
crossings = np.where(np.diff(np.sign(y - threshold)) != 0)[0]
|
||||
|
||||
if len(crossings) == 0:
|
||||
raise RuntimeError("No edge found in scan")
|
||||
|
||||
# If we expect only one useful edge, use the strongest/first one
|
||||
i = crossings[0]
|
||||
|
||||
# Linear interpolation for more accurate edge position
|
||||
x1, x2 = x[i], x[i + 1]
|
||||
y1, y2 = y[i], y[i + 1]
|
||||
|
||||
edge = x1 + (threshold - y1) * (x2 - x1) / (y2 - y1)
|
||||
|
||||
centre = edge + offset
|
||||
|
||||
return {"edge": edge, "centre": centre, "threshold": threshold}
|
||||
|
||||
|
||||
def find_feature_centre(x, y, feature="peak", fraction=0.5):
|
||||
"""
|
||||
Find the centre of a peak-like or dip-like region from its two edges.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x, y : array-like
|
||||
Scan positions and measured signal.
|
||||
|
||||
mode : {"peak", "dip"}
|
||||
"peak":
|
||||
Feature is the high-signal region, e.g. collimator scan.
|
||||
|
||||
"dip":
|
||||
Feature is the low-signal region, e.g. beamstop scan.
|
||||
|
||||
fraction : float
|
||||
Fraction of the signal range used for the edge threshold.
|
||||
0.5 means the 50% crossing points.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
centre
|
||||
left_edge
|
||||
right_edge
|
||||
width
|
||||
threshold
|
||||
"""
|
||||
|
||||
x = np.asarray(x, dtype=float)
|
||||
y = np.asarray(y, dtype=float)
|
||||
|
||||
y_low = np.min(y)
|
||||
y_high = np.max(y)
|
||||
|
||||
threshold = y_low + fraction * (y_high - y_low)
|
||||
|
||||
if feature == "peak":
|
||||
inside = y >= threshold
|
||||
|
||||
elif feature == "dip":
|
||||
inside = y <= threshold
|
||||
|
||||
else:
|
||||
raise ValueError("mode must be 'peak' or 'dip'")
|
||||
|
||||
# Find transitions into and out of the feature
|
||||
transitions = np.diff(inside.astype(int))
|
||||
|
||||
entering = np.where(transitions == 1)[0]
|
||||
leaving = np.where(transitions == -1)[0]
|
||||
|
||||
if len(entering) == 0 or len(leaving) == 0:
|
||||
raise ValueError("Could not find both feature edges")
|
||||
|
||||
# Use first entrance and last exit
|
||||
left_idx = entering[0]
|
||||
right_idx = leaving[-1]
|
||||
|
||||
left_edge = interpolate_crossing(
|
||||
x[left_idx], y[left_idx], x[left_idx + 1], y[left_idx + 1], threshold
|
||||
)
|
||||
|
||||
right_edge = interpolate_crossing(
|
||||
x[right_idx], y[right_idx], x[right_idx + 1], y[right_idx + 1], threshold
|
||||
)
|
||||
|
||||
centre = 0.5 * (left_edge + right_edge)
|
||||
width = right_edge - left_edge
|
||||
|
||||
return {
|
||||
"centre": centre,
|
||||
"left_edge": left_edge,
|
||||
"right_edge": right_edge,
|
||||
"width": width,
|
||||
"threshold": threshold,
|
||||
}
|
||||
|
||||
|
||||
def interpolate_crossing(x1, y1, x2, y2, threshold):
|
||||
"""Linearly interpolate the x position where y crosses threshold."""
|
||||
|
||||
if y2 == y1:
|
||||
return 0.5 * (x1 + x2)
|
||||
|
||||
return x1 + (threshold - y1) * (x2 - x1) / (y2 - y1)
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Get data from scan number, BEC history or H5 files"""
|
||||
|
||||
import h5py
|
||||
|
||||
# def get_data_from_h5(signal_name: str = "lu_bpmsum"):
|
||||
# """Get data from an h5 file."""
|
||||
# root, name = get_signal_values(signal_name)
|
||||
# with h5py.File("scan_676.h5", "r") as f:
|
||||
# entry = f["entry"]["collection"]
|
||||
# y_data = entry["devices"][root][name]["value"][:]
|
||||
# motor_data = entry["metadata"]["bec"]
|
||||
# motor_name = motor_data["scan_motors"][0].decode()
|
||||
# scan_number = motor_data["scan_number"][()]
|
||||
# x_data = entry["devices"][motor_name][motor_name]["value"][:]
|
||||
# return {
|
||||
# "x_data": x_data,
|
||||
# "y_data": y_data,
|
||||
# "signal_name": signal_name,
|
||||
# "motor_name": motor_name,
|
||||
# "scan_number": str(scan_number),
|
||||
# }
|
||||
|
||||
|
||||
def get_history(scan):
|
||||
"""Get scan container from the bec history
|
||||
Parameters: scan number or relative number
|
||||
Returns: scan container
|
||||
|
||||
"""
|
||||
|
||||
if isinstance(scan, int):
|
||||
if scan < 0:
|
||||
return bec.history[scan]
|
||||
return bec.history.get_by_scan_number(scan)
|
||||
# return bec.history.get_by_dataset_number(scan)
|
||||
return scan
|
||||
|
||||
|
||||
def get_data_from_history(scan: int, signal_dev):
|
||||
"""Get scan data from the BEC history
|
||||
Parameters: scan number or relative number
|
||||
Returns: the X and Y data as arrays
|
||||
signal name, motor name and scan number
|
||||
|
||||
"""
|
||||
|
||||
data = get_history(scan)
|
||||
md = data.metadata["bec"]
|
||||
motor_name = md["scan_report_devices"][0].decode()
|
||||
if "." in motor_name:
|
||||
root = motor_name.split(".")[0]
|
||||
name = motor_name.replace(".", "_")
|
||||
else:
|
||||
root = motor_name
|
||||
name = motor_name
|
||||
|
||||
x_data = data.devices[root][name].read()["value"]
|
||||
y_res = get_value(signal_dev, True)
|
||||
y_data = data.devices[y_res["root"]][y_res["name"]].read()["value"]
|
||||
signal_name = y_res["name"]
|
||||
|
||||
scan_number = md["scan_number"]
|
||||
|
||||
return {
|
||||
"signal_name": signal_name,
|
||||
"x_data": x_data,
|
||||
"y_data": y_data,
|
||||
"motor_name": motor_name,
|
||||
"scan_number": float(scan_number),
|
||||
}
|
||||
|
||||
|
||||
def get_value(dev_name, details=False):
|
||||
"""Get the value of a device
|
||||
Parameters: device, details: bool
|
||||
Returns: value or full details
|
||||
|
||||
"""
|
||||
|
||||
if dev_name == dev.scam:
|
||||
root = "scam"
|
||||
live_root = "scam"
|
||||
name = "scam_contrast"
|
||||
value = dev_name.read(cached=True)[name]["value"]
|
||||
else:
|
||||
try:
|
||||
name = live_root = root = dev_name.name
|
||||
value = dev_name.read(cached=True)[name]["value"]
|
||||
except:
|
||||
print("non-standard device")
|
||||
root = dev_name.dotted_name
|
||||
name = dev_name.full_name
|
||||
live_root = dev_name.parent.name
|
||||
value = dev_name.read(cached=True)[name]["value"]
|
||||
|
||||
if details:
|
||||
return {"value": value, "root": root, "name": name, "live_root": live_root}
|
||||
|
||||
return value
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Plotting routines for MX scans and fits in BEC."""
|
||||
|
||||
|
||||
def select_bec_window(window_name="Fitting"):
|
||||
"""
|
||||
Return the plot and results widgets for a BEC window.
|
||||
|
||||
If the requested window does not already exist, it is created with
|
||||
a Waveform plot and a TextBox for results.
|
||||
"""
|
||||
|
||||
open_docks = bec.gui.windows
|
||||
|
||||
if open_docks.get(window_name) is None:
|
||||
dock_area = bec.gui.new(window_name)
|
||||
|
||||
plot_widget = dock_area.new(widget="Waveform", object_name="Plot")
|
||||
|
||||
text_widget = dock_area.new(widget="TextBox", object_name="Results", where="bottom")
|
||||
|
||||
else:
|
||||
dock_area = open_docks[window_name]
|
||||
plot_widget = dock_area.Plot
|
||||
text_widget = dock_area.Results
|
||||
|
||||
return plot_widget, text_widget
|
||||
|
||||
|
||||
def plot_live_data(
|
||||
motor_name, signal_name, window_name="Fitting", plot_widget=None, title="Live scan"
|
||||
):
|
||||
"""
|
||||
Start live plotting of a signal against a motor in BEC.
|
||||
|
||||
If plot_widget is supplied, the data are plotted in that widget.
|
||||
Otherwise, the standard BEC fitting window is used.
|
||||
"""
|
||||
if isinstance(plot_widget, str):
|
||||
dock_area = bec.gui.windows[window_name]
|
||||
wf = getattr(dock_area, plot_widget)
|
||||
|
||||
elif plot_widget is not None:
|
||||
wf = plot_widget
|
||||
else:
|
||||
wf, text_widget = select_bec_window(window_name)
|
||||
text_widget.set_plain_text("Plotting live data")
|
||||
|
||||
wf.clear_all()
|
||||
wf.title = title
|
||||
wf.x_label = motor_name
|
||||
wf.y_label = signal_name
|
||||
|
||||
wf.plot(device_x=motor_name, device_y=signal_name)
|
||||
|
||||
|
||||
def plot_fitted_data(data, fit_result, window_name="Fitting", plot_widget=None, title=None):
|
||||
"""
|
||||
Plot completed scan data and its fitted curve in BEC.
|
||||
|
||||
If plot_widget is supplied, the fit is plotted in that widget.
|
||||
Otherwise, the standard BEC fitting window is used and the fitting
|
||||
results are displayed in its results text box.
|
||||
"""
|
||||
if isinstance(plot_widget, str):
|
||||
dock_area = bec.gui.windows[window_name]
|
||||
wf = getattr(dock_area, plot_widget)
|
||||
|
||||
elif plot_widget is not None:
|
||||
wf = plot_widget
|
||||
|
||||
else:
|
||||
wf, text_widget = select_bec_window(window_name)
|
||||
|
||||
fit_text = (
|
||||
f"Centre = {fit_result['centre']:.5g}\n"
|
||||
f"FWHM = {fit_result['fwhm']:.5f}\n"
|
||||
f"Height = {fit_result['height']:.4g}\n"
|
||||
f"Model = {fit_result['model']}\n"
|
||||
f"Centre_StdErr = {fit_result['centre_stderr']:.3g}\n"
|
||||
f"SNR = {fit_result['snr']:.3g}"
|
||||
)
|
||||
|
||||
text_widget.set_plain_text(fit_text)
|
||||
|
||||
wf.clear_all()
|
||||
wf.title = f"Scan: {data['scan_number']}"
|
||||
wf.x_label = data["motor_name"]
|
||||
wf.y_label = data["signal_name"]
|
||||
|
||||
wf.plot(x=data["x_data"], y=data["y_to_fit"], label="Data")
|
||||
|
||||
wf.plot(x=fit_result["fit_xdata"], y=fit_result["fit_ydata"], label="Fit")
|
||||
|
||||
# Display the fitted curve as a line without symbols.
|
||||
wf.get_curve("Fit").set(symbol_size=0)
|
||||
|
||||
|
||||
def plot_scan_data(data, window_name="Fitting", plot_widget=None, title=None):
|
||||
"""Plot completed scan data without a fitted curve."""
|
||||
|
||||
if isinstance(plot_widget, str):
|
||||
dock_area = bec.gui.windows[window_name]
|
||||
wf = getattr(dock_area, plot_widget)
|
||||
|
||||
elif plot_widget is not None:
|
||||
wf = plot_widget
|
||||
|
||||
else:
|
||||
wf, text_widget = select_bec_window(window_name)
|
||||
text_widget.set_plain_text("Fit rejected")
|
||||
|
||||
# Clear the live plot/subscription
|
||||
wf.clear_all()
|
||||
|
||||
wf.title = title or f"Scan: {data['scan_number']}"
|
||||
wf.x_label = data["motor_name"]
|
||||
wf.y_label = data["signal_name"]
|
||||
|
||||
wf.plot(x=data["x_data"], y=data["y_data"], label="Data")
|
||||
Executable
+551
@@ -0,0 +1,551 @@
|
||||
"""Custom scan routines:
|
||||
1) a go_to_peak scan, that scans a motor, finds the peak position and moves to peak
|
||||
2) a find feature scan, that finds a non-Gaussian peak or dip or an edge
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScanDefaults:
|
||||
"""Default values for fitting routines"""
|
||||
|
||||
# Preferred scan parameters
|
||||
SETTLE_TIME = 0.1
|
||||
RELATIVE_MODE = True
|
||||
|
||||
|
||||
# Method functions
|
||||
def calculate_step_size(start: float, stop: float, steps: int) -> float:
|
||||
"""
|
||||
Provides the function to calculate the step size for dividing a specified range
|
||||
into a given number of steps.
|
||||
|
||||
Args:
|
||||
start: The starting value of the range.
|
||||
stop: The stopping value of the range.
|
||||
steps: The number of steps to divide the range into. Must be at least 1.
|
||||
|
||||
Raises:
|
||||
ValueError: If the steps value is less than 1.
|
||||
|
||||
Returns:
|
||||
The calculated step size as a float, rounded to three decimal places.
|
||||
"""
|
||||
if steps < 1:
|
||||
raise ValueError("Number of steps must be at least 1.")
|
||||
return round((stop - start) / (steps - 1), 5)
|
||||
|
||||
|
||||
def move_to_scan_position(motor_device, motor_name: str, position: float, data: dict):
|
||||
"""
|
||||
Function to move a specified motor device to a given position.
|
||||
|
||||
The function verifies if the requested position is within the scan range of the
|
||||
motor device provided. If the position is outside the range, the motor is
|
||||
moved to the center of its scan range, an error message is raised, and the
|
||||
operation is halted. If the position is valid, the motor is moved to the
|
||||
specified position.
|
||||
|
||||
Parameters:
|
||||
motor_device: The motor device to be moved.
|
||||
motor_name: str
|
||||
The name of the motor as a string
|
||||
position: float
|
||||
The desired position to move the motor to. Position should be within
|
||||
the scan range of the motor determined by the provided data.
|
||||
data: dict
|
||||
A dictionary containing "x_data", which is used to determine the
|
||||
scan range of the motor.
|
||||
|
||||
Raises:
|
||||
ValueError: Raised if the specified position is outside the valid scan
|
||||
range determined by "x_data" in the data dictionary. The motor will
|
||||
return to the center of its scan range in this case.
|
||||
"""
|
||||
|
||||
motor_min = np.min(data["x_data"])
|
||||
motor_max = np.max(data["x_data"])
|
||||
motor_centre = (motor_max + motor_min) / 2
|
||||
|
||||
if not motor_min <= position <= motor_max:
|
||||
scans.umv(motor_device, motor_centre, relative=False)
|
||||
msg = (
|
||||
f"Position {position: .2f} is outside the scan range of "
|
||||
f"{motor_min: .2f} to {motor_max: .2f}. "
|
||||
f"Returning to centre of scan range {motor_centre: .3f}."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
motor_position = round(position, 4)
|
||||
scans.umv(motor_device, motor_position, relative=False)
|
||||
print(f"\n Moving {motor_name} to position {motor_position: .3f}")
|
||||
|
||||
|
||||
def restore_position(motor_device, position):
|
||||
"""Return a motor to its position before the scan."""
|
||||
scans.umv(motor_device, position, relative=False)
|
||||
|
||||
|
||||
def go_to_peak(
|
||||
motor_device,
|
||||
signal_device,
|
||||
start: float,
|
||||
stop: float,
|
||||
steps: int,
|
||||
relative: bool = ScanDefaults.RELATIVE_MODE,
|
||||
plot: bool = True,
|
||||
settle: float = ScanDefaults.SETTLE_TIME,
|
||||
confirm: bool = True,
|
||||
gomax: bool = False,
|
||||
negative: bool = False,
|
||||
gap: bool = False,
|
||||
min_fwhm_steps: float = 3,
|
||||
window_name: str = "Fitting",
|
||||
plot_widget=None,
|
||||
):
|
||||
"""
|
||||
Scan a motor and move either to the fitted peak centre or the
|
||||
highest measured data point.
|
||||
|
||||
If gomax=False:
|
||||
Fit the scan, validate the fit using check_peak_fit(),
|
||||
and move to the fitted centre.
|
||||
|
||||
If gomax=True:
|
||||
Skip peak-fit validation and move directly to the highest
|
||||
measured signal value. Information about whether the maximum
|
||||
occurred at a scan boundary is returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
motor_device
|
||||
Motor device to scan.
|
||||
signal_device
|
||||
Signal device to monitor.
|
||||
start : float
|
||||
Scan start position.
|
||||
stop : float
|
||||
Scan end position.
|
||||
steps : int
|
||||
Number of scan steps.
|
||||
relative : bool
|
||||
If True, start and stop are relative to current motor position.
|
||||
plot : bool
|
||||
Plot scan data/results.
|
||||
settle : float
|
||||
Settling time at each scan point.
|
||||
confirm : bool
|
||||
Ask for confirmation before starting scan.
|
||||
gomax : bool
|
||||
If True, move to highest measured point rather than fitted centre.
|
||||
negative : bool
|
||||
Fit a negative-going peak.
|
||||
gap : bool
|
||||
Apply minimum-gap protection when using fitted peak position.
|
||||
min_fwhm_steps : float
|
||||
Minimum allowed fitted FWHM in scan steps. Only used when
|
||||
gomax=False.
|
||||
window_name : str
|
||||
BEC GUI window name.
|
||||
plot_widget
|
||||
Optional explicit plot widget.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Result information.
|
||||
|
||||
For a successful fitted peak:
|
||||
success
|
||||
problems
|
||||
centre
|
||||
fwhm
|
||||
height
|
||||
chi_sq
|
||||
centre_stderr
|
||||
snr
|
||||
|
||||
For gomax=True:
|
||||
success
|
||||
problems
|
||||
x_max
|
||||
max_index
|
||||
at_lower_boundary
|
||||
at_upper_boundary
|
||||
|
||||
For a rejected fit:
|
||||
success=False
|
||||
problems=list of fit rejection reasons
|
||||
"""
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Get motor and signal names
|
||||
# ------------------------------------------------------------
|
||||
|
||||
mres = get_value(motor_device, True)
|
||||
motor_name = mres["root"]
|
||||
|
||||
sres = get_value(signal_device, True)
|
||||
signal_name = sres["root"]
|
||||
|
||||
print(f"Signal name is {signal_name}")
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Set up live plotting
|
||||
# ------------------------------------------------------------
|
||||
|
||||
if plot:
|
||||
plot_live_data(
|
||||
motor_name,
|
||||
signal_name,
|
||||
window_name=window_name,
|
||||
plot_widget=plot_widget,
|
||||
title=f"{motor_name} scan",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Validate scan range and get starting position
|
||||
# ------------------------------------------------------------
|
||||
|
||||
step_size = calculate_step_size(start, stop, steps)
|
||||
|
||||
starting_position = mres["value"]
|
||||
|
||||
print(f"Starting motor position = {starting_position}")
|
||||
|
||||
if confirm:
|
||||
if relative:
|
||||
scan_start = starting_position + start
|
||||
scan_end = starting_position + stop
|
||||
|
||||
print(
|
||||
f"\nScanning from {scan_start:.6g} to {scan_end:.6g} "
|
||||
f"in {steps} steps of size {step_size}"
|
||||
)
|
||||
print(f"Relative mode = {relative}")
|
||||
|
||||
else:
|
||||
print(
|
||||
f"\nScanning from {start:.5g} to {stop:.5g} "
|
||||
f"in {steps} steps of size {step_size}"
|
||||
)
|
||||
print(f"Relative mode = {relative}")
|
||||
|
||||
input("Press Enter to continue...")
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Perform scan
|
||||
# ------------------------------------------------------------
|
||||
|
||||
scan_result = scans.line_scan(
|
||||
motor_device, start, stop, steps=steps, relative=relative, settling_time=settle
|
||||
)
|
||||
|
||||
motor_data = scan_result.scan.live_data[mres["live_root"]][mres["name"]].val
|
||||
|
||||
signal_data = scan_result.scan.live_data[sres["root"]][sres["name"]].val
|
||||
|
||||
scan_number = "Current"
|
||||
|
||||
data = {
|
||||
"x_data": np.array(motor_data),
|
||||
"y_data": np.array(signal_data),
|
||||
"motor_name": motor_name,
|
||||
"signal_name": signal_name,
|
||||
"motor_device": motor_device,
|
||||
"scan_number": scan_number,
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# GOMAX MODE
|
||||
# ============================================================
|
||||
#
|
||||
# Do not fit or apply peak-shape validation.
|
||||
# Simply use the highest measured point.
|
||||
#
|
||||
|
||||
if gomax:
|
||||
|
||||
if negative:
|
||||
max_index = int(np.argmin(data["y_data"]))
|
||||
else:
|
||||
max_index = int(np.argmax(data["y_data"]))
|
||||
|
||||
x_max = float(data["x_data"][max_index])
|
||||
|
||||
at_lower_boundary = max_index == 0
|
||||
at_upper_boundary = max_index == len(data["x_data"]) - 1
|
||||
|
||||
print(f"Maximum measured signal at {x_max:.6g}")
|
||||
|
||||
if at_lower_boundary:
|
||||
print("Maximum is at the lower scan boundary")
|
||||
|
||||
if at_upper_boundary:
|
||||
print("Maximum is at the upper scan boundary")
|
||||
|
||||
# Plot raw scan rather than trying to assess a fitted peak
|
||||
if plot:
|
||||
plot_scan_data(
|
||||
data,
|
||||
window_name=window_name,
|
||||
plot_widget=plot_widget,
|
||||
title=f"{motor_name} scan - maximum at {x_max:.6g}",
|
||||
)
|
||||
|
||||
# Move to measured maximum
|
||||
move_to_scan_position(data["motor_device"], data["motor_name"], x_max, data)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"problems": [],
|
||||
"x_max": x_max,
|
||||
"max_index": max_index,
|
||||
"at_lower_boundary": at_lower_boundary,
|
||||
"at_upper_boundary": at_upper_boundary,
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# FITTED PEAK MODE
|
||||
# ============================================================
|
||||
|
||||
fit_params = create_fit_parameters(
|
||||
deriv=False, negative=negative, model=FitDefaults.MODEL, baseline=FitDefaults.BASELINE
|
||||
)
|
||||
|
||||
fit_result = fit(data, fit_params)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Validate fitted peak
|
||||
# ------------------------------------------------------------
|
||||
|
||||
check = check_peak_fit(data["x_data"], fit_result, min_fwhm_steps=min_fwhm_steps)
|
||||
|
||||
if not check["success"]:
|
||||
|
||||
print("Peak fit rejected:")
|
||||
|
||||
for problem in check["problems"]:
|
||||
print(f" - {problem}")
|
||||
|
||||
if plot:
|
||||
plot_scan_data(
|
||||
data,
|
||||
window_name=window_name,
|
||||
plot_widget=plot_widget,
|
||||
title=f"{motor_name} scan - FIT REJECTED",
|
||||
)
|
||||
|
||||
print(f"Returning motor to start position, " f"{starting_position:.5g}")
|
||||
|
||||
restore_position(motor_device, starting_position)
|
||||
|
||||
return {"success": False, "problems": check["problems"]}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Plot accepted fit
|
||||
# ------------------------------------------------------------
|
||||
|
||||
if plot:
|
||||
plot_fitted_data(data, fit_result, window_name=window_name, plot_widget=plot_widget)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Move to fitted peak centre
|
||||
# ------------------------------------------------------------
|
||||
|
||||
centre = fit_result["centre"]
|
||||
|
||||
if gap and centre < 4.5:
|
||||
print(f"Fitted gap position {centre:.5g} mm is below " "minimum gap of 4.5 mm")
|
||||
umv(dev.id_gap, 4.5)
|
||||
|
||||
else:
|
||||
move_to_scan_position(data["motor_device"], data["motor_name"], centre, data)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Return successful fit information
|
||||
# ------------------------------------------------------------
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"problems": [],
|
||||
"centre": centre,
|
||||
"fwhm": float(fit_result["fwhm"]),
|
||||
"height": float(fit_result["height"]),
|
||||
"chi_sq": float(fit_result["chi_sq"]),
|
||||
"centre_stderr": float(fit_result["centre_stderr"]),
|
||||
"snr": float(fit_result["snr"]),
|
||||
}
|
||||
|
||||
|
||||
def fit_history(
|
||||
history_index: int,
|
||||
signal_name: str,
|
||||
deriv: bool = False,
|
||||
negative: bool = False,
|
||||
smoothing: bool = False,
|
||||
model: str = "Voigt",
|
||||
move_to_peak: bool = False,
|
||||
):
|
||||
"""
|
||||
Retrieve and analyze historical data by fitting a model, optionally moving to
|
||||
a peak position.
|
||||
|
||||
Parameters:
|
||||
history_index (int): Index of the historical data set to retrieve.
|
||||
signal_name (str): Name of the signal to fit.
|
||||
deriv (bool, optional): Whether to include the derivative in the fitting
|
||||
procedure. Defaults to False.
|
||||
model (str, optional): Name of the model to use for fitting. Defaults to
|
||||
DEFAULT_MODEL.
|
||||
move_to_peak (bool, optional): Whether to move the motor to the peak position
|
||||
after fitting. Defaults to False.
|
||||
|
||||
Raises:
|
||||
KeyError: If required keys are not found in the retrieved data dictionary.
|
||||
ValueError: If the fitting process fails or produces invalid results.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
# Retrieve historical data
|
||||
data = get_data_from_history(history_index, signal_name)
|
||||
|
||||
# Define fitting parameters
|
||||
fit_params = create_fit_parameters(deriv, negative, model, FitDefaults.BASELINE)
|
||||
|
||||
# Perform fit and plot the data
|
||||
fit_result = fit(data, fit_params)
|
||||
plot_fitted_data(data, fit_result)
|
||||
|
||||
# Optionally move the motor to the peak position
|
||||
if move_to_peak:
|
||||
move_to_position(data["motor_device"], data["motor_name"], fit_result["centre"], data)
|
||||
return fit_result
|
||||
|
||||
|
||||
def find_feature(
|
||||
motor_device,
|
||||
signal_device,
|
||||
start: float,
|
||||
stop: float,
|
||||
steps: int,
|
||||
feature: str = "peak",
|
||||
offset=None,
|
||||
fraction: float = 0.5,
|
||||
relative: bool = ScanDefaults.RELATIVE_MODE,
|
||||
plot: bool = True,
|
||||
settle: float = ScanDefaults.SETTLE_TIME,
|
||||
confirm: bool = True,
|
||||
window_name: str = "Fitting",
|
||||
plot_widget=None,
|
||||
):
|
||||
"""
|
||||
Scan a motor, determine the centre of a high- or low-signal region,
|
||||
and move the motor to that position.
|
||||
"""
|
||||
|
||||
# Get motor and signal names
|
||||
mres = get_value(motor_device, True)
|
||||
motor_name = mres["root"]
|
||||
|
||||
sres = get_value(signal_device, True)
|
||||
signal_name = sres["root"]
|
||||
|
||||
print(f"Signal name is {signal_name}")
|
||||
|
||||
# Calculate step size
|
||||
step_size = calculate_step_size(start, stop, steps)
|
||||
|
||||
current_motor_position = mres["value"]
|
||||
print(f"Starting motor position = {current_motor_position}")
|
||||
|
||||
# Confirm scan range
|
||||
if confirm:
|
||||
if relative:
|
||||
scan_start = current_motor_position + start
|
||||
scan_end = current_motor_position + stop
|
||||
else:
|
||||
scan_start = start
|
||||
scan_end = stop
|
||||
|
||||
print(
|
||||
f"\nScanning from {scan_start:.6g} to {scan_end:.6g} "
|
||||
f"in {steps} steps of size {step_size}"
|
||||
)
|
||||
print(f"Relative mode = {relative}")
|
||||
print(f"Looking for a {feature} feature")
|
||||
|
||||
input("Press Enter to continue...")
|
||||
|
||||
# -------------------------------------------------
|
||||
# Set up live plot BEFORE starting the scan
|
||||
# -------------------------------------------------
|
||||
if plot:
|
||||
plot_live_data(
|
||||
motor_name,
|
||||
signal_name,
|
||||
window_name=window_name,
|
||||
plot_widget=plot_widget,
|
||||
title=f"{motor_name} scans",
|
||||
)
|
||||
# Perform scan
|
||||
scan_result = scans.line_scan(
|
||||
motor_device, start, stop, steps=steps, relative=relative, settling_time=settle
|
||||
)
|
||||
|
||||
# Retrieve data
|
||||
motor_data = scan_result.scan.live_data[mres["live_root"]][mres["name"]].val
|
||||
|
||||
signal_data = scan_result.scan.live_data[sres["root"]][sres["name"]].val
|
||||
|
||||
data = {
|
||||
"x_data": np.array(motor_data),
|
||||
"y_data": np.array(signal_data),
|
||||
"motor_name": motor_name,
|
||||
"signal_name": signal_name,
|
||||
"motor_device": motor_device,
|
||||
"scan_number": "Current",
|
||||
}
|
||||
|
||||
# Find feature centre
|
||||
if feature in ("peak", "dip"):
|
||||
result = find_feature_centre(
|
||||
data["x_data"], data["y_data"], feature=feature, fraction=fraction
|
||||
)
|
||||
elif feature == "edge_offset":
|
||||
result = find_edge_offset(data["x_data"], data["y_data"], offset=offset)
|
||||
else:
|
||||
raise ValueError(f"Unknown feature type {feature}")
|
||||
if feature in ("peak", "dip"):
|
||||
print(f"\nLeft edge = {result['left_edge']:.4f}")
|
||||
print(f"Right edge = {result['right_edge']:.4f}")
|
||||
print(f"Width = {result['width']:.4f}")
|
||||
print(f"Centre = {result['centre']:.4f}")
|
||||
elif feature == "edge_offset":
|
||||
print(f"Edge = {result['edge']:.4f}")
|
||||
print(f"Offset is {offset}")
|
||||
print(f"Centre = {result['centre']:.4f}")
|
||||
|
||||
# Move safely to centre
|
||||
move_to_scan_position(data["motor_device"], data["motor_name"], result["centre"], data)
|
||||
|
||||
if plot and plot_widget is not None:
|
||||
# Replace the live subscription with static completed scan data
|
||||
plot_widget.clear_all()
|
||||
|
||||
plot_widget.plot(x=data["x_data"], y=data["y_data"], label="Scan data")
|
||||
# Mark centre
|
||||
|
||||
plot_widget.plot(
|
||||
x=[result["centre"], result["centre"]],
|
||||
y=[np.min(data["y_data"]), np.max(data["y_data"])],
|
||||
label="Centre",
|
||||
)
|
||||
|
||||
plot_widget.title = f"{motor_name} scan"
|
||||
plot_widget.x_label = motor_name
|
||||
plot_widget.y_label = signal_name
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,324 @@
|
||||
"""General reusable utilities for MX beamline commissioning and operation."""
|
||||
|
||||
import epics
|
||||
import time
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Camera utilities
|
||||
# =============================================================================
|
||||
def predict_scint_transmission(target_flux=None):
|
||||
"""Predict transmission required for a scintillator measurement."""
|
||||
|
||||
if target_flux is None:
|
||||
target_flux = BeamMove.scint_target_flux
|
||||
|
||||
_, full_flux = bpm2flux()
|
||||
|
||||
if not np.isfinite(full_flux) or full_flux <= 0:
|
||||
raise ValueError(
|
||||
f"Cannot predict scintillator transmission: "
|
||||
f"invalid full flux ({full_flux:.3g} ph/s)"
|
||||
)
|
||||
|
||||
transmission = target_flux / full_flux
|
||||
|
||||
# Prevent an unreasonable attenuator setting
|
||||
transmission = np.clip(transmission, 0.001, 1.0)
|
||||
|
||||
return float(transmission)
|
||||
|
||||
|
||||
def prepare_scint_measurement(autoexp=True, zoom=1000):
|
||||
"""Prepare for taking a reading of the beam from the scintillator"""
|
||||
|
||||
# set transmission
|
||||
transmission = predict_scint_transmission()
|
||||
print(
|
||||
f"Predicted scintillator transmission: "
|
||||
f"{transmission:.2%}"
|
||||
)
|
||||
dev.transm.put(transmission)
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
# set camera zoom
|
||||
if get_value(dev.scam_zoom) != zoom:
|
||||
umv(dev.scam_zoom, zoom)
|
||||
|
||||
# move to beam visualisation
|
||||
if not planner.is_state(BeamlineState.BEAM_VISUALISATION):
|
||||
planner.move_to(BeamlineState.BEAM_VISUALISATION)
|
||||
|
||||
# open shutter
|
||||
dev.bcu_shutter.put(1)
|
||||
time.sleep(0.5)
|
||||
|
||||
# set autoexposure and check there is a reading in the image
|
||||
if autoexp:
|
||||
auto_exposure(cam="samcam", max_iter=25)
|
||||
|
||||
return zoom
|
||||
|
||||
|
||||
|
||||
def read_scintXY(image=False, measure_size=True):
|
||||
"""Get the X and Y beam positions from the sample camera at
|
||||
a sepcified zoom. Default zoom is 1000"""
|
||||
|
||||
# Target position on sample camera
|
||||
x_target = BeamMove.scint_target_x
|
||||
y_target = BeamMove.scint_target_y
|
||||
zoom = get_value(dev.scam_zoom)
|
||||
|
||||
results = {}
|
||||
|
||||
|
||||
max_signal = get_value(dev.samcam_max)
|
||||
results["max_signal"] = max_signal
|
||||
thresh = get_value(dev.samcam_thresh)
|
||||
|
||||
if max_signal < thresh:
|
||||
return {
|
||||
"ok": False,
|
||||
"message": (
|
||||
f"Insufficient signal for centroid calculation "
|
||||
f"(max signal = {max_signal:.0f}, threshold = {thresh:.0f})"
|
||||
),
|
||||
"max_signal": max_signal,
|
||||
}
|
||||
|
||||
|
||||
x_px = dev.samcam_x.read()["samcam_x"]["value"]
|
||||
y_px = dev.samcam_y.read()["samcam_y"]["value"]
|
||||
results["X_pixels"] = x_px
|
||||
results["Y_pixels"] = y_px
|
||||
|
||||
results["ok"] = True
|
||||
results["message"] = (
|
||||
f"Beam detected at X={x_px:.1f}, Y ={y_px:.1f} px"
|
||||
)
|
||||
|
||||
# Calculate difference from target position
|
||||
diff_x = x_target - x_px
|
||||
diff_y = y_target - y_px
|
||||
results["X_diff_px"] = diff_x
|
||||
results["Y diff_px"] = diff_y
|
||||
|
||||
# Convert to pixels
|
||||
diff_x_mic = calc_scam_microns(diff_x, zoom)
|
||||
diff_y_mic = calc_scam_microns(diff_y, zoom)
|
||||
results["X_diff_microns"] = diff_x_mic
|
||||
results["Y_diff_microns"] = diff_y_mic
|
||||
|
||||
# Measure beamsize
|
||||
if measure_size:
|
||||
x_fwhm, y_fwhm = measure_beamsize()
|
||||
results["X_FWHM"] = x_fwhm
|
||||
results["Y_FWHM"] = y_fwhm
|
||||
|
||||
print(f"Beam position: X = {results['X_pixels']:.1f} px, "
|
||||
f"Y = {results['Y_pixels']:.1f} px\n"
|
||||
f"Distance from crosshair: X = {results['X_diff_microns']:.1f} microns, "
|
||||
f"Y = {results['Y_diff_microns']:.1f} microns\n")
|
||||
|
||||
if image:
|
||||
imagedata = get_cam_image(Cameras.sample)
|
||||
results["image"] = imagedata
|
||||
|
||||
|
||||
return results
|
||||
|
||||
def finish_scint_measurement():
|
||||
dev.bcu_shutter.put(0)
|
||||
|
||||
|
||||
def get_cam_image(prefix):
|
||||
"""Return the current RGB image from an EPICS areaDetector camera."""
|
||||
|
||||
width = int(epics.caget(f"{prefix}:image1:ArraySize1_RBV"))
|
||||
height = int(epics.caget(f"{prefix}:image1:ArraySize2_RBV"))
|
||||
|
||||
data = epics.caget(f"{prefix}:image1:ArrayData")
|
||||
|
||||
if data is None:
|
||||
raise RuntimeError(f"Could not read camera image from {prefix}")
|
||||
|
||||
expected_size = width * height * 3
|
||||
|
||||
if data.size < expected_size:
|
||||
raise RuntimeError(
|
||||
f"Camera returned {data.size} bytes; "
|
||||
f"expected {expected_size}"
|
||||
)
|
||||
|
||||
return np.asarray(
|
||||
data[:expected_size],
|
||||
dtype=np.uint8,
|
||||
).reshape(height, width, 3)
|
||||
|
||||
def save_cam_image(
|
||||
image,
|
||||
filename,
|
||||
*,
|
||||
crosshair=None,
|
||||
beam_position=None,
|
||||
crop=None,
|
||||
title=None,
|
||||
):
|
||||
"""Save a camera image, optionally with crosshair and beam position."""
|
||||
|
||||
fig, ax = plt.subplots(figsize=(4, 4))
|
||||
|
||||
ax.imshow(image)
|
||||
|
||||
# Target crosshair
|
||||
if crosshair is not None:
|
||||
x, y = crosshair
|
||||
ax.axvline(x, linewidth=1)
|
||||
ax.axhline(y, linewidth=1)
|
||||
|
||||
# Measured beam position
|
||||
if beam_position is not None:
|
||||
x, y = beam_position
|
||||
ax.plot(
|
||||
x,
|
||||
y,
|
||||
marker="+",
|
||||
markersize=12,
|
||||
markeredgewidth=2,
|
||||
)
|
||||
|
||||
# Crop around crosshair
|
||||
if crop is not None:
|
||||
if crosshair is None:
|
||||
raise ValueError(
|
||||
"crosshair must be supplied when using crop"
|
||||
)
|
||||
|
||||
x, y = crosshair
|
||||
|
||||
ax.set_xlim(x - crop, x + crop)
|
||||
|
||||
# Image coordinates have Y increasing downwards
|
||||
ax.set_ylim(y + crop, y - crop)
|
||||
|
||||
if title:
|
||||
ax.set_title(title)
|
||||
|
||||
ax.axis("off")
|
||||
|
||||
fig.tight_layout()
|
||||
fig.savefig(filename, dpi=150, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
|
||||
def create_cam_report(
|
||||
image,
|
||||
pdf_filename,
|
||||
*,
|
||||
crosshair=None,
|
||||
beam_position=None,
|
||||
crop=None,
|
||||
title="Camera snapshot",
|
||||
):
|
||||
"""Create a one-page PDF containing an annotated camera image."""
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6, 6))
|
||||
|
||||
ax.imshow(image)
|
||||
|
||||
if crosshair is not None:
|
||||
x, y = crosshair
|
||||
ax.axvline(x, linewidth=1)
|
||||
ax.axhline(y, linewidth=1)
|
||||
|
||||
if beam_position is not None:
|
||||
x, y = beam_position
|
||||
ax.plot(
|
||||
x,
|
||||
y,
|
||||
marker="+",
|
||||
markersize=12,
|
||||
markeredgewidth=2,
|
||||
)
|
||||
|
||||
if crop is not None:
|
||||
if crosshair is None:
|
||||
raise ValueError(
|
||||
"crosshair must be supplied when using crop"
|
||||
)
|
||||
|
||||
x, y = crosshair
|
||||
ax.set_xlim(x - crop, x + crop)
|
||||
ax.set_ylim(y + crop, y - crop)
|
||||
|
||||
ax.set_title(title)
|
||||
ax.axis("off")
|
||||
|
||||
fig.tight_layout()
|
||||
|
||||
fig.savefig(
|
||||
pdf_filename,
|
||||
format="pdf",
|
||||
bbox_inches="tight",
|
||||
)
|
||||
|
||||
plt.close(fig)
|
||||
|
||||
return pdf_filename
|
||||
|
||||
# =============================================================================
|
||||
# Daily-setup PDF reports
|
||||
# =============================================================================
|
||||
|
||||
def create_empty_daily_report(filename):
|
||||
"""Create the placeholder Daily Setup report."""
|
||||
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 6))
|
||||
|
||||
ax.axis("off")
|
||||
|
||||
ax.text(
|
||||
0.5,
|
||||
0.60,
|
||||
"Daily Setup",
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=20,
|
||||
weight="bold",
|
||||
)
|
||||
|
||||
ax.text(
|
||||
0.5,
|
||||
0.48,
|
||||
"Setup in progress.",
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=12,
|
||||
)
|
||||
|
||||
ax.text(
|
||||
0.5,
|
||||
0.42,
|
||||
"The report will appear here when complete.",
|
||||
ha="center",
|
||||
va="center",
|
||||
fontsize=10,
|
||||
)
|
||||
|
||||
fig.savefig(
|
||||
filename,
|
||||
format="pdf",
|
||||
bbox_inches="tight",
|
||||
)
|
||||
plt.close(fig)
|
||||
|
||||
return filename
|
||||
|
||||
@@ -140,12 +140,17 @@ def set_mirror_stripe(energy_ev):
|
||||
print(f"Selected mirror stripe: {selected_stripe}")
|
||||
|
||||
|
||||
def mono_pitch_scan(plot=True):
|
||||
def mono_pitch_scan(
|
||||
plot=True,
|
||||
window_name='Fitting',
|
||||
plot_widget=None,
|
||||
):
|
||||
|
||||
"""Scan the monochromator pitch and move to the peak."""
|
||||
|
||||
if plot:
|
||||
print("Scanning monochromator pitch and moving to peak, with plotting.")
|
||||
go_to_peak(
|
||||
result = go_to_peak(
|
||||
EnergyDefaults.mono_pitch,
|
||||
EnergyDefaults.signals["sig2"],
|
||||
-EnergyDefaults.pitch_scan["halfwidth"],
|
||||
@@ -155,10 +160,13 @@ def mono_pitch_scan(plot=True):
|
||||
settle=0.01,
|
||||
plot=True,
|
||||
confirm=False,
|
||||
min_fwhm_steps=1.1,
|
||||
window_name=window_name,
|
||||
plot_widget=plot_widget,
|
||||
)
|
||||
else:
|
||||
print("Scanning monochromator pitch and moving to peak, without plotting.")
|
||||
go_to_peak(
|
||||
result = go_to_peak(
|
||||
EnergyDefaults.mono_pitch,
|
||||
EnergyDefaults.signals["sig2"],
|
||||
-EnergyDefaults.pitch_scan["halfwidth"],
|
||||
@@ -169,7 +177,7 @@ def mono_pitch_scan(plot=True):
|
||||
plot=False,
|
||||
confirm=False,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_dccm_motors_positions(energy_ev):
|
||||
@@ -212,7 +220,13 @@ def move_dccm_motors(energy_ev):
|
||||
|
||||
|
||||
|
||||
def bl_energy(energy_ev, plot=True):
|
||||
def bl_energy(
|
||||
energy_ev,
|
||||
mono_scan=True,
|
||||
plot=True,
|
||||
window_name="Fitting",
|
||||
plot_widget=None,
|
||||
):
|
||||
"""
|
||||
Adjusts the beamline's energy to the specified energy in electron volts (eV).
|
||||
The function validates the target energy, checks the current energy, and makes
|
||||
|
||||
@@ -49,22 +49,18 @@ class BPMScans:
|
||||
"y_device": dev.ss_bpm_y,
|
||||
}
|
||||
|
||||
# @dataclass(frozen=True)
|
||||
class PreChecks:
|
||||
mode_pv = "AGEOP-BL:STATUS-X06DA"
|
||||
fe_shutter_pv = "X06DA-FE-BST1-EMLS-0010:OPEN"
|
||||
hutch_shutter_pv = "X06DA-OP-PSH1-EMLS:STATE"
|
||||
|
||||
|
||||
# @dataclass(frozen=True)
|
||||
# class MirrorConfig:
|
||||
# """Define the names of the mirror channels"""
|
||||
class EHChecks:
|
||||
cryo_pv = "X06DA-ES-CS:TEMP_RBV"
|
||||
|
||||
# @dataclass(frozen=True)
|
||||
class DailySetup:
|
||||
energy = 13000
|
||||
|
||||
# hfm = {
|
||||
# "bu_name": dev.hfm_bu.name,
|
||||
# "bd_name": dev.hfm_bd.name,
|
||||
# "z_name": dev.samcam_xsig.name,
|
||||
# "x_device": dev.hfm_bu,
|
||||
# "y_device": dev.hfm_bd,
|
||||
# }
|
||||
# vfm = {
|
||||
# "bu_name": dev.vfm_bu.name,
|
||||
# "bd_name": dev.vfm_bd.name,
|
||||
# "z_name": dev.samcam_ysig.name,
|
||||
# "x_device": dev.vfm_bu,
|
||||
# "y_device": dev.vfm_bd,
|
||||
# }
|
||||
Reference in new issue
Block a user