absorptioncorrutils.py 23 KB
Newer Older
1
2
# Mantid Repository : https://github.com/mantidproject/mantid
#
3
# Copyright © 2020 ISIS Rutherford Appleton Laboratory UKRI,
4
5
6
#   NScD Oak Ridge National Laboratory, European Spallation Source,
#   Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
# SPDX - License - Identifier: GPL - 3.0 +
Kendrick, Coleman's avatar
Kendrick, Coleman committed
7
from mantid.api import AnalysisDataService, WorkspaceFactory
8
from mantid.kernel import Logger, Property, PropertyManager
Zhang, Chen's avatar
Zhang, Chen committed
9
10
11
12
from mantid.simpleapi import (AbsorptionCorrection, DeleteWorkspace, Divide, Load, Multiply,
                              PaalmanPingsAbsorptionCorrection, PreprocessDetectorsToMD,
                              RenameWorkspace, SetSample, SaveNexusProcessed, UnGroupWorkspace, mtd)
import mantid.simpleapi
13
14
import numpy as np
import os
Zhang, Chen's avatar
Zhang, Chen committed
15
from functools import wraps
16

17
VAN_SAMPLE_DENSITY = 0.0721
18
19
20
_EXTENSIONS_NXS = ["_event.nxs", ".nxs.h5"]


21
22
23
# ---------------------------- #
# ----- Helper functions ----- #
# ---------------------------- #
24
def _getBasename(filename):
25
26
27
    """
    Helper function to get the filename without the path or extension
    """
28
29
30
31
32
33
34
35
    if type(filename) == list:
        filename = filename[0]
    name = os.path.split(filename)[-1]
    for extension in _EXTENSIONS_NXS:
        name = name.replace(extension, '')
    return name


36
def __get_instrument_name(wksp):
37
    """
38
    Get the short name of given work space
39

40
    :param wksp: input workspace
41

42
    return instrument short name as string
43
    """
44
45
46
47
48
    if wksp in mtd:
        ws = mtd[wksp]
    else:
        raise ValueError(f"{wksp} cannot be found")
    return mantid.kernel.ConfigService.getInstrument(ws.getInstrument().getName()).shortName()
49

Zhang, Chen's avatar
Zhang, Chen committed
50

51
def __get_cache_name(meta_wksp_name, abs_method, cache_dir="", prefix_name=""):
Zhang, Chen's avatar
Zhang, Chen committed
52
53
    """generate cachefile name (full path) and sha1

Zhang, Chen's avatar
Zhang, Chen committed
54
    :param meta_wksp_name: name of workspace contains relevant meta data for hashing
Zhang, Chen's avatar
Zhang, Chen committed
55
    :param abs_method: method used to perform the absorption calculation
56
57
58
    :param cache_dir: cache directory to scan/load cache data
    :param prefix_name: prefix to add to wkspname for caching

Zhang, Chen's avatar
Zhang, Chen committed
59
60
    return cachefile_name: full path of the cache file
           sha1: MD5 value based on selected property
61
    """
Zhang, Chen's avatar
Zhang, Chen committed
62
    # grab the workspace
Zhang, Chen's avatar
Zhang, Chen committed
63
64
    if meta_wksp_name in mtd:
        ws = mtd[meta_wksp_name]
Zhang, Chen's avatar
Zhang, Chen committed
65
66
    else:
        raise ValueError(
Zhang, Chen's avatar
Zhang, Chen committed
67
            f"Cannot find workspace {meta_wksp_name} to extract meta data for hashing, aborting")
68

Zhang, Chen's avatar
Zhang, Chen committed
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
    # requires cache_dir
    if cache_dir == "":
        cache_filename, signature = "", ""
    else:
        # generate the property string for hashing
        property_string = [
            f"{key}={val}" for key, val in {
                'wavelength_min': ws.readX(0).min(),
                'wavelength_max': ws.readX(0).max(),
                "num_wl_bins": len(ws.readX(0)) - 1,
                "sample_formula": ws.run()['SampleFormula'].lastValue().strip(),
                "mass_density": ws.run()['SampleDensity'].lastValue(),
                "height_unit": ws.run()['BL11A:CS:ITEMS:HeightInContainerUnits'].lastValue(),
                "height": ws.run()['BL11A:CS:ITEMS:HeightInContainer'].lastValue(),
                "sample_container": ws.run()['SampleContainer'].lastValue().replace(" ", ""),
                "abs_method": abs_method,
            }.items()
        ]

        # use mantid build-in alg to generate the cache filename and sha1
        cache_filename, signature = mantid.simpleapi.CreateCacheFilename(
90
            Prefix=prefix_name,
Zhang, Chen's avatar
Zhang, Chen committed
91
92
93
94
95
96
97
            OtherProperties=property_string,
            CacheDir=cache_dir,
        )

    return cache_filename, signature


98
def __load_cached_data(cache_file_name, sha1, abs_method="", prefix_name=""):
Zhang, Chen's avatar
Zhang, Chen committed
99
100
101
102
103
    """try to load cached data from memory and disk

    :param abs_method: absorption calculation method
    :param sha1: SHA1 that identify cached workspace
    :param cache_file_name: cache file name to search
104
    :param prefix_name: prefix to add to wkspname for caching
Zhang, Chen's avatar
Zhang, Chen committed
105

Zhang, Chen's avatar
Zhang, Chen committed
106
107
    return  found_abs_wksp_sample, found_abs_wksp_container
            abs_wksp_sample, abs_wksp_container
108
    """
Zhang, Chen's avatar
Zhang, Chen committed
109
110
111
    # init
    abs_wksp_sample, abs_wksp_container = "", ""
    found_abs_wksp_sample, found_abs_wksp_container = False, False
112

113
    # step_0: depending on the abs_method, suffix will be different
114
    if abs_method == "SampleOnly":
115
        abs_wksp_sample = f"{prefix_name}_ass"
Zhang, Chen's avatar
Zhang, Chen committed
116
        found_abs_wksp_container = True
117
    elif abs_method == "SampleAndContainer":
118
119
        abs_wksp_sample = f"{prefix_name}_ass"
        abs_wksp_container = f"{prefix_name}_acc"
120
    elif abs_method == "FullPaalmanPings":
121
122
        abs_wksp_sample = f"{prefix_name}_assc"
        abs_wksp_container = f"{prefix_name}_ac"
123
    else:
124
        raise ValueError("Unrecognized absorption correction method '{}'".format(abs_method))
125

Zhang, Chen's avatar
Zhang, Chen committed
126
    # step_1: check memory
127
128
129
130
    if mtd.doesExist(abs_wksp_sample):
        found_abs_wksp_sample = mtd[abs_wksp_sample].run()["absSHA1"].value == sha1
    if mtd.doesExist(abs_wksp_container):
        found_abs_wksp_container = mtd[abs_wksp_container].run()["absSHA1"].value == sha1
Zhang, Chen's avatar
Zhang, Chen committed
131
132
133

    # step_2: load from disk if either is not found in memory
    if (not found_abs_wksp_sample) or (not found_abs_wksp_container):
Zhang, Chen's avatar
Zhang, Chen committed
134
        if os.path.exists(cache_file_name):
Zhang, Chen's avatar
Zhang, Chen committed
135
            wsntmp = f"tmpwsg"
Zhang, Chen's avatar
Zhang, Chen committed
136
137
            Load(Filename=cache_file_name, OutputWorkspace=wsntmp)
            wstype = mtd[wsntmp].id()
138
            if wstype == "Workspace2D":
Zhang, Chen's avatar
Zhang, Chen committed
139
                RenameWorkspace(InputWorkspace=wsntmp, OutputWorkspace=abs_wksp_sample)
Zhang, Chen's avatar
Zhang, Chen committed
140
141
142
            elif wstype == "WorkspaceGroup":
                UnGroupWorkspace(InputWorkspace=wsntmp)
            else:
143
                raise ValueError(f"Unsupported cached workspace type: {wstype}")
144

Zhang, Chen's avatar
Zhang, Chen committed
145
    # step_3: check memory again
