Skip to content

Commit

Permalink
Merge pull request #338 from spc-group/auto_shutter
Browse files Browse the repository at this point in the history
Automatically open the shutters when executing plans
  • Loading branch information
canismarko authored Jan 12, 2025
2 parents 2645708 + 564e4dc commit a879a9a
Show file tree
Hide file tree
Showing 46 changed files with 1,095 additions and 383 deletions.
11 changes: 6 additions & 5 deletions docs/topic_guides/index.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
##############
How-To Guides
##############
############
Topic Guides
############

These guides cover a particular topic in depth, coving material useful
to users and beamline staff.
These guides cover **a particular topic in depth**, coving material
useful to users and beamline staff.

These guides do not cover the individual beamlines thoroughly.

Expand All @@ -21,5 +21,6 @@ These guides do not cover the individual beamlines thoroughly.
fly_scanning.rst
hardware_triggering.rst
instrument_registry.rst
shutters_and_filters.rst
motor_positions.rst
xdi_writer.rst
197 changes: 197 additions & 0 deletions docs/topic_guides/shutters_and_filters.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
####################
Shutters and Filters
####################

A shutter is any device that has "shutters" in its `_ophyd_labels_`
attribute.

.. contents:: Table of Contents
:depth: 3

Automatic Shutter Control
=========================

To reduce radiation does, it is useful to include plans that
automatically open and close the shutters as needed. Additionally, the
run engine should be temporarily paused (suspeneded) when shutter
permit is lost.

Auto-Open Shutters
------------------

In the case of **radiation-sensitive samples**, the experiment may
require **keeping the shutter closed** except when collecting
data. Haven includes two preprocessors that can be used to
automatically open shutters that are closed:
:py:func:`~haven.preprocessors.open_shutters_decorator` and
:py:func:`~haven.preprocessors.open_shutters_wrapper`. Both the
end-station PSS shutter, and a secondary fast shutter can both be left
closed and then opened automatically only when needed for recording
data. This feature is enabled by default on plans in
:py:mod:`haven.plans` that trigger detectors (except for
:py:func:`haven.plans.record_dark_current`). :py:mod:`~haven.plans`
also includes wrapped versions of common Bluesky plans, like
:py:func:`haven.plans.scan`.

**Fast shutters** (those with ``"fast_shutters"`` in their
`_ophyd_labels_` attribute) will be opened before each "trigger"
message emitted by a plan, and closed after the subsequent "wait"
message. The "wait" message's *group* will be tracked, ensuring that
the fast shutters will only close after all triggers have been
awaited.

**Slow shutters** (those without ``"fast_shutters"`` in their
`_ophyd_labels_` attribute) will be opened at the start of the wrapped
plan, and closed again at the end.

Shutters that are open at the start of the plan, or haven *allow_open*
or *allow_close* set to ``False`` will be ignored.


Record Ion Chamber Dark Current
-------------------------------

The :py:func:`~haven.plans.record_dark_current` plan accepts a
sequence of shutter devices as an optional argument: *shutters*. Any
devices included will automatically be closed before measuring the
dark current, and opened again if they were initially open.


Suspend When Shutter Permit is Lost
-----------------------------------

.. note::

This is still a work in progress. If you have an interest in this
feature, please join the discussion.


Personnel Safety System (PSS) Shutters
======================================

The PSS shutters are typically large and slow to move. The APS PSS
shutters are controlled via three PVs:

- Open signal
- Close signal
- Beam blocking signal

Activating the open signal directly will instruct the IOC to open the
shutter, but will return a put complete before the shutter has
actually opened. This is not useful when actuating shutters in a
Bluesky plan. As such, the PSS shutters
(:py:class:`haven.devices.shutter.PssShutter`) are **implemented as
positioners** so that :py:meth:`haven.devices.shutter.PssShutter.set`
**completes only when** the beam blocking signal reports that the shutter
is open.

.. code-block:: python
from haven.devices import PssShutter, ShutterState
shutter = PssShutter(prefix="S255ID-PSS:FES:", name="front_end_shutter")]
# Waits for the shutter to actually close:
await shutter.set(ShutterState.CLOSED)
Or add the following to a **TOML file** read by the beamline startup:

.. code:: toml
[[ pss_shutter ]]
name = "front_end_shutter"
prefix = "S255ID-PSS:FES:"
# allow_close = true # Default
# allow_open = true # Default
The optional arguments *allow_open* and *allow_close* control whether
the device should be allowed to open and close the shutter. Typically,
if either *allow_open* or *allow_close* are false, the shutter will be
ignored by tools that automatically actuate the shutters, like
:py:func:`~haven.preprocessors.open_shutters_wrapper` and
:py:func:`~haven.plans.record_dark_current`.


