scan_model.py 28.6 KB
Newer Older
1
2
3
4
# -*- coding: utf-8 -*-
#
# 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
7
8
9
10
11
12
13
14
# Distributed under the GNU LGPLv3. See LICENSE for more info.
"""This module provides object to model a scan acquisition process.

This tree structure is supposed to be real-only when a scan was
started. During the scan, only data of channels are updated.

.. image:: _static/flint/model/scan_model.png
    :alt: Scan model
    :align: center
Valentin Valls's avatar
Valentin Valls committed
15
16
17
18
19
20
21
22

.. image:: _static/flint/model/scan_model_group.png
    :alt: Scan model
    :align: center

Channels can be structured by groups. Scatter groups are described in a specific
structure to provide helpers. Channels can be reached by axis id, or channels
which are not part of axis (named counters).
23
24
25
26
27
28
29
30
31
"""

from __future__ import annotations
from typing import Optional
from typing import List
from typing import Iterator
from typing import Dict
from typing import Any
from typing import Set
32
from typing import NamedTuple
33
34
35
36

import logging
import numpy
import enum
37
import weakref
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

from silx.gui import qt


_logger = logging.getLogger(__name__)


class SealedError(Exception):
    """Exception occured when an object is sealed."""

    def __init__(self, message=None):
        if message is None:
            message = "The object is sealed, then not anymore editable."
        super(SealedError, self).__init__(message)


class _Sealable:
    """Abstract class for sealable object."""

    def __init__(self):
        self.__isSealed = False

    def seal(self):
        self.__isSealed = True

    def isSealed(self):
        return self.__isSealed


class ScanDataUpdateEvent:
    """Event containing the list of the updated channels.

    This event is shared by the `Scan` signal `scanDataUpdated`.
    """

    def __init__(
        self,
        scan: Scan,
        masterDevice: Optional[Device] = None,
        channel: Optional[Channel] = None,
78
        channels: Optional[List[Channel]] = None,
79
    ):
80
81
82
83
84
85
86
87
88
89
90
91
        """Event emitted when data from a scan is updated.

        `masterDevice` and `channel` can't be used both at the same time.

        Args:
            scan: The source scan of this event
            masterDevice: The root device from the acquisition chain tree which
                emit this event. In this case all the sub-channels have to be
                updated (except image and MCA channels, which always have specific
                event).
            channel: The channel source of this event
        """
92
93
94
        nb = sum([channel is not None, channels is not None, masterDevice is not None])
        if nb > 1:
            raise ValueError("Only a single attribute have to be set")
95
96
        self.__masterDevice = masterDevice
        self.__channel = channel
97
        self.__channels = channels
98
99
100
        self.__scan = scan
        self.__channelNames: Optional[Set[str]] = None

101
102
103
104
105
106
    def scan(self) -> Scan:
        return self.__scan

    def selectedDevice(self) -> Optional[Device]:
        return self.__masterDevice

107
108
109
    def selectedChannel(self) -> Optional[Channel]:
        return self.__channel

110
111
112
    def selectedChannels(self) -> Optional[List[Channel]]:
        return self.__channels

113
114
115
116
117
    def __eq__(self, other):
        if not isinstance(other, ScanDataUpdateEvent):
            return False
        return self.__channel is other.selectedChannel()

118
119
120
121
122
123
124
    def updatedChannelNames(self) -> Set[str]:
        if self.__channelNames is None:
            channelNames = {c.name() for c in self.iterUpdatedChannels()}
            self.__channelNames = channelNames
        return self.__channelNames

    def isUpdatedChannelName(self, channelName: str) -> bool:
Valentin Valls's avatar
Typo    
Valentin Valls committed
125
126
        updatedChannels = self.updatedChannelNames()
        return channelName in updatedChannels
127

Valentin Valls's avatar
Valentin Valls committed
128
    def __iterUpdatedDevices(self):
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
        if self.__channel is not None:
            yield self.__channel.device()
            return
        for device in self.__scan.devices():
            if self.__masterDevice is not None:
                if device is not self.__masterDevice:
                    # FIXME: master() is not accurate. It could be a sub-master
                    if device.master() is not self.__masterDevice:
                        continue
            yield device

    def iterUpdatedChannels(self):
        if self.__channel is not None:
            yield self.__channel
            return