146
147
148
149
    if mtd.doesExist(abs_wksp_sample):
        found_abs_wksp_sample = mtd[abs_wksp_sample].run()["absSHA1"].value == sha1
    if mtd.doesExist(abs_wksp_container):
        found_abs_wksp_container = mtd[abs_wksp_container].run()["absSHA1"].value == sha1
150

Zhang, Chen's avatar
Zhang, Chen committed
151
    return found_abs_wksp_sample, found_abs_wksp_container, abs_wksp_sample, abs_wksp_container
Zhang, Chen's avatar
Zhang, Chen committed
152
153
154
155
156
157


# NOTE:
#  In order to use the decorator, we must have consistent naming
#  or kwargs as this is probably the most reliable way to get
#  the desired data piped in multiple location
Zhang, Chen's avatar
Zhang, Chen committed
158
159
#  -- bare minimum signaure of the function
#    func(wksp_name: str, abs_method:str, cache_dir="")
Zhang, Chen's avatar
Zhang, Chen committed
160
def abs_cache(func):
Zhang, Chen's avatar
Zhang, Chen committed
161
    """decorator to make the caching process easier
Zhang, Chen's avatar
Zhang, Chen committed
162
163
164
165
166
167
168
169

    NOTE: this decorator should only be used on function calls where
          - the first positional arguments is the workspace name
          - the second positional arguments is the absorption calculation method

    WARNING: currently this decorator should only be used on
                calc_absorption_corr_using_wksp

Zhang, Chen's avatar
Zhang, Chen committed
170
171
172
173
174
    example:
    without caching:
        SNSPowderReduction successful, Duration 5 minutes 53.54 seconds
    with caching (disk):
        SNSPowderReduction successful, Duration 1 minutes 14.18 seconds
Zhang, Chen's avatar
Zhang, Chen committed
175
176
    Speedup by
        4.7660x
Zhang, Chen's avatar
Zhang, Chen committed
177
    """
Zhang, Chen's avatar
Zhang, Chen committed
178
179
    @wraps(func)
    def inner(*args, **kwargs):
180
181
182
        """
        How caching name works
        """
Zhang, Chen's avatar
Zhang, Chen committed
183
184
185
186
        # unpack key arguments
        wksp_name = args[0]
        abs_method = args[1]
        cache_dir = kwargs.get("cache_dir", "")
187
        prefix_name = kwargs.get("prefix_name", "")
Zhang, Chen's avatar
Zhang, Chen committed
188

Zhang, Chen's avatar
Zhang, Chen committed
189
        # prompt return if no cache_dir specified
Zhang, Chen's avatar
Zhang, Chen committed
190
        if cache_dir == "":
Zhang, Chen's avatar
Zhang, Chen committed
191
192
193
194
            return func(*args, **kwargs)

        # step_1: generate the SHA1 and cachefile name
        #         baseon given kwargs
195
196
197
198
199
        cache_prefix = __get_instrument_name(wksp_name)
        cache_filename, signature = __get_cache_name(wksp_name,
                                                     abs_method,
                                                     cache_dir=cache_dir,
                                                     prefix_name=cache_prefix)
Zhang, Chen's avatar
Zhang, Chen committed
200

Zhang, Chen's avatar
Zhang, Chen committed
201
202
203
204
        # step_2: try load the cached data from disk
        found_sample, found_container, abs_wksp_sample, abs_wksp_container = __load_cached_data(
            cache_filename,
            signature,
205
206
            abs_method=abs_method,
            prefix_name=prefix_name,
Zhang, Chen's avatar
Zhang, Chen committed
207
        )
Zhang, Chen's avatar
Zhang, Chen committed
208
209

        # step_3: calculation
Zhang, Chen's avatar
Zhang, Chen committed
210
211
        if (abs_method == "SampleOnly") and found_sample:
            return abs_wksp_sample, ""
Zhang, Chen's avatar
Zhang, Chen committed
212
        else:
Zhang, Chen's avatar
Zhang, Chen committed
213
            if found_sample and found_container:
