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

PulsedMeasurementLogic._pulsed_analysis_loop()   C

Complexity

Conditions 7

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 2 Features 1
Metric Value
cc 7
c 7
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 'laser_ignore_list' in settings_dict:
694
                    self._laser_ignore_list = sorted(settings_dict.get('laser_ignore_list'))
695
                if 'alternating' in settings_dict:
696
                    self._alternating = bool(settings_dict.get('alternating'))
697
698
        # Perform sanity checks on settings
699
        self._measurement_settings_sanity_check()
700
701
        # emit update signal for master (GUI or other logic module)
702
        self.sigMeasurementSettingsUpdated.emit(self.measurement_settings)
703
        return self.measurement_settings
704
705
    @QtCore.Slot(bool, str)
706
    def toggle_pulsed_measurement(self, start, stash_raw_data_tag=''):
707
        """
708
        Convenience method to start/stop measurement
709
710
        @param bool start: Start the measurement (True) or stop the measurement (False)
711
        """
712
        if start:
713
            self.start_pulsed_measurement(stash_raw_data_tag)
714
        else:
715
            self.stop_pulsed_measurement(stash_raw_data_tag)
716
        return
717
718
    @QtCore.Slot(str)
719
    def start_pulsed_measurement(self, stashed_raw_data_tag=''):
720
        """Start the analysis loop."""
721
        self.sigMeasurementStatusUpdated.emit(True, False)
722
723
        # Check if measurement settings need to be invoked
724
        if self._invoke_settings_from_sequence:
725
            if self._measurement_information:
726
                self._apply_invoked_settings()
727
                self.sigMeasurementSettingsUpdated.emit(self.measurement_settings)
728
            else:
729
                # abort measurement if settings could not be invoked
730
                self.log.error('Unable to invoke measurement settings.\nThis feature can only be '
731
                               'used when creating the pulse sequence via predefined methods.\n'
732
                               'Aborting measurement start.')
733
                self.set_measurement_settings(invoke_settings=False)
734
                self.sigMeasurementStatusUpdated.emit(False, False)
735
                return
736
737
        with self._threadlock:
738
            if self.module_state() == 'idle':
739
                # Lock module state
740
                self.module_state.lock()
741
742
                # Clear previous fits
743
                self.fc.clear_result()
744
745
                # initialize data arrays
746
                self._initialize_data_arrays()
747
748
                # recall stashed raw data
749
                if stashed_raw_data_tag in self._saved_raw_data:
750
                    self._recalled_raw_data_tag = stashed_raw_data_tag
751
                    self.log.info('Starting pulsed measurement with stashed raw data "{0}".'
752
                                  ''.format(stashed_raw_data_tag))
753
                else:
754
                    self._recalled_raw_data_tag = None
755
756
                # start microwave source
757
                if self.__use_ext_microwave:
758
                    self.microwave_on()
759
                # start fast counter
760
                self.fast_counter_on()
761
                # start pulse generator
762
                self.pulse_generator_on()
763
764
                # initialize analysis_timer
765
                self.__elapsed_time = 0.0
766
                self.sigTimerUpdated.emit(self.__elapsed_time,
767
                                          self.__elapsed_sweeps,
768
                                          self.__timer_interval)
769
770
                # Set starting time and start timer (if present)
771
                self.__start_time = time.time()
772
                self.sigStartTimer.emit()
773
774
                # Set measurement paused flag
775
                self.__is_paused = False
776
            else:
777
                self.log.warning('Unable to start pulsed measurement. Measurement already running.')
778
        return
779
780
    @QtCore.Slot(str)
781
    def stop_pulsed_measurement(self, stash_raw_data_tag=''):
782
        """
783
        Stop the measurement
784
        """
785
        # Get raw data and analyze it a last time just before stopping the measurement.
786
        try:
787
            self._pulsed_analysis_loop()
788
        except:
789
            pass
790
791
        with self._threadlock:
792
            if self.module_state() == 'locked':
793
                # stopping the timer
794
                self.sigStopTimer.emit()
795
                # Turn off fast counter
796
                self.fast_counter_off()
797
                # Turn off pulse generator
