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:

  1. Ignore any config files besides iconfig_default.toml.

  2. Add iconfig_testing.toml to 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.toml

This 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.toml

This 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.

iconfig_testing.toml
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"