mask_workspace.py 18 KB
Newer Older
1
2
3
4
5
6
# 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
# SPDX - License - Identifier: GPL - 3.0 +
7
from __future__ import (absolute_import, division, print_function)
8

9
from abc import (ABCMeta, abstractmethod)
10
11
12

from six import with_metaclass

13
14
from mantid.kernel import Logger

15
16
from sans.algorithm_detail.mask_functions import SpectraBlock
from sans.algorithm_detail.xml_shapes import (add_cylinder, add_outside_cylinder, create_phi_mask, create_line_mask)
17
from sans.common.constants import EMPTY_NAME
18
from sans.common.enums import SANSInstrument
19
from sans.common.file_information import (find_full_file_path, get_instrument_paths_for_sans_file)
20
from sans.common.general_functions import create_unmanaged_algorithm
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42


# ------------------------------------------------------------------
# Free functions
# ------------------------------------------------------------------
def mask_bins(mask_info, workspace, detector_type):
    """
    Masks the bins on a workspace

    There are two parts to this:
    1. A general time mask is applied to all spectra
    2. A detector-specific time mask is applied depending on the detector_type
    :param mask_info: a SANSStateMask object
    :param workspace: the workspace which is about to be masked.
    :param detector_type: the detector which is currently being investigated and which (potentially) requires
                          additional masking
    :return: the workspace
    """
    # Mask the bins with the general setting
    bin_mask_general_start = mask_info.bin_mask_general_start
    bin_mask_general_stop = mask_info.bin_mask_general_stop
    # Mask the bins with the detector-specific setting
43
44
    bin_mask_start = mask_info.detectors[detector_type.value].bin_mask_start
    bin_mask_stop = mask_info.detectors[detector_type.value].bin_mask_stop
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

    # Combine the settings and run the binning
    start_mask = []
    stop_mask = []
    if bin_mask_general_start and bin_mask_general_stop:
        start_mask.extend(bin_mask_general_start)
        stop_mask.extend(bin_mask_general_stop)

    if bin_mask_start and bin_mask_stop:
        start_mask.extend(bin_mask_start)
        stop_mask.extend(bin_mask_stop)

    mask_name = "MaskBins"
    mask_options = {"InputWorkspace": workspace}
    mask_alg = create_unmanaged_algorithm(mask_name, **mask_options)
    for start, stop in zip(start_mask, stop_mask):
        # Bin mask should be operated in place
        mask_alg.setProperty("InputWorkspace", workspace)
        mask_alg.setPropertyValue("OutputWorkspace", EMPTY_NAME)
        mask_alg.setProperty("OutputWorkspace", workspace)
        mask_alg.setProperty("XMin", start)
        mask_alg.setProperty("XMax", stop)
        mask_alg.execute()
        workspace = mask_alg.getProperty("OutputWorkspace").value
    return workspace


def mask_cylinder(mask_info, workspace):
    """
    Masks  a (hollow) cylinder around (0,0)

    Two radii can be specified for the cylinder mask. An inner radius (radius_min) and an outer radius(radius_max)
    which specify a hollow cylinder mask.
    :param mask_info: a SANSStateMask object.
    :param workspace: the workspace which is about to be masked
    :return: the masked workspace.
    """
    radius_min = mask_info.radius_min
    radius_max = mask_info.radius_max

    xml = []
    # Set up the inner radius of the cylinder
    if radius_min is not None and radius_min > 0.0:
        add_cylinder(xml, radius_min, 0, 0, 'beam_stop')

    # Set up the outer radius of the cylinder
    if radius_max is not None and radius_max > 0.0:
        add_outside_cylinder(xml, radius_max, 0, 0, 'beam_area')

    # Mask the cylinder shape if there is anything to mask, else don't do anything
    if xml:
        mask_name = "MaskDetectorsInShape"
        mask_options = {"Workspace": workspace}
        mask_alg = create_unmanaged_algorithm(mask_name, **mask_options)
        for shape in xml:
            mask_alg.setProperty("Workspace", workspace)
            mask_alg.setProperty("ShapeXML", shape)
            mask_alg.execute()
            workspace = mask_alg.getProperty("Workspace").value
    return workspace


def mask_with_mask_files(mask_info, workspace):
    """
    Apply mask files to the workspace

    Rolling our own MaskDetectors wrapper since masking is broken in a couple
    of places that affect us here.
    Calling MaskDetectors(Workspace=ws_name, MaskedWorkspace=mask_ws_name) is
    not something we can do because the algorithm masks by ws index rather than
    detector id, and unfortunately for SANS the detector table is not the same
    for MaskingWorkspaces as it is for the workspaces containing the data to be
    masked.  Basically, we get a mirror image of what we expect.  Instead, we
    have to extract the det IDs and use those via the DetectorList property.
    :param mask_info: a SANSStateMask object.
    :param workspace: the workspace to be masked.
    :return: the masked workspace.
    """
    mask_files = mask_info.mask_files
    if mask_files:
        idf_path = mask_info.idf_path

        # Mask loader
        load_name = "LoadMask"
        load_options = {"Instrument": idf_path,
                        "OutputWorkspace": EMPTY_NAME}
        load_alg = create_unmanaged_algorithm(load_name, **load_options)
132
133
134
        dummy_params = {"OutputWorkspace": EMPTY_NAME}
        mask_alg = create_unmanaged_algorithm("MaskInstrument", **dummy_params)
        clear_alg = create_unmanaged_algorithm("ClearMaskedSpectra", **dummy_params)
135
136
137
138
139
140
141
142
143

        # Masker
        for mask_file in mask_files:
            mask_file = find_full_file_path(mask_file)

            # Get the detector ids which need to be masked
            load_alg.setProperty("InputFile", mask_file)
            load_alg.execute()
            masking_workspace = load_alg.getProperty("OutputWorkspace").value
144
145
146
            # Could use MaskDetectors directly with masking_workspace but it does not
            # support MPI. Use a three step approach via a, b, and c instead.
            # a) Extract detectors to mask from MaskWorkspace
147
            det_ids = masking_workspace.getMaskedDetectors()
148
149
150
151
            # b) Mask the detector ids on the instrument
            mask_alg.setProperty("InputWorkspace", workspace)
            mask_alg.setProperty("OutputWorkspace", workspace)
            mask_alg.setProperty("DetectorIDs", det_ids)
152
            mask_alg.execute()
153
154
155
156
157
158
            workspace = mask_alg.getProperty("OutputWorkspace").value
        # c) Clear data in all spectra associated with masked detectors
        clear_alg.setProperty("InputWorkspace", workspace)
        clear_alg.setProperty("OutputWorkspace", workspace)
        clear_alg.execute()
        workspace = clear_alg.getProperty("OutputWorkspace").value
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
    return workspace


def mask_spectra(mask_info, workspace, spectra_block, detector_type):
    """
    Masks particular spectra on the workspace.

    There are several spectra specifications which need to be evaluated
    1. General singular spectrum numbers
    2. General spectrum ranges
    3. Detector-specific horizontal singular strips
    4. Detector-specific horizontal range strips
    5. Detector-specific vertical singular strips
    6. Detector-specific vertical range strips
    7. Blocks
    8. Cross Blocks
    :param mask_info: a SANSStateMask object.
    :param workspace: the workspace to be masked.
    :param spectra_block: a SpectraBlock object, which contains instrument information to
                          calculate the selected spectra.
    :param detector_type: the selected detector type
    :return: the masked workspace.
    """

    total_spectra = []

185
    # All masks are detector-specific, hence we pull out only the relevant part
186
    detector = mask_info.detectors[detector_type.value]
187

188
189
190
    # ----------------------
    # Single spectra
    # -----------------------
191
    single_spectra = detector.single_spectra
192
193
194
195
196
197
    if single_spectra:
        total_spectra.extend(single_spectra)

    # ----------------------
    # Spectrum range
    # -----------------------
198
199
    spectrum_range_start = detector.spectrum_range_start
    spectrum_range_stop = detector.spectrum_range_stop
200
201
    if spectrum_range_start and spectrum_range_stop:
        for start, stop in zip(spectrum_range_start, spectrum_range_stop):