798
                self.pulse_generator_off()
799
                # Turn off microwave source
800
                if self.__use_ext_microwave:
801
                    self.microwave_off()
802
803
                # stash raw data if requested
804
                if stash_raw_data_tag:
805
                    self.log.info('Raw data saved with tag "{0}" to continue measurement at a '
806
                                  'later point.')
807
                    self._saved_raw_data[stash_raw_data_tag] = self.raw_data.copy()
808
                self._recalled_raw_data_tag = None
809
810
                # Set measurement paused flag
811
                self.__is_paused = False
812
813
                self.module_state.unlock()
814
                self.sigMeasurementStatusUpdated.emit(False, False)
815
        return
816
817
    @QtCore.Slot(bool)
818
    def toggle_measurement_pause(self, pause):
819
        """
820
        Convenience method to pause/continue measurement
821
822
        @param bool pause: Pause the measurement (True) or continue the measurement (False)
823
        """
824
        if pause:
825
            self.pause_pulsed_measurement()
826
        else:
827
            self.continue_pulsed_measurement()
828
        return
829
830 View Code Duplication
    @QtCore.Slot()
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
831
    def pause_pulsed_measurement(self):
832
        """
833
        Pauses the measurement
834
        """
835
        with self._threadlock:
836
            if self.module_state() == 'locked':
837
                # pausing the timer
838
                if self.__analysis_timer.isActive():
839
                    # stopping the timer
840
                    self.sigStopTimer.emit()
841
842
                self.fast_counter_pause()
843
                self.pulse_generator_off()
844
                if self.__use_ext_microwave:
845
                    self.microwave_off()
846
847
                # Set measurement paused flag
848
                self.__is_paused = True
849
850
                self.sigMeasurementStatusUpdated.emit(True, True)
851
            else:
852
                self.log.warning('Unable to pause pulsed measurement. No measurement running.')
853
                self.sigMeasurementStatusUpdated.emit(False, False)
854
        return
855
856 View Code Duplication
    @QtCore.Slot()
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
857
    def continue_pulsed_measurement(self):
858
        """
859
        Continues the measurement
860
        """
861
        with self._threadlock:
862
            if self.module_state() == 'locked':
863
                if self.__use_ext_microwave:
864
                    self.microwave_on()
865
                self.fast_counter_continue()
866
                self.pulse_generator_on()
867
868
                # un-pausing the timer
869
                if not self.__analysis_timer.isActive():
870
                    self.sigStartTimer.emit()
871
872
                # Set measurement paused flag
873
                self.__is_paused = False
874
875
                self.sigMeasurementStatusUpdated.emit(True, False)
876
            else:
877
                self.log.warning('Unable to continue pulsed measurement. No measurement running.')
878
                self.sigMeasurementStatusUpdated.emit(False, False)
879
        return
880
881
    @QtCore.Slot(float)
882
    @QtCore.Slot(int)
883
    def set_timer_interval(self, interval):
884
        """
885
        Change the interval of the measurement analysis timer
886
887
        @param int|float interval: Interval of the timer in s
888
        """
889
        with self._threadlock:
890
            self.__timer_interval = interval
891
            if self.__timer_interval > 0:
892
                self.__analysis_timer.setInterval(int(1000. * self.__timer_interval))
893
                if self.module_state() == 'locked' and not self.__is_paused:
894
                    self.sigStartTimer.emit()
895
            else:
896
                self.sigStopTimer.emit()
897
898
            self.sigTimerUpdated.emit(self.__elapsed_time, self.__elapsed_sweeps,
899
                                      self.__timer_interval)
900
        return
901
902
    @QtCore.Slot(str)
903
    def set_alternative_data_type(self, alt_data_type):
904
        """
905
906
        @param alt_data_type:
907
        @return:
908
        """
909
        with self._threadlock:
910
            if alt_data_type == 'Delta' and not self._alternating:
911
                if self._alternative_data_type == 'Delta':
912
                    self._alternative_data_type = None
913
                self.log.error('Can not set "Delta" as alternative data calculation if measurement is '
914
                               'not alternating.\n'
915
                               'Setting to previous type "{0}".'.format(self.alternative_data_type))
916
            else:
917
                self._alternative_data_type = alt_data_type
918
919
            self._compute_alt_data()
920
            self.sigMeasurementDataUpdated.emit()
921
        return
922
923
    @QtCore.Slot()
924
    def manually_pull_data(self):
925
        """ Analyse and display the data
926
        """
927
        if self.module_state() == 'locked':
928
            self._pulsed_analysis_loop()
929
        return
930
931
    @QtCore.Slot(str)
932
    @QtCore.Slot(str, np.ndarray)
933
    def do_fit(self, fit_method, data=None):
934
        """
935
        Performs the chosen fit on the measured data.
936
937
        @param str fit_method: name of the fit method to use
938
        @param 2D numpy.ndarray data: the x and y data points for the fit (shape=(2,X))
939
940
        @return (2D numpy.ndarray, result object): the resulting fit data and the fit result object
941
        """
942
        # Set current fit
943
        self.fc.set_current_fit(fit_method)
944
945
        if data is None:
946
            data = self.signal_data
947
            update_fit_data = True
948
        else:
949
            update_fit_data = False
950
951
        x_fit, y_fit, result = self.fc.do_fit(data[0], data[1])
952
953
        fit_data = np.array([x_fit, y_fit])
954
        fit_name = self.fc.current_fit
955
        fit_result = self.fc.current_fit_result
956
957
        if update_fit_data:
958
            self.signal_fit_data = fit_data
959
            self.sigFitUpdated.emit(fit_name, self.signal_fit_data, fit_result)
960
961
        return fit_data, fit_result
962
963
    def _apply_invoked_settings(self):
964
        """
965
        """
966
        if not isinstance(self._measurement_information, dict) or not self._measurement_information:
967
            self.log.warning('Can\'t invoke measurement settings from sequence information '
968
                             'since no measurement_information container is given.')
969
            return
970
971
        # First try to set parameters that can be changed during a running measurement
972
        if 'units' in self._measurement_information:
973
            with self._threadlock:
974
                self._data_units = self._measurement_information.get('units')
975
976
        # Check if a measurement is running and apply following settings if this is not the case
977
        if self.module_state() == 'locked':
978
            return
979
980
        if 'number_of_lasers' in self._measurement_information:
981
            self._number_of_lasers = int(self._measurement_information.get('number_of_lasers'))
982
        else:
983
            self.log.error('Unable to invoke setting for "number_of_lasers".\n'
984
                           'Measurement information container is incomplete/invalid.')
985
            return
986
987
        if 'laser_ignore_list' in self._measurement_information:
988
            self._laser_ignore_list = sorted(self._measurement_information.get('laser_ignore_list'))
989
        else:
990
            self.log.error('Unable to invoke setting for "laser_ignore_list".\n'
991
                           'Measurement information container is incomplete/invalid.')
992
            return
993
994
        if 'alternating' in self._measurement_information:
995
            self._alternating = bool(self._measurement_information.get('alternating'))
996
        else:
997
            self.log.error('Unable to invoke setting for "alternating".\n'
998
                           'Measurement information container is incomplete/invalid.')
999
            return
1000
1001
        if 'controlled_variable' in self._measurement_information:
1002
            self._controlled_variable = np.array(
1003
                self._measurement_information.get('controlled_variable'), dtype=float)
1004
        else:
1005
            self.log.error('Unable to invoke setting for "controlled_variable".\n'
1006
                           'Measurement information container is incomplete/invalid.')
1007
            return
1008
1009
        if 'counting_length' in self._measurement_information:
1010
            fast_counter_record_length = self._measurement_information.get('counting_length')
1011
        else:
1012
            self.log.error('Unable to invoke setting for "counting_length".\n'
1013
                           'Measurement information container is incomplete/invalid.')
1014
            return
1015
1016
        if self.fastcounter().is_gated():
1017
            self.set_fast_counter_settings(number_of_gates=self._number_of_lasers,
1018
                                           record_length=fast_counter_record_length)
1019
        else:
1020
            self.set_fast_counter_settings(record_length=fast_counter_record_length)
1021
        return
