Unverified Commit 76370556 authored by Peterson, Peter's avatar Peterson, Peter Committed by GitHub
Browse files

Merge pull request #32160 from peterfpeterson/setsamplefromlogs

SetSampleFromLogs (ornlnext)
parents 0ee9b4a2 9ed65e26
......@@ -8,12 +8,11 @@ from mantid.api import AnalysisDataService, WorkspaceFactory
from mantid.kernel import Logger, Property, PropertyManager
from mantid.simpleapi import (AbsorptionCorrection, DeleteWorkspace, Divide, Load, Multiply,
PaalmanPingsAbsorptionCorrection, PreprocessDetectorsToMD,
RenameWorkspace, SetSample, SaveNexusProcessed, UnGroupWorkspace, mtd)
RenameWorkspace, SaveNexusProcessed, UnGroupWorkspace, mtd)
import mantid.simpleapi
import numpy as np
import os
from functools import wraps
import warnings
VAN_SAMPLE_DENSITY = 0.0721
_EXTENSIONS_NXS = ["_event.nxs", ".nxs.h5"]
......@@ -87,7 +86,7 @@ def __get_cache_name(meta_wksp_name, abs_method, cache_dirs=[], prefix_name=""):
# use mantid build-in alg to generate the cache filename and sha1
ascii_hash = ""
for cache_dir in cache_dirs :
for cache_dir in cache_dirs:
ascii_name, ascii_hash = mantid.simpleapi.CreateCacheFilename(
Prefix=prefix_name,
......@@ -420,9 +419,9 @@ def create_absorption_input(
filename,
props=None,
num_wl_bins=1000,
material=None,
geometry=None,
environment=None,
material={},
geometry={},
environment={},
opt_wl_min=0,
opt_wl_max=Property.EMPTY_DBL,
metaws=None,
......@@ -526,69 +525,16 @@ def create_absorption_input(
# 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()
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):
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:
# cleanup inputs before delegating work
if not material:
material = {}
if not geometry:
geometry = {}
if geometry is not None:
if "Height" not in geometry or not geometry['Height']:
invalidUnit = False
# 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:
unitVal = absorptionWS.run()['BL11A:CS:ITEMS:HeightInContainerUnits'].lastValue()
warningMsg1 = "HeightInContainerUnits expects cm or mm;"
warningMsg2 = " specified units not recognized: {:s};".format(unitVal)
warningMsg3 = " we will reply on user input for sample density information."
warnings.warn(warningMsg1 + warningMsg2 + warningMsg3)
invalidUnit = True
if not invalidUnit:
geometry['Height'] = absorptionWS.run()['BL11A:CS:ITEMS:HeightInContainer'].lastValue(
) * conversion
else:
geometry = {}
# Set container if not set
if environment is not None:
if environment['Container'] == "":
environment['Container'] = absorptionWS.run()['SampleContainer'].lastValue().replace(
" ", "")
if not environment:
environment = {}
# Make sure one is set before calling SetSample
if material or geometry or environment is not None:
setup_sample(absName, material, geometry, environment)
if material or geometry or environment:
mantid.simpleapi.SetSampleFromLogs(InputWorkspace=absName, Material=material, Geometry=geometry, Environment=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)
......@@ -169,6 +169,7 @@ set(PYTHON_PLUGINS
algorithms/SaveYDA.py
algorithms/SelectNexusFilesByMetadata.py
algorithms/SetDetScale.py
algorithms/SetSampleFromLogs.py
algorithms/SortByQVectors.py
algorithms/SortDetectors.py
algorithms/StatisticsOfTableWorkspace.py
......
# Mantid Repository : https://github.com/mantidproject/mantid
#
# Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
# NScD Oak Ridge National Laboratory, European Spallation Source,
# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
# SPDX - License - Identifier: GPL - 3.0 +
from mantid.api import (AlgorithmFactory, DistributedDataProcessorAlgorithm)
from mantid.kernel import (ConfigService)
from mantid import _kernel as mtd_kernel
from mantid.simpleapi import (SetSample)
PROPS_FOR_SETSAMPLE = ["InputWorkspace", "Geometry", "Material", "Environment", "ContainerGeometry", "ContainerMaterial"]
def _toDict(propertyManager):
result = {}
if propertyManager:
for key in propertyManager.keys():
result[key] = propertyManager[key].value
if isinstance(result[key], mtd_kernel.std_vector_dbl):
result[key] = list(result[key])
return result
def _findKey(dictlike, *args):
for name in args:
if (name in dictlike) and bool(dictlike[name]):
return name
return '' # indicates failure
def _hasValue(dictLike, key): # TODO refactor into *args and return the one that exists or empty string
return (key in dictLike) and bool(dictLike[key])
def _getLogValue(propertyManager, key):
if not _hasValue(propertyManager, key):
raise ValueError('Failed to find "{}" in logs'.format(key))
return propertyManager[key].lastValue()
class SetSampleFromLogs(DistributedDataProcessorAlgorithm):
def category(self):
return "Sample"
def seeAlso(self):
return ["SetSample"]
def name(self):
return "SetSampleFromLogs"
def summary(self):
return "This algorithm looks through the logs to automatically determine the sample geometry and material"
def PyInit(self):
self.copyProperties("SetSample", PROPS_FOR_SETSAMPLE)
self.declareProperty("FindGeometry", True,
"Whether to look for the 'Height' parameter in the logs")
self.declareProperty("FindSample", True,
"Whether to look for the sample material in the logs")
self.declareProperty("FindEnvironment", True,
"Whether to look for the sample container in the logs")
def _createMaterial(self, runObject):
'''Create the sample material by combining the supplied material information with things found in the logs'''
# grab the starting information from the algorithm property
material = _toDict(self.getProperty("Material").value)
# only look in the logs if the user asks for it
if self.getProperty("FindSample").value: # SetSample calls it the material
if (not _hasValue(material, 'ChemicalFormula')) and _hasValue(runObject, "SampleFormula"):
self.log().information("Looking for 'SampleFormula' in logs")
material['ChemicalFormula'] = _getLogValue(runObject, 'SampleFormula').strip()
if (not _hasValue(material, "SampleMassDensity")) and _hasValue(runObject, "SampleDensity"):
self.log().information("Looking for 'SampleDensity', 'SampleMass', and 'SampleMassDensity' in logs")
value = _getLogValue(runObject, 'SampleDensity')
if value == 1.0 or value == 0.0:
material['Mass'] = _getLogValue(runObject, 'SampleMass')
elif _hasValue(runObject, "SampleMass"):
material['SampleMassDensity'] = value
# log the results and return
self.log().information('MATERIAL: ' + str(material))
return material
def _createGeometry(self, runObject, instrEnum):
# get height of sample from the logs
# this assumes the shape has a "Height" property
geometry = _toDict(self.getProperty("Geometry").value)
if self.getProperty("FindGeometry").value:
if not _hasValue(geometry, "Height"):
heightInContainerNames = ['HeightInContainer']
heightInContainerUnitsNames = ['HeightInContainerUnits']
# determine "special" logs for SNS instruments
if instrEnum.facility().name() == 'SNS':
beamline = ''
# TODO this would benefit from the beamline exposed through python
if instrEnum.name() == 'NOMAD':
beamline = 'BL1B'
elif instrEnum.name() == 'POWGEN':
beamline = 'BL11A'
else:
warningMsg = 'Do not know how to create lognames for "{}"'.format(instrEnum.name())
self.log().warn(warningMsg)
if beamline:
# "internal" log names at SNS are templated from the beamline number
heightInContainerUnitsNames.append('{}:CS:ITEMS:HeightInContainerUnits'.format(beamline))
heightInContainerNames.append('{}:CS:ITEMS:HeightInContainer'.format(beamline))
self.log().information("Looking for sample height from {} and units from {}".format(heightInContainerNames,
heightInContainerUnitsNames))
# Check units - SetSample expects cm
unitsKey = _findKey(runObject, *heightInContainerUnitsNames)
units = ''
if unitsKey:
units = _getLogValue(runObject, unitsKey)
# not finding units will generate a warning below
# create conversion factor into cm
conversion = 1.0 # don't do any conversion
if units == "cm":
conversion = 1.0
elif units == "mm":
conversion = 0.1
else:
warningMsg = "HeightInContainerUnits expects cm or mm;" + \
" specified units not recognized: {:s};".format(units) + \
" we will reply on user input for sample density information."
self.log().warn(warningMsg)
# set the height
heightKey = _findKey(runObject, *heightInContainerNames)
if heightKey:
geometry['Height'] = conversion * _getLogValue(runObject, heightKey)
else:
raise RuntimeError("Failed to determine the height from the logs")
# log the results and return
self.log().information('GEOMETRY (in cm): ' + str(geometry))
return geometry
def _createEnvironment(self, runObject, instrEnum):
'''Create the sample material by combining the supplied environment information with things found in the logs'''
# grab the starting information from the algorithm property
environment = _toDict(self.getProperty("Environment").value)
# get the container from the logs
if self.getProperty("FindEnvironment").value:
if (not _hasValue(environment, "Container")) and _hasValue(runObject, "SampleContainer"):
self.log().information('Looking for "SampleContainer" in logs')
environment['Container'] = runObject['SampleContainer'].lastValue().replace(" ", "")
if _hasValue(environment, "Container") and (not _hasValue(environment, "Name")):
# set a default environment
if instrEnum.facility().name() == 'SNS':
environment['Name'] = 'InAir'
self.log().information('ENVIRONMENT: ' + str(environment))
return environment
def PyExec(self):
wksp = self.getProperty("InputWorkspace").value
# these items are not grabbed from the logs
geometryContainer = _toDict(self.getProperty("ContainerGeometry").value)
materialContainer = _toDict(self.getProperty("ContainerMaterial").value)
# get a convenient handle to the logs
runObject = wksp.run()
# this is used for determining some of the log names
instrEnum = ConfigService.getInstrument(wksp.getInstrument().getFullName())
# get information from the logs
material = self._createMaterial(runObject)
geometry = self._createGeometry(runObject, instrEnum)
environment = self._createEnvironment(runObject, instrEnum)
# let SetSample generate errors if anything is wrong
SetSample(InputWorkspace=wksp,
Material=material,
Geometry=geometry,
Environment=environment,
ContainerGeometry=geometryContainer,
ContainerMaterial=materialContainer)
# Register algorithm with Mantid.
AlgorithmFactory.subscribe(SetSampleFromLogs)
......@@ -104,6 +104,7 @@ set(TEST_PY_FILES
SaveReflectionsTest.py
ExtractMonitorsTest.py
SetDetScaleTest.py
SetSampleFromLogsTest.py
SortByQVectorsTest.py
SortDetectorsTest.py
StatisticsOfTableWorkspaceTest.py
......
# Mantid Repository : https://github.com/mantidproject/mantid
#
# Copyright © 2020 ISIS Rutherford Appleton Laboratory UKRI,
# NScD Oak Ridge National Laboratory, European Spallation Source,
# Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
# SPDX - License - Identifier: GPL - 3.0 +
import unittest
from mantid.simpleapi import (Load, LoadNexusProcessed, SetSampleFromLogs)
from math import pi
PAC06_RADIUS = 0.00295
PAC06_HEIGHT = 0.0568
class SetSampleFromLogsTest(unittest.TestCase):
def loadPG3Data(self, wkspname: str):
wksp = Load(Filename="PG3_46577.nxs.h5", MetaDataOnly=True, OutputWorkspace=wkspname)
return wksp
def validateSample(self, sample, formula: str, height: float):
self.assertIsNotNone(sample)
self.assertEqual(sample.getMaterial().name(), formula)
assert sample.hasEnvironment()
volume = pi * PAC06_RADIUS * PAC06_RADIUS * height
self.assertEqual(sample.getShape().volume(), volume)
def testSpecifyEverything(self):
'''Specify everything. This gets the sample geometry directly from the PAC06 container'''
wksp = self.loadPG3Data("testSimple")
material = {"ChemicalFormula": "Si", "SampleMassDensity": 1.165}
environment = {"Name": "InAir", "Container": "PAC06"}
geometry = {"Height": PAC06_HEIGHT*100} # convert to cm
SetSampleFromLogs(InputWorkspace=wksp, Environment=environment,
Material=material, Geometry=geometry)
# verify the results
self.validateSample(wksp.sample(), "Si", PAC06_HEIGHT)
del wksp # remove from the ADS
def testSpecifyNothing(self):
'''Specify almost nothing. This gets the information from the logs.'''
wksp = self.loadPG3Data("testSpecifyNothing")
material = {}
environment = {"Name": "InAir"}
geometry = {}
SetSampleFromLogs(InputWorkspace=wksp, Environment=environment,
Material=material, Geometry=geometry)
# verify the results
self.validateSample(wksp.sample(), "Si", 0.04)
del wksp # remove from the ADS
def testSpecifyNothingIgnoreGeometry(self):
'''Specify almost nothing and ignore the geometry from the logs. This gets information from the logs.'''
wksp = self.loadPG3Data("testSpecifyNothingIgnoreGeometry")
material = {}
environment = {"Name": "InAir"}
geometry = {}
SetSampleFromLogs(InputWorkspace=wksp, Environment=environment,
Material=material, Geometry=geometry, FindGeometry=False)
# verify the results
self.validateSample(wksp.sample(), "Si", PAC06_HEIGHT)
del wksp # remove from the ADS
if __name__ == '__main__':
unittest.main()
\ No newline at end of file
.. algorithm::
.. summary::
.. relatedalgorithms::
.. properties::
Description
-----------
This is a workflow algorithm that reads information from the sample logs before calling :ref:`SetSample <algm-SetSample>`.
Usage
-----
**Example - get all information from logs**
This assumes that all of the logs are present and contain valid information.
The environment is partially specified as ``InAir`` so the framework knows to look into the correct `environement file <https://github.com/mantidproject/mantid/blob/master/instrument/sampleenvironments/SNS/InAir.xml>`_.
.. code-block:: python
material = {}
environment = {"Name": "InAir"}
geometry = {}
SetSampleFromLogs(InputWorkspace=wksp, Environment=environment,
Material=material, Geometry=geometry)
**Example - specify all information**
This could done using :ref:`SetSample <algm-SetSample>` directly.
.. code-block:: python
material = {"ChemicalFormula": "Si", "SampleMassDensity": 1.165}
environment = {"Name": "InAir", "Container": "PAC06"}
geometry = {"Height": 4.} # cm
SetSampleFromLogs(InputWorkspace=wksp, Environment=environment,
Material=material, Geometry=geometry)
.. categories::
.. sourcelink::
......@@ -14,6 +14,7 @@ Powder Diffraction
New features
############
- New algorithm :ref:`CombineDiffCal <algm-CombineDiffCal>` to calibrate groups of pixels after cross correlation so that diffraction peaks can be adjusted to the correct positions
- New algorithm :ref:`SetSampleFromLogs <algm-SetSampleFromLogs>` inspects the sample enviroment logs for sample material and geometry information
- New script for doing calibration by groups, :ref:`PowderDiffractionCalibration <calibration_tofpd_group_calibration-ref>`
- New algorithm :ref:`MultipleScatteringCorrection <algm-MultipleScatteringCorrection>` to compute the multiple scattering correction factor for sample using numerical integration.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment