Ticket #10569: isis_instrument_RKH.py

File isis_instrument_RKH.py, 53.3 KB (added by Anders Markvardsen, 6 years ago)
Line 
1import datetime
2import math
3import os
4import re
5import sys
6import time
7import xml.dom.minidom
8
9from mantid.simpleapi import *
10from mantid.api import WorkspaceGroup, Workspace, ExperimentInfo
11from mantid.kernel import Logger
12import SANSUtility as su
13
14sanslog = Logger("SANS")
15
16class BaseInstrument(object):
17    def __init__(self, instr_filen=None):
18        """
19            Reads the instrument definition xml file
20            @param instr_filen: the name of the instrument definition file to read
21            @raise IndexError: if any parameters (e.g. 'default-incident-monitor-spectrum') aren't in the xml definition
22        """
23        if instr_filen is None:
24            instr_filen = self._NAME+'_Definition.xml'
25
26        config = ConfigService.Instance()
27        self._definition_file = os.path.join(config["instrumentDefinition.directory"], instr_filen)
28
29        inst_ws_name = self.load_empty()
30        self.definition = AnalysisDataService.retrieve(inst_ws_name).getInstrument()
31
32    def get_default_beam_center(self):
33        """
34            Returns the default beam center position, or the pixel location
35            of real-space coordinates (0,0).
36        """
37        return [0, 0]
38
39    def name(self):
40        """
41            Return the name of the instrument
42        """
43        return self._NAME
44
45    def versioned_name(self):
46        """
47        Hack-workaround so that we may temporarily display "SANS2DTUBES" as
48        an option in the instrument dropdown menu in the interface.  To be removed
49        as part of #9367.
50        """
51        if "SANS2D_Definition_Tubes" in self.idf_path:
52            return "SANS2DTUBES"
53        return self._NAME
54
55    def view(self, workspace_name = None):
56        """
57            Opens Mantidplot's InstrumentView displaying the current instrument. This
58            empty instrument created contained in the named workspace (a default name
59            is generated if this the argument is left blank) unless the workspace already
60            exists and then it's contents are displayed
61            @param workspace_name: the name of the workspace to create and/or display
62        """
63        if workspace_name is None:
64            workspace_name = self._NAME+'_instrument_view'
65            self.load_empty(workspace_name)
66        elif not AnalysisDataService.doesExist(workspace_name):
67            self.load_empty(workspace_name)
68
69        import mantidplot
70        instrument_win = mantidplot.getInstrumentView(workspace_name)
71        instrument_win.show()
72
73        return workspace_name
74
75    def load_empty(self, workspace_name = None):
76        """
77            Loads the instrument definition file into a workspace with the given name.
78            If no name is given a hidden workspace is used
79            @param workspace_name: the name of the workspace to create and/or display
80            @return the name of the workspace that was created
81        """
82        if workspace_name is None:
83            workspace_name = '__'+self._NAME+'_empty'
84
85        LoadEmptyInstrument(Filename=self._definition_file, OutputWorkspace=workspace_name)
86
87        return workspace_name
88
89
90class DetectorBank:
91    class _DectShape:
92        """
93            Stores the dimensions of the detector, normally this is a square
94            which is easy, but it can have a hole in it which is harder!
95        """
96        def __init__(self, width, height, isRect = True, n_pixels = None):
97            """
98                Sets the dimensions of the detector
99                @param width: the detector's width, spectra numbers along the width should increase in intervals of one
100                @param height: the detector's height, spectra numbers along the down the height should increase in intervals of width
101                @param isRect: true for rectangular or square detectors, i.e. number of pixels = width * height
102                @param n_pixels: optional for rectangular shapes because if it is not given it is calculated from the height and width in that case
103            """
104            self._width = width
105            self._height = height
106            self._isRect = bool(isRect)
107            self._n_pixels = n_pixels
108            if n_pixels is None:
109                if self._isRect:
110                    self._n_pixels = self._width*self._height
111                else:
112                    raise AttributeError('Number of pixels in the detector unknown, you must state the number of pixels for non-rectangular detectors')
113
114
115        def width(self):
116            """
117                read-only property getter, this object can't be altered
118            """
119            return self._width
120
121        def height(self):
122            return self._height
123
124        def isRectangle(self):
125            return self._isRect
126
127        def n_pixels(self):
128            return self._n_pixels
129
130    class _RescaleAndShift:
131        """
132            Stores property about the detector which is used to rescale and shift
133            data in the bank after data have been reduced. The scale attempts to
134            take into account that the relative efficiency of different banks may not
135            be the same. By default this scale is set to 1.0. The shift is strictly
136            speaking more an effect of the geometry of the sample than the detector
137            but is included here since both these are required to bring the scale+shift of reduced data
138            collected on front and rear bank on the same 'level' before e.g. merging
139            such data
140        """
141        def __init__(self, scale=1.0, shift=0.0, fitScale=False, fitShift=False, qMin=None, qMax=None):
142            """
143                @param scale: Default to 1.0. Value to multiply data with
144                @param shift: Default to 0.0. Value to add to data
145                @param fitScale: Default is False. Whether or not to try and fit this param
146                @param fitShift: Default is False. Whether or not to try and fit this param
147                @param qMin: When set to None (default) then for fitting use the overlapping q region of front and rear detectors
148                @param qMax: When set to None (default) then for fitting use the overlapping q region of front and rear detectors
149            """
150            self.scale = scale
151            self.shift = shift
152            self.fitScale = bool(fitScale)
153            self.fitShift = bool(fitShift)
154            self.qMin = qMin
155            self.qMax = qMax
156
157            if self.qMin == None or self.qMax == None:
158                self.qRangeUserSelected = False
159            else:
160                self.qRangeUserSelected = True
161
162
163    def __init__(self, instr, det_type):
164        #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
165        self._names = {
166          'uni' : det_type,
167          'long': instr.getStringParameter(det_type+'-detector-name')[0],
168          'short': instr.getStringParameter(det_type+'-detector-short-name')[0]}
169        #the bank is often also referred to by its location, as seen by the sample
170        if det_type.startswith('low'):
171            position = 'rear'
172        else:
173            position = 'front'
174        self._names['position'] = position
175
176        cols_data = instr.getNumberParameter(det_type+'-detector-num-columns')
177        if len(cols_data) > 0 :
178            rectanglar_shape = True
179            width = int(cols_data[0])
180        else :
181            rectanglar_shape = False
182            width = instr.getNumberParameter(det_type+'-detector-non-rectangle-width')[0]
183
184        rows_data = instr.getNumberParameter(det_type+'-detector-num-rows')
185        if len(rows_data) > 0 :
186            height = int(rows_data[0])
187        else:
188            rectanglar_shape = False
189            height = instr.getNumberParameter(det_type+'-detector-non-rectangle-height')[0]
190
191        n_pixels = None
192        n_pixels_override = instr.getNumberParameter(det_type+'-detector-num-pixels')
193        if len(n_pixels_override) > 0 :
194            n_pixels = int(n_pixels_override[0])
195        #n_pixels is normally None and calculated by DectShape but LOQ (at least) has a detector with a hole
196        self._shape = self._DectShape(width, height, rectanglar_shape, n_pixels)
197
198        spec_entry = instr.getNumberParameter('first-low-angle-spec-number')
199        if len(spec_entry) > 0 :
200            self.set_first_spec_num(int(spec_entry[0]))
201        else :
202            #'first-low-angle-spec-number' is an optimal instrument parameter
203            self.set_first_spec_num(0)
204
205        #needed for compatibility with SANSReduction and SANSUtily, remove
206        self.n_columns = width
207
208        #this can be set to the name of a file with correction factor against wavelength
209        self.correction_file = ''
210        #this corrections are set by the mask file
211        self.z_corr = 0.0
212        self.x_corr = 0.0
213        self._y_corr = 0.0
214        self._rot_corr = 0.0
215        #23/3/12 RKH add 2 more variables
216        self._radius_corr = 0.0
217        self._side_corr = 0.0
218                # 11/11/14 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
219        self.x_tilt = 0.0
220        self.y_tilt = 0.0
221
222        # hold rescale and shift object _RescaleAndShift
223        self.rescaleAndShift = self._RescaleAndShift()
224
225        #in the empty instrument detectors are laid out as below on loading a run the orientation becomes run dependent
226        self._orientation = 'HorizontalFlipped'
227
228    def disable_y_and_rot_corrs(self):
229        """
230            Not all corrections are supported on all detectors
231        """
232        self._y_corr = None
233        self._rot_corr = None
234        #23/3/12 RKH add 2 more variables
235        self._radius_corr = None
236        self._side_corr = None
237
238    def get_y_corr(self):
239        if not self._y_corr is None:
240            return self._y_corr
241        else:
242            raise NotImplementedError('y correction is not used for this detector')
243
244    def set_y_corr(self, value):
245        """
246            Only set the value if it isn't disabled
247            @param value: set y_corr to this value, unless it's disabled
248        """
249        if not self._y_corr is None:
250            self._y_corr = value
251
252    def get_rot_corr(self):
253        if not self._rot_corr is None:
254            return self._rot_corr
255        else:
256            raise NotImplementedError('rot correction is not used for this detector')
257
258    def set_rot_corr(self, value):
259        """
260            Only set the value if it isn't disabled
261            @param value: set rot_corr to this value, unless it's disabled
262        """
263        if not self._rot_corr is None:
264            self._rot_corr = value
265
266    # 22/3/12 RKH added two new variables radius_corr, side_corr
267    def get_radius_corr(self):
268        if not self._radius_corr is None:
269            return self._radius_corr
270        else:
271            raise NotImplementedError('radius correction is not used for this detector')
272
273    def set_radius_corr(self, value):
274        """
275            Only set the value if it isn't disabled
276            @param value: set radius_corr to this value, unless it's disabled
277        """
278        if not self._rot_corr is None:
279            self._radius_corr = value
280
281    def get_side_corr(self):
282        if not self._side_corr is None:
283            return self._side_corr
284        else:
285            raise NotImplementedError('side correction is not used for this detector')
286
287    def set_side_corr(self, value):
288        """
289            Only set the value if it isn't disabled
290            @param value: set side_corr to this value, unless it's disabled
291        """
292        if not self._side_corr is None:
293            self._side_corr = value
294
295
296    y_corr = property(get_y_corr, set_y_corr, None, None)
297    rot_corr = property(get_rot_corr , set_rot_corr, None, None)
298    # 22/3/12 RKH added 2 new variables
299    radius_corr = property(get_radius_corr , set_radius_corr, None, None)
300    side_corr = property(get_side_corr , set_side_corr, None, None)
301
302    def get_first_spec_num(self):
303        return self._first_spec_num
304
305    def set_first_spec_num(self, value):
306        self._first_spec_num = value
307        self.last_spec_num = self._first_spec_num + self._shape.n_pixels() - 1
308
309    def place_after(self, previousDet):
310        self.set_first_spec_num(previousDet.last_spec_num + 1)
311
312    def name(self, form = 'long') :
313        if form.lower() == 'inst_view' : form = 'long'
314        if not self._names.has_key(form) : form = 'long'
315
316        return self._names[form]
317
318    def isAlias(self, guess) :
319        """
320            Detectors are often referred to by more than one name, check
321            if the supplied name is in the list
322            @param guess: this name will be searched for in the list
323            @return : True if the name was found, otherwise false
324        """
325        for name in self._names.values() :
326            if guess.lower() == name.lower() :
327                return True
328        return False
329
330    def spectrum_block(self, ylow, xlow, ydim, xdim):
331        """
332            Compile a list of spectrum IDs for rectangular block of size xdim by ydim
333        """
334        if ydim == 'all':
335            ydim = self._shape.height()
336        if xdim == 'all':
337            xdim = self._shape.width()
338        det_dimension = self._shape.width()
339        base = self._first_spec_num
340
341        if not self._shape.isRectangle():
342            sanslog.warning('Attempting to block rows or columns in a non-rectangular detector, this is likely to give unexpected results!')
343
344        output = ''
345        if self._orientation == 'Horizontal':
346            start_spec = base + ylow*det_dimension + xlow
347            for y in range(0, ydim):
348                for x in range(0, xdim):
349                    output += str(start_spec + x + (y*det_dimension)) + ','
350        elif self._orientation == 'Vertical':
351            start_spec = base + xlow*det_dimension + ylow
352            for x in range(det_dimension - 1, det_dimension - xdim-1,-1):
353                for y in range(0, ydim):
354                    std_i = start_spec + y + ((det_dimension-x-1)*det_dimension)
355                    output += str(std_i ) + ','
356        elif self._orientation == 'Rotated':
357            # This is the horizontal one rotated so need to map the xlow and vlow to their rotated versions
358            start_spec = base + ylow*det_dimension + xlow
359            max_spec = det_dimension*det_dimension + base - 1
360            for y in range(0, ydim):
361                for x in range(0, xdim):
362                    std_i = start_spec + x + (y*det_dimension)
363                    output += str(max_spec - (std_i - base)) + ','
364        elif self._orientation == 'HorizontalFlipped':
365            for y in range(ylow,ylow+ydim):
366                max_row = base + (y+1)*det_dimension - 1
367                min_row = base + (y)*det_dimension
368                for x in range(xlow,xlow+xdim):
369                    std_i = base + x + (y*det_dimension)
370                    diff_s = std_i - min_row
371                    output += str(max_row - diff_s) + ','
372
373        return output.rstrip(",")
374
375    # Used to constrain the possible values of the orientation of the detector bank against the direction that spectrum numbers increase in
376    _ORIENTED = {
377        'Horizontal' : None,        #most runs have the detectors in this state
378        'Vertical' : None,
379        'Rotated' : None,
380        'HorizontalFlipped' : None} # This is for the empty instrument
381
382    def set_orien(self, orien):
383        """
384            Sets to relationship between the detectors and the spectra numbers. The relationship
385            is given by an orientation string and this function throws if the string is not recognised
386            @param orien: the orienation string must be a string contained in the dictionary _ORIENTED
387        """
388        dummy = self._ORIENTED[orien]
389        self._orientation = orien
390
391    def crop_to_detector(self, input_name, output_name=None):
392        """
393            Crops the workspace that is passed so that it only contains the spectra that correspond
394            to this detector
395            @param input_name: name of the workspace to crop
396            @param output_name: name the workspace will take (default is the input name)
397        """
398        if not output_name:
399            output_name = input_name
400
401        try:
402            wki = mtd[input_name]
403            #Is it really necessary to crop?
404            if (wki.getNumberHistograms() != self.last_spec_num - self.get_first_spec_num() + 1):
405                CropWorkspace(InputWorkspace=input_name,OutputWorkspace= output_name,
406                              StartWorkspaceIndex = self.get_first_spec_num() - 1,
407                              EndWorkspaceIndex = self.last_spec_num - 1)
408        except :
409            raise ValueError('Can not find spectra for %s in the workspace %s [%d,%d]\nException:'
410                             %(self.name(), input_name,self.get_first_spec_num(),self.last_spec_num)
411                             + str(sys.exc_info()))
412
413class ISISInstrument(BaseInstrument):
414    def __init__(self, filename=None):
415        """
416            Reads the instrument definition xml file
417            @param filename: the name of the instrument definition file to read
418            @raise IndexError: if any parameters (e.g. 'default-incident-monitor-spectrum') aren't in the xml definition
419        """
420        super(ISISInstrument, self).__init__(instr_filen=filename)
421
422        self.idf_path = self._definition_file
423
424        #the spectrum with this number is used to normalize the workspace data
425        self._incid_monitor = int(self.definition.getNumberParameter(
426            'default-incident-monitor-spectrum')[0])
427        self.cen_find_step = float(self.definition.getNumberParameter(
428            'centre-finder-step-size')[0])
429
430        firstDetect = DetectorBank(self.definition, 'low-angle')
431        #firstDetect.disable_y_and_rot_corrs()
432        secondDetect = DetectorBank(self.definition, 'high-angle')
433        secondDetect.place_after(firstDetect)
434        #add det_selection variable that will receive the DET/ REAR/FRONT/BOTH/MERGED
435        self.det_selection = 'REAR'
436        self.DETECTORS = {'low-angle' : firstDetect}
437        self.DETECTORS['high-angle'] = secondDetect
438
439        self.setDefaultDetector()
440        # 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
441        self._use_interpol_norm = False
442        #remove use_interpol_trans_calc once the beam centre finder has been converted
443        self.use_interpol_trans_calc = False
444
445        # the sample will be moved this distance a long the beam axis
446        self.SAMPLE_Z_CORR = 0
447
448        # Detector position information for SANS2D
449        # why are these not defined in SANS2D
450        self.FRONT_DET_RADIUS = 306.0
451        self.FRONT_DET_DEFAULT_SD_M = 4.0
452        self.FRONT_DET_DEFAULT_X_M = 1.1
453        self.REAR_DET_DEFAULT_SD_M = 4.0
454
455        # LOG files for SANS2D will have these encoder readings
456        # why are these not defined in SANS2D
457        self.FRONT_DET_X = 0.0
458        self.FRONT_DET_Z = 0.0
459        self.FRONT_DET_ROT = 0.0
460        self.REAR_DET_Z = 0.0
461        self.REAR_DET_X = 0
462
463        #spectrum number of the monitor used to as the incidient in the transmission calculations
464        self.default_trans_spec = int(self.definition.getNumberParameter(
465            'default-transmission-monitor-spectrum')[0])
466        self.incid_mon_4_trans_calc = self._incid_monitor
467
468        isis = config.getFacility('ISIS')
469        # Number of digits in standard file name
470        self.run_number_width = isis.instrument(self._NAME).zeroPadding(0)
471
472        #this variable isn't used again and stops the instrument from being deep copied if this instance is deep copied
473        self.definition = None
474
475        #remove this function
476        self._del_incidient_set = False
477
478        #it is possible to set the TOF regions that is assumed to be background for each monitors
479        self._back_ground = {}
480        # the default start region, used for any monitors that a specific one wasn't set for
481        self._back_start = None
482        # default end region
483        self._back_end = None
484        #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
485        self.monitor_zs = {}
486        # Used when new calibration required.
487        self._newCalibrationWS = None
488
489    def get_incident_mon(self):
490        """
491            @return: the spectrum number of the incident scattering monitor
492        """
493        return self._incid_monitor
494
495    def set_incident_mon(self, spectrum_number):
496        """
497            set the incident scattering monitor spectrum number regardless of
498            lock
499            @param spectrum_number: monitor's sectrum number
500        """
501        self._incid_monitor = int(spectrum_number)
502        self._del_incidient_set = True
503
504    def suggest_incident_mntr(self, spectrum_number):
505        """
506            remove this function and the data memember it uses
507        """
508        if not self._del_incidient_set:
509            self.set_incident_mon(spectrum_number)
510
511    def set_sample_offset(self, value):
512        """
513            @param value: sample value offset
514        """
515        self.SAMPLE_Z_CORR = float(value)/1000.
516
517    def is_interpolating_norm(self):
518        return self._use_interpol_norm
519
520    def set_interpolating_norm(self, on=True):
521        """
522            This method sets that the monitor spectrum should be interpolated before
523            normalisation
524        """
525        self._use_interpol_norm = on
526
527    def cur_detector(self):
528        if self.lowAngDetSet : return self.DETECTORS['low-angle']
529        else : return self.DETECTORS['high-angle']
530
531    def get_low_angle_detector(self):
532        """ Provide a direct way to get the low bank detector.
533        This method does not require to pass the name of the detector bank.
534        """
535        return self.DETECTORS['low-angle']
536
537    def get_high_angle_detector(self):
538        """ Provide a direct way to get the high bank detector
539        This method does not require to pass the name of the detector bank.
540        """
541        return self.DETECTORS['high-angle']
542
543    def other_detector(self) :
544        if not self.lowAngDetSet : return self.DETECTORS['low-angle']
545        else : return self.DETECTORS['high-angle']
546
547    def getDetector(self, requested) :
548        for n, detect in self.DETECTORS.iteritems():
549            if detect.isAlias(requested):
550                return detect
551        sanslog.notice("getDetector: Detector " + requested + "not found")
552
553    def listDetectors(self) :
554        return self.cur_detector().name(), self.other_detector().name()
555
556    def isHighAngleDetector(self, detName) :
557        if self.DETECTORS['high-angle'].isAlias(detName) :
558            return True
559
560    def isDetectorName(self, detName) :
561        if self.other_detector().isAlias(detName) :
562            return True
563
564        return self.cur_detector().isAlias(detName)
565
566    def setDetector(self, detName) :
567        self.det_selection = detName
568        if self.other_detector().isAlias(detName) :
569            self.lowAngDetSet = not self.lowAngDetSet
570            return True
571        else:
572            #there are only two detectors, they must have selected the current one so no change is need
573            if self.cur_detector().isAlias(detName):
574                return True
575            else:
576                sanslog.notice("setDetector: Detector not found")
577                sanslog.notice("setDetector: Detector set to " + self.cur_detector().name() + ' in ' + self.name())
578
579    def setDefaultDetector(self):
580        self.lowAngDetSet = True
581
582    def copy_correction_files(self):
583        """
584            Check if one of the efficiency files hasn't been set and assume the other is to be used
585        """
586        a = self.cur_detector()
587        b = self.other_detector()
588        if a.correction_file == '' and b.correction_file != '':
589            a.correction_file = b.correction_file != ''
590        if b.correction_file == '' and a.correction_file != '':
591            b.correction_file = a.correction_file != ''
592
593    def detector_file(self, det_name):
594        det = self.getDetector(det_name)
595        return det.correction_file
596
597    def get_TOFs(self, monitor):
598        """
599            Gets the start and end time of flights for the region assumed to contain
600            only background counts for this instrument
601            @param monitor: spectrum number of the monitor's spectrum
602            @return: the start time, the end time
603        """
604        monitor = int(monitor)
605        if self._back_ground.has_key(monitor):
606            return self._back_ground[int(monitor)]['start'], \
607                self._back_ground[int(monitor)]['end']
608        else:
609            return self._back_start, self._back_end
610
611    def set_TOFs(self, start, end, monitor=None):
612        """
613            Defines the start and end time of flights for the assumed background region
614            for this instrument
615            @param: start defines the start of the background region
616            @param: end defines the end
617            @param monitor: spectrum number of the monitor's spectrum, if none given affect the default
618        """
619        if start != None:
620            start = float(start)
621        if end != None:
622            end = float(end)
623
624        if monitor:
625            self._back_ground[int(monitor)] = { 'start' : start, 'end' : end }
626        else:
627            self._back_start = start
628            self._back_end = end
629
630    def reset_TOFs(self, monitor=None):
631        """
632            Reset background region set by set_TOFs
633            @param monitor: spectrum number of the monitor's spectrum, if none given affect the default
634        """
635        if monitor:
636            monitor = int(monitor)
637            if self._back_ground.has_key(monitor):
638                del self._back_ground[int(monitor)]
639        else:
640            self._back_ground = {}
641            self._back_start = None
642            self._back_end = None
643
644    def move_all_components(self, ws):
645        """
646            Move the sample object to the location set in the logs or user settings file
647            @param ws: the workspace containing the sample to move
648        """
649        MoveInstrumentComponent(Workspace=ws,ComponentName= 'some-sample-holder', Z = self.SAMPLE_Z_CORR, RelativePosition=True)
650
651        for i in self.monitor_zs.keys():
652            #get the current location
653            component = self.monitor_names[i]
654            ws = mtd[str(ws)]
655            mon = ws.getInstrument().getComponentByName(component)
656            z_loc = mon.getPos().getZ()
657            #now the relative move
658            offset = (self.monitor_zs[i]/1000.) - z_loc
659            MoveInstrumentComponent(Workspace=ws,ComponentName= component, Z = offset,
660                                    RelativePosition=True)
661
662    def move_components(self, ws, beamX, beamY):
663        """Define how to move the bank to position beamX and beamY must be implemented"""
664        raise RuntimeError("Not Implemented")
665
666    def cur_detector_position(self, ws_name):
667        """Return the position of the center of the detector bank"""
668        raise RuntimeError("Not Implemented")
669
670    def on_load_sample(self, ws_name, beamcentre, isSample):
671        """It will be called just after loading the workspace for sample and can
672
673        It configures the instrument for the specific run of the workspace for handle historical changes in the instrument.
674
675        It centralizes the detector bank to teh beamcentre (tuple of two values)
676        """
677        ws_ref = mtd[str(ws_name)]
678        try:
679            run_num = ws_ref.getRun().getLogData('run_number').value
680        except:
681            run_num = int(re.findall(r'\d+',str(ws_name))[-1])
682
683        if isSample:
684            self.set_up_for_run(run_num)
685
686        if self._newCalibrationWS:
687            self.changeCalibration(ws_name)
688
689        # centralize the bank to the centre
690        self.move_components(ws_name, beamcentre[0], beamcentre[1])
691
692    def load_transmission_inst(self, ws_trans, ws_direct, beamcentre):
693        """
694        Called on loading of transmissions
695        """
696        pass
697
698    def changeCalibration(self, ws_name):
699        calib = mtd[self._newCalibrationWS]
700        sanslog.notice("Applying new calibration for the detectors from " + str(calib.name()))
701        CopyInstrumentParameters(calib, ws_name)
702
703    def setCalibrationWorkspace(self, ws_reference):
704        assert(isinstance(ws_reference, Workspace))
705        # we do deep copy of singleton - to be removed in 8470
706        # this forces us to have 'copyable' objects.
707        self._newCalibrationWS = str(ws_reference)
708
709
710
711class LOQ(ISISInstrument):
712    """
713        Contains all the LOQ specific data and functions
714    """
715    _NAME = 'LOQ'
716    #minimum wavelength of neutrons assumed to be measurable by this instrument
717    WAV_RANGE_MIN = 2.2
718    #maximum wavelength of neutrons assumed to be measurable by this instrument
719    WAV_RANGE_MAX = 10.0
720
721    def __init__(self):
722        """
723            Reads LOQ's instrument definition xml file
724            @raise IndexError: if any parameters (e.g. 'default-incident-monitor-spectrum') aren't in the xml definition
725        """
726        super(LOQ, self).__init__('LOQ_Definition_20020226-.xml')
727        #relates the numbers of the monitors to their names in the instrument definition file
728        self.monitor_names = {1 : 'monitor1',
729                              2 : 'monitor2'}
730
731    def move_components(self, ws, xbeam, ybeam):
732        """
733            Move the locations of the sample and detector bank based on the passed beam center
734            and information from the sample workspace logs
735            @param ws: workspace containing the instrument information
736            @param xbeam: x-position of the beam
737            @param ybeam: y-position of the beam
738            @return: the locations of (in the new coordinates) beam center, center of detector bank
739        """
740        self.move_all_components(ws)
741
742        xshift = (317.5/1000.) - xbeam
743        yshift = (317.5/1000.) - ybeam
744        MoveInstrumentComponent(Workspace=ws,ComponentName= self.cur_detector().name(), X = xshift, Y = yshift, RelativePosition="1")
745
746        # Have a separate move for x_corr, y_coor and z_coor just to make it more obvious in the
747        # history, and to expert users what is going on
748        det = self.cur_detector()
749        if det.x_corr != 0.0 or det.y_corr != 0.0 or det.z_corr != 0.0:
750            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")
751            xshift = xshift + det.x_corr/1000.0
752            yshift = yshift + det.y_corr/1000.0
753
754        return [xshift, yshift], [xshift, yshift]
755
756    def get_marked_dets(self):
757        raise NotImplementedError('The marked detector list isn\'t stored for instrument '+self._NAME)
758
759    def set_up_for_run(self, base_runno):
760        """
761            Needs to run whenever a sample is loaded
762        """
763        first = self.DETECTORS['low-angle']
764        second = self.DETECTORS['high-angle']
765
766        first.set_orien('Horizontal')
767        #probably _first_spec_num was already set to this when the instrument parameter file was loaded
768        first.set_first_spec_num(3)
769        second.set_orien('Horizontal')
770        second.place_after(first)
771
772    def load_transmission_inst(self, ws_trans, ws_direct, beamcentre):
773        """
774            Loads information about the setup used for LOQ transmission runs
775        """
776        trans_definition_file = os.path.join(config.getString('instrumentDefinition.directory'), self._NAME+'_trans_Definition.xml')
777        LoadInstrument(Workspace=ws_trans,Filename= trans_definition_file, RewriteSpectraMap=False)
778        LoadInstrument(Workspace=ws_direct, Filename = trans_definition_file, RewriteSpectraMap=False)
779
780    def cur_detector_position(self, ws_name):
781        """Return the position of the center of the detector bank"""
782        ws = mtd[ws_name]
783        pos = ws.getInstrument().getComponentByName(self.cur_detector().name()).getPos()
784        cent_pos = 317.5/1000.0
785        return [cent_pos - pos.getX(), cent_pos - pos.getY()]
786
787class SANS2D(ISISInstrument):
788    """
789        The SANS2D instrument has movable detectors whose locations have to
790        be read in from the workspace logs (Run object)
791    """
792    _NAME = 'SANS2D'
793    WAV_RANGE_MIN = 2.0
794    WAV_RANGE_MAX = 14.0
795
796    def __init__(self, idf_path=None):
797        super(SANS2D, self).__init__(idf_path)
798
799        self._marked_dets = []
800        # set to true once the detector positions have been moved to the locations given in the sample logs
801        self.corrections_applied = False
802        # a warning is issued if the can logs are not the same as the sample
803        self._can_logs = {}
804        #The user can set the distance between monitor 4 and the rear detector in millimetres, should be negative
805        self.monitor_4_offset = None
806        #relates the numbers of the monitors to their names in the instrument definition file
807        self.monitor_names = {1 : 'monitor1',
808                              2 : 'monitor2',
809                              3 : 'monitor3',
810                              4 : 'monitor4'}
811
812    def set_up_for_run(self, base_runno):
813        """
814            Handles changes required when a sample is loaded, both generic
815            and run specific
816        """
817        first = self.DETECTORS['low-angle']
818        second = self.DETECTORS['high-angle']
819
820        try:
821            base_runno = int(base_runno)
822            #first deal with some special cases
823            if base_runno < 568:
824                self.set_incident_mon(73730)
825                first.set_first_spec_num(1)
826                first.set_orien('Vertical')
827                second.set_orien('Vertical')
828            elif (base_runno >= 568 and base_runno < 684):
829                first.set_first_spec_num(9)
830                first.set_orien('Rotated')
831                second.set_orien('Rotated')
832            else:
833                #this is the default case
834                first.set_first_spec_num(9)
835                first.set_orien('Horizontal')
836                second.set_orien('Horizontal')
837        except ValueError:
838            #this is the default case
839            first.set_first_spec_num(9)
840            first.set_orien('Horizontal')
841            # empty instrument number spectra differently.
842            if base_runno == 'emptyInstrument':
843                second.set_orien('HorizontalFlipped')
844            else:
845                second.set_orien('Horizontal')
846
847        #as spectrum numbers of the first detector have changed we'll move those in the second too
848        second.place_after(first)
849
850    def getDetValues(self, ws_name):
851        """
852        Retrive the values of Front_Det_Z, Front_Det_X, Front_Det_Rot, Rear_Det_Z and Rear_Det_X from
853        the workspace. If it does not find the value at the run info, it takes as default value the
854        self.FRONT_DET_Z, self... which are extracted from the sample workspace at apply_detector_log.
855
856        This is done to allow the function move_components to use the correct values and not to use
857        all the values for TRANS ans SAMPLE the same, as sometimes, this assumption is not valid.
858
859        The reason for this method is explained at the ticket http://trac.mantidproject.org/mantid/ticket/7314.
860        """
861        # set the default value for these variables
862        values = [self.FRONT_DET_Z, self.FRONT_DET_X, self.FRONT_DET_ROT, self.REAR_DET_Z, self.REAR_DET_X]
863        # get these variables from the workspace run
864        run_info = mtd[str(ws_name)].run()
865        ind = 0
866        for name in ('Front_Det_Z', 'Front_Det_X', 'Front_Det_Rot',
867                     'Rear_Det_Z','Rear_Det_X'):
868            try:
869                var = run_info.get(name).value
870                if hasattr(var, '__iter__'):
871                    var = var[-1]
872                values[ind] = float(var)
873            except:
874                pass # ignore, because we do have a default value
875            ind+=1
876        #return these variables
877        return tuple(values)
878
879
880    def  move_components(self, ws, xbeam, ybeam):
881        """
882            Move the locations of the sample and detector bank based on the passed beam center
883            and information from the sample workspace logs. If the location of the monitor was
884            set with TRANS/TRANSPEC=4/SHIFT=... this function does the move instrument
885            @param ws: workspace containing the instrument information
886            @param xbeam: x-position of the beam in meters
887            @param ybeam: y-position of the beam in meters
888            @return: the locations of (in the new coordinates) beam center, center of detector bank
889        """
890        frontDet = self.getDetector('front')
891        rearDet = self.getDetector('rear')
892
893        FRONT_DET_Z, FRONT_DET_X, FRONT_DET_ROT, REAR_DET_Z, REAR_DET_X = self.getDetValues(ws)
894
895        # Deal with front detector
896
897        # 11/11/14 RKH need to add tilt of detector, in degrees, with respect to the horizontal or vertical of the detector plane
898        # this time we can rotate about the detector's own axis so can use RotateInstrumentComponent, ytilt rotates about x axis, xtilt rotates about z axis
899        #
900        RotateInstrumentComponent(Workspace=ws,ComponentName= self.getDetector('front').name(), X = "1.", Y = "0.", Z = "0.", Angle = frontDet.y_tilt)
901        RotateInstrumentComponent(Workspace=ws,ComponentName= self.getDetector('front').name(), X = "0.", Y = "0.", Z = "1.", Angle = frontDet.x_tilt)
902       
903        # 9/1/2  this all dates to Richard Heenan & Russell Taylor's original python development for SANS2d
904        # the rotation axis on the SANS2d front detector is actually set front_det_radius = 306mm behind the detector.
905        # Since RotateInstrumentComponent will only rotate about the centre of the detector, we have to to the rest here.
906        # rotate front detector according to value in log file and correction value provided in user file
907        rotateDet = (-FRONT_DET_ROT - frontDet.rot_corr)
908        RotateInstrumentComponent(Workspace=ws,ComponentName= self.getDetector('front').name(), X="0.", Y="1.0", Z="0.", Angle=rotateDet)
909        RotRadians = math.pi*(FRONT_DET_ROT + frontDet.rot_corr)/180.
910        # The rear detector is translated to the beam position using the beam centre coordinates in the user file.
911        # (Note that the X encoder values in NOT used for the rear detector.)
912        # The front detector is translated using the difference in X encoder values, with a correction from the user file.
913        # 21/3/12 RKH [In reality only the DIFFERENCE in X encoders is used, having separate X corrections for both detectors is unnecessary,
914        # but we will continue with this as it makes the mask file smore logical and avoids a retrospective change.]
915        # 21/3/12 RKH add .side_corr    allows rotation axis of the front detector being offset from the detector X=0
916        # this inserts *(1.0-math.cos(RotRadians)) into xshift, and
917        # - frontDet.side_corr*math.sin(RotRadians) into zshift.
918        # (Note we may yet need to introduce further corrections for parallax errors in the detectors, which may be wavelength dependent!)
919        xshift = (REAR_DET_X + rearDet.x_corr -frontDet.x_corr - FRONT_DET_X  -frontDet.side_corr*(1-math.cos(RotRadians)) + (self.FRONT_DET_RADIUS +frontDet.radius_corr)*math.sin(RotRadians) )/1000. - self.FRONT_DET_DEFAULT_X_M - xbeam
920        yshift = (frontDet.y_corr/1000.  - ybeam)
921        # Note don't understand the comment below (9/1/12 these are comments from the original python code, you may remove them if you like!)
922        # default in instrument description is 23.281m - 4.000m from sample at 19,281m !
923        # need to add ~58mm to det1 to get to centre of detector, before it is rotated.
924        # 21/3/12 RKH add .radius_corr       
925        zshift = (FRONT_DET_Z + frontDet.z_corr + (self.FRONT_DET_RADIUS +frontDet.radius_corr)*(1 - math.cos(RotRadians)) - frontDet.side_corr*math.sin(RotRadians))/1000.
926        zshift -= self.FRONT_DET_DEFAULT_SD_M
927        MoveInstrumentComponent(Workspace=ws,ComponentName= self.getDetector('front').name(), X = xshift, Y = yshift, Z = zshift, RelativePosition="1")
928               
929               
930        # deal with rear detector
931
932        # 11/11/14 RKH need to add tilt of detector, in degrees, with respect to the horizontal or vertical of the detector plane
933        # Best to do the tilts first, while the detector is still centred on the z axis, ytilt rotates about x axis, xtilt rotates about z axis
934        # NOTE the beam centre coordinates may change
935        RotateInstrumentComponent(Workspace=ws,ComponentName= rearDet.name(), X = "1.", Y = "0.", Z = "0.", Angle = rearDet.y_tilt)
936        RotateInstrumentComponent(Workspace=ws,ComponentName= rearDet.name(), X = "0.", Y = "0.", Z = "1.", Angle = rearDet.x_tilt)
937       
938        xshift = -xbeam
939        yshift = -ybeam
940        zshift = (REAR_DET_Z + rearDet.z_corr)/1000.
941        zshift -= self.REAR_DET_DEFAULT_SD_M
942        sanslog.notice("Setup move "+str(xshift*1000.)+" "+str(yshift*1000.)+" "+str(zshift*1000.))
943        MoveInstrumentComponent(Workspace=ws,ComponentName= rearDet.name(), X = xshift, Y = yshift, Z = zshift, RelativePosition="1")
944
945
946        self.move_all_components(ws)
947
948        #this implements the TRANS/TRANSPEC=4/SHIFT=... line, this overrides any other monitor move
949        if self.monitor_4_offset:
950            #get the current location of the monitor
951            component = 'monitor4'
952            ws = mtd[str(ws)]
953            mon = ws.getInstrument().getComponentByName(component)
954            z_orig = mon.getPos().getZ()
955
956            #the location is relative to the rear-detector, get its location
957            det = ws.getInstrument().getComponentByName(self.cur_detector().name())
958            det_z = det.getPos().getZ()
959
960            monitor_4_offset = self.monitor_4_offset/1000.
961            z_new = det_z + monitor_4_offset
962            z_move = z_new - z_orig
963            MoveInstrumentComponent(Workspace=ws,ComponentName= component, Z=z_move,
964                                    RelativePosition=True)
965            sanslog.notice('Monitor 4 is at z = ' + str(z_new) )
966
967        # Are these returned values used anywhere?
968        if self.cur_detector().name() == 'front-detector':
969            beam_cen = [0.0, 0.0]
970            det_cen = [0.0, 0.0]
971        else:
972            beam_cen = [0.0,0.0]
973            det_cen = [-xbeam, -ybeam]
974
975        return beam_cen, det_cen
976
977    def get_detector_log(self, wksp):
978        """
979            Reads information about the state of the instrument on the information
980            stored in the sample
981            @param logs: a workspace pointer
982            @return the values that were read as a dictionary
983        """
984        self._marked_dets = []
985        wksp = su.getWorkspaceReference(wksp)
986        #assume complete log information is stored in the first entry, it isn't stored in the group workspace itself
987        if isinstance(wksp, WorkspaceGroup):
988            wksp = wksp[0]
989
990        samp = wksp.getRun()
991
992        logvalues = {}
993        logvalues['Front_Det_Z'] = self._get_const_num(samp, 'Front_Det_Z')
994        logvalues['Front_Det_X'] = self._get_const_num(samp, 'Front_Det_X')
995        logvalues['Front_Det_Rot'] = self._get_const_num(samp, 'Front_Det_Rot')
996        logvalues['Rear_Det_Z'] = self._get_const_num(samp, 'Rear_Det_Z')
997        logvalues['Rear_Det_X'] = self._get_const_num(samp, 'Rear_Det_X')
998
999        return logvalues
1000
1001    def _get_const_num(self, log_data, log_name):
1002        """
1003            Get a the named entry from the log object. If the entry is a
1004            time series it's assumed to contain unchanging data and the first
1005            value is used. The answer must be convertible to float otherwise
1006            this throws.
1007            @param log_data: the sample object from a workspace
1008            @param log_name: a string with the name of the individual entry to load
1009            @return: the floating point number
1010            @raise TypeError: if that log entry can't be converted to a float
1011        """
1012        try:
1013            # return the log value if it stored as a single number
1014            return float(log_data.getLogData(log_name).value)
1015        except TypeError:
1016            # Python 2.4 doesn't have datetime.strptime...
1017            def format_date(date_string, format, date_str_len):
1018                if len(date_string)>date_str_len:
1019                    date_string = date_string[:date_str_len]
1020                from datetime import datetime
1021                if sys.version_info[0] == 2 and sys.version_info[1] <  5:
1022                    import time
1023                    return datetime(*(time.strptime(date_string, format)[0:6]))
1024                else:
1025                    return datetime.strptime(date_string, format)
1026
1027            # if the value was stored as a time series we have an array here
1028            property = log_data.getLogData(log_name)
1029
1030            size = len(property.value)
1031            if size == 1:
1032                return float(log_data.getLogData(log_name).value[0])
1033
1034            start = log_data.getLogData('run_start')
1035            dt_0 = format_date(start.value,"%Y-%m-%dT%H:%M:%S",19)
1036            for i in range(0, size):
1037                dt = format_date(str(property.times[i]),"%Y-%m-%dT%H:%M:%S",19)
1038                if dt > dt_0:
1039                    if i == 0:
1040                        return float(log_data.getLogData(log_name).value[0])
1041                    else:
1042                        return float(log_data.getLogData(log_name).value[i-1])
1043
1044            # this gets executed if all entries is before the start-time
1045            return float(log_data.getLogData(log_name).value[size-1])
1046
1047    def apply_detector_logs(self, logvalues):
1048        #apply the corrections that came from the logs
1049        self.FRONT_DET_Z = float(logvalues['Front_Det_Z'])
1050        self.FRONT_DET_X = float(logvalues['Front_Det_X'])
1051        self.FRONT_DET_ROT = float(logvalues['Front_Det_Rot'])
1052        self.REAR_DET_Z = float(logvalues['Rear_Det_Z'])
1053        self.REAR_DET_X = float(logvalues['Rear_Det_X'])
1054        self.corrections_applied = True
1055        if len(self._can_logs) > 0:
1056            self.check_can_logs(self._can_logs)
1057
1058    def check_can_logs(self, new_logs):
1059        """
1060            Tests if applying the corrections from the passed logvalues
1061            would give the same result as the corrections that were
1062            already made
1063            @param new_logs: the new values to check are equivalent
1064            @return: True if the are the same False if not
1065        """
1066        if not self.corrections_applied:
1067            #the check needs to wait until there's something to compare against
1068            self._can_logs = new_logs
1069
1070        if len(new_logs) == 0:
1071            return False
1072
1073        existing_values = []
1074        existing_values.append(self.FRONT_DET_Z)
1075        existing_values.append(self.FRONT_DET_X)
1076        existing_values.append(self.FRONT_DET_ROT)
1077        existing_values.append(self.REAR_DET_Z)
1078        existing_values.append(self.REAR_DET_X)
1079
1080        new_values = []
1081        new_values.append(float(new_logs['Front_Det_Z']))
1082        new_values.append(float(new_logs['Front_Det_X']))
1083        new_values.append(float(new_logs['Front_Det_Rot']))
1084        new_values.append(float(new_logs['Rear_Det_Z']))
1085        new_values.append(float(new_logs['Rear_Det_X']))
1086
1087        errors = 0
1088        corr_names = ['Front_Det_Z', 'Front_Det_X','Front_Det_Rot', 'Rear_Det_Z', 'Rear_Det_X']
1089        for i in range(0, len(existing_values)):
1090            if math.fabs(existing_values[i] - new_values[i]) > 5e-04:
1091                sanslog.warning('values differ between sample and can runs: Sample ' + corr_names[i] + ' = ' + str(existing_values[i]) + \
1092                    ', can value is ' + str(new_values[i]))
1093                errors += 1
1094
1095                self.append_marked(corr_names[i])
1096
1097        #the check has been done clear up
1098        self._can_logs = {}
1099
1100        return errors == 0
1101
1102    def append_marked(self, detNames):
1103        self._marked_dets.append(detNames)
1104
1105    def get_marked_dets(self):
1106        return self._marked_dets
1107
1108    def load_transmission_inst(self, ws_trans, ws_direct, beamcentre):
1109        """
1110        SANS2D requires the centralize the detectors of the transmission
1111        as well as the sample and can.
1112        """
1113        self.move_components(ws_trans, beamcentre[0], beamcentre[1])
1114        if ws_trans != ws_direct:
1115            self.move_components(ws_direct, beamcentre[0], beamcentre[1])
1116
1117
1118    def cur_detector_position(self, ws_name):
1119        """Return the position of the center of the detector bank"""
1120        ws = mtd[ws_name]
1121        pos = ws.getInstrument().getComponentByName(self.cur_detector().name()).getPos()
1122
1123        return [-pos.getX(), -pos.getY()]
1124
1125    def on_load_sample(self, ws_name, beamcentre, isSample):
1126        """For SANS2D in addition of the operations defines in on_load_sample of ISISInstrument
1127        it has to deal with the log, which defines some offsets for the movement of the
1128        detector bank.
1129        """
1130        ws_ref = mtd[str(ws_name)]
1131        try:
1132            log = self.get_detector_log(ws_ref)
1133            if log == "":
1134                raise "Invalid log"
1135        except:
1136            if isSample:
1137                raise RuntimeError('Sample logs cannot be loaded, cannot continue')
1138            else:
1139                logger.warning("Can logs could not be loaded, using sample values.")
1140
1141
1142        if isSample:
1143            self.apply_detector_logs(log)
1144        else:
1145            self.check_can_logs(log)
1146
1147
1148        ISISInstrument.on_load_sample(self, ws_name, beamcentre,  isSample)
1149
1150
1151class LARMOR(ISISInstrument):
1152    _NAME = 'LARMOR'
1153    WAV_RANGE_MIN = 2.2
1154    WAV_RANGE_MAX = 10.0
1155    def __init__(self):
1156        super(LARMOR,self).__init__('LARMOR_Definition.xml')
1157        self.monitor_names = dict()
1158
1159        for i in range(1,6):
1160            self.monitor_names[i] = 'monitor'+str(i)
1161
1162    def set_up_for_run(self, base_runno):
1163        """
1164            Needs to run whenever a sample is loaded
1165        """
1166        first = self.DETECTORS['low-angle']
1167        second = self.DETECTORS['high-angle']
1168
1169        first.set_orien('Horizontal')
1170        first.set_first_spec_num(10)
1171        second.set_orien('Horizontal')
1172        second.place_after(first)
1173
1174    def move_components(self, ws, xbeam, ybeam):
1175        self.move_all_components(ws)
1176
1177        detBanch = self.getDetector('rear')
1178
1179        xshift = -xbeam
1180        yshift = -ybeam
1181        #zshift = ( detBanch.z_corr)/1000.
1182        #zshift -= self.REAR_DET_DEFAULT_SD_M
1183        zshift = 0
1184        sanslog.notice("Setup move " + str(xshift*1000) + " " + str(yshift*1000) + " " + str(zshift*1000))
1185        MoveInstrumentComponent(ws, ComponentName=detBanch.name(), X=xshift,
1186                                Y=yshift, Z=zshift)
1187        # beam centre, translation
1188        return [0.0, 0.0], [-xbeam, -ybeam]
1189
1190    def cur_detector_position(self, ws_name):
1191        """Return the position of the center of the detector bank"""
1192        ws = mtd[ws_name]
1193        pos = ws.getInstrument().getComponentByName(self.cur_detector().name()).getPos()
1194
1195        return [-pos.getX(), -pos.getY()]
1196
1197class LARMOR(ISISInstrument):
1198    _NAME = 'LARMOR'
1199    WAV_RANGE_MIN = 2.2
1200    WAV_RANGE_MAX = 10.0
1201    def __init__(self):
1202        super(LARMOR,self).__init__('LARMOR_Definition.xml')
1203        self.monitor_names = dict()
1204
1205        for i in range(1,6):
1206            self.monitor_names[i] = 'monitor'+str(i)
1207
1208    def set_up_for_run(self, base_runno):
1209        """
1210            Needs to run whenever a sample is loaded
1211        """
1212        first = self.DETECTORS['low-angle']
1213        second = self.DETECTORS['high-angle']
1214
1215        first.set_orien('Horizontal')
1216        first.set_first_spec_num(10)
1217        second.set_orien('Horizontal')
1218        second.place_after(first)
1219
1220    def move_components(self, ws, xbeam, ybeam):
1221        self.move_all_components(ws)
1222
1223        detBanch = self.getDetector('rear')
1224
1225        xshift = -xbeam
1226        yshift = -ybeam
1227        #zshift = ( detBanch.z_corr)/1000.
1228        #zshift -= self.REAR_DET_DEFAULT_SD_M
1229        zshift = 0
1230        sanslog.notice("Setup move " + str(xshift*1000) + " " + str(yshift*1000) + " " + str(zshift*1000))
1231        MoveInstrumentComponent(ws, ComponentName=detBanch.name(), X=xshift,
1232                                Y=yshift, Z=zshift)
1233        # beam centre, translation
1234        return [0.0, 0.0], [-xbeam, -ybeam]
1235
1236    def load_transmission_inst(self, ws_trans, ws_direct, beamcentre):
1237        """
1238            Not required for SANS2D
1239        """
1240        pass
1241
1242    def cur_detector_position(self, ws_name):
1243        """Return the position of the center of the detector bank"""
1244        ws = mtd[ws_name]
1245        pos = ws.getInstrument().getComponentByName(self.cur_detector().name()).getPos()
1246
1247        return [-pos.getX(), -pos.getY()]
1248
1249
1250
1251if __name__ == '__main__':
1252    pass