Instrument Configuration¶
This page describes the procedure for defining the beamline configuration. Haven contains definitions for many Ophyd and Ophyd-async devices, however Haven needs a beamline configuration file to know which specific devices are needed for each beamline.
These files should be listed in the environmental variable
HAVEN_CONFIG_FILES as a semi-colon separated list (e.g. export
HAVEN_CONFIG_FILES=$HOME/bluesky/iconfig.toml:/local/bluesky/iconfig_extra.toml).
Then the devices defined in these files can be loaded in python:
from haven import beamline
await beamline.load()
Once the beamline has been loaded, the devices are available using an
Ophyd registry attached to the beamline object. For example,
beamline.registry["austin"] would return an Ophyd device instance
named “austin”, and beamline.registry.findall("ion_chambers")
would return all devices with the “ion_chambers” Ophyd label.
Motivation¶
Haven’s goal is to provide support for all of the spectroscopy beamlines. However, each beamline is different, and these differences are managed by a set of configuration files, similar to the .ini files used in the old LabView applications. To keep the complexity of these configuration files manageable, Haven gets much of the needed information from the IOCs directly.
The job of processing the configuration files is handled by the
Instrument class. This class keeps track
of the configuration file schema, as well as the resulting devices.
Haven/Firefly should always load without a specific configuration file, but will probably not do anything useful.
Device Definitions¶
The beamline instrument loader can either instantiate ophyd devices directly, or using factory functions.
Simple Devices¶
Each device class has an entry in the
:py:object:`~haven.instrument.beamline` loader. To create a new
device, add a table to the configuration file for each device instance
to create. The keys in the table should correspond to arguments passed
to the device’s __init__() method.
Typically, the key for the table is the joined-lower version of the
class name. For example, an instance of the
HighHeatLoadMirror device class
would be added to the configuration file as:
[[ high_heat_load_mirror ]]
name = "ORM1"
prefix = "255ida:ORM1:"
bendable = false
The instrument loader will then create a new device as
HighHeatLoadMirror(name="ORM1", prefix="255ida:", bendable=False).
The resulting device can then be retrieved from the beamline
instrument registry: beamline.registry["ORM1"].
Note
The Ophyd registry allows looking up devices by Ophyd
label. E.g. beamline.registry.findall("ion_chambers") will
retrieve all devices with “ion_chambers” in its labels.
The instrument loader itself does not handle labels. In most
cases, reasonable defaults should be set by the device’s
__init__() methods, however for more control the device table
could also contain the labels key, with the beamline then being
responsible for ensuring these labels are correct.
For example, the following device would be accesible by
registry['I0'] and registry.findall("detectors"), but not
by registry.findall(["ion_chambers"])
[[ ion_chamber ]]
name = "I0"
...
labels = ["detectors"]
Factory Functions¶
Devices can be created using functions instead of
Device classes. The general idea is the same. For
each factory function, the instrument loader will look for tables with
arguments to this function, typically derived from the joined-lower
name for the factory. For example, the function:
def make_area_detector(name: str, prefix: str, ad_version: str = "4.3") -> Device:
...
could have an entry in the configuration file:
[[ area_detector ]]
name = "sim_det"
These factory functions should return either a new Device, or a iterable of new devices.
Development and Testing¶
While adding features and tests to Haven, it is often necessary to
read a configuration file, for example when testing functions that
load devices through
load_instrument(). However,
the configuration that is loaded should not come from a real beamline
configuration or else there is a risk of controlling real hardware
while running tests.
To avoid this problem, pytest modifies the configuration file loading when running tests with pytest:
Ignore any config files besides
iconfig_default.toml.Add
iconfig_testing.tomlto the configuration
Additionally, all load_motors() style functions should accept an
optional config argument, that will determine the configuration
instead of using the above-mentioned priority.
If a feature is added to Haven that would benefit from beamline-specific configuration, it can be added in one of two places.
src/haven/iconfig_default.tomlThis is the best choice if the device or feature is critical to the operation of Haven and/or Firefly, such as the beamline scheduling system. The values listed should still not point at real hardware, but should be sensible defaults or dummy values to allow Haven to function.
src/haven/iconfig_testing.tomlThis is the best choice if the device or hardware is optional, and may or may not be present at any given beamline, for example, fluorescence detectors. This configuration should not point to real hardware.
Checking Configuration¶
If Haven is installed with pip, the command haven_config can be
used to read configuration variables as they will be seen by Haven:
$ haven_config beamline
{'hardware_is_present': False, 'name': 'SPC Beamline (sector unknown)'}
$ haven_config beamline.hardware_is_present
False
Example Configuration¶
Below is an example of a configuration that can be re-used for new device support or beamline setup.
area_detector_root_path = "/tmp"
[ beamline ]
# General name for the beamline, used for metadata.
name = "SPC Beamline (sector unknown)"
[xray_source]
type = "undulator"
prefix = "ID255ds:"
[bss]
prefix = "255idc:bss"
beamline = "255-ID-C"
##############
# Acquisition
##############
# This section describes how to connect to the queueserver and how
# queueserver data reaches the database. It does not generate any
# devices, but is intended to be read by the Firefly GUI application
# to determine how to interact with the queue.
[queueserver]
control_host = "localhost"
control_port = "60615"
info_host = "localhost"
info_port = "60625"
redis_addr = "localhost:6379"
[kafka]
servers = ["fedorov.xray.aps.anl.gov:9092"]
topic = "bluesky.documents.haven-dev"
[tiled]
# uri = "http://localhost:8000/api"
default_catalog = "testing"
cache_filepath = "/tmp/tiled/http_response_cache.db"
# In most cases, *api_key* is not necessary. Only used by the Tiled
# consumer.
# api_key = ""
[database.databroker]
catalog = "bluesky"
#################
# Device support
#################
[[ synchrotron ]]
name = "advanced_photon_source"
# PSS Shutters
# ============
# Each PSS shutter has optional arguments *allow_open* and
# *allow_close*. These determine whether Ophyd will allow the shutter
# to open and close, but has no relationship to EPICS permissions.
[[ pss_shutter ]]
name = "front_end_shutter"
prefix = "S255ID-PSS:FES:"
allow_close = false
# allow_open = true # Default
[[ pss_shutter ]]
name = "hutch_shutter"
prefix = "S255ID-PSS:SCS:"
# allow_open = true # Default
# allow_close = true # Default
# Energy Positioner
# =================
[[ energy ]]
monochromator_prefix = "mono_ioc:"
undulator_prefix = "id_ioc:"
# Ion chambers
# ============
#
# Each ion chamber is listed in a section starting with
# [[ ion_chamber ]]
#
# The *name* parameter can be omitted, in which case the name will be
# updated based on the .DESC PV for the scaler channel.
[[ ion_chamber ]]
scaler_prefix = "255idcVME:3820:"
scaler_channel = 2
preamp_prefix = "255idc:SR03:"
voltmeter_prefix = "255idc:LabJackT7_1:"
voltmeter_channel = 1
# From V2F100: Fmax / Vmax
counts_per_volt_second = 10e6
name = "I0"
# Scalers
# =======
#
# These definitions are not for using ion chambers, but for if the
# scaler is needed as an independent device. The ion chamber
# defintions include a scaler channel.
[[ scaler ]]
name = "scaler_1"
prefix = "255idcVME:3820:"
channels = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]
# Motors
# ======
#
# Add a new section for each IOC (or IOC prefix) that has motors
# matching the format {prefix}:m1. *num_motors* determines how many
# motors will be read. The example below will load three motors with
# PVs: "vme_crate_ioc:m1", "vme_crate_ioc:m2", and "vme_crate_ioc:m3".
#
# By default, the names of the motors are taken from the motor's .DESC
# field. This can be disabled by setting ``auto_name = false``.
[[ motors ]]
prefix = "255idVME:"
num_motors = 3
# auto_name = true
[[ motor ]]
# Individual motors can be created as well.
name = "m1"
prefix = "255idcVME:m1"
# labels=["motors"]
# auto_name = None
# Sample stages
# =============
[[ xy_stage ]]
vertical_prefix = "255idcVME:m13"
horizontal_prefix = "255idcVME:m14"
# Aerotech controller support disabled until new controllers are ready
# [aerotech_stage.aerotech]
# prefix = "255idc"
# delay_prefix = "255idc:DG645"
# pv_vert = ":m1"
# pv_horiz = ":m2"
# External high-voltage power supplies
# ====================================
[[ power_supply ]]
# An NHQ203M power supply
name = "NHQ01"
prefix = "ps_ioc:NHQ01"
ch_num = 1
# Slits
# =====
[[ blade_slits ]]
# A set of 4 slits, two for each direction
name = "KB_slits"
prefix = "vme_crate_ioc:KB"
[[ aperture_slits ]]
# A single rotating aperture slit, like the 25-ID whitebeam slits
name = "whitebeam_slits"
prefix = "255ida:slits:US:"
# KB Mirrors
# ==========
#
# A combined set of vertical and horizontal KB mirrors. Optionally,
# bender motors can also be given.
[[ kb_mirrors ]]
name = "kb_upstream"
prefix = "255idcVME:KB:"
horiz_upstream_motor = "255idcVME:KB:m35"
horiz_downstream_motor = "255idcVME:KB:m36"
vert_upstream_motor = "255idcVME:KB:m48"
vert_downstream_motor = "255idcVME:KB:m49"
# # Optional bender motors
# horiz_upstream_bender: str = "255idcVME:KB:m52",
# horiz_downstream_bender: str = "255idcVME:KB:m53",
# vert_upstream_bender: str = "255idcVME:KB:m61",
# vert_downstream_bender: str = "255idcVME:KB:m62",
# High-heat-load mirrors
# ======================
#
# A single-bounce mirror designed for white-beam. Optionally, also
# bendable with a single motor.
[[ high_heat_load_mirror ]]
name = "ORM1"
prefix = "25ida:ORM1:"
bendable = false
[[ high_heat_load_mirror ]]
name = "ORM2"
prefix = "25ida:ORM2:"
bendable = true
# Table
# =====
# An optical table with a specific configuration of motors
[[ table ]]
name = "downstream_table"
# # Optional, either will use vertical motor, or separate upstream/downstream
# vertical_prefix = "255idcVME:m24"
# horizontal_prefix = "255idcVME:m23"
# upstream_motor_prefix = "255idcVME:m21"
# downstream_motor_prefix = "255idcVME:m22"
# pseudo_motor_prefix = "255idcVME:table_ds:"
# transformprefix = "255idcVME:table_ds_trans:"
[[ table ]]
# An optical table with one vertical motor and one horizontal motor
vertical_prefix = "255idcVME:m26"
horizontal_prefix = "255idcVME:m25"
# Area detectors
# ==============
#
# Area detectors includes gigE vision cameras.
[[ sim_detector ]]
name = "sim_detector"
prefix = "255idSimDet:"
[[ camera ]]
# An Aravis-based area detector
name = "lerix_mono_flag"
prefix = "255idARV3:"
[[ eiger ]]
name = "eiger_500k"
prefix = "255idEiger:"
[[ lambda ]]
name = "lambda_250K"
prefix = "255idLambda250K:"
# What follows is the style for threaded ophyd area detectors. This is
# deprecated and will be removed in the future.
[[ area_detector ]]
name = "sim_det"
prefix = "255idSimDet:"
device_class = "SimDetector"
fake = false
# Heaters and Furnaces
# ====================
[[ capillary_heater ]]
name = "capillary_heater"
prefix = "255idptc10:"
# Robots
# ======
[[ robot ]]
name="austin"
prefix = "255idAustin"
# Managed IOC control PVs
# =======================
[[ beamline_manager ]]
name = "GLaDOS"
prefix = "255idc:glados:"
iocs = {ioc255idb = "ioc255idb:", ioc255idc = "ioc255idc:"}
# Fluorescence Detectors
# ======================
# [[ dxp ]]
# name = "vortex_me4"
# prefix = "vortex_me4:"
# num_elements = 4
# [[ dxp ]]
# name = "canberra_Ge7"
# prefix = "20xmap8:"
# num_elements = 4
[[ xspress3 ]]
name = "vortex_me4"
prefix = "vortex_me4_xsp:"
# Filter boxes
# ============
[[ pfcu4 ]]
name = "filter_bank0"
prefix = "255idc:pfcu0:"
[[ filter_bank0 ]]
class = "pfcu4"
prefix = "255idc:pfcu0:"
[[ pfcu4 ]]
name = "filter_bank1"
prefix = "255idc:pfcu1:"
shutters = [[2, 3]]
# Asymmetric Analyzer
# ===================
[[ analyzer ]]
horizontal_motor_prefix = "255idcVME:m1"
vertical_motor_prefix = "255idcVME:m1"
yaw_motor_prefix = "255idcVME:m1"
rowland_diameter = 0.5 # in m
lattice_constant = 0.543095 # in nm
wedge_angle = 30 # in degrees
surface_plane = "211"
name = "analyzer_crystal"