144
145
146
147
        if self.__channels is not None:
            for channel in self.__channels:
                yield channel
            return
Valentin Valls's avatar
Valentin Valls committed
148
        for device in self.__iterUpdatedDevices():
149
            for channel in device.channels():
150
151
                if channel.type() not in set([ChannelType.IMAGE, ChannelType.SPECTRUM]):
                    yield channel
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201


class ScanState(enum.Enum):
    INITIALIZED = 0
    PROCESSING = 1
    FINISHED = 2


class Scan(qt.QObject, _Sealable):
    """Description of the scan object.

    A scan object contains all the informations generated by Redis about a scan.

    The data structure is fixed during a scan. Only channel data will be updated
    to be updated.

    It provides:

    - Signals for the life cycle of the scan (`scanStarted`, `scanDataUpdated`...)
    - A tree of `Device`, and `Channel` objects, plus helper to reach them.
    - The raw `scan_info`
    - Helper to cache information at the `Scan` level (which have a meaning
        during the scan life cycle)
    """

    scanStarted = qt.Signal()
    """Emitted when the scan acquisition starts"""

    scanSuccessed = qt.Signal()
    """Emitted when the scan acquisition succeeded."""

    scanFailed = qt.Signal()
    """Emitted when the scan acquisition failed."""

    scanFinished = qt.Signal()
    """Emitted when the scan acquisition finished.

    This signal is emitted after `scanFailed` or `scanFinished`.
    """

    scanDataUpdated = qt.Signal([], [ScanDataUpdateEvent])

    def __init__(self, parent=None):
        qt.QObject.__init__(self, parent=parent)
        _Sealable.__init__(self)
        self.__devices: List[Device] = []
        self.__channels: Dict[str, Channel] = {}
        self.__cacheData: Dict[Any, Any] = {}
        self.__cacheMessage: Dict[Any, Any] = {}
        self.__scanInfo = {}
202
        self.__finalScanInfo = None
203
        self.__state = ScanState.INITIALIZED
204
        self.__group = None
205
        self.__scatterData: List[ScatterData] = []
206
207
208
209
210
211
212
213
214
215
216
217
218
219

    def _setState(self, state: ScanState):
        """Private method to set the state of the scan."""
        self.__state = state

    def state(self) -> ScanState:
        """Returns the state of the scan."""
        return self.__state

    def seal(self):
        self.__channels = {}
        for device in self.__devices:
            device.seal()
            self.__cacheChannels(device)
220
221
        for scatterData in self.__scatterData:
            scatterData.seal()
222
223
        super(Scan, self).seal()

224
225
226
227
228
229
230
231
    def setGroup(self, group):
        self.__group = weakref.ref(group)

    def group(self):
        if self.__group is None:
            return None
        return self.__group()

232
233
234
235
236
237
238
239
240
    def setScanInfo(self, scanInfo: Dict):
        if self.isSealed():
            raise SealedError()
        # FIXME: It would be good to create a read-only recursive proxy to expose it
        self.__scanInfo = scanInfo

    def scanInfo(self) -> Dict:
        return self.__scanInfo

Valentin Valls's avatar
Valentin Valls committed
241
242
243
244
    def type(self) -> Optional[str]:
        """Returns the scan type stored in the scan info"""
        return self.__scanInfo.get("type", None)

245
246
247
248
    def hasPlotDescription(self) -> bool:
        """True if the scan contains plot description"""
        return len(self.__scanInfo.get("plots", [])) > 0

249
250
251
252
253
254
    def _setFinalScanInfo(self, scanInfo: Dict):
        self.__finalScanInfo = scanInfo

    def finalScanInfo(self) -> Optional[Dict]:
        return self.__finalScanInfo

255
256
257
258
259
260
261
    def addDevice(self, device: Device):
        if self.isSealed():
            raise SealedError()
        if device in self.__devices:
            raise ValueError("Already in the device list")
        self.__devices.append(device)

262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
    def getDeviceByName(self, name: str, fromTopMaster=False) -> Device:
        """
        Returns a device from an absolute path name.

        Arguments:
            fromTopMaster: If true, the path is relative to the top master
        """
        if fromTopMaster:
            for topmaster in self.__devices:
                if topmaster.master() is not None:
                    continue
                try:
                    return self.getDeviceByName(topmaster.name() + ":" + name)
                except ValueError:
                    continue
        else:
            elements = name.split(":")
            for device in self.__devices:
                current = device
                for e in reversed(elements):
                    if current is None or current.name() != e:
                        break
                    current = current.master()
                else:
                    # The item was found
                    if current is None:
                        return device
289

290
291
292
        raise ValueError("Device %s not found." % name)

    def _fireScanDataUpdated(
293
294
295
296
        self,
        channelName: str = None,
        masterDeviceName: str = None,
        channels: List[Channel] = None,
297
298
299
300
301
    ):
        self.__cacheData = {}
        # FIXME: Only clean up object relative to the edited channels
        self.__cacheMessage = {}

302
        if masterDeviceName is None and channelName is None and channels is None:
303
304
305
306
307
308
            # Propagate the event to all the channels of the this scan
            event = ScanDataUpdateEvent(self)
        elif masterDeviceName is not None:
            # Propagate the event to all the channels contained on this device (recursively)
            device = self.getDeviceByName(masterDeviceName)
            event = ScanDataUpdateEvent(self, masterDevice=device)
309
310
311
312
        elif channels is not None:
            # Propagate the event to many channels
            channel = self.getChannelByName(channelName)
            event = ScanDataUpdateEvent(self, channels=channels)
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
        elif channelName is not None:
            # Propagate the event to a single channel
            channel = self.getChannelByName(channelName)
            event = ScanDataUpdateEvent(self, channel=channel)
        else:
            assert False
        self.scanDataUpdated[ScanDataUpdateEvent].emit(event)
        self.scanDataUpdated.emit()

    def __cacheChannels(self, device: Device):
        for channel in device.channels():
            name = channel.name()
            if name in self.__channels:
                _logger.error("Channel named %s is registered 2 times", name)
            self.__channels[name] = channel

    def devices(self) -> Iterator[Device]:
        # FIXME better to export iterator or read only list
        return iter(self.__devices)

    def getChannelByName(self, name) -> Optional[Channel]:
        return self.__channels.get(name, None)

336
337
338
    def getChannelNames(self) -> List[str]:
        return list(self.__channels.keys())

339
340
341
342
343
344
345
346
347
348
349
    def addScatterData(self, scatterData: ScatterData):
        if self.isSealed():
            raise SealedError()
        self.__scatterData.append(scatterData)

    def getScatterDataByChannel(self, channel: Channel) -> Optional[ScatterData]:
        for data in self.__scatterData:
            if data.contains(channel):
                return data
        return None

350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
    def hasCachedResult(self, obj: Any) -> bool:
        """True if the `obj` object have stored cache in this scan."""
        return obj in self.__cacheData

    def getCachedResult(self, obj: Any) -> Any:
        """Returns a cached data relative to `obj` else raise a `KeyError`."""
        return self.__cacheData[obj]

    def setCachedResult(self, obj: Any, result: Any):
        """Store a cache data relative to `obj`."""
        self.__cacheData[obj] = result

    def hasCacheValidation(self, obj: Any, version: int) -> bool:
        """
        Returns true if this version of the object was validated.
        """
        result = self.__cacheMessage.get(obj, None)
        if result is None:
            return False
        if result[0] != version:
            return False
        return True

    def setCacheValidation(self, obj: Any, version: int, result: Optional[str]):
        """
        Set the validation of a mutable object.

        This feature is used to store validation message relative to a scan time.
        When the scan data is updated, this cache have to be stored again.

        The implementation only store a validation for a single version of an
        object. This could change.
        """
        current = self.__cacheMessage.get(obj)
        if current is not None and current[0] == version:
            raise KeyError("Result already stored for this object version")
        self.__cacheMessage[obj] = (version, result)

    def getCacheValidation(self, obj: Any, version: int) -> Optional[str]:
        """
        Returns None if the object was validated, else returns a message
        """
        result = self.__cacheMessage[obj]
        if result[0] != version:
            raise KeyError("Version do not match")
        return result[1]


Valentin Valls's avatar
Valentin Valls committed
398
399
400
401
402
403
class ScanGroup(Scan):
    """Scan group object.

    It can be a normal scan but can contains extra scans.
    """