XIA PFCU-4 Filter Bank
=====================

One XIA PFCU controller can control four filters in a single
4-position PF4 filter box. Two filters in one box can be combined to
produce a shutter.

To **create a filter bank**:

.. code-block:: python
from haven.devices import PFCUFilterBank
filter_bank = PFCUFilterBank("255idc:pfcu0:", name="filter_bank")
Or add the following to a **TOML file** read by the beamline startup:

.. code-block:: toml
[[ pfcu4 ]]
name = "filter_bank1"
prefix = "255idc:pfcu1:"
Each :py:class:`~haven.devices.xia_pfcu.PFCUFilterBank` device is a
positioner, and can be set with a string of the bits for all
filters. For example, ``await filter_bank.set("1100")`` will close
(``"1"``) filters 0 and 1, and open (``"0"``) filters 2 and 3. The
ophyd-async device uses this to set both blades on a shutter at once.


XIA PFCU Filter
---------------

The :py:class:`~haven.devices.xia_pfcu.PFCUFilterBank` has an
attribute *filters* which holds
:py:class:`~haven.devices.xia_pfcu.PFCUFilter` instances for the
individual filters in the bank. The **key for each filter** is its
position in the filter box, starting from 0. Some **filters may be
absent** if they are used for shutters, described below.


.. warning::

A **TimeoutError** may occur when attempting to set multiple
filters on the same filter bank concurrently. The IOC will often
not process these requests properly, and one of the filters will
not move. It is recommended to move filters individually, e.g.:

.. code-block:: python
RE(bps.mv(filter_bank.filters[0], FilterState.IN))
RE(bps.mv(filter_bank.filters[1], FilterState.IN))

instead of combining into a single move plan.


XIA PFCU Shutter
----------------

Two filters in one filter bank can be combined to produce a
shutter. Provide the indices (starting from 0) of the filters to use
when creating the filter bank:

.. code-block::
filter_bank = PFCUFilterBank(..., shutters=[[3, 2]])
The first number listed (``3``) is the index of the filter holding the
top of the shutter, that is the filter that should be ``"In"`` to block
X-rays. The second number (``2``) is the index of the bottom
filter. **If the shutter is open when it should be closed**, consider
swapping the order of these numbers.

The resulting :py:class:`~haven.devices.xia_pfcu.PFCUShutter` instance
is available in the *shutters* device vector, with keys based on their
order in the original *shutters* argument. The recommended way to
**actuate the shutter** is by setting it directly rather than moving
the individual filters:

.. code-block:: python
from haven.devices import ShutterState
shutter = filter_bank.shutters[0]
await shutter.set(ShutterState.CLOSED)
38 changes: 12 additions & 26 deletions src/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import os
from pathlib import Path

Expand Down Expand Up @@ -26,7 +27,7 @@
from haven.devices.robot import Robot
from haven.devices.shutter import PssShutter
from haven.devices.slits import ApertureSlits, BladeSlits
from haven.devices.xia_pfcu import PFCUFilter, PFCUFilterBank, PFCUShutter
from haven.devices.xia_pfcu import PFCUFilter, PFCUFilterBank
from haven.devices.xspress import Xspress3Detector
from haven.devices.xspress import add_mcas as add_xspress_mcas

Expand Down Expand Up @@ -199,30 +200,18 @@ def aps(sim_registry):


@pytest.fixture()
def xia_shutter_bank(sim_registry):
class ShutterBank(PFCUFilterBank):
shutters = DCpt(
{
"shutter_0": (
PFCUShutter,
"",
{"top_filter": 4, "bottom_filter": 3, "labels": {"shutters"}},
)
}
)

def __new__(cls, *args, **kwargs):
return object.__new__(cls)

FakeBank = make_fake_device(ShutterBank)
bank = FakeBank(prefix="255id:pfcu4:", name="xia_filter_bank", shutters=[[3, 4]])
async def xia_shutter_bank(sim_registry):
bank = PFCUFilterBank(
prefix="255id:pfcu4:", name="xia_filter_bank", shutters=[[2, 3]]
)
await bank.connect(mock=True)
sim_registry.register(bank)
yield bank


@pytest.fixture()
def xia_shutter(xia_shutter_bank):
shutter = xia_shutter_bank.shutters.shutter_0
shutter = xia_shutter_bank.shutters[0]
yield shutter


Expand All @@ -242,16 +231,13 @@ def shutters(sim_registry):


