Skip to content

Commit 14fbb90

Browse files
imikejacksonclaude
andcommitted
feat(filter): scaffold MTRSimFilter + MTRSim algorithm, params, preflight validation + error tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6a79b63 commit 14fbb90

7 files changed

Lines changed: 614 additions & 0 deletions

File tree

MTRSimPlugin.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ set(${PLUGIN_NAME}_SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR})
3131
# MTRSim/src/MTRSim/Filters/ directory.
3232
set(FilterList
3333
ComputeODFFilter
34+
MTRSimFilter
3435
ReadMTRSimODFFilter
3536
WriteMTRSimODFFilter
3637
)
@@ -44,6 +45,7 @@ set(ActionList
4445
# ------------------------------------------------------------------------------
4546
set(AlgorithmList
4647
ComputeODF
48+
MTRSim
4749
ReadMTRSimODF
4850
WriteMTRSimODF
4951
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#include "MTRSim.hpp"
2+
3+
#include "simplnx/DataStructure/DataArray.hpp"
4+
5+
using namespace nx::core;
6+
7+
// -----------------------------------------------------------------------------
8+
MTRSim::MTRSim(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, MTRSimInputValues* inputValues)
9+
: m_DataStructure(dataStructure)
10+
, m_InputValues(inputValues)
11+
, m_ShouldCancel(shouldCancel)
12+
, m_MessageHandler(mesgHandler)
13+
{
14+
}
15+
16+
// -----------------------------------------------------------------------------
17+
MTRSim::~MTRSim() noexcept = default;
18+
19+
// -----------------------------------------------------------------------------
20+
Result<> MTRSim::operator()()
21+
{
22+
// NOTE: The algorithm body is implemented in a later task. For now this is a
23+
// no-op stub so the filter scaffolding (preflight + parameter validation) can
24+
// be built and tested independently.
25+
return {};
26+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#pragma once
2+
3+
#include "MTRSim/MTRSim_export.hpp"
4+
5+
#include "simplnx/DataStructure/DataPath.hpp"
6+
#include "simplnx/DataStructure/DataStructure.hpp"
7+
#include "simplnx/Filter/IFilter.hpp"
8+
9+
#include <vector>
10+
11+
namespace nx::core
12+
{
13+
14+
struct MTRSIM_EXPORT MTRSimInputValues
15+
{
16+
DataPath inputOdfGeometryPath;
17+
std::vector<DataPath> odfComponentPaths;
18+
std::vector<std::vector<double>> volumeFractions; // 1 row x N cols
19+
std::vector<std::vector<double>> thetaList; // M rows x 3 cols
20+
std::vector<float> physicalSize; // [x,y,z] microns
21+
std::vector<float> physicalSpacing; // [x,y,z] microns
22+
uint64 seed;
23+
bool generatePolarColoring;
24+
DataPath outputGeometryPath;
25+
std::string cellAttrMatName;
26+
std::string mtrIdsArrayName;
27+
std::string eulersArrayName;
28+
std::string polarColorsArrayName;
29+
};
30+
31+
/**
32+
* @class MTRSim
33+
* @brief Algorithm that generates a synthetic microtexture (MTR) microstructure
34+
* from an input ODF and a set of simulation parameters. The output ImageGeom and
35+
* its cell arrays are created by the filter's preflight; this algorithm fills them.
36+
*/
37+
class MTRSIM_EXPORT MTRSim
38+
{
39+
public:
40+
MTRSim(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, MTRSimInputValues* inputValues);
41+
~MTRSim() noexcept;
42+
43+
MTRSim(const MTRSim&) = delete;
44+
MTRSim(MTRSim&&) noexcept = delete;
45+
MTRSim& operator=(const MTRSim&) = delete;
46+
MTRSim& operator=(MTRSim&&) noexcept = delete;
47+
48+
Result<> operator()();
49+
50+
private:
51+
DataStructure& m_DataStructure;
52+
const MTRSimInputValues* m_InputValues = nullptr;
53+
const std::atomic_bool& m_ShouldCancel;
54+
const IFilter::MessageHandler& m_MessageHandler;
55+
};
56+
57+
} // namespace nx::core
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#include "MTRSimFilter.hpp"
2+
3+
#include "MTRSim/Filters/Algorithms/MTRSim.hpp"
4+
5+
#include "simplnx/Common/DataTypeUtilities.hpp"
6+
#include "simplnx/DataStructure/DataArray.hpp"
7+
#include "simplnx/DataStructure/DataPath.hpp"
8+
#include "simplnx/Filter/Actions/CreateArrayAction.hpp"
9+
#include "simplnx/Filter/Actions/CreateImageGeometryAction.hpp"
10+
#include "simplnx/Parameters/BoolParameter.hpp"
11+
#include "simplnx/Parameters/DataGroupCreationParameter.hpp"
12+
#include "simplnx/Parameters/DataObjectNameParameter.hpp"
13+
#include "simplnx/Parameters/DynamicTableParameter.hpp"
14+
#include "simplnx/Parameters/GeometrySelectionParameter.hpp"
15+
#include "simplnx/Parameters/MultiArraySelectionParameter.hpp"
16+
#include "simplnx/Parameters/NumberParameter.hpp"
17+
#include "simplnx/Parameters/VectorParameter.hpp"
18+
#include "simplnx/Parameters/util/DynamicTableInfo.hpp"
19+
20+
#include <fmt/format.h>
21+
22+
#include <chrono>
23+
#include <cmath>
24+
#include <random>
25+
#include <vector>
26+
27+
using namespace nx::core;
28+
29+
namespace nx::core
30+
{
31+
//------------------------------------------------------------------------------
32+
std::string MTRSimFilter::name() const
33+
{
34+
return FilterTraits<MTRSimFilter>::name.str();
35+
}
36+
37+
//------------------------------------------------------------------------------
38+
std::string MTRSimFilter::className() const
39+
{
40+
return FilterTraits<MTRSimFilter>::className;
41+
}
42+
43+
//------------------------------------------------------------------------------
44+
Uuid MTRSimFilter::uuid() const
45+
{
46+
return FilterTraits<MTRSimFilter>::uuid;
47+
}
48+
49+
//------------------------------------------------------------------------------
50+
std::string MTRSimFilter::humanName() const
51+
{
52+
return "Generate Synthetic Microtexture";
53+
}
54+
55+
//------------------------------------------------------------------------------
56+
std::vector<std::string> MTRSimFilter::defaultTags() const
57+
{
58+
return {className(), "MTRSim", "Synthetic", "Microtexture", "Generate"};
59+
}
60+
61+
//------------------------------------------------------------------------------
62+
Parameters MTRSimFilter::parameters() const
63+
{
64+
Parameters params;
65+
66+
params.insertSeparator(Parameters::Separator{"Input ODF"});
67+
params.insert(std::make_unique<GeometrySelectionParameter>(k_InputOdfGeometry_Key, "Input ODF Geometry", "Image Geometry holding the ODF (from the Read/Compute ODF filters).", DataPath{},
68+
GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image}));
69+
params.insert(std::make_unique<MultiArraySelectionParameter>(k_OdfComponentArrays_Key, "ODF Component Arrays", "Ordered list of per-component ODF cell arrays. Order maps to Volume Fraction columns.",
70+
MultiArraySelectionParameter::ValueType{}, MultiArraySelectionParameter::AllowedTypes{IArray::ArrayType::DataArray}, GetAllNumericTypes(),
71+
MultiArraySelectionParameter::AllowedComponentShapes{{1}}));
72+
73+
params.insertSeparator(Parameters::Separator{"Simulation Parameters"});
74+
{
75+
DynamicTableInfo vfInfo;
76+
vfInfo.setRowsInfo(DynamicTableInfo::StaticVectorInfo(1));
77+
vfInfo.setColsInfo(DynamicTableInfo::DynamicVectorInfo(1, 3, "Comp {}"));
78+
params.insert(std::make_unique<DynamicTableParameter>(k_VolumeFractions_Key, "Volume Fraction", "One value per ODF component; must match the component count and sum to 1.0.", vfInfo));
79+
}
80+
{
81+
DynamicTableInfo thetaInfo;
82+
thetaInfo.setRowsInfo(DynamicTableInfo::DynamicVectorInfo(1, 2, "Gaussian {}"));
83+
thetaInfo.setColsInfo(DynamicTableInfo::StaticVectorInfo(DynamicTableInfo::HeadersListType{"theta_x", "theta_y", "theta_z"}));
84+
params.insert(std::make_unique<DynamicTableParameter>(k_ThetaList_Key, "Theta List",
85+
"Correlation lengths [theta_x, theta_y, theta_z] per latent Gaussian. Needs >= (components - 1) rows. Same length unit as Physical "
86+
"Size/Spacing.",
87+
thetaInfo));
88+
}
89+
params.insert(std::make_unique<VectorFloat32Parameter>(k_PhysicalSize_Key, "Physical Size (microns)", "Domain extent X,Y,Z.", std::vector<float32>{38.1f, 12.7f, 0.0f},
90+
std::vector<std::string>{"X", "Y", "Z"}));
91+
params.insert(std::make_unique<VectorFloat32Parameter>(k_PhysicalSpacing_Key, "Physical Spacing (microns)", "Voxel spacing X,Y,Z.", std::vector<float32>{0.02f, 0.02f, 0.02f},
92+
std::vector<std::string>{"X", "Y", "Z"}));
93+
94+
params.insertSeparator(Parameters::Separator{"Random Number Seed Parameters"});
95+
params.insertLinkableParameter(std::make_unique<BoolParameter>(k_UseSeed_Key, "Use Seed for Random Generation", "When true the user can supply a fixed seed.", false));
96+
params.insert(std::make_unique<NumberParameter<uint64>>(k_SeedValue_Key, "Seed Value", "The seed fed into the random generator.", std::mt19937::default_seed));
97+
params.insert(std::make_unique<DataObjectNameParameter>(k_SeedArrayName_Key, "Stored Seed Value Array Name", "Top-level array recording the seed used.", "MTRSim SeedValue"));
98+
99+
params.insertSeparator(Parameters::Separator{"Outputs"});
100+
params.insertLinkableParameter(
101+
std::make_unique<BoolParameter>(k_GeneratePolarColoring_Key, "Generate Polar Coloring", "Create a 3-component UInt8 RGB array using the MATLAB polar color mapping.", false));
102+
params.insert(std::make_unique<DataGroupCreationParameter>(k_OutputGeometry_Key, "Output Image Geometry", "Path of the new microstructure Image Geometry.", DataPath({"MTR Microstructure"})));
103+
params.insert(std::make_unique<DataObjectNameParameter>(k_CellAttrMatName_Key, "Cell Attribute Matrix Name", "Name of the created cell AttributeMatrix.", "Cell Data"));
104+
params.insert(std::make_unique<DataObjectNameParameter>(k_MtrIdsArrayName_Key, "MTR Ids Array Name", "Int32 per-voxel MTR component id (1-based).", "MTRIds"));
105+
params.insert(std::make_unique<DataObjectNameParameter>(k_EulersArrayName_Key, "Euler Angles Array Name", "Float32 3-component Bunge Euler angles [rad].", "Eulers"));
106+
params.insert(std::make_unique<DataObjectNameParameter>(k_PolarColorsArrayName_Key, "Polar Colors Array Name", "UInt8 3-component RGB polar coloring.", "Polar Colors"));
107+
108+
params.linkParameters(k_UseSeed_Key, k_SeedValue_Key, true);
109+
params.linkParameters(k_GeneratePolarColoring_Key, k_PolarColorsArrayName_Key, true);
110+
111+
return params;
112+
}
113+
114+
//------------------------------------------------------------------------------
115+
IFilter::VersionType MTRSimFilter::parametersVersion() const
116+
{
117+
return 1;
118+
}
119+
120+
//------------------------------------------------------------------------------
121+
IFilter::UniquePointer MTRSimFilter::clone() const
122+
{
123+
return std::make_unique<MTRSimFilter>();
124+
}
125+
126+
//------------------------------------------------------------------------------
127+
IFilter::PreflightResult MTRSimFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel,
128+
const ExecutionContext& executionContext) const
129+
{
130+
auto pOdfArrays = filterArgs.value<MultiArraySelectionParameter::ValueType>(k_OdfComponentArrays_Key);
131+
auto pVolumeFractions = filterArgs.value<DynamicTableParameter::ValueType>(k_VolumeFractions_Key);
132+
auto pThetaList = filterArgs.value<DynamicTableParameter::ValueType>(k_ThetaList_Key);
133+
auto pSize = filterArgs.value<std::vector<float32>>(k_PhysicalSize_Key);
134+
auto pSpacing = filterArgs.value<std::vector<float32>>(k_PhysicalSpacing_Key);
135+
auto pGenPolar = filterArgs.value<bool>(k_GeneratePolarColoring_Key);
136+
auto pOutGeomPath = filterArgs.value<DataPath>(k_OutputGeometry_Key);
137+
auto pCellAttrMatName = filterArgs.value<std::string>(k_CellAttrMatName_Key);
138+
auto pMtrIdsName = filterArgs.value<std::string>(k_MtrIdsArrayName_Key);
139+
auto pEulersName = filterArgs.value<std::string>(k_EulersArrayName_Key);
140+
auto pPolarName = filterArgs.value<std::string>(k_PolarColorsArrayName_Key);
141+
auto pSeedArrayName = filterArgs.value<std::string>(k_SeedArrayName_Key);
142+
143+
nx::core::Result<OutputActions> resultOutputActions;
144+
std::vector<PreflightValue> preflightUpdatedValues;
145+
146+
const usize numComponents = pOdfArrays.size();
147+
if(numComponents < 2)
148+
{
149+
return {MakeErrorResult<OutputActions>(-13001, "MTRSim requires at least 2 ODF component arrays.")};
150+
}
151+
if(pVolumeFractions.size() != 1 || pVolumeFractions[0].size() != numComponents)
152+
{
153+
return {MakeErrorResult<OutputActions>(-13002, fmt::format("Volume Fraction must be 1 row x {} columns (one per ODF component).", numComponents))};
154+
}
155+
double vfSum = 0.0;
156+
for(double v : pVolumeFractions[0])
157+
{
158+
vfSum += v;
159+
}
160+
if(std::abs(vfSum - 1.0) > 1.0e-3)
161+
{
162+
return {MakeErrorResult<OutputActions>(-13003, fmt::format("Volume Fraction values must sum to 1.0 (got {:.4f}).", vfSum))};
163+
}
164+
if(pThetaList.size() < numComponents - 1)
165+
{
166+
return {MakeErrorResult<OutputActions>(-13004, fmt::format("Theta List needs at least {} rows (components - 1).", numComponents - 1))};
167+
}
168+
for(const auto& row : pThetaList)
169+
{
170+
if(row.size() != 3)
171+
{
172+
return {MakeErrorResult<OutputActions>(-13005, "Each Theta List row must have exactly 3 columns.")};
173+
}
174+
}
175+
176+
const auto dim = [](float len, float sp) { return static_cast<usize>(std::max(std::lround(len / sp), 1L)); };
177+
const usize nx = dim(pSize[0], pSpacing[0]);
178+
const usize ny = dim(pSize[1], pSpacing[1]);
179+
const usize nz = (pSize[2] <= 0.0f) ? 1 : dim(pSize[2], pSpacing[2]);
180+
181+
const std::vector<usize> imageGeomDimsXYZ = {nx, ny, nz};
182+
const std::vector<float32> origin = {0.0f, 0.0f, 0.0f};
183+
const std::vector<float32> spacingXYZ = {pSpacing[0], pSpacing[1], pSpacing[2]};
184+
const std::vector<usize> tupleShapeZYX = {nz, ny, nx};
185+
186+
resultOutputActions.value().appendAction(std::make_unique<CreateImageGeometryAction>(pOutGeomPath, imageGeomDimsXYZ, origin, spacingXYZ, pCellAttrMatName));
187+
188+
const DataPath cellAttrMatPath = pOutGeomPath.createChildPath(pCellAttrMatName);
189+
resultOutputActions.value().appendAction(std::make_unique<CreateArrayAction>(DataType::int32, tupleShapeZYX, std::vector<usize>{1}, cellAttrMatPath.createChildPath(pMtrIdsName)));
190+
resultOutputActions.value().appendAction(std::make_unique<CreateArrayAction>(DataType::float32, tupleShapeZYX, std::vector<usize>{3}, cellAttrMatPath.createChildPath(pEulersName)));
191+
if(pGenPolar)
192+
{
193+
resultOutputActions.value().appendAction(std::make_unique<CreateArrayAction>(DataType::uint8, tupleShapeZYX, std::vector<usize>{3}, cellAttrMatPath.createChildPath(pPolarName)));
194+
}
195+
resultOutputActions.value().appendAction(std::make_unique<CreateArrayAction>(DataType::uint64, std::vector<usize>{1}, std::vector<usize>{1}, DataPath({pSeedArrayName})));
196+
197+
preflightUpdatedValues.push_back({"Output Grid (X, Y, Z)", fmt::format("{} x {} x {}", nx, ny, nz)});
198+
preflightUpdatedValues.push_back({"Number of ODF Components", std::to_string(numComponents)});
199+
200+
return {std::move(resultOutputActions), std::move(preflightUpdatedValues)};
201+
}
202+
203+
//------------------------------------------------------------------------------
204+
Result<> MTRSimFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel,
205+
const ExecutionContext& executionContext) const
206+
{
207+
MTRSimInputValues inputValues;
208+
inputValues.inputOdfGeometryPath = filterArgs.value<DataPath>(k_InputOdfGeometry_Key);
209+
inputValues.odfComponentPaths = filterArgs.value<MultiArraySelectionParameter::ValueType>(k_OdfComponentArrays_Key);
210+
inputValues.volumeFractions = filterArgs.value<DynamicTableParameter::ValueType>(k_VolumeFractions_Key);
211+
inputValues.thetaList = filterArgs.value<DynamicTableParameter::ValueType>(k_ThetaList_Key);
212+
inputValues.physicalSize = filterArgs.value<std::vector<float32>>(k_PhysicalSize_Key);
213+
inputValues.physicalSpacing = filterArgs.value<std::vector<float32>>(k_PhysicalSpacing_Key);
214+
inputValues.generatePolarColoring = filterArgs.value<bool>(k_GeneratePolarColoring_Key);
215+
inputValues.outputGeometryPath = filterArgs.value<DataPath>(k_OutputGeometry_Key);
216+
inputValues.cellAttrMatName = filterArgs.value<std::string>(k_CellAttrMatName_Key);
217+
inputValues.mtrIdsArrayName = filterArgs.value<std::string>(k_MtrIdsArrayName_Key);
218+
inputValues.eulersArrayName = filterArgs.value<std::string>(k_EulersArrayName_Key);
219+
inputValues.polarColorsArrayName = filterArgs.value<std::string>(k_PolarColorsArrayName_Key);
220+
221+
uint64 seed = filterArgs.value<uint64>(k_SeedValue_Key);
222+
if(!filterArgs.value<bool>(k_UseSeed_Key))
223+
{
224+
seed = static_cast<uint64>(std::chrono::steady_clock::now().time_since_epoch().count());
225+
}
226+
dataStructure.getDataRefAs<UInt64Array>(DataPath({filterArgs.value<std::string>(k_SeedArrayName_Key)}))[0] = seed;
227+
inputValues.seed = seed;
228+
229+
return MTRSim(dataStructure, messageHandler, shouldCancel, &inputValues)();
230+
}
231+
232+
//------------------------------------------------------------------------------
233+
Result<Arguments> MTRSimFilter::FromSIMPLJson(const nlohmann::json& json)
234+
{
235+
Arguments args = MTRSimFilter().getDefaultArguments();
236+
237+
std::vector<Result<>> results;
238+
239+
/* This is a NEW filter and has no SIMPL (DREAM3D v6) equivalent. */
240+
241+
Result<> conversionResult = MergeResults(std::move(results));
242+
243+
return ConvertResultTo<Arguments>(std::move(conversionResult), std::move(args));
244+
}
245+
246+
} // namespace nx::core

0 commit comments

Comments
 (0)