Completed
Push — pulsed_with_queued_connections ( 6b1460...30fbbf )
by
unknown
02:58
created

ConfocalHistoryEntry.deserialize()   F

Complexity

Conditions 28

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 28
dl 0
loc 49
rs 2.7364
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like ConfocalHistoryEntry.deserialize() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
"""
3
This module operates a confocal microsope.
4
5
Qudi is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public License as published by
7
the Free Software Foundation, either version 3 of the License, or
8
(at your option) any later version.
9
10
Qudi is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
GNU General Public License for more details.
14
15
You should have received a copy of the GNU General Public License
16
along with Qudi. If not, see <http://www.gnu.org/licenses/>.
17
18
Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the
19
top-level directory of this distribution and at <https://github.com/Ulm-IQO/qudi/>
20
"""
21
22
from qtpy import QtCore
23
from collections import OrderedDict
24
from copy import copy
25
from datetime import datetime
26
import numpy as np
27
import matplotlib as mpl
28
import matplotlib.pyplot as plt
29
from io import BytesIO
30
31
from logic.generic_logic import GenericLogic
32
from core.util.mutex import Mutex
33
34
35
def numpy_from_b(compressed_b):
36
    f = BytesIO(bytes(compressed_b))
37
    np_file = np.load(f)
38
    redict = dict()
39
    for name in np_file.files:
40
        redict.update({name: np_file[name]})
41
    f.close()
42
    return redict
43
44
45
class OldConfigFileError(Exception):
46
    def __init__(self):
47
        super().__init__('Old configuration file detected. Ignoring history.')
48
49
50
class ConfocalHistoryEntry(QtCore.QObject):
51
    """ This class contains all relevant parameters of a Confocal scan.
52
        It provides methods to extract, restore and serialize this data.
53
    """
54
55
    def __init__(self, confocal):
56
        """ Make a confocal data setting with default values. """
57
        super().__init__()
58
59
        self.xy_line_pos = 0
60
        self.depth_line_pos = 0
61
62
        # Reads in the maximal scanning range. The unit of that scan range is micrometer!
63
        self.x_range = confocal._scanning_device.get_position_range()[0]
64
        self.y_range = confocal._scanning_device.get_position_range()[1]
65
        self.z_range = confocal._scanning_device.get_position_range()[2]
66
67
        # Sets the current position to the center of the maximal scanning range
68
        self.current_x = (self.x_range[0] + self.x_range[1]) / 2
69
        self.current_y = (self.y_range[0] + self.y_range[1]) / 2
70
        self.current_z = (self.z_range[0] + self.z_range[1]) / 2
71
        self.current_a = 0.0
72
73
        # Sets the size of the image to the maximal scanning range
74
        self.image_x_range = self.x_range
75
        self.image_y_range = self.y_range
76
        self.image_z_range = self.z_range
77
78
        # Default values for the resolution of the scan
79
        self.xy_resolution = 100
80
        self.z_resolution = 50
81
82
        # Initialization of internal counter for scanning
83
        self.xy_line_position = 0
84
        self.depth_line_position = 0
85
86
        # Variable to check if a scan is continuable
87
        self.xy_scan_continuable = False
88
        self.depth_scan_continuable = False
89
90
        # tilt correction stuff:
91
        self.tilt_correction = False
92
        # rotation point for tilt correction
93
        self.tilt_reference_x = 0.5 * (self.x_range[0] + self.x_range[1])
94
        self.tilt_reference_y = 0.5 * (self.y_range[0] + self.y_range[1])
95
        # sample slope
96
        self.tilt_slope_x = 0
97
        self.tilt_slope_y = 0
98
        # tilt correction points
99
        self.point1 = np.array((0, 0, 0))
100
        self.point2 = np.array((0, 0, 0))
101
        self.point3 = np.array((0, 0, 0))
102
103
    def restore(self, confocal):
104
        """ Write data back into confocal logic and pull all the necessary strings """
105
        confocal._current_x = self.current_x
106
        confocal._current_y = self.current_y
107
        confocal._current_z = self.current_z
108
        confocal._current_a = self.current_a
109
        confocal.image_x_range = np.copy(self.image_x_range)
110
        confocal.image_y_range = np.copy(self.image_y_range)
111
        confocal.image_z_range = np.copy(self.image_z_range)
112
        confocal.xy_resolution = self.xy_resolution
113
        confocal.z_resolution = self.z_resolution
114
        confocal._xy_line_pos = self.xy_line_position
115
        confocal._depth_line_pos = self.depth_line_position
116
        confocal._xyscan_continuable = self.xy_scan_continuable
117
        confocal._zscan_continuable = self.depth_scan_continuable
118
        confocal.TiltCorrection = self.tilt_correction
119
        confocal.point1 = np.copy(self.point1)
120
        confocal.point2 = np.copy(self.point2)
121
        confocal.point3 = np.copy(self.point3)
122
        confocal._tiltreference_x = self.tilt_reference_x
123
        confocal._tiltreference_y = self.tilt_reference_y
124
        confocal._tilt_variable_ax = self.tilt_slope_x
125
        confocal._tilt_variable_ay = self.tilt_slope_y
126
127
        confocal.initialize_image()
128
        try:
129
            if confocal.xy_image.shape == self.xy_image.shape:
130
                confocal.xy_image = np.copy(self.xy_image)
131
        except AttributeError:
132
            self.xy_image = np.copy(confocal.xy_image)
133
134
        confocal._zscan = True
135
        confocal.initialize_image()
136
        try:
137
            if confocal.depth_image.shape == self.depth_image.shape:
138
                confocal.depth_image = np.copy(self.depth_image)
139
        except AttributeError:
140
            self.depth_image = np.copy(confocal.depth_image)
141
        confocal._zscan = False
142
143
    def snapshot(self, confocal):
144
        """ Extract all necessary data from a confocal logic and keep it for later use """
145
        self.current_x = confocal._current_x
146
        self.current_y = confocal._current_y
