Experiments control
of EBS beamlines
CBS day out, 24 March 2017 - Château de la Baume, Seyssins (38), France
presented by Matias Guijarro, Beamline Control Unit, Software Group, ESRF
"A new sequencer for the beamlines will be developed to replace SPEC.
[...] The new sequencer will be based on Python. [...]
The new sequencer will rewrite the experiment macros so that
they are easier to maintain."
(excerpt from the ESRF Phase II TDS Orange Book)
SPEC
The good in SPEC
Direct control of devices
- (much) easier to debug / understand
- reconfig = reset
- no additional data transfers
Integrated tool
- plots display
- configuration
The dark side
Poor macro language, no extensibility
Single task operation
Exclusive control
the BLISS project
BeamLine Instrumentation Support Software
- project repository: gitlab.esrf.fr/bliss/bliss
- started in December, 2014
- 2700+ commits
- more than 50.000 lines of Python code
- 4 core developers, 10 participants
made with by BCU
BLISS project overview
Key concepts
- Python library + tools
- Modular architecture
- Synchronous API on top of evented I/O for cooperative multi-tasking
- Direct HW control from 1 BLISS process at a time, support for several peers via state sharing
- Centralized configuration
- Control objects
- Master/Slave Tree-based acquisition chains
- Data management
- Sequences as genuine Python functions
Multi-tasking + synchronous API
Python library with modular architecture
Configuration
Tools / User interfaces
Scans
User sequences
Data management
The path to BLISS
(bliss) sybil:~ % python
Python 2.7.9 (default, Mar 1 2015, 12:57:24)
[GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from bliss.common.axis import Axis
>>> from bliss.controllers.motors import IcePAP
>>> iceid2322 = IcePAP.IcePAP("iceid2322",
{"host": "iceid2322"},
[("mbv4mot", Axis, { "address": 1,
"steps_per_unit": 80,
"velocity": 125,
"acceleration": 500
}
)],
[])
>>> iceid2322.initialize()
>>> iceid2322.axes
{'mbv4mot': <bliss.common.axis.Axis object at 0x7fdb0defb410>}
>>> m = iceid2322.get_axis("mbv4mot")
>>> m.velocity()
125.0
>>> m.acceleration()
500.0
>>> m.position()
252.23750000000001
>>>
Multitasking ?
Evented I/O ?
Synchronous API ?
Experiment Control & Data Acquisition
heavily rely on Input/Output (IO) operations
run
run
run
run
Task execution
I/O system call
- For I/O, tasks must wait (sleep)
- Behind the scene, the underlying system will carry the I/O operation and wake up the task when buffers are ready
Task execution vs. I/O system call
Evented I/O is an operating system feature that allows to not block during I/O calls
Evented I/O
- System will fire READ or WRITE events when buffers are ready, so I/O can be performed in no time
- Multi-tasking can be achieved by switching tasks between read or write events
Task A
run
run
Task B
run
run
gevent Python library offers a "task" abstraction,
and uses evented I/O for system calls
- gevent adds a synchronous API on top of evented I/O
- Python library is patched to be gevent-friendly
- Task context switch becomes transparent
gevent synchronous API
Effective multi-tasking without
too much worries
Centralized Configuration:
introducing Beacon
Beacon, brilliant Beamline Configuration
- Client / server architecture
- Set of YAML files (.yml) containing objects configuration
- Services built on top of
- Web application : user interface for configuration editing
Beacon: example configuration from ID23-2
sybil:~/local/beamline_configuration % tree
.
├── beacon.rdb
├── eh
│ ├── diode.yml
│ ├── __init__.yml
│ └── motors
│ ├── bv.yml
│ ├── DtoX.yml
│ ├── __init__.yml
│ ├── md2.yml
│ ├── mirror1.yml
│ ├── slits.yml
│ └── table.yml
├── oh
│ ├── bpm.yml
│ ├── __init__.yml
│ ├── motors
│ │ ├── bv.yml
│ │ ├── __init__.yml
│ │ ├── mono.yml
│ │ ├── slits.yml
│ │ └── transfocators.yml
│ └── wagos.yml
└── sessions
├── id232_setup.py
├── id232.yml
└── __init__.yml
- controller:
class: IcePAP
host: iceid2322
axes:
- name: mbv4mot
address: 1
steps_per_unit: 817
velocity: 0.3
acceleration: 3
- name: wcid232a
class: wago
controller_ip: wcid232a
counter_gain_names: wbd_g
counter_names: wbd
mapping:
-
type: 750-516
logical_names: wbd_g,wbd_g,wbd_g,_
-
type: 750-630
logical_names: psenc
-
type: 750-630
logical_names: psenc
-
type: 750-630
logical_names: wbvenc
-
type: 750-513
logical_names: psrelay, _
-
type: 750-467
logical_names: wbd, _
-
type: 750-469
logical_names: pst,pst
-
type: 750-469
logical_names: pst,pst
-
type: 750-469
logical_names: pst,pst
Beacon: web application
Beacon: Sessions
- Sessions allow to group control objects under a logical entity
- the same object can be present in several sessions
- by default, a session contains all objects; otherwise one can define 'config-objects' or 'exclude-objects'
- a setup file can be specified, to be executed after session is loaded
class: Session
name: eh3
setup-file: ./eh3.py
config-objects:
- aerox
- aeroy
- musst
- pcoedge
- measurement_eh3
- multiplexer_eh3
class: Session
name: id29
default: True
setup-file: ./id29_setup.py
exclude-objects: pmbpress pmb
example from id29
example from id15
BLISS tools
Command Line Interface
Based on ptpython
sybil:~ % bliss
>>> config.names_list
['ss4hg', 'ss4ho', 'ss2vg', 'bm2v1', 'wbvmot', 'ss2vo', 'trot', 'm2theta', 'thetaenc', 'mbv2mot', 'tf1m4', 'psb', 'm1theta',
'psf', 'psd', 'mbv3mot', 't2ry', 't2rz', 'tyrot', 'bm2v2', 'psu', 'wbv', 't1rz', 't1ry', 'tf1m1', 'tf1m3', 'tf1m2', 'sampx',
'sampy', 'ss2hg', 'mbv1mot', 'ss2ho', 'diode', 't2y', 't2z', 'bm1v1enc', 'psvo', 'm1ty', 'theta', 'm2ty', 'psvg', 'thgt',
'ss4vo', 'tf2m4', 'tf2m2', 'tf2m3', 'tf2m1', 'ts1vg', 'ts1vo', 'ss4u', 'ss4b', 'ss4f', 't1y', 't1z', 'mbv2d', 'txrot', 'ss4vg',
'mbv4mot', 'ts1hg', 'ts1ho', 'ss2b', 'wcid232a', 'ss2d', 'ss2f', 'mbv3d', 'ttrans', 'ss2u', 'pshg', 'bm1v2', 'bm1v1', 'psho',
'ss1b', 'ss1d', 'ss1f', 'ss1u', 'ss1ho', 'ss1hg', 'tz1', 'tz3', 'tz2', 'DtoX', 'tyf', 'tyb', 'mbv1d', 'id23-2', 'ss1vg',
'ss4d', 'ss1vo', 'phiz', 'phiy', 'phix']
>>> config.get("mbv4mot")
<bliss.common.axis.Axis object at 0x7fc279b72fd0>
>>> mbv4mot = config.get("mbv4mot")
>>> mbv4mot.position()
2.0
>>>
[F4] Emacs 15/15 [F3] History [F6] Paste mode [F2] Menu - CPython 2.7.9
Command Line Interface
Session example
- from ID23-2
sybil:~ % bliss --show-sessions
Session name(s):
id23-2
(bliss) sybil:~ % bliss -s id23-2
Initializing 'ss4hg`
Initializing 'ss4ho`
Initializing 'ss2vg`
Initializing 'bm2v1`
Initializing 'wbvmot`
Initializing 'ss2vo`
Initializing 'trot`
Initializing 'm2theta`
Initializing 'thetaenc`
Initializing 'mbv2mot`
Initializing 'tf1m4`
Initializing 'psb`
Initializing 'm1theta`
Initializing 'psf`
Initializing 'psd`
Initializing 'mbv3mot`
Initializing 't2ry`
Initializing 't2rz`
Initializing 'tyrot`
Initializing 'bm2v2`
Initializing 'psu`
Initializing 'wbv`
Initializing 't1rz`
Initializing 't1ry`
Initializing 'tf1m1`
Initializing 'tf1m3`
Initializing 'tf1m2`
Initializing 'sampx`
Initializing 'sampy`
Initializing 'ss2hg`
Initializing 'mbv1mot`
Initializing 'ss2ho`
Initializing 'diode`
Initializing 't2y`
Initializing 't2z`
Initializing 'bm1v1enc`
Initializing 'psvo`
Initializing 'm1ty`
Initializing 'theta`
Initializing 'm2ty`
Initializing 'psvg`
Initializing 'thgt`
Initializing 'ss4vo`
Initializing 'tf2m4`
Initializing 'tf2m2`
Initializing 'tf2m3`
Initializing 'tf2m1`
Initializing 'ts1vg`
Initializing 'ts1vo`
Initializing 'ss4u`
Initializing 'ss4b`
Initializing 'ss4f`
Initializing 't1y`
Initializing 't1z`
Initializing 'mbv2d`
Initializing 'txrot`
Initializing 'ss4vg`
Initializing 'mbv4mot`
Initializing 'ts1hg`
Initializing 'ts1ho`
Initializing 'ss2b`
Initializing 'wcid232a`
Initializing 'ss2d`
Initializing 'ss2f`
Initializing 'mbv3d`
Initializing 'ttrans`
Initializing 'ss2u`
Initializing 'pshg`
Initializing 'bm1v2`
Initializing 'bm1v1`
Initializing 'psho`
Initializing 'ss1b`
Initializing 'ss1d`
Initializing 'ss1f`
Initializing 'ss1u`
Initializing 'ss1ho`
Initializing 'ss1hg`
Initializing 'tz1`
Initializing 'tz3`
Initializing 'tz2`
Initializing 'DtoX`
Initializing 'tyf`
Initializing 'tyb`
Initializing 'mbv1d`
Initializing 'id23-2`
Initializing 'ss1vg`
Initializing 'ss4d`
Initializing 'ss1vo`
Initializing 'phiz`
Initializing 'phiy`
Initializing 'phix`
Done.
>>>
[F4] Emacs 90/90 [F3] History [F6] Paste mode [F2] Menu - CPython 2.7.9
Web Application Interface
BLISS scans
sybil:~ % bliss
>>> from bliss.scanning.chain import AcquisitionChain
>>> from bliss.scanning.acquisition.motor import SoftwarePositionTriggerMaster
>>> from bliss.scanning.acquisition.lima import LimaAcquisitionDevice
>>> from PyTango.gevent import DeviceProxy
>>> m0 = config.get("m0")
>>> lima_dev = DeviceProxy("id30a3/limaccd/simulation")
>>> chain = AcquisitionChain()
>>> chain.add(SoftwarePositionTriggerMaster(m0, start=5, end=10, npoints=10, time=5),
LimaAcquisitionDevice(lima_dev, acq_nb_frames=5, acq_expo_time=0.03, acq_trigger_mode="INTERNAL_TRIGGER_MULTI"))
Continuous scan example
5
10
m0 position
m0 speed
detector frame triggering
1. Building the Acquisition Chain
m0: master
Lima device: slave
>>> SCAN_SAVING.template = '/data/id23eh2/inhouse/{date}/{sample}'
>>> SCAN_SAVING.sample = 'HAK1234'
>>> SCAN_SAVING.get_path()
"/data/id23eh2/inhouse/20170324/HAK1234"
Continuous scan example
2. Setting up where to save data
>>> from bliss.scanning.scan import Scan
>>> my_continuous_scan = Scan(chain)
>>> my_continuous_scan.prepare()
>>> my_continuous_scan.start()
3. Running scan
Scan can take additional arguments
- scan_info : dictionary with scan metadata
- writer : specifies how data has to be written ; defaults to bliss.scanning.writer.hdf5.HDF5Writer - setting to None means data is not saved
- scan_data_callback : in-process fast path to data being acquired, receives data events
3'. Running scan in background
>>> from bliss.scanning.scan import Scan
>>> my_continuous_scan = Scan(chain)
>>> my_continuous_scan.prepare()
>>> background_scan = gevent.spawn(my_continuous_scan.start)
>>> background_scan.ready() # is it finished ?
>>> background_scan.kill() # interrupt
Generic step-by-step scans
ascan, dscan, a2scan, d2scan, timescan
- functions from bliss.common.standard
- API example: ascan(axis, start, stop, npoints, count_time, *counters)
- uses the same underlying scan framework
chain = AcquisitionChain()
timer = SoftwareTimerMaster(count_time, npoints=npoints)
if not counters:
for cnt in MEASUREMENT_GROUP.enabled:
chain.add(timer, CounterAcqDevice(cnt, count_time, npoints))
else:
for cnt in counters:
chain.add(timer, CounterAcqDevice(cnt, count_time, npoints))
chain.add(LinearStepTriggerMaster(motor, start, end, npoints), timer)
scan = Scan(chain)
scan.prepare()
scan.start()
...
Measurement Groups
Mainly for 'ct' (counting for a period of time) or generic scans
- by default, the current active measurement group is used
MeasurementGroup objects
- only one active at a time, MEASUREMENT_GROUP global
- defined in configuration (.yml file)
- ability to enable/disable counters, and to save state for later
- default measurement group contains all counters
Data Management
Data Management
First-class citizen
- built in the scan framework from the ground up
How is it done ?
- flexible template for scan data base path (SCAN_SAVING)
- hierarchical organization of data, mirroring the Acquisition Chain tree
- Online data publishing
Hierarchical organization of data
Mirroring of the Acquisition Chain tree
- each device in the chain has a name, e.g. axis name for motors or 'timer' for Timer objects
- each device can define AcquisitionChannel objects, e.g. counters for a P201 card or image for a Lima camera
- Acquisition channels must have a name, a type and a shape
- scan_info dictionary ({ key: value, ... }) for metadata
Well-defined model for converting scan data into nicely organized directories and files
Online data publishing
Based on
As data acquisition (scan) is running, data is published to the redis database provided by Beacon
- scalar values are stored directly
- bigger data (images, spectra) is just referenced
- data is kept in Redis for 1 day by default
Online data publishing
Any external process can read data from Redis
- using standard Redis tools or libraries for any language
- BLISS provides a data iterator
>>> from bliss.data.node import get_node
>>> session_scans = get_node("my_session_name")
>>> def analyse_data(node):
... for data in node.iterator().walk_data():
... <do something useful with data>
>>> for node in session_scans.iterator().walk():
... analyse_data(node)
A perfect fit with silx-based data processing
User Sequences
Python functions as sequences
from bliss import * # imports generic scans, cleanup functions, etc
from bliss.setup_globals import * # imports objects from session (setup)
import time, numpy # I know you dreamt of it
import gevent
def set_detector_cover(in):
wcidxx.set('detcover', in)
# 5 seconds timeout waiting for detector cover to move
with gevent.Timeout(5):
while wcidxx.get('detcover_in') == in:
time.sleep(0.1)
def my_super_experiment(name):
safety_shutter.open()
old_att = attenuators.get()
def restore_beamline():
set_detcover_open(False)
attenuators.set(old_att)
# cleanup is always called at the end
with cleanup(safety_shutter.close):
# this will only be called in case of error
with error_cleanup(restore_beamline):
attenuators.set(50)
set_detcover_open(True)
SCAN_SAVING.name = name
MEASUREMENT_GROUP.enable('diode')
data_node = dscan(m0, -5, 5, 10, 0.1)
for data in data_node.walk_data():
# do something useful with data...
Conclusion
In the next months...
Scan framework being finalized (only a few weeks left !)
MCAs
New controllers to be added
- Eurotherm, Lakeshore
- future P201 replacement card
- N354 bench clock card
- BCDU8
- ...
KB alignment functions
Beamlines already using BLISS today
MX (ID29, ID30A-1, ID30A-3, ID30B, ID23-1, ID23-2):
80% SPEC free - ID23-1 not migrated yet, will be done soon
ID31, ID15: ongoing migration
ID28 side station: partially done (only for newly installed experiment)
ID16, ID09: only for some motors control and P201, Keithley
Careful deployment, development in parallel: slow progress, to ensure experiments stability
... but not all at once
Thanks for your attention !
Feel free to ask questions
bliss@cbs_day_out
By Matias Guijarro
bliss@cbs_day_out
- 1,100