404
405
406
407
408
409
410
411
412
413
414
415
416
417
    subScanAdded = qt.Signal(object)
    """Emitted when a sub scan is added to this scan."""

    def __init__(self, parent=None):
        Scan.__init__(self, parent=parent)
        self.__subScans = []

    def addSubScan(self, scan: Scan):
        self.__subScans.append(scan)
        self.subScanAdded.emit(scan)

    def subScans(self) -> List[Scan]:
        return list(self.__subScans)

Valentin Valls's avatar
Valentin Valls committed
418

419
420
421
422
423
424
class DeviceType(enum.Enum):
    """Enumerate the kind of devices"""

    NONE = 0
    """Default type"""

Valentin Valls's avatar
Valentin Valls committed
425
426
427
428
429
430
431
432
433
434
    UNKNOWN = -1
    """Unknown value specified in the scan_info"""

    LIMA = 1
    """Lima device as specified by the scan_info"""

    MCA = 2
    """MCA device as specified by the scan_info"""

    VIRTUAL_ROI = 3
435
436
437
438
    """Device containing channel data from the same ROI.
    It is a GUI concept, there is no related device on the BLISS side.
    """

439
440
441
442
443
444
    VIRTUAL_MCA_DETECTOR = 4
    """Device containing channel data from a MCA detector.

    A MCA device can contain many detectors.
    """

445

Valentin Valls's avatar
Valentin Valls committed
446
class DeviceMetadata(NamedTuple):
Valentin Valls's avatar
Valentin Valls committed
447
448
449
    info: Dict
    """raw metadata as stored by the scan_info"""

Valentin Valls's avatar
Valentin Valls committed
450
451
452
453
    roi: Optional[object]
    """Define a ROI geometry, is one"""


454
455
456
457
458
459
460
461
class Device(qt.QObject, _Sealable):
    """
    Description of a device.

    In the GUI side, a device is an named object which can contain other devices
    and channels. This could not exactly match the Bliss API.
    """

Valentin Valls's avatar
Valentin Valls committed
462
    _noneMetadata = DeviceMetadata({}, None)
Valentin Valls's avatar
Valentin Valls committed
463

464
465
466
467
    def __init__(self, parent: Scan):
        qt.QObject.__init__(self, parent=parent)
        _Sealable.__init__(self)
        self.__name: str = ""
Valentin Valls's avatar
Valentin Valls committed
468
        self.__metadata: DeviceMetadata = self._noneMetadata
469
        self.__type: DeviceType = DeviceType.NONE
470
471
472
473
474
475
476
477
        self.__channels: List[Channel] = []
        self.__master: Optional[Device] = None
        self.__topMaster: Optional[Device] = None
        parent.addDevice(self)

    def scan(self) -> Scan:
        return self.parent()

Valentin Valls's avatar
Valentin Valls committed
478
479
480
481
482
483
    def devices(self) -> List[Device]:
        """List sub devices from this device"""
        for d in self.scan().devices():
            if d.isChildOf(self):
                yield d

484
485
486
487
488
489
490
491
492
493
494
495
496
    def seal(self):
        for channel in self.__channels:
            channel.seal()
        super(Device, self).seal()

    def setName(self, name: str):
        if self.isSealed():
            raise SealedError()
        self.__name = name

    def name(self) -> str:
        return self.__name

Valentin Valls's avatar
Valentin Valls committed
497
498
499
500
501
502
503
504
505
506
507
508
    def fullName(self):
        """Path name from top master to this device.

        Each short name is separated by ":".
        """
        elements = [self.name()]
        parent = self.__master
        while parent is not None:
            elements.append(parent.name())
            parent = parent.__master
        return ":".join(reversed(elements))

Valentin Valls's avatar
Valentin Valls committed
509
510
511
512
513
514
515
516
517
518
519
    def setMetadata(self, metadata: DeviceMetadata):
        if self.isSealed():
            raise SealedError()
        self.__metadata = metadata

    def metadata(self) -> DeviceMetadata:
        """
        Returns a bunch of metadata stored within the channel.
        """
        return self.__metadata