147
        self.current_z = confocal._current_z
148
        self.current_a = confocal._current_a
149
        self.image_x_range = np.copy(confocal.image_x_range)
150
        self.image_y_range = np.copy(confocal.image_y_range)
151
        self.image_z_range = np.copy(confocal.image_z_range)
152
        self.xy_resolution = confocal.xy_resolution
153
        self.z_resolution = confocal.z_resolution
154
        self.xy_line_position = confocal._xy_line_pos
155
        self.depth_line_position = confocal._depth_line_pos
156
        self.xy_scan_continuable = confocal._xyscan_continuable
157
        self.depth_scan_continuable = confocal._zscan_continuable
158
        self.tilt_correction = confocal.TiltCorrection
159
        self.point1 = np.copy(confocal.point1)
160
        self.point2 = np.copy(confocal.point2)
161
        self.point3 = np.copy(confocal.point3)
162
        self.tilt_reference_x = confocal._tiltreference_x
163
        self.tilt_reference_y = confocal._tiltreference_y
164
        self.tilt_slope_x = confocal._tilt_variable_ax
165
        self.tile_slope_y = confocal._tilt_variable_ay
166
        self.xy_image = np.copy(confocal.xy_image)
167
        self.depth_image = np.copy(confocal.depth_image)
168
169
    def serialize(self):
170
        """ Give out a dictionary that can be saved via the usual means """
171
        serialized = dict()
172
        serialized['focus_position'] = [self.current_x, self.current_y, self.current_z, self.current_a]
173
        serialized['x_range'] = self.image_x_range
174
        serialized['y_range'] = self.image_y_range
175
        serialized['z_range'] = self.image_z_range
176
        serialized['xy_resolution'] = self.xy_resolution
177
        serialized['z_resolution'] = self.z_resolution
178
        serialized['xy_line_position'] = self.xy_line_position
179
        serialized['depth_linne_position'] = self.depth_line_position
180
        serialized['xy_scan_cont'] = self.xy_scan_continuable
181
        serialized['depth_scan_cont'] = self.depth_scan_continuable
182
        serialized['tilt_correction'] = self.tilt_correction
183
        serialized['tilt_point1'] = self.point1
184
        serialized['tilt_point2'] = self.point2
185
        serialized['tilt_point3'] = self.point3
186
        serialized['tilt_reference'] = [self.tilt_reference_x, self.tilt_reference_y]
187
        serialized['tilt_slope'] = [self.tilt_slope_x, self.tilt_slope_y]
188
        serialized['xy_image'] = self.xy_image
189
        serialized['depth_image'] = self.depth_image
190
        return serialized
191
192
    def deserialize(self, serialized):
193
        """ Restore Confocal history object from a dict """
194
        if 'focus_position' in serialized and len(serialized['focus_position']) == 4:
195
            self.current_x = serialized['focus_position'][0]
196
            self.current_y = serialized['focus_position'][1]
197
            self.current_z = serialized['focus_position'][2]
198
            self.current_a = serialized['focus_position'][3]
199
        if 'x_range' in serialized and len(serialized['x_range']) == 2:
200
            self.image_x_range = serialized['x_range']
201
        if 'y_range' in serialized and len(serialized['y_range']) == 2:
202
            self.image_y_range = serialized['y_range']
203
        if 'z_range' in serialized and len(serialized['z_range']) == 2:
204
            self.image_z_range = serialized['z_range']
205
        if 'xy_resolution' in serialized:
206
            self.xy_resolution = serialized['xy_resolution']
207
        if 'z_resolution' in serialized:
208
            self.z_resolution = serialized['z_resolution']
209
        if 'tilt_correction' in serialized:
210
            self.tilt_correction = serialized['tilt_correction']
211
        if 'tilt_reference' in serialized and len(serialized['tilt_reference']) == 2:
212
            self.tilt_reference_x = serialized['tilt_reference'][0]
213
            self.tilt_reference_y = serialized['tilt_reference'][1]
214
        if 'tilt_slope' in serialized and len(serialized['tilt_slope']) == 2:
215
            self.tilt_slope_x = serialized['tilt_slope'][0]
216
            self.tilt_slope_y = serialized['tilt_slope'][1]
217
        if 'tilt_point1' in serialized and len(serialized['tilt_point1'] ) == 3:
218
            self.point1 = np.array(serialized['tilt_point1'])
219
        if 'tilt_point2' in serialized and len(serialized['tilt_point2'] ) == 3:
220
            self.point2 = np.array(serialized['tilt_point2'])
221
        if 'tilt_point3' in serialized and len(serialized['tilt_point3'] ) == 3:
222
            self.point3 = np.array(serialized['tilt_point3'])
223
        if 'xy_image' in serialized:
224
            if isinstance(serialized['xy_image'], np.ndarray):
225
                self.xy_image = serialized['xy_image']
226
            else:
227
                try:
228
                    self.xy_image = numpy_from_b(
229
                            eval(serialized['xy_image']))['image']
230
                except:
231
                    raise OldConfigFileError()
232
        if 'depth_image' in serialized:
233
            if isinstance(serialized['depth_image'], np.ndarray):
234
                self.depth_image = serialized['depth_image'].copy()
235
            else:
236
                try:
237
                    self.depth_image = numpy_from_b(
238
                            eval(serialized['depth_image']))['image']
239
                except:
240
                    raise OldConfigFileError()
241
242
243
class ConfocalLogic(GenericLogic):
244
    """
245
    This is the Logic class for confocal scanning.
246
    """
247
    _modclass = 'confocallogic'
248
    _modtype = 'logic'
249
250
    # declare connectors
251
    _in = {
252
        'confocalscanner1': 'ConfocalScannerInterface',
253
        'savelogic': 'SaveLogic'
254
        }
255
    _out = {'scannerlogic': 'ConfocalLogic'}
256
257
    # signals
258
    signal_start_scanning = QtCore.Signal()
259
    signal_continue_scanning = QtCore.Signal()
260
    signal_scan_lines_next = QtCore.Signal()
261
    signal_xy_image_updated = QtCore.Signal()
262
    signal_depth_image_updated = QtCore.Signal()
263
    signal_change_position = QtCore.Signal(str)
264
265
    sigImageXYInitialized = QtCore.Signal()
266
    sigImageDepthInitialized = QtCore.Signal()
267
268
    signal_history_event = QtCore.Signal()
269
270
    def __init__(self, config, **kwargs):
271
        super().__init__(config=config, **kwargs)
272
273
        self.log.info('The following configuration was found.')
274
275
        # checking for the right configuration
276
        for key in config.keys():
277
            self.log.info('{0}: {1}'.format(key, config[key]))
278
279
        #locking for thread safety
280
        self.threadlock = Mutex()
281
282
        # counter for scan_image
283
        self._scan_counter = 0
284
        self._zscan = False
285
        self.stopRequested = False
286
        self.depth_scan_dir_is_xz = True
287
        self.permanent_scan = False
288
289
    def on_activate(self, e):
290
        """ Initialisation performed during activation of the module.
291
292
        @param e: error code
293
        """
294
        self._scanning_device = self.get_in_connector('confocalscanner1')
295
#        print("Scanning device is", self._scanning_device)
296
297
        self._save_logic = self.get_in_connector('savelogic')
298
299
        #default values for clock frequency and slowness
300
        #slowness: steps during retrace line
301
        if 'clock_frequency' in self._statusVariables:
302
            self._clock_frequency = self._statusVariables['clock_frequency']
303
        else:
304
            self._clock_frequency = 500
305
        if 'return_slowness' in self._statusVariables:
306
            self.return_slowness = self._statusVariables['return_slowness']
307
        else:
308
            self.return_slowness = 50
309
310
        # Reads in the maximal scanning range. The unit of that scan range is micrometer!
311
        self.x_range = self._scanning_device.get_position_range()[0]
312
        self.y_range = self._scanning_device.get_position_range()[1]
313
        self.z_range = self._scanning_device.get_position_range()[2]
314
315
        # restore here ...
316
        self.history = []
317
        if 'max_history_length' in self._statusVariables:
318
                self.max_history_length = self._statusVariables ['max_history_length']
319
                for i in reversed(range(1, self.max_history_length)):
320
                    try:
321
                        new_history_item = ConfocalHistoryEntry(self)
322
                        new_history_item.deserialize(self._statusVariables['history_{0}'.format(i)])
323
                        self.history.append(new_history_item)
324
                    except KeyError:
325
                        pass
326
                    except OldConfigFileError:
327
                        self.log.warning('Old style config file detected. '
328
                                'History {0} ignored.'.format(i))
329
                    except:
330
                        self.log.warning(
331
                                'Restoring history {0} failed.'.format(i))
332
        else:
333
            self.max_history_length = 10
334
        try:
335
            new_state = ConfocalHistoryEntry(self)
336
            new_state.deserialize(self._statusVariables['history_0'])
337
            new_state.restore(self)
338
        except:
339
            new_state = ConfocalHistoryEntry(self)
340
            new_state.restore(self)
341
        finally:
342
            self.history.append(new_state)
343
344
        self.history_index = len(self.history) - 1
345
346
        # Sets connections between signals and functions
347
        self.signal_scan_lines_next.connect(self._scan_line, QtCore.Qt.QueuedConnection)
348
        self.signal_start_scanning.connect(self.start_scanner, QtCore.Qt.QueuedConnection)
349
        self.signal_continue_scanning.connect(self.continue_scanner, QtCore.Qt.QueuedConnection)
350
351
        self._change_position('activation')
352
353
    def on_deactivate(self, e):
354
        """ Reverse steps of activation
355
356
        @param e: error code
357
358
        @return int: error code (0:OK, -1:error)
359
        """
360
        self._statusVariables['clock_frequency'] = self._clock_frequency
361
        self._statusVariables['return_slowness'] = self.return_slowness
362
        self._statusVariables['max_history_length'] = self.max_history_length
363
        closing_state = ConfocalHistoryEntry(self)
364
        closing_state.snapshot(self)
365
        self.history.append(closing_state)
366
        histindex = 0
367
        for state in reversed(self.history):
368
            self._statusVariables['history_{0}'.format(histindex)] = state.serialize()
369
            histindex += 1
370
        return 0
371
372
    def switch_hardware(self, to_on=False):
373
        """ Switches the Hardware off or on.
374
375
        @param to_on: True switches on, False switched off
376
377
        @return int: error code (0:OK, -1:error)
378
        """
379
        if to_on:
380
            return self._scanning_device.activation()
381
        else:
382
            return self._scanning_device.reset_hardware()
383
384
    def set_clock_frequency(self, clock_frequency):
385
        """Sets the frequency of the clock
386
387
        @param int clock_frequency: desired frequency of the clock
388
389
        @return int: error code (0:OK, -1:error)
390
        """
391
        self._clock_frequency = int(clock_frequency)
392
        #checks if scanner is still running
393
        if self.getState() == 'locked':
394
            return -1
395
        else:
396
            return 0
397
398
    def start_scanning(self, zscan = False):
399
        """Starts scanning
400
401
        @param bool zscan: zscan if true, xyscan if false
402
403
        @return int: error code (0:OK, -1:error)
404
        """
405
        # TODO: this is dirty, but it works for now
406
#        while self.getState() == 'locked':
407
#            time.sleep(0.01)
408
        self._scan_counter = 0
409
        self._zscan = zscan
410
        if self._zscan:
411
            self._zscan_continuable = True
412
        else:
413
            self._xyscan_continuable = True
414
415
        self.signal_start_scanning.emit()
416
        return 0
417
418
419
    def continue_scanning(self,zscan):
420
        """Continue scanning
421
422
        @return int: error code (0:OK, -1:error)
423
        """
424
        self._zscan = zscan
425
        if zscan:
