measurement.py 14 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
3
4
5
6
#
# This file is part of the bliss project
#
# Copyright (c) 2016 Beamline Control Unit, ESRF
# Distributed under the GNU LGPLv3. See LICENSE for more info.
7
8
9
10
11

# run tests for this module from the bliss root directory with:
# python -m unittest discover -s tests/acquisition -v

import numpy
12
import inspect
13
import weakref
14
from collections import namedtuple
15

16
from bliss.common.utils import add_conversion_function
17

Vincent Michel's avatar
Vincent Michel committed
18

19
20
# Counter namespaces

21

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def flat_namespace(dct):
    """A namespace allowing names with dots."""
    mapping = dict(dct)

    class getter(object):
        def __init__(self, parent, prefix):
            self.parent = parent
            self.prefix = prefix

        def __getattr__(self, key):
            return getattr(self.parent, self.prefix + key)

    class namespace(tuple):

        __slots__ = ()
        _fields = sorted(mapping)
        __dict__ = property(lambda _: mapping)

        def __getattr__(self, arg):
            if arg in mapping:
                return mapping[arg]
43
            if arg.startswith("__"):
44
45
                raise AttributeError(arg)
            for field in self._fields:
46
47
                if field.startswith(arg + "."):
                    return getter(self, arg + ".")
48
49
50
51
52
53
            raise AttributeError(arg)

        def __setattr__(self, arg, value):
            raise AttributeError("can't set attribute")

        def __repr__(self):
54
55
            reprs = ("{}={!r}".format(field, mapping[field]) for field in self._fields)
            return "{}({})".format("namespace", ", ".join(reprs))
56
57
58
59
60

    return namespace(mapping[field] for field in namespace._fields)


def namespace(dct):
61
    if any("." in key for key in dct):
62
        return flat_namespace(dct)
63
    return namedtuple("namespace", sorted(dct))(**dct)
64
65
66
67
68
69
70
71


def counter_namespace(counters):
    return namespace({counter.name: counter for counter in counters})


# Base counter class

72

73
74
class GroupedReadMixin(object):
    def __init__(self, controller):
Vincent Michel's avatar
Vincent Michel committed
75
        self._controller_ref = weakref.ref(controller)
76

77
78
79
80
    @property
    def name(self):
        return self.controller.name

81
82
    @property
    def controller(self):
Vincent Michel's avatar
Vincent Michel committed
83
        return self._controller_ref()
84

85
86
87
    @property
    def id(self):
        return id(self.controller)
88

89
90
    def prepare(self, *counters):
        pass
91

92
93
    def start(self, *counters):
        pass
94

95
    def stop(self, *counters):
96
        pass
97

98

Vincent Michel's avatar
Vincent Michel committed
99
100
101
class BaseCounter(object):
    """Define a standard counter interface."""

102
103
    # Properties

Vincent Michel's avatar
Vincent Michel committed
104
105
106
107
108
    @property
    def controller(self):
        """A controller or None."""
        return None

109
110
111
112
113
    @property
    def master_controller(self):
        """A master controller or None."""
        return None

Vincent Michel's avatar
Vincent Michel committed
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
    @property
    def name(self):
        """A unique name within the controller scope."""
        raise NotImplementedError

    @property
    def dtype(self):
        """The data type as used by numpy."""
        raise NotImplementedError

    @property
    def shape(self):
        """The data shape as used by numpy."""
        raise NotImplementedError

129
130
    # Methods

131
    def create_acquisition_device(self, scan_pars, **settings):
132
133
134
135
        """Instanciate the corresponding acquisition device."""
        raise NotImplementedError

    # Extra logic
Vincent Michel's avatar
Vincent Michel committed
136
137
138
139
140
141

    @property
    def fullname(self):
        """A unique name within the session scope.

        The standard implementation defines it as:
142
        `<master_controller_name>.<controller_name>.<counter_name>`.
Vincent Michel's avatar
Vincent Michel committed
143
        """
