Completed
Pull Request — master (#384)
by
unknown
02:47
created

PulsedMeasurementLogic._pulsed_analysis_loop()   C

Complexity

Conditions 7

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 2 Features 1
Metric Value
cc 7
c 6
b 2
f 1
dl 0
loc 54
rs 6.8325

How to fix   Long Method   

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:

1
# -*- coding: utf-8 -*-
2
"""
3
This file contains the Qudi logic which controls all pulsed measurements.
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
import numpy as np
25
import time
26
import datetime
27
import matplotlib.pyplot as plt
28
29
from core.module import Connector, ConfigOption, StatusVar
30
from core.util.mutex import Mutex
31
from core.util.network import netobtain
32
from core.util import units
33
from logic.generic_logic import GenericLogic
34
from logic.pulsed.pulse_extractor import PulseExtractor
35
from logic.pulsed.pulse_analyzer import PulseAnalyzer
36
37
38
class PulsedMeasurementLogic(GenericLogic):
39
    """
40
    This is the Logic class for the control of pulsed measurements.
41
    """
42
    _modclass = 'PulsedMeasurementLogic'
43
    _modtype = 'logic'
44
45
    ## declare connectors
46
    fitlogic = Connector(interface='FitLogic')
47
    savelogic = Connector(interface='SaveLogic')
48
    fastcounter = Connector(interface='FastCounterInterface')
49
    microwave = Connector(interface='MWInterface')
50
    pulsegenerator = Connector(interface='PulserInterface')
51
52
    # Config options
53
    # Optional additional paths to import from
54
    extraction_import_path = ConfigOption(name='additional_extraction_path', default=None)
55
    analysis_import_path = ConfigOption(name='additional_analysis_path', default=None)
56
    # Optional file type descriptor for saving raw data to file
57
    _raw_data_save_type = ConfigOption(name='raw_data_save_type', default='text')
58
59
    # status variables
60
    # ext. microwave settings
61
    __microwave_power = StatusVar(default=-30.0)
62
    __microwave_freq = StatusVar(default=2870e6)
63
    __use_ext_microwave = StatusVar(default=False)
64
65
    # fast counter settings
66
    __fast_counter_record_length = StatusVar(default=3.0e-6)
67
    __fast_counter_binwidth = StatusVar(default=1.0e-9)
68
    __fast_counter_gates = StatusVar(default=0)
69
70
    # measurement timer settings
71
    __timer_interval = StatusVar(default=5)
72
73
    # Pulsed measurement settings
74
    _invoke_settings_from_sequence = StatusVar(default=False)
75
    _number_of_lasers = StatusVar(default=50)
76
    _controlled_variable = StatusVar(default=list(range(50)))
77
    _alternating = StatusVar(default=False)
78
    _laser_ignore_list = StatusVar(default=list())
79
    _data_units = StatusVar(default=('s', ''))
80
81
    # PulseExtractor settings
82
    extraction_parameters = StatusVar(default=None)
83
    analysis_parameters = StatusVar(default=None)
84
85
    # Container to store measurement information about the currently loaded sequence
86
    _measurement_information = StatusVar(default=dict())
87
    # Container to store information about the sampled waveform/sequence currently loaded
88
    _sampling_information = StatusVar(default=dict())
89
90
    # alternative signal computation settings:
91
    _alternative_data_type = StatusVar(default=None)
92
    zeropad = StatusVar(default=0)
93
    psd = StatusVar(default=False)
94
    window = StatusVar(default='none')
95
    base_corr = StatusVar(default=True)
96
97
    # notification signals for master module (i.e. GUI)
98
    sigMeasurementDataUpdated = QtCore.Signal()
99
    sigTimerUpdated = QtCore.Signal(float, int, float)
100
    sigFitUpdated = QtCore.Signal(str, np.ndarray, object)
101
    sigMeasurementStatusUpdated = QtCore.Signal(bool, bool)
102
    sigPulserRunningUpdated = QtCore.Signal(bool)
103
    sigExtMicrowaveRunningUpdated = QtCore.Signal(bool)
104
    sigExtMicrowaveSettingsUpdated = QtCore.Signal(dict)
105
    sigFastCounterSettingsUpdated = QtCore.Signal(dict)
106
    sigMeasurementSettingsUpdated = QtCore.Signal(dict)
107
    sigAnalysisSettingsUpdated = QtCore.Signal(dict)
108
    sigExtractionSettingsUpdated = QtCore.Signal(dict)
109
    # Internal signals
110
    sigStartTimer = QtCore.Signal()
111
    sigStopTimer = QtCore.Signal()
112
113
    def __init__(self, config, **kwargs):
114
        super().__init__(config=config, **kwargs)
115
116
        self.log.debug('The following configuration was found.')
117
        # checking for the right configuration
118
        for key in config.keys():
119
            self.log.debug('{0}: {1}'.format(key, config[key]))
120
121
        # timer for measurement
122
        self.__analysis_timer = None
123
        self.__start_time = 0
124
        self.__elapsed_time = 0
125
        self.__elapsed_sweeps = 0  # FIXME: unused
126
127
        # threading
128
        self._threadlock = Mutex()
129
130
        # measurement data
131
        self.signal_data = np.empty((2, 0), dtype=float)
132
        self.signal_alt_data = np.empty((2, 0), dtype=float)
133
        self.measurement_error = np.empty((2, 0), dtype=float)
134
        self.laser_data = np.zeros((10, 20), dtype='int64')
135
        self.raw_data = np.zeros((10, 20), dtype='int64')
136
137
        self._saved_raw_data = OrderedDict()  # temporary saved raw data
138
        self._recalled_raw_data_tag = None  # the currently recalled raw data dict key
139
140
        # Paused measurement flag
141
        self.__is_paused = False
142
143
        # for fit:
144
        self.fc = None  # Fit container
145
        self.signal_fit_data = np.empty((2, 0), dtype=float)  # The x,y data of the fit result
146
        return
147
148
    def on_activate(self):
149
        """ Initialisation performed during activation of the module.
150
        """
151
        # Create an instance of PulseExtractor
152
        self._pulseextractor = PulseExtractor(pulsedmeasurementlogic=self)
153
        self._pulseanalyzer = PulseAnalyzer(pulsedmeasurementlogic=self)
154
155
        # QTimer must be created here instead of __init__ because otherwise the timer will not run
156
        # in this logic's thread but in the manager instead.
157
        self.__analysis_timer = QtCore.QTimer()
158
        self.__analysis_timer.setSingleShot(False)
159
        self.__analysis_timer.setInterval(round(1000. * self.__timer_interval))
160
        self.__analysis_timer.timeout.connect(self._pulsed_analysis_loop,
161
                                              QtCore.Qt.QueuedConnection)
162
163
        # Fitting
164
        self.fc = self.fitlogic().make_fit_container('pulsed', '1d')
165
        self.fc.set_units(['s', 'arb.u.'])
166
167
        # Recall saved status variables
168
        if 'fits' in self._statusVariables and isinstance(self._statusVariables.get('fits'), dict):
169
            self.fc.load_from_dict(self._statusVariables['fits'])
170
171
        # Turn off pulse generator
172
        self.pulse_generator_off()
173
174
        # Check and configure fast counter
175
        binning_constraints = self.fastcounter().get_constraints()['hardware_binwidth_list']
176
        if self.__fast_counter_binwidth not in binning_constraints:
177
            self.__fast_counter_binwidth = binning_constraints[0]
178
        if self.__fast_counter_record_length <= 0:
179
            self.__fast_counter_record_length = 3e-6
180
        self.fast_counter_off()
181
        self.set_fast_counter_settings()
182
183
        # Check and configure external microwave
184
        if self.__use_ext_microwave:
185
            self.microwave_off()
186
            self.set_microwave_settings(frequency=self.__microwave_freq,
187
                                        power=self.__microwave_power,
188
                                        use_ext_microwave=True)
189
190
        # Convert controlled variable list into numpy.ndarray
191
        self._controlled_variable = np.array(self._controlled_variable, dtype=float)
192
193
        # initialize arrays for the measurement data
194
        self._initialize_data_arrays()
195
196
        # recalled saved raw data dict key
197
        self._recalled_raw_data_tag = None
198
199
        # Connect internal signals
200
        self.sigStartTimer.connect(self.__analysis_timer.start, QtCore.Qt.QueuedConnection)
201
        self.sigStopTimer.connect(self.__analysis_timer.stop, QtCore.Qt.QueuedConnection)
202
        return
203
204
    def on_deactivate(self):
205
        """ Deactivate the module properly.
206
        """
207
        if self.module_state() == 'locked':
208
            self.stop_pulsed_measurement()
209
210
        self._statusVariables['_controlled_variable'] = list(self._controlled_variable)
211
        if len(self.fc.fit_list) > 0:
212
            self._statusVariables['fits'] = self.fc.save_to_dict()
213
214
        self.extraction_parameters = self._pulseextractor.full_settings_dict
215
        self.analysis_parameters = self._pulseanalyzer.full_settings_dict
216
217
        self.__analysis_timer.timeout.disconnect()
218
        self.sigStartTimer.disconnect()
219
        self.sigStopTimer.disconnect()
220
        return
221
222
    ############################################################################
223
    # Fast counter control methods and properties
224
    ############################################################################
225
    @property
226
    def fast_counter_settings(self):
227
        settings_dict = dict()
228
        settings_dict['bin_width'] = float(self.__fast_counter_binwidth)
229
        settings_dict['record_length'] = float(self.__fast_counter_record_length)
230
        settings_dict['number_of_gates'] = int(self.__fast_counter_gates)
231
        settings_dict['is_gated'] = bool(self.fastcounter().is_gated())
232
        return settings_dict
233
234
    @fast_counter_settings.setter
235
    def fast_counter_settings(self, settings_dict):
236
        if isinstance(settings_dict, dict):
237
            self.set_fast_counter_settings(settings_dict)
238
        return
239
240
    @property
241
    def fastcounter_constraints(self):
242
        return self.fastcounter().get_constraints()
243
244
    @QtCore.Slot(dict)
245
    def set_fast_counter_settings(self, settings_dict=None, **kwargs):
246
        """
247
        Either accept a settings dictionary as positional argument or keyword arguments.
248
        If both are present both are being used by updating the settings_dict with kwargs.
249
        The keyword arguments take precedence over the items in settings_dict if there are
250
        conflicting names.
251
252
        @param settings_dict:
253
        @param kwargs:
254
        @return:
255
        """
256
        # Check if fast counter is running and do nothing if that is the case
257
        counter_status = self.fastcounter().get_status()
258
        if not counter_status >= 2 and not counter_status < 0:
259
            # Determine complete settings dictionary
260
            if not isinstance(settings_dict, dict):
261
                settings_dict = kwargs
262
            else:
263
                settings_dict.update(kwargs)
264
265
            # Set parameters if present
266
            if 'bin_width' in settings_dict:
267
                self.__fast_counter_binwidth = float(settings_dict['bin_width'])
268
            if 'record_length' in settings_dict:
269
                self.__fast_counter_record_length = float(settings_dict['record_length'])
270
            if 'number_of_gates' in settings_dict:
271
                if self.fastcounter().is_gated():
272
                    self.__fast_counter_gates = int(settings_dict['number_of_gates'])
273
                else:
274
                    self.__fast_counter_gates = 0
275
276
            # Apply the settings to hardware
277
            self.__fast_counter_binwidth, \
278
            self.__fast_counter_record_length, \
279
            self.__fast_counter_gates = self.fastcounter().configure(self.__fast_counter_binwidth,
280
                                                                     self.__fast_counter_record_length,
281
                                                                     self.__fast_counter_gates)
282
        else:
283
            self.log.warning('Fast counter is not idle (status: {0}).\n'
284
                             'Unable to apply new settings.'.format(counter_status))
285
286
        # emit update signal for master (GUI or other logic module)
287
        self.sigFastCounterSettingsUpdated.emit(self.fast_counter_settings)
288
        return self.fast_counter_settings
289
290
    def fast_counter_on(self):
291
        """Switching on the fast counter
292
293
        @return int: error code (0:OK, -1:error)
294
        """
295
        return self.fastcounter().start_measure()
296
297
    def fast_counter_off(self):
298
        """Switching off the fast counter
299
300
        @return int: error code (0:OK, -1:error)
301
        """
302
        return self.fastcounter().stop_measure()
303
304
    @QtCore.Slot(bool)
305
    def toggle_fast_counter(self, switch_on):
306
        """
307
        """
308
        if not isinstance(switch_on, bool):
309
            return -1
310
311
        if switch_on:
312
            err = self.fast_counter_on()
313
        else:
314
            err = self.fast_counter_off()
315
        return err
316
317
    def fast_counter_pause(self):
318
        """Switching off the fast counter
319
320
        @return int: error code (0:OK, -1:error)
321
        """
322
        return self.fastcounter().pause_measure()
323
324
    def fast_counter_continue(self):
325
        """Switching off the fast counter
326
327
        @return int: error code (0:OK, -1:error)
328
        """
329
        return self.fastcounter().continue_measure()
330
331
    @QtCore.Slot(bool)
332
    def fast_counter_pause_continue(self, continue_counter):
333
        """
334
        """
335
        if not isinstance(continue_counter, bool):
336
            return -1
337
338
        if continue_counter:
339
            err = self.fast_counter_continue()
340
        else:
341
            err = self.fast_counter_pause()
342
        return err
343
    ############################################################################
344
345
    ############################################################################
346
    # External microwave control methods and properties
347
    ############################################################################
348
    @property
349
    def ext_microwave_settings(self):
350
        settings_dict = dict()
351
        settings_dict['power'] = float(self.__microwave_power)
352
        settings_dict['frequency'] = float(self.__microwave_freq)
353
        settings_dict['use_ext_microwave'] = bool(self.__use_ext_microwave)
354
        return settings_dict
355
356
    @ext_microwave_settings.setter
357
    def ext_microwave_settings(self, settings_dict):
358
        if isinstance(settings_dict, dict):
359
            self.set_microwave_settings(settings_dict)
360
        return
361
362
    @property
363
    def ext_microwave_constraints(self):
364
        return self.microwave().get_limits()
365
366
    def microwave_on(self):
367
        """
368
        Turns the external (CW) microwave output on.
369
370
        :return int: error code (0:OK, -1:error)
371
        """
372
        err = self.microwave().cw_on()
373
        if err < 0:
374
            self.log.error('Failed to turn on external CW microwave output.')
375
        self.sigExtMicrowaveRunningUpdated.emit(self.microwave().get_status()[1])
376
        return err
377
378
    def microwave_off(self):
379
        """
380
        Turns the external (CW) microwave output off.
381
382
        :return int: error code (0:OK, -1:error)
383
        """
384
        err = self.microwave().off()
385
        if err < 0:
386
            self.log.error('Failed to turn off external CW microwave output.')
387
        self.sigExtMicrowaveRunningUpdated.emit(self.microwave().get_status()[1])
388
        return err
389
390
    @QtCore.Slot(bool)
391
    def toggle_microwave(self, switch_on):
392
        """
393
        Turn the external microwave output on/off.
394
395
        :param switch_on: bool, turn microwave on (True) or off (False)
396
        :return int: error code (0:OK, -1:error)
397
        """
398
        if not isinstance(switch_on, bool):
399
            return -1
400
401
        if switch_on:
402
            err = self.microwave_on()
403
        else:
404
            err = self.microwave_off()
405
        return err
406
407
    @QtCore.Slot(dict)
408
    def set_microwave_settings(self, settings_dict=None, **kwargs):
409
        """
410
        Apply new settings to the external microwave device.
411
        Either accept a settings dictionary as positional argument or keyword arguments.
412
        If both are present both are being used by updating the settings_dict with kwargs.
413
        The keyword arguments take precedence over the items in settings_dict if there are
414
        conflicting names.
415
416
        @param settings_dict:
417
        @param kwargs:
418
        @return:
419
        """
420
        # Check if microwave is running and do nothing if that is the case
421
        if self.microwave().get_status()[1]:
422
            self.log.warning('Microwave device is running.\nUnable to apply new settings.')
423
        else:
424
            # Determine complete settings dictionary
425
            if not isinstance(settings_dict, dict):
426
                settings_dict = kwargs
427
            else:
428
                settings_dict.update(kwargs)
429
430
            # Set parameters if present
431
            if 'power' in settings_dict:
432
                self.__microwave_power = float(settings_dict['power'])
433
            if 'frequency' in settings_dict:
434
                self.__microwave_freq = float(settings_dict['frequency'])
435
            if 'use_ext_microwave' in settings_dict:
436
                self.__use_ext_microwave = bool(settings_dict['use_ext_microwave'])
437
438
            if self.__use_ext_microwave:
439
                # Apply the settings to hardware
440
                self.__microwave_freq, \
441
                self.__microwave_power, \
442
                dummy = self.microwave().set_cw(frequency=self.__microwave_freq,
443
                                                power=self.__microwave_power)
444
445
        # emit update signal for master (GUI or other logic module)
446
        self.sigExtMicrowaveSettingsUpdated.emit({'power': self.__microwave_power,
447
                                                  'frequency': self.__microwave_freq,
448
                                                  'use_ext_microwave': self.__use_ext_microwave})
449
        return self.__microwave_freq, self.__microwave_power, self.__use_ext_microwave
450
    ############################################################################
451
452
    ############################################################################
453
    # Pulse generator control methods and properties
454
    ############################################################################
455
    @property
456
    def pulse_generator_constraints(self):
457
        return self.pulsegenerator().get_constraints()
458
459
    def pulse_generator_on(self):
460
        """Switching on the pulse generator. """
461
        err = self.pulsegenerator().pulser_on()
462
        if err < 0:
463
            self.log.error('Failed to turn on pulse generator output.')
464
            self.sigPulserRunningUpdated.emit(False)
465
        else:
466
            self.sigPulserRunningUpdated.emit(True)
467
        return err
468
469
    def pulse_generator_off(self):
470
        """Switching off the pulse generator. """
471
        err = self.pulsegenerator().pulser_off()
472
        if err < 0:
473
            self.log.error('Failed to turn off pulse generator output.')
474
            self.sigPulserRunningUpdated.emit(True)
475
        else:
476
            self.sigPulserRunningUpdated.emit(False)
477
        return err
478
479
    @QtCore.Slot(bool)
480
    def toggle_pulse_generator(self, switch_on):
481
        """
482
        Switch the pulse generator on or off.
483
484
        :param switch_on: bool, turn the pulse generator on (True) or off (False)
485
        :return int: error code (0: OK, -1: error)
486
        """
487
        if not isinstance(switch_on, bool):
488
            return -1
489
490
        if switch_on:
491
            err = self.pulse_generator_on()
492
        else:
493
            err = self.pulse_generator_off()
494
        return err
495
    ############################################################################
496
497
    ############################################################################
498
    # Measurement control methods and properties
499
    ############################################################################
500
    @property
501
    def measurement_settings(self):
502
        settings_dict = dict()
503
        settings_dict['invoke_settings'] = bool(self._invoke_settings_from_sequence)
504
        settings_dict['controlled_variable'] = np.array(self._controlled_variable,
505
                                                        dtype=float).copy()
506
        settings_dict['number_of_lasers'] = int(self._number_of_lasers)
507
        settings_dict['laser_ignore_list'] = list(self._laser_ignore_list).copy()
508
        settings_dict['alternating'] = bool(self._alternating)
509
        settings_dict['units'] = self._data_units
510
        return settings_dict
511
512
    @measurement_settings.setter
513
    def measurement_settings(self, settings_dict):
514
        if isinstance(settings_dict, dict):
515
            self.set_measurement_settings(settings_dict)
516
        return
517
518
    @property
519
    def measurement_information(self):
520
        return self._measurement_information
521
522
    @measurement_information.setter
523
    def measurement_information(self, info_dict):
524
        # Check if mandatory params to invoke settings are missing and set empty dict in that case.
525
        mand_params = ('number_of_lasers',
526
                       'controlled_variable',
527
                       'laser_ignore_list',
528
                       'alternating',
529
                       'counting_length')
530
        if not isinstance(info_dict, dict) or not all(param in info_dict for param in mand_params):
531
            self._measurement_information = dict()
532
            return
533
534
        # Set measurement_information dict
535
        self._measurement_information = info_dict.copy()
536
537
        # invoke settings if needed
538
        if self._invoke_settings_from_sequence and self._measurement_information:
539
            self._apply_invoked_settings()
540
            self.sigMeasurementSettingsUpdated.emit(self.measurement_settings)
541
        return
542
543
    @property
544
    def sampling_information(self):
545
        return self._sampling_information
546
547
    @sampling_information.setter
548
    def sampling_information(self, info_dict):
549
        if isinstance(info_dict, dict):
550
            self._sampling_information = info_dict
551
        else:
552
            self._sampling_information = dict()
553
        return
554
555
    @property
556
    def timer_interval(self):
557
        return float(self.__timer_interval)
558
559
    @timer_interval.setter
560
    def timer_interval(self, value):
561
        if isinstance(value, (int, float)):
562
            self.set_timer_interval(value)
563
        return
564
565
    @property
566
    def alternative_data_type(self):
567
        return str(self._alternative_data_type)
568
569
    @alternative_data_type.setter
570
    def alternative_data_type(self, alt_data_type):
571
        if isinstance(alt_data_type, str) or alt_data_type is None:
572
            self.set_alternative_data_type(alt_data_type)
573
        return
574
575
    @property
576
    def analysis_methods(self):
577
        return self._pulseanalyzer.analysis_methods
578
579
    @property
580
    def extraction_methods(self):
581
        return self._pulseextractor.extraction_methods
582
583
    @property
584
    def analysis_settings(self):
585
        return self._pulseanalyzer.analysis_settings
586
587
    @analysis_settings.setter
588
    def analysis_settings(self, settings_dict):
589
        if isinstance(settings_dict, dict):
590
            self.set_analysis_settings(settings_dict)
591
        return
592
593
    @property
594
    def extraction_settings(self):
595
        return self._pulseextractor.extraction_settings
596
597
    @extraction_settings.setter
598
    def extraction_settings(self, settings_dict):
599
        if isinstance(settings_dict, dict):
600
            self.set_extraction_settings(settings_dict)
601
        return
602
603
    @QtCore.Slot(dict)
604
    def set_analysis_settings(self, settings_dict=None, **kwargs):
605
        """
606
        Apply new analysis settings.
607
        Either accept a settings dictionary as positional argument or keyword arguments.
608
        If both are present both are being used by updating the settings_dict with kwargs.
609
        The keyword arguments take precedence over the items in settings_dict if there are
610
        conflicting names.
611
612
        @param settings_dict:
613
        @param kwargs:
614
        @return:
615
        """
616
        # Determine complete settings dictionary
617
        if not isinstance(settings_dict, dict):
618
            settings_dict = kwargs
619
        else:
620
            settings_dict.update(kwargs)
621
622
        # Use threadlock to update settings during a running measurement
623
        with self._threadlock:
624
            self._pulseanalyzer.analysis_settings = settings_dict
625
            self.sigAnalysisSettingsUpdated.emit(self.analysis_settings)
626
        return
627
628
    @QtCore.Slot(dict)
629
    def set_extraction_settings(self, settings_dict=None, **kwargs):
630
        """
631
        Apply new analysis settings.
632
        Either accept a settings dictionary as positional argument or keyword arguments.
633
        If both are present both are being used by updating the settings_dict with kwargs.
634
        The keyword arguments take precedence over the items in settings_dict if there are
635
        conflicting names.
636
637
        @param settings_dict:
638
        @param kwargs:
639
        @return:
640
        """
641
        # Determine complete settings dictionary
642
        if not isinstance(settings_dict, dict):
643
            settings_dict = kwargs
644
        else:
645
            settings_dict.update(kwargs)
646
647
        # Use threadlock to update settings during a running measurement
648
        with self._threadlock:
649
            self._pulseextractor.extraction_settings = settings_dict
650
            self.sigExtractionSettingsUpdated.emit(self.extraction_settings)
651
        return
652
653
    @QtCore.Slot(dict)
654
    def set_measurement_settings(self, settings_dict=None, **kwargs):
655
        """
656
        Apply new measurement settings.
657
        Either accept a settings dictionary as positional argument or keyword arguments.
658
        If both are present both are being used by updating the settings_dict with kwargs.
659
        The keyword arguments take precedence over the items in settings_dict if there are
660
        conflicting names.
661
662
        @param settings_dict:
663
        @param kwargs:
664
        @return:
665
        """
666
        # Determine complete settings dictionary
667
        if not isinstance(settings_dict, dict):
668
            settings_dict = kwargs
669
        else:
670
            settings_dict.update(kwargs)
671
672
        # Check if invoke_settings flag has changed
673
        if 'invoke_settings' in settings_dict:
674
            self._invoke_settings_from_sequence = bool(settings_dict.get('invoke_settings'))
675
676
        # Invoke settings if measurement_information is present and flag is set
677
        if self._invoke_settings_from_sequence:
678
            if self._measurement_information:
679
                self._apply_invoked_settings()
680
        else:
681
            # Apply settings that can be changed while a measurement is running
682
            with self._threadlock:
683
                if 'units' in settings_dict:
684
                    self._data_units = settings_dict.get('units')
685
686
            if self.module_state() == 'idle':
687
                # Get all other parameters if present
688
                if 'controlled_variable' in settings_dict:
689
                    self._controlled_variable = np.array(settings_dict.get('controlled_variable'),
690
                                                         dtype=float)
691
                if 'number_of_lasers' in settings_dict:
692
                    self._number_of_lasers = int(settings_dict.get('number_of_lasers'))
693
                    if self.fastcounter().is_gated():
694
                        self.set_fast_counter_settings(number_of_gates=self._number_of_lasers)
695
                if 'laser_ignore_list' in settings_dict:
696
                    self._laser_ignore_list = sorted(settings_dict.get('laser_ignore_list'))
697
                if 'alternating' in settings_dict:
698
                    self._alternating = bool(settings_dict.get('alternating'))
699
700
        # Perform sanity checks on settings
701
        self._measurement_settings_sanity_check()
702
703
        # emit update signal for master (GUI or other logic module)
704
        self.sigMeasurementSettingsUpdated.emit(self.measurement_settings)
705
        return self.measurement_settings
706
707
    @QtCore.Slot(bool, str)
708
    def toggle_pulsed_measurement(self, start, stash_raw_data_tag=''):
709
        """
710
        Convenience method to start/stop measurement
711
712
        @param bool start: Start the measurement (True) or stop the measurement (False)
713
        """
714
        if start:
715
            self.start_pulsed_measurement(stash_raw_data_tag)
716
        else:
717
            self.stop_pulsed_measurement(stash_raw_data_tag)
718
        return
719
720
    @QtCore.Slot(str)
721
    def start_pulsed_measurement(self, stashed_raw_data_tag=''):
722
        """Start the analysis loop."""
723
        self.sigMeasurementStatusUpdated.emit(True, False)
724
725
        # Check if measurement settings need to be invoked
726
        if self._invoke_settings_from_sequence:
727
            if self._measurement_information:
728
                self._apply_invoked_settings()
729
                self.sigMeasurementSettingsUpdated.emit(self.measurement_settings)
730
            else:
731
                # abort measurement if settings could not be invoked
732
                self.log.error('Unable to invoke measurement settings.\nThis feature can only be '
733
                               'used when creating the pulse sequence via predefined methods.\n'
734
                               'Aborting measurement start.')
735
                self.set_measurement_settings(invoke_settings=False)
736
                self.sigMeasurementStatusUpdated.emit(False, False)
737
                return
738
739
        with self._threadlock:
740
            if self.module_state() == 'idle':
741
                # Lock module state
742
                self.module_state.lock()
743
744
                # Clear previous fits
745
                self.fc.clear_result()
746
747
                # initialize data arrays
748
                self._initialize_data_arrays()
749
750
                # recall stashed raw data
751
                if stashed_raw_data_tag in self._saved_raw_data:
752
                    self._recalled_raw_data_tag = stashed_raw_data_tag
753
                    self.log.info('Starting pulsed measurement with stashed raw data "{0}".'
754
                                  ''.format(stashed_raw_data_tag))
755
                else:
756
                    self._recalled_raw_data_tag = None
757
758
                # start microwave source
759
                if self.__use_ext_microwave:
760
                    self.microwave_on()
761
                # start fast counter
762
                self.fast_counter_on()
763
                # start pulse generator
764
                self.pulse_generator_on()
765
766
                # initialize analysis_timer
767
                self.__elapsed_time = 0.0
768
                self.sigTimerUpdated.emit(self.__elapsed_time,
769
                                          self.__elapsed_sweeps,
770
                                          self.__timer_interval)
771
772
                # Set starting time and start timer (if present)
773
                self.__start_time = time.time()
774
                self.sigStartTimer.emit()
775
776
                # Set measurement paused flag
777
                self.__is_paused = False
778
            else:
779
                self.log.warning('Unable to start pulsed measurement. Measurement already running.')
780
        return
781
782
    @QtCore.Slot(str)
783
    def stop_pulsed_measurement(self, stash_raw_data_tag=''):
784
        """
785
        Stop the measurement
786
        """
787
        # Get raw data and analyze it a last time just before stopping the measurement.
788
        try:
789
            self._pulsed_analysis_loop()
790
        except:
791
            pass
792
793
        with self._threadlock:
794
            if self.module_state() == 'locked':
795
                # stopping the timer
796
                self.sigStopTimer.emit()
797
                # Turn off fast counter
798
                self.fast_counter_off()
799
                # Turn off pulse generator
800
                self.pulse_generator_off()
801
                # Turn off microwave source
802
                if self.__use_ext_microwave:
803
                    self.microwave_off()
804
805
                # stash raw data if requested
806
                if stash_raw_data_tag:
807
                    self.log.info('Raw data saved with tag "{0}" to continue measurement at a '
808
                                  'later point.')
809
                    self._saved_raw_data[stash_raw_data_tag] = self.raw_data.copy()
810
                self._recalled_raw_data_tag = None
811
812
                # Set measurement paused flag
813
                self.__is_paused = False
814
815
                self.module_state.unlock()
816
                self.sigMeasurementStatusUpdated.emit(False, False)
817
        return
818
819
    @QtCore.Slot(bool)
820
    def toggle_measurement_pause(self, pause):
821
        """
822
        Convenience method to pause/continue measurement
823
824
        @param bool pause: Pause the measurement (True) or continue the measurement (False)
825
        """
826
        if pause:
827
            self.pause_pulsed_measurement()
828
        else:
829
            self.continue_pulsed_measurement()
830 View Code Duplication
        return
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
831
832
    @QtCore.Slot()
833
    def pause_pulsed_measurement(self):
834
        """
835
        Pauses the measurement
836
        """
837
        with self._threadlock:
838
            if self.module_state() == 'locked':
839
                # pausing the timer
840
                if self.__analysis_timer.isActive():
841
                    # stopping the timer
842
                    self.sigStopTimer.emit()
843
844
                self.fast_counter_pause()
845
                self.pulse_generator_off()
846
                if self.__use_ext_microwave:
847
                    self.microwave_off()
848
849
                # Set measurement paused flag
850
                self.__is_paused = True
851
852
                self.sigMeasurementStatusUpdated.emit(True, True)
853
            else:
854
                self.log.warning('Unable to pause pulsed measurement. No measurement running.')
855
                self.sigMeasurementStatusUpdated.emit(False, False)
856 View Code Duplication
        return
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
857
858
    @QtCore.Slot()
859
    def continue_pulsed_measurement(self):
860
        """
861
        Continues the measurement
862
        """
863
        with self._threadlock:
864
            if self.module_state() == 'locked':
865
                if self.__use_ext_microwave:
866
                    self.microwave_on()
867
                self.fast_counter_continue()
868
                self.pulse_generator_on()
869
870
                # un-pausing the timer
871
                if not self.__analysis_timer.isActive():
872
                    self.sigStartTimer.emit()
873
874
                # Set measurement paused flag
875
                self.__is_paused = False
876
877
                self.sigMeasurementStatusUpdated.emit(True, False)
878
            else:
879
                self.log.warning('Unable to continue pulsed measurement. No measurement running.')
880
                self.sigMeasurementStatusUpdated.emit(False, False)
881
        return
882
883
    @QtCore.Slot(float)
884
    @QtCore.Slot(int)
885
    def set_timer_interval(self, interval):
886
        """
887
        Change the interval of the measurement analysis timer
888
889
        @param int|float interval: Interval of the timer in s
890
        """
891
        with self._threadlock:
892
            self.__timer_interval = interval
893
            if self.__timer_interval > 0:
894
                self.__analysis_timer.setInterval(int(1000. * self.__timer_interval))
895
                if self.module_state() == 'locked' and not self.__is_paused:
896
                    self.sigStartTimer.emit()
897
            else:
898
                self.sigStopTimer.emit()
899
900
            self.sigTimerUpdated.emit(self.__elapsed_time, self.__elapsed_sweeps,
901
                                      self.__timer_interval)
902
        return
903
904
    @QtCore.Slot(str)
905
    def set_alternative_data_type(self, alt_data_type):
906
        """
907
908
        @param alt_data_type:
909
        @return:
910
        """
911
        with self._threadlock:
912
            if alt_data_type == 'Delta' and not self._alternating:
913
                if self._alternative_data_type == 'Delta':
914
                    self._alternative_data_type = None
915
                self.log.error('Can not set "Delta" as alternative data calculation if measurement is '
916
                               'not alternating.\n'
917
                               'Setting to previous type "{0}".'.format(self.alternative_data_type))
918
            else:
919
                self._alternative_data_type = alt_data_type
920
921
            self._compute_alt_data()
922
            self.sigMeasurementDataUpdated.emit()
923
        return
924
925
    @QtCore.Slot()
926
    def manually_pull_data(self):
927
        """ Analyse and display the data
928
        """
929
        if self.module_state() == 'locked':
930
            self._pulsed_analysis_loop()
931
        return
932
933
    @QtCore.Slot(str)
934
    @QtCore.Slot(str, np.ndarray)
935
    def do_fit(self, fit_method, data=None):
936
        """
937
        Performs the chosen fit on the measured data.
938
939
        @param str fit_method: name of the fit method to use
940
        @param 2D numpy.ndarray data: the x and y data points for the fit (shape=(2,X))
941
942
        @return (2D numpy.ndarray, result object): the resulting fit data and the fit result object
943
        """
944
        # Set current fit
945
        self.fc.set_current_fit(fit_method)
946
947
        if data is None:
948
            data = self.signal_data
949
            update_fit_data = True
950
        else:
951
            update_fit_data = False
952
953
        x_fit, y_fit, result = self.fc.do_fit(data[0], data[1])
954
955
        fit_data = np.array([x_fit, y_fit])
956
        fit_name = self.fc.current_fit
957
        fit_result = self.fc.current_fit_result
958
959
        if update_fit_data:
960
            self.signal_fit_data = fit_data
961
            self.sigFitUpdated.emit(fit_name, self.signal_fit_data, fit_result)
962
963
        return fit_data, fit_result
964
965
    def _apply_invoked_settings(self):
966
        """
967
        """
968
        if not isinstance(self._measurement_information, dict) or not self._measurement_information:
969
            self.log.warning('Can\'t invoke measurement settings from sequence information '
970
                             'since no measurement_information container is given.')
971
            return
972
973
        # First try to set parameters that can be changed during a running measurement
974
        if 'units' in self._measurement_information:
975
            with self._threadlock:
976
                self._data_units = self._measurement_information.get('units')
977
978
        # Check if a measurement is running and apply following settings if this is not the case
979
        if self.module_state() == 'locked':
980
            return
981
982
        if 'number_of_lasers' in self._measurement_information:
983
            self._number_of_lasers = int(self._measurement_information.get('number_of_lasers'))
984
        else:
985
            self.log.error('Unable to invoke setting for "number_of_lasers".\n'
986
                           'Measurement information container is incomplete/invalid.')
987
            return
988
989
        if 'laser_ignore_list' in self._measurement_information:
990
            self._laser_ignore_list = sorted(self._measurement_information.get('laser_ignore_list'))
991
        else:
992
            self.log.error('Unable to invoke setting for "laser_ignore_list".\n'
993
                           'Measurement information container is incomplete/invalid.')
994
            return
995
996
        if 'alternating' in self._measurement_information:
997
            self._alternating = bool(self._measurement_information.get('alternating'))
998
        else:
999
            self.log.error('Unable to invoke setting for "alternating".\n'
1000
                           'Measurement information container is incomplete/invalid.')
1001
            return
1002
1003
        if 'controlled_variable' in self._measurement_information:
1004
            self._controlled_variable = np.array(
1005
                self._measurement_information.get('controlled_variable'), dtype=float)
1006
        else:
1007
            self.log.error('Unable to invoke setting for "controlled_variable".\n'
1008
                           'Measurement information container is incomplete/invalid.')
1009
            return
1010
1011
        if 'counting_length' in self._measurement_information:
1012
            fast_counter_record_length = self._measurement_information.get('counting_length')
1013
        else:
1014
            self.log.error('Unable to invoke setting for "counting_length".\n'
1015
                           'Measurement information container is incomplete/invalid.')
1016
            return
1017
1018
        if self.fastcounter().is_gated():
1019
            self.set_fast_counter_settings(number_of_gates=self._number_of_lasers,
1020
                                           record_length=fast_counter_record_length)
1021
        else:
1022
            self.set_fast_counter_settings(record_length=fast_counter_record_length)
1023
        return
1024
1025
    def _measurement_settings_sanity_check(self):
1026
        number_of_analyzed_lasers = self._number_of_lasers - len(self._laser_ignore_list)
1027
        if len(self._controlled_variable) < 1:
1028
            self.log.error('Tried to set empty controlled variables array. This can not work.')
1029
1030
        if self._alternating and (number_of_analyzed_lasers // 2) != len(self._controlled_variable):
1031
            self.log.error('Half of the number of laser pulses to analyze ({0}) does not match the '
1032
                           'number of controlled_variable ticks ({1:d}).'
1033
                           ''.format(number_of_analyzed_lasers // 2,
1034
                                     len(self._controlled_variable)))
1035
        elif not self._alternating and number_of_analyzed_lasers != len(self._controlled_variable):
1036
            self.log.error('Number of laser pulses to analyze ({0:d}) does not match the number of '
1037
                           'controlled_variable ticks ({1:d}).'
1038
                           ''.format(number_of_analyzed_lasers, len(self._controlled_variable)))
1039
1040
        if self.fastcounter().is_gated() and self._number_of_lasers != self.__fast_counter_gates:
1041
            self.log.error('Gated fast counter gate number differs from number of laser pulses '
1042
                           'configured in measurement settings.')
1043
        return
1044
1045
    def _pulsed_analysis_loop(self):
1046
        """ Acquires laser pulses from fast counter,
1047
            calculates fluorescence signal and creates plots.
1048
        """
1049
        with self._threadlock:
1050
            if self.module_state() == 'locked':
1051
                # Update elapsed time
1052
                self.__elapsed_time = time.time() - self.__start_time
1053
1054
                # Get counter raw data (including recalled raw data from previous measurement)
1055
                self.raw_data = self._get_raw_data()
1056
1057
                # extract laser pulses from raw data
1058
                return_dict = self._pulseextractor.extract_laser_pulses(self.raw_data)
1059
                self.laser_data = return_dict['laser_counts_arr']
1060
1061
                # analyze pulses and get data points for signal array. Also check if extraction
1062
                # worked (non-zero array returned).
1063
                if self.laser_data.any():
1064
                    tmp_signal, tmp_error = self._pulseanalyzer.analyse_laser_pulses(
1065
                        self.laser_data)
1066
                else:
1067
                    tmp_signal = np.zeros(self.laser_data.shape[0])
1068
                    tmp_error = np.zeros(self.laser_data.shape[0])
1069
1070
                # exclude laser pulses to ignore
1071
                if len(self._laser_ignore_list) > 0:
1072
                    # Convert relative negative indices into absolute positive indices
1073
                    while self._laser_ignore_list[0] < 0:
1074
                        neg_index = self._laser_ignore_list[0]
1075
                        self._laser_ignore_list[0] = len(tmp_signal) + neg_index
1076
                        self._laser_ignore_list.sort()
1077
1078
                    tmp_signal = np.delete(tmp_signal, self._laser_ignore_list)
1079
                    tmp_error = np.delete(tmp_error, self._laser_ignore_list)
1080
1081
                # order data according to alternating flag
1082
                if self._alternating:
1083
                    self.signal_data[1] = tmp_signal[::2]
1084
                    self.signal_data[2] = tmp_signal[1::2]
1085
                    self.measurement_error[1] = tmp_error[::2]
1086
                    self.measurement_error[2] = tmp_error[1::2]
1087
                else:
1088
                    self.signal_data[1] = tmp_signal
1089
                    self.measurement_error[1] = tmp_error
1090
1091
                # Compute alternative data array from signal
1092
                self._compute_alt_data()
1093
1094
            # emit signals
1095
            self.sigTimerUpdated.emit(self.__elapsed_time, self.__elapsed_sweeps,
1096
                                      self.__timer_interval)
1097
            self.sigMeasurementDataUpdated.emit()
1098
            return
1099
1100
    def _get_raw_data(self):
1101
        """
1102
        Get the raw count data from the fast counting hardware and perform sanity checks.
1103
        Also add recalled raw data to the newly received data.
1104
        :return numpy.ndarray: The count data (1D for ungated, 2D for gated counter)
1105
        """
1106
        # get raw data from fast counter
1107
        fc_data = netobtain(self.fastcounter().get_data_trace())
1108
1109
        # add old raw data from previous measurements if necessary
1110
        if self._saved_raw_data.get(self._recalled_raw_data_tag) is not None:
1111
            self.log.info('Found old saved raw data with tag "{0}".'
1112
                          ''.format(self._recalled_raw_data_tag))
1113
            if not fc_data.any():
1114
                self.log.warning('Only zeros received from fast counter!\n'
1115
                                 'Using recalled raw data only.')
1116
                fc_data = self._saved_raw_data[self._recalled_raw_data_tag]
1117
            elif self._saved_raw_data[self._recalled_raw_data_tag].shape == fc_data.shape:
1118
                self.log.debug('Recalled raw data has the same shape as current data.')
1119
                fc_data = self._saved_raw_data[self._recalled_raw_data_tag] + fc_data
1120
            else:
1121
                self.log.warning('Recalled raw data has not the same shape as current data.'
1122
                                 '\nDid NOT add recalled raw data to current time trace.')
1123
        elif not fc_data.any():
1124
            self.log.warning('Only zeros received from fast counter!')
1125
            fc_data = np.zeros(fc_data.shape, dtype='int64')
1126
        return fc_data
1127
1128
    def _initialize_data_arrays(self):
1129
        """
1130
        Initializing the signal, error, laser and raw data arrays.
1131
        """
1132
        # Determine signal array dimensions
1133
        signal_dim = 3 if self._alternating else 2
1134
1135
        self.signal_data = np.zeros((signal_dim, len(self._controlled_variable)), dtype=float)
1136
        self.signal_data[0] = self._controlled_variable
1137
1138
        self.signal_alt_data = np.zeros((signal_dim, len(self._controlled_variable)), dtype=float)
1139
        self.signal_alt_data[0] = self._controlled_variable
1140
1141
        self.measurement_error = np.zeros((signal_dim, len(self._controlled_variable)), dtype=float)
1142
        self.measurement_error[0] = self._controlled_variable
1143
1144
        number_of_bins = int(self.__fast_counter_record_length / self.__fast_counter_binwidth)
1145
        laser_length = number_of_bins if self.__fast_counter_gates > 0 else 500
1146
        self.laser_data = np.zeros((self._number_of_lasers, laser_length), dtype='int64')
1147
1148
        if self.__fast_counter_gates > 0:
1149
            self.raw_data = np.zeros((self._number_of_lasers, number_of_bins), dtype='int64')
1150
        else:
1151
            self.raw_data = np.zeros(number_of_bins, dtype='int64')
1152
1153
        self.sigMeasurementDataUpdated.emit()
1154
        return
1155
1156
    # FIXME: Revise everything below
1157
1158
    ############################################################################
1159
    @QtCore.Slot(str, bool)
1160
    def save_measurement_data(self, tag=None, with_error=True):
1161
        """
1162
        Prepare data to be saved and create a proper plot of the data
1163
1164
        @param str tag: a filetag which will be included in the filename
1165
        @param bool with_error: select whether errors should be saved/plotted
1166
1167
        @return str: filepath where data were saved
1168
        """
1169
        filepath = self.savelogic().get_path_for_module('PulsedMeasurement')
1170
        timestamp = datetime.datetime.now()
1171
1172
        #####################################################################
1173
        ####                Save extracted laser pulses                  ####
1174
        #####################################################################
1175
        if tag is not None and len(tag) > 0:
1176
            filelabel = tag + '_laser_pulses'
1177
        else:
1178
            filelabel = 'laser_pulses'
1179
1180
        # prepare the data in a dict or in an OrderedDict:
1181
        data = OrderedDict()
1182
        laser_trace = self.laser_data
1183
        data['Signal (counts)'.format()] = laser_trace.transpose()
1184
1185
        # write the parameters:
1186
        parameters = OrderedDict()
1187
        parameters['bin width (s)'] = self.__fast_counter_binwidth
1188
        parameters['record length (s)'] = self.__fast_counter_record_length
1189
        parameters['gated counting'] = self.fast_counter_settings['is_gated']
1190
        parameters['extraction parameters'] = self.extraction_settings
1191
1192
        self.savelogic().save_data(data,
1193
                                   timestamp=timestamp,
1194
                                   parameters=parameters,
1195
                                   filepath=filepath,
1196
                                   filelabel=filelabel,
1197
                                   filetype='text',
1198
                                   fmt='%d',
1199
                                   delimiter='\t')
1200
1201
        #####################################################################
1202
        ####                Save measurement data                        ####
1203
        #####################################################################
1204
        if tag is not None and len(tag) > 0:
1205
            filelabel = tag + '_pulsed_measurement'
1206
        else:
1207
            filelabel = 'pulsed_measurement'
1208
1209
        # prepare the data in a dict or in an OrderedDict:
1210
        header_str = 'Controlled variable({0})\tSignal({1})\t'.format(*self._data_units)
1211
        if self._alternating:
1212
            header_str += 'Signal2({0})'.format(self._data_units[1])
1213
        if with_error:
1214
            header_str += 'Error({0})'.format(self._data_units[1])
1215
            if self._alternating:
1216
                header_str += 'Error2({0})'.format(self._data_units[1])
1217
        data = OrderedDict()
1218
        if with_error:
1219
            data[header_str] = np.vstack((self.signal_data, self.measurement_error)).transpose()
1220
        else:
1221
            data[header_str] = self.signal_data.transpose()
1222
1223
        # write the parameters:
1224
        parameters = OrderedDict()
1225
        parameters['Approx. measurement time (s)'] = self.__elapsed_time
1226
        parameters['Measurement sweeps'] = self.__elapsed_sweeps
1227
        parameters['Number of laser pulses'] = self._number_of_lasers
1228
        parameters['Laser ignore indices'] = self._laser_ignore_list
1229
        parameters['alternating'] = self._alternating
1230
        parameters['analysis parameters'] = self.analysis_settings
1231
        parameters['extraction parameters'] = self.extraction_settings
1232
        parameters['fast counter settings'] = self.fast_counter_settings
1233
1234
        # Prepare the figure to save as a "data thumbnail"
1235
        plt.style.use(self.savelogic().mpl_qd_style)
1236
1237
        # extract the possible colors from the colorscheme:
1238
        prop_cycle = self.savelogic().mpl_qd_style['axes.prop_cycle']
1239
        colors = {}
1240
        for i, color_setting in enumerate(prop_cycle):
1241
            colors[i] = color_setting['color']
1242
1243
        # scale the x_axis for plotting
1244
        max_val = np.max(self.signal_data[0])
1245
        scaled_float = units.ScaledFloat(max_val)
1246
        counts_prefix = scaled_float.scale
1247
        x_axis_scaled = self.signal_data[0] / scaled_float.scale_val
1248
1249
        # Create the figure object
1250
        if self._alternative_data_type:
1251
            fig, (ax1, ax2) = plt.subplots(2, 1)
1252
        else:
1253
            fig, ax1 = plt.subplots()
1254
1255
        if with_error:
1256
            ax1.errorbar(x=x_axis_scaled, y=self.signal_data[1],
1257
                         yerr=self.measurement_error[1], fmt='-o',
1258
                         linestyle=':', linewidth=0.5, color=colors[0],
1259
                         ecolor=colors[1], capsize=3, capthick=0.9,
1260
                         elinewidth=1.2, label='data trace 1')
1261
1262
            if self._alternating:
1263
                ax1.errorbar(x=x_axis_scaled, y=self.signal_data[2],
1264
                             yerr=self.measurement_error[2], fmt='-D',
1265
                             linestyle=':', linewidth=0.5, color=colors[3],
1266
                             ecolor=colors[4],  capsize=3, capthick=0.7,
1267
                             elinewidth=1.2, label='data trace 2')
1268
        else:
1269
            ax1.plot(x_axis_scaled, self.signal_data[1], '-o', color=colors[0],
1270
                     linestyle=':', linewidth=0.5, label='data trace 1')
1271
1272
            if self._alternating:
1273
                ax1.plot(x_axis_scaled, self.signal_data[2], '-o',
1274
                         color=colors[3], linestyle=':', linewidth=0.5,
1275
                         label='data trace 2')
1276
1277
        # Do not include fit curve if there is no fit calculated.
1278
        if self.signal_fit_data.size != 0 and np.max(self.signal_fit_data[1]) > 0:
1279
            x_axis_fit_scaled = self.signal_fit_data[0] / scaled_float.scale_val
1280
            ax1.plot(x_axis_fit_scaled, self.signal_fit_data[1],
1281
                     color=colors[2], marker='None', linewidth=1.5,
1282
                     label='fit: {0}'.format(self.fc.current_fit))
1283
1284
            # add then the fit result to the plot:
1285
1286
            # Parameters for the text plot:
1287
            # The position of the text annotation is controlled with the
1288
            # relative offset in x direction and the relative length factor
1289
            # rel_len_fac of the longest entry in one column
1290
            rel_offset = 0.02
1291
            rel_len_fac = 0.011
1292
            entries_per_col = 24
1293
1294
            # create the formatted fit text:
1295
            if hasattr(self.fc.current_fit_result, 'result_str_dict'):
1296
                fit_res = units.create_formatted_output(self.fc.current_fit_result.result_str_dict)
1297
            else:
1298
                self.log.warning('The fit container does not contain any data '
1299
                                 'from the fit! Apply the fit once again.')
1300
                fit_res = ''
1301
            # do reverse processing to get each entry in a list
1302
            entry_list = fit_res.split('\n')
1303
            # slice the entry_list in entries_per_col
1304
            chunks = [entry_list[x:x+entries_per_col] for x in range(0, len(entry_list), entries_per_col)]
1305
1306
            is_first_column = True  # first entry should contain header or \n
1307
            shift = rel_offset
1308
1309
            for column in chunks:
1310
1311
                max_length = max(column, key=len)   # get the longest entry
1312
                column_text = ''
1313
1314
                for entry in column:
1315
                    column_text += entry + '\n'
1316
1317
                column_text = column_text[:-1]  # remove the last new line
1318
1319
                heading = ''
1320
                if is_first_column:
1321
                    heading = 'Fit results:'
1322
1323
                column_text = heading + '\n' + column_text
1324
1325
                ax1.text(1.00 + shift, 0.99, column_text,
1326
                         verticalalignment='top',
1327
                         horizontalalignment='left',
1328
                         transform=ax1.transAxes,
1329
                         fontsize=12)
1330
1331
                # the shift in position of the text is a linear function
1332
                # which depends on the longest entry in the column
1333
                shift += rel_len_fac * len(max_length)
1334
1335
                is_first_column = False
1336
1337
        # handle the save of the alternative data plot
1338
        if self._alternative_data_type:
1339
1340
            # scale the x_axis for plotting
1341
            max_val = np.max(self.signal_alt_data[0])
1342
            scaled_float = units.ScaledFloat(max_val)
1343
            x_axis_prefix = scaled_float.scale
1344
            x_axis_ft_scaled = self.signal_alt_data[0] / scaled_float.scale_val
1345
1346
            # since no ft units are provided, make a small work around:
1347
            if self._alternative_data_type == 'FFT':
1348
                if self._data_units[0] == 's':
1349
                    inverse_cont_var = 'Hz'
1350
                elif self._data_units[0] == 'Hz':
1351
                    inverse_cont_var = 's'
1352
                else:
1353
                    inverse_cont_var = '(1/{0})'.format(self._data_units[0])
1354
                x_axis_ft_label = 'Fourier Transformed controlled variable (' + x_axis_prefix + inverse_cont_var + ')'
1355
                y_axis_ft_label = 'Fourier amplitude (arb. u.)'
1356
                ft_label = 'FT of data trace 1'
1357
            else:
1358
                x_axis_ft_label = 'controlled variable (' + self._data_units[0] + ')'
1359
                y_axis_ft_label = 'norm. sig (arb. u.)'
1360
                ft_label = ''
1361
1362
            ax2.plot(x_axis_ft_scaled, self.signal_alt_data[1], '-o',
1363
                     linestyle=':', linewidth=0.5, color=colors[0],
1364
                     label=ft_label)
1365
1366
            ax2.set_xlabel(x_axis_ft_label)
1367
            ax2.set_ylabel(y_axis_ft_label)
1368
            ax2.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, ncol=2,
1369
                       mode="expand", borderaxespad=0.)
1370
1371
        #FIXME: no fit plot for the alternating graph, use for that graph colors[5]
1372
        ax1.set_xlabel('controlled variable (' + counts_prefix + self._data_units[0] + ')')
1373
        ax1.set_ylabel('signal (' + self._data_units[1] + ')')
1374
1375
        fig.tight_layout()
1376
        ax1.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, ncol=2,
1377
                   mode="expand", borderaxespad=0.)
1378
        # plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, ncol=2,
1379
        #            mode="expand", borderaxespad=0.)
1380
1381
        self.savelogic().save_data(data, timestamp=timestamp,
1382
                                   parameters=parameters, fmt='%.15e',
1383
                                   filepath=filepath, filelabel=filelabel, filetype='text',
1384
                                   delimiter='\t', plotfig=fig)
1385
1386
        #####################################################################
1387
        ####                Save raw data timetrace                      ####
1388
        #####################################################################
1389
        filelabel = 'raw_timetrace' if not tag else tag + '_raw_timetrace'
1390
1391
        # prepare the data in a dict or in an OrderedDict:
1392
        data = OrderedDict()
1393
        raw_trace = self.raw_data.astype('int64')
1394
        data['Signal(counts)'] = raw_trace.transpose()
1395
        # write the parameters:
1396
        parameters = OrderedDict()
1397
        parameters['bin width (s)'] = self.__fast_counter_binwidth
1398
        parameters['record length (s)'] = self.__fast_counter_record_length
1399
        parameters['gated counting'] = self.fast_counter_settings['is_gated']
1400
        parameters['Number of laser pulses'] = self._number_of_lasers
1401
        parameters['alternating'] = self._alternating
1402
        parameters['Controlled variable'] = list(self.signal_data[0])
1403
1404
        self.savelogic().save_data(data, timestamp=timestamp,
1405
                                   parameters=parameters, fmt='%d',
1406
                                   filepath=filepath, filelabel=filelabel,
1407
                                   filetype=self._raw_data_save_type,
1408
                                   delimiter='\t')
1409
        return filepath
1410
1411
    def _compute_alt_data(self):
1412
        """
1413
        Performing transformations on the measurement data (e.g. fourier transform).
1414
        """
1415
        if self._alternative_data_type == 'Delta' and len(self.signal_data) == 3:
1416
            self.signal_alt_data = np.empty((2, self.signal_data.shape[1]), dtype=float)
1417
            self.signal_alt_data[0] = self.signal_data[0]
1418
            self.signal_alt_data[1] = self.signal_data[1] - self.signal_data[2]
1419
        elif self._alternative_data_type == 'FFT' and self.signal_data.shape[1] >= 2:
1420
            fft_x, fft_y = units.compute_ft(x_val=self.signal_data[0],
1421
                                            y_val=self.signal_data[1],
1422
                                            zeropad_num=self.zeropad,
1423
                                            window=self.window,
1424
                                            base_corr=self.base_corr,
1425
                                            psd=self.psd)
1426
            self.signal_alt_data = np.empty((len(self.signal_data), len(fft_x)), dtype=float)
1427
            self.signal_alt_data[0] = fft_x
1428
            self.signal_alt_data[1] = fft_y
1429
            for dim in range(2, len(self.signal_data)):
1430
                dummy, self.signal_alt_data[dim] = units.compute_ft(x_val=self.signal_data[0],
1431
                                                                    y_val=self.signal_data[dim],
1432
                                                                    zeropad_num=self.zeropad,
1433
                                                                    window=self.window,
1434
                                                                    base_corr=self.base_corr,
1435
                                                                    psd=self.psd)
1436
        else:
1437
            self.signal_alt_data = np.zeros(self.signal_data.shape, dtype=float)
1438
            self.signal_alt_data[0] = self.signal_data[0]
1439
        return
1440
1441
1442
1443