520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
    def addChannel(self, channel: Channel):
        if self.isSealed():
            raise SealedError()
        if channel in self.__channels:
            raise ValueError("Already in the channel list")
        self.__channels.append(channel)

    def channels(self) -> Iterator[Channel]:
        return iter(self.__channels)

    def setMaster(self, master: Optional[Device]):
        if self.isSealed():
            raise SealedError()
        self.__master = master
        self.__topMaster = None

    def master(self) -> Optional[Device]:
        return self.__master

    def topMaster(self) -> Device:
        if self.__topMaster is None:
            topMaster = self
            while topMaster:
                m = topMaster.master()
                if m is None:
                    break
                topMaster = m
            self.__topMaster = topMaster
        return self.__topMaster

    def isMaster(self) -> bool:
        """
        True if the device is a master device.
        """
        # FIXME: This have to be improved
        return self.__master is None

Valentin Valls's avatar
Valentin Valls committed
557
558
559
560
561
562
563
564
565
    def isChildOf(self, master: Device) -> bool:
        """Returns true if this device is the child of `master` device."""
        parent = self.__master
        while parent is not None:
            if parent is master:
                return True
            parent = parent.__master
        return False

566
567
568
569
570
571
572
573
574
575
576
    def setType(self, deviceType: DeviceType):
        if self.isSealed():
            raise SealedError()
        self.__type = deviceType

    def type(self) -> DeviceType:
        """
        Returns the kind of this channel.
        """
        return self.__type

577
578
579
580
581
582
583
584
585
586
587
588
589
590
591

class ChannelType(enum.Enum):
    """Enumerate the kind of channels"""

    COUNTER = 0
    """Type of channel which store a single data per trigger.
    The sequence of acquisition is stored."""
    SPECTRUM = 1
    """Type of channel which store a list of data per trigger.
    Only the last data stored."""
    IMAGE = 2
    """Type of channel which store a 2d image per trigger.
    Only the last data stored."""


Valentin Valls's avatar
Valentin Valls committed
592
class AxisKind(enum.Enum):
593
594
595
596
597
    FORTH = "forth"
    BACKNFORTH = "backnforth"
    STEP = "step"

    # Deprecated code from user scripts from BLISS 1.4
598
    FAST = "fast"
599
    # Deprecated code from user scripts from BLISS 1.4
600
    FAST_BACKNFORTH = "fast-backnforth"
601
    # Deprecated code from user scripts from BLISS 1.4
602
    SLOW = "slow"
603
    # Deprecated code from user scripts from BLISS 1.4
Valentin Valls's avatar
Valentin Valls committed
604
    SLOW_BACKNFORTH = "slow-backnforth"
605
606
607
608
609
610
611
612


class ChannelMetadata(NamedTuple):
    start: Optional[float]
    stop: Optional[float]
    min: Optional[float]
    max: Optional[float]
    points: Optional[int]
613
    axisId: Optional[int]
Valentin Valls's avatar
Valentin Valls committed
614
615
    axisPoints: Optional[int]
    axisKind: Optional[AxisKind]
616
    group: Optional[str]
Valentin Valls's avatar
Valentin Valls committed
617
    axisPointsHint: Optional[int]
618
    dim: Optional[int]
619
620


621
622
623
624
625
626
627
628
class ScatterData(_Sealable):
    """Data structure of a scatter"""

    def __init__(self):
        super(ScatterData, self).__init__()
        self.__channels: List[List[Channel]] = []
        self.__noIndexes: List[Channel] = []
        self.__contains: Set[Channel] = set([])
629
        self.__values: List[Channel] = []
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650

    def maxDim(self):
        return len(self.__channels)

    def channelsAt(self, axisId: int) -> List[Channel]:
        """Returns the list of channels stored at this axisId"""
        return self.__channels[axisId]

    def findGroupableAt(self, axisId: int) -> Optional[Channel]:
        """Returns a channel which can be grouped at a specific axisId"""
        for channel in self.channelsAt(axisId):
            if channel.metadata().axisKind == AxisKind.STEP:
                return channel
        return None

    def channelAxis(self, channel: Channel):
        for i in range(len(self.__channels)):
            if channel in self.__channels[i]:
                return i
        raise IndexError()

651
652
653
    def counterChannels(self):
        return list(self.__values)