144
145
146
147
148
149
150
151
152
        args = []
        # Master controller
        if self.master_controller is not None:
            args.append(self.master_controller.name)
        # Controller
        if self.controller is not None:
            args.append(self.controller.name)
        # Name
        args.append(self.name)
153
        return ".".join(args)
Vincent Michel's avatar
Vincent Michel committed
154
155
156


class Counter(BaseCounter):
157
    GROUPED_READ_HANDLERS = weakref.WeakKeyDictionary()
158
    ACQUISITION_DEVICE_CLASS = NotImplemented
159

160
161
162
    def __init__(
        self, name, grouped_read_handler=None, conversion_function=None, controller=None
    ):
Vincent Michel's avatar
Vincent Michel committed
163
164
165
        self._name = name
        self._controller = controller
        self._conversion_function = conversion_function
166
167
        if grouped_read_handler:
            Counter.GROUPED_READ_HANDLERS[self] = grouped_read_handler
168

Vincent Michel's avatar
Vincent Michel committed
169
170
171
172
173
    # Standard interface

    @property
    def controller(self):
        return self._controller
174
175
176

    @property
    def name(self):
Vincent Michel's avatar
Vincent Michel committed
177
        return self._name
178
179
180
181
182
183
184
185
186

    @property
    def dtype(self):
        return numpy.float

    @property
    def shape(self):
        return ()

187
188
189
190
191
192
    # Default chain handling

    @classmethod
    def get_acquisition_device_class(cls):
        raise NotImplementedError

193
    def create_acquisition_device(self, scan_pars, **settings):
194
        read_handler = self.GROUPED_READ_HANDLERS.get(self, self)
195
        scan_pars.update(settings)
196
197
        return self.get_acquisition_device_class()(read_handler, **scan_pars)

Vincent Michel's avatar
Vincent Michel committed
198
199
    # Extra interface

200
201
    @property
    def conversion_function(self):
Vincent Michel's avatar
Vincent Michel committed
202
        return self._conversion_function
203

204
205
206
207
208
209
210
211
    def prepare(self):
        pass

    def start(self):
        pass

    def stop(self):
        pass
212
213


214
class SamplingCounter(Counter):
215
216
217
    @classmethod
    def get_acquisition_device_class(cls):
        from bliss.scanning.acquisition.counter import SamplingCounterAcquisitionDevice
218

219
220
        return SamplingCounterAcquisitionDevice

221
222
223
    class GroupedReadHandler(GroupedReadMixin):
        def read(self, *counters):
            """
224
            this method should return a list of read values in the same order
225
226
227
            as counters
            """
            raise NotImplementedError
228

229
230
231
    class ConvertValue(object):
        def __init__(self, grouped_read_handler):
            self.read = grouped_read_handler.read
Vincent Michel's avatar
Vincent Michel committed
232

233
        def __call__(self, *counters):
234
235
236
237
238
239
            return [
                cnt.conversion_function(x) if cnt.conversion_function else x
                for x, cnt in zip(self.read(*counters), counters)
            ]

    def __init__(
240
241
242
243
244
245
        self,
        name,
        controller,
        grouped_read_handler=None,
        conversion_function=None,
        acquisition_device_mode=None,
246
    ):
247
        if grouped_read_handler is None and hasattr(controller, "read_all"):
248
            grouped_read_handler = DefaultSamplingCounterGroupedReadHandler(controller)
249

250
        if grouped_read_handler:
Vincent Michel's avatar
Vincent Michel committed
251
            if not isinstance(grouped_read_handler.read, self.ConvertValue):
252
                grouped_read_handler.read = self.ConvertValue(grouped_read_handler)
253
254
        else:
            if callable(conversion_function):
255
                add_conversion_function(self, "read", conversion_function)
256

257
258
        self.acquisition_device_mode = acquisition_device_mode

259
        super(SamplingCounter, self).__init__(
260
261
            name, grouped_read_handler, conversion_function, controller
        )