426
            self._scan_counter = self._depth_line_pos
427
        else:
428
            self._scan_counter = self._xy_line_pos
429
        self.signal_continue_scanning.emit()
430
        return 0
431
432
433
    def stop_scanning(self):
434
        """Stops the scan
435
436
        @return int: error code (0:OK, -1:error)
437
        """
438
        with self.threadlock:
439
            if self.getState() == 'locked':
440
                self.stopRequested = True
441
        return 0
442
443
    def initialize_image(self):
444
        """Initalization of the image.
445
446
        @return int: error code (0:OK, -1:error)
447
        """
448
        # x1: x-start-value, x2: x-end-value
449
        x1, x2 = self.image_x_range[0], self.image_x_range[1]
450
        # y1: x-start-value, y2: x-end-value
451
        y1, y2 = self.image_y_range[0], self.image_y_range[1]
452
        # z1: x-start-value, z2: x-end-value
453
        z1, z2 = self.image_z_range[0], self.image_z_range[1]
454
455
        # Checks if the x-start and x-end value are ok
456
        if x2 < x1:
457
            self.log.error(
458
                'x1 must be smaller than x2, but they are '
459
                '({0:.3f},{1:.3f}).'.format(x1, x2))
460
            return -1
461
462
        if self._zscan:
463
            # creates an array of evenly spaced numbers over the interval
464
            # x1, x2 and the spacing is equal to xy_resolution
465
            self._X = np.linspace(x1, x2, self.xy_resolution)
466
            # Checks if the z-start and z-end value are ok
467
            if z2 < z1:
468
                self.log.error(
469
                    'z1 must be smaller than z2, but they are '
470
                    '({0:.3f},{1:.3f}).'.format(z1, z2))
471
                return -1
472
            # creates an array of evenly spaced numbers over the interval
473
            # z1, z2 and the spacing is equal to z_resolution
474
            self._Z = np.linspace(z1, z2, max(self.z_resolution, 2))
475
        else:
476
            # Checks if the y-start and y-end value are ok
477
            if y2 < y1:
478
                self.log.error(
479
                    'y1 must be smaller than y2, but they are '
480
                    '({0:.3f},{1:.3f}).'.format(y1, y2))
481
                return -1
482
483
            # prevents distorion of the image
484
            if (x2 - x1) >= (y2 - y1):
485
                self._X = np.linspace(x1, x2, max(self.xy_resolution, 2))
486
                self._Y = np.linspace(y1, y2, max(int(self.xy_resolution*(y2-y1)/(x2-x1)), 2))
487
            else:
488
                self._Y = np.linspace(y1, y2, max(self.xy_resolution, 2))
489
                self._X = np.linspace(x1, x2, max(int(self.xy_resolution*(x2-x1)/(y2-y1)), 2))
490
491
        self._XL = self._X
492
        self._YL = self._Y
493
        self._AL = np.zeros(self._XL.shape)
494
495
        # Arrays for retrace line
496
        self._return_XL = np.linspace(self._XL[-1], self._XL[0], self.return_slowness)
497
        self._return_AL = np.zeros(self._return_XL.shape)
498
499
        if self._zscan:
500
            if self.depth_scan_dir_is_xz:
501
                self._image_vert_axis = self._Z
502
                # creates an image where each pixel will be [x,y,z,counts]
503
                self.depth_image = np.zeros((len(self._image_vert_axis), len(self._X), 4))
504
                self.depth_image[:, : ,0] = np.full((len(self._image_vert_axis), len(self._X)), self._XL)
505
                self.depth_image[:, :, 1] = self._current_y * np.ones((len(self._image_vert_axis), len(self._X)))
506
                z_value_matrix = np.full((len(self._X), len(self._image_vert_axis)), self._Z)
507
                self.depth_image[:, :, 2] = z_value_matrix.transpose()
508
            else: # depth scan is yz instead of xz
509
                self._image_vert_axis = self._Z
510
                # creats an image where each pixel will be [x,y,z,counts]
511
                self.depth_image = np.zeros((len(self._image_vert_axis), len(self._Y), 4))
512
                self.depth_image[:, :, 0] = self._current_x * np.ones((len(self._image_vert_axis), len(self._Y)))
513
                self.depth_image[:, :, 1] = np.full((len(self._image_vert_axis), len(self._Y)), self._YL)
514
                z_value_matrix = np.full((len(self._Y), len(self._image_vert_axis)), self._Z)
515
                self.depth_image[:, :, 2] = z_value_matrix.transpose()
516
                # now we are scanning along the y-axis, so we need a new return line along Y:
517
                self._return_YL = np.linspace(self._YL[-1], self._YL[0], self.return_slowness)
518
                self._return_AL = np.zeros(self._return_YL.shape)
519
            self.sigImageDepthInitialized.emit()
520
        else:
521
            self._image_vert_axis = self._Y
522
            # creats an image where each pixel will be [x,y,z,counts]
523
            self.xy_image = np.zeros((len(self._image_vert_axis), len(self._X), 4))
524
            self.xy_image[:, :, 0] = np.full((len(self._image_vert_axis), len(self._X)), self._XL)
525
            y_value_matrix = np.full((len(self._X), len(self._image_vert_axis)), self._Y)
526
            self.xy_image[:, :, 1] = y_value_matrix.transpose()
527
            self.xy_image[:, :, 2] = self._current_z * np.ones((len(self._image_vert_axis), len(self._X)))
528
            self.sigImageXYInitialized.emit()
529
        return 0
530
531
    def start_scanner(self):
532
        """Setting up the scanner device and starts the scanning procedure
533
534
        @return int: error code (0:OK, -1:error)
535
        """
536
        self.lock()
537
        self._scanning_device.lock()
538
        if self.initialize_image() < 0:
539
            self._scanning_device.unlock()
540
            self.unlock()
541
            return -1
542
543
        returnvalue = self._scanning_device.set_up_scanner_clock(clock_frequency=self._clock_frequency)
544
        if returnvalue < 0:
545
            self._scanning_device.unlock()
546
            self.unlock()
547
            self.set_position('scanner')
548
            return
549
550
        returnvalue = self._scanning_device.set_up_scanner()
551
        if returnvalue < 0:
552
            self._scanning_device.unlock()
553
            self.unlock()
554
            self.set_position('scanner')
555
            return
556
557
        self.signal_scan_lines_next.emit()
558
        return 0
559
560
    def continue_scanner(self):
561
        """Continue the scanning procedure
562
563
        @return int: error code (0:OK, -1:error)
564
        """
565
        self.lock()
566
        self._scanning_device.lock()
567
        self._scanning_device.set_up_scanner_clock(clock_frequency=self._clock_frequency)
568
        self._scanning_device.set_up_scanner()
569
        self.signal_scan_lines_next.emit()
570
        return 0
571
572
    def kill_scanner(self):
573
        """Closing the scanner device.
574
575
        @return int: error code (0:OK, -1:error)
576
        """
577
        try:
578
            self._scanning_device.close_scanner()
579
            self._scanning_device.close_scanner_clock()
580
        except Exception as e:
581
            self.log.exception('Could not even close the scanner, giving up.')
582
            raise e
583
        try:
584
            self._scanning_device.unlock()
585
        except Exception as e:
586
            self.log.exception('Could not unlock scanning device.')
587
588
        return 0
589
590
    def set_position(self, tag, x=None, y=None, z=None, a=None):
591
        """Forwarding the desired new position from the GUI to the scanning device.
592
593
        @param string tag: TODO
594
595
        @param float x: if defined, changes to postion in x-direction (microns)
596
        @param float y: if defined, changes to postion in y-direction (microns)
597
        @param float z: if defined, changes to postion in z-direction (microns)
598
        @param float a: if defined, changes to postion in a-direction (microns)
599
600
        @return int: error code (0:OK, -1:error)
601
        """
602
        # print(tag, x, y, z)
603
        # Changes the respective value
604
        if x is not None:
605
            self._current_x = x
606
        if y is not None:
607
            self._current_y = y
608
        if z is not None:
609
            self._current_z = z
610
611
        # Checks if the scanner is still running
612
        if self.getState() == 'locked' or self._scanning_device.getState() == 'locked':
613
            return -1
614
        else:
615
            self._change_position(tag)
616
            self.signal_change_position.emit(tag)
617
            return 0
618
619
620
    def _change_position(self, tag):
621
        """ Threaded method to change the hardware position.
622
623
        @return int: error code (0:OK, -1:error)
624
        """
625
        # if tag == 'optimizer' or tag == 'scanner' or tag == 'activation':
626
        self._scanning_device.scanner_set_position(
627
            x=self._current_x,
628
            y=self._current_y,
629
            z=self._current_z,
630
            a=self._current_a
631
        )
632
        return 0
633
634
635
    def get_position(self):
636
        """Forwarding the desired new position from the GUI to the scanning device.
637
638
        @return list: with three entries x, y and z denoting the current
639
                      position in microns
640
        """
641
        #FIXME: change that to SI units!
642
        return self._scanning_device.get_scanner_position()[:3]
643
644
645
    def _scan_line(self):
646
        """scanning an image in either depth or xy
647
648
        """
649
        # TODO: change z_values, if z is changed during scan!
650
        # stops scanning
651
        if self.stopRequested:
652
            with self.threadlock:
653
                self.kill_scanner()
654
                self.stopRequested = False
655
                self.unlock()
656
                self.signal_xy_image_updated.emit()
657
                self.signal_depth_image_updated.emit()
658
                self.set_position('scanner')
659
                if self._zscan:
660
                    self._depth_line_pos = self._scan_counter
661
                else:
662
                    self._xy_line_pos = self._scan_counter
663
                # add new history entry
664
                new_history = ConfocalHistoryEntry(self)
665
                new_history.snapshot(self)
666
                self.history.append(new_history)
667
                if len(self.history) > self.max_history_length:
668
                    self.history.pop(0)
669
                self.history_index = len(self.history) - 1
670
                return
671
672
        if self._zscan:
673
            if self.TiltCorrection:
674
                image = copy(self.depth_image)
675
                image[:, :, 2] += self._calc_dz(x=image[:, :, 0], y=image[:, :, 1])
676
            else:
677
                image = self.depth_image
678
        else:
679
            if self.TiltCorrection:
680
                image = copy(self.xy_image)
681
                image[:, :, 2] += self._calc_dz(x=image[:, :, 0], y=image[:, :, 1])
682
            else:
683
                image = self.xy_image
684
685
        try:
686
            if self._scan_counter == 0:
687
                # defines trace of positions for single line scan
688
                start_line = np.vstack((
689
                    np.linspace(self._current_x, image[self._scan_counter, 0, 0], self.return_slowness),
690
                    np.linspace(self._current_y, image[self._scan_counter, 0, 1], self.return_slowness),
691
                    np.linspace(self._current_z, image[self._scan_counter, 0, 2], self.return_slowness),
692
                    np.linspace(self._current_a, 0, self.return_slowness)
693
                    ))
694
                # scan of a single line
695
                start_line_counts = self._scanning_device.scan_line(start_line)
696
            # defines trace of positions for a single line scan
697
            line = np.vstack((image[self._scan_counter, :, 0],
698
                              image[self._scan_counter, :, 1],
699
                              image[self._scan_counter, :, 2],
700
                              image[self._scan_counter, :, 3]))
701
            # scan of a single line
702
            line_counts = self._scanning_device.scan_line(line)
703
            # defines trace of positions for a single return line scan
704
            if self.depth_scan_dir_is_xz:
705
                return_line = np.vstack((
706
                    self._return_XL,
707
                    image[self._scan_counter, 0, 1] * np.ones(self._return_XL.shape),
708
                    image[self._scan_counter, 0, 2] * np.ones(self._return_XL.shape),
709
                    self._return_AL
710
                    ))
