standard.py 13.1 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
3
4
#
# This file is part of the bliss project
#
Benoit Formet's avatar
Benoit Formet committed
5
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
6
# Distributed under the GNU LGPLv3. See LICENSE for more info.
7
8

"""
9
10
Standard bliss macros (:func:`~bliss.common.standard.wa`, \
:func:`~bliss.common.standard.mv`, etc)
11
"""
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
12
13
from collections import namedtuple
import functools
14
import fnmatch
15
import inspect
16
17
import contextlib
import gevent
18
import sys
19
20

from bliss import global_map, global_log, current_session
21
from bliss.common import scans
22
from bliss.common.scans import *
Matias Guijarro's avatar
tmp    
Matias Guijarro committed
23
from bliss.common.plot import plot
24
from bliss.common.soft_axis import SoftAxis
25
from bliss.common.counter import SoftCounter
26
from bliss.common.cleanup import cleanup, error_cleanup
27
from bliss.common import cleanup as cleanup_mod
28
from bliss.common.logtools import user_print, disable_user_output
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
29
from bliss.common.interlocks import interlock_state
Cyril Guilloud's avatar
wid()    
Cyril Guilloud committed
30
from bliss.controllers.motors import esrf_undulator
31
from bliss.config.channels import clear_cache
32

33
__all__ = (
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
34
    [
35
36
37
38
39
        "iter_axes_state_all",
        "iter_axes_state",
        "iter_axes_position_all",
        "iter_axes_position",
        "iter_counters",
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
40
41
        "mv",
        "mvr",
Jibril Mammeri's avatar
Jibril Mammeri committed
42
43
        "mvd",
        "mvdr",
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
44
45
46
        "move",
        "sync",
        "interlock_state",
47
        "reset_equipment",
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
48
    ]
49
    + scans.__all__
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
50
51
    + ["cleanup", "error_cleanup", "plot"]
    + ["SoftAxis", "SoftCounter"]
52
)
53
54


55
from bliss.common.motor_group import Group
56
from bliss.common.utils import safe_get, ErrorWithTraceback
57

58
_ERR = "!ERR"
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
59
60


Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
61
62
63
64
65
66
67
WhereAll = namedtuple("WhereAll", "axis_name unit user_position dial_position")
WhereMotor = namedtuple(
    "WhereMotor",
    "axis_name unit user_position user_high_limit user_low_limit offset, dial_position dial_high_limit dial_low_limit",
)
StateMotor = namedtuple("StateMotor", "axis_name state")
CountersList = namedtuple("CountersList", "fullname shape prefix name alias")
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
68
69


70
71
72
73
74
75
76
77
def sync(*axes):
    """
    Forces axes synchronization with the hardware

    Args:
        axes: list of axis objects or names. If no axis is given, it syncs all
              all axes present in the session
    """
78
    user_print("Forcing axes synchronization with hardware")
79
    if axes:
80
        axes = global_map.get_axis_objects_iter(*axes)
81
    else:
82
        axes = global_map.get_axes_iter()
83

84
    for axis in axes:
85
86
87
88
89
90
91
92
93
        try:
            axis.sync_hard()
        except Exception as exc:
            try:
                raise RuntimeError(
                    f"Synchronization error for axis '{axis.name}'"
                ) from exc
            except Exception:
                sys.excepthook(*sys.exc_info())
94
95


96
def iter_axes_state(*axes, read_hw=False):
97
    """
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
98
    Returns state information of the given axes
99

Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
100
101
102
103
104
105
106
107
108
109
110
111
112
113
    Args:
        axis (~bliss.common.axis.Axis): motor axis

    Keyword Args:
        read_hw (bool): If True, force communication with hardware, otherwise
                        (default) use cached value.
    """
    for axis in global_map.get_axis_objects_iter(*axes):
        if axis.name not in current_session.env_dict:
            continue
        state = safe_get(
            axis, "state", on_error=ErrorWithTraceback(error_txt=_ERR), read_hw=read_hw
        )
        yield StateMotor(global_map.alias_or_name(axis), state)
114

115

116
def iter_axes_state_all(read_hw=False):
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
117
118
    """
    Returns state information about all axes
119

Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
120
121
122
123
    Keyword Args:
        read_hw (bool): If True, force communication with hardware, otherwise
                        (default) use cached value.
    """
124
    return iter_axes_state(*list(global_map.get_axes_iter()), read_hw=read_hw)
125

126

127
def iter_axes_position_all(**kwargs):
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
128
    """
129
    Iterates all positions (Where All) in both user and dial units
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
130
131
132
133
134
135
136
137
138
139
140
    """
    err = kwargs.get("err", _ERR)
    for (
        axis_name,
        user_position,
        dial_position,
        unit,
    ) in global_map.get_axes_positions_iter(on_error=ErrorWithTraceback(error_txt=err)):
        if axis_name not in current_session.env_dict:
            continue
        yield WhereAll(axis_name, unit, user_position, dial_position)
141

142

143
def iter_axes_position(*axes, **kwargs):
144
    """
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
145
    Return information (position - user and dial, limits) of the given axes
146
147
148

    Args:
        axis (~bliss.common.axis.Axis): motor axis
149

Valentin Valls's avatar
Valentin Valls committed
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
    Example:

    .. code-block::

          DEMO [18]: wm(m2, m1, m3)

                           m2      m1[mm]       m3
          -------  ----------  ----------  -------
          User
           High     -123.00000   128.00000      inf
           Current   -12.00000     7.00000  3.00000
           Low       456.00000  -451.00000     -inf
          Offset       0.00000     3.00000  0.00000
          Dial
           High      123.00000   123.00000      inf
           Current    12.00000     2.00000  3.00000
           Low      -456.00000  -456.00000     -inf
167
    """
168
    if not axes:
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
169
170
        raise RuntimeError("need at least one axis name/object")

171
    err = kwargs.get("err", _ERR)
172
    get = functools.partial(safe_get, on_error=ErrorWithTraceback(error_txt=err))
173

174
    for axis in global_map.get_axis_objects_iter(*axes):
175
        # get limits in USER units.
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
176
        axis_name = global_map.alias_or_name(axis)
177
        axis_state = safe_get(axis, "state", on_error=err)
178
        if axis.disabled or "DISABLED" in axis_state:
179
            axis_name += " *DISABLED*"
180
        user_low_limit, user_high_limit = safe_get(axis, "limits", on_error=(err, err))
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
181
        user_position = get(axis, "position")
182
183
184
185
        offset = safe_get(axis, "offset", on_error=float("nan"))
        dial_low_limit, dial_high_limit = safe_get(
            axis, "dial_limits", on_error=(err, err)
        )
186
        dial_position = get(axis, "dial")
187
        unit = axis.config.get("unit", default=None)
188

Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
189
190
191
192
193
194
195
196
197
198
        where_motor = WhereMotor(
            axis_name,
            unit,
            user_position,
            user_high_limit,
            user_low_limit,
            offset,
            dial_position,
            dial_high_limit,
            dial_low_limit,
199
200
        )

Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
201
        yield where_motor
202

203
204

def mv(*args):
205
206
207
208
209
210
211
212
213
214
    """
    Moves given axes to given absolute positions

    Arguments are interleaved axis and respective absolute target position.
    Example::

        >>> mv(th, 180, chi, 90)

    See Also: move
    """
215
216
217
218
    move(*args)


def mvr(*args):
219
220
221
222
223
224
225
226
    """
    Moves given axes to given relative positions

    Arguments are interleaved axis and respective relative target position.
    Example::

        >>> mv(th, 180, chi, 90)
    """
227
228
229
    __move(*args, relative=True)


Jibril Mammeri's avatar
Jibril Mammeri committed
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def mvd(*args):
    """
    Moves given axes to given absolute dial positions

    Arguments are interleaved axis and respective relative target position.
    """
    # __move_dial(*args)
    __move(*args, dial=True)