262
263
264

    def read(self):
        try:
265
266
267
268
269
270
271
272
            grouped_read_handler = Counter.GROUPED_READ_HANDLERS[self]
        except KeyError:
            raise NotImplementedError
        else:
            grouped_read_handler.prepare(self)
            try:
                return grouped_read_handler.read(self)[0]
            finally:
273
                grouped_read_handler.stop(self)
274

Vincent Michel's avatar
Vincent Michel committed
275

276
277
278
279
280
281
282
283
284
class SoftCounter(SamplingCounter):
    """
    Transforms any given python object into a sampling counter.
    By default it assumes the object has a member called *value* which will be
    used on a read.
    You can overwrite this behaviour by passing the name of the object member
    as value. It can be an object method, a property/descriptor or even a simple
    attribute of the given object.

285
286
287
288
289
290
291
292
    If no name is given, the counter name is the string representation of the
    value argument.
    The counter full name is `controller.name` + '.' + counter_name. If no
    controller is given, the obj.name is used instead of controller.name. If no
    obj is given the counter full name is counter name.

    You can pass an optional apply function if you need to transform original
    value given by the object into something else.
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310

    Here are some examples::

        from bliss.common.measurement import SoftCounter

        class Potentiostat:

            def __init__(self, name):
                self.name = name

            @property
            def potential(self):
                return float(self.comm.write_readline('POT?\n'))

            def get_voltage(self):
                return float(self.comm.write_readline('VOL?\n'))

        pot = Potentiostat('p1')
311
312
313

        # counter from an object property (its name is 'potential'.
        # Its full name is 'p1.potential')
314
315
316
        pot_counter = SoftCounter(pot, 'potential')

        # counter form an object method
317
318
        milivol_counter = SoftCounter(pot, 'get_voltage', name='voltage',
                                      apply=lambda v: v*1000)
319
320
321

        # you can use the counters in any scan
        from bliss.common.standard import loopscan
322
        loopscan(10, 0.1, pot_counter, milivol_counter)
323
324
325
326
327
328
    """

    class Controller(object):
        def __init__(self, name):
            self.name = name

329
330
331
332
333
334
335
336
337
    def __init__(
        self,
        obj=None,
        value="value",
        name=None,
        controller=None,
        apply=None,
        acquisition_device_mode=None,
    ):
338
339
340
341
        if obj is None and inspect.ismethod(value):
            obj = value.__self__
        self.get_value, value_name = self.get_read_func(obj, value)
        name = value_name if name is None else name
342
        obj_has_name = hasattr(obj, "name") and isinstance(obj.name, str)
343
344
345
346
347
348
349
350
351
352
353
        if controller is None:
            if obj_has_name:
                ctrl_name = obj.name
            elif obj is None:
                ctrl_name = name
            else:
                ctrl_name = type(obj).__name__
            controller = self.Controller(ctrl_name)
        if apply is None:
            apply = lambda x: x
        self.apply = apply
354
355
356
        super(SoftCounter, self).__init__(
            name, controller, acquisition_device_mode=acquisition_device_mode
        )
357
358
359
360
361
362
363
364
365
366
367

    @staticmethod
    def get_read_func(obj, value):
        if callable(value):
            value_name = value.__name__
            value_func = value
        else:
            otype = type(obj)
            value_name = value
            val = getattr(otype, value_name, None)
            if val is None or not callable(val):
368

369
370
                def value_func():
                    return getattr(obj, value_name)
371

372
            else:
373

374
375
                def value_func():
                    return val(obj)
376

377
378
379
380
381
382
383
            value_func.__name__ = value_name
        return value_func, value_name

    def read(self):
        return self.apply(self.get_value())


Vincent Michel's avatar
Vincent Michel committed
384
def DefaultSamplingCounterGroupedReadHandler(
385
386
387
    controller, handlers=weakref.WeakValueDictionary()
):
    class DefaultSamplingCounterGroupedReadHandler(SamplingCounter.GroupedReadHandler):