711
            else:
712
                return_line = np.vstack((
713
                    image[self._scan_counter, 0, 1] * np.ones(self._return_YL.shape),
714
                    self._return_YL,
715
                    image[self._scan_counter, 0, 2] * np.ones(self._return_YL.shape),
716
                    self._return_AL
717
                    ))
718
719
            # scan of a single return-line
720
            # This is just needed in order to return the scanner to the start of next line
721
            return_line_counts = self._scanning_device.scan_line(return_line)
722
723
            # updating images
724
            if self._zscan:
725
                if self.depth_scan_dir_is_xz:
726
                    self.depth_image[self._scan_counter, :, 3] = line_counts
727
                else:
728
                    self.depth_image[self._scan_counter, :, 3] = line_counts
729
                self.signal_depth_image_updated.emit()
730
            else:
731
                self.xy_image[self._scan_counter, :, 3] = line_counts
732
                self.signal_xy_image_updated.emit()
733
734
        # call this again from event loop
735
            self._scan_counter += 1
736
            # stop scanning when last line scan was performed and makes scan not continuable
737
738
            if self._scan_counter >= np.size(self._image_vert_axis):
739
                if not self.permanent_scan:
740
                    self.stop_scanning()
741
                    if self._zscan:
742
                        self._zscan_continuable = False
743
                    else:
744
                        self._xyscan_continuable = False
745
                else:
746
                    self._scan_counter = 0
747
748
            self.signal_scan_lines_next.emit()
749
750
        except Exception as e:
751
            self.log.critical('The scan went wrong, killing the scanner.')
752
            self.stop_scanning()
753
            self.signal_scan_lines_next.emit()
754
            raise e
755
756
757
    def save_xy_data(self, colorscale_range=None, percentile_range=None):
758
        """ Save the current confocal xy data to file.
759
760
        Two files are created.  The first is the imagedata, which has a text-matrix of count values
761
        corresponding to the pixel matrix of the image.  Only count-values are saved here.
762
763
        The second file saves the full raw data with x, y, z, and counts at every pixel.
764
765
        A figure is also saved.
766
767
        @param: list colorscale_range (optional) The range [min, max] of the display colour scale (for the figure)
768
769
        @param: list percentile_range (optional) The percentile range [min, max] of the color scale
770
        """
771
        save_time = datetime.now()
772
773
        filepath = self._save_logic.get_path_for_module(module_name='Confocal')
774
775
        # Prepare the metadata parameters (common to both saved files):
776
        parameters = OrderedDict()
777
778
        parameters['X image min (micrometer)'] = self.image_x_range[0]
779
        parameters['X image max (micrometer)'] = self.image_x_range[1]
780
        parameters['X image range (micrometer)'] = self.image_x_range[1] - self.image_x_range[0]
781
782
        parameters['Y image min'] = self.image_y_range[0]
783
        parameters['Y image max'] = self.image_y_range[1]
784
        parameters['Y image range'] = self.image_y_range[1] - self.image_y_range[0]
785
786
        parameters['XY resolution (samples per range)'] = self.xy_resolution
787
        parameters['XY Image at z position (micrometer)'] = self._current_z
788
789
        parameters['Clock frequency of scanner (Hz)'] = self._clock_frequency
790
        parameters['Return Slowness (Steps during retrace line)'] = self.return_slowness
791
792
        # data for the text-array "image":
793
        image_data = OrderedDict()
794
        image_data['Confocal pure XY scan image data without axis.\n'
795
                   '# The upper left entry represents the signal at the upper '
796
                   'left pixel position.\n'
797
                   '# A pixel-line in the image corresponds to a row '
798
                   'of entries where the Signal is in counts/s:'] = self.xy_image[:,:,3]
799
800
        # Prepare a figure to be saved
801
        figure_data = self.xy_image[:,:,3]
802
        image_extent = [self.image_x_range[0],
803
                        self.image_x_range[1],
804
                        self.image_y_range[0],
805
                        self.image_y_range[1]
806
                        ]
807
        axes = ['X', 'Y']
808
        crosshair_pos = [self.get_position()[0], self.get_position()[1]]
809
810
        fig = self.draw_figure(data=figure_data,
811
                               image_extent=image_extent,
812
                               scan_axis=axes,
813
                               cbar_range=colorscale_range,
814
                               percentile_range=percentile_range,
815
                               crosshair_pos=crosshair_pos
816
                               )
817
818
        # Save the image data and figure
819
        filelabel = 'confocal_xy_image'
820
        self._save_logic.save_data(image_data,
821
                                   filepath,
822
                                   parameters=parameters,
823
                                   filelabel=filelabel,
824
                                   as_text=True,
825
                                   timestamp=save_time,
826
                                   plotfig=fig
827
                                   )
828
        #, as_xml=False, precision=None, delimiter=None)
829
        plt.close(fig)
830
831
        # prepare the full raw data in an OrderedDict:
832
        data = OrderedDict()
833
        x_data = []
834
        y_data = []
835
        z_data = []
836
        counts_data = []
837
838
        for row in self.xy_image:
839
            for entry in row:
840
                x_data.append(entry[0])
841
                y_data.append(entry[1])
842
                z_data.append(entry[2])
843
                counts_data.append(entry[3])
844
845
        data['x values (micron)'] = x_data
846
        data['y values (micron)'] = y_data
847
        data['z values (micron)'] = z_data
848
        data['count values (c/s)'] = counts_data
849
850
        # Save the raw data to file
851
        filelabel = 'confocal_xy_data'
852
        self._save_logic.save_data(data,
853
                                   filepath,
854
                                   parameters=parameters,
855
                                   filelabel=filelabel,
856
                                   as_text=True,
857
                                   timestamp=save_time
858
                                   )
859
        #, as_xml=False, precision=None, delimiter=None)
860
861
        self.log.debug('Confocal Image saved to:\n{0}'.format(filepath))
862
863
    def save_depth_data(self, colorscale_range=None, percentile_range=None):