Zhang, Chen's avatar
Zhang, Chen committed
214
215
216
217
                # cache is available in memory now, skip calculation
                return abs_wksp_sample, abs_wksp_container
            else:
                # no cache found, need calculation
218
219
220
221
222
223
                abs_wksp_sample, abs_wksp_container = func(*args, **kwargs)

                # set SHA1 to workspace
                mtd[abs_wksp_sample].mutableRun()["absSHA1"] = signature
                if abs_wksp_container != "":
                    mtd[abs_wksp_container].mutableRun()["absSHA1"] = signature
Zhang, Chen's avatar
Zhang, Chen committed
224

225
226
227
                # save to disk
                SaveNexusProcessed(InputWorkspace=abs_wksp_sample, Filename=cache_filename)
                if abs_wksp_container != "":
Zhang, Chen's avatar
Zhang, Chen committed
228
229
230
231
232
233
234
                    SaveNexusProcessed(InputWorkspace=abs_wksp_container,
                                       Filename=cache_filename,
                                       Append=True)

                return abs_wksp_sample, abs_wksp_container

    return inner
235
236
237
238
239


# ----------------------------- #
# ---- Core functionality ----- #
# ------------------------------#
240
def calculate_absorption_correction(
241
242
243
244
245
246
247
248
249
250
251
    filename,
    abs_method,
    props,
    sample_formula,
    mass_density,
    number_density=Property.EMPTY_DBL,
    container_shape="PAC06",
    num_wl_bins=1000,
    element_size=1,
    metaws=None,
    cache_dir="",
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
    """The absorption correction is applied by (I_s - I_c*k*A_csc/A_cc)/A_ssc for pull Paalman-Ping

    If no cross-term then I_s/A_ss - I_c/A_cc

    Therefore this will return 2 workspace, one for correcting the
    sample (A_s) and one for the container (A_c) depending on the
    absorption method, that will be passed to _focusAndSum and
    therefore AlignAndFocusPowderFromFiles.

    If SampleOnly then

    A_s = A_ss
    A_c = None

    If SampleAndContainer then

    A_s = A_ss
    A_c = A_cc

    If FullPaalmanPings then
    A_s = A_ssc
    A_c = A_cc*A_ssc/A_csc

    This will then return (A_s, A_c)
277
278
279

    :param filename: File to be used for absorption correction
    :param abs_method: Type of absorption correction: None, SampleOnly, SampleAndContainer, FullPaalmanPings
280
    :param props: PropertyManager of run characterizations, obtained from PDDetermineCharacterizations
281
282
283
284
285
286
    :param sample_formula: Sample formula to specify the Material for absorption correction
    :param mass_density: Mass density of the sample to specify the Material for absorption correction
    :param number_density: Optional number density of sample to be added to the Material for absorption correction
    :param container_shape: Shape definition of container, such as PAC06.
    :param num_wl_bins: Number of bins for calculating wavelength
    :param element_size: Size of one side of the integration element cube in mm
287
    :param metaws: Optional workspace containing metadata to use instead of reading from filename
288
    :param cache_dir: cache directory for storing cached absorption correction workspace
289
    :param prefix: How the prefix of cache file is determined - FILENAME to use file, or SHA prefix
290

291
    :return:
292
        Two workspaces (A_s, A_c) names
293
294
295
296
    """
    if abs_method == "None":
        return None, None

297
    material = {"ChemicalFormula": sample_formula, "SampleMassDensity": mass_density}
298
299
300
301
302
303

    if number_density != Property.EMPTY_DBL:
        material["SampleNumberDensity"] = number_density

    environment = {'Name': 'InAir', 'Container': container_shape}

304
305
306
307
308
309
    donorWS = create_absorption_input(filename,
                                      props,
                                      num_wl_bins,
                                      material=material,
                                      environment=environment,
                                      metaws=metaws)
310

Zhang, Chen's avatar
Zhang, Chen committed
311
    # NOTE: Ideally we want to separate cache related task from calculation,
312
313
314
315
316
317
    #       but the fact that we are trying to determine the name based on
    #       caching types requires us to use part of the caching here.
    #       Not a clean design, but it is unavoidable given that we are
    #       mixing caching into the regular routine from start.
    # NOTE: Cache will always use sha in both workspace name and cache filename
    #       Examples
Zhang, Chen's avatar
Zhang, Chen committed
318
    #
319
320
321
322
323
324
325
326
327
328
329
330
331
332
    #       PG3_11111.nxs with cache_dir=/tmp and abs_method="SampleOnly"
    #       -------------------------------------------------------------
    #       absName = PG3_sha1_abs_correction
    #       cachefilename = /tmp/PG3_sha1.nxs
    #       sampleWorkspace = PG3_sha1_abs_correction_ass
    #
    #       PG3_11111.nxs with cache_dir="" and abs_method="SampleOnly"
    #       -----------------------------------------------------------
    #       absName = PG3_11111_abs_correction
    #       sampleWorkspace = PG3_11111_abs_correction_ass
    if cache_dir != "":
        cache_prefix = __get_instrument_name(donorWS)
        _, sha = __get_cache_name(donorWS, abs_method, cache_dir, cache_prefix)
        absName = f"{cache_prefix}_{sha}_abs_correction"
333
    else:
334
        absName = f"{_getBasename(filename)}_abs_correction"
335

Zhang, Chen's avatar
Zhang, Chen committed
336
337
338
    return calc_absorption_corr_using_wksp(donorWS,
                                           abs_method,
                                           element_size,
Zhang, Chen's avatar
Zhang, Chen committed
339
                                           prefix_name=absName,
Zhang, Chen's avatar
Zhang, Chen committed
340
341
342
343
                                           cache_dir=cache_dir)


@abs_cache
344
345
346
347
348
349
350
def calc_absorption_corr_using_wksp(
    donor_wksp,
    abs_method,
    element_size=1,
    prefix_name="",
    cache_dir="",
):
351
352
353
354
355
356
357
358
    """
    Calculates absorption correction on the specified donor workspace. See the documentation
    for the ``calculate_absorption_correction`` function above for more details.

    :param donor_wksp: Input workspace to compute absorption correction on
    :param abs_method: Type of absorption correction: None, SampleOnly, SampleAndContainer, FullPaalmanPings
    :param element_size: Size of one side of the integration element cube in mm
    :param prefix_name: Optional prefix of the output workspaces, default is the donor_wksp name.
Zhang, Chen's avatar
Zhang, Chen committed
359
360
    :param cache_dir: Cache directory to store cached abs workspace.

361
362
    :return: Two workspaces (A_s, A_c), the first for the sample and the second for the container
    """
Zhang, Chen's avatar
Zhang, Chen committed
363
364
365
    log = Logger('calc_absorption_corr_using_wksp')
    if cache_dir != "":
        log.information(f"Storing cached data in {cache_dir}")
366
367

    if abs_method == "None":
Zhang, Chen's avatar
Zhang, Chen committed
368
        return "", ""
369
370
371
372
373
374
375
376
377

    if isinstance(donor_wksp, str):
        if not mtd.doesExist(donor_wksp):
            raise RuntimeError("Specified donor workspace not found in the ADS")
        donor_wksp = mtd[donor_wksp]

    absName = donor_wksp.name()
    if prefix_name != '':
        absName = prefix_name
378
379

    if abs_method == "SampleOnly":
380
        AbsorptionCorrection(donor_wksp,
381
382
383
                             OutputWorkspace=absName + '_ass',
                             ScatterFrom='Sample',
                             ElementSize=element_size)
Zhang, Chen's avatar
Zhang, Chen committed
384
        return absName + '_ass', ""
385
    elif abs_method == "SampleAndContainer":
386
        AbsorptionCorrection(donor_wksp,
387
388
389
                             OutputWorkspace=absName + '_ass',
                             ScatterFrom='Sample',
                             ElementSize=element_size)
390
        AbsorptionCorrection(donor_wksp,
391
392
393
394
                             OutputWorkspace=absName + '_acc',
                             ScatterFrom='Container',
                             ElementSize=element_size)
        return absName + '_ass', absName + '_acc'
395
    elif abs_method == "FullPaalmanPings":
396
        PaalmanPingsAbsorptionCorrection(donor_wksp,
397
398
399
400
401
402
403
404
405
                                         OutputWorkspace=absName,
                                         ElementSize=element_size)
        Multiply(LHSWorkspace=absName + '_acc',
                 RHSWorkspace=absName + '_assc',
                 OutputWorkspace=absName + '_ac')
        Divide(LHSWorkspace=absName + '_ac',
               RHSWorkspace=absName + '_acsc',
               OutputWorkspace=absName + '_ac')
        return absName + '_assc', absName + '_ac'
406
    else:
407
        raise ValueError("Unrecognized absorption correction method '{}'".format(abs_method))
408
409


410
def create_absorption_input(
411
412
413
414
415
416
417
418
419
    filename,
    props,
    num_wl_bins=1000,
    material=None,
    geometry=None,
    environment=None,
    opt_wl_min=0,
    opt_wl_max=Property.EMPTY_DBL,
    metaws=None,
420
):
421
422
    """
    Create an input workspace for carpenter or other absorption corrections
423
424

    :param filename: Input file to retrieve properties from the sample log
425
    :param props: PropertyManager of run characterizations, obtained from PDDetermineCharacterizations
426
427
428
429
    :param num_wl_bins: The number of wavelength bins used for absorption correction
    :param material: Optional material to use in SetSample
    :param geometry: Optional geometry to use in SetSample
    :param environment: Optional environment to use in SetSample
430
431
    :param opt_wl_min: Optional minimum wavelength. If specified, this is used instead of from the props
    :param opt_wl_max: Optional maximum wavelength. If specified, this is used instead of from the props
432
    :param metaws: Optional workspace name with metadata to use for donor workspace instead of reading from filename
433
    :return: Name of the donor workspace created
434
    """
435
436
437
438
439
    if props is None:
        raise RuntimeError("props is required to create donor workspace, props is None")

    if not isinstance(props, PropertyManager):
        raise RuntimeError("props must be a PropertyManager object")
440
441
442

    log = Logger('CreateAbsorptionInput')

443
444
445
446
    # Load from file if no workspace with metadata has been given, otherwise avoid a duplicate load with the metaws
    absName = metaws
    if metaws is None:
        absName = '__{}_abs'.format(_getBasename(filename))
Zhang, Chen's avatar
Zhang, Chen committed
447
448
449
450
451
        allowed_log = " ".join([
            'SampleFormula', 'SampleDensity', "BL11A:CS:ITEMS:HeightInContainerUnits",
            "SampleContainer"
        ])
        Load(Filename=filename, OutputWorkspace=absName, MetaDataOnly=True, AllowList=allowed_log)
452
453

    # first attempt to get the wavelength range from the properties file
454
    wl_min, wl_max = props['wavelength_min'].value, props['wavelength_max'].value
455
456
457
458
459
460
461
462
    # override that with what was given as parameters to the algorithm
    if opt_wl_min > 0.:
        wl_min = opt_wl_min
    if opt_wl_max != Property.EMPTY_DBL:
        wl_max = opt_wl_max

    # if it isn't found by this point, guess it from the time-of-flight range
    if (wl_min == wl_max == 0.):
463
464
        tof_min = props['tof_min'].value
        tof_max = props['tof_max'].value
465
466
467
468
469
470
471
        if tof_min >= 0. and tof_max > tof_min:
            log.information('TOF range is {} to {} microseconds'.format(tof_min, tof_max))

            # determine L1
            instr = mtd[absName].getInstrument()
            L1 = instr.getSource().getDistance(instr.getSample())
            # determine L2 range
472
473
474
            PreprocessDetectorsToMD(InputWorkspace=absName,
                                    OutputWorkspace=absName + '_dets',
                                    GetMaskState=False)
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
            L2 = mtd[absName + '_dets'].column('L2')
            Lmin = np.min(L2) + L1
            Lmax = np.max(L2) + L1
            DeleteWorkspace(Workspace=absName + '_dets')

            log.information('Distance range is {} to {} meters'.format(Lmin, Lmax))

            # wavelength is h*TOF / m_n * L  values copied from Kernel/PhysicalConstants.h
            usec_to_sec = 1.e-6
            meter_to_angstrom = 1.e10
            h_m_n = meter_to_angstrom * usec_to_sec * 6.62606896e-34 / 1.674927211e-27
            wl_min = h_m_n * tof_min / Lmax
            wl_max = h_m_n * tof_max / Lmin

    # there isn't a good way to guess it so error out
    if wl_max <= wl_min:
        DeleteWorkspace(Workspace=absName)  # no longer needed
        raise RuntimeError('Invalid wavelength range min={}A max={}A'.format(wl_min, wl_max))
    log.information('Using wavelength range min={}A max={}A'.format(wl_min, wl_max))

495
496
497
498
    absorptionWS = WorkspaceFactory.create(mtd[absName],
                                           NVectors=mtd[absName].getNumberHistograms(),
                                           XLength=num_wl_bins + 1,
                                           YLength=num_wl_bins)
499
500
501
502
503
504
505
506
507
508
509
510
    xaxis = np.arange(0., float(num_wl_bins + 1)) * (wl_max - wl_min) / (num_wl_bins) + wl_min
    for i in range(absorptionWS.getNumberHistograms()):
        absorptionWS.setX(i, xaxis)
    absorptionWS.getAxis(0).setUnit('Wavelength')

    # this effectively deletes the metadata only workspace
    AnalysisDataService.addOrReplace(absName, absorptionWS)

    # Set ChemicalFormula, and either SampleMassDensity or Mass, if SampleMassDensity not set
    if material is not None:
        if (not material['ChemicalFormula']) and ("SampleFormula" in absorptionWS.run()):
            material['ChemicalFormula'] = absorptionWS.run()['SampleFormula'].lastValue().strip()
511
512
513
514
        if ("SampleMassDensity" not in material
                or not material['SampleMassDensity']) and ("SampleDensity" in absorptionWS.run()):
            if (absorptionWS.run()['SampleDensity'].lastValue() !=
                    1.0) and (absorptionWS.run()['SampleDensity'].lastValue() != 0.0):
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
                material['SampleMassDensity'] = absorptionWS.run()['SampleDensity'].lastValue()
            else:
                material['Mass'] = absorptionWS.run()['SampleMass'].lastValue()

    # Set height for computing density if height not set
    if geometry is None:
        geometry = {}

    if geometry is not None:
        if "Height" not in geometry or not geometry['Height']:
            # Check units - SetSample expects cm
            if absorptionWS.run()['BL11A:CS:ITEMS:HeightInContainerUnits'].lastValue() == "mm":
                conversion = 0.1
            elif absorptionWS.run()['BL11A:CS:ITEMS:HeightInContainerUnits'].lastValue() == "cm":
                conversion = 1.0
            else:
531
532
533
                raise ValueError(
                    "HeightInContainerUnits expects cm or mm; specified units not recognized: ",
                    absorptionWS.run()['BL11A:CS:ITEMS:HeightInContainerUnits'].lastValue())
534

535
536
            geometry['Height'] = absorptionWS.run()['BL11A:CS:ITEMS:HeightInContainer'].lastValue(
            ) * conversion
537
538
539
540

    # Set container if not set
    if environment is not None:
        if environment['Container'] == "":
541
542
            environment['Container'] = absorptionWS.run()['SampleContainer'].lastValue().replace(
                " ", "")
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565

    # Make sure one is set before calling SetSample
    if material or geometry or environment is not None:
        setup_sample(absName, material, geometry, environment)

    return absName


def setup_sample(donor_ws, material, geometry, environment):
    """
    Calls SetSample with the associated sample and container material and geometry for use
    in creating an input workspace for an Absorption Correction algorithm
    :param donor_ws:
    :param material:
    :param geometry:
    :param environment:
    """

    # Set the material, geometry, and container info
    SetSample(InputWorkspace=donor_ws,
              Material=material,
              Geometry=geometry,
              Environment=environment)