1022
1023
    def _measurement_settings_sanity_check(self):
1024
        number_of_analyzed_lasers = self._number_of_lasers - len(self._laser_ignore_list)
1025
        if len(self._controlled_variable) < 1:
1026
            self.log.error('Tried to set empty controlled variables array. This can not work.')
1027
1028
        if self._alternating and (number_of_analyzed_lasers // 2) != len(self._controlled_variable):
1029
            self.log.error('Half of the number of laser pulses to analyze ({0}) does not match the '
1030
                           'number of controlled_variable ticks ({1:d}).'
1031
                           ''.format(number_of_analyzed_lasers // 2,
1032
                                     len(self._controlled_variable)))
1033
        elif not self._alternating and number_of_analyzed_lasers != len(self._controlled_variable):
1034
            self.log.error('Number of laser pulses to analyze ({0:d}) does not match the number of '
1035
                           'controlled_variable ticks ({1:d}).'
1036
                           ''.format(number_of_analyzed_lasers, len(self._controlled_variable)))
1037
1038
        if self.fastcounter().is_gated() and self._number_of_lasers != self.__fast_counter_gates:
1039
            self.log.error('Gated fast counter gate number differs from number of laser pulses '
1040
                           'configured in measurement settings.')
1041
        return
1042
1043
    def _pulsed_analysis_loop(self):
1044
        """ Acquires laser pulses from fast counter,
1045
            calculates fluorescence signal and creates plots.
1046
        """
1047
        with self._threadlock:
1048
            if self.module_state() == 'locked':
1049
                # Update elapsed time
1050
                self.__elapsed_time = time.time() - self.__start_time
1051
1052
                # Get counter raw data (including recalled raw data from previous measurement)
1053
                self.raw_data = self._get_raw_data()
1054
1055
                # extract laser pulses from raw data
1056
                return_dict = self._pulseextractor.extract_laser_pulses(self.raw_data)
1057
                self.laser_data = return_dict['laser_counts_arr']
1058
1059
                # analyze pulses and get data points for signal array. Also check if extraction
1060
                # worked (non-zero array returned).
1061
                if self.laser_data.any():
1062
                    tmp_signal, tmp_error = self._pulseanalyzer.analyse_laser_pulses(
1063
                        self.laser_data)
1064
                else:
1065
                    tmp_signal = np.zeros(self.laser_data.shape[0])
1066
                    tmp_error = np.zeros(self.laser_data.shape[0])
1067
1068
                # exclude laser pulses to ignore
1069
                if len(self._laser_ignore_list) > 0:
1070
                    # Convert relative negative indices into absolute positive indices
1071
                    while self._laser_ignore_list[0] < 0:
1072
                        neg_index = self._laser_ignore_list[0]
1073
                        self._laser_ignore_list[0] = len(tmp_signal) + neg_index
1074
                        self._laser_ignore_list.sort()
1075
1076
                    tmp_signal = np.delete(tmp_signal, self._laser_ignore_list)
1077
                    tmp_error = np.delete(tmp_error, self._laser_ignore_list)
1078
1079
                # order data according to alternating flag
1080
                if self._alternating:
1081
                    self.signal_data[1] = tmp_signal[::2]
1082
                    self.signal_data[2] = tmp_signal[1::2]
1083
                    self.measurement_error[1] = tmp_error[::2]
1084
                    self.measurement_error[2] = tmp_error[1::2]
1085
                else:
1086
                    self.signal_data[1] = tmp_signal
1087
                    self.measurement_error[1] = tmp_error
1088
1089
                # Compute alternative data array from signal
1090
                self._compute_alt_data()
1091
1092
            # emit signals
1093
            self.sigTimerUpdated.emit(self.__elapsed_time, self.__elapsed_sweeps,
1094
                                      self.__timer_interval)
1095
            self.sigMeasurementDataUpdated.emit()
1096
            return
1097
1098
    def _get_raw_data(self):
1099
        """
1100
        Get the raw count data from the fast counting hardware and perform sanity checks.
1101
        Also add recalled raw data to the newly received data.
1102
        :return numpy.ndarray: The count data (1D for ungated, 2D for gated counter)
1103
        """
1104
        # get raw data from fast counter
1105
        fc_data = netobtain(self.fastcounter().get_data_trace())
1106
1107
        # add old raw data from previous measurements if necessary
1108
        if self._saved_raw_data.get(self._recalled_raw_data_tag) is not None:
1109
            self.log.info('Found old saved raw data with tag "{0}".'
1110
                          ''.format(self._recalled_raw_data_tag))
1111
            if not fc_data.any():
1112
                self.log.warning('Only zeros received from fast counter!\n'
1113
                                 'Using recalled raw data only.')
1114
                fc_data = self._saved_raw_data[self._recalled_raw_data_tag]
1115
            elif self._saved_raw_data[self._recalled_raw_data_tag].shape == fc_data.shape:
1116
                self.log.debug('Recalled raw data has the same shape as current data.')
1117
                fc_data = self._saved_raw_data[self._recalled_raw_data_tag] + fc_data
1118
            else:
1119
                self.log.warning('Recalled raw data has not the same shape as current data.'
1120
                                 '\nDid NOT add recalled raw data to current time trace.')
1121
        elif not fc_data.any():
1122
            self.log.warning('Only zeros received from fast counter!')
1123
            fc_data = np.zeros(fc_data.shape, dtype='int64')
1124
        return fc_data
1125
1126
    def _initialize_data_arrays(self):
1127
        """
1128
        Initializing the signal, error, laser and raw data arrays.
1129
        """
1130
        # Determine signal array dimensions
1131
        signal_dim = 3 if self._alternating else 2
1132
1133
        self.signal_data = np.zeros((signal_dim, len(self._controlled_variable)), dtype=float)
1134
        self.signal_data[0] = self._controlled_variable
1135
1136
        self.signal_alt_data = np.zeros((signal_dim, len(self._controlled_variable)), dtype=float)
1137
        self.signal_alt_data[0] = self._controlled_variable
1138
1139
        self.measurement_error = np.zeros((signal_dim, len(self._controlled_variable)), dtype=float)
1140
        self.measurement_error[0] = self._controlled_variable
1141
1142
        number_of_bins = int(self.__fast_counter_record_length / self.__fast_counter_binwidth)
1143
        laser_length = number_of_bins if self.__fast_counter_gates > 0 else 500
1144
        self.laser_data = np.zeros((self._number_of_lasers, laser_length), dtype='int64')
1145
1146
        if self.__fast_counter_gates > 0:
1147
            self.raw_data = np.zeros((self._number_of_lasers, number_of_bins), dtype='int64')
1148
        else:
1149
            self.raw_data = np.zeros(number_of_bins, dtype='int64')
1150
1151
        self.sigMeasurementDataUpdated.emit()
1152
        return
1153
1154
    # FIXME: Revise everything below
1155
1156
    ############################################################################
1157
    @QtCore.Slot(str, bool)
1158
    def save_measurement_data(self, tag=None, with_error=True):
1159
        """
1160
        Prepare data to be saved and create a proper plot of the data
1161
1162
        @param str tag: a filetag which will be included in the filename
1163
        @param bool with_error: select whether errors should be saved/plotted
1164
1165
        @return str: filepath where data were saved
1166
        """
1167
        filepath = self.savelogic().get_path_for_module('PulsedMeasurement')
1168
        timestamp = datetime.datetime.now()
1169
1170
        #####################################################################
1171
        ####                Save extracted laser pulses                  ####
1172
        #####################################################################
1173
        if tag is not None and len(tag) > 0:
1174
            filelabel = tag + '_laser_pulses'
1175
        else:
1176
            filelabel = 'laser_pulses'
1177
1178
        # prepare the data in a dict or in an OrderedDict:
1179
        data = OrderedDict()
1180
        laser_trace = self.laser_data
1181
        data['Signal (counts)'.format()] = laser_trace.transpose()
1182
1183
        # write the parameters:
1184
        parameters = OrderedDict()
1185
        parameters['bin width (s)'] = self.__fast_counter_binwidth
1186
        parameters['record length (s)'] = self.__fast_counter_record_length
1187
        parameters['gated counting'] = self.fast_counter_settings['is_gated']
1188
        parameters['extraction parameters'] = self.extraction_settings
1189
1190
        self.savelogic().save_data(data,
1191
                                   timestamp=timestamp,
1192
                                   parameters=parameters,
1193
                                   filepath=filepath,
1194
                                   filelabel=filelabel,
1195
                                   filetype='text',
1196
                                   fmt='%d',
1197
                                   delimiter='\t')
1198
1199
        #####################################################################
1200
        ####                Save measurement data                        ####
1201
        #####################################################################
1202
        if tag is not None and len(tag) > 0:
1203
            filelabel = tag + '_pulsed_measurement'
1204
        else:
1205
            filelabel = 'pulsed_measurement'
1206
1207
        # prepare the data in a dict or in an OrderedDict:
1208
        header_str = 'Controlled variable({0})\tSignal({1})\t'.format(*self._data_units)
1209
        if self._alternating:
1210
            header_str += 'Signal2({0})'.format(self._data_units[1])
1211
        if with_error:
1212
            header_str += 'Error({0})'.format(self._data_units[1])
1213
            if self._alternating:
1214
                header_str += 'Error2({0})'.format(self._data_units[1])
1215
        data = OrderedDict()
1216
        if with_error:
1217
            data[header_str] = np.vstack((self.signal_data, self.measurement_error)).transpose()
1218
        else:
1219
            data[header_str] = self.signal_data.transpose()
1220
1221
        # write the parameters:
1222
        parameters = OrderedDict()
1223
        parameters['Approx. measurement time (s)'] = self.__elapsed_time
1224
        parameters['Measurement sweeps'] = self.__elapsed_sweeps
1225
        parameters['Number of laser pulses'] = self._number_of_lasers
1226
        parameters['Laser ignore indices'] = self._laser_ignore_list
1227
        parameters['alternating'] = self._alternating
1228
        parameters['analysis parameters'] = self.analysis_settings
1229
        parameters['extraction parameters'] = self.extraction_settings
1230
        parameters['fast counter settings'] = self.fast_counter_settings
1231
1232
        # Prepare the figure to save as a "data thumbnail"
1233
        plt.style.use(self.savelogic().mpl_qd_style)
1234
1235
        # extract the possible colors from the colorscheme:
1236
        prop_cycle = self.savelogic().mpl_qd_style['axes.prop_cycle']
1237
        colors = {}
1238
        for i, color_setting in enumerate(prop_cycle):
1239
            colors[i] = color_setting['color']
1240
1241
        # scale the x_axis for plotting
1242
        max_val = np.max(self.signal_data[0])
1243
        scaled_float = units.ScaledFloat(max_val)
1244
        counts_prefix = scaled_float.scale
1245
        x_axis_scaled = self.signal_data[0] / scaled_float.scale_val
1246
1247
        # Create the figure object
1248
        if self._alternative_data_type:
1249
            fig, (ax1, ax2) = plt.subplots(2, 1)
1250
        else:
1251
            fig, ax1 = plt.subplots()
1252
1253
        if with_error:
1254
            ax1.errorbar(x=x_axis_scaled, y=self.signal_data[1],
1255
                         yerr=self.measurement_error[1], fmt='-o',
1256
                         linestyle=':', linewidth=0.5, color=colors[0],
1257
                         ecolor=colors[1], capsize=3, capthick=0.9,
1258
                         elinewidth=1.2, label='data trace 1')
1259
1260
            if self._alternating:
1261
                ax1.errorbar(x=x_axis_scaled, y=self.signal_data[2],
1262
                             yerr=self.measurement_error[2], fmt='-D',
1263
                             linestyle=':', linewidth=0.5, color=colors[3],
1264
                             ecolor=colors[4],  capsize=3, capthick=0.7,
1265
                             elinewidth=1.2, label='data trace 2')
1266
        else:
1267
            ax1.plot(x_axis_scaled, self.signal_data[1], '-o', color=colors[0],
1268
                     linestyle=':', linewidth=0.5, label='data trace 1')
1269
1270
            if self._alternating:
1271
                ax1.plot(x_axis_scaled, self.signal_data[2], '-o',
1272
                         color=colors[3], linestyle=':', linewidth=0.5,
1273
                         label='data trace 2')
1274
1275
        # Do not include fit curve if there is no fit calculated.
1276
        if self.signal_fit_data.size != 0 and np.max(self.signal_fit_data[1]) > 0:
1277
            x_axis_fit_scaled = self.signal_fit_data[0] / scaled_float.scale_val
1278
            ax1.plot(x_axis_fit_scaled, self.signal_fit_data[1],
1279
                     color=colors[2], marker='None', linewidth=1.5,
1280
                     label='fit: {0}'.format(self.fc.current_fit))
1281
1282
            # add then the fit result to the plot:
1283
1284
            # Parameters for the text plot:
1285
            # The position of the text annotation is controlled with the
1286
            # relative offset in x direction and the relative length factor
1287
            # rel_len_fac of the longest entry in one column
1288
            rel_offset = 0.02
1289
            rel_len_fac = 0.011
1290
            entries_per_col = 24
1291
1292
            # create the formatted fit text:
1293
            if hasattr(self.fc.current_fit_result, 'result_str_dict'):
1294
                fit_res = units.create_formatted_output(self.fc.current_fit_result.result_str_dict)
1295
            else:
1296
                self.log.warning('The fit container does not contain any data '
1297
                                 'from the fit! Apply the fit once again.')
1298
                fit_res = ''
1299
            # do reverse processing to get each entry in a list
1300
            entry_list = fit_res.split('\n')
1301
            # slice the entry_list in entries_per_col
1302
            chunks = [entry_list[x:x+entries_per_col] for x in range(0, len(entry_list), entries_per_col)]
1303
1304
            is_first_column = True  # first entry should contain header or \n
1305
            shift = rel_offset
1306
1307
            for column in chunks:
1308
1309
                max_length = max(column, key=len)   # get the longest entry
1310
                column_text = ''
1311
1312
                for entry in column:
1313
                    column_text += entry + '\n'
1314
1315
                column_text = column_text[:-1]  # remove the last new line
1316
1317
                heading = ''
1318
                if is_first_column:
1319
                    heading = 'Fit results:'
1320
1321
                column_text = heading + '\n' + column_text
1322
1323
                ax1.text(1.00 + shift, 0.99, column_text,
1324
                         verticalalignment='top',
1325
                         horizontalalignment='left',
1326
                         transform=ax1.transAxes,
1327
                         fontsize=12)
1328
1329
                # the shift in position of the text is a linear function
1330
                # which depends on the longest entry in the column
1331
                shift += rel_len_fac * len(max_length)
1332
1333
                is_first_column = False
1334
1335
        # handle the save of the alternative data plot
1336
        if self._alternative_data_type:
1337
1338
            # scale the x_axis for plotting
1339
            max_val = np.max(self.signal_alt_data[0])
1340
            scaled_float = units.ScaledFloat(max_val)
1341
            x_axis_prefix = scaled_float.scale
1342
            x_axis_ft_scaled = self.signal_alt_data[0] / scaled_float.scale_val
1343
1344
            # since no ft units are provided, make a small work around:
1345
            if self._alternative_data_type == 'FFT':
1346
                if self._data_units[0] == 's':
1347
                    inverse_cont_var = 'Hz'
1348
                elif self._data_units[0] == 'Hz':
1349
                    inverse_cont_var = 's'
1350
                else:
1351
                    inverse_cont_var = '(1/{0})'.format(self._data_units[0])
1352
                x_axis_ft_label = 'Fourier Transformed controlled variable (' + x_axis_prefix + inverse_cont_var + ')'
1353
                y_axis_ft_label = 'Fourier amplitude (arb. u.)'
1354
                ft_label = 'FT of data trace 1'
1355
            else:
1356
                x_axis_ft_label = 'controlled variable (' + self._data_units[0] + ')'
1357
                y_axis_ft_label = 'norm. sig (arb. u.)'
1358
                ft_label = ''
1359
1360
            ax2.plot(x_axis_ft_scaled, self.signal_alt_data[1], '-o',
1361
                     linestyle=':', linewidth=0.5, color=colors[0],
1362
                     label=ft_label)
1363
1364
            ax2.set_xlabel(x_axis_ft_label)
1365
            ax2.set_ylabel(y_axis_ft_label)
1366
            ax2.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, ncol=2,
1367
                       mode="expand", borderaxespad=0.)
