Skip to content
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

Merged
merged 9 commits into from
Sep 12, 2024
5 changes: 4 additions & 1 deletion Framework/Crystal/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ set(SRC_FILES
src/SortPeaksWorkspace.cpp
src/StatisticsOfPeaksWorkspace.cpp
src/TransformHKL.cpp
src/RotateSampleShape.cpp
)

set(INC_FILES
Expand Down Expand Up @@ -153,6 +154,7 @@ set(INC_FILES
inc/MantidCrystal/SortPeaksWorkspace.h
inc/MantidCrystal/StatisticsOfPeaksWorkspace.h
inc/MantidCrystal/TransformHKL.h
inc/MantidCrystal/RotateSampleShape.h
)

set(TEST_FILES
Expand Down Expand Up @@ -226,6 +228,7 @@ set(TEST_FILES
SortPeaksWorkspaceTest.h
StatisticsOfPeaksWorkspaceTest.h
TransformHKLTest.h
RotateSampleShapeTest.h
)

if(COVERAGE)
Expand Down Expand Up @@ -261,7 +264,7 @@ include_directories(inc)
target_link_libraries(
Crystal
PUBLIC Mantid::API Mantid::Geometry Mantid::Kernel
PRIVATE Mantid::DataObjects Mantid::Indexing
PRIVATE Mantid::DataObjects Mantid::Indexing Mantid::DataHandling
)

# Add the unit tests directory
Expand Down
53 changes: 53 additions & 0 deletions Framework/Crystal/inc/MantidCrystal/RotateSampleShape.h
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
179 changes: 179 additions & 0 deletions Framework/Crystal/src/RotateSampleShape.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Mantid Repository : https://github.com/mantidproject/mantid
//
// Copyright &copy; 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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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 in SetGoniometer alg)

gon.pushAxis(axisName, x, y, z, angle, ccw); 

If you think those validations are better to be included in SetGoniometer algorithm as well, then I can move to a utility class.

Copy link
Contributor

@RichardWaiteSTFC RichardWaiteSTFC Sep 10, 2024

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)

Copy link
Contributor Author

@warunawickramasingha warunawickramasingha Sep 10, 2024

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

if (!Strings::convert(tokens[1], x))
        throw std::invalid_argument("Error converting x string '" + tokens[1] + "' to a number.");

vs

if (!Strings::convert(tokens[1], x))
          throw std::invalid_argument("Error converting string '" + tokens[1] + "' to a number.");

2- For SetGoniometer the below is a valid axis but not valid for RotateSampleShape 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 in RotateSampleShape below needs to differ from SetGoniometer as well to keep them separate when both algorithms are used for the same sample(which can be an input to helper class).

if (ei->mutableRun().hasProperty(axisName)) {
          ei->mutableRun().removeLogData(axisName);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK sounds good thanks!

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);
Copy link
Contributor

Choose a reason for hiding this comment

The 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 SetGoniometer - here everything we need is saved on the sample shape/xml right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 SetGoniometer) this bit can be removed, since it does not affect the functionality.

} 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
Loading
Loading