15 KiB
(user.command_line_interface)=
Command-Line Interface (CLI)
In the previous sections, you have succesfully started BEC and also already interacted with the CLI to update the BEC device configuration. This section aims to explore the CLI capabilities further.
Start-up
The CLI can be started from a terminal after activating the previously installed bec_venv using the shell command within the directory where bec_venv
is installed.
source ./bec_venv/bin/activate
bec
Client interface
The CLI is based on the IPython interactive shell.
As seen in the screenshot below, the prompt is prefixed with, e.g. demo [4/522] >>
.
The prefix contains the name of the current session (demo), the current cell number (4) and the next scan number (522).
Device access
Devices are grouped in dev
.
This allows users to use tab-completion for finding devices.
:align: center
:alt: tab completion for finding devices
:width: 300
``dev`` is imported as a builtin. As a result, you can access ``dev`` from everywhere. ``dev`` itself is just an alias for ``bec.device_manager.devices``.
To get a quick glance at all available devices, you can type
demo [1/3] ❯❯ dev.show_all()
If you want to the current position, setpoint and limits of some device, you can use the wm
command, e.g.
:align: center
:alt: tab completion for finding devices
:width: 400
The wm
command can receive multiple devices as strings, e.g. dev.wm(["samx, "samy"])
or a single device, e.g. dev.wm("samx")
. It also supports regular expressions, e.g. dev.wm("sam*")
. Instead of strings, you can also pass the device objects directly, e.g. dev.wm([dev.samx, dev.samy])
.
Inspect a device
To inspect the device samx, you can simply type dev.samx
and you'll get a printout of the relevant information about this device.
demo [1/3] ❯❯ dev.samx
Out[1]:
Positioner(name=samx, enabled=True):
--------------------
Details:
Description: samx
Status: enabled
Read only: False
Last recorded value: {'samx': {'value': -0.0011717217935431634, 'timestamp': 1702306192.450343}, 'samx_setpoint': {'value': 0, 'timestamp': 1702306192.382011}, 'samx_motor_is_moving': {'value': 0, 'timestamp': 1702306192.450175}}
Device class: SimPositioner
readoutPriority: baseline
Device tags: ['user motors']
User parameter: None
--------------------
Config:
delay: 1
limits: [-50, 50]
speed: 100
tolerance: 0.01
update_frequency: 400
If you just want to see the current position, setpoint and limits of a device, you can simply type
demo [1/3] ❯❯ dev.samx.wm
Read interface
While the device inspection as seen above is an easy way to quickly glance at the current state of a device, it cannot be used programmatically, i.e. within a script.
For reading from a device, we provide two interfaces: .read
and .get
.
Devices are composed of signals, each of their own kind
with possible values hinted/normal/config/omitted
.
It is the kind
attribute that determines what signals are read out by using e.g. dev.samx.read()
.
For more details on device
, signal
and kind
, refer to ophyd.
To read out hinted
and normal
signals from a device, use
demo [1/50] ❯❯ dev.samx.read()
Out[1]:
{'samx': {'value': 0, 'timestamp': 1701942802.6418009},
'samx_setpoint': {'value': 0, 'timestamp': 1701942802.641365},
'samx_motor_is_moving': {'value': 0, 'timestamp': 1701942802.641365}}
and
demo [4/50] ❯❯ dev.samx.read_configuration()
Out[4]:
{'samx_velocity': {'value': 1, 'timestamp': 1701942802.641421},
'samx_acceleration': {'value': 1, 'timestamp': 1701942802.641428}}
to read the config
signals.
In both cases, a nested dictionary is returned with value/timestamp pairs for each signal.
The current position of samx
is accessed dev.samx.read()['samx']['value']
.
The default behaviour for `.read` and `.read_configuration` is to read the last recorded value from redis, i.e. `cached=True`.
However, we can force an update by using `dev.samx.read(cached=False)` which will introduce additional overhead.
Signals of type `omitted` are currently not stored in redis, nor are they read from the device using e.g.`dev.samx.read()` and therefore must
be read out directly using e.g. `dev.samx.my_omitted_signal.read()`.
In addition, we can read for instance the readback or setpoint value from samx by
demo [14/3] ❯❯ dev.samx.readback.read()
Out[14]: {'samx': {'value': -0.0011717217935431634, 'timestamp': 1702306192.450343}}
demo [17/3] ❯❯ dev.samx.setpoint.read()
Out[17]: {'samx_setpoint': {'value': 0, 'timestamp': 1702306192.382011}}
demo [18/3] ❯❯ dev.samx.velocity.read()
Out[18]: {'samx_velocity': {'value': 1, 'timestamp': 1702306158.257976}}
which again returns a nested dictionary, however, this time only for the requested signal.
The keys in the returned dictionary are composed of `<devicename>_<signalname>`.
However, for positioners the signal name <readback> is typically ommited, i,e. see `dev.samx.readback.read()`.
Get interface
We also provide a more convenient access pattern to values of the devices.
Similar to .read()
you may call
demo [20/3] ❯❯ dev.samx.readback.get()
Out[20]: -0.0011717217935431634
which will return the value of the readback directly.
You can also retrieve all signals from samx
via get
.
demo [13/50] ❯❯ signals = dev.samx.get()
demo [14/50] ❯❯ signals
Out[14]: samx(readback=0, setpoint=0, motor_is_moving=0, velocity=1, acceleration=1, high_limit_travel=50, low_limit_travel=-50, unused=1)
which includes all different kind
of signals from the device.
The return object of dev.samx.get()
is a namedtuple with an access pattern similar to class attributes/properties: signals.readback
.
We recommend not using `dev.samx.get()` due to the fact that it forces a readback from all signals.
DeviceConfig
Besides signals, devices are initialized based on their deviceConfig
(see also BEC device config).
The current deviceConfig, e.g. for the device samx
can be retrieved either by simply typing
dev.samx
or directly by retrieving the deviceConfig through
demo [5/50] ❯❯ dev.samx.get_device_config()
Out[5]:
{'delay': 1,
'labels': 'samx',
'limits': [-50, 50],
'name': 'samx',
'speed': 100,
'tolerance': 0.01,
'update_frequency': 400}
To update the deviceConfig, please check set_device_config().
Move a motor
A very common operation in the beginning is to be able to move a device.
For this, there are two variants of device movements: updated move
and move
.
Updated move (umv)
A umv command blocks the command-line until the motor arrives at the target position (or an error occurs).
scans.umv(dev.samx, 5, relative=False)
Move (mv)
A mv command is non-blocking, i.e. it does not wait until the motor reaches the target position.
scans.mv(dev.samx, 5, relative=False)
Be aware of benefits and risks of executing a non-blocking command. A ``CTRL-C`` will not stop its motion, but it needs to be explicitly called via ``dev.samx.stop()`` ``%abort`` or ``%halt``.
However, it can be made a blocking call by
scans.mv(dev.samx, 5, relative=False).wait()
The same mv command can also be executed by calling the device method move
dev.samx.move(5, relative=False)
mv and umv can receive multiple devices, e.g.
```python
scans.umv(dev.samx, 5, dev.samy, 10, relative=False)
```
Update motor limits
In order to move motors in a safe manner, you can add software limits to a motor.
The following command, For example, changes the limits of samx
to -50 (low)
and 50 (high)
dev.samx.limits = [-50, 50]
You may also directly access the low and high limits via dev.samx.low_limit = -50
and dev.samx.high_limit=50
.
Both access patterns are identical.
Software limits are updated in the device_config, however, when done via command-line this only updates the current device_config session in redis.
To make sure that limits are stored after reloading the device BEC config, you need to update the deviceConfig on disk, please check bec.config.save_current_session().
As per default, software limits for motors are set to the values specified in the BEC device config, subfield device_config.
If no software limits are specified, the motor will be initialized without software limits.
This is equivalent to having identical values for high and low limits, e.g.
```python
dev.samx.limits = [0, 0]
```
Run a scan
All currently available scans are accessible through scans.
, e.g.
scans.line_scan(dev.samx, -5, 5, steps=50, exp_time=0.1, relative=False)
You may in addition, scan multiple axis simultaneously, e.g.
scans.line_scan(dev.samx, -5, 5, dev.samy, -5, 5, steps=50, exp_time=0.1, relative=False)
which would be a diagonal trajectory in the xy plane, assuming that samx and samy are in an rectangular coordinate system. There are also multiple ways plot and investigate the data, for this please explore data access and plotting. This also includes live plotting of data.
BEC has various different type of scans, for instance scans.grid_scan
, scans.list_scan
, which you can explore in the simulation.
Explore docstring documentation
What can be very convenient while exploring built-in scans, is using the Ipython syntax ?
to print out all sort of useful information about an object, e.g. for scans.list_scan
demo [3/31] ❯❯ scans.list_scan?
Signature: scans.list_scan(*args, parameter: dict = None, **kwargs)
Docstring:
A scan following the positions specified in a list.
Please note that all lists must be of equal length.
Args:
*args: pairs of motors and position lists
relative: Start from an absolute or relative position
burst: number of acquisition per point
Returns:
ScanReport
Examples:
>>> scans.list_scan(dev.motor1, [0,1,2,3,4], dev.motor2, [4,3,2,1,0], exp_time=0.1, relative=True)
File: ~/work_psi_awi/bec_workspace/bec/bec_lib/bec_lib/scans.py
Type: function
The shell printout provides information about the scan signature, parameters, as well as a syntax example at the bottom.
How to write a script
Scripts are user defined functions that can be executed from the BEC console (CLI).
They are stored in the scripts
folder and can be edited with any text editor.
The scripts are loaded automatically on startup of the BEC console but can also be reloaded by typing bec.load_all_user_scripts()
in the command-line.
This command will load scripts from three locations:
- from
~/bec/scripts/
in your home directory, - from the beamline plugin directory, e.g.
/csaxs_bec/csaxs_bec/scripts/
- from
bec/bec_lib/scripts/
(only useful if you have the entire source code of BEC installed locally).
An example of a user script could be a function to move a specific motor to a predefined position:
def samx_in():
umv(dev.samx, 0)
or
def close_shutter():
print("Closing the shutter")
umv(dev.shutter, 0)
A slightly more complex example could be a sequence of scans that are executed in a specific order:
def overnight_scan():
open_shutter()
samx_in()
for i in range(10):
scans.line_scan(dev.samy, 0, 10, steps=100, exp_time=1, relative=False)
samx_out()
close_shutter()
This script can be executed by typing overnight_scan()
in the BEC console and would execute the following sequence of commands:
- Open the shutter
- Move the sample in
- Perform 10 line scans on the sample
- Move the sample out
- Close the shutter
Create a custom scan
As seen above, scans can be access through scans.
.
However, sometimes it is necessary to run a sequence of functions as if it were a scan.
For example, we might want to run a grid scan (2D scan) with our sample motor stages but move the sample position in z after each 2D scan.
Normally, this would create multiple output files that one would need to merge together later.
This is where the scan definition comes in.
It allows us to run a sequence of functions as if it were a scan, resulting in a single scan_number
, a single scan_id
and a single output file.
@scans.scan_def
def custom_grid_scan():
open_shutter()
umv(dev.samz, 0) # move to samz to start position (absolut)
for i in range(10):
scans.grid_scan(dev.samx, 0, 10, 10, dev.samy, 0, 10, 10, exp_time=0.1, relative=False)
umvr(dev.samz, 0.1) # move samz + 0.1mm after each grid scan
close_shutter()
By adding the decorator @scans.scan_def
to the function definition, we mark this function as a scan definition.
Computed Signal
Here, we introduce the ComputedSignal
, which enables users to effortlessly generate custom signals based on signals from other devices.
To utilize this feature, add a new signal, such as pseudo_signal
, to the device configuration
pseudo_signal:
deviceClass: ophyd_devices.ComputedSignal
deviceConfig:
compute_method: "def compute_signals(signal1, signal2):\n return signal1.get()*signal2.get()\n"
input_signals:
- "bpm4i_readback"
- "bpm5i_readback"
enabled: true
readOnly: false
readoutPriority: baseline
The pseudo_signal
is a ComputedSignal
where the readback is calculated based on the configured input_signals and compute_method. In the provided example, the readback of bpm4i
and bpm5i
is multiplied to produce the readback
of the pseudo_signal
.
Additionally, we offer users a straightforward interface through the client (CLI) to adjust the compute_method and input_signals. The process involves two steps:
- Define the input_signals using
dev.<device_name>.set_input_signal
. - Upload a method for the compute_method via
dev.<device_name>.set_compute_method
.
It's worth noting that users have the option to leverage additional packages such as numpy as np
and scipy as sp
for accelerated computations.
Below is an example demonstrating the use of the pseudo_signal
to compute the sum over a 2D detector (dev.eiger
), excluding hot pixel values:
def calculate_readback(signal):
data = signal.get()
std = np.std(data)
mean = np.mean(data)
return np.sum(data[data>mean+3*std])
dev.pseudo_signal.set_compute_method(calculate_readback)
dev.pseudo_signal.set_input_signals(dev.eiger.image)
This setup enhances flexibility and efficiency in signal processing, empowering users to tailor computations to their specific needs.