1368
1369
        #FIXME: no fit plot for the alternating graph, use for that graph colors[5]
1370
        ax1.set_xlabel('controlled variable (' + counts_prefix + self._data_units[0] + ')')
1371
        ax1.set_ylabel('signal (' + self._data_units[1] + ')')
1372
1373
        fig.tight_layout()
1374
        ax1.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, ncol=2,
1375
                   mode="expand", borderaxespad=0.)
1376
        # plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc=3, ncol=2,
1377
        #            mode="expand", borderaxespad=0.)
1378
1379
        self.savelogic().save_data(data, timestamp=timestamp,
1380
                                   parameters=parameters, fmt='%.15e',
1381
                                   filepath=filepath, filelabel=filelabel, filetype='text',
1382
                                   delimiter='\t', plotfig=fig)
1383
1384
        #####################################################################
1385
        ####                Save raw data timetrace                      ####
1386
        #####################################################################
1387
        filelabel = 'raw_timetrace' if not tag else tag + '_raw_timetrace'
1388
1389
        # prepare the data in a dict or in an OrderedDict:
1390
        data = OrderedDict()
1391
        raw_trace = self.raw_data.astype('int64')
1392
        data['Signal(counts)'] = raw_trace.transpose()
1393
        # write the parameters:
1394
        parameters = OrderedDict()