@pytest.fixture()
def filters(sim_registry):
FakeFilter = make_fake_device(PFCUFilter)
kw = {
"labels": {"filters"},
}
async def filters(sim_registry):
filters = [
FakeFilter(name="Filter A", prefix="filter1", **kw),
FakeFilter(name="Filter B", prefix="filter2", **kw),
PFCUFilter(name="Filter A", prefix="filter1"),
PFCUFilter(name="Filter B", prefix="filter2"),
]
[sim_registry.register(f) for f in filters]
await asyncio.gather(*(filter.connect(mock=True) for filter in filters))
return filters


Expand Down
4 changes: 1 addition & 3 deletions src/firefly/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ def ui_filename(self):

def customize_device(self):
filters = beamline.devices.findall(label="filters", allow_none=True)
self.filters = sorted(
filters, key=lambda dev: (dev.material.get(), dev.thickness.get())
)
self.filters = sorted(filters, key=lambda dev: dev.name)

def customize_ui(self):
# Delete existing filter widgets
Expand Down
19 changes: 10 additions & 9 deletions src/firefly/tests/test_controller.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import MagicMock

import pytest
from bluesky_queueserver_api.zmq.aio import REManagerAPI
from ophyd import Device
from ophyd.sim import make_fake_device
from ophydregistry import Registry
Expand All @@ -16,16 +17,16 @@
async def controller(qapp):
controller = FireflyController()
await controller.setup_instrument(load_instrument=False)
return controller
yield controller


@pytest.fixture()
def ffapp():
return MagicMock()
async def api():
_api = REManagerAPI()
return _api


def test_prepare_queue_client(controller):
api = MagicMock()
def test_prepare_queue_client(controller, api):
controller.prepare_queue_client(api=api)
assert isinstance(controller._queue_client, QueueClient)

Expand Down Expand Up @@ -107,7 +108,7 @@ async def test_load_instrument_registry(controller, qtbot, monkeypatch):
"""Check that the instrument registry gets created."""
assert isinstance(controller.registry, Registry)
# Mock the underlying haven instrument loader
loader = AsyncMock()
loader = MagicMock()
monkeypatch.setattr(firefly.controller.beamline, "load", loader)
monkeypatch.setattr(controller, "prepare_queue_client", MagicMock())
# Reload the devices and see if the registry is changed
Expand All @@ -132,12 +133,12 @@ def test_queue_stopped(controller):
assert not controller.actions.queue_controls["stop_queue"].isChecked()


def test_autostart_changed(controller, qtbot):
async def test_autostart_changed(controller, qtbot, api):
"""Does the action respond to changes in the queue autostart
status?
"""
client = controller.prepare_queue_client(api=MagicMock())
client = controller.prepare_queue_client(api=api)
autostart_action = controller.actions.queue_settings["autostart"]
autostart_action.setChecked(True)
assert autostart_action.isChecked()
Expand Down
5 changes: 2 additions & 3 deletions src/firefly/tests/test_line_scan_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ async def motors(sim_registry, sync_motors):

@pytest.fixture()
async def display(qtbot, sim_registry, sync_motors, motors, dxp, ion_chamber):
print(motors[0].name)
display = LineScanDisplay()
qtbot.addWidget(display)
await display.update_devices(sim_registry)
Expand Down Expand Up @@ -125,7 +124,7 @@ async def test_line_scan_plan_queued(display, monkeypatch, qtbot):

# time is calculated when the selection is changed
display.ui.detectors_list.selected_detectors = mock.MagicMock(
return_value=["vortex_me4", "I0"]
return_value=["vortex_me4", "I00"]
)

# set up meta data
Expand All @@ -135,7 +134,7 @@ async def test_line_scan_plan_queued(display, monkeypatch, qtbot):

expected_item = BPlan(
"rel_scan",
["vortex_me4", "I0"],
["vortex_me4", "I00"],
"async_motor_1",
1.0,
111.0,
Expand Down
2 changes: 1 addition & 1 deletion src/firefly/voltmeters.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class VoltmetersDisplay(display.FireflyDisplay):

def customize_ui(self):
# Connect support for running the auto_gain and dark current plans
self.ui.auto_gain_button.setToolTip(haven.auto_gain.__doc__)
self.ui.auto_gain_button.setToolTip(haven.plans.auto_gain.__doc__)
self.ui.auto_gain_button.clicked.connect(self.run_auto_gain)
self.ui.dark_current_button.clicked.connect(self.record_dark_current)
# Adjust layouts
Expand Down
Loading

0 comments on commit a879a9a

Please sign in to comment.