def mvdr(*args):
    """
    Moves given axes to given relative dial positions

    Arguments are interleaved axis and respective relative target position.
    """
    __move(*args, relative=True, dial=True)


249
def move(*args, **kwargs):
250
251
252
253
254
255
256
257
258
259
    """
    Moves given axes to given absolute positions

    Arguments are interleaved axis and respective absolute target position.
    Example::

        >>> mv(th, 180, chi, 90)

    See Also: mv
    """
260
261
262
263
    __move(*args, **kwargs)


def __move(*args, **kwargs):
Jibril Mammeri's avatar
Jibril Mammeri committed
264
265
266
267
268
269
    wait, relative, dial = (
        kwargs.get("wait", True),
        kwargs.get("relative", False),
        kwargs.get("dial", False),
    )

270
    motor_pos = dict()
271
    for m, p in zip(global_map.get_axis_objects_iter(*args[::2]), args[1::2]):
Jibril Mammeri's avatar
Jibril Mammeri committed
272
        motor_pos[m] = m.dial2user(p) if dial and not relative else p
273
    group = Group(*motor_pos.keys())
274
    group.move(motor_pos, wait=wait, relative=relative)
275
276
277

    return group, motor_pos

278

279
def iter_counters(counters=None):
280
281
282
283
    """
    Return a dict of counters
    """
    counters_dict = dict()
284
    shape = ["0D", "1D", "2D"]
285
286
    if counters is None:
        counters = global_map.get_counters_iter()
287

288
    for cnt in counters:
289
        prefix, _, short_name = cnt.fullname.rpartition(":")
290
291
        counters_dict[cnt.fullname] = (
            shape[len(cnt.shape)],
292
            cnt._counter_controller.name if cnt._counter_controller else prefix,
293
            short_name,
Benoit Formet's avatar
Benoit Formet committed
294
            getattr(cnt, "original_name", None),
295
        )
Piergiorgio Pancino's avatar
Piergiorgio Pancino committed
296
297
    for fullname, (shape, prefix, name, alias) in counters_dict.items():
        yield CountersList(fullname, shape, prefix, name, alias)
298
299
300
301


def info(obj):
    """
302
303
304
305
    In Bliss `__info__` is used by the command line interface (Bliss shell or
    Bliss repl) to enquire information of the internal state of any object /
    controller in case it is available. this info function is to be seen as
    equivalent of str(obj) or repr(obj) in this context.
306

307
308
    if *obj* has `__info__` implemented this `__info__` function will be
    called. As a fallback option (`__info__` not implemented) repr(obj) is used.
309
    """
310
311

    if not inspect.isclass(obj) and hasattr(obj, "__info__"):
312
313
314
315
316
317
318
319
320
321
322
323
324
325
        # this is not a violation of EAFP, this is to
        # discriminate AttributeError raised *inside* __info__ ;
        # TODO: clean with protocol
        try:
            info_str = obj.__info__()
        except Exception:
            raise
        else:
            if not isinstance(info_str, str):
                raise TypeError("__info__ must return a string")
    else:
        info_str = repr(obj)

    return info_str
326
327


328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def _lsobj(pattern=None):
    """
    Used by bliss.shell.standard.lsobj()
    """
    obj_list = list()

    if pattern is None:
        pattern = "*"

    # Names of objects found in session
    for name in current_session.object_names:
        if fnmatch.fnmatch(name, pattern):

            # check if an object is an aliased object.
            try:
                # "name" is aliased -> add it's alias name.
                name = global_map.alias_or_name(name)
            except AttributeError:
                # "name" is not aliased -> add it.
                pass
            obj_list.append(name)

    # Add aliases (some are not in config-objects)
    for name in global_map.aliases.names_iter():
        if fnmatch.fnmatch(name, pattern):
            obj_list.append(name)

    return obj_list


def _lsmot():
    """
    Return list of motors configured in current session
    Used by bliss.shell.standard.lsmot()
    """
    motor_list = list()
    for name in global_map.get_axes_names_iter():
        motor_list.append(name)

    return motor_list


Cyril Guilloud's avatar
wid()    
Cyril Guilloud committed
370
371
372
373
374
375
376
377
378
379
380
381
382
def wid():
    """ Print the list of undulators defined in the session and their
    positions.
    Print all axes of the ID device server.
    """
    data = iter_axes_position_all()
    for axis_name, axis_unit, position, dial_position in data:
        _ = position

    ID_DS_list = esrf_undulator.get_all()

    undu_str = ""
    for id_ds in ID_DS_list:
383
        undu_str += "\n    ---------------------------------------\n"
Cyril Guilloud's avatar
wid()    
Cyril Guilloud committed
384
385
386
387
388
389
390
391
392
393
394
395
396
        undu_str += f"    ID Device Server {id_ds.ds_name}\n"

        power = f"{id_ds.device.Power:.3f}"
        max_power = f"{id_ds.device.MaxPower:.1f}"
        power_density = f"{id_ds.device.PowerDensity:.3f}"
        max_power_density = f"{id_ds.device.MaxPowerDensity:.1f}"

        undu_str += f"            Power: {power} /  {max_power}  KW\n"
        undu_str += (
            f"    Power density: {power_density} / {max_power_density}  KW/mr2\n\n"
        )

        for undu_axis in id_ds.axis_info:
397
398
            undu_axis.controller.get_axis_info(undu_axis)  # update info

Cyril Guilloud's avatar
wid()    
Cyril Guilloud committed
399
400
401
402
403
404
405
            uinfo = undu_axis.controller.axis_info[undu_axis]

            if uinfo["is_revolver"]:
                undu_type = " - Revolver"
            else:
                undu_type = " "

406
            able = "DISABLED" if "DISABLED" in undu_axis.state else "ENABLED"
Cyril Guilloud's avatar
wid()    
Cyril Guilloud committed
407
            upos = (
408
                "          " if able == "DISABLED" else f"GAP:{undu_axis.position:5.3f}"
Cyril Guilloud's avatar
wid()    
Cyril Guilloud committed
409
410
411
412
413
414
            )
            undu_str += f"    {undu_axis.name} - {upos} - {able} {undu_type} \n"

    return undu_str


415
416
417
418
419
@contextlib.contextmanager
def rockit(motor, total_move):
    """
    Rock an axis from it's current position +/- total_move/2.
    Usage example:
Valentin Valls's avatar
Valentin Valls committed
420
421
422
423
424
425

    .. code-block:: python

        with rockit(mot1, 10):
             ascan(mot2,-1,1,10,0.1,diode)
             amesh(....)
426
427
428
429
430
431
432
433
434
435
436
    """
    if motor.is_moving:
        raise RuntimeError(f"Motor {motor.name} is moving")

    lower_position = motor.position - (total_move / 2)
    upper_position = motor.position + (total_move / 2)
    # Check limits
    motor._get_motion(lower_position)
    motor._get_motion(upper_position)

    def rock():
437
        with disable_user_output():
438
439
440
441
442
443
444
445
446
447
448
            while True:
                motor.move(lower_position)
                motor.move(upper_position)

    with cleanup_mod.cleanup(motor, restore_list=(cleanup_mod.axis.POS,)):
        rock_task = gevent.spawn(rock)
        try:
            yield
        finally:
            rock_task.kill()
            rock_task.get()
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467


def reset_equipment(*devices):
    """
    This command will force all devices passed as argument to be reset
    For now we just force an re-initialization on next call.
    """
    device_to_reset = set()
    for dev in devices:
        device_to_reset.add(dev)
        try:
            ctrl = dev.controller
        except AttributeError:
            pass
        else:
            device_to_reset.add(ctrl)
    # clear controller cache
    clear_cache(*device_to_reset)
    # Maybe in future it'll be good to close the connection and do other things...