forked from qmolabucr/hyperdaq
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
1903 lines (1680 loc) · 75.5 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
main.py
A module for defining the main User Inferface and main thread of the program
Last Updated: January 2020
| Trevor Arp
| Gabor Lab
| University of California, Riverside
All Rights Reserved
'''
import tkinter as tk
import tkinter.messagebox as mb
import time
from timeit import default_timer as timer
import os
import numpy as np
from traceback import format_exc
from queue import Queue
from scipy.ndimage.measurements import center_of_mass
import threading
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import parameters as pm
from hyperdaq.utilities import changeEntry, checkEntryStar, isPosInt, brute_diff_min
from hyperdaq.card import CAL
class Scan_Spec():
'''
A specification for a given scan, passed to the control thread for processing.
Meant to be extensible to allow for increased functionality in the future.
The critical parameter is Scan_Spec.dimensions, a list containing the specifications for each
dimension, the index determines the order of the variables.
Each specification is a list with the following entries:
[variable_identifier, start_value, end_value, number_of_points, sampling_function]
where the sampling function is numpy.linspace by default i.e. numpy.linspace(start_value,
end_value, number_of_points)
'''
def __init__(self):
self.type = pm.SCAN_default_type
self.scanvars = []
self.constants = dict()
self.cube = False
self.drift_correct = False
self.drift_type = None
self.dimensions = []
# end __init__
def add_constant(self, parameter, value):
'''
Adds a scanning parameter that will be held constant during the scan.
Args:
parameter (str): the parameter to hold constant
value (float): the value to hold it at
'''
if parameter in self.constants.keys() or parameter in self.scanvars:
print("Warning parameter: " + str(parameter) + ' already in scan spec, nothing updated')
else:
self.constants[parameter] = value
# end add_constant
def add_axis(self, parameter, start, end, N):
'''
Adds a scanning parameter that will be scanned linearly over the range [start, end] taking
N samples.
If the paramter was previously defined as a constant this will overwrite it.
'''
if parameter in self.scanvars:
print("Warning parameter: " + str(parameter) + ' already in scan spec, nothing updated')
else:
self.dimensions.append([parameter, start, end, N, 'linspace'])
self.scanvars.append(parameter)
if parameter in self.constants.keys():
del self.constants[parameter]
# end add_axis
def add_cube_axis(self, parameter, start, end, N):
'''
Adds a scanning parameter as decribed in add_axis.
If this function is called the scan will save as 3D cubes instead of 2D scans.
'''
self.add_axis(parameter, start, end, N)
if self.cube:
print("Warning scan is already a cube, the third axis is always the cube axis")
self.cube = True
# end add_cube_axis
def set_drift_correction(self, type):
'''
Enable dirft correction and set which image to use
'''
self.drift_correct = True
self.drift_type = type
# end drift_correct
def set_scan_type(self, type):
'''
Change the scan type from default
'''
self.type = type
# end set_scan_type
def sampleaxis(self, dim):
'''
Returns the samples to an axis based on the specified sampling function, for an axis with
index dim. If dim is 0 or 1, sampleaxis may not be called as that is usually set by the
scanning function, be sure to check.
'''
if self.dimensions[dim][4] == 'linspace':
return np.linspace(self.dimensions[dim][1], self.dimensions[dim][2], self.dimensions[dim][3])
elif self.dimensions[dim][4] == 'geomspace':
return np.geomspace(self.dimensions[dim][1], self.dimensions[dim][2], self.dimensions[dim][3])
else:
raise ValueError("Could not sample with function " + str(self.dimensions[dim][4]))
# end sampleaxis
def verify(self):
'''
Returns true if all required scan parameters are present and there are at least two axes, false otherwise.
Required parameters are defined by SCAN_spatial_parameters and SCAN_voltage_parameters.
'''
for p in pm.SCAN_spatial_parameters:
if not p in self.constants.keys() and not p in self.scanvars:
return False
for p in pm.SCAN_voltage_parameters:
if not p in self.constants.keys() and not p in self.scanvars:
return False
return True
# end verify
# end Scan_Spec
class Control_Thread(threading.Thread):
'''
The thread that controls scanning and other active functions, while allowing
the GUI to continue updating
Accepts commands from a queue, while it is performing the task self.active is true, this
can be used to block input from the GUI while the task is working
Will process two kinds of commands:
(1) It will run a hardware command, for example moving the delay stage into position, called with
command [function, args], where function is a hardware function that takes args (list of arguments)
(2) It will run a scan based on a valid Scan_Spec object passed to it
'''
'''
Threading Functions
'''
def __init__(self, command_q, main_UI, device_dict, scanner, data_out):
self.com_q = command_q
self.gui = main_UI
self.device_dict = device_dict
self.scanner = scanner
self.data_out = data_out
self.drift_type = 'rfi'
threading.Thread.__init__(self)
self.running = True
self.active = False
self.start()
# end init
def run(self):
'''
The main loop of the program, called in the new thread.
'''
while self.running:
time.sleep(pm.MAIN_loop_delay/4.0)
if not self.com_q.empty():
self.active = True
c = self.com_q.get()
try:
if len(c) == 1:
self.process_scan(c[0])
elif len(c) == 2:
func = c[0]
func(*c[1])
else:
self.gui.display_Error("Invalid Command")
except Exception as e:
self.gui.display_Error("DAQ control thread cannot complete task")
print(format_exc())
self.active = False
# end run
def stop(self):
'''
Stops the active process
'''
self.running = False
if self.active:
self.gui.abort_scan()
self.active = False
# end stop
def process_scan(self, specification):
'''
Executes a scan based on the given specification
'''
if len(specification.dimensions) < 2:
self.gui.display_Error("Invalid specification, requires at least two axes")
return
#
outputs = self.starting_card_output_values(specification)
if specification.drift_correct:
self.init_drift_buffer(outputs)
self.drift_type = specification.drift_type
#
# Perform the scan
self.perform_scan(specification, outputs, len(specification.dimensions)-1)
# end process_scan
def perform_scan(self, specification, outputs, dim, scanNum=''):
'''
Perform a scan, either an individual scan or a data cube.
parameter dim is the scan dimension to consider, if the dimension is higher than the
output (scan or cube) repeat the scan recursively along that axis.
'''
if dim > 2 or (dim == 2 and not specification.cube): # recursive case
repeat_spec = specification.dimensions[dim]
Nrep = repeat_spec[3]
rngRep = specification.sampleaxis(dim)
if scanNum is '':
self.gui.display_Text('Starting ' + str(Nrep) + ' Scans')
else:
self.gui.display_Text('Starting ' + str(Nrep) + ' Scans ' + scanNum)
st = '/'+str(Nrep)+' '
for i in range(Nrep):
if specification.drift_correct:
outputs['xaxis'], outputs['yaxis'] = self.drift_correction(outputs)
if repeat_spec[0] in outputs.keys():
outputs[repeat_spec[0]] = (rngRep[i], rngRep[i])
else:
rep_axis = self.gui.scan_params[repeat_spec[0]]
interface = self.gui.dev_interfaces[repeat_spec[0]]
rep_func = getattr(interface, rep_axis[3])
rep_func(rngRep[i])
time.sleep(pm.CNTL_repeat_wait)
r = self.perform_scan(specification, outputs, dim-1, scanNum=str(i+1)+st)
if r != 1:
self.gui.display_Text('Ending Repeating Scan Early')
break
if specification.drift_correct:
self.drift_buffer_images()
return r
elif dim == 2 and specification.cube: # base case, data cube
self.log_params(specification, outputs)
fastspec = specification.dimensions[0]
slowspec = specification.dimensions[1]
cubespec = specification.dimensions[2]
Ncube = cubespec[3]
rngCube = specification.sampleaxis(dim)
# Figure out any functions that need to be passed to the scan function as keywords
kwargs = dict()
if not fastspec[0] in outputs.keys():
fast_axis = self.gui.scan_params[fastspec[0]]
fastinterface = self.gui.dev_interfaces[fastspec[0]]
kwargs['fast_func'] = getattr(fastinterface, fast_axis[2])
kwargs['fast_func_range'] = (fastspec[1], fastspec[2])
#
if not slowspec[0] in outputs.keys():
slow_axis = self.gui.scan_params[slowspec[0]]
slowinterface = self.gui.dev_interfaces[slowspec[0]]
kwargs['func'] = getattr(slowinterface, slow_axis[3])
kwargs['func_range'] = (slowspec[1], slowspec[2])
#
# Scan
self.data_out.init_data_cube(Ncube)
self.gui.display_Text("Data Cube Started")
t0 = timer()
for i in range(Ncube):
t1 = timer()
if specification.drift_correct:
outputs['xaxis'], outputs['yaxis'] = self.drift_correction(outputs)
if cubespec[0] in outputs.keys():
outputs[cubespec[0]] = (rngCube[i], rngCube[i])
else:
cube_axis = self.gui.scan_params[cubespec[0]]
interface = self.gui.dev_interfaces[cubespec[0]]
func = getattr(interface, cube_axis[3])
func(rngCube[i])
time.sleep(pm.CNTL_function_wait)
#
r = self.scanner.scan(specification.type, outputs, fastspec[0], **kwargs)
if specification.drift_correct:
self.drift_buffer_images()
t2 = timer()
if r == 1:
self.gui.display_Text("Scan " + str(i+1) + '/' + str(Ncube) + " Completed in " + str(round(t2-t1,1)) + ' seconds')
self.data_out.buffer_cube()
else:
self.gui.display_Text("Data Cube Aborted")
break
tf = timer()
# Cleanup
if r == 1:
self.gui.display_Text("Data Cube " + scanNum + "Completed in " + str(round((tf-t0)/60.0,1)) + ' minutes')
m = self.data_out.write_images()
if m is not None:
self.gui.display_Text(m)
return r
elif dim == 1: # Base case, 2D scan
self.log_params(specification, outputs)
fastspec = specification.dimensions[0]
slowspec = specification.dimensions[1]
# Figure out any functions that need to be passed to the scan function as keywords
kwargs = dict()
if not fastspec[0] in outputs.keys():
fast_axis = self.gui.scan_params[fastspec[0]]
fastinterface = self.gui.dev_interfaces[fastspec[0]]
kwargs['fast_func'] = getattr(fastinterface, fast_axis[2])
kwargs['fast_func_range'] = (fastspec[1], fastspec[2])
#
if not slowspec[0] in outputs.keys():
slow_axis = self.gui.scan_params[slowspec[0]]
slowinterface = self.gui.dev_interfaces[slowspec[0]]
kwargs['func'] = getattr(slowinterface, slow_axis[3])
kwargs['func_range'] = (slowspec[1], slowspec[2])
#
# Scan
self.gui.display_Text("Scan Started")
t0 = timer()
r = self.scanner.scan(specification.type, outputs, fastspec[0], **kwargs)
tf = timer()
# Cleanup
if r == 1:
self.gui.display_Text("Scan " + scanNum + "Completed in " + str(round(tf-t0,1)) + ' seconds')
else:
self.gui.display_Text("Scan Aborted")
m = self.data_out.write_images()
if m is not None:
self.gui.display_Text(m)
return r
else: # Something has gone horribly wrong
raise ValueError("Invalid scan dimension for recursive case")
#
# end perform_scan
def starting_card_output_values(self, specification):
'''
Compute the starting values for the voltage output channels
'''
outputs = dict()
for p in pm.SCAN_spatial_parameters:
if p in specification.scanvars:
for param in specification.dimensions:
if param[0] == p:
outputs[p] = (param[1], param[2])
elif p in specification.constants.keys():
outputs[p] = (specification.constants[p], specification.constants[p])
else:
raise ValueError("Spatial output channel " + str(p) +" not specified")
#
for p in pm.SCAN_voltage_parameters:
if p in specification.scanvars:
for param in specification.dimensions:
if param[0] == p:
outputs[p] = (param[1], param[2])
elif p in specification.constants.keys():
outputs[p] = (specification.constants[p], specification.constants[p])
else:
raise ValueError("Voltage output channel " + str(p) +" not specified")
#
return outputs
# end starting_card_output_values
def drift_correction(self, outputs):
'''
Calculate and apply the applicable drift correction
WARNING: RFI CORRECTION UNTESTED AS OF 2020/1/14
'''
x = outputs['xaxis']
y = outputs['yaxis']
if self.num_buffered >= 3:
# Calculate the applicable correction
if self.drift_type == 'rfi': # Find the reflection image correction
sft = brute_diff_min(self.rfi_buffer[2], self.rfi_buffer[0])
dx = self.pix_to_x*sft[1]
dy = self.pix_to_y*sft[0]
else: # Photocurrent image correction
dx = self.pix_to_x*(self.current_center[1] - self.scan_center[1])
dy = self.pix_to_y*(self.current_center[0] - self.scan_center[0])
# print('x:' + str(self.pix_to_x*self.current_center[1]), 'dx: ' +str(dx), 'y:' + str(self.pix_to_y*self.current_center[0]), 'dy:'+str(dy))
# Determine if the draft is large enough to correct for
if np.abs(dx) > pm.DRIFT_x_max:
dx = 1.0*pm.DRIFT_x_max*np.sign(dx)
newx = (x[0]+dx, x[1]+dx)
elif np.abs(dx) < pm.DRIFT_x_min:
dx = 0.0
newx = x
else:
dx = 1.0*round(dx, 2)
newx = (x[0]+dx, x[1]+dx)
if np.abs(dy) >pm.DRIFT_y_max:
dy = 1.0*pm.DRIFT_y_max*np.sign(dy)
newy = (y[0]+dy, y[1]+dy)
elif np.abs(dy) < pm.DRIFT_y_min:
dy = 0.0
newy = y
else:
dy = 1.0*round(dy, 2)
newy = (y[0]+dy, y[1]+dy)
#
# Correct if applicable
if np.abs(dx) > 0.0 or np.abs(dy) > 0.0:
if self.drift_type == 'rfi':
self.num_buffered = 0 # if you correct with reflection, restart the process
self.gui.drift_update_center_values(dx, dy)
self.gui.display_Text("Images corrected by " + str(dx) +',' + str(dy))
print(str(x)+' to '+str(newx), str(y)+' to '+str(newy))
return newx, newy
else:
return x, y
else:
return x, y
# end drift_correction
def drift_buffer_images(self):
'''
Buffers the current images for use in future drift correction
'''
if self.drift_type == 'rfi': # Reflection image correction, based on translating the reflection image
d = np.abs(self.gui.images[0].data)
d = d - np.min(d)
self.rfi_buffer[:,:,2] = self.rfi_buffer[:,:,1]
self.rfi_buffer[:,:,1] = self.rfi_buffer[:,:,0]
self.rfi_buffer[:,:,0] = d/np.max(d)
else: # Photocurrent image correction, based on finding the centroid of the photocurrent
d = np.abs(self.gui.images[1].data)
rows, cols = np.shape(d)
thres = np.max(d)/4
for i in range(rows):
for j in range(cols):
if d[i,j] > thres:
d[i,j] = 1.0
else:
d[i,j] = 0.0
center = center_of_mass(d)
self.current_center[0] = center[0]
self.current_center[1] = center[1]
if self.num_buffered < 3:
self.scan_center[0] = self.scan_center[0] + center[0]/3.0
self.scan_center[1] = self.scan_center[1] + center[1]/3.0
self.num_buffered += 1
# end drift_buffer_images
def init_drift_buffer(self, outputs):
'''
Resets the drift correction buffer
'''
x = outputs['xaxis']
y = outputs['yaxis']
self.current_center = [0,0]
self.scan_center = [0,0]
self.rfi_buffer = np.zeros((self.scanner.ny, self.scanner.nx, 3))
self.num_buffered = 0
self.pix_to_x = np.abs(x[0]-x[1])/float(self.scanner.nx)
self.pix_to_y = np.abs(y[0]-y[1])/float(self.scanner.ny)
# end init_drift_buffer
def log_params(self, specification, outputs):
'''
Log the basic scan parameters and the required scanning parameters
'''
# Type of output file
if specification.cube:
self.data_out.log_param("Scan Dimension", "3")
else:
self.data_out.log_param("Scan Dimension", "2")
#
# Go through the specification and write out the main data axes
fastvar = specification.dimensions[0][0]
self.data_out.log_param("Fast Axis Variable", str(self.gui.scan_params[fastvar][0]))
self.data_out.log_param("Fast Axis Units", str(self.gui.scan_params[fastvar][1]))
if fastvar == 'xaxis': # if so it may be updated for drift correction, use outputs
self.data_out.log_param("Fast Axis Start", outputs['xaxis'][0])
self.data_out.log_param("Fast Axis End", outputs['xaxis'][1])
else:
self.data_out.log_param("Fast Axis Start", specification.dimensions[0][1])
self.data_out.log_param("Fast Axis End", specification.dimensions[0][2])
self.data_out.log_param("Fast Axis Sampling", specification.dimensions[0][4])
slowvar = specification.dimensions[1][0]
self.data_out.log_param("Slow Axis Variable", str(self.gui.scan_params[slowvar][0]))
self.data_out.log_param("Slow Axis Units", str(self.gui.scan_params[slowvar][1]))
if slowvar == 'yaxis': # if so it may be updated for drift correction, use outputs
self.data_out.log_param("Slow Axis Start", outputs['yaxis'][0])
self.data_out.log_param("Slow Axis End", outputs['yaxis'][1])
else:
self.data_out.log_param("Slow Axis Start", specification.dimensions[1][1])
self.data_out.log_param("Slow Axis End", specification.dimensions[1][2])
self.data_out.log_param("Slow Axis Sampling", specification.dimensions[1][4])
if specification.cube:
cubevar = specification.dimensions[2][0]
self.data_out.log_param("Cube Axis", str(self.gui.scan_params[cubevar][0]))
self.data_out.log_param("Cube Axis Units", str(self.gui.scan_params[cubevar][1]))
self.data_out.log_param("Cube Axis Start", specification.dimensions[2][1])
self.data_out.log_param("Cube Axis End", specification.dimensions[2][2])
self.data_out.log_param("Cube Scan Number", specification.dimensions[2][3])
self.data_out.log_param("Cube Axis Sampling", specification.dimensions[2][4])
else:
cubevar = ""
# Make sure that all the spatial parameters are logged, if they are not the fast, slow or cube axes
for i in range(len(pm.SCAN_spatial_parameters)):
param = pm.SCAN_spatial_parameters[i]
if not param in [fastvar, slowvar, cubevar]:
if outputs[param][0] == outputs[param][1]:
self.data_out.log_param(pm.SCAN_spatial_lognames[i], outputs[param][0])
else:
raise ValueError("Attempting to log a non-constant value as a constant")
#
# Make sure that all the voltage parameters are logged, if they are not the fast, slow or cube axes
for i in range(len(pm.SCAN_voltage_parameters)):
param = pm.SCAN_voltage_parameters[i]
if not param in [fastvar, slowvar, cubevar]:
self.data_out.log_param(pm.SCAN_voltage_lognames[i]+" Channel", str(self.gui.scan_params[param][0]))
self.data_out.log_param(pm.SCAN_voltage_lognames[i]+" Units", str(self.gui.scan_params[param][1]))
self.data_out.log_param(pm.SCAN_voltage_lognames[i]+" Start", outputs[param][0])
self.data_out.log_param(pm.SCAN_voltage_lognames[i]+" End", outputs[param][1])
#
#Scanning parameters that are always logged
self.data_out.log_param("Line Rate", self.scanner.linerate)
self.data_out.log_param("nx", self.scanner.nx)
self.data_out.log_param("ny", self.scanner.ny)
for k, v in self.gui.exp_params.items():
self.data_out.log_param(k, v)
self.data_out.log_param("Start Time", time.strftime("%Y/%m/%d %H:%M:%S"))
for key, dev in self.gui.dev_interfaces.items():
dev.log_status()
self.data_out.write_params_file()
# end log_params
# end class Control_Thread
class axis_selector():
def __init__(self, gui, master, label, options, optional=False, opt_num_label=None, default=None):
self.frame = tk.Frame(master, relief=tk.RIDGE, bd=5)
self.gui = gui
self.optional = optional
self.label = label
tk.Label(self.frame, text=str(self.label)+": ", width=12).grid(row=0, column=0, sticky=tk.W)
self.param_value = tk.StringVar()
self.params_OPTION = tk.OptionMenu(self.frame, self.param_value, *options, command=self.param_select)
self.params_OPTION.config(width=20)
self.params_OPTION.grid(row=0, column=1, sticky=tk.W, columnspan=5)
if self.optional:
self.enable = tk.IntVar()
self.check = tk.Checkbutton(self.frame, text="Enable", variable=self.enable)
self.check.grid(row=0, column=6, sticky=tk.W)
self.startENTRY = tk.Entry(self.frame, width=6)
self.endENTRY = tk.Entry(self.frame, width=6)
tk.Label(self.frame, text="Range: ", width=12).grid(row=1, column=0, sticky=tk.W)
self.startENTRY.grid(row=1, column=1, sticky=tk.W)
tk.Label(self.frame, text="to").grid(row=1, column=2, sticky=tk.W)
self.endENTRY.grid(row=1, column=3, sticky=tk.W)
self.unitTEXT = tk.StringVar()
tk.Label(self.frame, textvariable=self.unitTEXT, width=8).grid(row=1, column=4, sticky=tk.W)
if self.optional:
tk.Label(self.frame, text=str(opt_num_label)+": ").grid(row=2, column=0, sticky=tk.W)
self.numberENTRY = tk.Entry(self.frame, width=4)
self.numberENTRY.insert(0, str(pm.DEFAULT_number))
self.numberENTRY.grid(row=2, column=1, sticky=tk.W)
if default is not None:
self.param_select(self.gui.scan_params[default][0])
# end
def param_select(self, event):
'''
Change selection of the parameter
'''
lbl = str(event)
k = self.gui.params_options[lbl]
rng = self.gui.scan_params[k][4]
self.param_value.set(self.gui.scan_params[k][0])
self.startENTRY.delete(0, 'end')
self.startENTRY.insert(0, str(rng[0]))
self.endENTRY.delete(0, 'end')
self.endENTRY.insert(0, str(rng[1]))
self.unitTEXT.set(str(self.gui.scan_params[k][1]))
if self.optional:
self.numberENTRY.delete(0, 'end')
self.numberENTRY.insert(0, str(pm.DEFAULT_number))
# end param_select
def get_entries(self):
'''
Return the start and end entries (and number if it is an optional axis)
'''
try:
s = float(self.startENTRY.get())
e = float(self.endENTRY.get())
if self.optional:
N = int(float(self.numberENTRY.get()))
return s, e, N
else:
return s, e
except ValueError:
self.gui.display_Error(str(self.label)+" entries must be floats or integers")
return -1
# end get_entries
# end axis_selector
class hyperDAQ():
'''
The main user inferface and control for the DAQ. Meant to be generic, extended
using inheritance.
To add new axes to this generic interface use inheritance and override the function
init_extra_axes, which sets any additional axes beyond the fast, slow and cube
Args:
card_in : is the data input object reading from the NI DAQ card
images : is a list of images to display
auximages : is a dictionary of auxillary images, that are not displayed but will still be
updated. The keys allow the images to be specifically accessed by various interfaces. Set
to None if there are no auxillary images
card_control : is the controller for the DAQ card
scan_params : is a dictionary containing the parameters that it is possible to scan over,
dictionary is formatted as "device_key":["Label", "unit", fast_function, slow_function,
(default_start, default_end)] where the device_key can be used to find the device in
the device dict or identifier for a voltage output, label, unit and defaults are
displayed and fast_function/slow_function are the functions that can be used to scan
the device over the fast and slow axies respectivly (or None for voltage output).
'''
'''
##############################
Core Functions
##############################
'''
def __init__(self, images, auximages, card_control, data_writer, device_dict, interface_dict, scan_params, exp_params, exp_params_units, showdrift=True):
'''
Main interface initilization
'''
self.scanner = card_control
self.data_out = data_writer
self.data_out.set_gui_ref(self)
self.exp_params = exp_params
self.exp_params_units = exp_params_units
self.auximages = auximages
self.device_dict = device_dict
self.showdrift = showdrift
self.init_logger()
self.num_plots = len(images)
self.images = images
# self.params_options is a dictionary where the labels are the keys and the values
# are the corresponding key in self.scan_params
self.scan_params = scan_params
self.params_options = {}
for k, v in self.scan_params.items():
self.params_options[v[0]] = k
# Initilize the UI
self.screen = tk.Tk()
self.screen.resizable(0,0)
self.screen.protocol("WM_DELETE_WINDOW", self.stop)
self.screen.wm_title(str(pm.DAQ_name))
self.screen.geometry('+0+0')
self.init_scanning_frame(self.screen)
self.init_device_wrapper_frame(self.screen, interface_dict)
self.init_plotting_frame(self.screen)
self.scanningFRAME.grid(row=0, column=0, sticky=tk.N)
self.deviceWrapperFRAME.grid(row=0, column=1, sticky=tk.N)
self.plottingFRAME.grid(row=0, column=2, sticky=tk.N)
self.init_menu()
self.screen.config(menu=self.mainMENU)
s = "Startup " + time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.display_Text(s)
self.running = True
# Start the control thread
self.control_queue = Queue()
self.control = Control_Thread(self.control_queue, self, self.device_dict, self.scanner, self.data_out)
# Start the Updating
self.job = self.screen.after(pm.PLT_update_delay, self.update_callback)
self.screen.mainloop()
# end init
def stop(self):
'''
Stop the treads of all devices and close the program
'''
if self.data_out.recording:
if not mb.askokcancel("Confirm Close?", str(pm.DAQ_name) + " is recording, are you sure you want to close?"):
return
#
#
self.running = False
if self.job is not None:
self.screen.after_cancel(self.job)
self.control.stop()
self.screen.quit()
self.logger.flush()
self.logger.close()
# Write out experimental parameters
f = open(pm.PARAMS_file, 'w')
for k, v in self.exp_params.items():
f.write(str(k) + ':' + str(v) + '\n')
f.flush()
f.close()
# Shut down input output functions
self.data_out.close_files()
for img in self.images:
img.stop()
if self.auximages is not None:
for key, im in self.auximages.items():
im.stop()
# Shut down all devices
for key, dev in self.device_dict.items():
if hasattr(dev, 'stop'):
dev.stop()
# Shut down the DAQ card
self.scanner.stop()
# end stop
def start_scan(self):
'''
Gathers the scan parameters from the interface and passes the scan
specification off to the control thread.
'''
if self.control.active:
self.display_Error('DAQ is currently active')
return
#
# Check the main axes
fast = self.params_options[self.fast_axis.param_value.get()]
slow = self.params_options[self.slow_axis.param_value.get()]
if self.cube_axis.enable.get():
cube = self.params_options[self.cube_axis.param_value.get()]
else:
cube = "cube_disabled"
axes = [fast, slow, cube]
# Check the additional axes
for i in range(len(self.extra_axes)):
axis = self.extra_axes[i]
if axis.enable.get():
axval = self.params_options[axis.param_value.get()]
else:
axval = "repeat_disabled"
axes.append(axval)
#
# Check that all enabled axes are scanning different parameters
if len(axes) != len(set(axes)):
self.display_Error('one or more axis is a duplicate')
return
#
# Initilize the scan specification
spec = Scan_Spec()
# Check that all the offset entries are valid and add them to the spec
for param in pm.SCAN_spatial_parameters:
try:
val = float(self.spatial_output_entries[param].get())
except ValueError:
self.display_Error("Could not read spatial values")
return
if not self.check_value(param, val):
return
spec.add_constant(param, val)
#
for param in pm.SCAN_voltage_parameters:
try:
v0 = float(self.voltage_output_entries[param].get())
except ValueError:
self.display_Error("Could not read voltage values")
return
if not self.check_value(param, v0):
return
spec.add_constant(param, v0)
#
# Check the scan varaible entries and add them to the spec
try:
fs, fe = self.fast_axis.get_entries()
if fast in pm.SCAN_spatial_parameters:
x0 = float(self.spatial_output_entries[fast].get())
fs = fs + x0
fe = fe + x0
except Exception as e:
self.display_Error('Could not read Fast Axis value')
return
if not self.check_value(fast, fs) or not self.check_value(fast, fe):
return
spec.add_axis(axes[0], fs, fe, self.scanner.nx)
try:
ss, se = self.slow_axis.get_entries()
if slow in pm.SCAN_spatial_parameters:
y0 = float(self.spatial_output_entries[slow].get())
ss = ss + y0
se = se + y0
except Exception as e:
self.display_Error('Could not read Slow Axis value')
return
if not self.check_value(slow, ss) or not self.check_value(slow, se):
return
spec.add_axis(axes[1], ss, se, self.scanner.ny)
if axes[2] != 'cube_disabled':
try:
cs, ce, Ncube = self.cube_axis.get_entries()
except Exception as e:
print(e)
self.display_Error('Could not read Cube Axis value')
return
if not self.check_value(cube, cs) or not self.check_value(cube, ce):
return
spec.add_cube_axis(axes[2], cs, ce, Ncube)
#
# Add any additional axes
for i in range(len(self.extra_axes)):
axis = self.extra_axes[i]
if axis.enable.get():
axval = self.params_options[axis.param_value.get()]
try:
rs, re, Nrep = axis.get_entries()
except Exception as e:
self.display_Error('Could not read ' + str(axis.label) +' value')
return
if not self.check_value(axval, rs) or not self.check_value(axval, re):
return
spec.add_axis(axval, rs, re, Nrep)
#
# Check for spatial drift correction
if (self.drift_im0.get() == 1 or self.drift_im1.get() == 1) and slow == 'yaxis' and fast == 'xaxis':
if self.drift_im0.get() == 1:
spec.set_drift_correction('rfi')
elif self.drift_im1.get() == 1:
spec.set_drift_correction('pci')
else:
self.display_Error('invalid drift correction type')
return
#
if spec.verify():
# Set the axis labels
xtks = np.linspace(fs, fe, 5)
ytks = np.linspace(ss, se, 5)
for i in range(self.num_plots):
xlbl = self.axes[i].get_xticklabels()
for j in range(len(xtks)):
xlbl[j] = str(round(xtks[j],3))
self.axes[i].set_xticklabels(xlbl)
ylbl = self.axes[i].get_yticklabels()
for j in range(len(ytks)):
ylbl[j] = str(round(ytks[j],3))
self.axes[i].set_yticklabels(ylbl)
self.axes[i].set_ylabel(self.scan_params[slow][1])
self.axes[i].set_xlabel(self.scan_params[fast][1])
self.canvases[i].draw()
#
# Hand the scan off the the other thread
self.control_queue.put([spec])
else:
self.display_Error('Invalid scan specification')
# end start_scan
def check_value(self, key, value):
'''
Checks if a value is valid for a given key to self.scan_params
'''
try:
prms = self.scan_params[key]
except KeyError: # If it's not a scanning parameter, there is no need to check it
return True
lims = prms[5]
if key in pm.SCAN_spatial_parameters:
value = value*pm.SCAN_units_to_volts
if key in pm.SCAN_spatial_parameters or key in pm.SCAN_voltage_parameters:
value = CAL(value, key)
if value < lims[0]:
self.display_Error(prms[0]+' real values must be greater than ' + str(lims[0]))
return False
elif value > lims[1]:
self.display_Error(prms[0]+' real values must be less than ' + str(lims[1]))
return False
else:
return True
# end check_value
def abort_scan(self):
'''
Abort the scan
'''
if self.scanner.scanning:
self.scanner.abortscan = True
# end abort_scan
'''
##############################
TK initilizations
##############################
'''
def init_menu(self):
'''
Initilizes the main file menu
'''
self.mainMENU = tk.Menu(self.screen)
self.fileMENU = tk.Menu(self.mainMENU, tearoff=0)
self.mainMENU.add_cascade(label="File", menu=self.fileMENU)
self.fileMENU.add_command(label="Save Current Images", command=self.data_out.write_current_images)
self.fileMENU.add_separator()
self.fileMENU.add_command(label="Exit", command=self.stop)
# Scanning Menu
self.scanningMENU = tk.Menu(self.mainMENU, tearoff=0)
self.mainMENU.add_cascade(label="Scanning", menu=self.scanningMENU)
self.scanningMENU.add_command(label="Clear Processing Queues", command=self.scanner.clear_queues)
self.savescanMENU = tk.Menu(self.scanningMENU, tearoff=0)
def makeFunc(x): # to get around scope issues
return lambda: self.save_scan(x)
for i in range(pm.NUM_Save_Slots):
self.savescanMENU.add_command(label=str(i+1)+": ", command=makeFunc(i))
self.scanningMENU.add_cascade(label="Save Current Scan", menu=self.savescanMENU)
self.loadscanMENU = tk.Menu(self.scanningMENU, tearoff=0)
def makeFunc(x): # to get around scope issues
return lambda: self.load_scan(x)
for i in range(pm.NUM_Save_Slots):
self.loadscanMENU.add_command(label=str(i+1)+": ", command=makeFunc(i))
self.scanningMENU.add_cascade(label="Load Scan Configuration", menu=self.loadscanMENU)