copied over new macros from pxii #38

Closed
perl_d wants to merge 1 commits from x06da_test_20260804T100246 into main
pull from: x06da_test_20260804T100246
39 changed files with 8622 additions and 2174 deletions

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
+52 -6
View File
@@ -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
+11
View File
@@ -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
@@ -0,0 +1,5 @@
base_config:
- !include ./pxiii-devices.yaml
#states_config:
# - !include ./pxiii-state-devices.yaml
+8 -3
View File
@@ -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
+30
View File
@@ -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")
+426
View File
@@ -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()
+74
View File
@@ -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
+259
View File
@@ -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()
+97
View File
@@ -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()
+47
View File
@@ -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()
+52
View File
@@ -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:")
+284
View File
@@ -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()
+249
View File
@@ -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()
+393
View File
@@ -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)
+206
View File
@@ -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()
+14
View File
@@ -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
+43
View File
@@ -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()
+178
View File
@@ -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
-22
View File
@@ -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
-252
View File
@@ -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)
-332
View File
@@ -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.
+320
View File
@@ -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)
+99
View File
@@ -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
+119
View File
@@ -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")
+551
View File
@@ -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
+324
View File
@@ -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
+19 -5
View File
@@ -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
+12 -16
View File
@@ -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,
# }