Skip to content

Commit f96da9d

Browse files
Rotate Sample shape done with unit tests and docs
1 parent e6cb934 commit f96da9d

File tree

7 files changed

+499
-4
lines changed

7 files changed

+499
-4
lines changed

Framework/Crystal/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ set(SRC_FILES
7373
src/SortPeaksWorkspace.cpp
7474
src/StatisticsOfPeaksWorkspace.cpp
7575
src/TransformHKL.cpp
76+
src/RotateSampleShape.cpp
7677
)
7778

7879
set(INC_FILES
@@ -153,6 +154,7 @@ set(INC_FILES
153154
inc/MantidCrystal/SortPeaksWorkspace.h
154155
inc/MantidCrystal/StatisticsOfPeaksWorkspace.h
155156
inc/MantidCrystal/TransformHKL.h
157+
inc/MantidCrystal/RotateSampleShape.h
156158
)
157159

158160
set(TEST_FILES
@@ -226,6 +228,7 @@ set(TEST_FILES
226228
SortPeaksWorkspaceTest.h
227229
StatisticsOfPeaksWorkspaceTest.h
228230
TransformHKLTest.h
231+
RotateSampleShapeTest.h
229232
)
230233

231234
if(COVERAGE)
@@ -261,7 +264,7 @@ include_directories(inc)
261264
target_link_libraries(
262265
Crystal
263266
PUBLIC Mantid::API Mantid::Geometry Mantid::Kernel
264-
PRIVATE Mantid::DataObjects Mantid::Indexing
267+
PRIVATE Mantid::DataObjects Mantid::Indexing Mantid::DataHandling
265268
)
266269

267270
# Add the unit tests directory
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Mantid Repository : https://github.com/mantidproject/mantid
2+
//
3+
// Copyright © 2024 ISIS Rutherford Appleton Laboratory UKRI,
4+
// NScD Oak Ridge National Laboratory, European Spallation Source,
5+
// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
6+
// SPDX - License - Identifier: GPL - 3.0 +
7+
#pragma once
8+
9+
#include "MantidAPI/Algorithm.h"
10+
#include "MantidAPI/MultipleExperimentInfos.h"
11+
#include "MantidCrystal/DllConfig.h"
12+
13+
namespace Mantid {
14+
namespace Geometry {
15+
class Goniometer;
16+
}
17+
} // namespace Mantid
18+
19+
namespace Mantid {
20+
namespace Crystal {
21+
22+
using Mantid::Geometry::Goniometer;
23+
24+
/** Define the initial orientation of the sample with respect to the beam and instrument by giving the axes and
25+
*directions of rotations.
26+
*/
27+
class MANTID_CRYSTAL_DLL RotateSampleShape final : public API::Algorithm {
28+
public:
29+
/// Algorithm's name for identification
30+
const std::string name() const override { return "RotateSampleShape"; };
31+
/// Summary of algorithms purpose
32+
const std::string summary() const override {
33+
return "Define the initial orientation of the sample with respect to the beam and instrument "
34+
"by giving the axes, angle and directions of rotations.";
35+
}
36+
37+
/// Algorithm's version for identification
38+
int version() const override { return 1; };
39+
const std::vector<std::string> seeAlso() const override { return {"SetGoniometer"}; }
40+
/// Algorithm's category for identification
41+
const std::string category() const override { return "Crystal\\Goniometer"; }
42+
43+
private:
44+
/// Initialise the properties
45+
void init() override;
46+
/// Run the algorithm
47+
void exec() override;
48+
void prepareGoniometerAxes(Goniometer &gon, const API::ExperimentInfo_sptr &ei);
49+
bool checkIsValidShape(const API::ExperimentInfo_sptr &ei, std::string &shapeXML, bool &isMeshShape);
50+
};
51+
52+
} // namespace Crystal
53+
} // namespace Mantid
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Mantid Repository : https://github.com/mantidproject/mantid
2+
//
3+
// Copyright &copy; 2018 ISIS Rutherford Appleton Laboratory UKRI,
4+
// NScD Oak Ridge National Laboratory, European Spallation Source,
5+
// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
6+
// SPDX - License - Identifier: GPL - 3.0 +
7+
#include "MantidCrystal/RotateSampleShape.h"
8+
#include "MantidAPI/MatrixWorkspace.h"
9+
#include "MantidAPI/Run.h"
10+
#include "MantidAPI/Sample.h"
11+
#include "MantidDataHandling/CreateSampleShape.h"
12+
#include "MantidGeometry/Instrument/Goniometer.h"
13+
#include "MantidGeometry/Objects/MeshObject.h"
14+
#include "MantidGeometry/Objects/ShapeFactory.h"
15+
#include "MantidKernel/Strings.h"
16+
#include "MantidKernel/TimeSeriesProperty.h"
17+
#include <boost/algorithm/string/classification.hpp>
18+
#include <boost/algorithm/string/split.hpp>
19+
20+
namespace Mantid::Crystal {
21+
22+
// Register the algorithm into the AlgorithmFactory
23+
DECLARE_ALGORITHM(RotateSampleShape)
24+
25+
using namespace Mantid::Geometry;
26+
using namespace Mantid::Kernel;
27+
using namespace Mantid::API;
28+
29+
/// How many axes (max) to define
30+
const size_t NUM_AXES = 6;
31+
Mantid::Kernel::Logger g_log("RotateSampleShape");
32+
33+
/** Initialize the algorithm's properties.
34+
*/
35+
void RotateSampleShape::init() {
36+
declareProperty(std::make_unique<WorkspaceProperty<Workspace>>("Workspace", "", Direction::InOut),
37+
"The workspace containing the sample whose orientation is to be rotated");
38+
39+
std::string axisHelp = ": degrees,x,y,z,1/-1 (1 for ccw, -1 for cw rotation).";
40+
for (size_t i = 0; i < NUM_AXES; i++) {
41+
std::ostringstream propName;
42+
propName << "Axis" << i;
43+
declareProperty(std::make_unique<PropertyWithValue<std::string>>(propName.str(), "", Direction::Input),
44+
propName.str() + axisHelp);
45+
}
46+
}
47+
48+
/** Execute the algorithm.
49+
*/
50+
void RotateSampleShape::exec() {
51+
Workspace_sptr ws = getProperty("Workspace");
52+
auto ei = std::dynamic_pointer_cast<ExperimentInfo>(ws);
53+
54+
if (!ei) {
55+
// We're dealing with an MD workspace which has multiple experiment infos
56+
auto infos = std::dynamic_pointer_cast<MultipleExperimentInfos>(ws);
57+
if (!infos) {
58+
throw std::invalid_argument("Input workspace does not support RotateSampleShape");
59+
}
60+
if (infos->getNumExperimentInfo() < 1) {
61+
ExperimentInfo_sptr info(new ExperimentInfo());
62+
infos->addExperimentInfo(info);
63+
}
64+
ei = infos->getExperimentInfo(0);
65+
}
66+
67+
std::string shapeXML;
68+
bool isMeshShape = false;
69+
if (!checkIsValidShape(ei, shapeXML, isMeshShape)) {
70+
throw std::runtime_error("Input sample does not have a valid shape!");
71+
}
72+
73+
// Create a goniometer with provided rotations
74+
Goniometer gon;
75+
prepareGoniometerAxes(gon, ei);
76+
if (gon.getNumberAxes() == 0)
77+
g_log.warning() << "Empty goniometer created; will always return an "
78+
"identity rotation matrix.\n";
79+
80+
const auto sampleShapeRotation = gon.getR();
81+
if (sampleShapeRotation == Kernel::Matrix<double>(3, 3, true)) {
82+
// If the resulting rotationMatrix is Identity, ignore the calculatrion
83+
g_log.warning("Rotation matrix set via RotateSampleShape is an Identity matrix. Ignored rotating sample shape");
84+
return;
85+
}
86+
87+
const auto oldRotation = ei->run().getGoniometer().getR();
88+
auto newSampleShapeRot = sampleShapeRotation * oldRotation;
89+
if (isMeshShape) {
90+
auto meshShape = std::dynamic_pointer_cast<MeshObject>(ei->sample().getShapePtr());
91+
meshShape->rotate(newSampleShapeRot);
92+
} else {
93+
shapeXML = Geometry::ShapeFactory().addGoniometerTag(newSampleShapeRot, shapeXML);
94+
Mantid::DataHandling::CreateSampleShape::setSampleShape(*ei, shapeXML, false);
95+
}
96+
}
97+
98+
bool RotateSampleShape::checkIsValidShape(const API::ExperimentInfo_sptr &ei, std::string &shapeXML,
99+
bool &isMeshShape) {
100+
if (ei->sample().hasShape()) {
101+
const auto csgShape = std::dynamic_pointer_cast<CSGObject>(ei->sample().getShapePtr());
102+
if (csgShape && csgShape->hasValidShape()) {
103+
shapeXML = csgShape->getShapeXML();
104+
if (!shapeXML.empty()) {
105+
return true;
106+
}
107+
} else {
108+
const auto meshShape = std::dynamic_pointer_cast<MeshObject>(ei->sample().getShapePtr());
109+
if (meshShape && meshShape->hasValidShape()) {
110+
isMeshShape = true;
111+
return true;
112+
}
113+
}
114+
}
115+
return false;
116+
}
117+
118+
void RotateSampleShape::prepareGoniometerAxes(Goniometer &gon, const API::ExperimentInfo_sptr &ei) {
119+
for (size_t i = 0; i < NUM_AXES; i++) {
120+
std::ostringstream propName;
121+
propName << "Axis" << i;
122+
std::string axisDesc = getPropertyValue(propName.str());
123+
if (!axisDesc.empty()) {
124+
std::vector<std::string> tokens;
125+
boost::split(tokens, axisDesc, boost::algorithm::detail::is_any_ofF<char>(","));
126+
if (tokens.size() != 5)
127+
throw std::invalid_argument("Wrong number of arguments to parameter " + propName.str() +
128+
". Expected 5 comma-separated arguments.");
129+
130+
std::transform(tokens.begin(), tokens.end(), tokens.begin(), [](std::string str) { return Strings::strip(str); });
131+
if (!std::all_of(tokens.begin(), tokens.end(), [](std::string tokenStr) { return !tokenStr.empty(); })) {
132+
throw std::invalid_argument("Empty axis parameters found!");
133+
}
134+
135+
double angle = 0;
136+
if (!Strings::convert(tokens[0], angle)) {
137+
throw std::invalid_argument("Error converting angle string '" + tokens[0] + "' to a number.");
138+
}
139+
140+
std::string axisName = "RotateSampleShapeAxis" + Strings::toString(i) + "_FixedValue";
141+
g_log.information() << "Axis " << i << " - create a new log value: " << axisName;
142+
try {
143+
Types::Core::DateAndTime now = Types::Core::DateAndTime::getCurrentTime();
144+
auto tsp = new Kernel::TimeSeriesProperty<double>(axisName);
145+
tsp->addValue(now, angle);
146+
tsp->setUnits("degree");
147+
if (ei->mutableRun().hasProperty(axisName)) {
148+
ei->mutableRun().removeLogData(axisName);
149+
}
150+
ei->mutableRun().addLogData(tsp);
151+
} catch (...) {
152+
g_log.error("Could not add axis:" + axisName);
153+
}
154+
155+
double x = 0, y = 0, z = 0;
156+
if (!Strings::convert(tokens[1], x))
157+
throw std::invalid_argument("Error converting x string '" + tokens[1] + "' to a number.");
158+
if (!Strings::convert(tokens[2], y))
159+
throw std::invalid_argument("Error converting y string '" + tokens[2] + "' to a number.");
160+
if (!Strings::convert(tokens[3], z))
161+
throw std::invalid_argument("Error converting z string '" + tokens[3] + "' to a number.");
162+
V3D vec(x, y, z);
163+
if (vec.norm() < 1e-4)
164+
throw std::invalid_argument("Rotation axis vector should be non-zero!");
165+
166+
int ccw = 0;
167+
if (!Strings::convert(tokens[4], ccw)) {
168+
throw std::invalid_argument("Error converting sense of roation '" + tokens[4] + "' to a number.");
169+
}
170+
if (ccw != 1 && ccw != -1) {
171+
throw std::invalid_argument("The sense of rotation parameter must only be 1 (ccw) or -1 (cw)");
172+
}
173+
// Default to degrees
174+
gon.pushAxis(axisName, x, y, z, angle, ccw);
175+
}
176+
}
177+
}
178+
179+
} // namespace Mantid::Crystal

0 commit comments

Comments
 (0)