Completed
Pull Request — master (#366)
by
unknown
04:51
created

OptimizerLogic._set_optimized_xy_from_fit()   D

Complexity

Conditions 10

Size

Total Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 10
dl 0
loc 63
rs 4.6153
c 5
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

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