|
| 1 | +#include "GroupMicroTextureRegions.hpp" |
| 2 | + |
| 3 | +#include "simplnx/Common/Constants.hpp" |
| 4 | +#include "simplnx/DataStructure/DataArray.hpp" |
| 5 | +#include "simplnx/DataStructure/NeighborList.hpp" |
| 6 | +#include "simplnx/Utilities/Math/GeometryMath.hpp" |
| 7 | +#include "simplnx/Utilities/MessageHelper.hpp" |
| 8 | + |
| 9 | +#include "EbsdLib/LaueOps/LaueOps.h" |
| 10 | + |
| 11 | +#include <random> |
| 12 | + |
| 13 | +using namespace nx::core; |
| 14 | + |
| 15 | +// ----------------------------------------------------------------------------- |
| 16 | +GroupMicroTextureRegions::GroupMicroTextureRegions(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, |
| 17 | + GroupMicroTextureRegionsInputValues* inputValues) |
| 18 | +: m_DataStructure(dataStructure) |
| 19 | +, m_InputValues(inputValues) |
| 20 | +, m_ShouldCancel(shouldCancel) |
| 21 | +, m_MessageHandler(mesgHandler) |
| 22 | +, m_FeaturePhases(m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->FeaturePhasesArrayPath)) |
| 23 | +, m_FeatureParentIds(m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->FeatureParentIdsArrayName)) |
| 24 | +, m_CrystalStructures(m_DataStructure.getDataRefAs<UInt32Array>(m_InputValues->CrystalStructuresArrayPath)) |
| 25 | +, m_AvgQuats(m_DataStructure.getDataRefAs<Float32Array>(m_InputValues->AvgQuatsArrayPath)) |
| 26 | +, m_Volumes(m_DataStructure.getDataRefAs<Float32Array>(m_InputValues->VolumesArrayPath)) |
| 27 | +{ |
| 28 | +} |
| 29 | + |
| 30 | +// ----------------------------------------------------------------------------- |
| 31 | +GroupMicroTextureRegions::~GroupMicroTextureRegions() noexcept = default; |
| 32 | + |
| 33 | +// ----------------------------------------------------------------------------- |
| 34 | +const std::atomic_bool& GroupMicroTextureRegions::getCancel() |
| 35 | +{ |
| 36 | + return m_ShouldCancel; |
| 37 | +} |
| 38 | + |
| 39 | +// ----------------------------------------------------------------------------- |
| 40 | +void GroupMicroTextureRegions::randomizeParentIds(usize totalPoints, usize totalParentIds) |
| 41 | +{ |
| 42 | + // Shuffle parent IDs in [1, totalParentIds-1] via Fisher-Yates with the same |
| 43 | + // RNG state already seeded by operator(). Parent ID 0 is reserved (unassigned) |
| 44 | + // and is excluded from the shuffle so cells with no parent stay at 0. |
| 45 | + auto& cellParentIds = m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->CellParentIdsArrayName); |
| 46 | + |
| 47 | + std::vector<int32> shuffle(totalParentIds); |
| 48 | + for(usize i = 0; i < totalParentIds; i++) |
| 49 | + { |
| 50 | + shuffle[i] = static_cast<int32>(i); |
| 51 | + } |
| 52 | + |
| 53 | + std::uniform_int_distribution<usize> intDist(1, totalParentIds - 1); |
| 54 | + for(usize i = 1; i < totalParentIds; i++) |
| 55 | + { |
| 56 | + usize r = intDist(m_Generator); |
| 57 | + std::swap(shuffle[i], shuffle[r]); |
| 58 | + } |
| 59 | + |
| 60 | + // Remap feature parent IDs first so cell parent IDs can index through the new mapping |
| 61 | + const usize numFeatures = m_FeatureParentIds.getNumberOfTuples(); |
| 62 | + for(usize f = 0; f < numFeatures; f++) |
| 63 | + { |
| 64 | + const int32 oldId = m_FeatureParentIds[f]; |
| 65 | + if(oldId >= 0 && static_cast<usize>(oldId) < totalParentIds) |
| 66 | + { |
| 67 | + m_FeatureParentIds[f] = shuffle[oldId]; |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + // Re-derive cell parent IDs from the shuffled feature parent IDs |
| 72 | + auto& featureIds = m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->FeatureIdsArrayPath); |
| 73 | + for(usize k = 0; k < totalPoints; k++) |
| 74 | + { |
| 75 | + cellParentIds[k] = m_FeatureParentIds[featureIds[k]]; |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +// ----------------------------------------------------------------------------- |
| 80 | +Result<> GroupMicroTextureRegions::execute() |
| 81 | +{ |
| 82 | + MessageHelper messageHelper(m_MessageHandler); |
| 83 | + ThrottledMessenger throttledMessenger = messageHelper.createThrottledMessenger(); |
| 84 | + |
| 85 | + NeighborList<int32>& featureNeighborListRef = m_DataStructure.getDataRefAs<NeighborList<int32>>(m_InputValues->ContiguousNeighborListArrayPath); |
| 86 | + NeighborList<int32>* nonContigNeighListPtr = nullptr; |
| 87 | + if(m_InputValues->UseNonContiguousNeighbors) |
| 88 | + { |
| 89 | + nonContigNeighListPtr = m_DataStructure.getDataAs<NeighborList<int32>>(m_InputValues->NonContiguousNeighborListArrayPath); |
| 90 | + if(nullptr == nonContigNeighListPtr) |
| 91 | + { |
| 92 | + return MakeErrorResult(-99345, "There was an error getting the Non-contiguous neighborlist from the DataStructure"); |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + std::vector<int32> groupList; |
| 97 | + |
| 98 | + int32 parentCount = 0; |
| 99 | + int32 featureSeed = 0; |
| 100 | + int32 list1size = 0; |
| 101 | + int32 list2size = 0; |
| 102 | + |
| 103 | + while(featureSeed >= 0) |
| 104 | + { |
| 105 | + parentCount++; |
| 106 | + featureSeed = getSeed(parentCount); |
| 107 | + if(featureSeed < 0) |
| 108 | + { |
| 109 | + continue; |
| 110 | + } |
| 111 | + |
| 112 | + groupList.clear(); |
| 113 | + groupList.push_back(featureSeed); |
| 114 | + for(std::vector<int32>::size_type j = 0; j < groupList.size(); j++) |
| 115 | + { |
| 116 | + const int32 firstFeature = groupList[j]; |
| 117 | + list1size = static_cast<int32>(featureNeighborListRef[firstFeature].size()); |
| 118 | + if(m_InputValues->UseNonContiguousNeighbors) |
| 119 | + { |
| 120 | + list2size = nonContigNeighListPtr->getListSize(firstFeature); |
| 121 | + } |
| 122 | + // Walk contiguous neighbors (k=0) then optional non-contiguous neighbors (k=1) |
| 123 | + for(int32 k = 0; k < 2; k++) |
| 124 | + { |
| 125 | + const int32 listSize = (k == 0) ? list1size : list2size; |
| 126 | + for(int32 l = 0; l < listSize; l++) |
| 127 | + { |
| 128 | + int32 neigh = -1; |
| 129 | + if(k == 0) |
| 130 | + { |
| 131 | + neigh = featureNeighborListRef[firstFeature][l]; |
| 132 | + } |
| 133 | + else if(k == 1 && m_InputValues->UseNonContiguousNeighbors) |
| 134 | + { |
| 135 | + bool ok = false; |
| 136 | + neigh = nonContigNeighListPtr->getValue(firstFeature, l, ok); |
| 137 | + } |
| 138 | + if(neigh >= 0 && neigh != firstFeature) |
| 139 | + { |
| 140 | + if(determineGrouping(firstFeature, neigh, parentCount)) |
| 141 | + { |
| 142 | + groupList.push_back(neigh); |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + throttledMessenger.sendThrottledMessage([&]() { return fmt::format("Parent Count: {}", parentCount); }); |
| 150 | + } |
| 151 | + return {}; |
| 152 | +} |
| 153 | + |
| 154 | +// ----------------------------------------------------------------------------- |
| 155 | +Result<> GroupMicroTextureRegions::operator()() |
| 156 | +{ |
| 157 | + MessageHelper messageHelper(m_MessageHandler); |
| 158 | + |
| 159 | + m_Generator = std::mt19937_64(m_InputValues->SeedValue); |
| 160 | + m_Distribution = std::uniform_real_distribution<float32>(0.0f, 1.0f); |
| 161 | + |
| 162 | + // Initialize Data |
| 163 | + m_AvgCAxes[0] = 0.0f; |
| 164 | + m_AvgCAxes[1] = 0.0f; |
| 165 | + m_AvgCAxes[2] = 0.0f; |
| 166 | + m_FeatureParentIds.fill(-1); |
| 167 | + |
| 168 | + // Execute the main grouping algorithm |
| 169 | + messageHelper.sendMessage(fmt::format("Start Grouping.....")); |
| 170 | + |
| 171 | + // Execute the grouping algorithm |
| 172 | + Result<> result = execute(); |
| 173 | + if(result.invalid()) |
| 174 | + { |
| 175 | + return result; |
| 176 | + } |
| 177 | + |
| 178 | + // handle active array resize |
| 179 | + if(m_NumTuples < 2) |
| 180 | + { |
| 181 | + return MakeErrorResult(-87000, fmt::format("The number of grouped Features was {} which means no grouped features were detected. A grouping value may be set too high", m_NumTuples)); |
| 182 | + } |
| 183 | + m_DataStructure.getDataRefAs<AttributeMatrix>(m_InputValues->NewCellFeatureAttributeMatrixName).resizeTuples(ShapeType{m_NumTuples}); |
| 184 | + |
| 185 | + auto& cellParentIds = m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->CellParentIdsArrayName); |
| 186 | + auto& featureIds = m_DataStructure.getDataRefAs<Int32Array>(m_InputValues->FeatureIdsArrayPath); |
| 187 | + const usize totalPoints = featureIds.getNumberOfTuples(); |
| 188 | + for(usize k = 0; k < totalPoints; k++) |
| 189 | + { |
| 190 | + cellParentIds[k] = m_FeatureParentIds[featureIds[k]]; |
| 191 | + } |
| 192 | + |
| 193 | + if(m_InputValues->RandomizeParentIds) |
| 194 | + { |
| 195 | + messageHelper.sendMessage(fmt::format("Randomizing Parent Ids")); |
| 196 | + randomizeParentIds(totalPoints, m_NumTuples); |
| 197 | + } |
| 198 | + |
| 199 | + return {}; |
| 200 | +} |
| 201 | + |
| 202 | +// ----------------------------------------------------------------------------- |
| 203 | +int GroupMicroTextureRegions::getSeed(int32 newFid) |
| 204 | +{ |
| 205 | + usize numFeatures = m_FeaturePhases.getNumberOfTuples(); |
| 206 | + |
| 207 | + int32 featureIdSeed = -1; |
| 208 | + |
| 209 | + // Precalculate some constants |
| 210 | + const int32 totalFMinus1 = static_cast<int32>(numFeatures) - 1; |
| 211 | + |
| 212 | + usize counter = 0; |
| 213 | + // This section finds a feature id that has not been grouped yet. It starts by |
| 214 | + // randomly selecting a feature id between 0 and numFeatures-1. We then start |
| 215 | + // looping. If the initial random value is valid then we exit the loop after |
| 216 | + // a single iteration. If that feature has already been grouped, then we add one |
| 217 | + // to the `randFeature` value and try again. If we get to the end of the range of |
| 218 | + // featureIds then the algorithm will loop back to featureId = 0 and start incrementing |
| 219 | + // from there. This is reasonably efficient as we only generate random numbers |
| 220 | + // as needed. |
| 221 | + auto randFeature = static_cast<int32>(m_Distribution(m_Generator) * static_cast<float32>(totalFMinus1)); |
| 222 | + while(featureIdSeed == -1 && counter < numFeatures) |
| 223 | + { |
| 224 | + if(randFeature > totalFMinus1) |
| 225 | + { |
| 226 | + randFeature = randFeature - numFeatures; |
| 227 | + } |
| 228 | + if(m_FeatureParentIds.getValue(randFeature) == -1) |
| 229 | + { |
| 230 | + featureIdSeed = randFeature; |
| 231 | + } |
| 232 | + randFeature++; |
| 233 | + counter++; |
| 234 | + } |
| 235 | + |
| 236 | + // // Used for debugging and demonstration |
| 237 | + // if(newFid == 1) |
| 238 | + // { |
| 239 | + // auto& centroids = m_DataStructure.getDataRefAs<Float32Array>(m_InputValues->VolumesArrayPath.replaceName("Centroids")); |
| 240 | + // std::ofstream fout ("/tmp/GroupMicroTextureInitialVoxelSeeds.txt", std::ios_base::out | std::ios_base::app); |
| 241 | + // fout << fmt::format("Feature Parent Id: {} | X: {}, Y: {}\n", voxelSeed, centroids.getComponent(voxelSeed, 0), centroids.getComponent(voxelSeed, 1)); |
| 242 | + // } |
| 243 | + |
| 244 | + if(featureIdSeed >= 0) |
| 245 | + { |
| 246 | + m_FeatureParentIds[featureIdSeed] = newFid; |
| 247 | + m_NumTuples = newFid + 1; |
| 248 | + |
| 249 | + if(m_InputValues->UseRunningAverage) |
| 250 | + { |
| 251 | + usize index = featureIdSeed * 4; |
| 252 | + // Get the orientation matrix (which is passive) and then transpose it to make it active transform |
| 253 | + ebsdlib::Matrix3X3F g1t = ebsdlib::Quaternion<float32>(m_AvgQuats.getValue(index + 0), m_AvgQuats.getValue(index + 1), m_AvgQuats.getValue(index + 2), m_AvgQuats.getValue(index + 3)) |
| 254 | + .toOrientationMatrix() |
| 255 | + .toGMatrix() |
| 256 | + .transpose(); |
| 257 | + ebsdlib::Matrix3X1F cAxis(0.0f, 0.0f, 1.0f); |
| 258 | + // normalize so that the dot product can be taken below without |
| 259 | + // dividing by the magnitudes (they would be 1) |
| 260 | + const ebsdlib::Matrix3X1F c1 = (g1t * cAxis).normalize(); |
| 261 | + |
| 262 | + m_AvgCAxes = c1 * m_Volumes.getValue(featureIdSeed); |
| 263 | + } |
| 264 | + } |
| 265 | + |
| 266 | + return featureIdSeed; |
| 267 | +} |
| 268 | + |
| 269 | +// ----------------------------------------------------------------------------- |
| 270 | +bool GroupMicroTextureRegions::determineGrouping(int32 referenceFeature, int32 neighborFeature, int32 newFid) |
| 271 | +{ |
| 272 | + const int32 neighborParentId = m_FeatureParentIds.getValue(neighborFeature); |
| 273 | + const int32 referenceFeaturePhase = m_FeaturePhases.getValue(referenceFeature); |
| 274 | + const int32 neighborFeaturePhase = m_FeaturePhases.getValue(neighborFeature); |
| 275 | + |
| 276 | + if(neighborParentId == -1 && referenceFeaturePhase > 0 && neighborFeaturePhase > 0) |
| 277 | + { |
| 278 | + ebsdlib::Matrix3X1F c1 = {0.0f, 0.0f, 0.0f}; |
| 279 | + ebsdlib::Matrix3X1F cAxis(0.0f, 0.0f, 1.0f); |
| 280 | + |
| 281 | + if(!m_InputValues->UseRunningAverage) |
| 282 | + { |
| 283 | + const usize index = referenceFeature * 4; |
| 284 | + // Get the orientation matrix (which is passive) and then transpose it to make it active transform |
| 285 | + // transpose the g matrix so when c-axis is multiplied by it, |
| 286 | + // it will give the sample direction that the c-axis is along |
| 287 | + ebsdlib::Matrix3X3F g1t = ebsdlib::Quaternion<float32>(m_AvgQuats.getValue(index + 0), m_AvgQuats.getValue(index + 1), m_AvgQuats.getValue(index + 2), m_AvgQuats.getValue(index + 3)) |
| 288 | + .toOrientationMatrix() |
| 289 | + .toGMatrix() |
| 290 | + .transpose(); |
| 291 | + c1 = (g1t * cAxis).normalize(); |
| 292 | + } |
| 293 | + uint32 phase1 = m_CrystalStructures.getValue(referenceFeaturePhase); |
| 294 | + uint32 phase2 = m_CrystalStructures.getValue(neighborFeaturePhase); |
| 295 | + if(phase1 == phase2 && (phase1 == ebsdlib::CrystalStructure::Hexagonal_High)) |
| 296 | + { |
| 297 | + const usize index = neighborFeature * 4; |
| 298 | + // Get the orientation matrix (which is passive) and then transpose it to make it active transform |
| 299 | + // transpose the g matrix so when c-axis is multiplied by it, |
| 300 | + // it will give the sample direction that the c-axis is along |
| 301 | + ebsdlib::Matrix3X3F g2t = ebsdlib::Quaternion<float32>(m_AvgQuats.getValue(index + 0), m_AvgQuats.getValue(index + 1), m_AvgQuats.getValue(index + 2), m_AvgQuats.getValue(index + 3)) |
| 302 | + .toOrientationMatrix() |
| 303 | + .toGMatrix() |
| 304 | + .transpose(); |
| 305 | + ebsdlib::Matrix3X1F c2 = (g2t * cAxis).normalize(); |
| 306 | + |
| 307 | + float32 w; |
| 308 | + if(m_InputValues->UseRunningAverage) |
| 309 | + { |
| 310 | + w = m_AvgCAxes.cosTheta(c2); |
| 311 | + } |
| 312 | + else |
| 313 | + { |
| 314 | + w = c1.cosTheta(c2); |
| 315 | + } |
| 316 | + w = std::acos(std::clamp(w, -1.0f, 1.0f)); |
| 317 | + |
| 318 | + // Convert user defined tolerance to radians. |
| 319 | + float32 cAxisToleranceRad = m_InputValues->CAxisTolerance * nx::core::Constants::k_PiF / 180.0f; |
| 320 | + if(w <= cAxisToleranceRad || (nx::core::Constants::k_PiD - w) <= cAxisToleranceRad) |
| 321 | + { |
| 322 | + m_FeatureParentIds.setValue(neighborFeature, newFid); |
| 323 | + if(m_InputValues->UseRunningAverage) |
| 324 | + { |
| 325 | + c2 = c2 * m_Volumes.getValue(neighborFeature); |
| 326 | + m_AvgCAxes = m_AvgCAxes + c2; |
| 327 | + } |
| 328 | + return true; |
| 329 | + } |
| 330 | + } |
| 331 | + } |
| 332 | + return false; |
| 333 | +} |
0 commit comments