864
        """ Save the current confocal depth data to file.
865
866
        Two files are created.  The first is the imagedata, which has a text-matrix of count values
867
        corresponding to the pixel matrix of the image.  Only count-values are saved here.
868
869
        The second file saves the full raw data with x, y, z, and counts at every pixel.
870
        """
871
        save_time = datetime.now()
872
873
        filepath = self._save_logic.get_path_for_module(module_name='Confocal')
874
875
        # Prepare the metadata parameters (common to both saved files):
876
        parameters = OrderedDict()
877
878
        # TODO: This needs to check whether the scan was XZ or YZ direction
879
        parameters['X image min (micrometer)'] = self.image_x_range[0]
880
        parameters['X image max (micrometer)'] = self.image_x_range[1]
881
        parameters['X image range (micrometer)'] = self.image_x_range[1] - self.image_x_range[0]
882
883
        parameters['Z image min'] = self.image_z_range[0]
884
        parameters['Z image max'] = self.image_z_range[1]
885
        parameters['Z image range'] = self.image_z_range[1] - self.image_z_range[0]
886
887
        parameters['XY resolution (samples per range)'] = self.xy_resolution
888
        parameters['Z resolution (samples per range)'] = self.z_resolution
889
        parameters['Depth Image at y position (micrometer)'] = self._current_y
890
891
        parameters['Clock frequency of scanner (Hz)'] = self._clock_frequency
892
        parameters['Return Slowness (Steps during retrace line)'] = self.return_slowness
893
894
        # data for the text-array "image":
895
        image_data = OrderedDict()
896
        image_data['Confocal pure depth scan image data without axis.\n'
897
                   '# The upper left entry represents the signal at the upper '
898
                   'left pixel position.\n'
899
                   '# A pixel-line in the image corresponds to a row in '
900
                   'of entries where the Signal is in counts/s:'] = self.depth_image[:,:,3]
901
902
        # Prepare a figure to be saved
903
        figure_data = self.depth_image[:,:,3]
904
905
        if self.depth_scan_dir_is_xz:
906
            horizontal_range = [self.image_x_range[0], self.image_x_range[1]]
907
            axes = ['X', 'Z']
908
            crosshair_pos = [self.get_position()[0], self.get_position()[2]]
909
        else:
910
            horizontal_range = [self.image_y_range[0], self.image_y_range[1]]
911
            axes = ['Y', 'Z']
912
            crosshair_pos = [self.get_position()[1], self.get_position()[2]]
913
914
        image_extent = [horizontal_range[0],
915
                        horizontal_range[1],
916
                        self.image_z_range[0],
917
                        self.image_z_range[1]
918
                        ]
919
920
        fig = self.draw_figure(data=figure_data,
921
                               image_extent=image_extent,
922
                               scan_axis=axes,
923
                               cbar_range=colorscale_range,
924
                               percentile_range=percentile_range,
925
                               crosshair_pos=crosshair_pos
926
                               )
927
928
        # Save the image data and figure
929
        filelabel = 'confocal_xy_image'
930
        self._save_logic.save_data(image_data,
931
                                   filepath,
932
                                   parameters=parameters,
933
                                   filelabel=filelabel,
934
                                   as_text=True,
935
                                   timestamp=save_time,
936
                                   plotfig=fig
937
                                   )
938
        #, as_xml=False, precision=None, delimiter=None)
939
        plt.close(fig)
940
941
        # prepare the full raw data in an OrderedDict:
942
        data = OrderedDict()
943
        x_data = []
944
        y_data = []
945
        z_data = []
946
        counts_data = []
947
948
        for row in self.depth_image:
949
            for entry in row:
950
                x_data.append(entry[0])
951
                y_data.append(entry[1])
952
                z_data.append(entry[2])
953
                counts_data.append(entry[3])
954
955
        data['x values (micros)'] = x_data
956
        data['y values (micros)'] = y_data
957
        data['z values (micros)'] = z_data
958
        data['count values (micros)'] = counts_data
959
960
        # Save the raw data to file
961
        filelabel = 'confocal_depth_data'
962
        self._save_logic.save_data(data,
963
                                   filepath,
964
                                   parameters=parameters,
965
                                   filelabel=filelabel,
966
                                   as_text=True,
967
                                   timestamp=save_time
968
                                   )
969
        #, as_xml=False, precision=None, delimiter=None)
970
971
        self.log.debug('Confocal Image saved to:\n{0}'.format(filepath))
972
973
    def draw_figure(self, data, image_extent, scan_axis=None, cbar_range=None, percentile_range=None,  crosshair_pos=None):
974
        """ Create a 2-D color map figure of the scan image.
975
976
        @param: array data: The NxM array of count values from a scan with NxM pixels.
977
978
        @param: list image_extent: The scan range in the form [hor_min, hor_max, ver_min, ver_max]
979
980
        @param: list axes: Names of the horizontal and vertical axes in the image
981
982
        @param: list cbar_range: (optional) [color_scale_min, color_scale_max].  If not supplied then a default of
983
                                 data_min to data_max will be used.
984
985
        @param: list percentile_range: (optional) Percentile range of the chosen cbar_range.
986
987
        @param: list crosshair_pos: (optional) crosshair position as [hor, vert] in the chosen image axes.
988
989
        @return: fig fig: a matplotlib figure object to be saved to file.
990
        """
991
        if scan_axis is None:
992
            scan_axis = ['X', 'Y']
993
994
        # If no colorbar range was given, take full range of data
995
        if cbar_range is None:
996
            cbar_range = [np.min(data), np.max(data)]
997
998
        # Scale color values using SI prefix
999
        prefix = ['', 'k', 'M', 'G']
1000
        prefix_count = 0
1001
        image_data = data
1002
        draw_cb_range = np.array(cbar_range)
1003
1004
        while draw_cb_range[1] > 1000:
1005
            image_data = image_data/1000
1006
            draw_cb_range = draw_cb_range/1000
1007
            prefix_count = prefix_count + 1
1008
1009
        c_prefix = prefix[prefix_count]
1010
1011
        # Use qudi style
1012
        plt.style.use(self._save_logic.mpl_qd_style)
1013
1014
        # Create figure
1015
        fig, ax = plt.subplots()
1016
1017
        # Create image plot
1018
        cfimage = ax.imshow(image_data,
1019
                            cmap=plt.get_cmap('inferno'), # reference the right place in qd
1020
                            origin="lower",
1021
                            vmin=draw_cb_range[0],
1022
                            vmax=draw_cb_range[1],
1023
                            interpolation='none',
1024
                            extent=image_extent
1025
                            )
1026
1027
        ax.set_aspect(1)
1028
        ax.set_xlabel(scan_axis[0] + ' position (um)')
1029
        ax.set_ylabel(scan_axis[1] + ' position (um)')
1030
        ax.spines['bottom'].set_position(('outward', 10))
1031
        ax.spines['left'].set_position(('outward', 10))
1032
        ax.spines['top'].set_visible(False)
1033
        ax.spines['right'].set_visible(False)
1034
        ax.get_xaxis().tick_bottom()
1035
        ax.get_yaxis().tick_left()
1036
1037
        # draw the crosshair position if defined
1038
        if crosshair_pos is not None:
1039
            trans_xmark = mpl.transforms.blended_transform_factory(
1040
                ax.transData,
1041
                ax.transAxes)
1042
1043
            trans_ymark = mpl.transforms.blended_transform_factory(
1044
                ax.transAxes,
1045
                ax.transData)
1046
1047
            ax.annotate('', xy=(crosshair_pos[0], 0), xytext=(crosshair_pos[0], -0.01), xycoords=trans_xmark,
1048
                        arrowprops=dict(facecolor='#17becf', shrink=0.05),
1049
                        )
1050
1051
            ax.annotate('', xy=(0, crosshair_pos[1]), xytext=(-0.01, crosshair_pos[1]), xycoords=trans_ymark,
1052
                        arrowprops=dict(facecolor='#17becf', shrink=0.05),
1053
                        )
1054
1055
        # Draw the colorbar
1056
        cbar = plt.colorbar(cfimage, shrink=0.8)#, fraction=0.046, pad=0.08, shrink=0.75)
1057
        cbar.set_label('Fluorescence (' + c_prefix + 'c/s)')
1058
1059
        # remove ticks from colorbar for cleaner image
1060
        cbar.ax.tick_params(which=u'both', length=0)
1061
1062
        # If we have percentile information, draw that to the figure
1063
        if percentile_range is not None:
1064
            cbar.ax.annotate(str(percentile_range[0]),
1065
                             xy=(-0.3, 0.0),
1066
                             xycoords='axes fraction',
1067
                             horizontalalignment='right',
1068
                             verticalalignment='center',
1069
                             rotation=90
1070
                             )
1071
            cbar.ax.annotate(str(percentile_range[1]),
1072
                             xy=(-0.3, 1.0),
1073
                             xycoords='axes fraction',
1074
                             horizontalalignment='right',
1075
                             verticalalignment='center',
1076
                             rotation=90
1077
                             )
1078
            cbar.ax.annotate('(percentile)',
1079
                             xy=(-0.3, 0.5),
1080
                             xycoords='axes fraction',
1081
                             horizontalalignment='right',
1082
                             verticalalignment='center',
1083
                             rotation=90
1084
                             )
1085
1086
        return fig
1087
1088
    ##################################### Tilit correction ########################################
1089
1090
    def set_tilt_point1(self):
1091
        """ Gets the first reference point for tilt correction."""
1092
        self.point1 = np.array(self._scanning_device.get_scanner_position()[:3])
1093
1094
    def set_tilt_point2(self):
1095
        """ Gets the second reference point for tilt correction."""
1096
        self.point2 = np.array(self._scanning_device.get_scanner_position()[:3])
1097
1098
    def set_tilt_point3(self):
1099
        """Gets the third reference point for tilt correction."""
1100
        self.point3 = np.array(self._scanning_device.get_scanner_position()[:3])
1101
1102
    def calc_tilt_correction(self):
1103
        """Calculates the values for the tilt correction."""
1104
        a = self.point2 - self.point1
1105
        b = self.point3 - self.point1
1106
        n = np.cross(a,b)
1107
        self._scanning_device.tilt_variable_ax = n[0] / n[2]
1108
        self._scanning_device.tilt_variable_ay = n[1] / n[2]
1109
1110
    def activate_tiltcorrection(self):
1111
        self._scanning_device.tiltcorrection = True
1112
        self._scanning_device.tilt_reference_x = self._scanning_device.get_scanner_position()[0]
1113
        self._scanning_device.tilt_reference_y = self._scanning_device.get_scanner_position()[1]
1114
1115
    def deactivate_tiltcorrection(self):
1116
        self._scanning_device.tiltcorrection = False
1117
        self._scanning_device.tilt_reference_x = self._scanning_device.get_scanner_position()[0]
1118
        self._scanning_device.tilt_reference_y = self._scanning_device.get_scanner_position()[1]
1119
1120 View Code Duplication
    def history_forward(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1121
        if self.history_index < len(self.history) - 1:
1122
            self.history_index += 1
1123
            self.history[self.history_index].restore(self)
1124
            self.signal_xy_image_updated.emit()
1125
            self.signal_depth_image_updated.emit()
1126
            self._change_position('history')
1127
            self.signal_change_position.emit('history')
1128
            self.signal_history_event.emit()
1129
1130 View Code Duplication
    def history_back(self):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1131
        if self.history_index > 0:
1132
            self.history_index -= 1
1133
            self.history[self.history_index].restore(self)
1134
            self.signal_xy_image_updated.emit()
1135
            self.signal_depth_image_updated.emit()
1136
            self._change_position('history')
1137
            self.signal_change_position.emit('history')
1138
            self.signal_history_event.emit()
1139