202
            total_spectra.extend(list(range(start, stop + 1)))
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

    # ---------------------------
    # Horizontal single spectrum
    # ---------------------------
    single_horizontal_strip_masks = detector.single_horizontal_strip_mask
    if single_horizontal_strip_masks:
        for single_horizontal_strip_mask in single_horizontal_strip_masks:
            total_spectra.extend(spectra_block.get_block(single_horizontal_strip_mask, 0, 1, None))

    # ---------------------------
    # Vertical single spectrum
    # ---------------------------
    single_vertical_strip_masks = detector.single_vertical_strip_mask
    if single_vertical_strip_masks:
        for single_vertical_strip_mask in single_vertical_strip_masks:
            total_spectra.extend(spectra_block.get_block(0, single_vertical_strip_mask, None, 1))

    # ---------------------------
    # Horizontal spectrum range
    # ---------------------------
    range_horizontal_strip_start = detector.range_horizontal_strip_start
    range_horizontal_strip_stop = detector.range_horizontal_strip_stop
    if range_horizontal_strip_start and range_horizontal_strip_stop:
        for start, stop in zip(range_horizontal_strip_start, range_horizontal_strip_stop):
            number_of_strips = abs(stop - start) + 1
            total_spectra.extend(spectra_block.get_block(start, 0, number_of_strips, None))

    # ---------------------------
    # Vertical spectrum range
    # ---------------------------
    range_vertical_strip_start = detector.range_vertical_strip_start
    range_vertical_strip_stop = detector.range_vertical_strip_stop
    if range_vertical_strip_start and range_vertical_strip_stop:
        for start, stop in zip(range_vertical_strip_start, range_vertical_strip_stop):
            number_of_strips = abs(stop - start) + 1
            total_spectra.extend(spectra_block.get_block(0, start, None, number_of_strips))

    # ---------------------------
    # Blocks
    # ---------------------------
    block_horizontal_start = detector.block_horizontal_start
    block_horizontal_stop = detector.block_horizontal_stop
    block_vertical_start = detector.block_vertical_start
    block_vertical_stop = detector.block_vertical_stop

    if block_horizontal_start and block_horizontal_stop and block_vertical_start and block_vertical_stop:
        for h_start, h_stop, v_start, v_stop in zip(block_horizontal_start, block_horizontal_stop,
                                                    block_vertical_start, block_vertical_stop):
            x_dim = abs(v_stop - v_start) + 1
            y_dim = abs(h_stop - h_start) + 1
            total_spectra.extend(spectra_block.get_block(h_start, v_start, y_dim, x_dim))

    # ---------------------------
    # Blocks Cross
    # ---------------------------
    block_cross_horizontal = detector.block_cross_horizontal
    block_cross_vertical = detector.block_cross_vertical

    if block_cross_horizontal and block_cross_vertical:
        for horizontal, vertical in zip(block_cross_horizontal, block_cross_vertical):
            total_spectra.extend(spectra_block.get_block(horizontal, vertical, 1, 1))

265
266
267
    if not total_spectra:
        return workspace

268
    # Perform the masking
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
    ws_spectra_list = workspace.getSpectrumNumbers()
    # Any gaps in the spectra list we skip over attempting to mask
    filtered_mask_spectra = [spec for spec in total_spectra if spec in ws_spectra_list]

    if len(filtered_mask_spectra) != len(total_spectra):
        log = Logger("SANS - Mask Workspace")
        log.warning("Skipped masking some spectrum numbers that do not exist in the workspace. Re-run"
                    " with logging set to information for more details")
        log.information("The following spectrum numbers do not exist in the ws (cropped to component):")
        for i in list(set(total_spectra) - set(filtered_mask_spectra)):
            log.information(str(i))

    mask_name = "MaskSpectra"
    mask_options = {"InputWorkspace": workspace,
                    "InputWorkspaceIndexType": "SpectrumNumber",
                    "OutputWorkspace": "__dummy"}
    mask_alg = create_unmanaged_algorithm(mask_name, **mask_options)
    mask_alg.setProperty("InputWorkspaceIndexSet", list(set(filtered_mask_spectra)))
    mask_alg.setProperty("OutputWorkspace", workspace)
    mask_alg.execute()
    workspace = mask_alg.getProperty("OutputWorkspace").value
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
    return workspace


def mask_angle(mask_info, workspace):
    """
    Creates a pizza slice mask on the detector around (0,0)

    :param mask_info: a SANSStateMask object
    :param workspace: the workspace which is to be masked.
    :return: a masked workspace
    """
    phi_mirror = mask_info.use_mask_phi_mirror
    phi_min = mask_info.phi_min
    phi_max = mask_info.phi_max
    if phi_min is not None and phi_max is not None and phi_mirror is not None:
        # Check for edge cases for the mirror
        if phi_mirror:
            if phi_min > phi_max:
                phi_min, phi_max = phi_max, phi_min

            if phi_max - phi_min == 180.0:
                phi_min = -90.0
                phi_max = 90.0

        # Create the phi mask and apply it if anything was created
        phi_mask = create_phi_mask('unique phi', [0, 0, 0], phi_min, phi_max, phi_mirror)

        if phi_mask:
            mask_name = "MaskDetectorsInShape"
            mask_options = {"Workspace": workspace,
                            "ShapeXML": phi_mask}
            mask_alg = create_unmanaged_algorithm(mask_name, **mask_options)
            mask_alg.execute()
            workspace = mask_alg.getProperty("Workspace").value
    return workspace


327
def mask_beam_stop(mask_info, workspace, instrument, detector_names):
328
    """
Matthew Andrew's avatar
Matthew Andrew committed
329
    The beam stop is being masked here.
330
331
332

    :param mask_info: a SANSStateMask object.
    :param workspace: the workspace which is to be masked.
luz.paz's avatar
luz.paz committed
333
    :param instrument: the instrument associated with the current workspace.
334
335
336
337
338
339
340
    :return: a masked workspace
    """
    beam_stop_arm_width = mask_info.beam_stop_arm_width
    beam_stop_arm_angle = mask_info.beam_stop_arm_angle
    beam_stop_arm_pos1 = mask_info.beam_stop_arm_pos1
    beam_stop_arm_pos2 = mask_info.beam_stop_arm_pos2
    if beam_stop_arm_width is not None and beam_stop_arm_angle is not None:
341
342
343
344
        detector = workspace.getInstrument().getComponentByName(detector_names['LAB'])
        z_position = detector.getPos().getZ()
        start_point = [beam_stop_arm_pos1, beam_stop_arm_pos2, z_position]
        line_mask = create_line_mask(start_point, 100., beam_stop_arm_width, beam_stop_arm_angle)
345

346
347
348
349
350
351
        mask_name = "MaskDetectorsInShape"
        mask_options = {"Workspace": workspace,
                        "ShapeXML": line_mask}
        mask_alg = create_unmanaged_algorithm(mask_name, **mask_options)
        mask_alg.execute()
        workspace = mask_alg.getProperty("Workspace").value
352
353
354
355
356
357
358
    return workspace


# ------------------------------------------------------------------
# Masker classes
# ------------------------------------------------------------------

359
class Masker(with_metaclass(ABCMeta, object)):
360
361
362
363
    def __init__(self):
        super(Masker, self).__init__()

    @abstractmethod
364
    def mask_workspace(self, mask_info, workspace_to_mask, detector_type):
365
366
367
368
369
370
371
        pass


class NullMasker(Masker):
    def __init__(self):
        super(NullMasker, self).__init__()

372
    def mask_workspace(self, mask_info, workspace_to_mask, detector_type):
373
374
375
376
        return workspace_to_mask


class MaskerISIS(Masker):
377
    def __init__(self, spectra_block, instrument, detector_names):
378
379
380
        super(MaskerISIS, self).__init__()
        self._spectra_block = spectra_block
        self._instrument = instrument
381
        self._detector_names = detector_names
382

383
    def mask_workspace(self, mask_info, workspace_to_mask, detector_type):
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
        """
        Performs the different types of masks that are currently available for ISIS reductions.

        :param mask_info: a SANSStateMask object.
        :param workspace_to_mask: the workspace to mask.
        :param detector_type: the detector type which is currently used , i.e. HAB or LAB
        :param progress: a Progress object
        :return: a masked workspace.
        """
        # Perform bin masking
        workspace_to_mask = mask_bins(mask_info, workspace_to_mask, detector_type)

        # Perform cylinder masking
        workspace_to_mask = mask_cylinder(mask_info, workspace_to_mask)

        # Apply the xml mask files
        workspace_to_mask = mask_with_mask_files(mask_info, workspace_to_mask)

        # Mask spectrum list
        workspace_to_mask = mask_spectra(mask_info, workspace_to_mask, self._spectra_block, detector_type)

        # Mask angle
        workspace_to_mask = mask_angle(mask_info, workspace_to_mask)

        # Mask beam stop
409
        return mask_beam_stop(mask_info, workspace_to_mask, self._instrument, self._detector_names)
410
411


412
413
414
def create_masker(state, detector_type):
    """
    Provides the appropriate masker.
415

416
417
418
419
420
421
    :param state: a SANSState object
    :param detector_type: either HAB or LAB
    :return: the corresponding slicer
    """
    data_info = state.data
    instrument = data_info.instrument
422
423
424

    # TODO remove this shim

425
426
427
428
429
430
431
432
433
434
435
436
    detector_names = state.reduction.detector_names
    if instrument is SANSInstrument.LARMOR or instrument is SANSInstrument.LOQ or\
                    instrument is SANSInstrument.SANS2D or instrument is SANSInstrument.ZOOM:  # noqa
        run_number = data_info.sample_scatter_run_number
        file_name = data_info.sample_scatter
        _, ipf_path = get_instrument_paths_for_sans_file(file_name)
        spectra_block = SpectraBlock(ipf_path, run_number, instrument, detector_type)
        masker = MaskerISIS(spectra_block, instrument, detector_names)
    else:
        masker = NullMasker()
        NotImplementedError("create_masker: Other instruments are not implemented yet.")
    return masker