654
655
656
657
658
659
660
661
662
663
664
665
    def addAxisChannel(self, channel: Channel, axisId: int):
        """Add channel as an axis of the scatter"""
        if self.isSealed():
            raise SealedError()
        if axisId is None:
            self.__noIndexes.append(channel)
        else:
            while len(self.__channels) <= axisId:
                self.__channels.append([])
            self.__channels[axisId].append(channel)
        self.__contains.add(channel)

666
667
668
669
    def addCounterChannel(self, channel: Channel):
        """Add channel used as a counter"""
        self.__values.append(channel)

670
671
672
673
674
675
676
677
678
    def contains(self, channel: Channel) -> bool:
        return channel in self.__contains

    def seal(self):
        for channel in self.__noIndexes:
            self.__channels.append([channel])
        del self.__noIndexes
        super(ScatterData, self).seal()

679
680
681
682
683
684
685
686
    def shape(self):
        """Returns the theorical ndim shape based on channels metadata.

        It is supported by numpy arrays. If a channel do not have `axisPoints`
        specified, -1 is used.
        """
        result = []
        for axisId in range(self.maxDim()):
687
            size = None
688
689
690
691
692
693
694
            for channel in self.__channels[axisId]:
                size = channel.metadata().axisPoints
                if size is not None:
                    break
            result.append(size)
        return tuple(reversed(result))

695

696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
class Channel(qt.QObject, _Sealable):
    """
    Description of a channel.

    In the GUI side, a channel is leaf of the scan tree, which contain the raw
    data from Bliss through Redis.

    A channel have a specific data kind which can't change during the scan.
    It will only be feed with this kind of data.
    """

    dataUpdated = qt.Signal(object)
    """Emitted when setData is invoked.
    """

711
    _noneMetadata = ChannelMetadata(
712
        None, None, None, None, None, None, None, None, None, None, None
713
714
    )

715
716
    _dimToType = {0: ChannelType.COUNTER, 1: ChannelType.SPECTRUM, 2: ChannelType.IMAGE}

717
718
719
720
    def __init__(self, parent: Device):
        qt.QObject.__init__(self, parent=parent)
        _Sealable.__init__(self)
        self.__data: Optional[Data] = None
721
        self.__metadata: ChannelMetadata = self._noneMetadata
722
        self.__name: str = ""
723
        self.__type: ChannelType = None
724
725
        self.__displayName: Optional[str] = None
        self.__unit: Optional[str] = None
726
        self.__refreshRates: Dict[str, Optional[int]] = {}
727
        self.__updatedCount = 0
728
729
730
731
732
733
734
735
736
737
        parent.addChannel(self)

    def setType(self, channelType: ChannelType):
        if self.isSealed():
            raise SealedError()
        self.__type = channelType

    def type(self) -> ChannelType:
        """
        Returns the kind of this channel.
738
739

        FIXME this have to be property checked before remove (use device type instead or not)
740
        """
741
742
        if self.__type is None:
            return self._dimToType.get(self.__metadata.dim, ChannelType.COUNTER)
743
744
        return self.__type

745
746
747
748
749
    def setMetadata(self, metadata: ChannelMetadata):
        if self.isSealed():
            raise SealedError()
        self.__metadata = metadata

750
    def metadata(self) -> ChannelMetadata:
751
        """
Valentin Valls's avatar
Valentin Valls committed
752
        Returns a bunch of metadata stored within the channel.
753
754
755
        """
        return self.__metadata

756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
    def setDisplayName(self, displayName: str):
        if self.isSealed():
            raise SealedError()
        self.__displayName = displayName

    def displayName(self) -> Optional[str]:
        """
        Returns the preferred display name of this channel.
        """
        return self.__displayName

    def setUnit(self, unit: str):
        if self.isSealed():
            raise SealedError()
        self.__unit = unit

    def unit(self) -> Optional[str]:
        """
        Returns the unit of this channel.
        """
        return self.__unit

    def device(self) -> Device:
        """
        Returns the device containing this channel.
        """
        return self.parent()

    def master(self) -> Device:
        """
        Returns the first master containing this channel.
        """
        parent = self.device()
        if parent.isMaster():
            return parent
        else:
            return parent.master()

    def name(self) -> str:
        """
        Returns the full name of the channel.

        It is a unique identifier during a scan.
        """
        return self.__name

    def baseName(self) -> str:
        """
        Returns the trail sequence of the channel name.
        """
        return self.__name.split(":")[-1]

    @property
    def ndim(self) -> int:
        """
        Returns the amount of dimensions of the data, before reaching the data.

        Mimics numpy arrays."""
