Completed
Pull Request — master (#366)
by
unknown
01:09
created

OptimizerLogic   A

Size/Duplication

Total Lines 840
Duplicated Lines 0 %

Importance

Changes 9
Bugs 1 Features 1
Metric Value
dl 0
loc 840
rs 9.7391
c 9
b 1
f 1

24 Methods

Rating   Name   Duplication   Size   Complexity  
A set_clock_frequency() 0 17 3
A kill_scanner() 0 17 3
A __init__() 0 11 1
B _initialize_z_refocus_image() 0 24 3
A set_refocus_Z_size() 0 7 1
A get_scanner_count_channels() 0 5 1
D _scan_z_line() 0 74 10
A on_activate() 0 53 2
A start_scanner() 0 20 4
A set_refocus_XY_size() 0 7 1
B z_template_fit() 0 38 2
A on_deactivate() 0 6 1
D start_refocus() 0 52 9
A xy_template_fit() 0 47 2
B finish_refocus() 0 32 3
A check_optimization_sequence() 0 10 2
A set_position() 0 16 4
A _move_to_start_pos() 0 21 3
D _do_next_optimization_step() 0 41 8
F _refocus_xy_line() 0 71 11
F do_z_optimization() 0 90 13
D _set_optimized_xy_from_fit() 0 63 10
A stop_refocus() 0 6 3
B _initialize_xy_refocus_image() 0 41 3
1
# -*- coding: utf-8 -*
2
"""
3
This file contains the Qudi logic class for optimizing scanner position.
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
import numpy as np
24
import time
25
import scipy.signal as sig
26
27
from logic.generic_logic import GenericLogic
28
from core.module import Connector, ConfigOption, StatusVar
29
from core.util.mutex import Mutex
30
31
32
class OptimizerLogic(GenericLogic):
33
34
    """This is the Logic class for optimizing scanner position on bright features.
35
    """
36
37
    _modclass = 'optimizerlogic'
38
    _modtype = 'logic'
39
40
    # declare connectors
41
    confocalscanner1 = Connector(interface='ConfocalScannerInterface')
42
    fitlogic = Connector(interface='FitLogic')
43
44
    # declare status vars
45
    _clock_frequency = StatusVar('clock_frequency', 50)
46
    return_slowness = StatusVar(default=20)
47
    _template_clock_frequency = StatusVar('clock_frequency', 50)
48
    template_return_slowness = StatusVar(default=20)
49
    refocus_XY_size = StatusVar('xy_size', 0.6e-6)
50
    optimizer_XY_res = StatusVar('xy_resolution', 10)
51
    refocus_Z_size = StatusVar('z_size', 2e-6)
52
    optimizer_Z_res = StatusVar('z_resolution', 30)
53
    hw_settle_time = StatusVar('settle_time', 0.1)
54
    optimization_sequence = StatusVar(default=['XY', 'Z'])
55
    do_surface_subtraction = StatusVar('surface_subtraction', False)
56
    surface_subtr_scan_offset = StatusVar('surface_subtraction_offset', 1e-6)
57
    opt_channel = StatusVar('optimization_channel', 0)
58
    fit_type = StatusVar('fit_type', 'normal')
59
    template_cursor = StatusVar('template_cursor', default=[0, 0, 0, 0])
60
    xy_template_image = StatusVar('xy_template_image', np.zeros(1))
61
    z_template_data = StatusVar('z_template_data', np.zeros(1))
62
    zimage_template_Z_values = StatusVar('zimage_template_Z_values', np.zeros(1))
63
64
    # "private" signals to keep track of activities here in the optimizer logic
65
    _sigScanNextXyLine = QtCore.Signal()
66
    _sigScanZLine = QtCore.Signal()
67
    _sigCompletedXyOptimizerScan = QtCore.Signal()
68
    _sigDoNextOptimizationStep = QtCore.Signal()
69
    _sigFinishedAllOptimizationSteps = QtCore.Signal()
70
71
    # public signals
72
    sigImageUpdated = QtCore.Signal()
73
    sigRefocusStarted = QtCore.Signal(str)
74
    sigRefocusXySizeChanged = QtCore.Signal()
75
    sigRefocusZSizeChanged = QtCore.Signal()
76
    sigRefocusFinished = QtCore.Signal(str, list)
77
    sigClockFrequencyChanged = QtCore.Signal(int)
78
    sigPositionChanged = QtCore.Signal(float, float, float)
79
80
    def __init__(self, config, **kwargs):
81
        super().__init__(config=config, **kwargs)
82
83
        # locking for thread safety
84
        self.threadlock = Mutex()
85
86
        self.stopRequested = False
87
        self.is_crosshair = True
88
89
        # Keep track of who called the refocus
90
        self._caller_tag = ''
91
92
    def on_activate(self):
93
        """ Initialisation performed during activation of the module.
94
95
        @return int: error code (0:OK, -1:error)
96
        """
97
        self._scanning_device = self.confocalscanner1()
98
        self._fit_logic = self.fitlogic()
99
100
        # Reads in the maximal scanning range. The unit of that scan range is micrometer!
101
        self.x_range = self._scanning_device.get_position_range()[0]
102
        self.y_range = self._scanning_device.get_position_range()[1]
103
        self.z_range = self._scanning_device.get_position_range()[2]
104
105
        self._initial_pos_x = 0.
106
        self._initial_pos_y = 0.
107
        self._initial_pos_z = 0.
108
        self.optim_pos_x = self._initial_pos_x
109
        self.optim_pos_y = self._initial_pos_y
110
        self.optim_pos_z = self._initial_pos_z
111
        self.optim_sigma_x = 0.
112
        self.optim_sigma_y = 0.
113
        self.optim_sigma_z = 0.
114
115
        self._max_offset = 3.
116
117
        # Sets the current position to the center of the maximal scanning range
118
        self._current_x = (self.x_range[0] + self.x_range[1]) / 2
119
        self._current_y = (self.y_range[0] + self.y_range[1]) / 2
120
        self._current_z = (self.z_range[0] + self.z_range[1]) / 2
121
        self._current_a = 0.0
122
123
        ###########################
124
        # Fit Params and Settings #
125
        model, params = self._fit_logic.make_gaussianlinearoffset_model()
126
        self.z_params = params
127
        self.use_custom_params = {name: False for name, param in params.items()}
128
129
        # Initialization of internal counter for scanning
130
        self._xy_scan_line_count = 0
131
132
        # Initialization of optimization sequence step counter
133
        self._optimization_step = 0
134
135
        # Sets connections between signals and functions
136
        self._sigScanNextXyLine.connect(self._refocus_xy_line, QtCore.Qt.QueuedConnection)
137
        self._sigScanZLine.connect(self.do_z_optimization, QtCore.Qt.QueuedConnection)
138
        self._sigCompletedXyOptimizerScan.connect(self._set_optimized_xy_from_fit, QtCore.Qt.QueuedConnection)
139
140
        self._sigDoNextOptimizationStep.connect(self._do_next_optimization_step, QtCore.Qt.QueuedConnection)
141
        self._sigFinishedAllOptimizationSteps.connect(self.finish_refocus)
142
        self._initialize_xy_refocus_image()
143
        self._initialize_z_refocus_image()
144
        return 0
145
146
    def on_deactivate(self):
147
        """ Reverse steps of activation
148
149
        @return int: error code (0:OK, -1:error)
150
        """
151
        return 0
152
153
    def check_optimization_sequence(self):
154
        """ Check the sequence of scan events for the optimization.
155
        """
156
157
        # Check the supplied optimization sequence only contains 'XY' and 'Z'
158
        if len(set(self.optimization_sequence).difference({'XY', 'Z'})) > 0:
159
            self.log.error('Requested optimization sequence contains unknown steps. Please provide '
160
                           'a sequence containing only \'XY\' and \'Z\' strings. '
161
                           'The default [\'XY\', \'Z\'] will be used.')
162
            self.optimization_sequence = ['XY', 'Z']
163
164
    def get_scanner_count_channels(self):
165
        """ Get lis of counting channels from scanning device.
166
          @return list(str): names of counter channels
167
        """
168
        return self._scanning_device.get_scanner_count_channels()
169
170
    def set_clock_frequency(self, clock_frequency, template_clock_frequency=-1):
171
        """Sets the frequency of the clock
172
173
        @param int clock_frequency: desired frequency of the clock
174
        @param int template_clock_frequency: clock frequency for the fitting template image
175
176
        @return int: error code (0:OK, -1:error)
177
        """
178
        # checks if scanner is still running
179
        if self.module_state() == 'locked':
180
            return -1
181
        else:
182
            self._clock_frequency = int(clock_frequency)
183
            if template_clock_frequency > 0:
184
                self._template_clock_frequency = int(template_clock_frequency)
185
        self.sigClockFrequencyChanged.emit(self._clock_frequency)
186
        return 0
187
188
    def set_refocus_XY_size(self, size):
189
        """ Set the number of pixels in the refocus image for X and Y directions
190
191
            @param int size: XY image size in pixels
192
        """
193
        self.refocus_XY_size = size
194
        self.sigRefocusXySizeChanged.emit()
195
196
    def set_refocus_Z_size(self, size):
197
        """ Set the number of values for Z refocus
198
199
            @param int size: number of values for Z refocus
200
        """
201
        self.refocus_Z_size = size
202
        self.sigRefocusZSizeChanged.emit()
203
204
    def start_refocus(self, initial_pos=None, caller_tag='unknown', tag='logic'):
205
        """ Starts the optimization scan around initial_pos
206
207
            @param list initial_pos: with the structure [float, float, float]
208
            @param str caller_tag:
209
            @param str tag:
210
        """
211
        # checking if refocus corresponding to crosshair or corresponding to initial_pos
212
213
214
        if isinstance(initial_pos, (np.ndarray,)) and initial_pos.size >= 3:
215
            self._initial_pos_x, self._initial_pos_y, self._initial_pos_z = initial_pos[0:3]
216
        elif isinstance(initial_pos, (list, tuple)) and len(initial_pos) >= 3:
217
            self._initial_pos_x, self._initial_pos_y, self._initial_pos_z = initial_pos[0:3]
218
        elif initial_pos is None:
219
            scpos = self._scanning_device.get_scanner_position()[0:3]
220
            self._initial_pos_x, self._initial_pos_y, self._initial_pos_z = scpos
221
        else:
222
            pass  # TODO: throw error
223
224
        # if the template has a cursor shift, it needs to be subtracted before scanning
225
        if self.fit_type in ('xy_template', 'all_template'):
226
            self._initial_pos_x = self._initial_pos_x - self.template_cursor[0]
227
            self._initial_pos_y = self._initial_pos_y - self.template_cursor[1]
228
        if self.fit_type in ('z_template', 'all_template'):
229
            self._initial_pos_z = self._initial_pos_z - self.template_cursor[2]
230
231
        # Keep track of where the start_refocus was initiated
232
        self._caller_tag = caller_tag
233
234
        # Set the optim_pos values to match the initial_pos values.
235
        # This means we can use optim_pos in subsequent steps and ensure
236
        # that we benefit from any completed optimization step.
237
        self.optim_pos_x = self._initial_pos_x
238
        self.optim_pos_y = self._initial_pos_y
239
        self.optim_pos_z = self._initial_pos_z
240
        self.optim_sigma_x = 0.
241
        self.optim_sigma_y = 0.
242
        self.optim_sigma_z = 0.
243
        #
244
        self._xy_scan_line_count = 0
245
        self._optimization_step = 0
246
        self.check_optimization_sequence()
247
248
        scanner_status = self.start_scanner()
249
        if scanner_status < 0:
250
            self.sigRefocusFinished.emit(
251
                self._caller_tag,
252
                [self.optim_pos_x, self.optim_pos_y, self.optim_pos_z, 0])
253
            return
254
        self.sigRefocusStarted.emit(tag)
255
        self._sigDoNextOptimizationStep.emit()
256
257
    def stop_refocus(self):
258
        """Stops refocus."""
259
        with self.threadlock:
260
            self.stopRequested = True
261
            if self.stopRequested:
262
                self._sigScanNextXyLine.emit()
263
264
    def _initialize_xy_refocus_image(self):
265
        """Initialisation of the xy refocus image."""
266
        self._xy_scan_line_count = 0
267
268
        # Take optim pos as center of refocus image, to benefit from any previous
269
        # optimization steps that have occurred.
270
        x0 = self.optim_pos_x
271
        y0 = self.optim_pos_y
272
        z0 = self.optim_pos_z
273
274
        # defining position intervals for refocus
275
        xmin = np.clip(x0 - 0.5 * self.refocus_XY_size, self.x_range[0], self.x_range[1])
276
        xmax = np.clip(x0 + 0.5 * self.refocus_XY_size, self.x_range[0], self.x_range[1])
277
        ymin = np.clip(y0 - 0.5 * self.refocus_XY_size, self.y_range[0], self.y_range[1])
278
        ymax = np.clip(y0 + 0.5 * self.refocus_XY_size, self.y_range[0], self.y_range[1])
279
280
        self._X_values = np.linspace(xmin, xmax, num=self.optimizer_XY_res)
281
        self._Y_values = np.linspace(ymin, ymax, num=self.optimizer_XY_res)
282
        self._Z_values = z0 * np.ones(self._X_values.shape)
283
        self._A_values = np.zeros(self._X_values.shape)
284
        self._return_X_values = np.linspace(xmax, xmin, num=self.optimizer_XY_res)
285
        self._return_A_values = np.zeros(self._return_X_values.shape)
286
287
        self.xy_refocus_image = np.zeros((
288
            len(self._Y_values),
289
            len(self._X_values),
290
            3 + len(self.get_scanner_count_channels())))
291
        self.xy_refocus_image[:, :, 0] = np.full((len(self._Y_values), len(self._X_values)), self._X_values)
292
        y_value_matrix = np.full((len(self._X_values), len(self._Y_values)), self._Y_values)
293
        self.xy_refocus_image[:, :, 1] = y_value_matrix.transpose()
294
        self.xy_refocus_image[:, :, 2] = z0 * np.ones((len(self._Y_values), len(self._X_values)))
295
296
        if self._caller_tag == 'xy_template_image' or np.max(self.xy_template_image) == 0:
297
            self.xy_template_image = np.zeros((
298
                len(self._Y_values),
299
                len(self._X_values),
300
                3 + len(self.get_scanner_count_channels())))
301
            self.xy_template_image[:, :, 0] = np.full((len(self._Y_values), len(self._X_values)), self._X_values)
302
            y_value_matrix = np.full((len(self._X_values), len(self._Y_values)), self._Y_values)
303
            self.xy_template_image[:, :, 1] = y_value_matrix.transpose()
304
            self.xy_template_image[:, :, 2] = z0 * np.ones((len(self._Y_values), len(self._X_values)))
305
306
    def _initialize_z_refocus_image(self):
307
        """Initialisation of the z refocus image."""
308
        self._xy_scan_line_count = 0
309
310
        # Take optim pos as center of refocus image, to benefit from any previous
311
        # optimization steps that have occurred.
312
        z0 = self.optim_pos_z
313
314
        zmin = np.clip(z0 - 0.5 * self.refocus_Z_size, self.z_range[0], self.z_range[1])
315
        zmax = np.clip(z0 + 0.5 * self.refocus_Z_size, self.z_range[0], self.z_range[1])
316
317
        self._zimage_Z_values = np.linspace(zmin, zmax, num=self.optimizer_Z_res)
318
        self._fit_zimage_Z_values = np.linspace(zmin, zmax, num=self.optimizer_Z_res)
319
        self._zimage_A_values = np.zeros(self._zimage_Z_values.shape)
320
        self.z_refocus_line = np.zeros((
321
            len(self._zimage_Z_values),
322
            len(self.get_scanner_count_channels())))
323
        self.z_fit_data = np.zeros(len(self._fit_zimage_Z_values))
324
325
        if self._caller_tag == 'z_template_image' or np.max(self.z_template_data) == 0:
326
            self.zimage_template_Z_values = np.linspace(zmin-z0, zmax-z0, num=self.optimizer_Z_res)
327
            self.z_template_data = np.zeros((
328
                len(self.zimage_template_Z_values),
329
                len(self.get_scanner_count_channels())))
330
331
    def _move_to_start_pos(self, start_pos):
332
        """Moves the scanner from its current position to the start position of the optimizer scan.
333
334
        @param start_pos float[]: 3-point vector giving x, y, z position to go to.
335
        """
336
        n_ch = len(self._scanning_device.get_scanner_axes())
337
        scanner_pos = self._scanning_device.get_scanner_position()
338
        lsx = np.linspace(scanner_pos[0], start_pos[0], self.return_slowness)
339
        lsy = np.linspace(scanner_pos[1], start_pos[1], self.return_slowness)
340
        lsz = np.linspace(scanner_pos[2], start_pos[2], self.return_slowness)
341
        if n_ch <= 3:
342
            move_to_start_line = np.vstack((lsx, lsy, lsz)[0:n_ch])
343
        else:
344
            move_to_start_line = np.vstack((lsx, lsy, lsz, np.ones(lsx.shape) * scanner_pos[3]))
345
346
        counts = self._scanning_device.scan_line(move_to_start_line)
347
        if np.any(counts == -1):
348
            return -1
349
350
        time.sleep(self.hw_settle_time)
351
        return 0
352
353
    def _refocus_xy_line(self):
354
        """Scanning a line of the xy optimization image.
355
        This method repeats itself using the _sigScanNextXyLine
356
        until the xy optimization image is complete.
357
        """
358
        n_ch = len(self._scanning_device.get_scanner_axes())
359
        # stop scanning if instructed
360
        if self.stopRequested:
361
            with self.threadlock:
362
                self.stopRequested = False
363
                self.finish_refocus()
364
                self.sigImageUpdated.emit()
365
                return
366
367
        # move to the start of the first line
368
        if self._xy_scan_line_count == 0:
369
            status = self._move_to_start_pos([self.xy_refocus_image[0, 0, 0],
370
                                              self.xy_refocus_image[0, 0, 1],
371
                                              self.xy_refocus_image[0, 0, 2]])
372
            if status < 0:
373
                self.log.error('Error during move to starting point.')
374
                self.stop_refocus()
375
                self._sigScanNextXyLine.emit()
376
                return
377
378
        lsx = self.xy_refocus_image[self._xy_scan_line_count, :, 0]
379
        lsy = self.xy_refocus_image[self._xy_scan_line_count, :, 1]
380
        lsz = self.xy_refocus_image[self._xy_scan_line_count, :, 2]
381
382
        # scan a line of the xy optimization image
383
        if n_ch <= 3:
384
            line = np.vstack((lsx, lsy, lsz)[0:n_ch])
385
        else:
386
            line = np.vstack((lsx, lsy, lsz, np.zeros(lsx.shape)))
387
388
        line_counts = self._scanning_device.scan_line(line)
389
        if np.any(line_counts == -1):
390
            self.log.error('The scan went wrong, killing the scanner.')
391
            self.stop_refocus()
392
            self._sigScanNextXyLine.emit()
393
            return
394
395
        lsx = self._return_X_values
396
        lsy = self.xy_refocus_image[self._xy_scan_line_count, 0, 1] * np.ones(lsx.shape)
397
        lsz = self.xy_refocus_image[self._xy_scan_line_count, 0, 2] * np.ones(lsx.shape)
398
        if n_ch <= 3:
399
            return_line = np.vstack((lsx, lsy, lsz))
400
        else:
401
            return_line = np.vstack((lsx, lsy, lsz, np.zeros(lsx.shape)))
402
403
        return_line_counts = self._scanning_device.scan_line(return_line)
404
        if np.any(return_line_counts == -1):
405
            self.log.error('The scan went wrong, killing the scanner.')
406
            self.stop_refocus()
407
            self._sigScanNextXyLine.emit()
408
            return
409
410
        s_ch = len(self.get_scanner_count_channels())
411
        self.xy_refocus_image[self._xy_scan_line_count, :, 3:3 + s_ch] = line_counts
412
413
        if self._caller_tag == 'xy_template_image':
414
            self.xy_template_image[self._xy_scan_line_count, :, 3:3 + s_ch] = line_counts
415
416
        self.sigImageUpdated.emit()
417
418
        self._xy_scan_line_count += 1
419
420
        if self._xy_scan_line_count < np.size(self._Y_values):
421
            self._sigScanNextXyLine.emit()
422
        else:
423
            self._sigCompletedXyOptimizerScan.emit()
424
425
    def xy_template_fit(self, xy_axes, data, template):
426
427
        # check dimensionality of template against optimizer
428
        if np.shape(data) != np.shape(template):
429
            self.log.warn('XY template fit: The length of data ({0:d}) and template ({1:d}) are unequal.\n'
430
                          'I really hope you know what you are doing here but will calculate the convolution anyways.'
431
                          ''.format(len(data), len(template)))
432
433
        fit_template = np.flipud(np.fliplr(template))
434
        fit_data = data
435
436
        # It turns out the best results are achieved when the convolutions fills 
437
        # the edges with 70% of the mean of the whole picture.
438
        convoluted_image = sig.convolve2d(fit_template,
439
                                          fit_data,
440
                                          mode='full',
441
                                          fillvalue=fit_template.min() + 0.7*(np.mean(fit_template)-fit_template.min())
442
                                          )
443
444
        # get the dimensions in order
445
        (x, y) = xy_axes
446
        x0, y0 = self.optimizer_XY_res, self.optimizer_XY_res
447
        xc, yc = convoluted_image.shape[0], convoluted_image.shape[1]
448
449
        # shift of picture 2 with respect to picture 1
450
        max_index = np.array(np.unravel_index(convoluted_image.argmax(), convoluted_image.shape))
451
        image_index_shift = [max_index[1] - (xc / 2.)+0.5,
452
                             max_index[0] - (yc / 2.)+0.5]
453
454
        # recalculate real coordinate shift from index shift (add 0.5 pixel to hit the middle)
455
        image_shift = [(image_index_shift[0]) / x0 * (x.max() - x.min()),
456
                       (image_index_shift[1]) / y0 * (y.max() - y.min())]
457
458
        # TODO: This is a quick and dirty way to emulate the output from a fit
459
        class _param():
460
            best_values = dict()
461
            success = False
462
463
        results = _param()
464
        results.best_values['center_x'] = self.optim_pos_x + image_shift[0]
465
        results.best_values['center_y'] = self.optim_pos_y + image_shift[1]
466
        # sigma is set to represent an uncertainty of one pixel in the template
467
        results.best_values['sigma_x'] = 1 / x0 * (x.max() - x.min())
468
        results.best_values['sigma_y'] = 1 / y0 * (y.max() - y.min())
469
        results.success = True
470
471
        return results
472
473
    def _set_optimized_xy_from_fit(self):
474
        """Fit the completed xy optimizer scan and set the optimized xy position."""
475
476
        # for acquiring the template image, no fit needs to be done, so return the initial position
477
        if self._caller_tag == 'xy_template_image':
478
            self.optim_pos_x = self._initial_pos_x
479
            self.optim_pos_y = self._initial_pos_y
480
            self.optim_sigma_x = 0.
481
            self.optim_sigma_y = 0.
482
483
            # emit image updated signal so crosshair can be updated from this fit
484
            self.sigImageUpdated.emit()
485
            self._sigDoNextOptimizationStep.emit()
486
            return
487
488
        fit_x, fit_y = np.meshgrid(self._X_values, self._Y_values)
489
        xy_fit_data = self.xy_refocus_image[:, :, 3].ravel()
490
        axes = np.empty((len(self._X_values) * len(self._Y_values), 2))
491
        axes = (fit_x.flatten(), fit_y.flatten())
492
493
        if self.fit_type not in ('xy_template', 'all_template'):
494
            result_2D_gaus = self._fit_logic.make_twoDgaussian_fit(
495
                xy_axes=axes,
496
                data=xy_fit_data,
497
                estimator=self._fit_logic.estimate_twoDgaussian_MLE
498
            )
499
        else:
500
            xy_fit_data = self.xy_refocus_image[:, :, 3 + self.opt_channel]
501
            xy_template_data = self.xy_template_image[:, :, 3 + self.opt_channel]
502
503
            result_2D_gaus = self.xy_template_fit(
504
                xy_axes=axes,
505
                data=xy_fit_data,
506
                template=xy_template_data
507
            )
508
            # print(result_2D_gaus.fit_report())
509
510
        if result_2D_gaus.success is False:
511
            self.log.error('Error: 2D Gaussian Fit was not successfull!.')
512
            print('2D gaussian fit not successfull')
513
            self.optim_pos_x = self._initial_pos_x
514
            self.optim_pos_y = self._initial_pos_y
515
            self.optim_sigma_x = 0.
516
            self.optim_sigma_y = 0.
517
            # hier abbrechen
518
        else:
519
            #                @reviewer: Do we need this. With constraints not one of these cases will be possible....
520
            if abs(self._initial_pos_x - result_2D_gaus.best_values['center_x']) < self._max_offset and abs(self._initial_pos_x - result_2D_gaus.best_values['center_x']) < self._max_offset:
521
                if result_2D_gaus.best_values['center_x'] >= self.x_range[0] and result_2D_gaus.best_values['center_x'] <= self.x_range[1]:
522
                    if result_2D_gaus.best_values['center_y'] >= self.y_range[0] and result_2D_gaus.best_values['center_y'] <= self.y_range[1]:
523
                        self.optim_pos_x = result_2D_gaus.best_values['center_x']
524
                        self.optim_pos_y = result_2D_gaus.best_values['center_y']
525
                        self.optim_sigma_x = result_2D_gaus.best_values['sigma_x']
526
                        self.optim_sigma_y = result_2D_gaus.best_values['sigma_y']
527
            else:
528
                self.optim_pos_x = self._initial_pos_x
529
                self.optim_pos_y = self._initial_pos_y
530
                self.optim_sigma_x = 0.
531
                self.optim_sigma_y = 0.
532
533
        # emit image updated signal so crosshair can be updated from this fit
534
        self.sigImageUpdated.emit()
535
        self._sigDoNextOptimizationStep.emit()
536
537
    def z_template_fit(self, x_axis, data, template):
538
539
        # check dimensionality of template against optimizer
540
        if len(data) != len(template):
541
            self.log.warn('Z template fit: The length of data ({0:d}) and template ({1:d}) are unequal.\n'
542
                          'I really hope you know what you are doing here but will calculate the convolution anyways.'
543
                          ''.format(len(data), len(template)))
544
545
        fit_template = template
546
        fit_data = np.flip(data, 0)
547
548
        convoluted = sig.convolve(fit_data,
549
                                  fit_template,
550
                                  mode='full'
551
                                  )
552
553
        # shift of picture 2 with respect to picture 1
554
        max_index = convoluted.argmax()
555
556
        template_size = len(fit_template)
557
        conv_size = len(convoluted)
558
559
        # recalculate real coordinate shift from index shift (add 0.5 pixel to hit the middle)
560
        z_index_shift = max_index - (conv_size / 2.)+0.5
561
        z_shift = z_index_shift / template_size * (x_axis.max() - x_axis.min())
562
563
        # TODO: This is a quick and dirty way to emulate the output from a fit
564
        class _param():
565
            best_values = dict()
566
            success = False
567
            params = dict()
568
569
        results = _param()
570
        results.best_values['center'] = self.optim_pos_z - z_shift
571
        results.best_values['sigma'] = 0
572
        results.success = True
573
574
        return results
575
576
    def do_z_optimization(self):
577
        """ Do the z axis optimization."""
578
        # z scaning
579
        self._scan_z_line()
580
581
        # the template does not need a fit
582
        if self._caller_tag == 'z_template_image':
583
            self.optim_pos_z = self._initial_pos_z
584
            self.optim_sigma_z = 0.
585
            self.sigImageUpdated.emit()
586
            self._sigDoNextOptimizationStep.emit()
587
            return
588
589
        if self.fit_type in ('z_template', 'all_template'):
590
            result = self.z_template_fit(
591
                x_axis=self._zimage_Z_values,
592
                data=self.z_refocus_line[:, self.opt_channel],
593
                template=self.z_template_data[:, self.opt_channel]
594
            )
595
        else:
596
597
            # z-fit
598
            # If subtracting surface, then data can go negative and the gaussian fit offset constraints need to be adjusted
599
            if self.do_surface_subtraction:
600
                adjusted_param = {}
601
                adjusted_param['offset'] = {
602
                    'value': 1e-12,
603
                    'min': -self.z_refocus_line[:, self.opt_channel].max(),
604
                    'max': self.z_refocus_line[:, self.opt_channel].max()
605
                }
606
                result = self._fit_logic.make_gausspeaklinearoffset_fit(
607
                    x_axis=self._zimage_Z_values,
608
                    data=self.z_refocus_line[:, self.opt_channel],
609
                    add_params=adjusted_param)
610
            else:
611
                if any(self.use_custom_params.values()):
612
                    result = self._fit_logic.make_gausspeaklinearoffset_fit(
613
                        x_axis=self._zimage_Z_values,
614
                        data=self.z_refocus_line[:, self.opt_channel],
615
                        # Todo: It is required that the changed parameters are given as a dictionary or parameter object
616
                        add_params=None)
617
                else:
618
                    result = self._fit_logic.make_gaussianlinearoffset_fit(
619
                        x_axis=self._zimage_Z_values,
620
                        data=self.z_refocus_line[:, self.opt_channel],
621
                        units='m',
622
                        estimator=self._fit_logic.estimate_gaussianlinearoffset_peak
623
                        )
624
        self.z_params = result.params
625
626
        if result.success is False:
627
            self.log.error('error in 1D Gaussian Fit.')
628
            self.optim_pos_z = self._initial_pos_z
629
            self.optim_sigma_z = 0.
630
            # interrupt here?
631
        else:  # move to new position
632
            #                @reviewer: Do we need this. With constraints not one of these cases will be possible....
633
            # checks if new pos is too far away
634
            if abs(self._initial_pos_z - result.best_values['center']) < self._max_offset:
635
                # checks if new pos is within the scanner range
636
                if result.best_values['center'] >= self.z_range[0] and result.best_values['center'] <= self.z_range[1]:
637
                    self.optim_pos_z = result.best_values['center']
638
                    self.optim_sigma_z = result.best_values['sigma']
639
640
                    # for the template fit, the plot of the fit is just the template
641
                    if self.fit_type in ('z_template', 'all_template'):
642
                        self.z_fit_data = self.z_template_data[:, self.opt_channel]
643
                    # for a normal fit, sample the function and plot it
644
                    else:
645
                        gauss, params = self._fit_logic.make_gaussianlinearoffset_model()
646
                        self.z_fit_data = gauss.eval(
647
                            x=self._fit_zimage_Z_values, params=result.params)
648
                else:  # new pos is too far away
649
                    # checks if new pos is too high
650
                    self.optim_sigma_z = 0.
651
                    if result.best_values['center'] > self._initial_pos_z:
652
                        if self._initial_pos_z + 0.5 * self.refocus_Z_size <= self.z_range[1]:
653
                            # moves to higher edge of scan range
654
                            self.optim_pos_z = self._initial_pos_z + 0.5 * self.refocus_Z_size
655
                        else:
656
                            self.optim_pos_z = self.z_range[1]  # moves to highest possible value
657
                    else:
658
                        if self._initial_pos_z + 0.5 * self.refocus_Z_size >= self.z_range[0]:
659
                            # moves to lower edge of scan range
660
                            self.optim_pos_z = self._initial_pos_z + 0.5 * self.refocus_Z_size
661
                        else:
662
                            self.optim_pos_z = self.z_range[0]  # moves to lowest possible value
663
664
        self.sigImageUpdated.emit()
665
        self._sigDoNextOptimizationStep.emit()
666
667
    def finish_refocus(self):
668
        """ Finishes up and releases hardware after the optimizer scans."""
669
670
        n_ch = len(self._scanning_device.get_scanner_axes())
671
672
        self.kill_scanner()
673
674
        if self.fit_type in ('xy_template', 'all_template'):
675
            self._initial_pos_x += self.template_cursor[0]
676
            self._initial_pos_y += self.template_cursor[1]
677
            self.optim_pos_x += self.template_cursor[0]
678
            self.optim_pos_y += self.template_cursor[1]
679
680
        if self.fit_type in ('z_template', 'all_template'):
681
            self._initial_pos_z += self.template_cursor[2]
682
            self.optim_pos_z += self.template_cursor[2]
683
684
        self.log.info(
685
                'Optimised from ({0:.3e},{1:.3e},{2:.3e}) to local '
686
                'maximum at ({3:.3e},{4:.3e},{5:.3e}).'.format(
687
                    self._initial_pos_x,
688
                    self._initial_pos_y,
689
                    self._initial_pos_z,
690
                    self.optim_pos_x,
691
                    self.optim_pos_y,
692
                    self.optim_pos_z))
693
694
        # Signal that the optimization has finished, and "return" the optimal position along with
695
        # caller_tag
696
        self.sigRefocusFinished.emit(
697
            self._caller_tag,
698
            [self.optim_pos_x, self.optim_pos_y, self.optim_pos_z, 0][0:n_ch])
699
700
    def _scan_z_line(self):
701
        """Scans the z line for refocus."""
702
703
        x0 = self.optim_pos_x
704
        y0 = self.optim_pos_y
705
706
        # Moves to the start value of the z-scan
707
        status = self._move_to_start_pos(
708
            [x0, y0, self._zimage_Z_values[0]])
709
        if status < 0:
710
            self.log.error('Error during move to starting point.')
711
            self.stop_refocus()
712
            return
713
714
        n_ch = len(self._scanning_device.get_scanner_axes())
715
716
        # defining trace of positions for z-refocus
717
        scan_z_line = self._zimage_Z_values
718
        scan_x_line = x0 * np.ones(self._zimage_Z_values.shape)
719
        scan_y_line = y0 * np.ones(self._zimage_Z_values.shape)
720
721
        if n_ch <= 3:
722
            line = np.vstack((scan_x_line, scan_y_line, scan_z_line)[0:n_ch])
723
        else:
724
            line = np.vstack((scan_x_line, scan_y_line, scan_z_line, np.zeros(scan_x_line.shape)))
725
726
        # Perform scan
727
        line_counts = self._scanning_device.scan_line(line)
728
        if np.any(line_counts == -1):
729
            self.log.error('Z scan went wrong, killing the scanner.')
730
            self.stop_refocus()
731
            return
732
733
        # Set the data
734
        self.z_refocus_line = line_counts
735
736
        if self._caller_tag == 'z_template_image':
737
            self.z_template_data = line_counts
738
739
740
        # If subtracting surface, perform a displaced depth line scan
741
        if self.do_surface_subtraction:
742
            # Move to start of z-scan
743
            status = self._move_to_start_pos([
744
                x0 + self.surface_subtr_scan_offset,
745
                y0,
746
                self._zimage_Z_values[0]])
747
            if status < 0:
748
                self.log.error('Error during move to starting point.')
749
                self.stop_refocus()
750
                return
751
752
            # define an offset line to measure "background"
753
            if n_ch <= 3:
754
                line_bg = np.vstack(
755
                    (scan_x_line + self.surface_subtr_scan_offset, scan_y_line, scan_z_line)[0:n_ch])
756
            else:
757
                line_bg = np.vstack(
758
                    (scan_x_line + self.surface_subtr_scan_offset,
759
                     scan_y_line,
760
                     scan_z_line,
761
                     np.zeros(scan_x_line.shape)))
762
763
            line_bg_counts = self._scanning_device.scan_line(line_bg)
764
            if np.any(line_bg_counts[0] == -1):
765
                self.log.error('The scan went wrong, killing the scanner.')
766
                self.stop_refocus()
767
                return
768
769
            # surface-subtracted line scan data is the difference
770
            self.z_refocus_line = line_counts - line_bg_counts
771
772
            if self._caller_tag == 'z_template_image':
773
                self.z_template_data = line_counts - line_bg_counts
774
775
    def start_scanner(self):
776
        """Setting up the scanner device.
777
778
        @return int: error code (0:OK, -1:error)
779
        """
780
        self.module_state.lock()
781
        clock_frequency = self._template_clock_frequency if self._caller_tag in ('xy_template_image', 'z_template_image') else self._clock_frequency
782
        clock_status = self._scanning_device.set_up_scanner_clock(
783
            clock_frequency=clock_frequency)
784
        if clock_status < 0:
785
            self.module_state.unlock()
786
            return -1
787
788
        scanner_status = self._scanning_device.set_up_scanner()
789
        if scanner_status < 0:
790
            self._scanning_device.close_scanner_clock()
791
            self.module_state.unlock()
792
            return -1
793
794
        return 0
795
796
    def kill_scanner(self):
797
        """Closing the scanner device.
798
799
        @return int: error code (0:OK, -1:error)
800
        """
801
        try:
802
            rv = self._scanning_device.close_scanner()
803
        except:
804
            self.log.exception('Closing refocus scanner failed.')
805
            return -1
806
        try:
807
            rv2 = self._scanning_device.close_scanner_clock()
808
        except:
809
            self.log.exception('Closing refocus scanner clock failed.')
810
            return -1
811
        self.module_state.unlock()
812
        return rv + rv2
813
814
    def _do_next_optimization_step(self):
815
        """Handle the steps through the specified optimization sequence
816
        """
817
        # If XY template image requested, just take a XY scan and save it as template image
818
        if self._caller_tag == 'xy_template_image':
819
            if self._optimization_step >= 1:
820
                self._sigFinishedAllOptimizationSteps.emit()
821
            else:
822
                self._optimization_step += 1
823
                self._initialize_xy_refocus_image()
824
                self._sigScanNextXyLine.emit()
825
            return
826
827
        # If Z template image requested, just take a Z scan and save it as template data
828
        if self._caller_tag == 'z_template_image':
829
            if self._optimization_step >= 1:
830
                self._sigFinishedAllOptimizationSteps.emit()
831
            else:
832
                self._optimization_step += 1
833
                self._initialize_z_refocus_image()
834
                self._sigScanZLine.emit()
835
            return
836
837
        # At the end fo the sequence, finish the optimization
838
        if self._optimization_step == len(self.optimization_sequence):
839
            self._sigFinishedAllOptimizationSteps.emit()
840
            return
841
842
        # Read the next step in the optimization sequence
843
        this_step = self.optimization_sequence[self._optimization_step]
844
845
        # Increment the step counter
846
        self._optimization_step += 1
847
848
        # Launch the next step
849
        if this_step == 'XY':
850
            self._initialize_xy_refocus_image()
851
            self._sigScanNextXyLine.emit()
852
        elif this_step == 'Z':
853
            self._initialize_z_refocus_image()
854
            self._sigScanZLine.emit()
855
856
    def set_position(self, tag, x=None, y=None, z=None, a=None):
857
        """ Set focus position.
858
859
            @param str tag: sting indicating who caused position change
860
            @param float x: x axis position in m
861
            @param float y: y axis position in m
862
            @param float z: z axis position in m
863
            @param float a: a axis position in m
864
        """
865
        if x is not None:
866
            self._current_x = x
867
        if y is not None:
868
            self._current_y = y
869
        if z is not None:
870
            self._current_z = z
871
        self.sigPositionChanged.emit(self._current_x, self._current_y, self._current_z)
872
873