1395
        parameters['bin width (s)'] = self.__fast_counter_binwidth
1396
        parameters['record length (s)'] = self.__fast_counter_record_length
1397
        parameters['gated counting'] = self.fast_counter_settings['is_gated']
1398
        parameters['Number of laser pulses'] = self._number_of_lasers
1399
        parameters['alternating'] = self._alternating
1400
        parameters['Controlled variable'] = list(self.signal_data[0])
1401
1402
        self.savelogic().save_data(data, timestamp=timestamp,
1403
                                   parameters=parameters, fmt='%d',
1404
                                   filepath=filepath, filelabel=filelabel,
1405
                                   filetype=self._raw_data_save_type,
1406
                                   delimiter='\t')
1407
        return filepath
1408
1409
    def _compute_alt_data(self):
1410
        """
1411
        Performing transformations on the measurement data (e.g. fourier transform).
1412
        """
1413
        if self._alternative_data_type == 'Delta' and len(self.signal_data) == 3:
1414
            self.signal_alt_data = np.empty((2, self.signal_data.shape[1]), dtype=float)
1415
            self.signal_alt_data[0] = self.signal_data[0]
1416
            self.signal_alt_data[1] = self.signal_data[1] - self.signal_data[2]
1417
        elif self._alternative_data_type == 'FFT' and self.signal_data.shape[1] >= 2:
1418
            fft_x, fft_y = units.compute_ft(x_val=self.signal_data[0],
1419
                                            y_val=self.signal_data[1],
1420
                                            zeropad_num=self.zeropad,
1421
                                            window=self.window,
1422
                                            base_corr=self.base_corr,
1423
                                            psd=self.psd)
1424
            self.signal_alt_data = np.empty((len(self.signal_data), len(fft_x)), dtype=float)
1425
            self.signal_alt_data[0] = fft_x
1426
            self.signal_alt_data[1] = fft_y
1427
            for dim in range(2, len(self.signal_data)):
1428
                dummy, self.signal_alt_data[dim] = units.compute_ft(x_val=self.signal_data[0],
1429
                                                                    y_val=self.signal_data[dim],
1430
                                                                    zeropad_num=self.zeropad,
1431
                                                                    window=self.window,
1432
                                                                    base_corr=self.base_corr,
1433
                                                                    psd=self.psd)
1434
        else:
1435
            self.signal_alt_data = np.zeros(self.signal_data.shape, dtype=float)
1436
            self.signal_alt_data[0] = self.signal_data[0]
1437
        return
1438
1439
1440
1441