814
815
816
817
818
819
820
        dim = self.__metadata.dim
        if dim is not None:
            if dim == 0:
                # scalar are stored with an extra "time/step" dimension
                return dim + 1
            return dim

821
822
823
824
825
826
827
        if self.__type == ChannelType.COUNTER:
            # one value per count
            return 1
        elif self.__type == ChannelType.SPECTRUM:
            # one value per MCA channel
            return 1
        elif self.__type == ChannelType.IMAGE:
828
            # FIXME; This have no meaning anymore as we support RGB and RGBA
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
            return 2
        else:
            assert False

    def setName(self, name: str):
        if self.isSealed():
            raise SealedError()
        self.__name = name

    def hasData(self) -> bool:
        """
        True if a data is set to this channel.

        A channel can contain nothing during a scan.
        """
        return self.__data is not None

    def data(self) -> Optional[Data]:
        """
        Returns the data associated to this channel.

        It is the only one attribute which can be updated during a scan.
        """
        return self.__data

854
855
856
857
858
859
860
861
862
863
    def array(self) -> Optional[numpy.array]:
        """
        Returns the array associated to this channel.

        This method is a shortcut to `.data().array()`.
        """
        if self.__data is None:
            return None
        return self.__data.array()

864
865
    def isDataCompatible(self, data: Data) -> bool:
        """
Valentin Valls's avatar
Typo    
Valentin Valls committed
866
        True if this `data` is compatible with this channel.
867
868
869
        """
        if data is None:
            return True
870
871
872
        array = data.array()
        if array is None:
            return True
873
874
875
876
877
878
879
        if self.ndim == array.ndim:
            return True
        if self.__type == ChannelType.IMAGE:
            if array.ndim == 3:
                if array.shape[2] in [3, 4]:
                    return True
        return False
880
881
882
883
884
885
886
887
888
889
890

    def setData(self, data: Data):
        """
        Set the data associated to this channel.

        If the data is updated the signal `dataUpdated` is invoked.
        """
        if not self.isDataCompatible(data):
            raise ValueError("Data do not fit the channel requirements")
        if self.__data is data:
            return
891
        self.__updatedCount += 1
892
893
894
        self.__data = data
        self.dataUpdated.emit(data)

895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
    def setPreferedRefreshRate(self, key: str, rate: Optional[int]):
        """Allow to specify the prefered refresh rate.

        It have to be specified in millisecond.
        """
        if rate is None:
            if key in self.__refreshRates:
                del self.__refreshRates[key]
        else:
            self.__refreshRates[key] = rate

    def preferedRefreshRate(self) -> Optional[int]:
        if len(self.__refreshRates) == 0:
            return None
        return min(self.__refreshRates.values())

911
912
913
914
    def updatedCount(self) -> int:
        """Amount of time the data was updated."""
        return self.__updatedCount

915
916
917
918
919
920
921

class Data(qt.QObject):
    """
    Store a `numpy.array` associated to a channel.

    This object was designed to be non-mutable in order to allow fast comparison,
    and to store metadata relative to the measurement (like unit, error) or
Valentin Valls's avatar
Typo    
Valentin Valls committed
922
    helper to deal with the data (like hash). Could be renamed into `Quantity`.
923
924
    """

925
926
927
928
929
930
    def __init__(
        self,
        parent=None,
        array: numpy.ndarray = None,
        frameId: int = None,
        source: str = None,
931
        receivedTime: float = None,
932
    ):
933
        qt.QObject.__init__(self, parent=parent)
934
935
        self.__array = array
        self.__frameId = frameId
936
        self.__source = source
937
        self.__receivedTime = receivedTime
938
939
940
941
942

    def array(self) -> numpy.ndarray:
        return self.__array

    def frameId(self) -> int:
943
        """Frame number, only valid for images"""
944
        return self.__frameId
945
946
947
948

    def source(self) -> str:
        """Source of the image, only valid for images"""
        return self.__source
949
950
951
952

    def receivedTime(self) -> float:
        """Timestamp in second when the application received this data"""
        return self.__receivedTime