forked from npshub/mantid
-
Notifications
You must be signed in to change notification settings - Fork 0
/
isis_instrument.py
1847 lines (1596 loc) · 80.7 KB
/
isis_instrument.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
# Mantid Repository : https://github.com/mantidproject/mantid
#
# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
# NScD Oak Ridge National Laboratory, European Spallation Source,
# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
# SPDX - License - Identifier: GPL - 3.0 +
# pylint: disable=too-many-lines, invalid-name, bare-except, too-many-instance-attributes
import math
import re
from mantid.simpleapi import *
from mantid.api import WorkspaceGroup, Workspace
from mantid.kernel import Logger
from mantid.kernel import V3D
import SANSUtility as su
from math import copysign
sanslog = Logger("SANS")
class BaseInstrument(object):
def __init__(self, instr_filen=None):
"""
Reads the instrument definition xml file
@param instr_filen: the name of the instrument definition file to read
@raise IndexError: if any parameters (e.g. 'default-incident-monitor-spectrum') aren't in the xml definition
"""
if instr_filen is None:
instr_filen = self._NAME + '_Definition.xml'
config = ConfigService.Instance()
self._definition_file = os.path.join(config["instrumentDefinition.directory"], instr_filen)
inst_ws_name = self.load_empty()
self.definition = AnalysisDataService.retrieve(inst_ws_name).getInstrument()
def get_idf_file_path(self):
return self._definition_file
def get_default_beam_center(self):
"""
Returns the default beam center position, or the pixel location
of real-space coordinates (0,0).
"""
return [0, 0]
def name(self):
"""
Return the name of the instrument
"""
return self._NAME
def versioned_name(self):
"""
Hack-workaround so that we may temporarily display "SANS2DTUBES" as
an option in the instrument dropdown menu in the interface. To be removed
as part of #9367.
"""
if "SANS2D_Definition_Tubes" in self.idf_path:
return "SANS2DTUBES"
return self._NAME
def load_empty(self, workspace_name=None):
"""
Loads the instrument definition file into a workspace with the given name.
If no name is given a hidden workspace is used
@param workspace_name: the name of the workspace to create and/or display
@return the name of the workspace that was created
"""
if workspace_name is None:
workspace_name = '__' + self._NAME + '_empty'
LoadEmptyInstrument(Filename=self._definition_file, OutputWorkspace=workspace_name)
return workspace_name
class DetectorBank(object):
class _DectShape(object):
"""
Stores the dimensions of the detector, normally this is a square
which is easy, but it can have a hole in it which is harder!
"""
def __init__(self, width, height, isRect=True, n_pixels=None):
"""
Sets the dimensions of the detector
@param width: the detector's width, spectra numbers along the width should increase in intervals of one
@param height: the detector's height, spectra numbers along the down the height should increase in intervals of width
@param isRect: true for rectangular or square detectors, i.e. number of pixels = width * height
@param n_pixels: optional for rectangular shapes because if it is not given it is calculated from the
height and width in that case
"""
self._width = width
self._height = height
self._isRect = bool(isRect)
self._n_pixels = n_pixels
if n_pixels is None:
if self._isRect:
self._n_pixels = self._width * self._height
else:
raise AttributeError(
'Number of pixels in the detector unknown, you must state the number of pixels for non-rectangular detectors')
def width(self):
"""
read-only property getter, this object can't be altered
"""
return self._width
def height(self):
return self._height
def isRectangle(self):
return self._isRect
def n_pixels(self):
return self._n_pixels
class _MergeRange(object):
"""
Stores property about the detector which is used to specify the merge ranges after the data has been reduced.
"""
def __init__(self, q_min=None, q_max=None):
"""
@param q_max: Default to None. Merge region maximum
@param q_min: Default to 0.0. Merge region minimum
"""
self.q_min = q_min
self.q_max = q_max
if self.q_min is None and self.q_max is None:
self.q_merge_range = False
else:
self.q_merge_range = True
class _RescaleAndShift(object):
"""
Stores property about the detector which is used to rescale and shift
data in the bank after data have been reduced. The scale attempts to
take into account that the relative efficiency of different banks may not
be the same. By default this scale is set to 1.0. The shift is strictly
speaking more an effect of the geometry of the sample than the detector
but is included here since both these are required to bring the scale+shift of reduced data
collected on front and rear bank on the same 'level' before e.g. merging
such data
"""
def __init__(self, scale=1.0, shift=0.0, fitScale=False, fitShift=False, qMin=None, qMax=None):
"""
@param scale: Default to 1.0. Value to multiply data with
@param shift: Default to 0.0. Value to add to data
@param fitScale: Default is False. Whether or not to try and fit this param
@param fitShift: Default is False. Whether or not to try and fit this param
@param qMin: When set to None (default) then for fitting use the overlapping q region of front and rear detectors
@param qMax: When set to None (default) then for fitting use the overlapping q region of front and rear detectors
"""
self.scale = scale
self.shift = shift
self.fitScale = bool(fitScale)
self.fitShift = bool(fitShift)
self.qMin = qMin
self.qMax = qMax
if self.qMin is None or self.qMax is None:
self.qRangeUserSelected = False
else:
self.qRangeUserSelected = True
_first_spec_num = None
last_spec_num = None
def __init__(self, instr, det_type):
# detectors are known by many names, the 'uni' name is an instrument independent alias the 'long'
# name is the instrument view name and 'short' name often used for convenience
self._names = {
'uni': det_type,
'long': instr.getStringParameter(det_type + '-detector-name')[0],
'short': instr.getStringParameter(det_type + '-detector-short-name')[0]}
# the bank is often also referred to by its location, as seen by the sample
if det_type.startswith('low'):
position = 'rear'
else:
position = 'front'
self._names['position'] = position
cols_data = instr.getNumberParameter(det_type + '-detector-num-columns')
if len(cols_data) > 0:
rectanglar_shape = True
width = int(cols_data[0])
else:
rectanglar_shape = False
width = instr.getNumberParameter(det_type + '-detector-non-rectangle-width')[0]
rows_data = instr.getNumberParameter(det_type + '-detector-num-rows')
if len(rows_data) > 0:
height = int(rows_data[0])
else:
rectanglar_shape = False
height = instr.getNumberParameter(det_type + '-detector-non-rectangle-height')[0]
n_pixels = None
n_pixels_override = instr.getNumberParameter(det_type + '-detector-num-pixels')
if len(n_pixels_override) > 0:
n_pixels = int(n_pixels_override[0])
# n_pixels is normally None and calculated by DectShape but LOQ (at least) has a detector with a hole
self._shape = self._DectShape(width, height, rectanglar_shape, n_pixels)
spec_entry = instr.getNumberParameter('first-low-angle-spec-number')
if len(spec_entry) > 0:
self.set_first_spec_num(int(spec_entry[0]))
else:
# 'first-low-angle-spec-number' is an optimal instrument parameter
self.set_first_spec_num(0)
# needed for compatibility with SANSReduction and SANSUtily, remove
self.n_columns = width
# this can be set to the name of a file with correction factor against wavelength
self.correction_file = ''
# this corrections are set by the mask file
self.z_corr = 0.0
self.x_corr = 0.0
self._y_corr = 0.0
self._rot_corr = 0.0
# 23/3/12 RKH add 2 more variables
self._radius_corr = 0.0
self._side_corr = 0.0
# 10/03/15 RKH add 2 more, valid for all detectors. WHY do some of the above have an extra leading
# underscore?? Seems they are the optional ones sorted below
self.x_tilt = 0.0
self.y_tilt = 0.0
# hold rescale and shift object _RescaleAndShift
self.rescaleAndShift = self._RescaleAndShift()
self.mergeRange = self._MergeRange()
# The orientation is set by default to Horizontal (Note this used to be HorizontalFlipped,
# probably as part of some hack for specific run numbers of SANS2D)
self._orientation = 'Horizontal'
def disable_y_and_rot_corrs(self):
"""
Not all corrections are supported on all detectors
"""
self._y_corr = None
self._rot_corr = None
# 23/3/12 RKH add 2 more variables
self._radius_corr = None
self._side_corr = None
def get_y_corr(self):
if self._y_corr is not None:
return self._y_corr
else:
raise NotImplementedError('y correction is not used for this detector')
def set_y_corr(self, value):
"""
Only set the value if it isn't disabled
@param value: set y_corr to this value, unless it's disabled
"""
if self._y_corr is not None:
self._y_corr = value
def get_rot_corr(self):
if self._rot_corr is not None:
return self._rot_corr
else:
raise NotImplementedError('rot correction is not used for this detector')
def set_rot_corr(self, value):
"""
Only set the value if it isn't disabled
@param value: set rot_corr to this value, unless it's disabled
"""
if self._rot_corr is not None:
self._rot_corr = value
# 22/3/12 RKH added two new variables radius_corr, side_corr
def get_radius_corr(self):
if self._radius_corr is not None:
return self._radius_corr
else:
raise NotImplementedError('radius correction is not used for this detector')
def set_radius_corr(self, value):
"""
Only set the value if it isn't disabled
@param value: set radius_corr to this value, unless it's disabled
"""
if self._rot_corr is not None:
self._radius_corr = value
def get_side_corr(self):
if self._side_corr is not None:
return self._side_corr
else:
raise NotImplementedError('side correction is not used for this detector')
def set_side_corr(self, value):
"""
Only set the value if it isn't disabled
@param value: set side_corr to this value, unless it's disabled
"""
if self._side_corr is not None:
self._side_corr = value
y_corr = property(get_y_corr, set_y_corr, None, None)
rot_corr = property(get_rot_corr, set_rot_corr, None, None)
# 22/3/12 RKH added 2 new variables
radius_corr = property(get_radius_corr, set_radius_corr, None, None)
side_corr = property(get_side_corr, set_side_corr, None, None)
def get_first_spec_num(self):
return self._first_spec_num
def set_first_spec_num(self, value):
self._first_spec_num = value
self.last_spec_num = self._first_spec_num + self._shape.n_pixels() - 1
def place_after(self, previousDet):
self.set_first_spec_num(previousDet.last_spec_num + 1)
def name(self, form='long'):
if form.lower() == 'inst_view':
form = 'long'
if form not in self._names:
form = 'long'
return self._names[form]
def isAlias(self, guess):
"""
Detectors are often referred to by more than one name, check
if the supplied name is in the list
@param guess: this name will be searched for in the list
@return : True if the name was found, otherwise false
"""
for name in list(self._names.values()):
if guess.lower() == name.lower():
return True
return False
def spectrum_block(self, ylow, xlow, ydim, xdim):
"""
Compile a list of spectrum Numbers for rectangular block of size xdim by ydim
"""
if ydim == 'all':
ydim = self._shape.height()
if xdim == 'all':
xdim = self._shape.width()
det_dimension = self._shape.width()
base = self._first_spec_num
if not self._shape.isRectangle():
sanslog.warning(
'Attempting to block rows or columns in a non-rectangular detector, this is likely to give unexpected results!')
output = ''
if self._orientation == 'Horizontal':
start_spec = base + ylow * det_dimension + xlow
for y in range(0, ydim):
for x in range(0, xdim):
output += str(start_spec + x + (y * det_dimension)) + ','
elif self._orientation == 'Vertical':
start_spec = base + xlow * det_dimension + ylow
for x in range(det_dimension - 1, det_dimension - xdim - 1, -1):
for y in range(0, ydim):
std_i = start_spec + y + ((det_dimension - x - 1) * det_dimension)
output += str(std_i) + ','
elif self._orientation == 'Rotated':
# This is the horizontal one rotated so need to map the xlow and vlow to their rotated versions
start_spec = base + ylow * det_dimension + xlow
max_spec = det_dimension * det_dimension + base - 1
for y in range(0, ydim):
for x in range(0, xdim):
std_i = start_spec + x + (y * det_dimension)
output += str(max_spec - (std_i - base)) + ','
return output.rstrip(",")
# Used to constrain the possible values of the orientation of the detector bank against the direction that spectrum numbers increase in
_ORIENTED = {
'Horizontal': None, # most runs have the detectors in this state
'Vertical': None,
'Rotated': None}
def set_orien(self, orien):
"""
Sets to relationship between the detectors and the spectra numbers. The relationship
is given by an orientation string and this function throws if the string is not recognised
@param orien: the orientation string must be a string contained in the dictionary _ORIENTED
"""
self._ORIENTED[orien]
self._orientation = orien
def crop_to_detector(self, input_name, output_name=None):
"""
Crops the workspace that is passed so that it only contains the spectra that correspond
to this detector
@param input_name: name of the workspace to crop
@param output_name: name the workspace will take (default is the input name)
"""
if not output_name:
output_name = input_name
try:
wki = mtd[input_name]
# Is it really necessary to crop?
if wki.getNumberHistograms() != self.last_spec_num - self.get_first_spec_num() + 1:
CropWorkspace(InputWorkspace=input_name, OutputWorkspace=output_name,
StartWorkspaceIndex=self.get_first_spec_num() - 1,
EndWorkspaceIndex=self.last_spec_num - 1)
except:
raise ValueError('Can not find spectra for %s in the workspace %s [%d,%d]\nException:'
% (self.name(), input_name, self.get_first_spec_num(), self.last_spec_num)
+ str(sys.exc_info()))
class ISISInstrument(BaseInstrument):
lowAngDetSet = None
def __init__(self, filename=None, m4_instrument_component_name = None):
"""
Reads the instrument definition xml file
@param filename: the name of the instrument definition file to read
@raise IndexError: if any parameters (e.g. 'default-incident-monitor-spectrum') aren't in the xml definition
"""
super(ISISInstrument, self).__init__(instr_filen=filename)
self.idf_path = self._definition_file
# the spectrum with this number is used to normalize the workspace data
self._incid_monitor = int(self.definition.getNumberParameter(
'default-incident-monitor-spectrum')[0])
self.cen_find_step = float(self.definition.getNumberParameter('centre-finder-step-size')[0])
# see if a second step size is defined. If not set the second value to the first for compatibility
# logger.warning("Trying to find centre-finder-step-size2")
try:
self.cen_find_step2 = float(self.definition.getNumberParameter('centre-finder-step-size2')[0])
except:
# logger.warning("Failed to find centre-finder-step-size2")
self.cen_find_step2 = self.cen_find_step
try:
self.beam_centre_scale_factor1 = float(self.definition.getNumberParameter('beam-centre-scale-factor1')[0])
except:
logger.information("Setting beam-centre-scale-factor1 to default (1000).")
self.beam_centre_scale_factor1 = 1000.0
try:
self.beam_centre_scale_factor2 = float(self.definition.getNumberParameter('beam-centre-scale-factor2')[0])
except:
logger.information("Setting beam-centre-scale-factor1 to default (1000).")
self.beam_centre_scale_factor2 = 1000.0
firstDetect = DetectorBank(self.definition, 'low-angle')
# firstDetect.disable_y_and_rot_corrs()
secondDetect = DetectorBank(self.definition, 'high-angle')
secondDetect.place_after(firstDetect)
# add det_selection variable that will receive the DET/ REAR/FRONT/BOTH/MERGED
self.det_selection = 'REAR'
self.DETECTORS = {'low-angle': firstDetect}
self.DETECTORS['high-angle'] = secondDetect
self.setDefaultDetector()
# if this is set InterpolationRebin will be used on the monitor spectrum used to normalize the sample,
# useful because wavelength resolution in the monitor spectrum can be course in the range of interest
self._use_interpol_norm = False
# remove use_interpol_trans_calc once the beam centre finder has been converted
self.use_interpol_trans_calc = False
# the sample will be moved this distance a long the beam axis
self.SAMPLE_Z_CORR = 0
# Detector position information for SANS2D
# why are these not defined in SANS2D
self.FRONT_DET_RADIUS = 306.0
self.FRONT_DET_DEFAULT_SD_M = 4.0
self.FRONT_DET_DEFAULT_X_M = 1.1
self.REAR_DET_DEFAULT_SD_M = 4.0
# LOG files for SANS2D will have these encoder readings
# why are these not defined in SANS2D
self.FRONT_DET_X = 0.0
self.FRONT_DET_Z = 0.0
self.FRONT_DET_ROT = 0.0
self.REAR_DET_Z = 0.0
self.REAR_DET_X = 0
# LOG files for Larmor will have these encoder readings
# why are these not defined in Larmor
self.BENCH_ROT = 0.0
# spectrum number of the monitor used to as the incidient in the transmission calculations
self.default_trans_spec = int(self.definition.getNumberParameter(
'default-transmission-monitor-spectrum')[0])
self.incid_mon_4_trans_calc = self._incid_monitor
isis = config.getFacility('ISIS')
# Number of digits in standard file name
self.run_number_width = isis.instrument(self._NAME).zeroPadding(0)
# Set a flag if the instrument has an M4 monitor or not
self.has_m4_monitor = self._has_m4_monitor_in_idf(m4_instrument_component_name)
# this variable isn't used again and stops the instrument from being deep copied if this instance is deep copied
self.definition = None
# remove this function
self._del_incidient_set = False
# it is possible to set the TOF regions that is assumed to be background for each monitors
self._back_ground = {}
# the default start region, used for any monitors that a specific one wasn't set for
self._back_start = None
# default end region
self._back_end = None
# the background TOF region for ROI data. Note that either this or a transmission monitor is used.
self._back_start_ROI = None
self._back_end_ROI = None
# if the user moves a monitor to this z coordinate (with MON/LENGTH ...) this will be recorded here.
# These are overridden lines like TRANS/TRANSPEC=4/SHIFT=-100
self.monitor_zs = {}
# Used when new calibration required.
self._newCalibrationWS = None
# Centre of beam after a move has been applied,
self.beam_centre_pos1_after_move = 0.0
self.beam_centre_pos2_after_move = 0.0
def get_incident_mon(self):
"""
@return: the spectrum number of the incident scattering monitor
"""
return self._incid_monitor
def set_incident_mon(self, spectrum_number):
"""
set the incident scattering monitor spectrum number regardless of
lock
@param spectrum_number: monitor's sectrum number
"""
self._incid_monitor = int(spectrum_number)
self._del_incidient_set = True
def suggest_incident_mntr(self, spectrum_number):
"""
remove this function and the data member it uses
"""
if not self._del_incidient_set:
self.set_incident_mon(spectrum_number)
def set_sample_offset(self, value):
"""
@param value: sample value offset
"""
self.SAMPLE_Z_CORR = float(value) / 1000.
def is_interpolating_norm(self):
return self._use_interpol_norm
def set_interpolating_norm(self, on=True):
"""
This method sets that the monitor spectrum should be interpolated before
normalisation
"""
self._use_interpol_norm = on
def cur_detector(self):
if self.lowAngDetSet:
return self.DETECTORS['low-angle']
else:
return self.DETECTORS['high-angle']
def get_low_angle_detector(self):
""" Provide a direct way to get the low bank detector.
This method does not require to pass the name of the detector bank.
"""
return self.DETECTORS['low-angle']
def get_high_angle_detector(self):
""" Provide a direct way to get the high bank detector
This method does not require to pass the name of the detector bank.
"""
return self.DETECTORS['high-angle']
def other_detector(self):
if not self.lowAngDetSet:
return self.DETECTORS['low-angle']
else:
return self.DETECTORS['high-angle']
def getDetector(self, requested):
for _n, detect in self.DETECTORS.items():
if detect.isAlias(requested):
return detect
sanslog.notice("getDetector: Detector " + requested + "not found")
def listDetectors(self):
return self.cur_detector().name(), self.other_detector().name()
def isHighAngleDetector(self, detName):
if self.DETECTORS['high-angle'].isAlias(detName):
return True
def isDetectorName(self, detName):
if self.other_detector().isAlias(detName):
return True
return self.cur_detector().isAlias(detName)
def setDetector(self, detName):
self.det_selection = detName
if self.other_detector().isAlias(detName):
self.lowAngDetSet = not self.lowAngDetSet
return True
elif self.cur_detector().isAlias(detName):
return True
def get_detector_selection(self):
return self.det_selection
def setDefaultDetector(self):
self.lowAngDetSet = True
def copy_correction_files(self):
"""
Check if one of the efficiency files hasn't been set and assume the other is to be used
"""
a = self.cur_detector()
b = self.other_detector()
if a.correction_file == '' and b.correction_file != '':
a.correction_file = b.correction_file != ''
if b.correction_file == '' and a.correction_file != '':
b.correction_file = a.correction_file != ''
def detector_file(self, det_name):
det = self.getDetector(det_name)
return det.correction_file
def get_TOFs(self, monitor):
"""
Gets the start and end time of flights for the region assumed to contain
only background counts for this instrument
@param monitor: spectrum number of the monitor's spectrum
@return: the start time, the end time
"""
monitor = int(monitor)
if monitor in self._back_ground:
return self._back_ground[int(monitor)]['start'], \
self._back_ground[int(monitor)]['end']
else:
return self._back_start, self._back_end
def set_TOFs(self, start, end, monitor=None):
"""
Defines the start and end time of flights for the assumed background region
for this instrument
@param: start defines the start of the background region
@param: end defines the end
@param monitor: spectrum number of the monitor's spectrum, if none given affect the default
"""
if start is not None:
start = float(start)
if end is not None:
end = float(end)
if monitor:
self._back_ground[int(monitor)] = {'start': start, 'end': end}
else:
self._back_start = start
self._back_end = end
def reset_TOFs(self, monitor=None):
"""
Reset background region set by set_TOFs
@param monitor: spectrum number of the monitor's spectrum, if none given affect the default
"""
if monitor:
monitor = int(monitor)
if monitor in self._back_ground:
del self._back_ground[int(monitor)]
else:
self._back_ground = {}
self._back_start = None
self._back_end = None
self.reset_TOFs_for_ROI()
def get_TOFs_for_ROI(self):
"""
Gets the TOFs for the ROI which is required for the Transmission calculation. If it is
not available then use the default setting
@return: the start time, the end time
"""
if self._back_start_ROI and self._back_end_ROI:
return self._back_start_ROI, self._back_end_ROI
else:
return None, None
def set_TOFs_for_ROI(self, start, end):
"""
Sets the TOFs for the ROI which is required for the Transmission calculation.
@param: start : defines the start of the background region for ROI
@param: end : defines the end of the background region for ROI
"""
if start is not None:
start = float(start)
if end is not None:
end = float(end)
self._back_start_ROI = start
self._back_end_ROI = end
def reset_TOFs_for_ROI(self):
"""
Reset background region set by set_TOFs for ROI
"""
self._back_start_ROI = None
self._back_end_ROI = None
def move_all_components(self, ws):
"""
Move the sample object to the location set in the logs or user settings file
@param ws: the workspace containing the sample to move
"""
MoveInstrumentComponent(Workspace=ws, ComponentName='some-sample-holder', Z=self.SAMPLE_Z_CORR,
RelativePosition=True)
for i in list(self.monitor_zs.keys()):
# get the current location
component = self.monitor_names[i]
ws = mtd[str(ws)]
mon = ws.getInstrument().getComponentByName(component)
z_loc = mon.getPos().getZ()
# now the relative move
offset = (self.monitor_zs[i] / 1000.) - z_loc
MoveInstrumentComponent(Workspace=ws, ComponentName=component, Z=offset,
RelativePosition=True)
def move_components(self, ws, beamX, beamY):
"""Define how to move the bank to position beamX and beamY must be implemented"""
raise RuntimeError("Not Implemented")
def elementary_displacement_of_single_component(self, workspace, component_name, coord1, coord2,
coord1_scale_factor=1., coord2_scale_factor=1.,
relative_displacement=True):
"""
A simple elementary displacement of a single component.
This provides the adequate displacement for finding the beam centre.
@param workspace: the workspace which needs to have the move applied to it
@param component_name: the name of the component which being displaced
@param coord1: the first coordinate, which is x here
@param coord2: the second coordinate, which is y here
@param coord1_scale_factor: scale factor for the first coordinate
@param coord2_scale_factor: scale factor for the second coordinate
@param relative_displacement: If the the displacement is to be relative (it normally should be)
"""
raise RuntimeError("Not Implemented")
def cur_detector_position(self, ws_name):
'''
Return the position of the center of the detector bank
@param ws_name: the input workspace name
@raise RuntimeError: Not implemented
'''
raise RuntimeError("Not Implemented")
def on_load_sample(self, ws_name, beamcentre, isSample):
"""It will be called just after loading the workspace for sample and can
It configures the instrument for the specific run of the workspace for handle historical changes in the instrument.
It centralizes the detector bank to the beamcentre (tuple of two values)
"""
ws_ref = mtd[str(ws_name)]
try:
run_num = LARMOR.get_run_number_from_workspace_reference(ws_ref)
except:
run_num = int(re.findall(r'\d+', str(ws_name))[0])
if isSample:
self.set_up_for_run(run_num)
if self._newCalibrationWS:
# We are about to transfer the Instrument Parameter File from the
# calibration to the original workspace. We want to add new parameters
# which the calibration file has not yet picked up.
# IMPORTANT NOTE: This takes the parameter settings from the original workspace
# if they are old too, then we don't pick up newly added parameters
self._add_parmeters_absent_in_calibration(ws_name, self._newCalibrationWS)
self.changeCalibration(ws_name)
# centralize the bank to the centre
dummy_centre, centre_shift = self.move_components(ws_name, beamcentre[0], beamcentre[1])
return centre_shift
def load_transmission_inst(self, ws_trans, ws_direct, beamcentre):
"""
Called on loading of transmissions
"""
pass
def changeCalibration(self, ws_name):
calib = mtd[self._newCalibrationWS]
sanslog.notice("Applying new calibration for the detectors from " + str(calib.name()))
CopyInstrumentParameters(calib, ws_name)
def setCalibrationWorkspace(self, ws_reference):
assert isinstance(ws_reference, Workspace)
# we do deep copy of singleton - to be removed in 8470
# this forces us to have 'copyable' objects.
self._newCalibrationWS = str(ws_reference)
def get_updated_beam_centre_after_move(self):
'''
@returns the beam centre position after the instrument has moved
'''
return self.beam_centre_pos1_after_move, self.beam_centre_pos2_after_move
def _add_parmeters_absent_in_calibration(self, ws_name, calib_name):
'''
We load the instrument specific Instrument Parameter File (IPF) and check if
there are any settings which the calibration workspace does not have. The calibration workspace
has its own parameter map stored in the nexus file. This means that if we add new
entries to the IPF, then they are not being picked up. We do not want to change the existing
values, just add new entries.
@param ws_name: the name of the main workspace with the data
@param calibration_workspace: the name of the calibration workspace
'''
if calib_name is None or ws_name is None:
return
workspace = mtd[ws_name]
calibration_workspace = mtd[calib_name]
# 1.Iterate over all parameters in the original workspace
# 2. Compare with the calibration workspace
# 3. If it does not exist, then add it
original_parmeters = workspace.getInstrument().getParameterNames()
for param in original_parmeters:
if not calibration_workspace.getInstrument().hasParameter(param):
self._add_new_parameter_to_calibration(param, workspace, calibration_workspace)
def _add_new_parameter_to_calibration(self, param_name, workspace, calibration_workspace):
'''
Adds the missing value from the Instrument Parameter File (IPF) of the workspace to
the IPF of the calibration workspace. We check for the
1. Type
2. Value
3. Name
4. ComponentName (which is the instrument)
@param param_name: the name of the parameter to add
@param workspace: the donor of the parameter
@param calibration_workspace: the receiver of the parameter
'''
ws_instrument = workspace.getInstrument()
component_name = ws_instrument.getName()
ipf_type = ws_instrument.getParameterType(param_name)
# For now we only expect string, int and double
type_ids = ["string", "int", "double"]
value = None
type_to_save = "Number"
if ipf_type == type_ids[0]:
value = ws_instrument.getStringParameter(param_name)
type_to_save = "String"
elif ipf_type == type_ids[1]:
value = ws_instrument.getIntParameter(param_name)
elif ipf_type == type_ids[2]:
value = ws_instrument.getNumberParameter(param_name)
else:
raise RuntimeError("ISISInstrument: An Instrument Parameter File value of unknown type"
"is trying to be copied. Cannot handle this currently.")
SetInstrumentParameter(Workspace=calibration_workspace,
ComponentName=component_name,
ParameterName=param_name,
ParameterType=type_to_save,
Value=str(value[0]))
def get_m4_monitor_det_ID(self):
"""
Gets the detecor ID associated with Monitor 4
@returns: the det ID of Monitor 4
"""
raise RuntimeError("Monitor 4 does not seem to be implemented.")
def _has_m4_monitor_in_idf(self, m4_name):
"""
Checks if the instrument contains a component with the M4 name
@param m4_name: the name of the M4 component
@returns true if it has an M4 component, else false
"""
return False if self.definition.getComponentByName(m4_name) is None else True
class LOQ(ISISInstrument):
"""
Contains all the LOQ specific data and functions
"""
_NAME = 'LOQ'
# minimum wavelength of neutrons assumed to be measurable by this instrument
WAV_RANGE_MIN = 2.2
# maximum wavelength of neutrons assumed to be measurable by this instrument
WAV_RANGE_MAX = 10.0
def __init__(self, idf_path='LOQ_Definition_20020226-.xml'):
"""
Reads LOQ's instrument definition xml file
@param idf_path: the idf file
@raise IndexError: if any parameters (e.g. 'default-incident-monitor-spectrum') aren't in the xml definition
"""
# The det id for the M4 monitor in LOQ
self._m4_det_id = 17788
self._m4_monitor_name = "monitor4"
super(LOQ, self).__init__(idf_path, self._m4_monitor_name)
# relates the numbers of the monitors to their names in the instrument definition file
self.monitor_names = {1: 'monitor1',
2: 'monitor2'}
if self.has_m4_monitor:
self.monitor_names.update({self._m4_det_id: self._m4_monitor_name})
elif self._m4_det_id in list(self.monitor_names.keys()):
del self.monitor_names[self._m4_det_id]
def on_load_sample(self, ws_name, beamcentre, isSample, other_centre=None):
"""It will be called just after loading the workspace for sample and can
It configures the instrument for the specific run of the workspace for handle historical changes in the instrument.
It centralizes the detector bank to the beamcentre (tuple of two values)
"""
ws_ref = mtd[str(ws_name)]
try:
run_num = LARMOR.get_run_number_from_workspace_reference(ws_ref)
except:
run_num = int(re.findall(r'\d+', str(ws_name))[0])
if isSample:
self.set_up_for_run(run_num)
if self._newCalibrationWS:
# We are about to transfer the Instrument Parameter File from the
# calibration to the original workspace. We want to add new parameters
# which the calibration file has not yet picked up.
# IMPORTANT NOTE: This takes the parameter settings from the original workspace
# if they are old too, then we don't pick up newly added parameters
self._add_parmeters_absent_in_calibration(ws_name, self._newCalibrationWS)
self.changeCalibration(ws_name)
# centralize the bank to the centre
if other_centre:
dummy_centre, centre_shift = self.move_components(ws_name, beamcentre[0], beamcentre[1],
xbeam_other=other_centre[0], ybeam_other=other_centre[1])
else:
dummy_centre, centre_shift = self.move_components(ws_name, beamcentre[0], beamcentre[1])
return centre_shift
def move_components(self, ws, xbeam, ybeam, xbeam_other=None, ybeam_other=None):
"""
Move the locations of the sample and detector bank based on the passed beam center
and information from the sample workspace logs
@param ws: workspace containing the instrument information
@param xbeam: x-position of the beam
@param ybeam: y-position of the beam
@return: the locations of (in the new coordinates) beam center, center of detector bank
"""
self.move_all_components(ws)
xshift = (317.5 / 1000.) - xbeam
yshift = (317.5 / 1000.) - ybeam
MoveInstrumentComponent(Workspace=ws,
ComponentName=self.cur_detector().name(),
X=xshift, Y=yshift, RelativePosition="1")
if ybeam_other and xbeam_other:
xshift_other = (317.5 / 1000.) - xbeam_other
yshift_other = (317.5 / 1000.) - ybeam_other
MoveInstrumentComponent(Workspace=ws,
ComponentName=self.other_detector().name(),
X=xshift_other, Y=yshift_other, RelativePosition="1")
# Have a separate move for x_corr, y_coor and z_coor just to make it more obvious in the
# history, and to expert users what is going on
det = self.cur_detector()
det_other = self.other_detector()
if det.x_corr != 0.0 or det.y_corr != 0.0 or det.z_corr != 0.0:
MoveInstrumentComponent(Workspace=ws, ComponentName=det.name(), X=det.x_corr / 1000.0,
Y=det.y_corr / 1000.0, Z=det.z_corr / 1000.0, RelativePosition="1")
if ybeam_other and xbeam_other:
MoveInstrumentComponent(Workspace=ws, ComponentName=det_other.name(), X=det_other.x_corr / 1000.0,
Y=det_other.y_corr / 1000.0, Z=det_other.z_corr / 1000.0, RelativePosition="1")
xshift = xshift + det.x_corr / 1000.0
yshift = yshift + det.y_corr / 1000.0
# Set the beam centre position afte the move, leave as they were
self.beam_centre_pos1_after_move = xbeam
self.beam_centre_pos2_after_move = ybeam
return [xshift, yshift], [xshift, yshift]
def elementary_displacement_of_single_component(self, workspace, component_name, coord1, coord2,
coord1_scale_factor=1., coord2_scale_factor=1.,
relative_displacement=True):
"""
A simple elementary displacement of a single component.