-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RotateSampleShape algorithm created #37908
Changes from 7 commits
e6cb934
f96da9d
8485e97
1ee23f8
4d15d09
74e6800
e9b885c
a03defe
faacdcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Mantid Repository : https://github.com/mantidproject/mantid | ||
// | ||
// Copyright © 2024 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 + | ||
#pragma once | ||
|
||
#include "MantidAPI/Algorithm.h" | ||
#include "MantidAPI/MultipleExperimentInfos.h" | ||
#include "MantidCrystal/DllConfig.h" | ||
|
||
namespace Mantid { | ||
namespace Geometry { | ||
class Goniometer; | ||
} | ||
} // namespace Mantid | ||
|
||
namespace Mantid { | ||
namespace Crystal { | ||
|
||
using Mantid::Geometry::Goniometer; | ||
|
||
/** Define the initial orientation of the sample with respect to the beam and instrument by giving the axes and | ||
*directions of rotations. | ||
*/ | ||
class MANTID_CRYSTAL_DLL RotateSampleShape final : public API::Algorithm { | ||
public: | ||
/// Algorithm's name for identification | ||
const std::string name() const override { return "RotateSampleShape"; }; | ||
/// Summary of algorithms purpose | ||
const std::string summary() const override { | ||
return "Define the initial orientation of the sample with respect to the beam and instrument " | ||
"by giving the axes, angle and directions of rotations."; | ||
} | ||
|
||
/// Algorithm's version for identification | ||
int version() const override { return 1; }; | ||
const std::vector<std::string> seeAlso() const override { return {"SetGoniometer"}; } | ||
/// Algorithm's category for identification | ||
const std::string category() const override { return "Crystal\\Goniometer"; } | ||
|
||
private: | ||
/// Initialise the properties | ||
void init() override; | ||
/// Run the algorithm | ||
void exec() override; | ||
void prepareGoniometerAxes(Goniometer &gon, const API::ExperimentInfo_sptr &ei); | ||
bool checkIsValidShape(const API::ExperimentInfo_sptr &ei, std::string &shapeXML, bool &isMeshShape); | ||
}; | ||
|
||
} // namespace Crystal | ||
} // namespace Mantid |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
// Mantid Repository : https://github.com/mantidproject/mantid | ||
// | ||
// Copyright © 2024 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 + | ||
#include "MantidCrystal/RotateSampleShape.h" | ||
#include "MantidAPI/MatrixWorkspace.h" | ||
#include "MantidAPI/Run.h" | ||
#include "MantidAPI/Sample.h" | ||
#include "MantidDataHandling/CreateSampleShape.h" | ||
#include "MantidGeometry/Instrument/Goniometer.h" | ||
#include "MantidGeometry/Objects/MeshObject.h" | ||
#include "MantidGeometry/Objects/ShapeFactory.h" | ||
#include "MantidKernel/Strings.h" | ||
#include "MantidKernel/TimeSeriesProperty.h" | ||
#include <boost/algorithm/string/classification.hpp> | ||
#include <boost/algorithm/string/split.hpp> | ||
|
||
namespace Mantid::Crystal { | ||
|
||
// Register the algorithm into the AlgorithmFactory | ||
DECLARE_ALGORITHM(RotateSampleShape) | ||
|
||
using namespace Mantid::Geometry; | ||
using namespace Mantid::Kernel; | ||
using namespace Mantid::API; | ||
|
||
/// How many axes (max) to define | ||
const size_t NUM_AXES = 6; | ||
Mantid::Kernel::Logger g_log("RotateSampleShape"); | ||
|
||
/** Initialize the algorithm's properties. | ||
*/ | ||
void RotateSampleShape::init() { | ||
declareProperty(std::make_unique<WorkspaceProperty<Workspace>>("Workspace", "", Direction::InOut), | ||
"The workspace containing the sample whose orientation is to be rotated"); | ||
|
||
std::string axisHelp = ": degrees,x,y,z,1/-1 (1 for ccw, -1 for cw rotation)."; | ||
for (size_t i = 0; i < NUM_AXES; i++) { | ||
std::ostringstream propName; | ||
propName << "Axis" << i; | ||
declareProperty(std::make_unique<PropertyWithValue<std::string>>(propName.str(), "", Direction::Input), | ||
propName.str() + axisHelp); | ||
} | ||
} | ||
|
||
/** Execute the algorithm. | ||
*/ | ||
void RotateSampleShape::exec() { | ||
Workspace_sptr ws = getProperty("Workspace"); | ||
auto ei = std::dynamic_pointer_cast<ExperimentInfo>(ws); | ||
|
||
if (!ei) { | ||
// We're dealing with an MD workspace which has multiple experiment infos | ||
auto infos = std::dynamic_pointer_cast<MultipleExperimentInfos>(ws); | ||
if (!infos) { | ||
throw std::invalid_argument("Input workspace does not support RotateSampleShape"); | ||
} | ||
if (infos->getNumExperimentInfo() < 1) { | ||
ExperimentInfo_sptr info(new ExperimentInfo()); | ||
infos->addExperimentInfo(info); | ||
} | ||
ei = infos->getExperimentInfo(0); | ||
} | ||
|
||
std::string shapeXML; | ||
bool isMeshShape = false; | ||
if (!checkIsValidShape(ei, shapeXML, isMeshShape)) { | ||
throw std::runtime_error("Input sample does not have a valid shape!"); | ||
} | ||
|
||
// Create a goniometer with provided rotations | ||
Goniometer gon; | ||
prepareGoniometerAxes(gon, ei); | ||
if (gon.getNumberAxes() == 0) | ||
g_log.warning() << "Empty goniometer created; will always return an " | ||
"identity rotation matrix.\n"; | ||
|
||
const auto sampleShapeRotation = gon.getR(); | ||
if (sampleShapeRotation == Kernel::Matrix<double>(3, 3, true)) { | ||
// If the resulting rotationMatrix is Identity, ignore the calculatrion | ||
g_log.warning("Rotation matrix set via RotateSampleShape is an Identity matrix. Ignored rotating sample shape"); | ||
return; | ||
} | ||
|
||
const auto oldRotation = ei->run().getGoniometer().getR(); | ||
auto newSampleShapeRot = sampleShapeRotation * oldRotation; | ||
if (isMeshShape) { | ||
auto meshShape = std::dynamic_pointer_cast<MeshObject>(ei->sample().getShapePtr()); | ||
meshShape->rotate(newSampleShapeRot); | ||
} else { | ||
shapeXML = Geometry::ShapeFactory().addGoniometerTag(newSampleShapeRot, shapeXML); | ||
Mantid::DataHandling::CreateSampleShape::setSampleShape(*ei, shapeXML, false); | ||
} | ||
} | ||
|
||
bool RotateSampleShape::checkIsValidShape(const API::ExperimentInfo_sptr &ei, std::string &shapeXML, | ||
bool &isMeshShape) { | ||
if (ei->sample().hasShape()) { | ||
const auto csgShape = std::dynamic_pointer_cast<CSGObject>(ei->sample().getShapePtr()); | ||
if (csgShape && csgShape->hasValidShape()) { | ||
shapeXML = csgShape->getShapeXML(); | ||
if (!shapeXML.empty()) { | ||
return true; | ||
} | ||
} else { | ||
const auto meshShape = std::dynamic_pointer_cast<MeshObject>(ei->sample().getShapePtr()); | ||
if (meshShape && meshShape->hasValidShape()) { | ||
isMeshShape = true; | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
void RotateSampleShape::prepareGoniometerAxes(Goniometer &gon, const API::ExperimentInfo_sptr &ei) { | ||
for (size_t i = 0; i < NUM_AXES; i++) { | ||
std::ostringstream propName; | ||
propName << "Axis" << i; | ||
std::string axisDesc = getPropertyValue(propName.str()); | ||
if (!axisDesc.empty()) { | ||
std::vector<std::string> tokens; | ||
boost::split(tokens, axisDesc, boost::algorithm::detail::is_any_ofF<char>(",")); | ||
if (tokens.size() != 5) | ||
throw std::invalid_argument("Wrong number of arguments to parameter " + propName.str() + | ||
". Expected 5 comma-separated arguments."); | ||
|
||
std::transform(tokens.begin(), tokens.end(), tokens.begin(), [](std::string str) { return Strings::strip(str); }); | ||
if (!std::all_of(tokens.begin(), tokens.end(), [](std::string tokenStr) { return !tokenStr.empty(); })) { | ||
throw std::invalid_argument("Empty axis parameters found!"); | ||
} | ||
|
||
double angle = 0; | ||
if (!Strings::convert(tokens[0], angle)) { | ||
throw std::invalid_argument("Error converting angle string '" + tokens[0] + "' to a number."); | ||
} | ||
|
||
std::string axisName = "RotateSampleShapeAxis" + Strings::toString(i) + "_FixedValue"; | ||
g_log.information() << "Axis " << i << " - create a new log value: " << axisName; | ||
try { | ||
Types::Core::DateAndTime now = Types::Core::DateAndTime::getCurrentTime(); | ||
auto tsp = new Kernel::TimeSeriesProperty<double>(axisName); | ||
tsp->addValue(now, angle); | ||
tsp->setUnits("degree"); | ||
if (ei->mutableRun().hasProperty(axisName)) { | ||
ei->mutableRun().removeLogData(axisName); | ||
} | ||
ei->mutableRun().addLogData(tsp); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at this again, I don't think it's necessary to add the gonio axis to the logs as in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it is not necessary to add this gonio axis log values (with a different name opposed to |
||
} catch (...) { | ||
g_log.error("Could not add axis:" + axisName); | ||
} | ||
|
||
double x = 0, y = 0, z = 0; | ||
if (!Strings::convert(tokens[1], x)) | ||
throw std::invalid_argument("Error converting x string '" + tokens[1] + "' to a number."); | ||
if (!Strings::convert(tokens[2], y)) | ||
throw std::invalid_argument("Error converting y string '" + tokens[2] + "' to a number."); | ||
if (!Strings::convert(tokens[3], z)) | ||
throw std::invalid_argument("Error converting z string '" + tokens[3] + "' to a number."); | ||
V3D vec(x, y, z); | ||
if (vec.norm() < 1e-4) | ||
throw std::invalid_argument("Rotation axis vector should be non-zero!"); | ||
|
||
int ccw = 0; | ||
if (!Strings::convert(tokens[4], ccw)) { | ||
throw std::invalid_argument("Error converting sense of roation '" + tokens[4] + "' to a number."); | ||
} | ||
if (ccw != 1 && ccw != -1) { | ||
throw std::invalid_argument("The sense of rotation parameter must only be 1 (ccw) or -1 (cw)"); | ||
} | ||
// Default to degrees | ||
gon.pushAxis(axisName, x, y, z, angle, ccw); | ||
} | ||
} | ||
} | ||
|
||
} // namespace Mantid::Crystal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appears to be the same as
SetGoniometer
code - I wonder if it's possible to refactor and put in a place it can be called from both algorithms? Like a goniometer helperfile?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although they appear to be the same code, there are some differences in terms of some additional validations, throw errors I have added in
RotateSampleShape
's code and in the below line(angle
was set as 0 inSetGoniometer
alg)If you think those validations are better to be included in SetGoniometer algorithm as well, then I can move to a utility class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, is there any reason why the validation has to be different in this case? If so then I'm OK to duplicate some code here - I definitely don't want to change the behaviour of
SetGoniometer
)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1- Validation was done a little bit different to give more detailed information to the user such as
vs
2- For
SetGoniometer
the below is a valid axis but not valid forRotateSampleShape
as it needs the rotation angle as an input and hence needs a different validation.Axis0="Motor1,0,1,0,1"
3- The name of the TimeSeriesProperty(
axisName
) added as a log value inRotateSampleShape
below needs to differ fromSetGoniometer
as well to keep them separate when both algorithms are used for the same sample(which can be an input to helper class).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK sounds good thanks!