388
389
390
        """
        Default read all handler for controller which have read_all method
        """
Vincent Michel's avatar
Vincent Michel committed
391

392
393
        def read(self, *counters):
            return self.controller.read_all(*counters)
394

Vincent Michel's avatar
Vincent Michel committed
395
    return handlers.setdefault(
396
397
        controller, DefaultSamplingCounterGroupedReadHandler(controller)
    )
Vincent Michel's avatar
Vincent Michel committed
398

399

400
class IntegratingCounter(Counter):
401
402
    @classmethod
    def get_acquisition_device_class(cls):
403
404
405
406
        from bliss.scanning.acquisition.counter import (
            IntegratingCounterAcquisitionDevice
        )

407
408
409
410
411
412
        return IntegratingCounterAcquisitionDevice

    @property
    def master_controller(self):
        return self._master_controller_ref()

413
414
415
    class GroupedReadHandler(GroupedReadMixin):
        def get_values(self, from_index, *counters):
            """
416
            this method should return a list of numpy arrays in the same order
417
418
419
420
            as the counter_name
            """
            raise NotImplementedError

421
422
423
424
425
    class ConvertValues(object):
        def __init__(self, grouped_read_handler):
            self.get_values = grouped_read_handler.get_values

        def __call__(self, from_index, *counters):
426
427
428
429
430
431
432
433
434
            return [
                cnt.conversion_function(x) if cnt.conversion_function else x
                for x, cnt in zip(self.get_values(from_index, *counters), counters)
            ]

    def __init__(
        self,
        name,
        controller,
435
        master_controller,
436
437
438
        grouped_read_handler=None,
        conversion_function=None,
    ):
439
        if grouped_read_handler is None and hasattr(controller, "get_values"):
Vincent Michel's avatar
Vincent Michel committed
440
            grouped_read_handler = DefaultIntegratingCounterGroupedReadHandler(
441
442
                controller
            )
443

444
        if grouped_read_handler:
445
446
            if not isinstance(grouped_read_handler.get_values, self.ConvertValues):
                grouped_read_handler.get_values = self.ConvertValues(
447
448
                    grouped_read_handler
                )
449
450
        else:
            if callable(conversion_function):
451
                add_conversion_function(self, "get_values", conversion_function)
452

453
        super(IntegratingCounter, self).__init__(
454
455
            name, grouped_read_handler, conversion_function, controller
        )
456

457
        self._master_controller_ref = weakref.ref(master_controller)
458
459
460
461
462

    def get_values(self, from_index=0):
        """
        Overwrite in your class to provide a useful integrated counter class

Vincent Michel's avatar
Vincent Michel committed
463
464
        This method is called after the prepare and start on the master handler.
        This method can block until the data is ready or not and return empty data.
465
        When data is ready should return the data from the acquisition
466
        point **from_index**
467
468
469
        """
        raise NotImplementedError

Vincent Michel's avatar
Vincent Michel committed
470
471

def DefaultIntegratingCounterGroupedReadHandler(
472
473
    controller, handlers=weakref.WeakValueDictionary()
):
Vincent Michel's avatar
Vincent Michel committed
474
    class DefaultIntegratingCounterGroupedReadHandler(
475
476
        IntegratingCounter.GroupedReadHandler
    ):
477
478
479
        """
        Default read all handler for controller which have get_values method
        """
Vincent Michel's avatar
Vincent Michel committed
480

481
        def get_values(self, from_index, *counters):
482
483
484
485
486
487
488
            return [
                cnt.conversion_function(x) if cnt.conversion_function else x
                for x, cnt in zip(
                    self.controller.get_values(from_index, *counters), counters
                )
            ]

Vincent Michel's avatar
Vincent Michel committed
489
    return handlers.setdefault(
490
491
        controller, DefaultIntegratingCounterGroupedReadHandler(controller)
    )