diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..01c041e --- /dev/null +++ b/.clang-format @@ -0,0 +1,89 @@ +--- +Language: Cpp +# AccessModifierOffset: -2 +# AlignAfterOpenBracket: Align +# AlignConsecutiveAssignments: false +# AlignConsecutiveDeclarations: false +# AlignEscapedNewlinesLeft: false +# AlignOperands: true +# AlignTrailingComments: true +# AllowAllParametersOfDeclarationOnNextLine: true +# AllowShortBlocksOnASingleLine: false +# AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +# AllowShortIfStatementsOnASingleLine: false +# AllowShortLoopsOnASingleLine: false +# AlwaysBreakAfterDefinitionReturnType: None +# AlwaysBreakAfterReturnType: None +# AlwaysBreakBeforeMultilineStrings: false +AlwaysBreakTemplateDeclarations: true +# BinPackArguments: true +# BinPackParameters: true +BraceWrapping: + AfterClass: true + AfterControlStatement: true + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: true + AfterUnion: false + BeforeCatch: false + BeforeElse: true + IndentBraces: false +#BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeTernaryOperators: false +BreakConstructorInitializersBeforeComma: true +ColumnLimit: 200 +# CommentPragmas: '^ IWYU pragma:' +# ConstructorInitializerAllOnOneLineOrOnePerLine: false +ConstructorInitializerIndentWidth: 0 +# ContinuationIndentWidth: 4 +# Cpp11BracedListStyle: true +# DerivePointerAlignment: false +# DisableFormat: false +# ExperimentalAutoDetectBinPacking: false +# ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] +# IncludeCategories: +# - Regex: '^"(llvm|llvm-c|clang|clang-c)/' +# Priority: 2 +# - Regex: '^(<|"(gtest|isl|json)/)' +# Priority: 3 +# - Regex: '.*' +# Priority: 1 +# IndentCaseLabels: false +# IndentWidth: 2 +# IndentWrappedFunctionNames: false +# KeepEmptyLinesAtTheStartOfBlocks: true +# MacroBlockBegin: '' +# MacroBlockEnd: '' +# MaxEmptyLinesToKeep: 1 +# NamespaceIndentation: None +# ObjCBlockIndentWidth: 2 +# ObjCSpaceAfterProperty: false +# ObjCSpaceBeforeProtocolList: true +# PenaltyBreakBeforeFirstCallParameter: 19 +# PenaltyBreakComment: 300 +# PenaltyBreakFirstLessLess: 120 +# PenaltyBreakString: 1000 +# PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 100 +PointerAlignment: Left +# ReflowComments: true +SortIncludes: true +# SpaceAfterCStyleCast: false +# SpaceBeforeAssignmentOperators: true +SpaceBeforeParens: Never +# SpaceInEmptyParentheses: false +# SpacesBeforeTrailingComments: 1 +# SpacesInAngles: false +# SpacesInContainerLiterals: true +# SpacesInCStyleCastParentheses: false +# SpacesInParentheses: false +# SpacesInSquareBrackets: false +SpaceAfterTemplateKeyword: true +Standard: c++20 +TabWidth: 2 +UseTab: Never +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..6384629 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,56 @@ +--- +Checks: "-abseil-*, + bugprone-*, + -boost-*, + -cert-*, + clang-diagnostic-*, + -clang-diagnostic-documentation, + clang-analyzer-*, + cppcoreguidelines-*, + -cppcoreguidelines-avoid-magic-numbers, + -darwin-*, + -fuchsia-*, + -google-*, + -hicpp-*, + -linuxkernel-*, + -llvm-*, + misc-*, + modernize-*, + -modernize-use-trailing-return-type, + -modernize-use-nodiscard, + -modernize-use-auto, + -mpi-*, + -objc-*, + -openmp-*, + performance-*, + -portability-*, + readability-*, + -readability-function-cognitive-complexity, + -readability-function-size, + -readability-uppercase-literal-suffix, + -readability-magic-numbers" +WarningsAsErrors: '*' +HeaderFilterRegex: 'simplnx/.*\.hpp' +FormatStyle: file +CheckOptions: + cppcoreguidelines-macro-usage.AllowedRegexp: 'SIMPLNX_EXPORT|SIMPLNX_NO_EXPORT|SIMPLNX_DEPRECATED' + readability-identifier-naming.IgnoreMainLikeFunctions: 'false' + readability-identifier-naming.PrivateMemberPrefix: 'm_' + readability-identifier-naming.NamespaceCase: lower_case + readability-identifier-naming.ClassCase: CamelCase + readability-identifier-naming.ClassMethodCase: camelBack + readability-identifier-naming.PrivateMember: CamelCase + readability-identifier-naming.PublicMemberCase: CamelCase + readability-identifier-naming.StructCase: CamelCase + readability-identifier-naming.FunctionCase: camelBack + readability-identifier-naming.VariableCase: camelBack + readability-identifier-naming.GlobalVariableCase: CamelCase + readability-identifier-naming.GlobalConstantCase: CamelCase + readability-identifier-naming.GlobalConstantPrefix: 'k_' + readability-identifier-naming.GlobalFunctionCase: CamelCase + readability-identifier-naming.LocalPointerCase: camelBack + readability-identifier-naming.LocalPointerSuffix: 'Ptr' + readability-identifier-naming.TypeAliasCase: CamelCase + readability-identifier-naming.TypeAliasSuffix: 'Type' + readability-identifier-naming.MacroDefinitionCase: UPPER_CASE +... diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..fd9cc39 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,99 @@ +name: Bug Report +description: File a bug report +title: "BUG: " +labels: ["bug", "needs triage"] +body: + - type: markdown + attributes: + value: | + We're sorry to hear your having trouble with our application. By filling out the following form in its entirety, we will be able to better diagnose the problem and help you reach a resolution. Thank you in advance for taking the time to fill out the form! + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues, known issues in release notes, and documentation. + required: true + - type: textarea + id: brief-description + attributes: + label: Brief Description of the Issue and Expected Behavior + description: Briefly describe the issue you encountered and what you expected to happen. + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: textarea + id: system-information + attributes: + label: Platform and Version Information + description: | + Please complete the following steps and paste it in the box below: + + 1. Select the `Help` dropdown from the taskbar at the top left side of the application + 2. Select the `About DREAM3D-NX` option near the bottom of the dropdown submenu + 3. Click the `Copy Info` button from the pop-up window to copy it to your clipboard + 4. Paste the information in the box below, using `ctrl-v` on the keyboard or by left-clicking and selecting `paste` + + Example copied information shown in the preview. + placeholder: | + DREAM3D-NX Build Revision: cba61ebbca + DREAM3D-NX Build Date: 2025/07/18 + Operating System: Ubuntu 22.04.5 LTS + Architecture: x86_64 + System Locale: en_US + Installed RAM: 62.5 GB + Built and maintained by BlueQuartz Software, LLC. + validations: + required: true + - type: dropdown + id: error-type + attributes: + label: What section did you encounter the error in? [Further details may be required during triage process] + multiple: true + options: + - GUI client + - NXRunner + - Visualization + - Workflow + - Filter Library (or Search Bar) + - Filter Parameters + validations: + required: false + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps To Reproduce + description: Please include the steps to reproduce the behavior in order to help you as efficiently as possible. + placeholder: | + 1. With this config... + 2. Run '...' + 3. See error... + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This can be found in the output widget of the interface or the console output of PipelineRunner. + render: "Text" + validations: + required: false + - type: textarea + id: further-detail + attributes: + label: Anything else? + description: | + Links? Pipelines? References? Compiler? Hardware? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you understand that your issue may be closed if you do not remain cordial, do not provide further detail if prompted, or do not engage with responses from the developers in a reasonable time. + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/documentation_report.yml b/.github/ISSUE_TEMPLATE/documentation_report.yml new file mode 100644 index 0000000..6f69412 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_report.yml @@ -0,0 +1,73 @@ +name: Documentation Report +description: File a report for additional sections or discrepancies, missing, or comment errors in our documentation. +title: "DOC: " +labels: ["documentation", "needs triage"] +body: + - type: markdown + attributes: + value: | + Thank you for helping to better our documentation! By filling out the following form in its entirety, we will be able to better what needs to be fixed or improved on. Thank you in advance for taking the time to fill out the form! + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues, known issues in release notes, and documentation. + required: true + - type: textarea + id: brief-description + attributes: + label: Brief Description of the Documentation Issue or Improvement + description: Briefly describe the issue you found and what you expected. + placeholder: Tell us what you see! + value: "The discrepancy can be found at ___ and I think it should be ___" + validations: + required: true + - type: dropdown + id: version + attributes: + label: Version + description: What version of our software are you running? [Further details may be required during triage process] + options: + - DREAM3D NX (version 7.0.0+) + - NXRunner built from source - Please provide git hash of commit in description + - other (Please enter in the extended description at the bottom) + validations: + required: true + - type: dropdown + id: error-type + attributes: + label: What section of the documentation did you encounter the discrepancy in? [Further details may be required during triage process] + multiple: true + options: + - Filter Documentation + - Python Bindings Documentation + - Release Notes + - Acknowledgements + - Licensing + - README.md + - CONTRIBUTING.md + - SUPPORT.md + - Other - Please provide additional information in the "Anything Else?" Section + validations: + required: false + - type: textarea + id: further-detail + attributes: + label: Anything else? + description: | + Is this in relation to multilingual support? + Is there sections you think should be added to help other users? + Is there a missing reference? + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you understand that your issue may be closed if you do not remain cordial, do not provide further detail if prompted, or do not engage with responses from the developers in a reasonable time. + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/functionality_report.yml b/.github/ISSUE_TEMPLATE/functionality_report.yml new file mode 100644 index 0000000..5577384 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/functionality_report.yml @@ -0,0 +1,77 @@ +name: Request New Functionality +description: Share your recommendations for new features, filters, and functionality! +title: "ENH: " +labels: ["enhancement", "needs triage"] +body: + - type: markdown + attributes: + value: | + We're excited to hear your visions for our library. By filling out the following form in its entirety, we will be able to get a better understanding of your idea. Thank you in advance for taking the time to fill out the form! + - type: checkboxes + attributes: + label: Is there an existing plan for this? + description: Please search to see if a plan already exists for your suggestion. [If so, feel free to comment your support in that discussion/report] + options: + - label: I have searched the existing discussions, release notes, and documentation. + required: true + - type: textarea + id: brief-description + attributes: + label: Description of the Feature, Filter, or Functionality? + description: Describe your feature in detail or even provide an implementation plan/existing open source library to reference! + value: What if simplnx had... + validations: + required: true + - type: dropdown + id: version + attributes: + label: Version + description: What version of our software are you running? [Further details may be required during triage process] + options: + - 7.x.x+ (DREAM3DNX) + - NXRunner built from source - Please provide git hash of commit in description + - other (Please enter in the extended description at the bottom) + validations: + required: true + - type: dropdown + id: suggestion-type + attributes: + label: What section did you foresee your suggestion falling in? [Further details may be required during triage process] + multiple: true + options: + - Python Bindings + - NXRunner + - Filter Library (or Search Bar) + - Filter Parameters + - Infrastructure + - External Compatibility + validations: + required: false + - type: textarea + id: steps-to-implement + attributes: + label: High Level Steps To Implement + description: Please include the steps to implement the behavior in order to help you as efficiently as possible. + placeholder: | + 1. Based on this paper/library... + 2. Alter... + validations: + required: false + - type: textarea + id: further-detail + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you understand that your issue may be closed if you do not remain cordial, do not provide further detail if prompted, or do not engage with responses from the developers in a reasonable time. + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/performance_report.yml b/.github/ISSUE_TEMPLATE/performance_report.yml new file mode 100644 index 0000000..611c33c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance_report.yml @@ -0,0 +1,96 @@ +name: Performance Report +description: File a performance report +title: "PERF: " +labels: ["performance", "needs triage"] +body: + - type: markdown + attributes: + value: | + We're sorry to hear your having trouble with our library. By filling out the following form in its entirety, we will be able to better diagnose the problem and help you reach a resolution. Thank you in advance for taking the time to fill out the form! + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the performance issue you encountered. + options: + - label: I have searched the existing issues, known issues in release notes, and documentation. + required: true + - type: textarea + id: brief-description + attributes: + label: Brief Description of the Issue and Expected Behavior + description: Briefly describe the issue you encountered and what you expected to happen. + placeholder: Tell us what you see profiling wise. + value: "This profile shows..." + validations: + required: true + - type: textarea + id: system-information + attributes: + label: Platform and Version Information + description: | + Please complete the following steps and paste it in the box below: + + 1. Select the `Help` dropdown from the taskbar at the top left side of the application + 2. Select the `About DREAM3D-NX` option near the bottom of the dropdown submenu + 3. Click the `Copy Info` button from the pop-up window to copy it to your clipboard + 4. Paste the information in the box below, using `ctrl-v` on the keyboard or by left-clicking and selecting `paste` + + Example copied information shown in the preview. + placeholder: | + DREAM3D-NX Build Revision: cba61ebbca + DREAM3D-NX Build Date: 2025/07/18 + Operating System: Ubuntu 22.04.5 LTS + Architecture: x86_64 + System Locale: en_US + Installed RAM: 62.5 GB + Built and maintained by BlueQuartz Software, LLC. + validations: + required: true + - type: dropdown + id: error-type + attributes: + label: What section did you encounter the deprecated performance in? [Further details may be required during triage process] + multiple: true + options: + - GUI client + - NXRunner + - Python Bindings + - Specific filter + validations: + required: false + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps To Reproduce + description: Please include the steps to reproduce the behavior in order to help you as efficiently as possible. + placeholder: | + 1. With this config... + 2. Run '...' + validations: + required: false + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This can be found in the output widget of the interface or the console output of PipelineRunner. + render: "Text" + validations: + required: false + - type: textarea + id: further-detail + attributes: + label: Anything else? + description: | + Pipeline? Data? References? Compiler? Hardware? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you understand that your issue may be closed if you do not remain cordial, do not provide further detail if prompted, or do not engage with responses from the developers in a reasonable time. + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b6a3a67 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,60 @@ + + + + + +## Naming Conventions + +Naming of variables should descriptive where needed. Loop Control Variables can use `i` if warranted. Most of these conventions are enforced through the clang-tidy and clang-format configuration files. See the file `simplnx/docs/Code_Style_Guide.md` for a more in depth explanation. + + +## Filter Checklist + +The help file `simplnx/docs/Porting_Filters.md` has documentation to help you port or write new filters. At the top is a nice checklist of items that should be noted when porting a filter. + + +## Unit Testing + +The idea of unit testing is to test the filter for proper execution and error handling. How many variations on a unit test each filter needs is entirely dependent on what the filter is doing. Generally, the variations can fall into a few categories: + +- [ ] 1 Unit test to test output from the filter against known exemplar set of data +- [ ] 1 Unit test to test invalid input code paths that are specific to a filter. Don't test that a DataPath does not exist since that test is already performed as part of the SelectDataArrayAction. + +## Code Cleanup +- [ ] No commented out code (rare exceptions to this is allowed..) +- [ ] No API changes were made (or the changes have been approved) +- [ ] No major design changes were made (or the changes have been approved) +- [ ] Added test (or behavior not changed) +- [ ] Updated API documentation (or API not changed) +- [ ] Added license to new files (if any) +- [ ] Added example pipelines that use the filter +- [ ] Classes and methods are properly documented + + + diff --git a/.github/workflows/format_pr.yml b/.github/workflows/format_pr.yml new file mode 100644 index 0000000..87966cb --- /dev/null +++ b/.github/workflows/format_pr.yml @@ -0,0 +1,33 @@ +name: clang-format pr + +on: + pull_request: + branches: + - develop + - master + +jobs: + clang_format_pr: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 2 + - name: Add Problem Matcher + uses: ammaraskar/gcc-problem-matcher@a141586609e2a558729b99a8c574c048f7f56204 + - name: Check Formatting + id: check_format + continue-on-error: true + run: | + python3 scripts/clang_format.py --format-version 13 --commits HEAD^ HEAD + - name: Apply Formatting + if: steps.check_format.outcome != 'success' + run: | + python3 scripts/clang_format.py --format-version 13 --modify --commits HEAD^ HEAD + - name: Add Suggestions + if: steps.check_format.outcome != 'success' + uses: reviewdog/action-suggester@v1 + with: + tool_name: clang-format + fail_level: error diff --git a/.github/workflows/format_push.yml b/.github/workflows/format_push.yml new file mode 100644 index 0000000..a4a6497 --- /dev/null +++ b/.github/workflows/format_push.yml @@ -0,0 +1,19 @@ +name: clang-format + +on: + push: + branches: + - develop + - master + +jobs: + clang_format: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v2 + - name: Add Problem Matcher + uses: ammaraskar/gcc-problem-matcher@a141586609e2a558729b99a8c574c048f7f56204 + - name: Check Formatting + run: | + python3 scripts/clang_format.py --format-version 13 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..6494da1 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,79 @@ +name: linux + +on: + pull_request: + branches: + - develop + - master + push: + branches: + - develop + - master + +jobs: + build: + env: + VCPKG_BINARY_SOURCES: 'clear;nuget,GitHub,readwrite' + strategy: + fail-fast: false + matrix: + os: + - ubuntu-22.04 + cxx: + - g++-11 + - clang++-14 + include: + - cxx: g++-11 + cc: gcc-11 + - cxx: clang++-14 + cc: clang-14 + runs-on: ${{matrix.os}} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Checkout simplnx + run: | + git clone -b develop https://www.github.com/bluequartzsoftware/simplnx ${{github.workspace}}/../../simplnx + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install sphinx myst-parser sphinx-markdown-tables sphinx_rtd_theme numpy + - name: Add C++ Problem Matcher + uses: ammaraskar/gcc-problem-matcher@0.2.0 + - name: Install Dependencies - 2 + run: | + sudo apt-get -y install ninja-build + - name: Install Sphinx + run: | + sudo pip3 install sphinx myst-parser sphinx-markdown-tables sphinx_rtd_theme numpy + - name: Setup NuGet Credentials + shell: bash + run: | + mono `vcpkg fetch nuget | tail -n 1` \ + sources add \ + -source "https://nuget.pkg.github.com/BlueQuartzSoftware/index.json" \ + -storepasswordincleartext \ + -name "GitHub" \ + -username "BlueQuartzSoftware" \ + -password "${{secrets.GITHUB_TOKEN}}" + mono `vcpkg fetch nuget | tail -n 1` \ + setapikey "${{secrets.GITHUB_TOKEN}}" \ + -source "https://nuget.pkg.github.com/BlueQuartzSoftware/index.json" + - name: Configure + env: + CC: ${{matrix.cc}} + CXX: ${{matrix.cxx}} + run: | + cmake --preset ci-linux-x64 -DSIMPLNX_EXTRA_PLUGINS="MTRSim" -DSIMPLNX_PLUGIN_ENABLE_MTRSim=ON -DSIMPLNX_MTRSim_SOURCE_DIR=${{github.workspace}} ${{github.workspace}}/../../simplnx + - name: Build + run: | + cmake --build ${{github.workspace}}/../../simplnx/build --config Release + - name: Test + run: | + ctest --output-on-failure --test-dir ${{github.workspace}}/../../simplnx/build --build-config Release diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000..364bbd3 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,81 @@ +name: macos + +on: + pull_request: + branches: + - develop + - master + push: + branches: + - develop + - master + +jobs: + build: + env: + VCPKG_BINARY_SOURCES: 'clear;nuget,GitHub,readwrite' + strategy: + fail-fast: false + matrix: + os: + - macos-14 + - macos-15 + include: + - os: macos-14 + preset: ci-macos-arm64 + - os: macos-15 + preset: ci-macos-arm64 + runs-on: ${{matrix.os}} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Checkout simplnx + run: | + git clone -b develop https://www.github.com/bluequartzsoftware/simplnx ${{github.workspace}}/../../simplnx + - name: Checkout vcpkg + run: | + git clone https://www.github.com/microsoft/vcpkg && cd vcpkg && ./bootstrap-vcpkg.sh + VCPKG_INSTALLATION_ROOT=${{github.workspace}}/vcpkg + echo "$VCPKG_INSTALLATION_ROOT" >> $GITHUB_PATH + echo "VCPKG_INSTALLATION_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" + if: matrix.os == 'macos-14' + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install sphinx myst-parser sphinx-markdown-tables sphinx_rtd_theme numpy + - name: Add C++ Problem Matcher + uses: ammaraskar/gcc-problem-matcher@0.2.0 + - name: Install Dependencies - 2 + run: | + brew install ninja mono + - name: Install Sphinx + run: | + sudo pip3 install sphinx myst-parser sphinx-markdown-tables sphinx_rtd_theme numpy + - name: Setup NuGet Credentials + shell: bash + run: | + mono `vcpkg fetch nuget | tail -n 1` \ + sources add \ + -source "https://nuget.pkg.github.com/BlueQuartzSoftware/index.json" \ + -storepasswordincleartext \ + -name "GitHub" \ + -username "BlueQuartzSoftware" \ + -password "${{secrets.GITHUB_TOKEN}}" + mono `vcpkg fetch nuget | tail -n 1` \ + setapikey "${{secrets.GITHUB_TOKEN}}" \ + -source "https://nuget.pkg.github.com/BlueQuartzSoftware/index.json" + - name: Configure + run: | + cmake --preset ${{matrix.preset}} -DSIMPLNX_EXTRA_PLUGINS="MTRSim" -DSIMPLNX_PLUGIN_ENABLE_MTRSim=ON -DSIMPLNX_MTRSim_SOURCE_DIR=${{github.workspace}} ${{github.workspace}}/../../simplnx + - name: Build + run: | + cmake --build ${{github.workspace}}/../../simplnx/build --config Release + - name: Test + run: | + ctest --output-on-failure --test-dir ${{github.workspace}}/../../simplnx/build --build-config Release diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml new file mode 100644 index 0000000..473f399 --- /dev/null +++ b/.github/workflows/windows.yml @@ -0,0 +1,64 @@ +name: windows + +on: + pull_request: + branches: + - develop + - master + push: + branches: + - develop + - master + +jobs: + build: + env: + VCPKG_BINARY_SOURCES: 'clear;nuget,GitHub,readwrite' + strategy: + fail-fast: false + matrix: + os: + - windows-2022 + toolset: + - v143 + runs-on: ${{matrix.os}} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: true + - name: Checkout simplnx + run: | + git clone -b develop https://www.github.com/bluequartzsoftware/simplnx ${{github.workspace}}/../../simplnx + - name: Add C++ Problem Matcher + uses: ammaraskar/msvc-problem-matcher@0.2.0 + - name: Setup Build Environment + uses: ilammy/msvc-dev-cmd@v1.12.1 + with: + vsversion: 2022 + - name: Setup NuGet Credentials + shell: bash + run: | + `vcpkg fetch nuget | tail -n 1` \ + sources add \ + -source "https://nuget.pkg.github.com/BlueQuartzSoftware/index.json" \ + -storepasswordincleartext \ + -name "GitHub" \ + -username "BlueQuartzSoftware" \ + -password "${{secrets.GITHUB_TOKEN}}" + `vcpkg fetch nuget | tail -n 1` \ + setapikey "${{secrets.GITHUB_TOKEN}}" \ + -source "https://nuget.pkg.github.com/BlueQuartzSoftware/index.json" + - name: Install Sphinx + run: | + pip install sphinx myst-parser sphinx-markdown-tables sphinx_rtd_theme numpy + - name: Configure + run: | + cmake --preset ci-windows-${{matrix.toolset}} -T ${{matrix.toolset}} -DSIMPLNX_EXTRA_PLUGINS="MTRSim" -DSIMPLNX_PLUGIN_ENABLE_MTRSim=ON -DSIMPLNX_MTRSim_SOURCE_DIR=${{github.workspace}} ${{github.workspace}}/../../simplnx + - name: Build + run: | + cmake --build ${{github.workspace}}/../../simplnx/build --config Release + - name: Test + run: | + ctest --output-on-failure --test-dir ${{github.workspace}}/../../simplnx/build --build-config Release + diff --git a/.gitignore b/.gitignore index 42cebde..4d070d9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ CLAUDE.md *.mat /docs/superpowers +/Testing +output/*.dream3d diff --git a/MTRSimPlugin.cmake b/MTRSimPlugin.cmake index 0ce8d0f..75dbad0 100644 --- a/MTRSimPlugin.cmake +++ b/MTRSimPlugin.cmake @@ -31,6 +31,7 @@ set(${PLUGIN_NAME}_SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}) # MTRSim/src/MTRSim/Filters/ directory. set(FilterList ComputeODFFilter + MTRSimFilter ReadMTRSimODFFilter WriteMTRSimODFFilter ) @@ -44,6 +45,7 @@ set(ActionList # ------------------------------------------------------------------------------ set(AlgorithmList ComputeODF + MTRSim ReadMTRSimODF WriteMTRSimODF ) @@ -102,6 +104,7 @@ target_include_directories(simplnx PUBLIC ) # ------------------------------------------------------------------------------ set(PLUGIN_EXTRA_SOURCES ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/AssignmentRule.cpp + ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/ConfigIO.cpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/GPGenerator.cpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/IPFMapper.cpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/MTRDataLoader.cpp @@ -112,11 +115,14 @@ set(PLUGIN_EXTRA_SOURCES ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/PGRFSimulation.cpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/PoleFigure.cpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/QSimVN.cpp + ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/MTRSimDriver.cpp ) set(PLUGIN_EXTRA_HEADERS ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/AssignmentRule.hpp + ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/ConfigIO.hpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/GPGenerator.hpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/IPFMapper.hpp + ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/ISimulationObserver.hpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/MTRDataLoader.hpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/ODFBuilder.hpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/ODFCalculator.hpp @@ -125,6 +131,8 @@ set(PLUGIN_EXTRA_HEADERS ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/PGRFSimulation.hpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/PoleFigure.hpp ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/QSimVN.hpp + ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/MTRSimDriver.hpp + ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/SimulationObservers.hpp ) target_sources(${PLUGIN_NAME} @@ -204,22 +212,14 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) if(NOT EXISTS ${DREAM3D_DATA_DIR}/TestFiles/) file(MAKE_DIRECTORY "${DREAM3D_DATA_DIR}/TestFiles/") endif() - # download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} - # ARCHIVE_NAME T12-MAI-2010.tar.gz - # SHA512 e33f224d19ad774604aa28a3263a00221a3a5909040685a3d14b6cba78e36d174b045223c28b462ab3eaea0fbc1c9f0657b1bd791a947799b9f088b13d777568 - # INSTALL - # ) - # add_custom_target(Copy_${PLUGIN_NAME}_T12-MAI-2010 ALL - # COMMAND ${CMAKE_COMMAND} -E tar xzf "${DREAM3D_DATA_DIR}/TestFiles/T12-MAI-2010.tar.gz" - # COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different "${DREAM3D_DATA_DIR}/TestFiles/T12-MAI-2010" "${DATA_DEST_DIR}/T12-MAI-2010" - # COMMAND ${CMAKE_COMMAND} -E rm -rf "${DREAM3D_DATA_DIR}/TestFiles/T12-MAI-2010" - # WORKING_DIRECTORY "${DREAM3D_DATA_DIR}/TestFiles" - # COMMENT "Copying ${PLUGIN_NAME}/T12-MAI-2010 data into Binary Directory" - # DEPENDS Fetch_Remote_Data_Files # Make sure all remote files are downloaded before trying this - # COMMAND_EXPAND_LISTS - # VERBATIM - # ) - # set_target_properties(Copy_${PLUGIN_NAME}_T12-MAI-2010 PROPERTIES FOLDER Plugins/${PLUGIN_NAME}) + + add_custom_target(Copy_${PLUGIN_NAME}_ODF ALL + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${${PLUGIN_NAME}_SOURCE_DIR}/data/simulation_ODF.h5" "${DATA_DEST_DIR}/MTRSim/simulation_ODF.h5" + WORKING_DIRECTORY "${${PLUGIN_NAME}_SOURCE_DIR}" + COMMENT "Copying ${PLUGIN_NAME}/Data into Binary Directory" + COMMAND_EXPAND_LISTS + VERBATIM + ) endif() @@ -227,18 +227,18 @@ endif() # Create build folder copy rules and install rules for the 'data' folder # for this plugin # ----------------------------------------------------------------------- -add_custom_target(Copy_${PLUGIN_NAME}_Data ALL - COMMAND ${CMAKE_COMMAND} -E copy_directory ${${PLUGIN_NAME}_SOURCE_DIR}/data ${DATA_DEST_DIR}/${PLUGIN_NAME} - COMMENT "Copying ${PLUGIN_NAME} data into Binary Directory" - COMMAND_EXPAND_LISTS - VERBATIM -) -set_target_properties(Copy_${PLUGIN_NAME}_Data PROPERTIES FOLDER Plugins/${PLUGIN_NAME}) +# add_custom_target(Copy_${PLUGIN_NAME}_Data ALL +# COMMAND ${CMAKE_COMMAND} -E copy_directory ${${PLUGIN_NAME}_SOURCE_DIR}/data ${DATA_DEST_DIR}/${PLUGIN_NAME} +# COMMENT "Copying ${PLUGIN_NAME} data into Binary Directory" +# COMMAND_EXPAND_LISTS +# VERBATIM +# ) +# set_target_properties(Copy_${PLUGIN_NAME}_Data PROPERTIES FOLDER Plugins/${PLUGIN_NAME}) option(${PLUGIN_NAME}_INSTALL_DATA_FILES "Enables install of ${PLUGIN_NAME} data files" ON) set(Installed_Data_Files - + "${${PLUGIN_NAME}_SOURCE_DIR}/data/simulation_ODF.h5" ) if(${PLUGIN_NAME}_INSTALL_DATA_FILES) diff --git a/README.txt b/ReadMe.md similarity index 82% rename from README.txt rename to ReadMe.md index 84eca6d..fb6fd37 100644 --- a/README.txt +++ b/ReadMe.md @@ -37,25 +37,27 @@ dx : voxel spacing in x dy : voxel spacing in y dz : voxel spacing in z -###The MTR spatial parameters are set in simulate_MTRs.m +### The MTR spatial parameters are set in simulate_MTRs.m + volume_fractions : volume fraction of each MTR "class" theta_list : correlation lengths of plurigaussian model nugvar : nugget variance (not used) -###The plurigaussian model is used to simulate the underlying MTR assignment -###for each voxel +### The plurigaussian model is used to simulate the underlying MTR assignment for each voxel + +### The plurigaussian parameters are set in PGRF_simulation.m -###The plurigaussian parameters are set in PGRF_simulation.m correlation_function_selected : generally set to 'anisotropic' corr_func_name : correlation model, set to 'exp' or 'gaussian' boundary_conditions : 'periodic' or 'nonperiodic' mean_function_selected : generally set to 'stationary' -###Based on the MTR assignments, for each voxel a random crystallographic -###orientation is drawn from the corresponding MTR-/component-ODF. -###The finite mixture model for the ODF is essentially a collection of -###normalized histograms over the Euler angles, contained in simulation_ODF.mat -###Parameterized families of models are not provided +Based on the MTR assignments, for each voxel a random crystallographic +orientation is drawn from the corresponding MTR-/component-ODF. +The finite mixture model for the ODF is essentially a collection of +normalized histograms over the Euler angles, contained in simulation_ODF.mat + +- Parameterized families of models are not provided ODF_best : typically components are estimated from data diff --git a/configs/smoke_test.json b/configs/smoke_test.json new file mode 100644 index 0000000..5ea237b --- /dev/null +++ b/configs/smoke_test.json @@ -0,0 +1,19 @@ +{ + "_comment": "Tiny smoke-test grid for fast end-to-end execution (Debug builds included).", + "_image_size": "50 x 30 px (1.0 mm / 0.02 mm x 0.6 mm / 0.02 mm) = 1500 voxels", + "_purpose": "Exercises the full MTRSim execute path quickly so debug-only Eigen/assert issues surface.", + "xLen": 1.0, + "yLen": 0.6, + "zLen": 0.0, + "dx": 0.02, + "dy": 0.02, + "dz": 0.02, + "volumeFractions": [0.30, 0.35, 0.35], + "thetaList": [ + [0.10, 0.45, 0.10], + [0.08, 0.37, 0.08] + ], + "nuggetVariance": [0.67, 0.71, 0.72], + "odfInputPath": "data/simulation_ODF.h5", + "seed": 42 +} diff --git a/data/real_world_microtexture_data.d3dpipeline b/data/real_world_microtexture_data.d3dpipeline index c50bc71..5df4198 100644 --- a/data/real_world_microtexture_data.d3dpipeline +++ b/data/real_world_microtexture_data.d3dpipeline @@ -2159,7 +2159,7 @@ "version": 1 }, "export_file_path": { - "value": "/Users/mjackson/Workspace7/DREAM3D_Plugins/MTRSim/data/real_world_microtexture_data.dream3d", + "value": "data/real_world_microtexture_data.dream3d", "version": 1 }, "parameters_version": 2, diff --git a/docs/Images/mtrsim_algorithm.png b/docs/Images/mtrsim_algorithm.png new file mode 100644 index 0000000..699db40 Binary files /dev/null and b/docs/Images/mtrsim_algorithm.png differ diff --git a/docs/Images/mtrsim_odf_euler_space.png b/docs/Images/mtrsim_odf_euler_space.png new file mode 100644 index 0000000..996bf32 Binary files /dev/null and b/docs/Images/mtrsim_odf_euler_space.png differ diff --git a/docs/Images/mtrsim_overview.png b/docs/Images/mtrsim_overview.png new file mode 100644 index 0000000..07973d8 Binary files /dev/null and b/docs/Images/mtrsim_overview.png differ diff --git a/docs/MTRSimFilter.md b/docs/MTRSimFilter.md new file mode 100644 index 0000000..f494063 --- /dev/null +++ b/docs/MTRSimFilter.md @@ -0,0 +1,109 @@ +# Generate Synthetic Microtexture + +## Group (Subgroup) + +MTRSim (Generate) + +## Description + +This **Filter** runs a plurigaussian random field (PGRF) Micro-Texture Region (MTR) simulation to produce a fully synthetic crystallographic microstructure. Each voxel of the output **Image Geometry** is assigned to one of N microtexture components via correlated latent Gaussian fields and a winner-takes-all assignment rule; a crystallographic orientation is then sampled from that component's Orientation Distribution Function (ODF). The result is a synthetic microstructure with per-voxel MTR ids and Bunge Euler angles (and optional polar coloring), suitable for downstream texture analysis or as a training dataset. + +![MTRSim inputs and the synthetic microstructure it generates](Images/mtrsim_overview.png) + +*From per-component volume fractions, spatial correlation lengths (θ), and per-component ODFs (left), MTRSim generates a spatially-correlated, IPF-colored polycrystal cross-section (right). HCP α-titanium is shown.* + +The algorithm reproduces the behavior of the MATLAB MTRSim research code (`matlab/simulate_MTRs.m`). Input ODFs are typically prepared by the **Read MTRSim ODF (HDF5)** or **Compute ODF From Euler Angles** filters; the bin layout and axis conventions defined by those filters are used directly by this filter. + +![An orientation distribution function over Bunge Euler space](Images/mtrsim_odf_euler_space.png) + +*An Orientation Distribution Function (ODF) over Bunge Euler space. One ODF per MTR component defines the crystallographic texture from which each voxel's orientation is drawn.* + +### Configuration Source + +By default, the simulation parameters (Volume Fraction, Theta List, Physical Size, Physical Spacing, and the seed group) are entered manually in the filter UI. + +Enabling **Load Simulation Parameters from Config File** hides those manual fields and instead reads all simulation parameters from a JSON file that follows the same schema used by the standalone MTRSim tool (see `configs/*.json` in the MTRSim repository). This is useful when you already have a validated config file from a prior MATLAB or standalone MTRSim run. + +When config-file mode is active: + +- The **MTRSim Config File (JSON)** path parameter becomes visible; the manual Simulation Parameters and seed fields are hidden. +- The `odfInputPath` and `nuggetVariance` keys in the JSON are ignored; the ODF always comes from the **Input ODF Geometry** / **ODF Component Arrays** selection in the UI. +- All output array names and paths (output geometry, cell attribute matrix, array names) always come from the UI regardless of mode. +- If the config contains no `seed` key, or `seed: 0`, a time-based random seed is generated automatically — the same behavior as leaving **Use Seed for Random Generation** off in manual mode. +- The **Units Note** below still applies: `xLen`/`yLen`/`zLen`/`dx`/`dy`/`dz` and the theta values in the JSON must all be in a consistent length unit. + +### Algorithm Overview + +![How MTRSim works](Images/mtrsim_algorithm.png) + +*The MTRSim pipeline: correlated latent Gaussian fields partition the volume into MTR components, then a crystallographic orientation is sampled from each component's ODF.* + +The simulation proceeds in the following steps: + +1. The ODF grid geometry (bin spacing in degrees) is read from the **Input ODF Geometry**. The component data arrays are assembled in the order specified by **ODF Component Arrays** — this order is critical and must match the Volume Fraction columns. +2. For each pair of adjacent components, a latent Gaussian random field with spatial correlation lengths (`theta_x`, `theta_y`, `theta_z`) drawn from the corresponding row of the **Theta List** is generated over the output domain. +3. A winner-takes-all rule applied to the (N−1) latent fields partitions every voxel into one of the N MTR components. The expected volume fraction of each component is controlled by the **Volume Fraction** parameter. +4. Each voxel draws a Bunge Euler triple (`phi1`, `PHI`, `phi2`) by inverse-CDF sampling from its assigned component's ODF. +5. If **Generate Polar Coloring** is enabled, the Euler angles are mapped to an RGB color using the HCP polar (MATLAB Sparkman) color scheme, looking along the Z reference direction. + +### Units Note (Important) + +**Physical Size**, **Physical Spacing**, and the **Theta List** correlation lengths must all use the **same length unit**. Only the dimensionless ratio (lag / theta) enters the Gaussian covariance calculation, so mixing units (e.g., microns for size but millimeters for theta) will produce physically incorrect correlation lengths. The original MATLAB MTRSim defaults used millimeter-scale values; if you import those defaults directly into this filter with micron-scale size/spacing, scale the theta values by 1000 to match. + +### Preflight Preview + +The **Filter** reports the following derived values before the user clicks Apply: + +- **Output Grid (X, Y, Z):** the voxel dimensions of the output geometry, computed as `round(Size / Spacing)` on each axis (Z is forced to 1 when Physical Size Z ≤ 0). +- **Number of ODF Components:** the number of paths selected in **ODF Component Arrays**. + +### Performance + +The simulation runs in the execute phase. For large output grids (e.g., the default ~1900 × 635 grid) this may take a substantial amount of time. Progress is reported continuously throughout the simulation — covering plurigaussian field generation and per-component orientation sampling — so the progress bar advances incrementally rather than jumping from 0 % to 100 %. The filter also checks for cancellation continuously; if cancelled mid-run, the filter returns without populating the output arrays with simulation results (the arrays exist but remain unfilled). + +## Outputs + +A new **Image Geometry** (default path `MTR Microstructure`) is created with a cell **Attribute Matrix** (default `Cell Data`) containing the following arrays: + +| Array | Type | Components | Contents | +|---|---|---|---| +| MTRIds | Int32 | 1 | Per-voxel MTR component id, 1-based (0 is reserved, consistent with the FeatureIds convention). | +| Eulers | Float32 | 3 | Bunge Euler angles `(phi1, PHI, phi2)` in **radians**. | +| Polar Colors | UInt8 | 3 (RGB) | HCP polar coloring; present only when **Generate Polar Coloring** is enabled. | + +A scalar `UInt64` array (default name `MTRSim SeedValue`) is created at the top level of the **DataStructure** to record the seed actually used during execution, enabling exact replay of the simulation. + +## Downstream Tips + +- Follow this filter with **Compute IPF Colors** (using the Eulers output) for a standard inverse-pole-figure visualization. +- Use **Write Image** to export slice images from the output geometry. +- The MTRIds array is compatible with downstream feature-level statistics filters that consume a `FeatureIds`-style Int32 array. + +## Errors + +| Code | Meaning | +| --- | --- | +| `-13501` | (Preflight) Fewer than 2 ODF component arrays were selected. MTRSim requires at least 2 components. | +| `-13502` | (Preflight) The **Volume Fraction** table must have exactly 1 row and exactly one column per ODF component. | +| `-13507` | (Preflight) One or more Volume Fraction values is outside the range [0, 1]. | +| `-13503` | (Preflight) Volume Fraction values do not sum to 1.0 (tolerance: 1 × 10⁻³). | +| `-13504` | (Preflight) The **Theta List** has fewer than (components − 1) rows. Each pair of adjacent components requires one latent Gaussian field with its own correlation lengths. | +| `-13505` | (Preflight) A row in the **Theta List** does not have exactly 3 columns (`theta_x`, `theta_y`, `theta_z`). | +| `-13506` | (Preflight) Physical Spacing X or Y is ≤ 0. Both must be strictly positive; the Z spacing is unused when Physical Size Z ≤ 0. | +| `-13520` | (Preflight) The MTRSim config file is missing or invalid (cannot be opened / not valid JSON). Only reported when **Load Simulation Parameters from Config File** is ON. | +| `-13521` | (Execute) The MTRSim config file could not be read at execute time (missing or invalid). Only reported when **Load Simulation Parameters from Config File** is ON. | +| `-13550` | (Execute) The core MTR simulation threw an unexpected exception. The error message includes the underlying cause. | + +% Auto generated parameter table will be inserted here + +## Example Pipelines + +Example pipelines can be found in the Prebuilt Pipelines directory under the MTRSim directory. There are many examples covering a wide range of generated microtextures. + +## License & Copyright + +Please see the description file distributed with this plugin. + +## DREAM3D Mailing Lists + +If you need help, need to file a bug report or want to request a new feature, please head over to the [DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues/discussions) GitHub site where the community of DREAM3D-NX users can help answer your questions. diff --git a/docs/superpowers/plans/2026-06-01-milestone-ak.md b/docs/superpowers/plans/2026-06-01-milestone-ak.md new file mode 100644 index 0000000..9cc29cf --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-milestone-ak.md @@ -0,0 +1,1376 @@ +# Milestone AK — `MTRSimFilter` Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver the `MTRSimFilter` ("Generate Synthetic Microtexture") DREAM3D-NX filter that runs the full MTR plurigaussian simulation from a pipeline, writing MTR ids, Euler angles, and optional polar-color cell data into a new Image Geometry, backed by deterministic + statistical unit tests and green CI. + +**Architecture:** The heavy lifting is consolidated into a single reusable LibMTRSim entry point `simulateMTR()` that runs PGRF → orientation sampling → per-voxel assignment and returns results in **SIMPLNX z,y,x voxel order**. LibMTRSim Catch2 tests (in `tests/`) cover the numerics (deterministic helpers + a statistical end-to-end test reusing the ODF exemplar). The filter is a thin SIMPLNX wrapper (`MTRSimFilter` + `MTRSim` algorithm) whose tests (in `test/`) only verify the filter's value-add: parameter→params mapping, ODF-geometry→component reconstruction, array creation/types/names/1-based ids, seed handling, preflight validation, and the optional color array. + +**Tech Stack:** C++17, Eigen, simplnx filter framework, Catch2 v2 (this repo uses the **bare `Approx`** matcher — `Approx` does NOT compile here), CMake, GitHub Actions. + +> **Correction applied during execution:** all Catch2 tests in this plan use +> bare `Approx(...)`, not `Approx(...)`. The standalone LibMTRSim source +> list is in `src/LibMTRSim/CMakeLists.txt` (not `LibMTRSim.cmake`). + +--- + +## Background the engineer needs + +**Voxel orderings (the highest-risk detail):** +- `GPGenerator::generate` and therefore `PGRFResult::mtrIndex` are ordered + `kSim = iz·(nx·ny) + ix·ny + iy` (z slowest, **y fastest** — MATLAB + column-major for an `ny×nx×nz` array). See `src/LibMTRSim/GPGenerator.cpp:53-117`. +- SIMPLNX Image Geometry cell arrays must be ordered + `kNx = iz·(ny·nx) + iy·nx + ix` (z slowest, **x fastest**). +- The remap from sim order to SIMPLNX order is therefore: + `out[ iz·(ny·nx) + iy·nx + ix ] = in[ iz·(nx·ny) + ix·ny + iy ]`. + +**Grid dimensions:** `nx = round(xLen/dx)`, `ny = round(yLen/dy)`, +`nz = max(round(zLen/dz), 1)`. For the filter: `xLen=Size[0]`, `yLen=Size[1]`, +`zLen=Size[2]`; `dx=Spacing[0]` etc. + +**Existing patterns to mirror exactly:** +- Filter boilerplate: `src/MTRSim/Filters/ReadMTRSimODFFilter.{hpp,cpp}`. +- Algorithm boilerplate: `src/MTRSim/Filters/Algorithms/ReadMTRSimODF.{hpp,cpp}`. +- Random-seed parameter pattern: `simplnx` `MergeTwinsFilter.cpp` (lines 60-63, + 103, 184-191). +- ImageGeom + per-array preflight actions: `ReadMTRSimODFFilter::preflightImpl`. + +**Reference ODF reconstruction:** the AJ read path writes ODFval row-major in +ZYX (`ReadMTRSimODFFilter.cpp:124-128`). The filter's ODF→component helper is the +inverse: flat values + grid dims + degree-spacing → `mtrsim::ODFComponent` with +bin centres in radians. + +**Build/test commands** (dual build per the simplnx convention; the local +simplnx checkout is at `/Users/mjackson/Workspace7/simplnx`): +- LibMTRSim Catch2 tests build via the standalone preset and run with `ctest`. + Standalone configure/build: `cmake --preset ` then + `cmake --build `; run `ctest --test-dir -R MTRSim` (see + `CMakePresets.json` / `CMakeUserPresets.json`). +- Plugin + filter tests build inside the simplnx tree: + `cmake --build /Users/mjackson/Workspace7/simplnx/build` then + `ctest --test-dir /Users/mjackson/Workspace7/simplnx/build -R MTRSim --output-on-failure`. + +> Before each "run the test" step, prefer the standalone build for Task 1-4 +> (fast, no SIMPLNX) and the simplnx build for Task 5-11. + +--- + +## File Structure + +**Create:** +- `src/LibMTRSim/MTRSimDriver.hpp` — `MTRSimResult`, `simulateMTR()`, + `buildUniformODF()`, `gridToODFComponent()`, `remapSimToZYX()` declarations. +- `src/LibMTRSim/MTRSimDriver.cpp` — implementations (logic lifted from + `src/app/main.cpp`). +- `src/MTRSim/Filters/MTRSimFilter.{hpp,cpp}` — the filter. +- `src/MTRSim/Filters/Algorithms/MTRSim.{hpp,cpp}` — the algorithm. +- `tests/test_mtrsim_driver.cpp` — Catch2 tests for the driver + helpers. +- `test/MTRSimTest.cpp` — SIMPLNX filter tests. +- `docs/MTRSim/MTRSimFilter.md` — filter documentation stub. + +**Modify:** +- `src/app/main.cpp` — call `simulateMTR()` instead of inline orchestration. +- `MTRSimPlugin.cmake` — add `MTRSimDriver.{hpp,cpp}` to LibMTRSim sources; + add `MTRSimFilter`/`MTRSim` to `FilterList`/`AlgorithmList`. +- `src/LibMTRSim/CMakeLists.txt` — add `MTRSimDriver.{hpp,cpp}` to the standalone library source/header lists. +- `tests/CMakeLists.txt` — add `test_mtrsim_driver.cpp`. +- `test/CMakeLists.txt` — add `MTRSimTest.cpp`. + +> **No git worktree** for this repo (DREAM3DNX build expects MTRSim at a fixed +> path). Work directly on branch `topic/create_mtr_sim_filter`. + +--- + +## Phase A — LibMTRSim building blocks (fast, deterministic, no SIMPLNX) + +### Task 1: `buildUniformODF(n1, nPHI, n2)` in the driver header + +**Files:** +- Create: `src/LibMTRSim/MTRSimDriver.hpp` +- Create: `src/LibMTRSim/MTRSimDriver.cpp` +- Modify: `LibMTRSim.cmake`, `MTRSimPlugin.cmake` +- Create: `tests/test_mtrsim_driver.cpp` +- Modify: `tests/CMakeLists.txt` + +- [ ] **Step 1: Create the driver header skeleton with `buildUniformODF`** + +Create `src/LibMTRSim/MTRSimDriver.hpp`: + +```cpp +#pragma once + +#include "libmtrsim_export.h" + +#include "ODFSampler.hpp" // mtrsim::ODFComponent, mtrsim::EulerAngles +#include "SimulationParams.hpp" + +#include +#include +#include +#include + +namespace mtrsim { + +/** + * @brief Build a uniform reference ODF on an (n1 x nPHI x n2) Euler grid. + * + * The flat bin-centre arrays match exactly what ODFCalculator::compute and the + * ODFSampler expect, with ix = i1*(nPHI*n2) + iPHI*n2 + i2: + * phi1Bins[ix] = (i1 + 0.5) * 2*pi / n1 + * phiBins[ix] = (iPHI + 0.5) * pi / nPHI + * phi2Bins[ix] = (i2 + 0.5) * 2*pi / n2 + */ +LIBMTRSIM_EXPORT ODFComponent buildUniformODF(int n1, int nPHI, int n2); + +} // namespace mtrsim +``` + +Create `src/LibMTRSim/MTRSimDriver.cpp` (move the body from +`src/app/main.cpp:118-148`, parameterized by dims): + +```cpp +#include "MTRSimDriver.hpp" + +#include + +namespace mtrsim { + +ODFComponent buildUniformODF(int n1, int nPHI, int n2) { + const int nTotal = n1 * nPHI * n2; + const double twoPiOverN1 = 2.0 * std::numbers::pi / static_cast(n1); + const double piOverNPHI = std::numbers::pi / static_cast(nPHI); + const double twoPiOverN2 = 2.0 * std::numbers::pi / static_cast(n2); + + Eigen::VectorXd phi1Bins(nTotal); + Eigen::VectorXd phiBins(nTotal); + Eigen::VectorXd phi2Bins(nTotal); + + for (int ix = 0; ix < nTotal; ++ix) { + const int i1 = ix / (nPHI * n2); + const int iPHI = (ix % (nPHI * n2)) / n2; + const int i2 = ix % n2; + phi1Bins[ix] = (i1 + 0.5) * twoPiOverN1; + phiBins[ix] = (iPHI + 0.5) * piOverNPHI; + phi2Bins[ix] = (i2 + 0.5) * twoPiOverN2; + } + + ODFComponent uni; + uni.odfVal = Eigen::VectorXd::Constant(nTotal, 1.0 / static_cast(nTotal)); + uni.phi1Bins = std::move(phi1Bins); + uni.phiBins = std::move(phiBins); + uni.phi2Bins = std::move(phi2Bins); + return uni; +} + +} // namespace mtrsim +``` + +- [ ] **Step 2: Register the new files in both CMake lists** + +In `LibMTRSim.cmake`, add `MTRSimDriver.cpp`/`.hpp` to the standalone library's +source/header lists (mirror the existing `ODFSampler.cpp` entries). + +In `MTRSimPlugin.cmake`, after line 112 add: +```cmake + ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/MTRSimDriver.cpp +``` +and after line 126 add: +```cmake + ${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/MTRSimDriver.hpp +``` + +- [ ] **Step 3: Write the failing test** + +Create `tests/test_mtrsim_driver.cpp`: + +```cpp +#include "LibMTRSim/MTRSimDriver.hpp" + +#include + +#include + +TEST_CASE("buildUniformODF produces correct bin centres", "[mtrsim_driver]") { + const mtrsim::ODFComponent uni = mtrsim::buildUniformODF(72, 36, 72); + + REQUIRE(uni.odfVal.size() == 72 * 36 * 72); + REQUIRE(uni.phi1Bins.size() == 72 * 36 * 72); + + // Uniform mass: every bin equal, sums to 1. + REQUIRE(uni.odfVal.sum() == Approx(1.0)); + REQUIRE(uni.odfVal[0] == Approx(1.0 / (72.0 * 36.0 * 72.0))); + + // First bin centre: i1=iPHI=i2=0 -> all 0.5 * step. + REQUIRE(uni.phi1Bins[0] == Approx(0.5 * 2.0 * std::numbers::pi / 72.0)); + REQUIRE(uni.phiBins[0] == Approx(0.5 * std::numbers::pi / 36.0)); + REQUIRE(uni.phi2Bins[0] == Approx(0.5 * 2.0 * std::numbers::pi / 72.0)); +} +``` + +Add `test_mtrsim_driver.cpp` to the test sources in `tests/CMakeLists.txt` +(mirror how `test_odf_sampler.cpp` is listed). + +- [ ] **Step 4: Run to verify it fails** + +Run: `ctest --test-dir -R mtrsim_driver --output-on-failure` +Expected: build succeeds, test FAILS only if logic is wrong — if it passes +immediately, good (this helper is straightforward). If the target does not yet +exist, configure first: `cmake --preset `. + +- [ ] **Step 5: Run to verify it passes** + +Run: `ctest --test-dir -R mtrsim_driver --output-on-failure` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/LibMTRSim/MTRSimDriver.hpp src/LibMTRSim/MTRSimDriver.cpp \ + LibMTRSim.cmake MTRSimPlugin.cmake \ + tests/test_mtrsim_driver.cpp tests/CMakeLists.txt +git commit -m "feat(lib): add buildUniformODF grid helper to MTRSimDriver" +``` + +--- + +### Task 2: `gridToODFComponent()` — reconstruct an ODF component from grid arrays + +**Files:** +- Modify: `src/LibMTRSim/MTRSimDriver.hpp`, `src/LibMTRSim/MTRSimDriver.cpp` +- Modify: `tests/test_mtrsim_driver.cpp` + +- [ ] **Step 1: Declare the helper in the header** + +Add to `MTRSimDriver.hpp` inside `namespace mtrsim`: + +```cpp +/** + * @brief Reconstruct an ODF component from flat grid data + degree spacing. + * + * @param values Flat ODFval, length n1*nPHI*n2, row-major with + * ix = i1*(nPHI*n2) + iPHI*n2 + i2 (phi1 slowest, phi2 fastest). + * @param n1,nPHI,n2 Bin counts along phi1, PHI, phi2. + * @param stepDeg1,stepDegPHI,stepDeg2 Bin sizes [degrees] (geometry spacing). + * @return ODFComponent with bin centres [rad] and values normalized to sum 1. + */ +LIBMTRSIM_EXPORT ODFComponent gridToODFComponent(const std::vector& values, int n1, int nPHI, int n2, double stepDeg1, double stepDegPHI, double stepDeg2); +``` + +- [ ] **Step 2: Write the failing test** + +Add to `tests/test_mtrsim_driver.cpp`: + +```cpp +TEST_CASE("gridToODFComponent derives bin centres in radians and normalizes", "[mtrsim_driver]") { + const int n1 = 72, nPHI = 36, n2 = 72; + std::vector values(n1 * nPHI * n2, 2.0); // unnormalized constant + + const mtrsim::ODFComponent c = + mtrsim::gridToODFComponent(values, n1, nPHI, n2, 5.0, 5.0, 5.0); + + REQUIRE(c.odfVal.size() == n1 * nPHI * n2); + REQUIRE(c.odfVal.sum() == Approx(1.0)); // normalized + // 5 deg step -> first bin centre 2.5 deg in radians. + const double deg2rad = std::numbers::pi / 180.0; + REQUIRE(c.phi1Bins[0] == Approx(2.5 * deg2rad)); + REQUIRE(c.phiBins[0] == Approx(2.5 * deg2rad)); + REQUIRE(c.phi2Bins[0] == Approx(2.5 * deg2rad)); +} +``` + +- [ ] **Step 3: Run to verify it fails** + +Run: `ctest --test-dir -R mtrsim_driver --output-on-failure` +Expected: FAIL to compile/link ("undefined reference to gridToODFComponent"). + +- [ ] **Step 4: Implement** + +Add to `MTRSimDriver.cpp` (and `#include ` is already present): + +```cpp +ODFComponent gridToODFComponent(const std::vector& values, int n1, int nPHI, int n2, double stepDeg1, double stepDegPHI, double stepDeg2) { + const int nTotal = n1 * nPHI * n2; + const double deg2rad = std::numbers::pi / 180.0; + const double s1 = stepDeg1 * deg2rad; + const double sP = stepDegPHI * deg2rad; + const double s2 = stepDeg2 * deg2rad; + + Eigen::VectorXd phi1Bins(nTotal); + Eigen::VectorXd phiBins(nTotal); + Eigen::VectorXd phi2Bins(nTotal); + for (int ix = 0; ix < nTotal; ++ix) { + const int i1 = ix / (nPHI * n2); + const int iPHI = (ix % (nPHI * n2)) / n2; + const int i2 = ix % n2; + phi1Bins[ix] = (i1 + 0.5) * s1; + phiBins[ix] = (iPHI + 0.5) * sP; + phi2Bins[ix] = (i2 + 0.5) * s2; + } + + ODFComponent c; + c.odfVal = Eigen::Map(values.data(), nTotal); + const double total = c.odfVal.sum(); + if (total > 0.0) { + c.odfVal /= total; + } + c.phi1Bins = std::move(phi1Bins); + c.phiBins = std::move(phiBins); + c.phi2Bins = std::move(phi2Bins); + return c; +} +``` + +- [ ] **Step 5: Run to verify it passes** + +Run: `ctest --test-dir -R mtrsim_driver --output-on-failure` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/LibMTRSim/MTRSimDriver.hpp src/LibMTRSim/MTRSimDriver.cpp tests/test_mtrsim_driver.cpp +git commit -m "feat(lib): add gridToODFComponent reconstruction helper" +``` + +--- + +### Task 3: `remapSimToZYX()` — convert sim voxel order to SIMPLNX order + +**Files:** +- Modify: `src/LibMTRSim/MTRSimDriver.hpp`, `src/LibMTRSim/MTRSimDriver.cpp` +- Modify: `tests/test_mtrsim_driver.cpp` + +- [ ] **Step 1: Declare the helper** + +Add to `MTRSimDriver.hpp`: + +```cpp +/** + * @brief Remap a per-voxel vector from simulation order to SIMPLNX z,y,x order. + * + * Simulation order: kSim = iz*(nx*ny) + ix*ny + iy (y fastest). + * SIMPLNX order: kNx = iz*(ny*nx) + iy*nx + ix (x fastest). + * out[kNx] = in[kSim]. + * + * @tparam T element type (int or double). + */ +template +std::vector remapSimToZYX(const std::vector& in, int nx, int ny, int nz) { + std::vector out(in.size()); + for (int iz = 0; iz < nz; ++iz) { + for (int iy = 0; iy < ny; ++iy) { + for (int ix = 0; ix < nx; ++ix) { + const std::size_t kSim = static_cast(iz) * nx * ny + static_cast(ix) * ny + iy; + const std::size_t kNx = static_cast(iz) * ny * nx + static_cast(iy) * nx + ix; + out[kNx] = in[kSim]; + } + } + } + return out; +} +``` + +> Template lives in the header (no .cpp entry needed). Add `#include ` +> and `#include ` if not already present. + +- [ ] **Step 2: Write the failing test** + +Add to `tests/test_mtrsim_driver.cpp`: + +```cpp +TEST_CASE("remapSimToZYX moves y-fastest data to x-fastest layout", "[mtrsim_driver]") { + // 2x3x2 grid (nx=2, ny=3, nz=2). Fill sim-order vector with its own index. + const int nx = 2, ny = 3, nz = 2; + std::vector in(nx * ny * nz); + for (int i = 0; i < nx * ny * nz; ++i) { in[i] = i; } + + const std::vector out = mtrsim::remapSimToZYX(in, nx, ny, nz); + + // Spot-check: SIMPLNX (ix=1, iy=0, iz=0) -> kNx = 1. + // source sim index = iz*(nx*ny) + ix*ny + iy = 0 + 1*3 + 0 = 3. + REQUIRE(out[1] == 3); + // SIMPLNX (ix=0, iy=1, iz=0) -> kNx = 2; sim = 0 + 0 + 1 = 1. + REQUIRE(out[2] == 1); + // Same total size, same multiset of values. + REQUIRE(out.size() == in.size()); +} +``` + +- [ ] **Step 3: Run to verify it fails, then passes** + +Run: `ctest --test-dir -R mtrsim_driver --output-on-failure` +Expected: FAIL before adding the template (compile error), PASS after. + +- [ ] **Step 4: Commit** + +```bash +git add src/LibMTRSim/MTRSimDriver.hpp tests/test_mtrsim_driver.cpp +git commit -m "feat(lib): add remapSimToZYX voxel-order helper" +``` + +--- + +### Task 4: `simulateMTR()` end-to-end driver (returns SIMPLNX-ordered results) + +**Files:** +- Modify: `src/LibMTRSim/MTRSimDriver.hpp`, `src/LibMTRSim/MTRSimDriver.cpp` +- Modify: `src/app/main.cpp` +- Modify: `tests/test_mtrsim_driver.cpp` + +- [ ] **Step 1: Declare `MTRSimResult` and `simulateMTR` in the header** + +Add to `MTRSimDriver.hpp`: + +```cpp +/** + * @brief Per-voxel simulation output in SIMPLNX z,y,x order. + */ +struct LIBMTRSIM_EXPORT MTRSimResult { + int nx = 0; + int ny = 0; + int nz = 0; + std::vector mtrIndex; ///< 1-based component id per voxel, length N + std::vector phi1; ///< Euler phi1 [rad] per voxel, length N + std::vector phi; ///< Euler PHI [rad] per voxel, length N + std::vector phi2; ///< Euler phi2 [rad] per voxel, length N +}; + +/** + * @brief Run the full MTR simulation: PGRF assignment -> per-component ODF + * sampling -> per-voxel orientation assignment, returned in SIMPLNX + * z,y,x voxel order. + * + * @param params Fully populated SimulationParams (sizes/spacing in a + * single consistent length unit; volumeFractions define + * numComponents; thetaList has >= numComponents-1 rows). + * @param odfComponents One ODFComponent per volume-fraction entry, on a + * shared (n1 x nPHI x n2) grid. + * @param rng Seeded RNG (mt19937_64). + * @param n1,nPHI,n2 Bin counts of the ODF grid (for the uniform reference). + */ +LIBMTRSIM_EXPORT MTRSimResult simulateMTR(const SimulationParams& params, const std::vector& odfComponents, std::mt19937_64& rng, int n1, int nPHI, int n2); +``` + +- [ ] **Step 2: Implement `simulateMTR` (logic lifted from `main.cpp:231-319`)** + +Add to `MTRSimDriver.cpp` the includes and function: + +```cpp +#include "PGRFSimulation.hpp" + +#include +#include +#include +``` + +```cpp +MTRSimResult simulateMTR(const SimulationParams& params, const std::vector& odfComponents, std::mt19937_64& rng, int n1, int nPHI, int n2) { + const int nx = static_cast(std::round(params.xLen / params.dx)); + const int ny = static_cast(std::round(params.yLen / params.dy)); + const int nz = std::max(static_cast(std::round(params.zLen / params.dz)), 1); + const int N = nx * ny * nz; + + if (static_cast(odfComponents.size()) != static_cast(params.volumeFractions.size())) { + throw std::invalid_argument("simulateMTR: odfComponents count must equal volumeFractions count"); + } + + // 1. PGRF assignment (sim-ordered, 1-based component ids). + PGRFSimulation pgrf{rng}; + const PGRFResult pgrf_result = pgrf.run(params); // throws on bad dims + + // 2. Sample N orientations per component against the uniform reference. + const ODFComponent uniformOdf = buildUniformODF(n1, nPHI, n2); + const int numComponents = static_cast(odfComponents.size()); + std::vector orientSamples(static_cast(numComponents)); + ODFSampler sampler{rng}; + for (int j = 0; j < numComponents; ++j) { + orientSamples[static_cast(j)] = sampler.sampleN(N, odfComponents[static_cast(j)], uniformOdf); + } + + // 3. Assign per-voxel orientation by component (sim order). + std::vector mtrSim(N); + std::vector phi1Sim(N), phiSim(N), phi2Sim(N); + for (int i = 0; i < N; ++i) { + const int comp = pgrf_result.mtrIndex[i] - 1; + mtrSim[i] = pgrf_result.mtrIndex[i]; + phi1Sim[i] = orientSamples[static_cast(comp)](i, 0); + phiSim[i] = orientSamples[static_cast(comp)](i, 1); + phi2Sim[i] = orientSamples[static_cast(comp)](i, 2); + } + + // 4. Remap to SIMPLNX z,y,x order. + MTRSimResult out; + out.nx = nx; out.ny = ny; out.nz = nz; + out.mtrIndex = remapSimToZYX(mtrSim, nx, ny, nz); + out.phi1 = remapSimToZYX(phi1Sim, nx, ny, nz); + out.phi = remapSimToZYX(phiSim, nx, ny, nz); + out.phi2 = remapSimToZYX(phi2Sim, nx, ny, nz); + return out; +} +``` + +- [ ] **Step 3: Refactor `src/app/main.cpp` to call `simulateMTR`** + +Replace the inline blocks `main.cpp:263-319` (PGRF run, ODF load already above, +sampling, per-voxel assign) with a single call. Keep the existing +`loadODFComponents()` and the CSV/PNG output. After loading `odfComponents`, +replace the sampling/assignment with: + +```cpp + mtrsim::MTRSimResult sim = mtrsim::simulateMTR(params, odfComponents, rng, 72, 36, 72); + // main.cpp writes its CSV/PNG in MATLAB (z,x,y) order historically; for the + // standalone tool, rebuild phi vectors from sim (now z,y,x) — acceptable, the + // CSV is diagnostic only. Map sim.phi1/phi/phi2 into the existing Eigen + // VectorXd phi1Vec/phiVec/phi2Vec used by the writers. + Eigen::VectorXd phi1Vec = Eigen::Map(sim.phi1.data(), sim.phi1.size()); + Eigen::VectorXd phiVec = Eigen::Map(sim.phi.data(), sim.phi.size()); + Eigen::VectorXd phi2Vec = Eigen::Map(sim.phi2.data(), sim.phi2.size()); +``` + +Remove the now-unused inline `buildUniformODF` from `main.cpp` (it lives in +`MTRSimDriver` now); keep `loadODFComponents`. Update `result.mtrIndex[i]` CSV +references to `sim.mtrIndex[i]`. + +> The standalone CSV/PNG are diagnostic; their exact voxel order does not affect +> the filter. The LibMTRSim statistical tests (Step 4) are the source of truth. + +- [ ] **Step 4: Write the statistical end-to-end test** + +Add to `tests/test_mtrsim_driver.cpp`. Reuse the committed ODF exemplar at +`data/simulation_ODF.h5` via the existing `mtrsim::readODFComponents` (from +`ODFFileIO.hpp`) OR build synthetic components if the test harness lacks a path; +use the project test-data macro already used by `test_odf_sampler.cpp` to locate +`data/`. + +```cpp +#include "LibMTRSim/ODFFileIO.hpp" // mtrsim::readODFComponents (if used) + +TEST_CASE("simulateMTR reproduces target volume fractions (statistical)", "[mtrsim_driver][statistical]") { + mtrsim::SimulationParams params; + params.xLen = 2.0; params.yLen = 2.0; params.zLen = 0.0; + params.dx = 0.02; params.dy = 0.02; params.dz = 0.02; + params.volumeFractions = {0.30, 0.35, 0.35}; + params.thetaList = {{0.10, 0.45, 0.10}, {0.08, 0.37, 0.08}}; + params.seed = 42; + + // Three identical uniform components (sampling correctness is covered by + // test_odf_sampler.cpp; here we validate assignment fractions + ordering). + std::vector comps = { + mtrsim::buildUniformODF(72, 36, 72), + mtrsim::buildUniformODF(72, 36, 72), + mtrsim::buildUniformODF(72, 36, 72)}; + + std::mt19937_64 rng(params.seed); + const mtrsim::MTRSimResult r = mtrsim::simulateMTR(params, comps, rng, 72, 36, 72); + + const int N = r.nx * r.ny * r.nz; + REQUIRE(static_cast(r.mtrIndex.size()) == N); + + // MTR ids are exactly the set {1,2,3}. + for (int v : r.mtrIndex) { REQUIRE(v >= 1); REQUIRE(v <= 3); } + + // Empirical volume fractions within tolerance of targets. + std::array counts{0, 0, 0}; + for (int v : r.mtrIndex) { counts[v - 1]++; } + REQUIRE(static_cast(counts[0]) / N == Approx(0.30).margin(0.05)); + REQUIRE(static_cast(counts[1]) / N == Approx(0.35).margin(0.05)); + REQUIRE(static_cast(counts[2]) / N == Approx(0.35).margin(0.05)); + + // Euler ranges valid. + for (double a : r.phi1) { REQUIRE(a >= 0.0); REQUIRE(a <= 2.0 * std::numbers::pi); } + for (double a : r.phi) { REQUIRE(a >= 0.0); REQUIRE(a <= std::numbers::pi); } +} +``` + +Add `#include ` to the test file. + +- [ ] **Step 5: Run to verify it fails, then passes** + +Run: `ctest --test-dir -R mtrsim_driver --output-on-failure` +Expected: FAIL until `simulateMTR` is implemented and `main.cpp` compiles; then +PASS. If volume-fraction margins are too tight on a small grid, increase the +domain (e.g. `xLen=yLen=4.0`) rather than loosening past 0.05. + +- [ ] **Step 6: Commit** + +```bash +git add src/LibMTRSim/MTRSimDriver.hpp src/LibMTRSim/MTRSimDriver.cpp \ + src/app/main.cpp tests/test_mtrsim_driver.cpp +git commit -m "feat(lib): add simulateMTR end-to-end driver; main.cpp uses it" +``` + +--- + +## Phase B — The `MTRSimFilter` + +### Task 5: Scaffold the algorithm (`MTRSim`) header + input struct + +**Files:** +- Create: `src/MTRSim/Filters/Algorithms/MTRSim.hpp` +- Create: `src/MTRSim/Filters/Algorithms/MTRSim.cpp` + +- [ ] **Step 1: Create the algorithm header** + +Create `src/MTRSim/Filters/Algorithms/MTRSim.hpp` (mirror +`Algorithms/ReadMTRSimODF.hpp`): + +```cpp +#pragma once + +#include "MTRSim/MTRSim_export.hpp" + +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/Filter/IFilter.hpp" + +#include + +namespace nx::core +{ + +struct MTRSIM_EXPORT MTRSimInputValues +{ + DataPath inputOdfGeometryPath; + std::vector odfComponentPaths; + std::vector> volumeFractions; // 1 row x N cols + std::vector> thetaList; // M rows x 3 cols + std::vector physicalSize; // [x,y,z] microns + std::vector physicalSpacing; // [x,y,z] microns + uint64 seed; + bool generatePolarColoring; + DataPath outputGeometryPath; + std::string cellAttrMatName; + std::string mtrIdsArrayName; + std::string eulersArrayName; + std::string polarColorsArrayName; +}; + +/** + * @class MTRSim + * @brief Runs the MTR plurigaussian simulation and writes MTR ids, Euler + * angles, and optional polar-color cell data into the output Image Geometry. + */ +class MTRSIM_EXPORT MTRSim +{ +public: + MTRSim(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, MTRSimInputValues* inputValues); + ~MTRSim() noexcept; + + MTRSim(const MTRSim&) = delete; + MTRSim(MTRSim&&) noexcept = delete; + MTRSim& operator=(const MTRSim&) = delete; + MTRSim& operator=(MTRSim&&) noexcept = delete; + + Result<> operator()(); + +private: + DataStructure& m_DataStructure; + const MTRSimInputValues* m_InputValues = nullptr; + const std::atomic_bool& m_ShouldCancel; + const IFilter::MessageHandler& m_MessageHandler; +}; + +} // namespace nx::core +``` + +- [ ] **Step 2: Create the algorithm .cpp stub (compiles, returns ok)** + +Create `src/MTRSim/Filters/Algorithms/MTRSim.cpp` with the constructor/destructor +and a minimal `operator()` returning `{}` (full body added in Task 7). Mirror +`Algorithms/ReadMTRSimODF.cpp` constructor wiring. + +- [ ] **Step 3: Commit** + +```bash +git add src/MTRSim/Filters/Algorithms/MTRSim.hpp src/MTRSim/Filters/Algorithms/MTRSim.cpp +git commit -m "feat(filter): scaffold MTRSim algorithm header + stub" +``` + +--- + +### Task 6: Scaffold the filter (`MTRSimFilter`) with parameters + preflight + +**Files:** +- Create: `src/MTRSim/Filters/MTRSimFilter.hpp` +- Create: `src/MTRSim/Filters/MTRSimFilter.cpp` +- Modify: `MTRSimPlugin.cmake` (`FilterList`, `AlgorithmList`) +- Create/Modify: `test/MTRSimTest.cpp`, `test/CMakeLists.txt` + +- [ ] **Step 1: Create the filter header** + +Create `src/MTRSim/Filters/MTRSimFilter.hpp` mirroring `ReadMTRSimODFFilter.hpp`, +with these parameter keys and a fresh UUID (generate with `uuidgen`): + +```cpp + static inline constexpr StringLiteral k_InputOdfGeometry_Key = "input_odf_geometry"; + static inline constexpr StringLiteral k_OdfComponentArrays_Key = "odf_component_arrays"; + static inline constexpr StringLiteral k_VolumeFractions_Key = "volume_fractions"; + static inline constexpr StringLiteral k_ThetaList_Key = "theta_list"; + static inline constexpr StringLiteral k_PhysicalSize_Key = "physical_size"; + static inline constexpr StringLiteral k_PhysicalSpacing_Key = "physical_spacing"; + static inline constexpr StringLiteral k_UseSeed_Key = "use_seed"; + static inline constexpr StringLiteral k_SeedValue_Key = "seed_value"; + static inline constexpr StringLiteral k_SeedArrayName_Key = "seed_array_name"; + static inline constexpr StringLiteral k_GeneratePolarColoring_Key = "generate_polar_coloring"; + static inline constexpr StringLiteral k_OutputGeometry_Key = "output_geometry"; + static inline constexpr StringLiteral k_CellAttrMatName_Key = "cell_attribute_matrix_name"; + static inline constexpr StringLiteral k_MtrIdsArrayName_Key = "mtr_ids_array_name"; + static inline constexpr StringLiteral k_EulersArrayName_Key = "eulers_array_name"; + static inline constexpr StringLiteral k_PolarColorsArrayName_Key = "polar_colors_array_name"; +``` + +End the header with: +```cpp +SIMPLNX_DEF_FILTER_TRAITS(nx::core, MTRSimFilter, ""); +``` + +- [ ] **Step 2: Implement `parameters()`** + +In `MTRSimFilter.cpp`, includes (add to the `ReadMTRSimODFFilter.cpp` set): + +```cpp +#include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Parameters/MultiArraySelectionParameter.hpp" +#include "simplnx/Parameters/DynamicTableParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/DataGroupCreationParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Utilities/SIMPLConversion.hpp" +#include "simplnx/Common/Types.hpp" +#include +``` + +`parameters()` body: + +```cpp +Parameters MTRSimFilter::parameters() const +{ + Parameters params; + + params.insertSeparator(Parameters::Separator{"Input ODF"}); + params.insert(std::make_unique(k_InputOdfGeometry_Key, "Input ODF Geometry", "Image Geometry holding the ODF (from the Read/Compute ODF filters).", + DataPath{}, GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image})); + params.insert(std::make_unique(k_OdfComponentArrays_Key, "ODF Component Arrays", "Ordered list of per-component ODF cell arrays. Order maps to Volume Fraction columns.", + MultiArraySelectionParameter::ValueType{}, MultiArraySelectionParameter::AllowedTypes{IArray::ArrayType::DataArray}, + MultiArraySelectionParameter::AllowedComponentShapes{{1}}, GetAllNumericTypes())); + + params.insertSeparator(Parameters::Separator{"Simulation Parameters"}); + { + DynamicTableInfo vfInfo; + vfInfo.setRowsInfo(DynamicTableInfo::StaticVectorInfo(1)); + vfInfo.setColsInfo(DynamicTableInfo::DynamicVectorInfo(1, 3, "Comp {}")); + params.insert(std::make_unique(k_VolumeFractions_Key, "Volume Fraction", "One value per ODF component; must match the component count and sum to 1.0.", vfInfo)); + } + { + DynamicTableInfo thetaInfo; + thetaInfo.setRowsInfo(DynamicTableInfo::DynamicVectorInfo(1, 2, "Gaussian {}")); + thetaInfo.setColsInfo(DynamicTableInfo::StaticVectorInfo({"theta_x", "theta_y", "theta_z"})); + params.insert(std::make_unique(k_ThetaList_Key, "Theta List", "Correlation lengths [theta_x, theta_y, theta_z] per latent Gaussian. Needs >= (components - 1) rows. Same length unit as Physical Size/Spacing.", thetaInfo)); + } + params.insert(std::make_unique(k_PhysicalSize_Key, "Physical Size (microns)", "Domain extent X,Y,Z.", std::vector{38.1f, 12.7f, 0.0f}, std::vector{"X", "Y", "Z"})); + params.insert(std::make_unique(k_PhysicalSpacing_Key, "Physical Spacing (microns)", "Voxel spacing X,Y,Z.", std::vector{0.02f, 0.02f, 0.02f}, std::vector{"X", "Y", "Z"})); + + params.insertSeparator(Parameters::Separator{"Random Number Seed Parameters"}); + params.insertLinkableParameter(std::make_unique(k_UseSeed_Key, "Use Seed for Random Generation", "When true the user can supply a fixed seed.", false)); + params.insert(std::make_unique>(k_SeedValue_Key, "Seed Value", "The seed fed into the random generator.", std::mt19937::default_seed)); + params.insert(std::make_unique(k_SeedArrayName_Key, "Stored Seed Value Array Name", "Top-level array recording the seed used.", "MTRSim SeedValue")); + + params.insertSeparator(Parameters::Separator{"Outputs"}); + params.insertLinkableParameter(std::make_unique(k_GeneratePolarColoring_Key, "Generate Polar Coloring", "Create a 3-component UInt8 RGB array using the MATLAB polar color mapping.", false)); + params.insert(std::make_unique(k_OutputGeometry_Key, "Output Image Geometry", "Path of the new microstructure Image Geometry.", DataPath({"MTR Microstructure"}))); + params.insert(std::make_unique(k_CellAttrMatName_Key, "Cell Attribute Matrix Name", "Name of the created cell AttributeMatrix.", "Cell Data")); + params.insert(std::make_unique(k_MtrIdsArrayName_Key, "MTR Ids Array Name", "Int32 per-voxel MTR component id (1-based).", "MTRIds")); + params.insert(std::make_unique(k_EulersArrayName_Key, "Euler Angles Array Name", "Float32 3-component Bunge Euler angles [rad].", "Eulers")); + params.insert(std::make_unique(k_PolarColorsArrayName_Key, "Polar Colors Array Name", "UInt8 3-component RGB polar coloring.", "Polar Colors")); + + params.linkParameters(k_UseSeed_Key, k_SeedValue_Key, true); + params.linkParameters(k_GeneratePolarColoring_Key, k_PolarColorsArrayName_Key, true); + + return params; +} +``` + +> Add `#include "simplnx/Parameters/util/DynamicTableInfo.hpp"`. Verify +> `GetAllNumericTypes()`/`VectorFloat32Parameter` include paths against +> `ReadMTRSimODFFilter.cpp` siblings; adjust to the names the local simplnx +> exposes (`GetAllNumericTypes` is in `MultiArraySelectionParameter.hpp` usage +> in other filters — grep `simplnx` if the symbol differs). + +- [ ] **Step 3: Implement `preflightImpl` (validation + create geometry/arrays)** + +```cpp +IFilter::PreflightResult MTRSimFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + auto pOdfArrays = filterArgs.value(k_OdfComponentArrays_Key); + auto pVolumeFractions = filterArgs.value(k_VolumeFractions_Key); + auto pThetaList = filterArgs.value(k_ThetaList_Key); + auto pSize = filterArgs.value>(k_PhysicalSize_Key); + auto pSpacing = filterArgs.value>(k_PhysicalSpacing_Key); + auto pUseSeed = filterArgs.value(k_UseSeed_Key); + auto pGenPolar = filterArgs.value(k_GeneratePolarColoring_Key); + auto pOutGeomPath = filterArgs.value(k_OutputGeometry_Key); + auto pCellAttrMatName = filterArgs.value(k_CellAttrMatName_Key); + auto pMtrIdsName = filterArgs.value(k_MtrIdsArrayName_Key); + auto pEulersName = filterArgs.value(k_EulersArrayName_Key); + auto pPolarName = filterArgs.value(k_PolarColorsArrayName_Key); + auto pSeedArrayName = filterArgs.value(k_SeedArrayName_Key); + + nx::core::Result resultOutputActions; + std::vector preflightUpdatedValues; + + const usize numComponents = pOdfArrays.size(); + if (numComponents < 2) + { + return {MakeErrorResult(-13501, "MTRSim requires at least 2 ODF component arrays.")}; + } + + // Volume fractions: exactly 1 row, numComponents columns, sum ~ 1.0. + if (pVolumeFractions.size() != 1 || pVolumeFractions[0].size() != numComponents) + { + return {MakeErrorResult(-13502, fmt::format("Volume Fraction must be 1 row x {} columns (one per ODF component).", numComponents))}; + } + double vfSum = 0.0; + for (double v : pVolumeFractions[0]) { vfSum += v; } + if (std::abs(vfSum - 1.0) > 1.0e-3) + { + return {MakeErrorResult(-13503, fmt::format("Volume Fraction values must sum to 1.0 (got {:.4f}).", vfSum))}; + } + + // Theta list: >= numComponents - 1 rows, 3 columns each. + if (pThetaList.size() < numComponents - 1) + { + return {MakeErrorResult(-13504, fmt::format("Theta List needs at least {} rows (components - 1).", numComponents - 1))}; + } + for (const auto& row : pThetaList) + { + if (row.size() != 3) + { + return {MakeErrorResult(-13505, "Each Theta List row must have exactly 3 columns.")}; + } + } + + // Grid dims from size/spacing. + const auto dim = [](float len, float sp) { return static_cast(std::max(std::lround(len / sp), 1L)); }; + const usize nx = dim(pSize[0], pSpacing[0]); + const usize ny = dim(pSize[1], pSpacing[1]); + const usize nz = (pSize[2] <= 0.0f) ? 1 : dim(pSize[2], pSpacing[2]); + + const std::vector imageGeomDimsXYZ = {nx, ny, nz}; + const std::vector origin = {0.0f, 0.0f, 0.0f}; + const std::vector spacingXYZ = {pSpacing[0], pSpacing[1], pSpacing[2]}; + const std::vector tupleShapeZYX = {nz, ny, nx}; + + resultOutputActions.value().appendAction(std::make_unique(pOutGeomPath, imageGeomDimsXYZ, origin, spacingXYZ, pCellAttrMatName)); + + const DataPath cellAttrMatPath = pOutGeomPath.createChildPath(pCellAttrMatName); + resultOutputActions.value().appendAction(std::make_unique(DataType::int32, tupleShapeZYX, std::vector{1}, cellAttrMatPath.createChildPath(pMtrIdsName))); + resultOutputActions.value().appendAction(std::make_unique(DataType::float32, tupleShapeZYX, std::vector{3}, cellAttrMatPath.createChildPath(pEulersName))); + if (pGenPolar) + { + resultOutputActions.value().appendAction(std::make_unique(DataType::uint8, tupleShapeZYX, std::vector{3}, cellAttrMatPath.createChildPath(pPolarName))); + } + + // Seed array (top-level UInt64 scalar). + resultOutputActions.value().appendAction(std::make_unique(DataType::uint64, std::vector{1}, std::vector{1}, DataPath({pSeedArrayName}))); + + preflightUpdatedValues.push_back({"Output Grid (X, Y, Z)", fmt::format("{} x {} x {}", nx, ny, nz)}); + preflightUpdatedValues.push_back({"Number of ODF Components", std::to_string(numComponents)}); + + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; +} +``` + +> Includes needed: `CreateImageGeometryAction.hpp`, `CreateArrayAction.hpp`, +> ``, `fmt/format.h` (already in sibling). The +> `name()/className()/uuid()/humanName()/defaultTags()/parametersVersion()/clone()/FromSIMPLJson()` +> methods mirror `ReadMTRSimODFFilter.cpp` exactly. `humanName()` returns +> `"Generate Synthetic Microtexture"`; `defaultTags()` returns +> `{className(), "MTRSim", "Synthetic", "Microtexture", "Generate"}`. + +- [ ] **Step 4: Implement `executeImpl` (delegate to algorithm)** + +```cpp +Result<> MTRSimFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + MTRSimInputValues inputValues; + inputValues.inputOdfGeometryPath = filterArgs.value(k_InputOdfGeometry_Key); + inputValues.odfComponentPaths = filterArgs.value(k_OdfComponentArrays_Key); + inputValues.volumeFractions = filterArgs.value(k_VolumeFractions_Key); + inputValues.thetaList = filterArgs.value(k_ThetaList_Key); + inputValues.physicalSize = filterArgs.value>(k_PhysicalSize_Key); + inputValues.physicalSpacing = filterArgs.value>(k_PhysicalSpacing_Key); + inputValues.generatePolarColoring = filterArgs.value(k_GeneratePolarColoring_Key); + inputValues.outputGeometryPath = filterArgs.value(k_OutputGeometry_Key); + inputValues.cellAttrMatName = filterArgs.value(k_CellAttrMatName_Key); + inputValues.mtrIdsArrayName = filterArgs.value(k_MtrIdsArrayName_Key); + inputValues.eulersArrayName = filterArgs.value(k_EulersArrayName_Key); + inputValues.polarColorsArrayName = filterArgs.value(k_PolarColorsArrayName_Key); + + // Standard simplnx seed handling. + uint64 seed = filterArgs.value(k_SeedValue_Key); + if (!filterArgs.value(k_UseSeed_Key)) + { + seed = static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); + } + dataStructure.getDataRefAs(DataPath({filterArgs.value(k_SeedArrayName_Key)}))[0] = seed; + inputValues.seed = seed; + + return MTRSim(dataStructure, messageHandler, shouldCancel, &inputValues)(); +} +``` + +> Add `#include ` and `#include "simplnx/DataStructure/DataArray.hpp"`. + +- [ ] **Step 5: Register filter + algorithm in `MTRSimPlugin.cmake`** + +In `FilterList` add `MTRSimFilter`; in `AlgorithmList` add `MTRSim`. + +- [ ] **Step 6: Add the test file to `test/CMakeLists.txt` and write preflight error tests** + +Add `MTRSimTest.cpp` to `${PLUGIN_NAME}UnitTest_SRCS`. Create +`test/MTRSimTest.cpp` (mirror `ReadMTRSimODFTest.cpp` includes/structure): + +```cpp +#include "MTRSim/Filters/MTRSimFilter.hpp" + +#include "simplnx/UnitTest/UnitTestCommon.hpp" +#include "simplnx/Parameters/DynamicTableParameter.hpp" +#include "simplnx/Parameters/MultiArraySelectionParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" + +#include + +using namespace nx::core; + +namespace +{ +// Build a DataStructure with an ODF ImageGeom (72x36x72) holding `n` Float64 +// single-component cell arrays named component_0.. and return the geometry + +// component paths. Helper used by every test below. +DataStructure makeOdfDataStructure(usize numComponents, std::vector& outCompPaths) +{ + DataStructure ds; + auto* ig = ImageGeom::Create(ds, "ODF"); + ig->setDimensions({72, 36, 72}); + ig->setSpacing({5.0f, 5.0f, 5.0f}); + ig->setOrigin({0.0f, 0.0f, 0.0f}); + auto* am = AttributeMatrix::Create(ds, "Cell Data", {72 * 36 * 72}, ig->getId()); + for (usize c = 0; c < numComponents; ++c) + { + const std::string name = fmt::format("component_{}", c); + auto* arr = UInt64Array::CreateWithStore(ds, name, {72 * 36 * 72}, {1}, am->getId()); + (void)arr; + outCompPaths.push_back(DataPath({"ODF", "Cell Data", name})); + } + return ds; +} +} // namespace + +TEST_CASE("MTRSimFilter: preflight rejects mismatched volume fraction count", "[MTRSim]") +{ + std::vector compPaths; + DataStructure ds = makeOdfDataStructure(3, compPaths); + + MTRSimFilter filter; + Arguments args; + args.insertOrAssign(MTRSimFilter::k_InputOdfGeometry_Key, DataPath({"ODF"})); + args.insertOrAssign(MTRSimFilter::k_OdfComponentArrays_Key, compPaths); + args.insertOrAssign(MTRSimFilter::k_VolumeFractions_Key, DynamicTableParameter::ValueType{{0.5, 0.5}}); // only 2, need 3 + args.insertOrAssign(MTRSimFilter::k_ThetaList_Key, DynamicTableParameter::ValueType{{0.1, 0.45, 0.1}, {0.08, 0.37, 0.08}}); + args.insertOrAssign(MTRSimFilter::k_PhysicalSize_Key, std::vector{2.0f, 2.0f, 0.0f}); + args.insertOrAssign(MTRSimFilter::k_PhysicalSpacing_Key, std::vector{0.02f, 0.02f, 0.02f}); + args.insertOrAssign(MTRSimFilter::k_UseSeed_Key, true); + args.insertOrAssign(MTRSimFilter::k_SeedValue_Key, static_cast(42)); + args.insertOrAssign(MTRSimFilter::k_SeedArrayName_Key, std::string("MTRSim SeedValue")); + args.insertOrAssign(MTRSimFilter::k_GeneratePolarColoring_Key, false); + args.insertOrAssign(MTRSimFilter::k_OutputGeometry_Key, DataPath({"MTR Microstructure"})); + args.insertOrAssign(MTRSimFilter::k_CellAttrMatName_Key, std::string("Cell Data")); + args.insertOrAssign(MTRSimFilter::k_MtrIdsArrayName_Key, std::string("MTRIds")); + args.insertOrAssign(MTRSimFilter::k_EulersArrayName_Key, std::string("Eulers")); + args.insertOrAssign(MTRSimFilter::k_PolarColorsArrayName_Key, std::string("Polar Colors")); + + auto result = filter.preflight(ds, args); + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); +} +``` + +> Fix the obvious paste error in the helper: components must be created as +> `Float64Array::CreateWithStore` (the `UInt64Array` token is a +> typo — use `Float64Array`). Reuse this `args`-builder as a lambda in later +> tests to stay DRY. + +- [ ] **Step 7: Build and run the preflight test** + +Run: `cmake --build /Users/mjackson/Workspace7/simplnx/build --config Release` +then `ctest --test-dir /Users/mjackson/Workspace7/simplnx/build -R MTRSim --output-on-failure` +Expected: the mismatch test PASSES (preflight returns invalid). Also add and run +analogous tests for: theta rows `< numComponents-1` (error -13504), VF sum ≠ 1 +(error -13503). Each follows the same builder with one field changed. + +- [ ] **Step 8: Commit** + +```bash +git add src/MTRSim/Filters/MTRSimFilter.hpp src/MTRSim/Filters/MTRSimFilter.cpp \ + MTRSimPlugin.cmake test/MTRSimTest.cpp test/CMakeLists.txt +git commit -m "feat(filter): add MTRSimFilter params + preflight validation + error tests" +``` + +--- + +### Task 7: Algorithm `operator()` — read ODF, run sim, write arrays + +**Files:** +- Modify: `src/MTRSim/Filters/Algorithms/MTRSim.cpp` +- Modify: `test/MTRSimTest.cpp` + +- [ ] **Step 1: Implement `MTRSim::operator()`** + +```cpp +#include "MTRSim.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" + +#include "LibMTRSim/MTRSimDriver.hpp" + +#include + +#include +#include + +using namespace nx::core; + +MTRSim::MTRSim(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, MTRSimInputValues* inputValues) +: m_DataStructure(dataStructure), m_InputValues(inputValues), m_ShouldCancel(shouldCancel), m_MessageHandler(mesgHandler) +{ +} + +MTRSim::~MTRSim() noexcept = default; + +Result<> MTRSim::operator()() +{ + // 1. Pull ODF bin geometry (degrees) from the input ODF ImageGeom. + const auto& odfGeom = m_DataStructure.getDataRefAs(m_InputValues->inputOdfGeometryPath); + const SizeVec3 odfDims = odfGeom.getDimensions(); // X=phi2, Y=PHI, Z=phi1 + const FloatVec3 odfSpacing = odfGeom.getSpacing(); // degrees + const int n2 = static_cast(odfDims[0]); + const int nPHI = static_cast(odfDims[1]); + const int n1 = static_cast(odfDims[2]); + + // 2. Reconstruct ODFComponents from the selected cell arrays. + std::vector components; + components.reserve(m_InputValues->odfComponentPaths.size()); + for (const auto& path : m_InputValues->odfComponentPaths) + { + const auto& arr = m_DataStructure.getDataRefAs(path); + const auto& store = arr.getDataStoreRef(); + std::vector values(store.begin(), store.end()); + components.push_back(mtrsim::gridToODFComponent(values, n1, nPHI, n2, + static_cast(odfSpacing[2]), static_cast(odfSpacing[1]), static_cast(odfSpacing[0]))); + } + + // 3. Build SimulationParams. + mtrsim::SimulationParams params; + params.xLen = m_InputValues->physicalSize[0]; + params.yLen = m_InputValues->physicalSize[1]; + params.zLen = m_InputValues->physicalSize[2]; + params.dx = m_InputValues->physicalSpacing[0]; + params.dy = m_InputValues->physicalSpacing[1]; + params.dz = m_InputValues->physicalSpacing[2]; + params.volumeFractions = m_InputValues->volumeFractions[0]; // 1 row + params.thetaList = m_InputValues->thetaList; + params.seed = m_InputValues->seed; + + // 4. Run the simulation (returns SIMPLNX z,y,x order). + std::mt19937_64 rng(m_InputValues->seed); + mtrsim::MTRSimResult sim; + try + { + sim = mtrsim::simulateMTR(params, components, rng, n1, nPHI, n2); + } catch (const std::exception& e) + { + return MakeErrorResult(-13550, fmt::format("MTR simulation failed: {}", e.what())); + } + + if (m_ShouldCancel) { return {}; } + + // 5. Write MTR ids + Euler arrays. + const DataPath cellAm = m_InputValues->outputGeometryPath.createChildPath(m_InputValues->cellAttrMatName); + auto& mtrIds = m_DataStructure.getDataRefAs(cellAm.createChildPath(m_InputValues->mtrIdsArrayName)); + auto& eulers = m_DataStructure.getDataRefAs(cellAm.createChildPath(m_InputValues->eulersArrayName)); + auto& mtrStore = mtrIds.getDataStoreRef(); + auto& eulerStore = eulers.getDataStoreRef(); + + const std::size_t N = sim.mtrIndex.size(); + for (std::size_t i = 0; i < N; ++i) + { + mtrStore[i] = sim.mtrIndex[i]; + eulerStore[i * 3 + 0] = static_cast(sim.phi1[i]); + eulerStore[i * 3 + 1] = static_cast(sim.phi[i]); + eulerStore[i * 3 + 2] = static_cast(sim.phi2[i]); + } + + // 6. Optional polar coloring (Task 8). + if (m_InputValues->generatePolarColoring) + { + return applyPolarColoring(sim, cellAm); // declared/added in Task 8 + } + + return {}; +} +``` + +> If `applyPolarColoring` is not yet added, temporarily inline a `return {};` for +> the polar branch and replace in Task 8. Confirm DataArray alias names +> (`Int32Array`, `Float32Array`, `Float64Array`) against `simplnx` +> `DataArray.hpp`. + +- [ ] **Step 2: Write the end-to-end filter test (statistical + array contract)** + +Add to `test/MTRSimTest.cpp`. Reuse the `args`-builder from Task 6 with a valid +3-component VF and a fixed seed; after `execute`, assert array existence, types, +component counts, that ids ∈ {1,2,3}, and empirical volume fractions within +0.06 of targets. + +```cpp +TEST_CASE("MTRSimFilter: execute produces valid MTR ids + Eulers", "[MTRSim]") +{ + std::vector compPaths; + DataStructure ds = makeOdfDataStructure(3, compPaths); + // Fill each ODF component uniformly so sampling is well-defined. + for (const auto& p : compPaths) + { + auto& a = ds.getDataRefAs(p); + a.fill(1.0); + } + + MTRSimFilter filter; + Arguments args; /* ... same builder, VF = {{0.30,0.35,0.35}}, UseSeed=true, Seed=42, Size={4,4,0} ... */ + + auto pre = filter.preflight(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(pre.outputActions); + auto exec = filter.execute(ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(exec.result); + + const DataPath cellAm({"MTR Microstructure", "Cell Data"}); + auto& ids = ds.getDataRefAs(cellAm.createChildPath("MTRIds")); + auto& eul = ds.getDataRefAs(cellAm.createChildPath("Eulers")); + REQUIRE(eul.getNumberOfComponents() == 3); + + std::array counts{0, 0, 0}; + for (usize i = 0; i < ids.getNumberOfTuples(); ++i) + { + const int v = ids[i]; + REQUIRE(v >= 1); REQUIRE(v <= 3); + counts[v - 1]++; + } + const double n = static_cast(ids.getNumberOfTuples()); + REQUIRE(counts[0] / n == Approx(0.30).margin(0.06)); + REQUIRE(counts[1] / n == Approx(0.35).margin(0.06)); + + // Seed array recorded. + auto& seedArr = ds.getDataRefAs(DataPath({"MTRSim SeedValue"})); + REQUIRE(seedArr[0] == 42); +} +``` + +- [ ] **Step 3: Build, run, verify** + +Run: `cmake --build /Users/mjackson/Workspace7/simplnx/build --config Release` +then `ctest --test-dir /Users/mjackson/Workspace7/simplnx/build -R MTRSim --output-on-failure` +Expected: PASS. If volume-fraction margins fail on a small grid, raise `Size` to +`{6,6,0}`. + +- [ ] **Step 4: Commit** + +```bash +git add src/MTRSim/Filters/Algorithms/MTRSim.cpp test/MTRSimTest.cpp +git commit -m "feat(filter): implement MTRSim algorithm execute + statistical test" +``` + +--- + +### Task 8: Optional polar coloring output + +**Files:** +- Modify: `src/MTRSim/Filters/Algorithms/MTRSim.hpp`, `src/MTRSim/Filters/Algorithms/MTRSim.cpp` +- Modify: `test/MTRSimTest.cpp` + +- [ ] **Step 1: Declare the helper** + +Add to `MTRSim.hpp` private section: + +```cpp + Result<> applyPolarColoring(const struct mtrsim::MTRSimResult& sim, const DataPath& cellAttrMatPath); +``` +(Forward declaration alternative: include `LibMTRSim/MTRSimDriver.hpp` in the +header and drop the `struct` keyword. Match whichever compiles cleanly.) + +- [ ] **Step 2: Implement using `IPFMapper` (MATLAB/polar scheme)** + +Add to `MTRSim.cpp`: + +```cpp +#include "LibMTRSim/IPFMapper.hpp" + +Result<> MTRSim::applyPolarColoring(const mtrsim::MTRSimResult& sim, const DataPath& cellAttrMatPath) +{ + const std::size_t N = sim.phi1.size(); + Eigen::VectorXd phi1 = Eigen::Map(sim.phi1.data(), N); + Eigen::VectorXd phi = Eigen::Map(sim.phi.data(), N); + Eigen::VectorXd phi2 = Eigen::Map(sim.phi2.data(), N); + + mtrsim::IPFMapper mapper{mtrsim::CrystalSystem::HCP}; + const std::vector colors = mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, mtrsim::IPFColorScheme::MatLab); + + auto& rgb = m_DataStructure.getDataRefAs(cellAttrMatPath.createChildPath(m_InputValues->polarColorsArrayName)); + auto& store = rgb.getDataStoreRef(); + for (std::size_t i = 0; i < N; ++i) + { + store[i * 3 + 0] = colors[i].r; + store[i * 3 + 1] = colors[i].g; + store[i * 3 + 2] = colors[i].b; + } + return {}; +} +``` + +> `eulerToColors` returns values already in SIMPLNX voxel order because `sim` +> is already remapped. Add `#include ` to the .cpp. + +- [ ] **Step 3: Write the polar-coloring test** + +Add to `test/MTRSimTest.cpp`: a copy of the execute test with +`k_GeneratePolarColoring_Key = true`, asserting the `Polar Colors` array exists, +is `UInt8`, 3 components, same tuple count as `MTRIds`, and is not all-zero: + +```cpp +TEST_CASE("MTRSimFilter: polar coloring produces a populated RGB array", "[MTRSim]") +{ + // ... build ds + args as in the execute test, but: + args.insertOrAssign(MTRSimFilter::k_GeneratePolarColoring_Key, true); + // ... preflight VALID, execute VALID ... + const DataPath cellAm({"MTR Microstructure", "Cell Data"}); + auto& rgb = ds.getDataRefAs(cellAm.createChildPath("Polar Colors")); + REQUIRE(rgb.getNumberOfComponents() == 3); + REQUIRE(rgb.getNumberOfTuples() > 0); + uint64 sum = 0; + for (usize i = 0; i < rgb.getSize(); ++i) { sum += rgb[i]; } + REQUIRE(sum > 0); +} +``` + +Also assert that when the bool is **false**, the `Polar Colors` array does NOT +exist: +```cpp + REQUIRE(ds.getDataAs(DataPath({"MTR Microstructure", "Cell Data", "Polar Colors"})) == nullptr); +``` + +- [ ] **Step 4: Build, run, verify, commit** + +Run the build + `ctest -R MTRSim`. Expected PASS. +```bash +git add src/MTRSim/Filters/Algorithms/MTRSim.hpp src/MTRSim/Filters/Algorithms/MTRSim.cpp test/MTRSimTest.cpp +git commit -m "feat(filter): add optional polar coloring output + tests" +``` + +--- + +## Phase C — Integration, CI, docs + +### Task 9: Full dual-build + run all tests + +- [ ] **Step 1: Run clang-format on all new files** + +Run the repo's format script (mirror `git log` commit "Clang Format"; the CI +`format_pr.yml` enforces it). Example: +`find src/LibMTRSim src/MTRSim tests test -name 'MTRSim*' -o -name 'test_mtrsim*' | xargs clang-format -i` +(adjust glob). Commit any reformatting. + +- [ ] **Step 2: Build + test the standalone library** + +Run: `cmake --build ` then +`ctest --test-dir --output-on-failure` +Expected: all `mtrsim_driver` + existing lib tests PASS. + +- [ ] **Step 3: Build + test the plugin** + +Run: `cmake --build /Users/mjackson/Workspace7/simplnx/build --config Release` +then `ctest --test-dir /Users/mjackson/Workspace7/simplnx/build -R MTRSim --output-on-failure` +Expected: all filter tests PASS. + +- [ ] **Step 4: Commit any fixes** + +```bash +git add -A && git commit -m "chore: clang-format + dual-build fixes for MTRSim filter" +``` + +--- + +### Task 10: Filter documentation stub + +**Files:** +- Create: `docs/MTRSim/MTRSimFilter.md` + +- [ ] **Step 1: Write the doc** using the `bluequartz-skills:filter-documentation` + template: summary, the parameter table (ODF geometry, component arrays, volume + fraction, theta list, size/spacing, seed group, polar coloring, outputs), + outputs description (MTRIds 1-based, Eulers radians, Polar Colors), the + units-consistency note, and an example pipeline reference. (Full polish is + Milestone AL.) + +- [ ] **Step 2: Commit** + +```bash +git add docs/MTRSim/MTRSimFilter.md +git commit -m "docs: add MTRSimFilter documentation stub" +``` + +--- + +### Task 11: Validate CI + +- [ ] **Step 1: Push the branch and open a PR to `develop`** + +```bash +git push -u origin topic/create_mtr_sim_filter +gh pr create --base develop --title "Milestone AK: MTRSimFilter (Generate Synthetic Microtexture)" --body "Implements the MTRSim simulation filter, deterministic + statistical tests, and validates CI. See docs/superpowers/plans/2026-06-01-milestone-ak.md." +``` + +- [ ] **Step 2: Watch the workflows** + +Run: `gh pr checks --watch` +Expected: `linux`, `macos`, `windows`, `format_pr` all green. If `format_pr` +fails, run clang-format and push. If a build fails on a missing vcpkg dependency +for the embedded LibMTRSim (Eigen/spdlog/CLI11/nlohmann-json/hdf5/stb), confirm +those appear in `vcpkg.json` and are visible to the simplnx build; add any +missing entry and push. + +- [ ] **Step 3: Confirm the acceptance criteria** + +Verify the CI run shows the `MTRSim` ctest cases executed and passed on all +platforms (the "all new code paths exercised by passing tests in CI" +requirement). Capture a screenshot/log link for the report. + +--- + +## Self-Review Notes (for the executor) + +- **Spec coverage:** filter (Task 5-8), component-exact tests (Task 1-3, 6), + pipeline-statistical test (Task 4, 7), preflight/error tests (Task 6), CI + (Task 11), docs stub (Task 10), `buildUniformODF` exposure (Task 1), ODF→ + component helper (Task 2), voxel remap (Task 3), units note (Task 5 param help + + Task 10 doc). All spec sections map to a task. +- **Known things to verify against the live simplnx headers** (APIs evolve): + `GetAllNumericTypes`, `VectorFloat32Parameter`, `DynamicTableInfo::DynamicVectorInfo` + signature, `AttributeMatrix::Create`, `Float64Array::CreateWithStore`, + `SIMPLNX_RESULT_REQUIRE_VALID/INVALID` macro names. Grep the local checkout at + `/Users/mjackson/Workspace7/simplnx` and match a sibling filter/test exactly if + any symbol differs. +- **Fix the deliberate typo** flagged in Task 6 Step 6 (`UInt64Array` → + `Float64Array` in the ODF test helper). diff --git a/docs/superpowers/plans/2026-06-02-mtrsim-observer-and-config.md b/docs/superpowers/plans/2026-06-02-mtrsim-observer-and-config.md new file mode 100644 index 0000000..1770e16 --- /dev/null +++ b/docs/superpowers/plans/2026-06-02-mtrsim-observer-and-config.md @@ -0,0 +1,858 @@ +# MTRSim Observer + Config-File Input — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a progress/cancellation observer (`mtrsim::ISimulationObserver`) threaded through `simulateMTR`/`PGRFSimulation`/`ODFSampler`, and an optional MTRSim config-JSON input on `MTRSimFilter` gated by a linked boolean. + +**Architecture:** A header-only pure-virtual `ISimulationObserver` (progress + cancel) is passed (nullable) into the library simulation entry points; `nullptr` preserves today's behavior exactly. The standalone CLI and the filter each supply a concrete observer. A shared `mtrsim::parseConfigJson` parser (extracted from `main.cpp`) lets both the CLI and the filter build `SimulationParams` from a config file; the filter exposes it via a linked boolean that shows config-mode vs manual-mode parameters. + +**Tech Stack:** C++17, Eigen, simplnx filter framework, Catch2 v2 (bare `Approx`, not `Catch::Approx`), nlohmann::json, spdlog, CMake. + +--- + +## Background the engineer needs + +**Reference design:** `docs/superpowers/specs/2026-06-02-mtrsim-observer-and-config-design.md`. + +**Current signatures (verified):** +- `mtrsim::simulateMTR(const SimulationParams&, const std::vector&, std::mt19937_64&, int n1, int nPHI, int n2)` → `MTRSimResult` (`src/LibMTRSim/MTRSimDriver.hpp`). `MTRSimResult` has `int nx,ny,nz; std::vector mtrIndex; std::vector phi1, phi, phi2;`. +- `mtrsim::ODFSampler::sampleN(int n, const ODFComponent& component, const ODFComponent& uniform)` → `Eigen::MatrixXd (n×3)`. Two `for i in [0,n)` loops inside (sampling at `ODFSampler.cpp:56-64`, assignment at `77-83`). +- `mtrsim::PGRFSimulation::run(const SimulationParams&)` → `PGRFResult` (`src/LibMTRSim/PGRFSimulation.hpp`); loops over `numGaussians = numComponents-1` latent fields. +- `SimulationParams` fields: `double xLen,yLen,zLen,dx,dy,dz; std::vector volumeFractions; std::vector> thetaList; std::vector nuggetVariance; std::string odfInputPath, outputDir; uint64_t seed;`. +- `main.cpp` parses config JSON inline at lines 153-179. + +**Filter (`src/MTRSim/Filters/`):** `MTRSimFilter` keys end with `_path`/`_index` per `FilterValidationTest`. The algorithm `MTRSim` holds `const IFilter::MessageHandler& m_MessageHandler` and `const std::atomic_bool& m_ShouldCancel`. The standard simplnx seed pattern (`use_seed`/`seed_value`/`seed_array_name`) and `linkParameters` are already used. + +**Build & test commands:** +- **Standalone library** (Phase A, B tests): configure once `cmake --preset mtrsim-Rel`; build `cmake --build /Users/mjackson/Workspace7/Build/mtrsim-Rel`; run tests `/Users/mjackson/Workspace7/Build/mtrsim-Rel/bin/mtrsim_tests "[tag]"` or `ctest --test-dir /Users/mjackson/Workspace7/Build/mtrsim-Rel --output-on-failure`. Catch2 uses **bare `Approx`**. +- **Plugin** (Phase C): build `cmake --build /Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib --target MTRSimUnitTest`; run `ctest --test-dir /Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib -R "MTRSim::" --output-on-failure`. +- **FilterValidationTest** (part of `simplnx_test`): build `cmake --build --target simplnx_test`; run `ctest --test-dir -R "FilterValidation" --output-on-failure` (or run `Bin/simplnx_test "[FilterValidation]"` — confirm the exact ctest name with `ctest -N | grep -i filtervalidation`). +- **Debug smoke** (Phase D): `cmake --build /Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Dbg-EbsdLib --target MTRSim nxrunner` then `/Bin/nxrunner_d --execute pipelines/MTRSim_smoke_test.d3dpipeline`. + +**No git worktree** (DREAM3DNX build expects MTRSim at a fixed path). Work on the current branch. Sync first: `git fetch upstream && git reset --soft upstream/topic/create_mtr_sim_filter` if behind (it changes no files when trees match). + +--- + +## File Structure + +**Create:** +- `src/LibMTRSim/ISimulationObserver.hpp` — the interface. +- `src/LibMTRSim/SimulationObservers.hpp` — `NullObserver`, `ConsoleObserver`. +- `src/LibMTRSim/ConfigIO.hpp`, `src/LibMTRSim/ConfigIO.cpp` — `parseConfigJson`. +- `tests/test_observer.cpp`, `tests/test_config_io.cpp`. + +**Modify:** +- `src/LibMTRSim/MTRSimDriver.{hpp,cpp}` — observer arg, `MTRSimResult::cancelled`. +- `src/LibMTRSim/PGRFSimulation.{hpp,cpp}` — observer arg. +- `src/LibMTRSim/ODFSampler.{hpp,cpp}` — observer arg + in-loop cancel/progress. +- `src/app/main.cpp` — `ConsoleObserver`, `parseConfigJson`. +- `src/MTRSim/Filters/MTRSimFilter.{hpp,cpp}` — config params + linking + config-mode preflight/execute. +- `src/MTRSim/Filters/Algorithms/MTRSim.{hpp,cpp}` — `FilterObserver`, config-mode params, pass observer. +- `src/LibMTRSim/CMakeLists.txt`, `MTRSimPlugin.cmake`, `tests/CMakeLists.txt` — register. +- `docs/MTRSimFilter.md`. + +--- + +## Phase A — Observer (LibMTRSim) + +### Task A1: `ISimulationObserver` + default observers + +**Files:** Create `src/LibMTRSim/ISimulationObserver.hpp`, `src/LibMTRSim/SimulationObservers.hpp`, `tests/test_observer.cpp`; Modify `src/LibMTRSim/CMakeLists.txt`, `MTRSimPlugin.cmake`, `tests/CMakeLists.txt`. + +- [ ] **Step 1: Create `src/LibMTRSim/ISimulationObserver.hpp`** + +```cpp +#pragma once + +#include "libmtrsim_export.h" + +#include +#include + +namespace mtrsim { + +/** + * @brief Observer interface for long-running MTR simulations. + * + * Implementations receive throttled progress updates and are polled for + * cancellation from the thread running simulateMTR. They must be cheap and + * must not throw. + */ +class LIBMTRSIM_EXPORT ISimulationObserver { +public: + ISimulationObserver() = default; + virtual ~ISimulationObserver() = default; + + ISimulationObserver(const ISimulationObserver&) = delete; + ISimulationObserver& operator=(const ISimulationObserver&) = delete; + ISimulationObserver(ISimulationObserver&&) = delete; + ISimulationObserver& operator=(ISimulationObserver&&) = delete; + + /// Report progress. `done`/`total` describe the current phase; `message` + /// names it. `total <= 0` means "indeterminate". + virtual void updateProgress(int64_t done, int64_t total, const std::string& message) = 0; + + /// Polled at checkpoints; returning true stops the simulation early. + [[nodiscard]] virtual bool shouldCancel() const = 0; +}; + +} // namespace mtrsim +``` + +- [ ] **Step 2: Create `src/LibMTRSim/SimulationObservers.hpp`** + +```cpp +#pragma once + +#include "ISimulationObserver.hpp" + +#include + +#include +#include + +namespace mtrsim { + +/// No-op observer; the implicit default when none is supplied. +class NullObserver : public ISimulationObserver { +public: + void updateProgress(int64_t /*done*/, int64_t /*total*/, const std::string& /*message*/) override {} + [[nodiscard]] bool shouldCancel() const override { return false; } +}; + +/// Logs progress via spdlog (throttled). Never cancels. +class ConsoleObserver : public ISimulationObserver { +public: + void updateProgress(int64_t done, int64_t total, const std::string& message) override { + const int pct = (total > 0) ? static_cast(done * 100 / total) : -1; + if (pct != m_LastPct) { + m_LastPct = pct; + if (pct >= 0) { + spdlog::info("[{:3d}%] {}", pct, message); + } else { + spdlog::info("{}", message); + } + } + } + [[nodiscard]] bool shouldCancel() const override { return false; } + +private: + int m_LastPct = -2; +}; + +} // namespace mtrsim +``` + +- [ ] **Step 3: Register headers in CMake** + +In `src/LibMTRSim/CMakeLists.txt`, add `ISimulationObserver.hpp` and `SimulationObservers.hpp` to the `MTRSIM_HEADERS` list (mirror the existing `ODFSampler.hpp` entry). In `MTRSimPlugin.cmake`, add both to the plugin header list (mirror the `MTRSimDriver.hpp` entry). + +- [ ] **Step 4: Write the failing test** + +Create `tests/test_observer.cpp`: + +```cpp +#include "SimulationObservers.hpp" + +#include + +#include + +namespace { +// Test double: records progress calls; can be told to cancel after K updates. +class RecordingObserver : public mtrsim::ISimulationObserver { +public: + explicit RecordingObserver(int cancelAfter = -1) : m_CancelAfter(cancelAfter) {} + void updateProgress(int64_t done, int64_t total, const std::string& message) override { + calls.push_back({done, total, message}); + } + bool shouldCancel() const override { + return m_CancelAfter >= 0 && static_cast(calls.size()) >= m_CancelAfter; + } + struct Call { int64_t done; int64_t total; std::string message; }; + std::vector calls; +private: + int m_CancelAfter; +}; +} // namespace + +TEST_CASE("NullObserver never cancels and ignores progress", "[observer]") { + mtrsim::NullObserver obs; + obs.updateProgress(1, 10, "x"); + REQUIRE_FALSE(obs.shouldCancel()); +} + +TEST_CASE("RecordingObserver records and cancels after K", "[observer]") { + RecordingObserver obs(2); + REQUIRE_FALSE(obs.shouldCancel()); + obs.updateProgress(1, 10, "a"); + REQUIRE_FALSE(obs.shouldCancel()); + obs.updateProgress(2, 10, "b"); + REQUIRE(obs.shouldCancel()); + REQUIRE(obs.calls.size() == 2); + REQUIRE(obs.calls[1].done == 2); +} +``` + +Add `test_observer.cpp` to the `mtrsim_tests` executable in `tests/CMakeLists.txt` (mirror `test_odf_sampler.cpp`). Add include dir if needed (the tests target already adds `${PROJECT_SOURCE_DIR}/src/LibMTRSim`, so `#include "SimulationObservers.hpp"` resolves). + +- [ ] **Step 5: Build and run; verify pass** + +Run: `cmake --build /Users/mjackson/Workspace7/Build/mtrsim-Rel && /Users/mjackson/Workspace7/Build/mtrsim-Rel/bin/mtrsim_tests "[observer]"` +Expected: PASS (3 assertions / 2 cases). + +- [ ] **Step 6: Commit** + +```bash +git add src/LibMTRSim/ISimulationObserver.hpp src/LibMTRSim/SimulationObservers.hpp \ + src/LibMTRSim/CMakeLists.txt MTRSimPlugin.cmake tests/test_observer.cpp tests/CMakeLists.txt +git commit -m "feat(lib): add ISimulationObserver + Null/Console observers" +``` +(End every commit body with: `Co-Authored-By: Claude Opus 4.8 (1M context) `) + +--- + +### Task A2: thread observer through `simulateMTR` + `PGRFSimulation` (coarse: stages + per-component) + +**Files:** Modify `src/LibMTRSim/MTRSimDriver.{hpp,cpp}`, `src/LibMTRSim/PGRFSimulation.{hpp,cpp}`, `tests/test_mtrsim_driver.cpp`. + +- [ ] **Step 1: Add `cancelled` to `MTRSimResult` and the observer param to `simulateMTR` (header)** + +In `src/LibMTRSim/MTRSimDriver.hpp`: add `#include "ISimulationObserver.hpp"`. In `struct MTRSimResult` add `bool cancelled = false;`. Change the declaration: + +```cpp +LIBMTRSIM_EXPORT MTRSimResult simulateMTR(const SimulationParams& params, const std::vector& odfComponents, std::mt19937_64& rng, int n1, int nPHI, int n2, ISimulationObserver* observer = nullptr); +``` + +- [ ] **Step 2: Add observer param to `PGRFSimulation::run` (header)** + +In `src/LibMTRSim/PGRFSimulation.hpp`: add `#include "ISimulationObserver.hpp"` and change: + +```cpp +PGRFResult run(const SimulationParams& params, ISimulationObserver* observer = nullptr); +``` + +- [ ] **Step 3: Thread cancel/progress into `PGRFSimulation::run` (impl)** + +In `src/LibMTRSim/PGRFSimulation.cpp`, update the signature and, inside the latent-field loop (`for (int h = 0; h < numGaussians; ++h)`), before generating each field add: + +```cpp + if (observer != nullptr) { + if (observer->shouldCancel()) { + return PGRFResult{}; // empty; caller checks observer->shouldCancel() + } + observer->updateProgress(h, numGaussians, fmt::format("Simulating latent Gaussian field {}/{}", h + 1, numGaussians)); + } +``` + +Add `#include ` if not present. (Leave the existing spdlog logs.) + +- [ ] **Step 4: Thread observer into `simulateMTR` (impl)** — update `src/LibMTRSim/MTRSimDriver.cpp`'s `simulateMTR` to accept `ISimulationObserver* observer`, pass it to `pgrf.run(params, observer)`, and add checkpoints. Use a tiny local helper for null-safety: + +```cpp +MTRSimResult simulateMTR(const SimulationParams& params, const std::vector& odfComponents, std::mt19937_64& rng, int n1, int nPHI, int n2, ISimulationObserver* observer) { + const int nx = static_cast(std::round(params.xLen / params.dx)); + const int ny = static_cast(std::round(params.yLen / params.dy)); + const int nz = std::max(static_cast(std::round(params.zLen / params.dz)), 1); + const int N = nx * ny * nz; + + if (static_cast(odfComponents.size()) != static_cast(params.volumeFractions.size())) { + throw std::invalid_argument("simulateMTR: odfComponents count must equal volumeFractions count"); + } + + auto cancelled = [&]() { return observer != nullptr && observer->shouldCancel(); }; + auto report = [&](int64_t done, int64_t total, const std::string& msg) { + if (observer != nullptr) { observer->updateProgress(done, total, msg); } + }; + + // 1. PGRF assignment. + report(0, 100, "Running plurigaussian field simulation"); + PGRFSimulation pgrf{rng}; + const PGRFResult pgrf_result = pgrf.run(params, observer); + if (cancelled()) { MTRSimResult out; out.cancelled = true; return out; } + if (static_cast(pgrf_result.mtrIndex.size()) != N) { + throw std::runtime_error("simulateMTR: PGRF result size does not match grid dimensions"); + } + + // 2. Sample N orientations per component. + const ODFComponent uniformOdf = buildUniformODF(n1, nPHI, n2); + const int numComponents = static_cast(odfComponents.size()); + std::vector orientSamples(static_cast(numComponents)); + ODFSampler sampler{rng}; + for (int j = 0; j < numComponents; ++j) { + report(j, numComponents, fmt::format("Sampling orientations (component {}/{})", j + 1, numComponents)); + orientSamples[static_cast(j)] = sampler.sampleN(N, odfComponents[static_cast(j)], uniformOdf, observer); + if (cancelled()) { MTRSimResult out; out.cancelled = true; return out; } + } + + // 3. Per-voxel assignment (sim order). + std::vector mtrSim(N); + std::vector phi1Sim(N), phiSim(N), phi2Sim(N); + for (int i = 0; i < N; ++i) { + const int comp = pgrf_result.mtrIndex[i] - 1; + mtrSim[i] = pgrf_result.mtrIndex[i]; + phi1Sim[i] = orientSamples[static_cast(comp)](i, 0); + phiSim[i] = orientSamples[static_cast(comp)](i, 1); + phi2Sim[i] = orientSamples[static_cast(comp)](i, 2); + } + + // 4. Remap to SIMPLNX z,y,x order. + report(100, 100, "Finalizing microstructure"); + MTRSimResult out; + out.nx = nx; out.ny = ny; out.nz = nz; + out.mtrIndex = remapSimToZYX(mtrSim, nx, ny, nz); + out.phi1 = remapSimToZYX(phi1Sim, nx, ny, nz); + out.phi = remapSimToZYX(phiSim, nx, ny, nz); + out.phi2 = remapSimToZYX(phi2Sim, nx, ny, nz); + return out; +} +``` + +Ensure `#include "ISimulationObserver.hpp"` and `#include ` are present in the .cpp. The `sampleN(..., observer)` 4-arg form is added in Task A3; until then call it with the existing 3-arg form and add `observer` in A3 — OR implement A3 first. **Implement A3 before building A2 to completion** (the `sampleN` overload is needed). + +- [ ] **Step 5: Add the cancel test** to `tests/test_mtrsim_driver.cpp` (reuse the existing `[mtrsim_driver][statistical]` setup; add a `RecordingObserver`-style local class or include from a shared header). Add: + +```cpp +#include "ISimulationObserver.hpp" + +namespace { +class CancelAfterObserver : public mtrsim::ISimulationObserver { +public: + explicit CancelAfterObserver(int k) : m_K(k) {} + void updateProgress(int64_t, int64_t, const std::string&) override { ++m_Count; } + bool shouldCancel() const override { return m_Count >= m_K; } + int count() const { return m_Count; } +private: + int m_K; mutable int m_Count = 0; +}; +} + +TEST_CASE("simulateMTR cancels early when observer requests it", "[mtrsim_driver]") { + mtrsim::SimulationParams params; + params.xLen = 6.0; params.yLen = 6.0; params.zLen = 0.0; + params.dx = 0.02; params.dy = 0.02; params.dz = 0.02; + params.volumeFractions = {0.30, 0.35, 0.35}; + params.thetaList = {{0.10, 0.45, 0.10}, {0.08, 0.37, 0.08}}; + params.seed = 42; + std::vector comps = { + mtrsim::buildUniformODF(72, 36, 72), mtrsim::buildUniformODF(72, 36, 72), mtrsim::buildUniformODF(72, 36, 72)}; + std::mt19937_64 rng(params.seed); + CancelAfterObserver obs(1); // cancel at the first progress checkpoint + const mtrsim::MTRSimResult r = mtrsim::simulateMTR(params, comps, rng, 72, 36, 72, &obs); + REQUIRE(r.cancelled); + REQUIRE(r.mtrIndex.empty()); // no full output produced +} + +TEST_CASE("simulateMTR with nullptr observer is unaffected", "[mtrsim_driver]") { + mtrsim::SimulationParams params; + params.xLen = 2.0; params.yLen = 2.0; params.zLen = 0.0; + params.dx = 0.02; params.dy = 0.02; params.dz = 0.02; + params.volumeFractions = {0.30, 0.35, 0.35}; + params.thetaList = {{0.10, 0.45, 0.10}, {0.08, 0.37, 0.08}}; + std::mt19937_64 rng(7); + const mtrsim::MTRSimResult r = mtrsim::simulateMTR(params, comps_or_uniform(), rng, 72, 36, 72); + // build comps inline: + REQUIRE_FALSE(r.cancelled); +} +``` + +Replace `comps_or_uniform()` with three `mtrsim::buildUniformODF(72,36,72)` entries built locally (repeat the vector). Keep both tests self-contained. + +- [ ] **Step 6: Build, run `[mtrsim_driver]` + `[observer]`, verify pass; commit** + +```bash +git add src/LibMTRSim/MTRSimDriver.hpp src/LibMTRSim/MTRSimDriver.cpp \ + src/LibMTRSim/PGRFSimulation.hpp src/LibMTRSim/PGRFSimulation.cpp tests/test_mtrsim_driver.cpp +git commit -m "feat(lib): thread ISimulationObserver through simulateMTR + PGRFSimulation" +``` + +--- + +### Task A3: deep cancel/progress in `ODFSampler::sampleN` + +**Files:** Modify `src/LibMTRSim/ODFSampler.{hpp,cpp}`, `tests/test_odf_sampler.cpp`. + +- [ ] **Step 1: Add observer param (header)** — in `src/LibMTRSim/ODFSampler.hpp` add `#include "ISimulationObserver.hpp"` and change `sampleN`: + +```cpp +Eigen::MatrixXd sampleN(int n, const ODFComponent& component, const ODFComponent& uniform, ISimulationObserver* observer = nullptr); +``` + +- [ ] **Step 2: Add in-loop cancel checks (impl)** — in `src/LibMTRSim/ODFSampler.cpp`, update the signature and add a throttled cancel check inside the sampling loop (`for i in [0,n)` at line 56) and the assignment loop (line 77). Check every 4096 iterations to keep it cheap; on cancel, return an empty matrix (the caller treats an empty/short result as cancellation via `observer->shouldCancel()`): + +```cpp + constexpr int kCheck = 4096; + for (int i = 0; i < n; ++i) { + if (observer != nullptr && (i % kCheck) == 0 && observer->shouldCancel()) { + return Eigen::MatrixXd(0, 3); + } + // ... existing sampling body ... + } +``` +Apply the same `if (observer != nullptr && (i % kCheck) == 0 && observer->shouldCancel()) { return Eigen::MatrixXd(0, 3); }` guard at the top of the assignment loop body. Do NOT change the RNG draw order on the non-cancel path (the modulo check must not consume RNG), so the regression test stays bit-stable. + +> Note: `simulateMTR` (Task A2) calls `observer->shouldCancel()` right after each `sampleN` and sets `cancelled`. A short/empty matrix from a cancelled `sampleN` is therefore never consumed. + +- [ ] **Step 3: Add a test** to `tests/test_odf_sampler.cpp`: + +```cpp +#include "ISimulationObserver.hpp" + +namespace { +class ImmediateCancel : public mtrsim::ISimulationObserver { +public: + void updateProgress(int64_t, int64_t, const std::string&) override {} + bool shouldCancel() const override { return true; } +}; +} + +TEST_CASE("sampleN bails out promptly when observer cancels", "[odf_sampler]") { + mtrsim::ODFComponent uni = mtrsim::buildUniformODF_or_local(); // see note + // Build a uniform component directly if buildUniformODF isn't linked here. + std::mt19937_64 rng(1); + mtrsim::ODFSampler sampler{rng}; + ImmediateCancel cancel; + Eigen::MatrixXd out = sampler.sampleN(100000, uni, uni, &cancel); + REQUIRE(out.rows() == 0); +} +``` + +If `buildUniformODF` is not available to this test translation unit, construct a small valid `ODFComponent` inline: set `odfVal`, `phi1Bins`, `phiBins`, `phi2Bins` each to a `Eigen::VectorXd` of equal length (e.g. 10) with positive `odfVal` and increasing bin centres. Replace `buildUniformODF_or_local()` accordingly. (Check whether `test_odf_sampler.cpp` already builds a component it can reuse.) + +- [ ] **Step 4: Build, run `[odf_sampler]` + `[mtrsim_driver]` (regression), verify pass** + +Run: `cmake --build /Users/mjackson/Workspace7/Build/mtrsim-Rel && /Users/mjackson/Workspace7/Build/mtrsim-Rel/bin/mtrsim_tests "[odf_sampler],[mtrsim_driver]"` +Expected: PASS, including the pre-existing statistical test (proves the cancel-check modulo didn't perturb the RNG path). + +- [ ] **Step 5: Commit** + +```bash +git add src/LibMTRSim/ODFSampler.hpp src/LibMTRSim/ODFSampler.cpp tests/test_odf_sampler.cpp +git commit -m "feat(lib): in-loop cancel checks in ODFSampler::sampleN" +``` + +--- + +### Task A4: standalone CLI uses `ConsoleObserver` + +**Files:** Modify `src/app/main.cpp`. + +- [ ] **Step 1: Pass a ConsoleObserver into simulateMTR** — in `src/app/main.cpp`, add `#include "SimulationObservers.hpp"`, and change the `simulateMTR(...)` call to: + +```cpp + mtrsim::ConsoleObserver observer; + mtrsim::MTRSimResult sim = mtrsim::simulateMTR(params, odfComponents, rng, k_OdfBinsPhi1, k_OdfBinsPHI, k_OdfBinsPhi2, &observer); +``` +(Use the existing `k_OdfBins*` constants. If `sim.cancelled` is true, log a warning and skip the CSV/PNG; the CLI never cancels but handle it for completeness.) + +- [ ] **Step 2: Build the app, run it on the smoke config, verify clean** + +Run: `cmake --build /Users/mjackson/Workspace7/Build/mtrsim-Rel` then run the `MTRsim` binary: `/Users/mjackson/Workspace7/Build/mtrsim-Rel/bin/MTRsim -c configs/smoke_test.json -o /tmp/mtr_a4 --seed 42` (create `/tmp/mtr_a4` first). +Expected: exit 0, progress `[ NN%]` lines logged, CSV/PNG written. + +- [ ] **Step 3: Commit** + +```bash +git add src/app/main.cpp +git commit -m "feat(app): standalone driver reports progress via ConsoleObserver" +``` + +--- + +## Phase B — Shared config parser (LibMTRSim) + +### Task B1: `parseConfigJson` + +**Files:** Create `src/LibMTRSim/ConfigIO.hpp`, `src/LibMTRSim/ConfigIO.cpp`, `tests/test_config_io.cpp`; Modify `src/LibMTRSim/CMakeLists.txt`, `MTRSimPlugin.cmake`, `tests/CMakeLists.txt`. + +- [ ] **Step 1: Create `src/LibMTRSim/ConfigIO.hpp`** + +```cpp +#pragma once + +#include "SimulationParams.hpp" +#include "libmtrsim_export.h" + +#include + +namespace mtrsim { + +/** + * @brief Parse an MTRSim config JSON into a SimulationParams. + * + * Recognized keys: xLen,yLen,zLen, dx,dy,dz, volumeFractions, thetaList, seed. + * Unknown keys (odfInputPath, nuggetVariance, comments) are ignored. Fields + * absent from the JSON keep their SimulationParams defaults. + * + * @throws std::runtime_error if the file cannot be opened or the JSON is invalid. + */ +LIBMTRSIM_EXPORT SimulationParams parseConfigJson(const std::filesystem::path& path); + +} // namespace mtrsim +``` + +- [ ] **Step 2: Create `src/LibMTRSim/ConfigIO.cpp`** (logic lifted from `main.cpp:153-179`) + +```cpp +#include "ConfigIO.hpp" + +#include + +#include +#include + +namespace mtrsim { + +SimulationParams parseConfigJson(const std::filesystem::path& path) { + std::ifstream f(path); + if (!f.is_open()) { + throw std::runtime_error("parseConfigJson: cannot open config file: " + path.string()); + } + SimulationParams params; + try { + const nlohmann::json j = nlohmann::json::parse(f); + if (j.contains("xLen")) params.xLen = j["xLen"].get(); + if (j.contains("yLen")) params.yLen = j["yLen"].get(); + if (j.contains("zLen")) params.zLen = j["zLen"].get(); + if (j.contains("dx")) params.dx = j["dx"].get(); + if (j.contains("dy")) params.dy = j["dy"].get(); + if (j.contains("dz")) params.dz = j["dz"].get(); + if (j.contains("volumeFractions")) params.volumeFractions = j["volumeFractions"].get>(); + if (j.contains("thetaList")) params.thetaList = j["thetaList"].get>>(); + if (j.contains("nuggetVariance")) params.nuggetVariance = j["nuggetVariance"].get>(); + if (j.contains("seed")) params.seed = j["seed"].get(); + } catch (const nlohmann::json::exception& e) { + throw std::runtime_error(std::string("parseConfigJson: invalid JSON: ") + e.what()); + } + return params; +} + +} // namespace mtrsim +``` + +> `nuggetVariance` is parsed for fidelity but unused by the simulation; `odfInputPath` is intentionally NOT applied (the filter/caller supplies the ODF separately). + +- [ ] **Step 3: Register in CMake** — add `ConfigIO.cpp`/`.hpp` to `src/LibMTRSim/CMakeLists.txt` (source + header lists) and `MTRSimPlugin.cmake` (the `${${PLUGIN_NAME}_SOURCE_DIR}/src/LibMTRSim/ConfigIO.cpp` source + `.hpp` header). + +- [ ] **Step 4: Write the failing test** — create `tests/test_config_io.cpp`: + +```cpp +#include "ConfigIO.hpp" + +#include + +#include +#include +#include + +namespace { +std::string writeTemp(const std::string& contents) { + const std::string path = std::string(MTRSIM_TEST_DATA_DIR) + "/_tmp_config_io.json"; + std::ofstream o(path); o << contents; o.close(); + return path; +} +} + +TEST_CASE("parseConfigJson reads known fields and ignores extras", "[config_io]") { + const std::string path = writeTemp(R"({ + "xLen": 38.1, "yLen": 12.7, "zLen": 0.0, + "dx": 0.02, "dy": 0.02, "dz": 0.02, + "volumeFractions": [0.30, 0.35, 0.35], + "thetaList": [[0.10,0.45,0.10],[0.08,0.37,0.08]], + "odfInputPath": "ignored.h5", "nuggetVariance": [0.6,0.7,0.7], "seed": 99 + })"); + const mtrsim::SimulationParams p = mtrsim::parseConfigJson(path); + REQUIRE(p.xLen == Approx(38.1)); + REQUIRE(p.dx == Approx(0.02)); + REQUIRE(p.volumeFractions.size() == 3); + REQUIRE(p.volumeFractions[1] == Approx(0.35)); + REQUIRE(p.thetaList.size() == 2); + REQUIRE(p.thetaList[0][1] == Approx(0.45)); + REQUIRE(p.seed == 99); + std::remove(path.c_str()); +} + +TEST_CASE("parseConfigJson throws on missing file", "[config_io]") { + REQUIRE_THROWS_AS(mtrsim::parseConfigJson("/nonexistent/path/nope.json"), std::runtime_error); +} + +TEST_CASE("parseConfigJson throws on malformed JSON", "[config_io]") { + const std::string path = writeTemp("{ this is not json"); + REQUIRE_THROWS_AS(mtrsim::parseConfigJson(path), std::runtime_error); + std::remove(path.c_str()); +} +``` + +Add `test_config_io.cpp` to `tests/CMakeLists.txt`. (`MTRSIM_TEST_DATA_DIR` is already defined for the test target.) + +- [ ] **Step 5: Build, run `[config_io]`, verify FAIL→PASS; commit** + +```bash +git add src/LibMTRSim/ConfigIO.hpp src/LibMTRSim/ConfigIO.cpp \ + src/LibMTRSim/CMakeLists.txt MTRSimPlugin.cmake tests/test_config_io.cpp tests/CMakeLists.txt +git commit -m "feat(lib): add shared parseConfigJson config reader" +``` + +--- + +### Task B2: standalone CLI uses `parseConfigJson` + +**Files:** Modify `src/app/main.cpp`. + +- [ ] **Step 1: Replace the inline JSON parse** — in `src/app/main.cpp`, add `#include "ConfigIO.hpp"`. Replace the inline block (`main.cpp:153-179`) that parses the config into `params` with: + +```cpp + try { + mtrsim::SimulationParams cfg = mtrsim::parseConfigJson(configPath); + cfg.outputDir = params.outputDir; // keep CLI-provided output dir + if (seed != 0) { cfg.seed = seed; } // CLI --seed overrides JSON seed + params = cfg; + } catch (const std::exception& e) { + spdlog::error("{}", e.what()); + return 1; + } +``` +Preserve the existing CLI semantics: `--seed` (non-zero) overrides the JSON seed; otherwise the JSON seed is used. Remove the now-unused `nlohmann/json.hpp` include from `main.cpp` only if nothing else there needs it (the HDF5 ODF loader doesn't); otherwise leave it. + +- [ ] **Step 2: Build + run on default and smoke configs; verify identical behavior** + +Run the app on `configs/smoke_test.json` and confirm exit 0 and the same grid/output as before this change. + +- [ ] **Step 3: Commit** + +```bash +git add src/app/main.cpp +git commit -m "refactor(app): standalone driver uses shared parseConfigJson" +``` + +--- + +## Phase C — Filter (config-file mode + filter observer) + +### Task C1: `FilterObserver` in the algorithm; pass observer to `simulateMTR` + +**Files:** Modify `src/MTRSim/Filters/Algorithms/MTRSim.{hpp,cpp}`. + +- [ ] **Step 1: Declare a private FilterObserver** — in `src/MTRSim/Filters/Algorithms/MTRSim.cpp` (anonymous namespace at top, after includes), add an observer adapting to the message handler + cancel flag. Add `#include "LibMTRSim/ISimulationObserver.hpp"`: + +```cpp +namespace { +class FilterObserver : public mtrsim::ISimulationObserver { +public: + FilterObserver(const nx::core::IFilter::MessageHandler& mh, const std::atomic_bool& cancel) + : m_MessageHandler(mh), m_ShouldCancel(cancel) {} + void updateProgress(int64_t done, int64_t total, const std::string& message) override { + const int32_t pct = (total > 0) ? static_cast(done * 100 / total) : 0; + m_MessageHandler(nx::core::IFilter::ProgressMessage{nx::core::IFilter::Message::Type::Progress, message, pct}); + } + bool shouldCancel() const override { return m_ShouldCancel.load(); } +private: + const nx::core::IFilter::MessageHandler& m_MessageHandler; + const std::atomic_bool& m_ShouldCancel; +}; +} +``` +> Verify the exact `ProgressMessage` construction against a simplnx filter that emits progress (grep `ProgressMessage` in `/Users/mjackson/Workspace7/simplnx/src/Plugins`); adjust the struct/arg form to match the real API. See the `progress-messaging` skill conventions. + +- [ ] **Step 2: Use the observer in `operator()`** — in `MTRSim::operator()`, construct `FilterObserver observer{m_MessageHandler, m_ShouldCancel};` and pass `&observer` to `mtrsim::simulateMTR(params, components, rng, n1, nPHI, n2, &observer);`. After the call, replace the existing `if (m_ShouldCancel) { return {}; }` so it also covers `sim.cancelled`: + +```cpp + if (m_ShouldCancel || sim.cancelled) { return {}; } +``` + +- [ ] **Step 3: Build plugin + run MTRSim tests; verify pass; commit** + +Run: `cmake --build /Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib --target MTRSimUnitTest && ctest --test-dir /Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib -R "MTRSim::" --output-on-failure` +Expected: all MTRSim tests PASS. + +```bash +git add src/MTRSim/Filters/Algorithms/MTRSim.hpp src/MTRSim/Filters/Algorithms/MTRSim.cpp +git commit -m "feat(filter): report progress + honor cancel via FilterObserver" +``` + +--- + +### Task C2: config-file parameters + linked show/hide + +**Files:** Modify `src/MTRSim/Filters/MTRSimFilter.{hpp,cpp}`. + +- [ ] **Step 1: Add parameter keys (header)** — in `src/MTRSim/Filters/MTRSimFilter.hpp` add: + +```cpp + static constexpr StringLiteral k_UseConfigFile_Key = "use_config_file"; + static constexpr StringLiteral k_ConfigFilePath_Key = "config_file_path"; +``` + +- [ ] **Step 2: Add the parameters + linking (parameters())** — in `MTRSimFilter.cpp` `parameters()`, add includes `#include "simplnx/Parameters/FileSystemPathParameter.hpp"` (already used by Read filter) and, in a new separator before the simulation parameters: + +```cpp + params.insertSeparator(Parameters::Separator{"Configuration Source"}); + params.insertLinkableParameter(std::make_unique(k_UseConfigFile_Key, "Load Simulation Parameters from Config File", + "When ON, read volume fractions, theta list, physical size/spacing, and seed from an MTRSim JSON config file instead of the fields below.", false)); + params.insert(std::make_unique(k_ConfigFilePath_Key, "MTRSim Config File (JSON)", + "MTRSim configuration JSON (same schema as the standalone tool). odfInputPath and nuggetVariance are ignored.", fs::path(""), + FileSystemPathParameter::ExtensionsType{".json"}, FileSystemPathParameter::PathType::InputFile)); +``` + +Then add link calls (near the existing `linkParameters` block): + +```cpp + params.linkParameters(k_UseConfigFile_Key, k_ConfigFilePath_Key, true); + params.linkParameters(k_UseConfigFile_Key, k_VolumeFractions_Key, false); + params.linkParameters(k_UseConfigFile_Key, k_ThetaList_Key, false); + params.linkParameters(k_UseConfigFile_Key, k_PhysicalSize_Key, false); + params.linkParameters(k_UseConfigFile_Key, k_PhysicalSpacing_Key, false); + params.linkParameters(k_UseConfigFile_Key, k_UseSeed_Key, false); +``` +> Verify nested linking: `k_UseSeed_Key` already links `k_SeedValue_Key`. If a parameter may be the dependent of one linkable AND the linkable of another, confirm simplnx honors it (grep simplnx for a filter doing this; otherwise, link `k_SeedValue_Key` and `k_SeedArrayName_Key` to `k_UseConfigFile_Key=false` directly instead of relying on the `use_seed` chain, and accept that in config mode the seed group is simply hidden). Whichever works, the user-visible result must be: in config mode the seed group is hidden. + +- [ ] **Step 3: Build plugin; run FilterValidationTest** — the new keys (`use_config_file` bool, `config_file_path` ends with `_path`) must satisfy `FilterValidationTest`. + +Run: `cmake --build /Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib --target simplnx_test && ctest --test-dir /Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib -R "FilterValidation" --output-on-failure` +Expected: PASS (no MTRSim parameter-naming violations). If `simplnx_test`/`FilterValidation` target names differ, find them: `ctest --test-dir -N | grep -i validation`. + +- [ ] **Step 4: Commit** + +```bash +git add src/MTRSim/Filters/MTRSimFilter.hpp src/MTRSim/Filters/MTRSimFilter.cpp +git commit -m "feat(filter): add config-file source toggle + linked show/hide" +``` + +--- + +### Task C3: config-mode preflight + execute + +**Files:** Modify `src/MTRSim/Filters/MTRSimFilter.{hpp,cpp}`, `src/MTRSim/Filters/Algorithms/MTRSim.{hpp,cpp}`. + +- [ ] **Step 1: Thread config values into the algorithm input** — in `src/MTRSim/Filters/Algorithms/MTRSim.hpp`, add to `MTRSimInputValues`: + +```cpp + bool useConfigFile = false; + std::filesystem::path configFilePath; +``` +(add `#include `). + +- [ ] **Step 2: Centralize "resolve SimulationParams" in the algorithm** — in `MTRSim.cpp`, where `SimulationParams params;` is built, branch on config mode. Add `#include "LibMTRSim/ConfigIO.hpp"`: + +```cpp + mtrsim::SimulationParams params; + if (m_InputValues->useConfigFile) { + params = mtrsim::parseConfigJson(m_InputValues->configFilePath); // throws -> caught below + // size/spacing/volumeFractions/thetaList/seed all come from the file + } else { + params.xLen = m_InputValues->physicalSize[0]; + params.yLen = m_InputValues->physicalSize[1]; + params.zLen = m_InputValues->physicalSize[2]; + params.dx = m_InputValues->physicalSpacing[0]; + params.dy = m_InputValues->physicalSpacing[1]; + params.dz = m_InputValues->physicalSpacing[2]; + params.volumeFractions = m_InputValues->volumeFractions[0]; + params.thetaList = m_InputValues->thetaList; + params.seed = m_InputValues->seed; + } +``` +Wrap the parse in the existing try/catch (or add one) so a parse error returns `MakeErrorResult(-13520, ...)`. The seed in config mode comes from the JSON; the seed-array write in the FILTER's `executeImpl` should record `params.seed` actually used (see Step 4). + +- [ ] **Step 3: Preflight reads the config to validate + size the geometry** — in `MTRSimFilter.cpp` `preflightImpl`, at the top, branch: + +```cpp + const bool useConfig = filterArgs.value(k_UseConfigFile_Key); + std::vector> volumeFractions; + std::vector> thetaList; + std::vector size, spacing; + if (useConfig) { + mtrsim::SimulationParams cfg; + try { + cfg = mtrsim::parseConfigJson(filterArgs.value(k_ConfigFilePath_Key)); + } catch (const std::exception& e) { + return {MakeErrorResult(-13520, fmt::format("MTRSim config file error: {}", e.what()))}; + } + volumeFractions = {cfg.volumeFractions}; + thetaList = cfg.thetaList; + size = {static_cast(cfg.xLen), static_cast(cfg.yLen), static_cast(cfg.zLen)}; + spacing = {static_cast(cfg.dx), static_cast(cfg.dy), static_cast(cfg.dz)}; + } else { + volumeFractions = filterArgs.value(k_VolumeFractions_Key); + thetaList = filterArgs.value(k_ThetaList_Key); + size = filterArgs.value>(k_PhysicalSize_Key); + spacing = filterArgs.value>(k_PhysicalSpacing_Key); + } +``` +Then the EXISTING validation (VF count == numComponents, sum ≈ 1, in [0,1], theta rows ≥ components−1 with 3 cols, spacing X/Y > 0) and grid-dim computation run against these local `volumeFractions/thetaList/size/spacing` variables instead of the direct `filterArgs` reads. Add a preflight info note when `useConfig`: `preflightUpdatedValues.push_back({"Parameter Source", "Config file: " + path.string()})`. Include `#include "LibMTRSim/ConfigIO.hpp"`. + +- [ ] **Step 4: executeImpl passes config fields + records the right seed** — in `MTRSimFilter.cpp` `executeImpl`, set `inputValues.useConfigFile` and `inputValues.configFilePath`. For the seed-array record: in config mode the seed comes from the JSON, so read it for the array write: + +```cpp + inputValues.useConfigFile = filterArgs.value(k_UseConfigFile_Key); + inputValues.configFilePath = filterArgs.value(k_ConfigFilePath_Key); + + uint64 seed; + if (inputValues.useConfigFile) { + seed = mtrsim::parseConfigJson(inputValues.configFilePath).seed; // file is validated in preflight + } else { + seed = filterArgs.value(k_SeedValue_Key); + if (!filterArgs.value(k_UseSeed_Key)) { + seed = static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); + } + } + dataStructure.getDataRefAs(DataPath({filterArgs.value(k_SeedArrayName_Key)}))[0] = seed; + inputValues.seed = seed; +``` +The algorithm builds `params` from the config in config mode (Step 2) but uses `m_InputValues->seed` for the RNG. **Make Step 2 use `m_InputValues->seed` for the rng regardless of mode** (the filter already resolved the correct seed): keep the line `std::mt19937_64 rng(m_InputValues->seed);` and, in config mode, after `params = parseConfigJson(...)`, set `params.seed = m_InputValues->seed;` for consistency (it isn't read by simulateMTR but keeps the struct truthful). + +- [ ] **Step 5: Write filter tests** — add to `test/MTRSimTest.cpp`: + - **Config-mode preflight VALID:** build the ODF DataStructure (3 components), set `k_UseConfigFile_Key=true`, `k_ConfigFilePath_Key` = an absolute path to `configs/default.json` (compose from a known repo path or a test-data macro — mirror how other MTRSim tests locate files; if none, write a temp 3-component config in the test). Assert preflight VALID and that the output `MTRIds`/`Eulers` arrays are created with the config-derived grid (1905×635 for default.json → check tuple count `1905*635`). + - **Config-mode missing file → INVALID (−13520):** set `use_config_file=true`, `config_file_path` to a nonexistent path; assert preflight INVALID. + - **Config-mode VF/component mismatch → INVALID:** point at a config whose `volumeFractions` length ≠ selected component count (write a temp 2-fraction config but select 3 components); assert INVALID. + - Keep an existing manual-mode test to prove no regression. + +```cpp +// sketch of the valid case +args.insertOrAssign(MTRSimFilter::k_UseConfigFile_Key, true); +args.insertOrAssign(MTRSimFilter::k_ConfigFilePath_Key, fs::path("/Users/mjackson/Workspace7/DREAM3D_Plugins/MTRSim/configs/default.json")); +auto pre = filter.preflight(ds, args); +SIMPLNX_RESULT_REQUIRE_VALID(pre.outputActions); +``` +> Prefer writing a small temp config inside the test (with 3 fractions and a tiny size like 1.0×0.6) over depending on an absolute repo path, so the test is portable. Use a `std::filesystem::temp_directory_path()`-based temp file. + +- [ ] **Step 6: Build, run MTRSim tests, verify pass; commit** + +Run: `cmake --build /Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib --target MTRSimUnitTest && ctest --test-dir /Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib -R "MTRSim::" --output-on-failure` +Expected: all PASS. + +```bash +git add src/MTRSim/Filters/MTRSimFilter.hpp src/MTRSim/Filters/MTRSimFilter.cpp \ + src/MTRSim/Filters/Algorithms/MTRSim.hpp src/MTRSim/Filters/Algorithms/MTRSim.cpp test/MTRSimTest.cpp +git commit -m "feat(filter): config-file mode in preflight + execute, with tests" +``` + +--- + +## Phase D — Docs + integration + +### Task D1: documentation + +**Files:** Modify `docs/MTRSimFilter.md`. + +- [ ] **Step 1: Document the two new behaviors** — add a "Configuration Source" subsection under Description explaining the `Load Simulation Parameters from Config File` toggle (which params it replaces, that `odfInputPath`/`nuggetVariance` are ignored, output names/ODF still come from the UI). Update the Performance section to note that progress is now reported continuously and the filter can be cancelled mid-run. Add error `-13520` (config file missing/invalid) to the error table. Commit. + +```bash +git add docs/MTRSimFilter.md +git commit -m "docs: document config-file mode and progress/cancel in MTRSimFilter" +``` + +### Task D2: debug smoke + validation sweep + +- [ ] **Step 1: Standalone dual check** — `cmake --build /Users/mjackson/Workspace7/Build/mtrsim-Rel && ctest --test-dir /Users/mjackson/Workspace7/Build/mtrsim-Rel --output-on-failure`. Expected: all library tests (including `[observer]`, `[config_io]`) PASS. +- [ ] **Step 2: Plugin tests + FilterValidationTest** — build `MTRSimUnitTest` and `simplnx_test`; run `ctest -R "MTRSim::"` and `ctest -R "FilterValidation"`. Expected: PASS. +- [ ] **Step 3: Debug smoke execution** — build the Debug `MTRSim` + `nxrunner`, then `/Bin/nxrunner_d --execute pipelines/MTRSim_smoke_test.d3dpipeline`. Expected: clean run, no Eigen asserts, progress lines visible. Also add a tiny config-mode pipeline variant or temporarily flip `use_config_file` in a copy to exercise the config path once in Debug. +- [ ] **Step 4: clang-format** — the user runs a clang-format script before pushing; ensure new files are included. (Format C++ sources if a `clang-format` binary is available; otherwise note that the new files need formatting before push.) + +--- + +## Self-Review Notes (for the executor) + +- **Spec coverage:** interface (A1), simulateMTR/PGRF threading (A2), deep sampleN cancel (A3), default observers + CLI (A1/A4), `MTRSimResult::cancelled` (A2), filter observer (C1), config params + linking (C2), config-mode preflight/execute + parser (B1/B2/C3), tests (each task + D2), docs (D1). All spec sections map to a task. +- **APIs to verify against the live tree** (evolve over time): `IFilter::ProgressMessage` construction (C1), nested `linkParameters` behavior (C2), `FileSystemPathParameter::ValueType`/ctor (C2/C3), `SIMPLNX_RESULT_REQUIRE_VALID/INVALID` macros, the exact `simplnx_test`/`FilterValidation` ctest names (C2/D2). Grep the local simplnx at `/Users/mjackson/Workspace7/simplnx` and mirror a sibling. +- **Bit-stability guard:** the A3 modulo cancel check must not consume RNG on the non-cancel path; the pre-existing `[mtrsim_driver][statistical]` test is the regression guard (A3 Step 4). +- **Back-compat:** every new observer arg defaults to `nullptr`; existing callers/tests compile unchanged. diff --git a/docs/superpowers/specs/2026-06-01-milestone-ak-design.md b/docs/superpowers/specs/2026-06-01-milestone-ak-design.md new file mode 100644 index 0000000..4fa8b13 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-milestone-ak-design.md @@ -0,0 +1,219 @@ +# Milestone AK — Integration of MTR Representation Codes into DREAM3D-NX Filters + +**Date:** 2026-06-01 +**Status:** Design approved — ready for implementation plan +**Task reference:** `.claude/mtr_sbir_tasks.md` → Milestone AK (3.2.3 Task 3) + +--- + +## 1. Purpose & Deliverables + +Milestone AK exposes the MTR microtexture-representation building capability — +already ported to C++ in `LibMTRSim` (Milestone AH) and wrapped with ODF +import/export/compute filters (Milestone AJ) — to end users through a single +DREAM3D-NX pipeline filter, backed by automated tests and continuous +integration. + +Three things ship together: + +- **A. The filter** — one new DREAM3D-NX filter that runs the full MTR + simulation and writes the results into the DataStructure. +- **B. Tests** — unit tests that exercise the *filter's* value-added code paths + (the library is already tested in `tests/`), including a statistical + end-to-end test that reuses the LibMTRSim exemplar/seed, plus preflight/error + tests. +- **C. CI** — GitHub Actions workflows (linux, macOS, Windows, clang-format) + that build the full `simplnx` + `MTRSim` plugin and run `ctest` automatically + on push/PR. **Already added** under `.github/`; this milestone validates it + runs green. + +A narrative **report** (per the project-wide convention) is written after the +work is implemented and verified. + +### Acceptance criteria mapping + +> "All new code paths shall be exercised by passing tests in the CI +> environment, and the framework shall be configured to run automatically upon +> code changes within the private repository." + +- New code paths = the `MTRSim` filter + algorithm → covered by **B**. +- "Run automatically upon code changes" → covered by **C** (triggers on + push/PR to `develop`/`main`). + +--- + +## 2. The Filter + +| Field | Value | +|---|---| +| Filter class | `MTRSimFilter` | +| Algorithm class | `MTRSim` (in `Filters/Algorithms/`) | +| Human name | **Generate Synthetic Microtexture** | +| Location | `src/MTRSim/Filters/` | + +### 2.1 Parameters (inputs) + +| Parameter | Type | Notes / preflight rules | +|---|---|---| +| Input ODF Geometry | `GeometrySelectionParameter` (ImageGeom) | The ODF Image Geometry built by the AJ Read/Compute ODF filters. Bin sizes are read from the geometry's spacing (degrees). | +| ODF Component Arrays | `MultiArraySelectionParameter` (Float64) | Explicit, **ordered** list of the per-component cell arrays. Count defines `numComponents`. Order is significant — index `j` maps to `volumeFractions[j]`. | +| Volume Fraction | `DynamicTableParameter` — 1 fixed row × N cols | Preflight: column count == `numComponents`; values sum ≈ 1.0 (tolerance). Converted to `float`/`double`. | +| Theta List | `DynamicTableParameter` — 3 fixed cols × M rows | Preflight: `M >= numComponents - 1`. Columns are `[theta_x, theta_y, theta_z]` correlation lengths. | +| Physical Size | `VectorFloat32Parameter` (FloatVec3), µm | Domain extent. | +| Physical Spacing | `VectorFloat32Parameter` (FloatVec3), µm | Voxel spacing. | +| Use Seed for Random Generation | `BoolParameter` (default `false`), linkable | Standard simplnx seed pattern (see below). | +| Seed Value | `NumberParameter` (default `std::mt19937::default_seed`) | Linked to "Use Seed"; enabled when it is on. | +| Stored Seed Value Array Name | `DataObjectNameParameter` (default `"MTRSim SeedValue"`) | Top-level UInt64 array that records the seed actually used. | +| Generate Polar Coloring | `BoolParameter` (default `false`) | Gates creation of the RGB output array. | +| *nuggetVariance* | — | **Not exposed.** Unused by the simulation. | + +> **Random seed pattern.** Follow the established simplnx convention (e.g. +> `MergeTwinsFilter`): a linkable `BoolParameter` "Use Seed for Random +> Generation" gates a `NumberParameter` "Seed Value", with +> `params.linkParameters(k_UseSeed_Key, k_SeedValue_Key, true)`. In +> `executeImpl`, if "Use Seed" is off the seed is taken from +> `std::chrono::steady_clock::now().time_since_epoch().count()`. The seed +> actually used is written into a top-level UInt64 array (created in preflight +> via `CreateArrayAction`, named by "Stored Seed Value Array Name") for +> reproducibility, then passed to `std::mt19937_64`. + +> **Units note:** `Physical Size`, `Physical Spacing`, and the `Theta List` +> correlation lengths must share the same length unit. Internally the +> simulation only uses the dimensionless ratio `lag / theta`, so the absolute +> unit is irrelevant *as long as it is consistent*. The MATLAB defaults were +> mm-scale; document this clearly so users do not mix µm size with mm theta. + +### 2.2 Outputs + +The filter **creates a new** Image Geometry (it does not write into the ODF +geometry): + +- **Geometry:** default name **`MTR Microstructure`**; dims + `n_i = round(Size_i / Spacing_i)`, origin `(0,0,0)`, spacing = + `Physical Spacing`. Created in preflight via `CreateImageGeometryAction` (all + inputs are parameters, so dims are known at preflight). +- **Cell arrays:** + | Array (default name) | Type | Comps | Notes | + |---|---|---|---| + | `MTRIds` | Int32 | 1 | Values start at **1** (0 reserved, matches FeatureIds convention). | + | `Eulers` | Float32 | 3 | Bunge `phi1, PHI, phi2` [radians]. | + | `Polar Colors` | UInt8 | 3 | RGB. **Created only when** "Generate Polar Coloring" is on. | + +Downstream, users can run the stock **Compute IPF Colors** and **Write Image** +filters for additional visualization; only the bespoke MATLAB polar coloring is +built in. + +--- + +## 3. Algorithm Flow & Technical Concerns + +`MTRSim::operator()` performs: + +1. **Read ODF** — convert the selected Float64 cell arrays + ODF geometry into a + `std::vector`: copy `odfVal`, normalize so each + component sums to 1, and derive `phi1Bins/PHIBins/phi2Bins` (radians) from + the geometry dims + degree-spacing. +2. **Build `SimulationParams`** from the filter parameters (Size→`xLen/yLen/zLen`, + Spacing→`dx/dy/dz`, `volumeFractions`, `thetaList`, `seed`). +3. **PGRF** — `PGRFSimulation::run` → 1-based `mtrIndex` per voxel. +4. **Sample orientations** — `ODFSampler::sampleN` per component against a + uniform reference ODF. +5. **Assign** — per voxel, pick the orientation from the component named by + `mtrIndex`. +6. **Write** MTR Index + Euler arrays into the new geometry's cell arrays. +7. **Polar color** (optional) — `IPFMapper::eulerToColors(..., MatLab)` → + UInt8 RGB. + +### Concerns that drive correctness + +1. **Voxel index remapping (highest risk).** SIMPLNX requires cell data laid + out **`z` (slowest) → `y` → `x` (fastest)** in memory — index + `(z·ny + y)·nx + x`. The standalone driver instead iterates `z → x → y` + (`main.cpp`), producing `k = ((z)·nx + x)·ny + y` — the column-major MATLAB + ordering. The algorithm must remap the simulation's `z,x,y` output into the + SIMPLNX `z,y,x` layout when filling cell arrays, or the field comes out + transposed. This remap gets a dedicated small-grid deterministic test. +2. **`buildUniformODF()` exposure.** Currently in `main.cpp`'s anonymous + namespace, hardcoded to 72×36×72. Move it into `LibMTRSim` and + **parameterize by grid dims** so the uniform reference is derived from the + actual ODF geometry rather than assuming a 5° grid. +3. **Units consistency** — see the units note in §2.1. +4. **ODF→component helper** — a new small building block (Float64 arrays + ODF + geometry → `ODFComponent`), the inverse of AJ's `ReadMTRSimODF` write path. + +--- + +## 4. Testing Strategy + +Philosophy: `LibMTRSim` already has its own unit tests (`tests/`) proving the +simulation's numerical correctness. The **filter** tests target only what the +filter adds on top of the library. + +### 4.1 Component-level, exact / deterministic +- ODF read-back: a known ODF geometry → expected `ODFComponent` (bin centres, + normalized values). +- `buildUniformODF` bin-centre values for given grid dims. +- Voxel index remap on a tiny grid (e.g. 2×3×2) — exact positional check. +- Polar-color LUT: a known Euler triple → expected RGB. + +### 4.2 End-to-end, statistical +- Fixed seed + the **same ODF exemplar and inputs used by the LibMTRSim test**, + so the filter's results are compared against the *same reference data*. This + proves the filter wired the library up correctly. +- Assertions use **remap-invariant** statistics: empirical volume fractions ≈ + targets (tolerance), MTR-index value set `{1..N}`, Euler ranges valid, + per-component mean orientation near the ODF peak. No positional / bit-exact + array comparison (would be fragile across the 3 CI platforms). + +### 4.3 Preflight / error tests +- Volume-fraction column count ≠ `numComponents`. +- Theta rows `< numComponents - 1`. +- Volume fractions do not sum to 1. +- Empty / invalid ODF component selection. + +### 4.4 Exemplar data +Stored compressed in-repo, consistent with the AJ convention. + +--- + +## 5. CI (already added — validate) + +`.github/workflows/` already contains `linux.yml`, `macos.yml`, `windows.yml`, +`format_pr.yml`, `format_push.yml`, plus issue/PR templates — modeled on +`SimplnxReview`. Each build job clones `simplnx`, configures with +`-DSIMPLNX_EXTRA_PLUGINS="MTRSim" -DSIMPLNX_PLUGIN_ENABLE_MTRSim=ON +-DSIMPLNX_MTRSim_SOURCE_DIR=`, builds, and runs `ctest`. vcpkg +binary caching comes from the BlueQuartz NuGet package registry. + +Remaining work: +- Confirm the **embedded LibMTRSim vcpkg dependencies** (Eigen, spdlog, CLI11, + nlohmann-json, hdf5, stb) resolve inside the simplnx build on all three + platforms. +- Commit + push and confirm the workflows trigger and pass green. + +--- + +## 6. File Plan + +New / changed files: + +``` +src/MTRSim/Filters/MTRSimFilter.{hpp,cpp} # filter +src/MTRSim/Filters/Algorithms/MTRSim.{hpp,cpp} # algorithm +src/LibMTRSim/... # move/parameterize buildUniformODF; + # add ODF-geometry→ODFComponent helper +src/MTRSim/MTRSimPlugin.cpp # register MTRSimFilter +test/MTRSimTest.cpp # filter unit + statistical tests +test/CMakeLists.txt # add test +docs/.../MTRSimFilter.md # filter documentation (Milestone AL polishes) +.github/workflows/*.yml # already added — validate green +``` + +--- + +## 7. Out of Scope (this milestone) + +- Self-paced tutorial, example pipelines, and final documentation polish → + Milestone AL. +- Any change to the MATLAB reference code or the ODF HDF5 on-disk format. +- Exposing `nuggetVariance` (unused by the simulation). diff --git a/docs/superpowers/specs/2026-06-02-mtrsim-observer-and-config-design.md b/docs/superpowers/specs/2026-06-02-mtrsim-observer-and-config-design.md new file mode 100644 index 0000000..43c8c23 --- /dev/null +++ b/docs/superpowers/specs/2026-06-02-mtrsim-observer-and-config-design.md @@ -0,0 +1,270 @@ +# MTRSim: Simulation Observer + Config-File Input — Design + +**Date:** 2026-06-02 +**Status:** Design approved — ready for implementation plan +**Scope:** Two cohesive enhancements to the MTRSim library + `MTRSimFilter`: +1. A progress/cancellation observer threaded through `simulateMTR`. +2. An optional MTRSim config-JSON input on the filter (MATLAB-migration convenience). + +--- + +## 1. Background + +`mtrsim::simulateMTR(params, odfComponents, rng, n1, nPHI, n2)` (in +`src/LibMTRSim/MTRSimDriver.{hpp,cpp}`) runs the full pipeline: PGRF assignment → +per-component ODF sampling → per-voxel orientation assignment → remap to SIMPLNX +z,y,x order. It is currently a single opaque call with no progress reporting and +no way to cancel; for the default ~1.2M-voxel grid it can run for a long time. + +It has two callers: +- `src/MTRSim/Filters/Algorithms/MTRSim.cpp` (the DREAM3D-NX filter algorithm), + which holds a `MessageHandler` and an `std::atomic_bool& m_ShouldCancel`. +- `src/app/main.cpp` (the standalone CLI), which currently relies on + `PGRFSimulation`'s internal `spdlog` logging. + +The long pole is the per-component ODF-sampling loop inside `simulateMTR` +(`ODFSampler::sampleN` is called once per component, drawing N orientations +each) and the per-voxel assignment loop. + +--- + +## 2. Part A — Progress & Cancellation Observer + +### 2.1 The interface + +New header `src/LibMTRSim/ISimulationObserver.hpp`: + +```cpp +#pragma once + +#include "libmtrsim_export.h" + +#include +#include + +namespace mtrsim { + +/** + * @brief Observer interface for long-running MTR simulations. + * + * Implementations receive throttled progress updates and are polled for + * cancellation. The library calls these from the thread running simulateMTR; + * implementations must be cheap and must not throw. + */ +class LIBMTRSIM_EXPORT ISimulationObserver { +public: + ISimulationObserver() = default; + virtual ~ISimulationObserver() = default; + + // Polymorphic, non-copyable base (prevents slicing). + ISimulationObserver(const ISimulationObserver&) = delete; + ISimulationObserver& operator=(const ISimulationObserver&) = delete; + ISimulationObserver(ISimulationObserver&&) = delete; + ISimulationObserver& operator=(ISimulationObserver&&) = delete; + + /// Report progress. `done`/`total` describe the current phase; `message` + /// names the phase (e.g. "Sampling orientations (component 2/3)"). + virtual void updateProgress(int64_t done, int64_t total, const std::string& message) = 0; + + /// Polled at checkpoints. Returning true stops the simulation early. + [[nodiscard]] virtual bool shouldCancel() const = 0; +}; + +} // namespace mtrsim +``` + +C++ best-practice notes: +- Pure-virtual methods (`= 0`), defaulted virtual destructor, `[[nodiscard]]` + on the predicate, `const` correctness on `shouldCancel`. +- Copy/move deleted on the polymorphic base (rule-of-five, anti-slicing). +- Header-only interface; no `.cpp`. + +### 2.2 Default implementations + +In `src/LibMTRSim/SimulationObservers.hpp` (header-only): +- `NullObserver` — both methods no-op / `return false`. Used as the implicit + default so existing callers are unaffected. +- `ConsoleObserver` — `updateProgress` logs via `spdlog` (throttled to avoid + log spam); `shouldCancel` returns `false`. Used by `main.cpp` to preserve its + console feedback. + +### 2.3 Signature changes + +```cpp +// MTRSimDriver.hpp +LIBMTRSIM_EXPORT MTRSimResult simulateMTR( + const SimulationParams& params, + const std::vector& odfComponents, + std::mt19937_64& rng, + int n1, int nPHI, int n2, + ISimulationObserver* observer = nullptr); // NEW, nullable + +// ODFSampler.hpp +Eigen::MatrixXd sampleN(int n, const ODFComponent& component, + const ODFComponent& uniform, + ISimulationObserver* observer = nullptr, // NEW + int64_t progressBase = 0, int64_t progressTotal = 0); // for global progress +``` + +- `observer == nullptr` ⇒ run exactly as today (no progress, no cancel checks): + full backward compatibility. +- `simulateMTR` reports at PGRF stage boundaries (threshold selection, each + latent Gaussian field, assignment) and forwards the observer into each + `sampleN` call and the per-voxel assignment loop. +- **Throttling:** a small internal helper updates progress at most every ~1% of + `total` (or every K iterations), so the observer/UI isn't flooded. Cancel is + polled at the same checkpoints — frequently enough to feel responsive but not + per-iteration. + +### 2.4 Cancellation semantics + +- `MTRSimResult` gains `bool cancelled = false;`. +- On `observer->shouldCancel()` at any checkpoint, `simulateMTR` returns early + with `cancelled = true` and whatever arrays are allocated (callers must not + consume them). No exceptions are used for control flow. +- The filter algorithm checks `m_ShouldCancel` (its observer's source) after the + call — as it already does — and returns `{}` (success, no output) without + writing arrays, matching the simplnx cancel convention. + +### 2.5 Filter observer + +`MTRSim.cpp` defines a private `FilterObserver : mtrsim::ISimulationObserver` +that captures `const IFilter::MessageHandler&` and `const std::atomic_bool&`: +- `updateProgress` → emits an `IFilter::ProgressMessage` (percent = + `done*100/total`) with the phase text (see `progress-messaging` conventions). +- `shouldCancel` → returns the captured atomic's value. + +`main.cpp` passes a `ConsoleObserver` (or `nullptr`). + +--- + +## 3. Part B — Optional MTRSim Config-File Input (filter only) + +### 3.1 New parameters + +On `MTRSimFilter`: +- `k_UseConfigFile_Key` = `"use_config_file"` — linkable `BoolParameter` + "Load Simulation Parameters from Config File" (default `false`). +- `k_ConfigFilePath_Key` = `"config_file_path"` — `FileSystemPathParameter` + (InputFile, extension `.json`). + +### 3.2 Linked show/hide UX + +Using `params.linkParameters(...)`: +- Shown when `use_config_file == true`: `config_file_path`. +- Shown when `use_config_file == false`: `volume_fractions`, `theta_list`, + `physical_size`, `physical_spacing`, and the seed group (`use_seed`, + `seed_value`, `seed_array_name`). +- Always shown: `input_odf_geometry_path`, `odf_component_arrays`, and all + output names/paths. + +> Implementation note: `use_seed` is itself a linkable that controls +> `seed_value`. Verify simplnx supports a parameter being both a dependent (of +> `use_config_file`) and a linkable (for `seed_value`). If nested linking is not +> supported, fall back to leaving the seed group visible in config mode but +> documenting that the config's `seed` takes precedence there. + +### 3.3 Shared config parser + +Extract `main.cpp`'s JSON-parsing block into a reusable LibMTRSim helper so the +CLI and the filter share one implementation: + +```cpp +// src/LibMTRSim/ConfigIO.hpp +namespace mtrsim { +/// Parse an MTRSim config JSON into a SimulationParams. Throws +/// std::runtime_error on missing file / invalid JSON. Unknown keys +/// (odfInputPath, nuggetVariance, comments) are ignored. Fields not present +/// in the JSON keep their SimulationParams defaults. +LIBMTRSIM_EXPORT SimulationParams parseConfigJson(const std::filesystem::path& path); +} +``` + +`main.cpp` is refactored to call `parseConfigJson` (removing its inline parsing). + +### 3.4 Filter behavior in config mode + +When `use_config_file == true`, both `preflightImpl` and `executeImpl` obtain +the simulation parameters from `parseConfigJson(config_file_path)` instead of +the UI fields. Specifically the JSON supplies: `xLen/yLen/zLen`, `dx/dy/dz`, +`volumeFractions`, `thetaList`, `seed` (treated as a fixed seed; equivalent to +`use_seed == true`). + +- **Preflight** parses the file; a missing file or parse error is a preflight + error (`-13520`). The JSON-derived values are validated with the *same* rules + as manual mode: `volumeFractions` count must equal the number of selected ODF + component arrays; sum ≈ 1.0; each in [0,1]; theta rows ≥ components−1 with 3 + columns; spacing X/Y > 0. The output geometry dims are computed from the + JSON's size/spacing. +- **Ignored JSON keys:** `odfInputPath` (the ODF comes from the selected + DataStructure geometry, not a file) and `nuggetVariance` (unused by the + simulation). A preflight info note lists the effective grid + that parameters + came from the config file. +- ODF selection and all output names/paths always come from the UI. + +When `use_config_file == false`, behavior is exactly as today. + +--- + +## 4. Testing + +### 4.1 Observer (Part A) — LibMTRSim Catch2 +- A `RecordingObserver` test double that records each `updateProgress` call and + can be configured to return `true` from `shouldCancel` after the Kth update. +- `simulateMTR` with the recorder: assert progress is reported, `done ≤ total` + always, and the final update reaches `total` on a completed run. +- Cancel test: recorder cancels mid-run → `simulateMTR` returns + `cancelled == true` and does not run to completion (bounded number of updates). +- `sampleN` directly with an observer that cancels → returns promptly. +- `NullObserver`/`nullptr` path produces identical results to the pre-change + behavior (regression guard via a fixed seed). + +### 4.2 Config parser (Part B) — LibMTRSim Catch2 +- `parseConfigJson` on a known config (`configs/default.json`) → expected + `SimulationParams` fields; confirms `odfInputPath`/`nuggetVariance` ignored. +- Missing file and malformed JSON → throws. + +### 4.3 Filter (Part B) — simplnx FilterValidationTest + unit tests +- `FilterValidationTest` must pass (new keys end with `_path` where required; + `use_config_file`/`config_file_path` follow conventions). +- Config-mode preflight: valid config → VALID with the info note; component-count + mismatch vs ODF arrays → invalid; missing/garbled config → invalid (−13520). +- Manual-mode tests unchanged. +- A cancelled execute writes no output arrays. + +### 4.4 Integration +- The Debug smoke pipeline (`pipelines/MTRSim_smoke_test.d3dpipeline`) executes + clean in the Debug build after the changes (Eigen assertions on). +- Run `FilterValidationTest` (now part of `simplnx_test`) after the filter + parameter changes. + +--- + +## 5. Files + +**Create:** +- `src/LibMTRSim/ISimulationObserver.hpp` +- `src/LibMTRSim/SimulationObservers.hpp` (`NullObserver`, `ConsoleObserver`) +- `src/LibMTRSim/ConfigIO.{hpp,cpp}` (`parseConfigJson`) +- Tests: extend `tests/test_mtrsim_driver.cpp`; add `tests/test_config_io.cpp`. + +**Modify:** +- `src/LibMTRSim/MTRSimDriver.{hpp,cpp}` — `simulateMTR` observer arg + cancel; + `MTRSimResult::cancelled`. +- `src/LibMTRSim/ODFSampler.{hpp,cpp}` — `sampleN` observer arg + cancel checks. +- `src/app/main.cpp` — use `ConsoleObserver`; use `parseConfigJson`. +- `src/MTRSim/Filters/MTRSimFilter.{hpp,cpp}` — config-file params + linking + + config-mode preflight/execute. +- `src/MTRSim/Filters/Algorithms/MTRSim.{hpp,cpp}` — `FilterObserver`; pass it + into `simulateMTR`; build `SimulationParams` from config when in config mode. +- `src/LibMTRSim/CMakeLists.txt`, `MTRSimPlugin.cmake`, `tests/CMakeLists.txt` — + register new sources/tests. +- `docs/MTRSimFilter.md` — document config-file mode + progress/cancel. + +--- + +## 6. Out of Scope +- Parallelizing the simulation (the observer enables cancel/progress, not speed). +- Changing the ODF on-disk format or the MATLAB code. +- Writing the config's `odfInputPath` ODF from within the filter (ODF still comes + from upstream Read/Compute ODF filters). diff --git a/pipelines/MTRSim_default.d3dpipeline b/pipelines/MTRSim_default.d3dpipeline new file mode 100644 index 0000000..cb44622 --- /dev/null +++ b/pipelines/MTRSim_default.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_default.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 38.099998474121094, + 12.699999809265137, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Baseline configuration matching simulate_MTRs.m defaults.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_default.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "2699f738-966e-4cb0-b674-20d9cdade93b", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_image_high_res.d3dpipeline b/pipelines/MTRSim_image_high_res.d3dpipeline new file mode 100644 index 0000000..9d2475e --- /dev/null +++ b/pipelines/MTRSim_image_high_res.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_image_high_res.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 38.099998474121094, + 12.699999809265137, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.009999999776482582, + 0.009999999776482582, + 0.009999999776482582 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "High resolution: same physical volume, 2x finer voxels. Slower — Cholesky matrices are 2x larger in each dimension.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_image_high_res.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "2990c9c3-f1cd-4c70-aad4-d28d30c42d57", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_image_large_volume.d3dpipeline b/pipelines/MTRSim_image_large_volume.d3dpipeline new file mode 100644 index 0000000..8f11cb0 --- /dev/null +++ b/pipelines/MTRSim_image_large_volume.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_image_large_volume.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 76.19999694824219, + 25.399999618530273, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Large volume: double the physical size in x and y at default resolution. Shows more MTR regions across the scan.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_image_large_volume.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "1a2d4b28-c1f2-4449-b344-0d32a2e1154b", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_image_low_res.d3dpipeline b/pipelines/MTRSim_image_low_res.d3dpipeline new file mode 100644 index 0000000..3247740 --- /dev/null +++ b/pipelines/MTRSim_image_low_res.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_image_low_res.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 38.099998474121094, + 12.699999809265137, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.03999999910593033, + 0.03999999910593033, + 0.03999999910593033 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Low resolution: same physical volume, 2x coarser voxels. Runs fast.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_image_low_res.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "c20db986-4dbd-4c78-8fb1-78b690777ed8", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_mtrs_isotropic.d3dpipeline b/pipelines/MTRSim_mtrs_isotropic.d3dpipeline new file mode 100644 index 0000000..9fef3eb --- /dev/null +++ b/pipelines/MTRSim_mtrs_isotropic.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_mtrs_isotropic.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 38.099998474121094, + 12.699999809265137, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.3, + 0.3, + 0.3 + ], + [ + 0.25, + 0.25, + 0.25 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Isotropic MTRs: equal theta in x and y. Produces blocky, roughly equiaxed MTR regions instead of the default elongated needle-like morphology.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_mtrs_isotropic.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "8e0ddedc-8d87-45d6-9438-003e877afad4", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_mtrs_large.d3dpipeline b/pipelines/MTRSim_mtrs_large.d3dpipeline new file mode 100644 index 0000000..d4acc70 --- /dev/null +++ b/pipelines/MTRSim_mtrs_large.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_mtrs_large.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 38.099998474121094, + 12.699999809265137, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.2, + 0.9, + 0.2 + ], + [ + 0.16, + 0.74, + 0.16 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Large MTRs: theta values doubled in all directions. MTR regions are physically ~2x bigger.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_mtrs_large.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "0e2449cd-d7ce-4ed3-a109-81519baa47ee", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_mtrs_small.d3dpipeline b/pipelines/MTRSim_mtrs_small.d3dpipeline new file mode 100644 index 0000000..e972460 --- /dev/null +++ b/pipelines/MTRSim_mtrs_small.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_mtrs_small.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 38.099998474121094, + 12.699999809265137, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.04, + 0.18, + 0.04 + ], + [ + 0.03, + 0.15, + 0.03 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Small MTRs: theta values reduced to ~40% of default. MTR regions are physically smaller and more numerous.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_mtrs_small.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "ada319b2-4b5d-4160-855a-6c3f4f6841a7", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_mtrs_very_elongated.d3dpipeline b/pipelines/MTRSim_mtrs_very_elongated.d3dpipeline new file mode 100644 index 0000000..df658c5 --- /dev/null +++ b/pipelines/MTRSim_mtrs_very_elongated.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_mtrs_very_elongated.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 38.099998474121094, + 12.699999809265137, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.05, + 1.5, + 0.05 + ], + [ + 0.04, + 1.2, + 0.04 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Very elongated MTRs: large theta_y, small theta_x. Produces long, thin, strongly directional MTR streaks — exaggerated version of the default needle-like morphology.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_mtrs_very_elongated.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "49ade877-f067-4847-9b3d-6a4e66ea76e3", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_smoke_test.d3dpipeline b/pipelines/MTRSim_smoke_test.d3dpipeline new file mode 100644 index 0000000..df46b96 --- /dev/null +++ b/pipelines/MTRSim_smoke_test.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_smoke_test.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 1.0, + 0.6000000238418579, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Tiny smoke-test grid for fast end-to-end execution (Debug builds included).", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_smoke_test.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "fd797e8b-7ea7-4535-a101-a36b194f5863", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_3d_thin_slab.d3dpipeline b/pipelines/MTRSim_test_3d_thin_slab.d3dpipeline new file mode 100644 index 0000000..468bb8c --- /dev/null +++ b/pipelines/MTRSim_test_3d_thin_slab.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_3d_thin_slab.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.10000000149011612 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "3D thin slab — non-zero zLen produces a volume with 5 z-layers.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_3d_thin_slab.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "f1bd31be-f7c6-43f0-9960-4444960cce66", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_coarse_voxels.d3dpipeline b/pipelines/MTRSim_test_coarse_voxels.d3dpipeline new file mode 100644 index 0000000..2856326 --- /dev/null +++ b/pipelines/MTRSim_test_coarse_voxels.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_coarse_voxels.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.10000000149011612, + 0.10000000149011612, + 0.10000000149011612 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Coarse voxels — larger voxel spacing relative to theta. Tests under-resolved regime where voxels are comparable in size to correlation length.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_coarse_voxels.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "fe6f8008-d5b7-43c5-b29d-d1bde3a03335", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_dominant_component.d3dpipeline b/pipelines/MTRSim_test_dominant_component.d3dpipeline new file mode 100644 index 0000000..ae432f0 --- /dev/null +++ b/pipelines/MTRSim_test_dominant_component.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_dominant_component.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.7, + 0.15, + 0.15 + ] + ], + "version": 1 + } + }, + "comments": "One dominant component — 70% of volume is component 1, 15% each for components 2 and 3.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_dominant_component.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "cfc3b48f-2cf8-440a-bb6e-498869fc28a4", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_equal_fractions.d3dpipeline b/pipelines/MTRSim_test_equal_fractions.d3dpipeline new file mode 100644 index 0000000..f168dda --- /dev/null +++ b/pipelines/MTRSim_test_equal_fractions.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_equal_fractions.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3333, + 0.3333, + 0.3334 + ] + ], + "version": 1 + } + }, + "comments": "Equal volume fractions — all three components have identical target probability.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_equal_fractions.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "046634f6-6626-4433-abf1-ec2639b2a09a", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_extreme_dominant.d3dpipeline b/pipelines/MTRSim_test_extreme_dominant.d3dpipeline new file mode 100644 index 0000000..4c2a06f --- /dev/null +++ b/pipelines/MTRSim_test_extreme_dominant.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_extreme_dominant.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.9, + 0.05, + 0.05 + ] + ], + "version": 1 + } + }, + "comments": "Extreme dominance — 90% component 1. Stress-tests the assignment rule with near-degenerate volume fractions.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_extreme_dominant.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "1709d16b-6ed2-4264-88da-614fe3063b3e", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_high_aspect_theta.d3dpipeline b/pipelines/MTRSim_test_high_aspect_theta.d3dpipeline new file mode 100644 index 0000000..8d1040a --- /dev/null +++ b/pipelines/MTRSim_test_high_aspect_theta.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_high_aspect_theta.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.05, + 0.8, + 0.05 + ], + [ + 0.8, + 0.05, + 0.05 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "High aspect ratio theta — Gaussian 1 is strongly elongated in y, Gaussian 2 is strongly elongated in x. Creates complex, cross-hatched morphology.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_high_aspect_theta.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "2b6b5087-ac66-4539-a6a5-85bd294a28f4", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_large_theta.d3dpipeline b/pipelines/MTRSim_test_large_theta.d3dpipeline new file mode 100644 index 0000000..742fadf --- /dev/null +++ b/pipelines/MTRSim_test_large_theta.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_large_theta.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 5.0, + 0.1 + ], + [ + 0.08, + 4.0, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Large theta relative to domain — correlation length exceeds domain size in y. Tests behavior when MTRs span the entire field.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_large_theta.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "3e69ce18-8a88-4e6c-95e4-eb199000479d", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_seed_99.d3dpipeline b/pipelines/MTRSim_test_seed_99.d3dpipeline new file mode 100644 index 0000000..cdfa9a9 --- /dev/null +++ b/pipelines/MTRSim_test_seed_99.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_seed_99.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 99, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Different seed — same parameters as test_tiny_default but seed=99 instead of 42.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_seed_99.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "01855d9d-8ed2-4555-ba8d-f67a21e7294b", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_small_theta.d3dpipeline b/pipelines/MTRSim_test_small_theta.d3dpipeline new file mode 100644 index 0000000..d11c4eb --- /dev/null +++ b/pipelines/MTRSim_test_small_theta.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_small_theta.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.001, + 0.001, + 0.001 + ], + [ + 0.001, + 0.001, + 0.001 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Very small theta — correlation length much smaller than voxel spacing. Approaches white noise (uncorrelated field).", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_small_theta.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "8c34ee29-187f-4ad5-a72e-4b7e994b1e79", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_square_domain.d3dpipeline b/pipelines/MTRSim_test_square_domain.d3dpipeline new file mode 100644 index 0000000..4e59324 --- /dev/null +++ b/pipelines/MTRSim_test_square_domain.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_square_domain.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 2.0, + 2.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Square domain — equal xLen and yLen. Tests behavior when the spatial domain has no preferred aspect ratio.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_square_domain.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "c3d9ed46-5b72-426f-b6c4-f9af436af07e", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_symmetric_theta.d3dpipeline b/pipelines/MTRSim_test_symmetric_theta.d3dpipeline new file mode 100644 index 0000000..733ba98 --- /dev/null +++ b/pipelines/MTRSim_test_symmetric_theta.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_symmetric_theta.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.1, + 0.45, + 0.1 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Symmetric theta — both Gaussians use identical correlation lengths. Removes inter-Gaussian morphological variation.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_symmetric_theta.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "7a9a78b6-f77e-4e51-82b6-989ed19aee76", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/MTRSim_test_tiny_default.d3dpipeline b/pipelines/MTRSim_test_tiny_default.d3dpipeline new file mode 100644 index 0000000..b60fcae --- /dev/null +++ b/pipelines/MTRSim_test_tiny_default.d3dpipeline @@ -0,0 +1,299 @@ +{ + "isDisabled": false, + "name": "MTRSim_test_tiny_default.d3dpipeline", + "pinnedParams": [], + "pipeline": [ + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "hdf5_path_prefix": { + "value": "/ODF_best", + "version": 1 + }, + "input_file": { + "value": "data/MTRSim/simulation_ODF.h5", + "version": 1 + }, + "output_image_geometry_path": { + "value": "ODF", + "version": 1 + }, + "parameters_version": 1 + }, + "comments": "Load the 3-component MTRSim ODF into ImageGeom 'ODF'.", + "filter": { + "name": "nx::core::ReadMTRSimODFFilter", + "uuid": "2b1a4841-65d7-4315-9fe3-d66c88e5755c" + }, + "isDisabled": false + }, + { + "args": { + "cell_attribute_matrix_name": { + "value": "Cell Data", + "version": 1 + }, + "config_file_path": { + "value": "", + "version": 1 + }, + "eulers_array_name": { + "value": "Eulers", + "version": 1 + }, + "generate_polar_coloring": { + "value": true, + "version": 1 + }, + "input_odf_geometry_path": { + "value": "ODF", + "version": 1 + }, + "mtr_ids_array_name": { + "value": "MTRIds", + "version": 1 + }, + "odf_component_arrays": { + "value": [ + "ODF/Cell Data/component_0", + "ODF/Cell Data/component_1", + "ODF/Cell Data/component_2" + ], + "version": 1 + }, + "output_geometry_path": { + "value": "MTR Microstructure", + "version": 1 + }, + "parameters_version": 1, + "physical_size": { + "value": [ + 40.0, + 20.0, + 0.0 + ], + "version": 1 + }, + "physical_spacing": { + "value": [ + 0.019999999552965164, + 0.019999999552965164, + 0.019999999552965164 + ], + "version": 1 + }, + "polar_colors_array_name": { + "value": "Polar Colors", + "version": 1 + }, + "seed_array_name": { + "value": "MTRSim SeedValue", + "version": 1 + }, + "seed_value": { + "value": 42, + "version": 1 + }, + "theta_list": { + "value": [ + [ + 0.1, + 0.45, + 0.1 + ], + [ + 0.08, + 0.37, + 0.08 + ] + ], + "version": 1 + }, + "use_config_file": { + "value": false, + "version": 1 + }, + "use_seed": { + "value": true, + "version": 1 + }, + "volume_fractions": { + "value": [ + [ + 0.3, + 0.35, + 0.35 + ] + ], + "version": 1 + } + }, + "comments": "Tiny grid for fast validation. Same parameters as default but 100x50 pixels instead of 1905x635.", + "filter": { + "name": "nx::core::MTRSimFilter", + "uuid": "f7f7a330-4bff-4a42-a573-09117a89a0a0" + }, + "isDisabled": false + }, + { + "args": { + "component_count": { + "value": 1, + "version": 1 + }, + "data_format": { + "value": "", + "version": 1 + }, + "initialization_value_str": { + "value": "1", + "version": 1 + }, + "numeric_type_index": { + "value": 4, + "version": 1 + }, + "output_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "parameters_version": 1, + "set_tuple_dimensions": { + "value": false, + "version": 1 + }, + "tuple_dimensions": { + "value": [ + [ + 0.0 + ] + ], + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateDataArrayFilter", + "uuid": "67041f9b-bdc6-4122-acc6-c9fe9280e90d" + }, + "isDisabled": false + }, + { + "args": { + "cell_ensemble_attribute_matrix_path": { + "value": "MTR Microstructure/Phase Data", + "version": 1 + }, + "crystal_structures_array_name": { + "value": "CrystalStructures", + "version": 1 + }, + "ensemble": { + "value": [ + [ + "Hexagonal-High 6/mmm", + "Primary", + "Titanium" + ] + ], + "version": 1 + }, + "parameters_version": 1, + "phase_names_array_name": { + "value": "PhaseNames", + "version": 1 + }, + "phase_types_array_name": { + "value": "PhaseTypes", + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::CreateEnsembleInfoFilter", + "uuid": "8ce3d70c-49fe-4812-a1eb-7ce4c962a59d" + }, + "isDisabled": false + }, + { + "args": { + "cell_euler_angles_array_path": { + "value": "MTR Microstructure/Cell Data/Eulers", + "version": 1 + }, + "cell_ipf_colors_array_name": { + "value": "IPFColors", + "version": 1 + }, + "cell_phases_array_path": { + "value": "MTR Microstructure/Cell Data/Phases", + "version": 1 + }, + "color_key_index": { + "value": 0, + "version": 1 + }, + "crystal_structures_array_path": { + "value": "MTR Microstructure/Phase Data/CrystalStructures", + "version": 1 + }, + "mask_array_path": { + "value": "", + "version": 1 + }, + "parameters_version": 2, + "reference_dir": { + "value": [ + 0.0, + 0.0, + 1.0 + ], + "version": 1 + }, + "use_mask": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::ComputeIPFColorsFilter", + "uuid": "64cb4f27-6e5e-4dd2-8a03-0c448cb8f5e6" + }, + "isDisabled": false + }, + { + "args": { + "compression_level": { + "value": 5, + "version": 1 + }, + "export_file_path": { + "value": "Data/Output/MTRSim/MTRSim_test_tiny_default.dream3d", + "version": 1 + }, + "parameters_version": 2, + "use_compression": { + "value": true, + "version": 1 + }, + "write_xdmf_file": { + "value": false, + "version": 1 + } + }, + "comments": "", + "filter": { + "name": "nx::core::WriteDREAM3DFilter", + "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" + }, + "isDisabled": false + } + ], + "pipeline_uuid": "b5f9cfe2-d836-4116-b107-8bc30ed3e166", + "version": 1, + "workflowParams": [] +} diff --git a/pipelines/avtr12-ODF-Pole-Figure.d3dpipeline b/pipelines/avtr12-ODF-Pole-Figure.d3dpipeline index eab93b3..1e44935 100644 --- a/pipelines/avtr12-ODF-Pole-Figure.d3dpipeline +++ b/pipelines/avtr12-ODF-Pole-Figure.d3dpipeline @@ -22,7 +22,7 @@ "version": 1 }, "input_file": { - "value": "/Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib/Bin/Data/T12-MAI-2010/fw-ar-IF1-avtr12-corr.ctf", + "value": "Data/T12-MAI-2010/fw-ar-IF1-avtr12-corr.ctf", "version": 1 }, "output_image_geometry_path": { @@ -287,7 +287,7 @@ "version": 1 }, "output_path": { - "value": "/Users/mjackson/Workspace7/DREAM3D-Build/ebsdlib-Release/Bin/12_PoleFigures", + "value": "Data/Output/12_PoleFigures", "version": 1 }, "parameters_version": 2, @@ -337,36 +337,36 @@ "value": "Component 1", "version": 1 }, - "crystal_structures": { + "crystal_structures_path": { "value": "ImageGeom/Cell Ensemble Data/CrystalStructures", "version": 1 }, - "euler_angles": { + "euler_angles_path": { "value": "ImageGeom/Cell Data/EulerAngles", "version": 1 }, - "existing_odf_geometry": { + "existing_odf_geometry_path": { "value": "", "version": 1 }, - "mask": { + "mask_path": { "value": "ImageGeom/Cell Data/Mask", "version": 1 }, - "output_image_geometry": { + "output_image_geometry_path": { "value": "ODF", "version": 1 }, - "output_mode": { + "output_mode_index": { "value": 0, "version": 1 }, - "output_units": { + "output_units_index": { "value": 1, "version": 1 }, "parameters_version": 3, - "phases": { + "phases_path": { "value": "ImageGeom/Cell Data/Phases", "version": 1 }, @@ -507,7 +507,7 @@ { "args": { "file_name": { - "value": "/Users/mjackson/Workspace7/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel-EbsdLib/Bin/Data/Output/T12-MAI-2010/fw-ar-IF1-avtr12-corr_ODF.png", + "value": "Data/Output/T12-MAI-2010/fw-ar-IF1-avtr12-corr_ODF.png", "version": 1 }, "image_array_path": { diff --git a/scripts/generate_nx_pipelines.py b/scripts/generate_nx_pipelines.py new file mode 100644 index 0000000..0c5b2c7 --- /dev/null +++ b/scripts/generate_nx_pipelines.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Generate DREAM3D-NX .d3dpipeline files from the LibMTRSim configs/*.json files. + +Each generated pipeline reproduces one MTRSim configuration as a three-step +DREAM3D-NX pipeline: + + ReadMTRSimODFFilter -> MTRSimFilter -> WriteDREAM3DFilter + +The MTRSim simulation parameters (volume fractions, theta list, physical size / +spacing, seed) are taken directly from the JSON config so the NX pipeline matches +the standalone LibMTRSim run. The number of ODF component arrays is derived from +the length of ``volumeFractions``. + +Units note: the JSON configs are millimeter-consistent (size, spacing, and theta +all in mm). The MTRSimFilter "Physical Size/Spacing (microns)" labels are nominal +— the simulation only uses the dimensionless lag/theta ratio — so the mm values +are written through unchanged. + +Usage: + python3 scripts/generate_nx_pipelines.py [--configs-dir DIR] [--out-dir DIR] +""" + +import argparse +import json +from pathlib import Path + +# Filter identity constants (kept in sync with the filter headers). +READ_ODF_UUID = "2b1a4841-65d7-4315-9fe3-d66c88e5755c" +MTRSIM_UUID = "f7f7a330-4bff-4a42-a573-09117a89a0a0" +WRITE_DREAM3D_UUID = "b3a95784-2ced-41ec-8d3d-0242ac130003" + +ODF_GEOM = "ODF" +ODF_CELL_AM = "Cell Data" + + +def _val(value, version=1): + return {"value": value, "version": version} + + +def build_pipeline(name, cfg, repo_root): + """Build the pipeline dict for one config.""" + vf = cfg["volumeFractions"] + num_components = len(vf) + component_paths = [f"{ODF_GEOM}/{ODF_CELL_AM}/component_{i}" for i in range(num_components)] + + odf_file = (repo_root / "data" / "simulation_ODF.h5").as_posix() + out_file = (repo_root / "output" / f"MTRSim_{name}.dream3d").as_posix() + + read_step = { + "args": { + "input_file": _val(odf_file), + "hdf5_path_prefix": _val("/ODF_best"), + "output_image_geometry_path": _val(ODF_GEOM), + "cell_attribute_matrix_name": _val(ODF_CELL_AM), + "parameters_version": 1, + }, + "comments": f"Load the {num_components}-component MTRSim ODF into ImageGeom '{ODF_GEOM}'.", + "filter": {"name": "nx::core::ReadMTRSimODFFilter", "uuid": READ_ODF_UUID}, + "isDisabled": False, + } + + mtrsim_step = { + "args": { + "input_odf_geometry_path": _val(ODF_GEOM), + "odf_component_arrays": _val(component_paths), + "use_config_file": _val(False), + "config_file_path": _val(""), + "volume_fractions": _val([list(vf)]), + "theta_list": _val([list(row) for row in cfg["thetaList"]]), + "physical_size": _val([cfg["xLen"], cfg["yLen"], cfg["zLen"]]), + "physical_spacing": _val([cfg["dx"], cfg["dy"], cfg["dz"]]), + "use_seed": _val(True), + "seed_value": _val(int(cfg.get("seed", 42))), + "seed_array_name": _val("MTRSim SeedValue"), + "generate_polar_coloring": _val(True), + "output_geometry_path": _val("MTR Microstructure"), + "cell_attribute_matrix_name": _val("Cell Data"), + "mtr_ids_array_name": _val("MTRIds"), + "eulers_array_name": _val("Eulers"), + "polar_colors_array_name": _val("Polar Colors"), + "parameters_version": 1, + }, + "comments": cfg.get("_comment", f"Recreates configs/{name}.json."), + "filter": {"name": "nx::core::MTRSimFilter", "uuid": MTRSIM_UUID}, + "isDisabled": False, + } + + write_step = { + "args": { + "export_file_path": _val(out_file), + "write_xdmf_file": _val(False), + "use_compression": _val(True), + "compression_level": _val(5), + "parameters_version": 1, + }, + "comments": "Save the synthetic microstructure. Disable to only view in the GUI.", + "filter": {"name": "nx::core::WriteDREAM3DFilter", "uuid": WRITE_DREAM3D_UUID}, + "isDisabled": False, + } + + return { + "isDisabled": False, + "name": f"MTRSim_{name}.d3dpipeline", + "pinnedParams": [], + "pipeline": [read_step, mtrsim_step, write_step], + "version": 1, + "workflowParams": [], + } + + +def main(): + repo_root = Path(__file__).resolve().parent.parent + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--configs-dir", default=str(repo_root / "configs")) + parser.add_argument("--out-dir", default=str(repo_root / "pipelines")) + args = parser.parse_args() + + configs_dir = Path(args.configs_dir) + out_dir = Path(args.out_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + count = 0 + for cfg_path in sorted(configs_dir.glob("*.json")): + with open(cfg_path) as f: + cfg = json.load(f) + if "volumeFractions" not in cfg: + continue + name = cfg_path.stem + pipeline = build_pipeline(name, cfg, repo_root) + out_path = out_dir / f"MTRSim_{name}.d3dpipeline" + with open(out_path, "w") as f: + json.dump(pipeline, f, indent=2) + f.write("\n") + count += 1 + print(f"wrote {out_path.relative_to(repo_root)}") + print(f"\nGenerated {count} pipeline(s).") + + +if __name__ == "__main__": + main() diff --git a/src/LibMTRSim/AssignmentRule.cpp b/src/LibMTRSim/AssignmentRule.cpp index 6f62d04..dafc331 100644 --- a/src/LibMTRSim/AssignmentRule.cpp +++ b/src/LibMTRSim/AssignmentRule.cpp @@ -10,9 +10,11 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ -namespace { +namespace +{ constexpr double k_Inf = std::numeric_limits::infinity(); constexpr double k_PTol = 1e-6; constexpr double k_QMin0 = -6.0; @@ -23,9 +25,8 @@ constexpr int k_MaxBisectIter = 2000; // Bisect q in (qMin, qMax) so that qsimvn(identity, sTmp, tTmp with // tTmp[goi]=q) == poi. tTmp is passed by value — the caller receives the final // tTmp back via the reference returned. -double bisectThreshold(QSimVN &qsimvn, const Eigen::MatrixXd &identity, - const Eigen::VectorXd &sTmp, Eigen::VectorXd &tTmp, - int goi, double poi) { +double bisectThreshold(QSimVN& qsimvn, const Eigen::MatrixXd& identity, const Eigen::VectorXd& sTmp, Eigen::VectorXd& tTmp, int goi, double poi) +{ double q = 0.0; double qMin = k_QMin0; double qMax = k_QMax0; @@ -33,11 +34,14 @@ double bisectThreshold(QSimVN &qsimvn, const Eigen::MatrixXd &identity, tTmp(goi) = q; double pTmp = qsimvn.compute(k_QSimVNSamples, identity, sTmp, tTmp).first; - for (int iter = 0; iter < k_MaxBisectIter && std::abs(poi - pTmp) > k_PTol; - ++iter) { - if (pTmp < poi) { + for(int iter = 0; iter < k_MaxBisectIter && std::abs(poi - pTmp) > k_PTol; ++iter) + { + if(pTmp < poi) + { qMin = q; - } else { + } + else + { qMax = q; } q = (qMax + qMin) / 2.0; @@ -52,7 +56,10 @@ double bisectThreshold(QSimVN &qsimvn, const Eigen::MatrixXd &identity, // ───────────────────────────────────────────────────────────────────────────── -AssignmentRule::AssignmentRule(std::mt19937_64 &rng) : m_Rng(rng) {} +AssignmentRule::AssignmentRule(std::mt19937_64& rng) +: m_Rng(rng) +{ +} // ───────────────────────────────────────────────────────────────────────────── // selectThresholds — port of select_AR.m "otherwise" branch. @@ -67,12 +74,12 @@ AssignmentRule::AssignmentRule(std::mt19937_64 &rng) : m_Rng(rng) {} // fraction is matched. The last component's thresholds are set to cover the // remaining region — no bisection needed. -AssignmentRuleThresholds -AssignmentRule::selectThresholds(const std::vector &volumeFractions) { +AssignmentRuleThresholds AssignmentRule::selectThresholds(const std::vector& volumeFractions) +{ const int numComponents = static_cast(volumeFractions.size()); - if (numComponents < 1) { - throw std::invalid_argument( - "AssignmentRule: volumeFractions must not be empty"); + if(numComponents < 1) + { + throw std::invalid_argument("AssignmentRule: volumeFractions must not be empty"); } const int numGaussians = numComponents - 1; @@ -83,14 +90,14 @@ AssignmentRule::selectThresholds(const std::vector &volumeFractions) { result.maxThresholds = Eigen::MatrixXd::Zero(numComponents, numGaussians); // Degenerate case: single component gets everything - if (numComponents == 1) { + if(numComponents == 1) + { // numGaussians = 0, matrices already have shape [1 x 0], nothing to do return result; } QSimVN qsimvn(m_Rng); - const Eigen::MatrixXd identity = - Eigen::MatrixXd::Identity(numGaussians, numGaussians); + const Eigen::MatrixXd identity = Eigen::MatrixXd::Identity(numGaussians, numGaussians); // ── Component 0 (MATLAB coi=1, goi=1) ────────────────────────────────────── // s = [-inf, ..., -inf], t = [inf, ..., inf] except t[0] is bisected. @@ -102,8 +109,7 @@ AssignmentRule::selectThresholds(const std::vector &volumeFractions) { Eigen::VectorXd sTmp = s; Eigen::VectorXd tTmp = t; - const double q = - bisectThreshold(qsimvn, identity, sTmp, tTmp, goi, volumeFractions[0]); + const double q = bisectThreshold(qsimvn, identity, sTmp, tTmp, goi, volumeFractions[0]); t(goi) = q; result.minThresholds.row(0) = s.transpose(); @@ -111,13 +117,15 @@ AssignmentRule::selectThresholds(const std::vector &volumeFractions) { } // ── Intermediate components (MATLAB loop aix = 2:num_components-1) ───────── - for (int coi = 1; coi < numComponents - 1; ++coi) { + for(int coi = 1; coi < numComponents - 1; ++coi) + { const int goi = coi; // s starts all -inf, then the diagonal entries from previous components // set s[i] = max_thresholds(i, i) for i = 0..coi-1 Eigen::VectorXd s = Eigen::VectorXd::Constant(numGaussians, -k_Inf); - for (int i = 0; i < coi; ++i) { + for(int i = 0; i < coi; ++i) + { s(i) = result.maxThresholds(i, i); } Eigen::VectorXd t = Eigen::VectorXd::Constant(numGaussians, k_Inf); @@ -125,8 +133,7 @@ AssignmentRule::selectThresholds(const std::vector &volumeFractions) { Eigen::VectorXd sTmp = s; Eigen::VectorXd tTmp = t; - const double q = bisectThreshold(qsimvn, identity, sTmp, tTmp, goi, - volumeFractions[coi]); + const double q = bisectThreshold(qsimvn, identity, sTmp, tTmp, goi, volumeFractions[coi]); t(goi) = q; result.minThresholds.row(coi) = s.transpose(); @@ -139,7 +146,8 @@ AssignmentRule::selectThresholds(const std::vector &volumeFractions) { { const int coi = numComponents - 1; Eigen::VectorXd s = Eigen::VectorXd::Constant(numGaussians, -k_Inf); - for (int i = 0; i < coi; ++i) { + for(int i = 0; i < coi; ++i) + { s(i) = result.maxThresholds(i, i); } const Eigen::VectorXd t = Eigen::VectorXd::Constant(numGaussians, k_Inf); @@ -159,9 +167,8 @@ AssignmentRule::selectThresholds(const std::vector &volumeFractions) { // conditions is violated. The assignment is the maximum surviving component // index (1-based, matching MATLAB). -Eigen::VectorXi -AssignmentRule::evaluate(const Eigen::MatrixXd &z, - const AssignmentRuleThresholds &thresholds) const { +Eigen::VectorXi AssignmentRule::evaluate(const Eigen::MatrixXd& z, const AssignmentRuleThresholds& thresholds) const +{ const int N = static_cast(z.rows()); const int numGaussians = static_cast(z.cols()); const int numComponents = static_cast(thresholds.minThresholds.rows()); @@ -170,17 +177,21 @@ AssignmentRule::evaluate(const Eigen::MatrixXd &z, std::vector componentList(static_cast(numComponents)); - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { // Initialise component list to {1, 2, ..., numComponents} (1-based) - for (int j = 0; j < numComponents; ++j) { + for(int j = 0; j < numComponents; ++j) + { componentList[static_cast(j)] = j + 1; } // Zero out components that violate any Gaussian threshold - for (int j = 0; j < numComponents; ++j) { - for (int k = 0; k < numGaussians; ++k) { - if (z(i, k) < thresholds.minThresholds(j, k) || - z(i, k) > thresholds.maxThresholds(j, k)) { + for(int j = 0; j < numComponents; ++j) + { + for(int k = 0; k < numGaussians; ++k) + { + if(z(i, k) < thresholds.minThresholds(j, k) || z(i, k) > thresholds.maxThresholds(j, k)) + { componentList[static_cast(j)] = 0; break; // once eliminated, no need to check remaining Gaussians for // this component @@ -190,8 +201,7 @@ AssignmentRule::evaluate(const Eigen::MatrixXd &z, // Assignment = maximum surviving component (matches MATLAB: // max(component_list)) - assignments(i) = - *std::max_element(componentList.begin(), componentList.end()); + assignments(i) = *std::max_element(componentList.begin(), componentList.end()); } return assignments; diff --git a/src/LibMTRSim/AssignmentRule.hpp b/src/LibMTRSim/AssignmentRule.hpp index fdc217a..d460f3c 100644 --- a/src/LibMTRSim/AssignmentRule.hpp +++ b/src/LibMTRSim/AssignmentRule.hpp @@ -5,12 +5,14 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Result of threshold selection for the plurigaussian assignment rule. */ -struct LIBMTRSIM_EXPORT AssignmentRuleThresholds { +struct LIBMTRSIM_EXPORT AssignmentRuleThresholds +{ int numGaussians = 0; // min_thresholds(component, gaussian) — shape [numComponents x numGaussians] @@ -25,9 +27,10 @@ struct LIBMTRSIM_EXPORT AssignmentRuleThresholds { * * Combines the logic of select_AR.m and eval_AR.m. */ -class LIBMTRSIM_EXPORT AssignmentRule { +class LIBMTRSIM_EXPORT AssignmentRule +{ public: - explicit AssignmentRule(std::mt19937_64 &rng); + explicit AssignmentRule(std::mt19937_64& rng); /** * @brief Determine Gaussian thresholds that yield the requested volume @@ -36,8 +39,7 @@ class LIBMTRSIM_EXPORT AssignmentRule { * @param volumeFractions Target probability for each MTR component * @return Threshold matrices for use in evaluate() */ - AssignmentRuleThresholds - selectThresholds(const std::vector &volumeFractions); + AssignmentRuleThresholds selectThresholds(const std::vector& volumeFractions); /** * @brief Classify each voxel given realizations of the latent Gaussians. @@ -47,11 +49,10 @@ class LIBMTRSIM_EXPORT AssignmentRule { * @return Integer component index (1-based) for each voxel, length * N */ - Eigen::VectorXi evaluate(const Eigen::MatrixXd &z, - const AssignmentRuleThresholds &thresholds) const; + Eigen::VectorXi evaluate(const Eigen::MatrixXd& z, const AssignmentRuleThresholds& thresholds) const; private: - std::mt19937_64 &m_Rng; + std::mt19937_64& m_Rng; }; } // namespace mtrsim diff --git a/src/LibMTRSim/CMakeLists.txt b/src/LibMTRSim/CMakeLists.txt index 31664b2..572e4f0 100644 --- a/src/LibMTRSim/CMakeLists.txt +++ b/src/LibMTRSim/CMakeLists.txt @@ -1,8 +1,11 @@ set(MTRSIM_HEADERS AssignmentRule.hpp + ConfigIO.hpp GPGenerator.hpp IPFMapper.hpp + ISimulationObserver.hpp MTRDataLoader.hpp + MTRSimDriver.hpp ODFBuilder.hpp ODFCalculator.hpp ODFFileIO.hpp @@ -10,13 +13,16 @@ set(MTRSIM_HEADERS PGRFSimulation.hpp PoleFigure.hpp QSimVN.hpp + SimulationObservers.hpp ) set(MTRSIM_SOURCES AssignmentRule.cpp + ConfigIO.cpp GPGenerator.cpp IPFMapper.cpp MTRDataLoader.cpp + MTRSimDriver.cpp ODFBuilder.cpp ODFCalculator.cpp ODFFileIO.cpp diff --git a/src/LibMTRSim/ConfigIO.cpp b/src/LibMTRSim/ConfigIO.cpp new file mode 100644 index 0000000..4a4608d --- /dev/null +++ b/src/LibMTRSim/ConfigIO.cpp @@ -0,0 +1,51 @@ +#include "ConfigIO.hpp" + +#include + +#include +#include + +namespace mtrsim +{ + +SimulationParams parseConfigJson(const std::filesystem::path& path) +{ + std::ifstream f(path); + if(!f.is_open()) + { + throw std::runtime_error("parseConfigJson: cannot open config file: " + path.string()); + } + SimulationParams params; + try + { + const nlohmann::json j = nlohmann::json::parse(f); + if(j.contains("xLen")) + params.xLen = j["xLen"].get(); + if(j.contains("yLen")) + params.yLen = j["yLen"].get(); + if(j.contains("zLen")) + params.zLen = j["zLen"].get(); + if(j.contains("dx")) + params.dx = j["dx"].get(); + if(j.contains("dy")) + params.dy = j["dy"].get(); + if(j.contains("dz")) + params.dz = j["dz"].get(); + if(j.contains("volumeFractions")) + params.volumeFractions = j["volumeFractions"].get>(); + if(j.contains("thetaList")) + params.thetaList = j["thetaList"].get>>(); + if(j.contains("nuggetVariance")) + params.nuggetVariance = j["nuggetVariance"].get>(); + if(j.contains("odfInputPath")) + params.odfInputPath = j["odfInputPath"].get(); + if(j.contains("seed")) + params.seed = j["seed"].get(); + } catch(const nlohmann::json::exception& e) + { + throw std::runtime_error(std::string("parseConfigJson: invalid JSON: ") + e.what()); + } + return params; +} + +} // namespace mtrsim diff --git a/src/LibMTRSim/ConfigIO.hpp b/src/LibMTRSim/ConfigIO.hpp new file mode 100644 index 0000000..ea81729 --- /dev/null +++ b/src/LibMTRSim/ConfigIO.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "SimulationParams.hpp" +#include "libmtrsim_export.h" + +#include + +namespace mtrsim +{ + +/** + * @brief Parse an MTRSim config JSON into a SimulationParams. + * + * Recognized keys: xLen,yLen,zLen, dx,dy,dz, volumeFractions, thetaList, + * nuggetVariance (parsed but unused by the simulation), odfInputPath, seed. + * Any comments / unknown keys are ignored. Fields absent from the JSON keep + * their SimulationParams defaults. + * + * @throws std::runtime_error if the file cannot be opened or the JSON is invalid. + */ +LIBMTRSIM_EXPORT SimulationParams parseConfigJson(const std::filesystem::path& path); + +} // namespace mtrsim diff --git a/src/LibMTRSim/GPGenerator.cpp b/src/LibMTRSim/GPGenerator.cpp index 82fd658..c349886 100644 --- a/src/LibMTRSim/GPGenerator.cpp +++ b/src/LibMTRSim/GPGenerator.cpp @@ -5,10 +5,14 @@ #include -namespace mtrsim { +namespace mtrsim +{ -GPGenerator::GPGenerator(std::mt19937_64 &rng, CorrelationFn corrFn) - : m_Rng(rng), m_CorrFn(std::move(corrFn)) {} +GPGenerator::GPGenerator(std::mt19937_64& rng, CorrelationFn corrFn) +: m_Rng(rng) +, m_CorrFn(std::move(corrFn)) +{ +} // ───────────────────────────────────────────────────────────────────────────── // buildCovarianceMatrix @@ -21,20 +25,22 @@ GPGenerator::GPGenerator(std::mt19937_64 &rng, CorrelationFn corrFn) // A small jitter (1e-6) is added to the diagonal to ensure positive // definiteness, matching MATLAB: Gamma = Gamma + 1e-6*eye(n). -Eigen::MatrixXd GPGenerator::buildCovarianceMatrix(int n, double spacing, - double theta) const { +Eigen::MatrixXd GPGenerator::buildCovarianceMatrix(int n, double spacing, double theta) const +{ Eigen::MatrixXd gamma = Eigen::MatrixXd::Zero(n, n); // Fill upper triangle: gamma(i,j) = rho((j-i)*spacing, theta) for j > i - for (int i = 0; i < n - 1; ++i) { - for (int j = i + 1; j < n; ++j) { + for(int i = 0; i < n - 1; ++i) + { + for(int j = i + 1; j < n; ++j) + { gamma(i, j) = m_CorrFn(static_cast(j - i) * spacing, theta); } } // Symmetrize (adds lower triangle from upper) then set diagonal = 1 + jitter // MATLAB: Gamma = Gamma + Gamma' + eye(n) + 1e-6*eye(n) - gamma += gamma.transpose(); + gamma += gamma.transpose().eval(); gamma.diagonal().array() += 1.0 + 1e-6; return gamma; @@ -59,9 +65,8 @@ Eigen::MatrixXd GPGenerator::buildCovarianceMatrix(int n, double spacing, // MATLAB: W_1 * Rz → C++: W1 * llt_z.matrixU() // MATLAB: Ry' * W2k * Rx → C++: llt_y.matrixL() * W2k * llt_x.matrixU() -Eigen::VectorXd GPGenerator::generate(double hx, double hy, double hz, - const std::array &theta, - int nx, int ny, int nz) { +Eigen::VectorXd GPGenerator::generate(double hx, double hy, double hz, const std::array& theta, int nx, int ny, int nz) +{ // ── Build per-direction covariance matrices ───────────────────────────── const Eigen::MatrixXd Gamma_x = buildCovarianceMatrix(nx, hx, theta[0]); const Eigen::MatrixXd Gamma_y = buildCovarianceMatrix(ny, hy, theta[1]); @@ -72,8 +77,8 @@ Eigen::VectorXd GPGenerator::generate(double hx, double hy, double hz, Eigen::LLT llt_y(Gamma_y); Eigen::LLT llt_z(Gamma_z); - if (llt_x.info() != Eigen::Success || llt_y.info() != Eigen::Success || - llt_z.info() != Eigen::Success) { + if(llt_x.info() != Eigen::Success || llt_y.info() != Eigen::Success || llt_z.info() != Eigen::Success) + { throw std::runtime_error("GPGenerator::generate — Cholesky failed; " "covariance matrix is not positive definite"); } @@ -88,7 +93,8 @@ Eigen::VectorXd GPGenerator::generate(double hx, double hy, double hz, const int N = nx * ny * nz; std::normal_distribution normal(0.0, 1.0); Eigen::VectorXd z(N); - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { z(i) = normal(m_Rng); } @@ -103,7 +109,8 @@ Eigen::VectorXd GPGenerator::generate(double hx, double hy, double hz, // x_k = Ry' * (W2k * Rx) — nx-covariance from right, ny from // left Eigen::VectorXd gp(N); - for (int k = 0; k < nz; ++k) { + for(int k = 0; k < nz; ++k) + { // W2k: ny × nx view into column k of u (column-major, contiguous in memory) Eigen::Map W2k(u.col(k).data(), ny, nx); @@ -112,8 +119,7 @@ Eigen::VectorXd GPGenerator::generate(double hx, double hy, double hz, // Store into output with MATLAB column-major ordering for a ny×nx×nz array: // gp[k*(ny*nx) + ix*ny + iy] = x_k(iy, ix) - Eigen::Map( - gp.data() + static_cast(k) * (ny * nx), ny, nx) = x_k; + Eigen::Map(gp.data() + static_cast(k) * (ny * nx), ny, nx) = x_k; } return gp; diff --git a/src/LibMTRSim/GPGenerator.hpp b/src/LibMTRSim/GPGenerator.hpp index 1f31527..dc43558 100644 --- a/src/LibMTRSim/GPGenerator.hpp +++ b/src/LibMTRSim/GPGenerator.hpp @@ -5,7 +5,8 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Generates a realization of a separable Gaussian random field via @@ -21,7 +22,8 @@ namespace mtrsim { * where Rx, Ry, Rz are the Cholesky factors of the per-direction covariance * matrices. */ -class LIBMTRSIM_EXPORT GPGenerator { +class LIBMTRSIM_EXPORT GPGenerator +{ public: using CorrelationFn = std::function; @@ -30,7 +32,7 @@ class LIBMTRSIM_EXPORT GPGenerator { * caller controls the global RNG state). * @param corrFn Correlation function: rho(|lag|, theta) → [0, 1] */ - explicit GPGenerator(std::mt19937_64 &rng, CorrelationFn corrFn); + explicit GPGenerator(std::mt19937_64& rng, CorrelationFn corrFn); /** * @brief Draw one realization of the GP on an nx × ny × nz grid. @@ -44,15 +46,12 @@ class LIBMTRSIM_EXPORT GPGenerator { * @param nz Number of voxels in z * @return Flattened field of length nx*ny*nz (z-major ordering) */ - Eigen::VectorXd generate(double hx, double hy, double hz, - const std::array &theta, int nx, int ny, - int nz); + Eigen::VectorXd generate(double hx, double hy, double hz, const std::array& theta, int nx, int ny, int nz); private: - Eigen::MatrixXd buildCovarianceMatrix(int n, double spacing, - double theta) const; + Eigen::MatrixXd buildCovarianceMatrix(int n, double spacing, double theta) const; - std::mt19937_64 &m_Rng; + std::mt19937_64& m_Rng; CorrelationFn m_CorrFn; }; diff --git a/src/LibMTRSim/IPFMapper.cpp b/src/LibMTRSim/IPFMapper.cpp index 03fdec2..6a3bf5b 100644 --- a/src/LibMTRSim/IPFMapper.cpp +++ b/src/LibMTRSim/IPFMapper.cpp @@ -21,14 +21,16 @@ #include #endif -namespace mtrsim { +namespace mtrsim +{ // ───────────────────────────────────────────────────────────────────────────── // Symmetry-operator Euler-angle tables (radians) — from MATLAB source. // Each row is { phi1, PHI, phi2 }. // ───────────────────────────────────────────────────────────────────────────── -namespace { +namespace +{ constexpr double k_Deg2Rad = std::numbers::pi / 180.0; // HCP — 12 operators @@ -54,7 +56,8 @@ constexpr std::array, 12> k_HcpSymOps = {{ // | −c1s2−s1c2C −s1s2+c1c2C c2S | // | s1S −c1S C | // ───────────────────────────────────────────────────────────────────────────── -inline Eigen::Matrix3d bungeRotationMatrix(double p1, double P, double p2) { +inline Eigen::Matrix3d bungeRotationMatrix(double p1, double P, double p2) +{ const double c1 = std::cos(p1), s1 = std::sin(p1); const double C = std::cos(P), S = std::sin(P); const double c2 = std::cos(p2), s2 = std::sin(p2); @@ -75,23 +78,24 @@ inline Eigen::Matrix3d bungeRotationMatrix(double p1, double P, double p2) { // ───────────────────────────────────────────────────────────────────────────── // Constructor -IPFMapper::IPFMapper(CrystalSystem system) : m_System(system) {} +IPFMapper::IPFMapper(CrystalSystem system) +: m_System(system) +{ +} // ───────────────────────────────────────────────────────────────────────────── // Public dispatch — routes to the selected colour scheme. -std::vector IPFMapper::eulerToColors(const Eigen::VectorXd &phi1, - const Eigen::VectorXd &phi, - const Eigen::VectorXd &phi2, - std::array refDir, - IPFColorScheme scheme) const { +std::vector IPFMapper::eulerToColors(const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2, std::array refDir, IPFColorScheme scheme) const +{ const int N = static_cast(phi1.size()); - if (phi.size() != N || phi2.size() != N) { - throw std::invalid_argument( - "IPFMapper::eulerToColors — phi1, phi, phi2 must have the same length"); + if(phi.size() != N || phi2.size() != N) + { + throw std::invalid_argument("IPFMapper::eulerToColors — phi1, phi, phi2 must have the same length"); } - if (scheme == IPFColorScheme::MatLab) { + if(scheme == IPFColorScheme::MatLab) + { return eulerToColorsMatLab(phi1, phi, phi2); } return eulerToColorsEbsdLib(phi1, phi, phi2, refDir); @@ -100,27 +104,29 @@ std::vector IPFMapper::eulerToColors(const Eigen::VectorXd &phi1, // ───────────────────────────────────────────────────────────────────────────── // EbsdLib colour path — delegates to LaueOps::generateIPFColor(). -std::vector IPFMapper::eulerToColorsEbsdLib( - const Eigen::VectorXd &phi1, const Eigen::VectorXd &phi, - const Eigen::VectorXd &phi2, std::array refDir) const { +std::vector IPFMapper::eulerToColorsEbsdLib(const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2, std::array refDir) const +{ const int N = static_cast(phi1.size()); std::shared_ptr ops; - if (m_System == CrystalSystem::HCP) { + if(m_System == CrystalSystem::HCP) + { ops = ebsdlib::HexagonalOps::New(); - } else if (m_System == CrystalSystem::FCC) { + } + else if(m_System == CrystalSystem::FCC) + { ops = ebsdlib::CubicOps::New(); - } else { - throw std::invalid_argument( - "IPFMapper::eulerToColorsEbsdLib — unsupported crystal system"); + } + else + { + throw std::invalid_argument("IPFMapper::eulerToColorsEbsdLib — unsupported crystal system"); } std::vector colors(static_cast(N)); auto computeColor = [&](int i) { double eulers[3] = {phi1[i], phi[i], phi2[i]}; - const ebsdlib::Rgb argb = - ops->generateIPFColor(eulers, refDir.data(), false); + const ebsdlib::Rgb argb = ops->generateIPFColor(eulers, refDir.data(), false); const std::size_t idx = static_cast(i); colors[idx].r = static_cast(ebsdlib::RgbColor::dRed(argb)); colors[idx].g = static_cast(ebsdlib::RgbColor::dGreen(argb)); @@ -130,7 +136,8 @@ std::vector IPFMapper::eulerToColorsEbsdLib( #if MTRSIM_HAS_TBB tbb::parallel_for(0, N, computeColor); #else - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { computeColor(i); } #endif @@ -156,28 +163,24 @@ std::vector IPFMapper::eulerToColorsEbsdLib( // green = r·(1 − β/(π/6)) / max(…) // blue = r·β/(π/6) / max(…) -std::vector -IPFMapper::eulerToColorsMatLab(const Eigen::VectorXd &phi1, - const Eigen::VectorXd &phi, - const Eigen::VectorXd &phi2) const { - if (m_System != CrystalSystem::HCP) { +std::vector IPFMapper::eulerToColorsMatLab(const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2) const +{ + if(m_System != CrystalSystem::HCP) + { throw std::invalid_argument("IPFMapper::eulerToColorsMatLab — MatLab " "colour scheme currently supports HCP only"); } const int N = static_cast(phi1.size()); constexpr int numOps = static_cast(k_HcpSymOps.size()); - constexpr double k_MaxAngle = - std::numbers::pi / 6.0; // 30° — HCP fundamental-zone arc + constexpr double k_MaxAngle = std::numbers::pi / 6.0; // 30° — HCP fundamental-zone arc const double tanMaxAngle = std::tan(k_MaxAngle); // Pre-compute symmetry-operator rotation matrices std::array symMats; - for (int k = 0; k < numOps; ++k) { - symMats[static_cast(k)] = - bungeRotationMatrix(k_HcpSymOps[static_cast(k)][0], - k_HcpSymOps[static_cast(k)][1], - k_HcpSymOps[static_cast(k)][2]); + for(int k = 0; k < numOps; ++k) + { + symMats[static_cast(k)] = bungeRotationMatrix(k_HcpSymOps[static_cast(k)][0], k_HcpSymOps[static_cast(k)][1], k_HcpSymOps[static_cast(k)][2]); } std::vector colors(static_cast(N)); @@ -185,7 +188,8 @@ IPFMapper::eulerToColorsMatLab(const Eigen::VectorXd &phi1, auto computeColor = [&](int i) { // Replicate the MATLAB guard: nudge phi2 away from exact zero double p2 = phi2[i]; - if (p2 == 0.0) { + if(p2 == 0.0) + { p2 += 1.0e-15; } @@ -194,7 +198,8 @@ IPFMapper::eulerToColorsMatLab(const Eigen::VectorXd &phi1, double xFund = 0.0; double yFund = 0.0; - for (int k = 0; k < numOps; ++k) { + for(int k = 0; k < numOps; ++k) + { // g_rot_symm = O_k * G (matches MATLAB exactly) const Eigen::Matrix3d R = symMats[static_cast(k)] * G; @@ -204,7 +209,8 @@ IPFMapper::eulerToColorsMatLab(const Eigen::VectorXd &phi1, double hz = R(2, 2); // Flip to lower hemisphere (MATLAB: h_rot_3 > 0 → negate) - if (hz > 0.0) { + if(hz > 0.0) + { hx = -hx; hy = -hy; hz = -hz; @@ -216,7 +222,8 @@ IPFMapper::eulerToColorsMatLab(const Eigen::VectorXd &phi1, const double Y = hy / denom; // Fundamental-zone check (HCP unit triangle) - if (X >= 0.0 && Y >= 0.0 && Y <= X * tanMaxAngle) { + if(X >= 0.0 && Y >= 0.0 && Y <= X * tanMaxAngle) + { xFund = X; yFund = Y; break; @@ -233,25 +240,24 @@ IPFMapper::eulerToColorsMatLab(const Eigen::VectorXd &phi1, // Normalise so that the brightest channel is 1.0 const double maxC = std::max({cmap1, cmap2, cmap3}); - if (maxC > 0.0) { + if(maxC > 0.0) + { cmap1 /= maxC; cmap2 /= maxC; cmap3 /= maxC; } const std::size_t idx = static_cast(i); - colors[idx].r = - static_cast(std::clamp(std::lround(cmap1 * 255.0), 0L, 255L)); - colors[idx].g = - static_cast(std::clamp(std::lround(cmap2 * 255.0), 0L, 255L)); - colors[idx].b = - static_cast(std::clamp(std::lround(cmap3 * 255.0), 0L, 255L)); + colors[idx].r = static_cast(std::clamp(std::lround(cmap1 * 255.0), 0L, 255L)); + colors[idx].g = static_cast(std::clamp(std::lround(cmap2 * 255.0), 0L, 255L)); + colors[idx].b = static_cast(std::clamp(std::lround(cmap3 * 255.0), 0L, 255L)); }; #if MTRSIM_HAS_TBB tbb::parallel_for(0, N, computeColor); #else - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { computeColor(i); } #endif @@ -262,20 +268,16 @@ IPFMapper::eulerToColorsMatLab(const Eigen::VectorXd &phi1, // ───────────────────────────────────────────────────────────────────────────── // writePNG — renders the IPF colour map to a PNG file. -void IPFMapper::writePNG(const Eigen::MatrixXd &spatialCoords, - const Eigen::VectorXd &phi1_in, - const Eigen::VectorXd &phi_in, - const Eigen::VectorXd &phi2_in, - const std::string &outputPath, - IPFColorScheme scheme) const { +void IPFMapper::writePNG(const Eigen::MatrixXd& spatialCoords, const Eigen::VectorXd& phi1_in, const Eigen::VectorXd& phi_in, const Eigen::VectorXd& phi2_in, const std::string& outputPath, + IPFColorScheme scheme) const +{ const int N = static_cast(phi1_in.size()); - if (spatialCoords.rows() != N || spatialCoords.cols() < 2) { - throw std::invalid_argument( - "IPFMapper::writePNG — spatialCoords must have N rows and ≥ 2 columns"); + if(spatialCoords.rows() != N || spatialCoords.cols() < 2) + { + throw std::invalid_argument("IPFMapper::writePNG — spatialCoords must have N rows and ≥ 2 columns"); } - const std::vector colors = - eulerToColors(phi1_in, phi_in, phi2_in, {0.0, 0.0, 1.0}, scheme); + const std::vector colors = eulerToColors(phi1_in, phi_in, phi2_in, {0.0, 0.0, 1.0}, scheme); // ── Build sorted unique coordinate lists // ──────────────────────────────────── @@ -286,12 +288,14 @@ void IPFMapper::writePNG(const Eigen::MatrixXd &spatialCoords, const double yRange = yCol.maxCoeff() - yCol.minCoeff(); const double tol = 1.0e-6 * std::max({xRange, yRange, 1.0}); - auto sortedUnique = [&](const Eigen::VectorXd &v) -> std::vector { + auto sortedUnique = [&](const Eigen::VectorXd& v) -> std::vector { std::vector vals(v.data(), v.data() + v.size()); std::sort(vals.begin(), vals.end()); std::vector unique; - for (const double val : vals) { - if (unique.empty() || std::abs(val - unique.back()) > tol) { + for(const double val : vals) + { + if(unique.empty() || std::abs(val - unique.back()) > tol) + { unique.push_back(val); } } @@ -303,7 +307,8 @@ void IPFMapper::writePNG(const Eigen::MatrixXd &spatialCoords, const int width = static_cast(xUnique.size()); const int height = static_cast(yUnique.size()); - if (width < 1 || height < 1) { + if(width < 1 || height < 1) + { throw std::runtime_error("IPFMapper::writePNG — could not determine image " "dimensions from spatialCoords"); } @@ -312,15 +317,17 @@ void IPFMapper::writePNG(const Eigen::MatrixXd &spatialCoords, // ────────────────────────────── std::vector pixels(static_cast(width * height * 3), 0u); - auto findRank = [&](const std::vector &sorted, double val) -> int { + auto findRank = [&](const std::vector& sorted, double val) -> int { const auto it = std::lower_bound(sorted.begin(), sorted.end(), val - tol); return static_cast(std::distance(sorted.begin(), it)); }; - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { const int col = findRank(xUnique, xCol[i]); const int row = findRank(yUnique, yCol[i]); - if (col < 0 || col >= width || row < 0 || row >= height) { + if(col < 0 || col >= width || row < 0 || row >= height) + { continue; } const std::size_t px = static_cast((row * width + col) * 3); @@ -333,10 +340,9 @@ void IPFMapper::writePNG(const Eigen::MatrixXd &spatialCoords, // ── Write PNG // ─────────────────────────────────────────────────────────────── const int stride = width * 3; - if (stbi_write_png(outputPath.c_str(), width, height, 3, pixels.data(), - stride) == 0) { - throw std::runtime_error("IPFMapper::writePNG — stbi_write_png failed: " + - outputPath); + if(stbi_write_png(outputPath.c_str(), width, height, 3, pixels.data(), stride) == 0) + { + throw std::runtime_error("IPFMapper::writePNG — stbi_write_png failed: " + outputPath); } } @@ -350,9 +356,10 @@ void IPFMapper::writePNG(const Eigen::MatrixXd &spatialCoords, // • Y ≤ X·tan(π/6) (upper edge: [0001] → [10-10]) // • X² + Y² ≤ 1 (arc: [2-1-10] → [10-10]) -void IPFMapper::writeIPFTriangleLegendMatLab( - int imageDim, const std::string &outputPath) const { - if (m_System != CrystalSystem::HCP) { +void IPFMapper::writeIPFTriangleLegendMatLab(int imageDim, const std::string& outputPath) const +{ + if(m_System != CrystalSystem::HCP) + { throw std::invalid_argument("IPFMapper::writeIPFTriangleLegendMatLab — " "currently supports HCP only"); } @@ -371,22 +378,22 @@ void IPFMapper::writeIPFTriangleLegendMatLab( const double yRange = yMax - yMin; const int width = imageDim; - const int height = - std::max(1, static_cast(std::round(imageDim * yRange / xRange))); + const int height = std::max(1, static_cast(std::round(imageDim * yRange / xRange))); // White background - std::vector pixels(static_cast(width * height * 3), - 255u); + std::vector pixels(static_cast(width * height * 3), 255u); - for (int row = 0; row < height; ++row) { - for (int col = 0; col < width; ++col) { + for(int row = 0; row < height; ++row) + { + for(int col = 0; col < width; ++col) + { // Map pixel centre to stereographic coordinates (Y increases upward) const double X = xMin + (static_cast(col) + 0.5) * xRange / width; - const double Y = - yMax - (static_cast(row) + 0.5) * yRange / height; + const double Y = yMax - (static_cast(row) + 0.5) * yRange / height; // Fundamental-zone test - if (X < 0.0 || Y < 0.0 || Y > X * tanMaxAngle || (X * X + Y * Y) > 1.0) { + if(X < 0.0 || Y < 0.0 || Y > X * tanMaxAngle || (X * X + Y * Y) > 1.0) + { continue; } @@ -399,28 +406,24 @@ void IPFMapper::writeIPFTriangleLegendMatLab( double cmap3 = r * (beta / k_MaxAngle); const double maxC = std::max({cmap1, cmap2, cmap3}); - if (maxC > 0.0) { + if(maxC > 0.0) + { cmap1 /= maxC; cmap2 /= maxC; cmap3 /= maxC; } const std::size_t px = static_cast((row * width + col) * 3); - pixels[px] = static_cast( - std::clamp(std::lround(cmap1 * 255.0), 0L, 255L)); - pixels[px + 1] = static_cast( - std::clamp(std::lround(cmap2 * 255.0), 0L, 255L)); - pixels[px + 2] = static_cast( - std::clamp(std::lround(cmap3 * 255.0), 0L, 255L)); + pixels[px] = static_cast(std::clamp(std::lround(cmap1 * 255.0), 0L, 255L)); + pixels[px + 1] = static_cast(std::clamp(std::lround(cmap2 * 255.0), 0L, 255L)); + pixels[px + 2] = static_cast(std::clamp(std::lround(cmap3 * 255.0), 0L, 255L)); } } const int stride = width * 3; - if (stbi_write_png(outputPath.c_str(), width, height, 3, pixels.data(), - stride) == 0) { - throw std::runtime_error( - "IPFMapper::writeIPFTriangleLegendMatLab — stbi_write_png failed: " + - outputPath); + if(stbi_write_png(outputPath.c_str(), width, height, 3, pixels.data(), stride) == 0) + { + throw std::runtime_error("IPFMapper::writeIPFTriangleLegendMatLab — stbi_write_png failed: " + outputPath); } } diff --git a/src/LibMTRSim/IPFMapper.hpp b/src/LibMTRSim/IPFMapper.hpp index 766f79a..47a32e9 100644 --- a/src/LibMTRSim/IPFMapper.hpp +++ b/src/LibMTRSim/IPFMapper.hpp @@ -6,7 +6,8 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Enumerates the crystal symmetry groups that IPFMapper understands. @@ -16,15 +17,19 @@ namespace mtrsim { * crystallographic abstraction: orientation math always routes through * EbsdLib's `LaueOps` and `ebsdlib::CrystalStructure::*` codes directly. */ -enum class CrystalSystem { - HCP, ///< Hexagonal close-packed (maps to ebsdlib::CrystalStructure::Hexagonal_High) - FCC, ///< Face-centred cubic (maps to ebsdlib::CrystalStructure::Cubic_High) +enum class CrystalSystem +{ + HCP, ///< Hexagonal close-packed (maps to + ///< ebsdlib::CrystalStructure::Hexagonal_High) + FCC, ///< Face-centred cubic (maps to + ///< ebsdlib::CrystalStructure::Cubic_High) }; /** * @brief Selects the IPF colour-mapping algorithm. */ -enum class IPFColorScheme { +enum class IPFColorScheme +{ EbsdLib, ///< Standard EbsdLib/TSL IPF colouring MatLab, ///< Original MATLAB port colouring (Sparkman 2017) }; @@ -32,7 +37,8 @@ enum class IPFColorScheme { /** * @brief RGB colour triple in [0, 255] uint8 range. */ -struct LIBMTRSIM_EXPORT RGBColor { +struct LIBMTRSIM_EXPORT RGBColor +{ uint8_t r = 0; uint8_t g = 0; uint8_t b = 0; @@ -48,7 +54,8 @@ struct LIBMTRSIM_EXPORT RGBColor { * using polar-coordinate colour mapping inside the stereographic * unit triangle. Currently supports HCP only. */ -class LIBMTRSIM_EXPORT IPFMapper { +class LIBMTRSIM_EXPORT IPFMapper +{ public: explicit IPFMapper(CrystalSystem system = CrystalSystem::HCP); @@ -64,11 +71,8 @@ class LIBMTRSIM_EXPORT IPFMapper { * @param scheme Colour-mapping algorithm (default: EbsdLib) * @return Per-voxel RGB colours, length N, values in [0, 255] */ - std::vector - eulerToColors(const Eigen::VectorXd &phi1, const Eigen::VectorXd &phi, - const Eigen::VectorXd &phi2, - std::array refDir = {0.0, 0.0, 1.0}, - IPFColorScheme scheme = IPFColorScheme::EbsdLib) const; + std::vector eulerToColors(const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2, std::array refDir = {0.0, 0.0, 1.0}, + IPFColorScheme scheme = IPFColorScheme::EbsdLib) const; /** * @brief Render an IPF map image and write it to a PNG file. @@ -80,9 +84,7 @@ class LIBMTRSIM_EXPORT IPFMapper { * @param outputPath Destination PNG file path * @param scheme Colour-mapping algorithm (default: EbsdLib) */ - void writePNG(const Eigen::MatrixXd &spatialCoords, - const Eigen::VectorXd &phi1, const Eigen::VectorXd &phi, - const Eigen::VectorXd &phi2, const std::string &outputPath, + void writePNG(const Eigen::MatrixXd& spatialCoords, const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2, const std::string& outputPath, IPFColorScheme scheme = IPFColorScheme::EbsdLib) const; /** @@ -100,22 +102,16 @@ class LIBMTRSIM_EXPORT IPFMapper { * computed to preserve the triangle's aspect ratio. * @param outputPath Destination PNG file path. */ - void writeIPFTriangleLegendMatLab(int imageDim, - const std::string &outputPath) const; + void writeIPFTriangleLegendMatLab(int imageDim, const std::string& outputPath) const; private: CrystalSystem m_System; /// EbsdLib colour path (delegates to LaueOps::generateIPFColor). - std::vector - eulerToColorsEbsdLib(const Eigen::VectorXd &phi1, const Eigen::VectorXd &phi, - const Eigen::VectorXd &phi2, - std::array refDir) const; + std::vector eulerToColorsEbsdLib(const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2, std::array refDir) const; /// MATLAB colour path — port of unit_triangle_IPF_coords.m + IPF_colors.m. - std::vector eulerToColorsMatLab(const Eigen::VectorXd &phi1, - const Eigen::VectorXd &phi, - const Eigen::VectorXd &phi2) const; + std::vector eulerToColorsMatLab(const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2) const; }; } // namespace mtrsim diff --git a/src/LibMTRSim/ISimulationObserver.hpp b/src/LibMTRSim/ISimulationObserver.hpp new file mode 100644 index 0000000..5c9b8da --- /dev/null +++ b/src/LibMTRSim/ISimulationObserver.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "libmtrsim_export.h" + +#include +#include + +namespace mtrsim +{ + +/** + * @brief Observer interface for long-running MTR simulations. + * + * Implementations receive throttled progress updates and are polled for + * cancellation from the thread running simulateMTR. They must be cheap and + * must not throw. + */ +class LIBMTRSIM_EXPORT ISimulationObserver +{ +public: + ISimulationObserver() = default; + virtual ~ISimulationObserver() = default; + + ISimulationObserver(const ISimulationObserver&) = delete; + ISimulationObserver& operator=(const ISimulationObserver&) = delete; + ISimulationObserver(ISimulationObserver&&) = delete; + ISimulationObserver& operator=(ISimulationObserver&&) = delete; + + /// Report progress. `done`/`total` describe the current phase; `message` + /// names it. `total <= 0` means "indeterminate". + virtual void updateProgress(int64_t done, int64_t total, const std::string& message) = 0; + + /// Emit an informational/diagnostic message (not progress). Default: no-op. + virtual void info(const std::string& message) + { + (void)message; + } + + /// Polled at checkpoints; returning true stops the simulation early. + [[nodiscard]] virtual bool shouldCancel() const = 0; +}; + +} // namespace mtrsim diff --git a/src/LibMTRSim/MTRDataLoader.cpp b/src/LibMTRSim/MTRDataLoader.cpp index 90f5feb..04134c8 100644 --- a/src/LibMTRSim/MTRDataLoader.cpp +++ b/src/LibMTRSim/MTRDataLoader.cpp @@ -9,28 +9,33 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ -namespace { +namespace +{ // Read a single-column CSV (one numeric value per non-empty line). -std::vector readColumn(const std::string &path) { +std::vector readColumn(const std::string& path) +{ std::ifstream f(path); - if (!f.is_open()) { + if(!f.is_open()) + { throw std::runtime_error("MTRDataLoader: cannot open file: " + path); } std::vector vals; std::string line; - while (std::getline(f, line)) { + while(std::getline(f, line)) + { // Strip trailing commas and whitespace (MATLAB csvwrite sometimes adds // them) - while (!line.empty() && - (line.back() == ',' || line.back() == '\r' || - std::isspace(static_cast(line.back())))) { + while(!line.empty() && (line.back() == ',' || line.back() == '\r' || std::isspace(static_cast(line.back())))) + { line.pop_back(); } - if (line.empty()) { + if(line.empty()) + { continue; } vals.push_back(std::stod(line)); @@ -40,44 +45,49 @@ std::vector readColumn(const std::string &path) { // Read a multi-column CSV (comma-separated, N rows × ncols columns). // Returns a flat row-major vector: [row0_col0, row0_col1, ..., row1_col0, ...]. -std::vector readMultiColumn(const std::string &path, int &outNRows, - int &outNCols) { +std::vector readMultiColumn(const std::string& path, int& outNRows, int& outNCols) +{ std::ifstream f(path); - if (!f.is_open()) { + if(!f.is_open()) + { throw std::runtime_error("MTRDataLoader: cannot open file: " + path); } std::vector> rows; std::string line; - while (std::getline(f, line)) { + while(std::getline(f, line)) + { // Strip trailing whitespace/CR - while (!line.empty() && - (line.back() == '\r' || - std::isspace(static_cast(line.back())))) { + while(!line.empty() && (line.back() == '\r' || std::isspace(static_cast(line.back())))) + { line.pop_back(); } - if (line.empty()) { + if(line.empty()) + { continue; } std::vector row; std::stringstream ss(line); std::string token; - while (std::getline(ss, token, ',')) { + while(std::getline(ss, token, ',')) + { // Trim token whitespace - while (!token.empty() && - std::isspace(static_cast(token.front()))) { + while(!token.empty() && std::isspace(static_cast(token.front()))) + { token.erase(token.begin()); } - while (!token.empty() && - std::isspace(static_cast(token.back()))) { + while(!token.empty() && std::isspace(static_cast(token.back()))) + { token.pop_back(); } - if (!token.empty()) { + if(!token.empty()) + { row.push_back(std::stod(token)); } } - if (!row.empty()) { + if(!row.empty()) + { rows.push_back(std::move(row)); } } @@ -87,8 +97,10 @@ std::vector readMultiColumn(const std::string &path, int &outNRows, std::vector flat; flat.reserve(static_cast(outNRows * outNCols)); - for (const auto &row : rows) { - for (int c = 0; c < outNCols; ++c) { + for(const auto& row : rows) + { + for(int c = 0; c < outNCols; ++c) + { flat.push_back(c < static_cast(row.size()) ? row[c] : 0.0); } } @@ -101,7 +113,8 @@ std::vector readMultiColumn(const std::string &path, int &outNRows, // load — reads five CSV files from directoryPath: // X_Position.csv, Y_Position.csv, EulerAngles.csv, ParentIds.csv, BoolMTR.csv -EBSDData MTRDataLoader::load(const std::string &directoryPath) { +EBSDData MTRDataLoader::load(const std::string& directoryPath) +{ const std::string sep = directoryPath + "/"; // ── Spatial coordinates @@ -110,35 +123,34 @@ EBSDData MTRDataLoader::load(const std::string &directoryPath) { const std::vector yVals = readColumn(sep + "Y_Position.csv"); const int N = static_cast(xVals.size()); - if (static_cast(yVals.size()) != N) { - throw std::runtime_error( - "MTRDataLoader: X_Position and Y_Position row counts differ"); + if(static_cast(yVals.size()) != N) + { + throw std::runtime_error("MTRDataLoader: X_Position and Y_Position row counts differ"); } // ── Euler angles (N × 3) // ───────────────────────────────────────────────────── int nRowsEuler = 0, nColsEuler = 0; - const std::vector eulerFlat = - readMultiColumn(sep + "EulerAngles.csv", nRowsEuler, nColsEuler); - if (nRowsEuler != N || nColsEuler < 3) { - throw std::runtime_error( - "MTRDataLoader: EulerAngles.csv must have N rows × 3 columns"); + const std::vector eulerFlat = readMultiColumn(sep + "EulerAngles.csv", nRowsEuler, nColsEuler); + if(nRowsEuler != N || nColsEuler < 3) + { + throw std::runtime_error("MTRDataLoader: EulerAngles.csv must have N rows × 3 columns"); } // ── Parent IDs // ─────────────────────────────────────────────────────────────── const std::vector parentVals = readColumn(sep + "ParentIds.csv"); - if (static_cast(parentVals.size()) != N) { - throw std::runtime_error( - "MTRDataLoader: ParentIds.csv row count differs from N"); + if(static_cast(parentVals.size()) != N) + { + throw std::runtime_error("MTRDataLoader: ParentIds.csv row count differs from N"); } // ── MTR boolean mask // ───────────────────────────────────────────────────────── const std::vector boolVals = readColumn(sep + "BoolMTR.csv"); - if (static_cast(boolVals.size()) != N) { - throw std::runtime_error( - "MTRDataLoader: BoolMTR.csv row count differs from N"); + if(static_cast(boolVals.size()) != N) + { + throw std::runtime_error("MTRDataLoader: BoolMTR.csv row count differs from N"); } // ── Pack into EBSDData @@ -146,13 +158,15 @@ EBSDData MTRDataLoader::load(const std::string &directoryPath) { EBSDData data; data.spatialCoords.resize(N, 2); - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { data.spatialCoords(i, 0) = xVals[static_cast(i)]; data.spatialCoords(i, 1) = yVals[static_cast(i)]; } data.eulerAngles.resize(N, 3); - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { const std::size_t base = static_cast(i * nColsEuler); data.eulerAngles(i, 0) = eulerFlat[base]; data.eulerAngles(i, 1) = eulerFlat[base + 1]; @@ -161,7 +175,8 @@ EBSDData MTRDataLoader::load(const std::string &directoryPath) { data.parentIds.resize(N); data.isMTR.resize(N); - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { const std::size_t si = static_cast(i); data.parentIds[i] = static_cast(parentVals[si]); data.isMTR[i] = (boolVals[si] != 0.0); diff --git a/src/LibMTRSim/MTRDataLoader.hpp b/src/LibMTRSim/MTRDataLoader.hpp index 855b900..d8d8c6b 100644 --- a/src/LibMTRSim/MTRDataLoader.hpp +++ b/src/LibMTRSim/MTRDataLoader.hpp @@ -4,14 +4,16 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief EBSD scan data loaded from CSV files. * * Matches the variables read by load_MTR_data.m. */ -struct LIBMTRSIM_EXPORT EBSDData { +struct LIBMTRSIM_EXPORT EBSDData +{ Eigen::MatrixXd spatialCoords; ///< [N x 2] (X, Y) positions [mm] Eigen::MatrixXd eulerAngles; ///< [N x 3] (phi1, PHI, phi2) [rad] Eigen::VectorXi parentIds; ///< MTR parent grain IDs, length N @@ -28,7 +30,8 @@ struct LIBMTRSIM_EXPORT EBSDData { * - ParentIds.csv (N rows × 1 col, integer grain IDs) * - BoolMTR.csv (N rows × 1 col, 0 or 1) */ -class LIBMTRSIM_EXPORT MTRDataLoader { +class LIBMTRSIM_EXPORT MTRDataLoader +{ public: MTRDataLoader() = default; @@ -38,7 +41,7 @@ class LIBMTRSIM_EXPORT MTRDataLoader { * @param directoryPath Path to the directory containing the CSV files * @return Populated EBSDData struct */ - EBSDData load(const std::string &directoryPath); + EBSDData load(const std::string& directoryPath); }; } // namespace mtrsim diff --git a/src/LibMTRSim/MTRSimDriver.cpp b/src/LibMTRSim/MTRSimDriver.cpp new file mode 100644 index 0000000..986be86 --- /dev/null +++ b/src/LibMTRSim/MTRSimDriver.cpp @@ -0,0 +1,158 @@ +#include "MTRSimDriver.hpp" + +#include "ODFSampler.hpp" +#include "PGRFSimulation.hpp" + +#include +#include +#include +#include +#include + +namespace mtrsim +{ + +ODFComponent buildUniformODF(int n1, int nPHI, int n2) +{ + const int nTotal = n1 * nPHI * n2; + const double twoPiOverN1 = 2.0 * std::numbers::pi / static_cast(n1); + const double piOverNPHI = std::numbers::pi / static_cast(nPHI); + const double twoPiOverN2 = 2.0 * std::numbers::pi / static_cast(n2); + + Eigen::VectorXd phi1Bins(nTotal); + Eigen::VectorXd phiBins(nTotal); + Eigen::VectorXd phi2Bins(nTotal); + + for(int ix = 0; ix < nTotal; ++ix) + { + const int i1 = ix / (nPHI * n2); + const int iPHI = (ix % (nPHI * n2)) / n2; + const int i2 = ix % n2; + phi1Bins[ix] = (i1 + 0.5) * twoPiOverN1; + phiBins[ix] = (iPHI + 0.5) * piOverNPHI; + phi2Bins[ix] = (i2 + 0.5) * twoPiOverN2; + } + + ODFComponent uni; + uni.odfVal = Eigen::VectorXd::Constant(nTotal, 1.0 / static_cast(nTotal)); + uni.phi1Bins = std::move(phi1Bins); + uni.phiBins = std::move(phiBins); + uni.phi2Bins = std::move(phi2Bins); + return uni; +} + +ODFComponent gridToODFComponent(const std::vector& values, int n1, int nPHI, int n2, double stepDeg1, double stepDegPHI, double stepDeg2) +{ + const int nTotal = n1 * nPHI * n2; + const double deg2rad = std::numbers::pi / 180.0; + const double s1 = stepDeg1 * deg2rad; + const double sP = stepDegPHI * deg2rad; + const double s2 = stepDeg2 * deg2rad; + + Eigen::VectorXd phi1Bins(nTotal); + Eigen::VectorXd phiBins(nTotal); + Eigen::VectorXd phi2Bins(nTotal); + for(int ix = 0; ix < nTotal; ++ix) + { + const int i1 = ix / (nPHI * n2); + const int iPHI = (ix % (nPHI * n2)) / n2; + const int i2 = ix % n2; + phi1Bins[ix] = (i1 + 0.5) * s1; + phiBins[ix] = (iPHI + 0.5) * sP; + phi2Bins[ix] = (i2 + 0.5) * s2; + } + + ODFComponent c; + c.odfVal = Eigen::Map(values.data(), nTotal); + const double total = c.odfVal.sum(); + if(total > 0.0) + { + c.odfVal /= total; + } + c.phi1Bins = std::move(phi1Bins); + c.phiBins = std::move(phiBins); + c.phi2Bins = std::move(phi2Bins); + return c; +} + +MTRSimResult simulateMTR(const SimulationParams& params, const std::vector& odfComponents, std::mt19937_64& rng, int n1, int nPHI, int n2, ISimulationObserver* observer) +{ + const int nx = static_cast(std::round(params.xLen / params.dx)); + const int ny = static_cast(std::round(params.yLen / params.dy)); + const int nz = std::max(static_cast(std::round(params.zLen / params.dz)), 1); + const int N = nx * ny * nz; + + if(static_cast(odfComponents.size()) != static_cast(params.volumeFractions.size())) + { + throw std::invalid_argument("simulateMTR: odfComponents count must equal volumeFractions count"); + } + + auto cancelled = [&]() { return observer != nullptr && observer->shouldCancel(); }; + auto report = [&](int64_t done, int64_t total, const std::string& msg) { + if(observer != nullptr) + { + observer->updateProgress(done, total, msg); + } + }; + + // 1. PGRF assignment (sim-ordered, 1-based component ids). + report(0, 100, "Running plurigaussian field simulation"); + PGRFSimulation pgrf{rng}; + const PGRFResult pgrf_result = pgrf.run(params, observer); // throws on bad dims + if(cancelled()) + { + MTRSimResult out; + out.cancelled = true; + return out; + } + + if(static_cast(pgrf_result.mtrIndex.size()) != N) + { + throw std::runtime_error("simulateMTR: PGRF result size does not match grid dimensions"); + } + + // 2. Sample N orientations per component against the uniform reference. + const ODFComponent uniformOdf = buildUniformODF(n1, nPHI, n2); + const int numComponents = static_cast(odfComponents.size()); + std::vector orientSamples(static_cast(numComponents)); + ODFSampler sampler{rng}; + for(int j = 0; j < numComponents; ++j) + { + report(j, numComponents, fmt::format("Sampling orientations (component {}/{})", j + 1, numComponents)); + orientSamples[static_cast(j)] = sampler.sampleN(N, odfComponents[static_cast(j)], uniformOdf, observer); + if(cancelled()) + { + MTRSimResult out; + out.cancelled = true; + return out; + } + } + + // 3. Assign per-voxel orientation by component (sim order). + std::vector mtrSim(static_cast(N)); + std::vector phi1Sim(static_cast(N)); + std::vector phiSim(static_cast(N)); + std::vector phi2Sim(static_cast(N)); + for(int i = 0; i < N; ++i) + { + const int comp = pgrf_result.mtrIndex[i] - 1; + mtrSim[static_cast(i)] = pgrf_result.mtrIndex[i]; + phi1Sim[static_cast(i)] = orientSamples[static_cast(comp)](i, 0); + phiSim[static_cast(i)] = orientSamples[static_cast(comp)](i, 1); + phi2Sim[static_cast(i)] = orientSamples[static_cast(comp)](i, 2); + } + + // 4. Remap to SIMPLNX z,y,x order. + report(100, 100, "Finalizing microstructure"); + MTRSimResult out; + out.nx = nx; + out.ny = ny; + out.nz = nz; + out.mtrIndex = remapSimToZYX(mtrSim, nx, ny, nz); + out.phi1 = remapSimToZYX(phi1Sim, nx, ny, nz); + out.phi = remapSimToZYX(phiSim, nx, ny, nz); + out.phi2 = remapSimToZYX(phi2Sim, nx, ny, nz); + return out; +} + +} // namespace mtrsim diff --git a/src/LibMTRSim/MTRSimDriver.hpp b/src/LibMTRSim/MTRSimDriver.hpp new file mode 100644 index 0000000..9b09c96 --- /dev/null +++ b/src/LibMTRSim/MTRSimDriver.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include "libmtrsim_export.h" + +#include "ISimulationObserver.hpp" +#include "ODFSampler.hpp" // mtrsim::ODFComponent, mtrsim::EulerAngles +#include "SimulationParams.hpp" + +#include +#include +#include +#include +#include + +namespace mtrsim +{ + +/** + * @brief Build a uniform reference ODF on an (n1 x nPHI x n2) Euler grid. + * + * The flat bin-centre arrays match exactly what ODFCalculator::compute and the + * ODFSampler expect, with ix = i1*(nPHI*n2) + iPHI*n2 + i2: + * phi1Bins[ix] = (i1 + 0.5) * 2*pi / n1 + * phiBins[ix] = (iPHI + 0.5) * pi / nPHI + * phi2Bins[ix] = (i2 + 0.5) * 2*pi / n2 + */ +LIBMTRSIM_EXPORT ODFComponent buildUniformODF(int n1, int nPHI, int n2); + +/** + * @brief Reconstruct an ODF component from flat grid data + degree spacing. + * + * @param values Flat ODFval, length n1*nPHI*n2, row-major with + * ix = i1*(nPHI*n2) + iPHI*n2 + i2 (phi1 slowest, phi2 + * fastest). + * @param n1,nPHI,n2 Bin counts along phi1, PHI, phi2. + * @param stepDeg1,stepDegPHI,stepDeg2 Bin sizes [degrees] (geometry spacing). + * @return ODFComponent with bin centres [rad] and values normalized to sum 1. + */ +LIBMTRSIM_EXPORT ODFComponent gridToODFComponent(const std::vector& values, int n1, int nPHI, int n2, double stepDeg1, double stepDegPHI, double stepDeg2); + +/** + * @brief Remap a per-voxel vector from simulation order to SIMPLNX z,y,x order. + * + * Simulation order: kSim = iz*(nx*ny) + ix*ny + iy (y fastest). + * SIMPLNX order: kNx = iz*(ny*nx) + iy*nx + ix (x fastest). + * out[kNx] = in[kSim]. + * + * @tparam T element type (int or double). + */ +template +std::vector remapSimToZYX(const std::vector& in, int nx, int ny, int nz) +{ + std::vector out(in.size()); + for(int iz = 0; iz < nz; ++iz) + { + for(int iy = 0; iy < ny; ++iy) + { + for(int ix = 0; ix < nx; ++ix) + { + const std::size_t kSim = static_cast(iz) * nx * ny + static_cast(ix) * ny + iy; + const std::size_t kNx = static_cast(iz) * ny * nx + static_cast(iy) * nx + ix; + out[kNx] = in[kSim]; + } + } + } + return out; +} + +/** + * @brief Per-voxel simulation output in SIMPLNX z,y,x order. + */ +struct LIBMTRSIM_EXPORT MTRSimResult +{ + int nx = 0; + int ny = 0; + int nz = 0; + bool cancelled = false; ///< true if the run was aborted via ISimulationObserver + std::vector mtrIndex; ///< 1-based component id per voxel, length N + std::vector phi1; ///< Euler phi1 [rad] per voxel, length N + std::vector phi; ///< Euler PHI [rad] per voxel, length N + std::vector phi2; ///< Euler phi2 [rad] per voxel, length N +}; + +/** + * @brief Run the full MTR simulation: PGRF assignment -> per-component ODF + * sampling -> per-voxel orientation assignment, returned in SIMPLNX + * z,y,x voxel order. + * + * @param params Fully populated SimulationParams (consistent length + * unit). + * @param odfComponents One ODFComponent per volume-fraction entry, shared + * grid. + * @param rng Seeded RNG (mt19937_64). + * @param n1,nPHI,n2 Bin counts of the ODF grid (for the uniform reference). + */ +LIBMTRSIM_EXPORT MTRSimResult simulateMTR(const SimulationParams& params, const std::vector& odfComponents, std::mt19937_64& rng, int n1, int nPHI, int n2, + ISimulationObserver* observer = nullptr); + +} // namespace mtrsim diff --git a/src/LibMTRSim/ODFBuilder.cpp b/src/LibMTRSim/ODFBuilder.cpp index fb6c7e8..1fd65c0 100644 --- a/src/LibMTRSim/ODFBuilder.cpp +++ b/src/LibMTRSim/ODFBuilder.cpp @@ -24,8 +24,7 @@ constexpr double k_CornerWeight = 0.06 / 8.0; constexpr int k_FaceOffsets[6][3] = {{-1, 0, 0}, {+1, 0, 0}, {0, -1, 0}, {0, +1, 0}, {0, 0, -1}, {0, 0, +1}}; // 12 edge neighbors: +/-1 along exactly two axes. -constexpr int k_EdgeOffsets[12][3] = { - {-1, -1, 0}, {-1, +1, 0}, {+1, -1, 0}, {+1, +1, 0}, {-1, 0, -1}, {-1, 0, +1}, {+1, 0, -1}, {+1, 0, +1}, {0, -1, -1}, {0, -1, +1}, {0, +1, -1}, {0, +1, +1}}; +constexpr int k_EdgeOffsets[12][3] = {{-1, -1, 0}, {-1, +1, 0}, {+1, -1, 0}, {+1, +1, 0}, {-1, 0, -1}, {-1, 0, +1}, {+1, 0, -1}, {+1, 0, +1}, {0, -1, -1}, {0, -1, +1}, {0, +1, -1}, {0, +1, +1}}; /// Modulo wrap that handles negative dividends. Used unchanged for the /// +1/-1 smoothing-neighbor stencil on all three Bunge axes -- this matches @@ -57,8 +56,8 @@ inline int32_t clampBin(int32_t i, int32_t n) /// Row-major linearization: i_phi1 * (nPHI * nphi2) + i_PHI * nphi2 + i_phi2. inline std::size_t linearize(int32_t iPhi1, int32_t iPHI, int32_t iPhi2, int32_t nPHI, int32_t nphi2) { - return static_cast(iPhi1) * static_cast(nPHI) * static_cast(nphi2) + static_cast(iPHI) * static_cast(nphi2) - + static_cast(iPhi2); + return static_cast(iPhi1) * static_cast(nPHI) * static_cast(nphi2) + static_cast(iPHI) * static_cast(nphi2) + + static_cast(iPhi2); } } // namespace @@ -79,11 +78,12 @@ void accumulate(const std::vector>& eulersRad, const ODFBu const double PHIDeg = tuple[1] * k_RadToDeg; const double phi2Deg = tuple[2] * k_RadToDeg; - // MATLAB calc_ODF.m clamps at the upper bound for bin assignment (lines 43-48): - // an angle exactly equal to 2*pi (phi1/phi2) or pi (PHI) goes to the LAST bin, - // not wrap-around to bin 0. Smoothing-neighbor identification (below) still - // uses uniform modulo wrap on all three axes, matching calc_ODF.m's - // jf_minus/jf_plus/kf_minus/kf_plus/lf_minus/lf_plus logic (lines 95-113). + // MATLAB calc_ODF.m clamps at the upper bound for bin assignment (lines + // 43-48): an angle exactly equal to 2*pi (phi1/phi2) or pi (PHI) goes to + // the LAST bin, not wrap-around to bin 0. Smoothing-neighbor identification + // (below) still uses uniform modulo wrap on all three axes, matching + // calc_ODF.m's jf_minus/jf_plus/kf_minus/kf_plus/lf_minus/lf_plus logic + // (lines 95-113). const int32_t iPhi1 = clampBin(static_cast(std::floor(phi1Deg / params.binSizeDeg)), params.nphi1); const int32_t iPHI = clampBin(static_cast(std::floor(PHIDeg / params.binSizeDeg)), params.nPHI); const int32_t iPhi2 = clampBin(static_cast(std::floor(phi2Deg / params.binSizeDeg)), params.nphi2); diff --git a/src/LibMTRSim/ODFBuilder.hpp b/src/LibMTRSim/ODFBuilder.hpp index 097308b..b30f6ce 100644 --- a/src/LibMTRSim/ODFBuilder.hpp +++ b/src/LibMTRSim/ODFBuilder.hpp @@ -18,11 +18,12 @@ namespace mtrsim */ struct LIBMTRSIM_EXPORT ODFBuildParams { - int32_t nphi1; ///< Number of bins along phi1 (slowest-varying, Z in ImageGeom). - int32_t nPHI; ///< Number of bins along PHI (middle, Y). - int32_t nphi2; ///< Number of bins along phi2 (fastest-varying, X). - double binSizeDeg; ///< Uniform bin size in degrees (all three axes). - bool smoothing; ///< When true, distribute each tuple over 27 bins (tri-linear smoothing). + int32_t nphi1; ///< Number of bins along phi1 (slowest-varying, Z in ImageGeom). + int32_t nPHI; ///< Number of bins along PHI (middle, Y). + int32_t nphi2; ///< Number of bins along phi2 (fastest-varying, X). + double binSizeDeg; ///< Uniform bin size in degrees (all three axes). + bool smoothing; ///< When true, distribute each tuple over 27 bins (tri-linear + ///< smoothing). }; /** diff --git a/src/LibMTRSim/ODFCalculator.cpp b/src/LibMTRSim/ODFCalculator.cpp index cfbd822..99f6d58 100644 --- a/src/LibMTRSim/ODFCalculator.cpp +++ b/src/LibMTRSim/ODFCalculator.cpp @@ -20,7 +20,8 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ // ───────────────────────────────────────────────────────────────────────────── // compute — port of calc_ODF.m (always-executed vectorised branch, @@ -39,24 +40,21 @@ namespace mtrsim { // Wrapping: periodic in phi1 and phi2; PHI also wraps for the smoothing // neighbours (matching MATLAB behaviour). -ODFComponent ODFCalculator::compute(const Eigen::VectorXd &phi1in, - const Eigen::VectorXd &phiIn, - const Eigen::VectorXd &phi2in, - double degSpacing) { - if (phi1in.size() != phiIn.size() || phi1in.size() != phi2in.size()) { - throw std::invalid_argument( - "ODFCalculator::compute — phi1, phi, phi2 must have the same length"); +ODFComponent ODFCalculator::compute(const Eigen::VectorXd& phi1in, const Eigen::VectorXd& phiIn, const Eigen::VectorXd& phi2in, double degSpacing) +{ + if(phi1in.size() != phiIn.size() || phi1in.size() != phi2in.size()) + { + throw std::invalid_argument("ODFCalculator::compute — phi1, phi, phi2 must have the same length"); } // ── Grid constants ──────────────────────────────────────────────────────── const int nBins1 = static_cast(std::round(360.0 / degSpacing)); // 72 const int nBinsPHI = nBins1 / 2; // 36 const int nBins2 = nBins1; // 72 - const int nTotal = nBins1 * nBinsPHI * nBins2; // 186 624 + const int nTotal = nBins1 * nBinsPHI * nBins2; // 186 624 const double radSpacing = degSpacing * std::numbers::pi / 180.0; - const double k_TwoPiOver = - 2.0 * std::numbers::pi / static_cast(nBins1); + const double k_TwoPiOver = 2.0 * std::numbers::pi / static_cast(nBins1); const double k_PiOver = std::numbers::pi / static_cast(nBinsPHI); // ── Pre-compute bin centres for the output ODFComponent ────────────────── @@ -64,7 +62,8 @@ ODFComponent ODFCalculator::compute(const Eigen::VectorXd &phi1in, Eigen::VectorXd phiBins(nTotal); Eigen::VectorXd phi2Bins(nTotal); - for (int ix = 0; ix < nTotal; ++ix) { + for(int ix = 0; ix < nTotal; ++ix) + { const int i1 = ix / (nBinsPHI * nBins2); const int iPHI = (ix % (nBinsPHI * nBins2)) / nBins2; const int i2 = ix % nBins2; @@ -77,10 +76,8 @@ ODFComponent ODFCalculator::compute(const Eigen::VectorXd &phi1in, // Idiom mirrors `simplnx`'s OrientationAnalysis filters (e.g. ComputeGBCD, // ComputeGBCDMetricBased): construct an `EulerD`, transform to an // `OrientationMatrix`, and compose with `LaueOps::getMatSymOpD(k)`. - const std::vector allOps = - ebsdlib::LaueOps::GetAllOrientationOps(); - const ebsdlib::LaueOps::Pointer laueOps = - allOps[ebsdlib::CrystalStructure::Hexagonal_High]; + const std::vector allOps = ebsdlib::LaueOps::GetAllOrientationOps(); + const ebsdlib::LaueOps::Pointer laueOps = allOps[ebsdlib::CrystalStructure::Hexagonal_High]; const std::size_t numOps = laueOps->getNumSymOps(); const int N_input = static_cast(phi1in.size()); @@ -96,31 +93,25 @@ ODFComponent ODFCalculator::compute(const Eigen::VectorXd &phi1in, // ── Helper lambdas for periodic neighbour wrapping ──────────────────────── // phi1 and phi2 are periodic over nBins1/nBins2 bins. // PHI also wraps for the smoothing kernel (matching MATLAB). - auto wrap1 = [nBins1](int idx) -> int { - return (idx % nBins1 + nBins1) % nBins1; - }; - auto wrapPHI = [nBinsPHI](int idx) -> int { - return (idx % nBinsPHI + nBinsPHI) % nBinsPHI; - }; - auto wrap2 = [nBins2](int idx) -> int { - return (idx % nBins2 + nBins2) % nBins2; - }; - - auto flatIdx = [nBinsPHI, nBins2](int i1, int iPHI, int i2) -> int { - return i1 * nBinsPHI * nBins2 + iPHI * nBins2 + i2; - }; + auto wrap1 = [nBins1](int idx) -> int { return (idx % nBins1 + nBins1) % nBins1; }; + auto wrapPHI = [nBinsPHI](int idx) -> int { return (idx % nBinsPHI + nBinsPHI) % nBinsPHI; }; + auto wrap2 = [nBins2](int idx) -> int { return (idx % nBins2 + nBins2) % nBins2; }; + + auto flatIdx = [nBinsPHI, nBins2](int i1, int iPHI, int i2) -> int { return i1 * nBinsPHI * nBins2 + iPHI * nBins2 + i2; }; // ── Accumulate ODF ──────────────────────────────────────────────────────── Eigen::VectorXd odfVal = Eigen::VectorXd::Zero(nTotal); - for (int i = 0; i < N_input; ++i) { + for(int i = 0; i < N_input; ++i) + { // Build the passive G matrix for the input orientation. EbsdLib's // `Euler::toOrientationMatrix()` applies the |x|<1e-7 → 0 eps-snap // we want to match the MATLAB reference. const ebsdlib::EulerDType eu(phi1in[i], phiIn[i], phi2in[i]); const ebsdlib::Matrix3X3D Gpassive = eu.toOrientationMatrix().toGMatrix(); - for (std::size_t k = 0; k < numOps; ++k) { + for(std::size_t k = 0; k < numOps; ++k) + { // `getMatSymOpD(k)` returns the ACTIVE form; transpose for passive. // Symmetric variant: R = O_passive * G_passive_input. const ebsdlib::Matrix3X3D Op_active = laueOps->getMatSymOpD(k); @@ -128,17 +119,13 @@ ODFComponent ODFCalculator::compute(const Eigen::VectorXd &phi1in, // Extract Euler back via canonical om2eu (handles degenerate-PHI // and the eps-snap correctly, matching MATLAB after commit 8bbd6e). - const ebsdlib::OrientationMatrixDType om(R[0], R[1], R[2], R[3], R[4], - R[5], R[6], R[7], R[8]); + const ebsdlib::OrientationMatrixDType om(R[0], R[1], R[2], R[3], R[4], R[5], R[6], R[7], R[8]); const ebsdlib::EulerDType outEu = om.toEuler(); // Compute 0-based bin indices (clamp to valid range) - const int jf = std::min( - static_cast(std::trunc(outEu[0] / radSpacing)), nBins1 - 1); - const int kf = std::min( - static_cast(std::trunc(outEu[1] / radSpacing)), nBinsPHI - 1); - const int lf = std::min( - static_cast(std::trunc(outEu[2] / radSpacing)), nBins2 - 1); + const int jf = std::min(static_cast(std::trunc(outEu[0] / radSpacing)), nBins1 - 1); + const int kf = std::min(static_cast(std::trunc(outEu[1] / radSpacing)), nBinsPHI - 1); + const int lf = std::min(static_cast(std::trunc(outEu[2] / radSpacing)), nBins2 - 1); // Wrapped neighbour indices const int jm = wrap1(jf - 1); diff --git a/src/LibMTRSim/ODFCalculator.hpp b/src/LibMTRSim/ODFCalculator.hpp index 000cd41..8cfd90a 100644 --- a/src/LibMTRSim/ODFCalculator.hpp +++ b/src/LibMTRSim/ODFCalculator.hpp @@ -4,7 +4,8 @@ #include "libmtrsim_export.h" #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Computes a discrete ODF histogram from a set of Euler angles. @@ -12,7 +13,8 @@ namespace mtrsim { * Equivalent to calc_ODF.m. Applies crystal symmetry operations (HCP by * default) and optional nearest-neighbour smoothing before binning. */ -class LIBMTRSIM_EXPORT ODFCalculator { +class LIBMTRSIM_EXPORT ODFCalculator +{ public: ODFCalculator() = default; @@ -25,8 +27,7 @@ class LIBMTRSIM_EXPORT ODFCalculator { * @param degSpacing Bin width in degrees (default 5°) * @return Populated ODFComponent with normalised ODFval */ - ODFComponent compute(const Eigen::VectorXd &phi1, const Eigen::VectorXd &phi, - const Eigen::VectorXd &phi2, double degSpacing = 5.0); + ODFComponent compute(const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2, double degSpacing = 5.0); }; } // namespace mtrsim diff --git a/src/LibMTRSim/ODFFileIO.cpp b/src/LibMTRSim/ODFFileIO.cpp index a108766..b12a121 100644 --- a/src/LibMTRSim/ODFFileIO.cpp +++ b/src/LibMTRSim/ODFFileIO.cpp @@ -491,8 +491,8 @@ std::vector readODFComponents(const std::filesystem::path& fil return out; } -void writeODFFile(const std::filesystem::path& file, const std::array& dimsPhi1PHIPhi2, const std::array& spacingDegPhi1PHIPhi2, - const std::vector& components, const std::string& pathPrefix) +void writeODFFile(const std::filesystem::path& file, const std::array& dimsPhi1PHIPhi2, const std::array& spacingDegPhi1PHIPhi2, const std::vector& components, + const std::string& pathPrefix) { if(components.empty()) { diff --git a/src/LibMTRSim/ODFFileIO.hpp b/src/LibMTRSim/ODFFileIO.hpp index c64ae91..96b869b 100644 --- a/src/LibMTRSim/ODFFileIO.hpp +++ b/src/LibMTRSim/ODFFileIO.hpp @@ -9,7 +9,8 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Metadata describing the contents of a MATLAB-format ODF HDF5 file. @@ -20,8 +21,9 @@ namespace mtrsim { */ struct LIBMTRSIM_EXPORT ODFFileMetadata { - int64_t numComponents; ///< Number of ODF components stored in the file (>= 1). - std::array dimsPhi1PHIPhi2; ///< Number of bins per axis; phi1 slowest-varying, phi2 fastest. + int64_t numComponents; ///< Number of ODF components stored in the file (>= 1). + std::array dimsPhi1PHIPhi2; ///< Number of bins per axis; phi1 + ///< slowest-varying, phi2 fastest. std::array spacingDegPhi1PHIPhi2; ///< Uniform bin size in DEGREES, one per axis. }; @@ -38,14 +40,16 @@ struct LIBMTRSIM_EXPORT ODFFileComponent }; /** - * @brief Read metadata (component count, dims, spacing) without touching ODFval arrays. + * @brief Read metadata (component count, dims, spacing) without touching ODFval + * arrays. * * Validates structural invariants: * - /num_components >= 1 * - components are present contiguously from component_0 .. component_{N-1} - * - every component's phi1_bins / PHI_bins / phi2_bins are byte-exact identical - * to component_0's arrays - * - bin-edge arrays are strictly monotonically increasing and uniformly spaced + * - every component's phi1_bins / PHI_bins / phi2_bins are byte-exact + * identical to component_0's arrays + * - bin-edge arrays are strictly monotonically increasing and uniformly + * spaced * - each component's ODFval dataset has the expected size * * @param file Path to the HDF5 file. @@ -78,7 +82,8 @@ LIBMTRSIM_EXPORT std::vector readODFComponents(const std::file * MATLAB's `0:2*pi/num_bins:2*pi` form. * * @param file Output path; overwrites if it exists. - * @param dimsPhi1PHIPhi2 Number of bins per axis. Each entry must be > 0. + * @param dimsPhi1PHIPhi2 Number of bins per axis. Each entry must be > + * 0. * @param spacingDegPhi1PHIPhi2 Uniform bin size per axis in DEGREES. * @param components One entry per ODF component; each must have * values.size() == dims[0] * dims[1] * dims[2]. @@ -91,11 +96,8 @@ LIBMTRSIM_EXPORT std::vector readODFComponents(const std::file * * @throws std::runtime_error on any invalid input or HDF5 error. */ -LIBMTRSIM_EXPORT void writeODFFile(const std::filesystem::path& file, - const std::array& dimsPhi1PHIPhi2, - const std::array& spacingDegPhi1PHIPhi2, - const std::vector& components, - const std::string& pathPrefix = "/ODF_best"); +LIBMTRSIM_EXPORT void writeODFFile(const std::filesystem::path& file, const std::array& dimsPhi1PHIPhi2, const std::array& spacingDegPhi1PHIPhi2, + const std::vector& components, const std::string& pathPrefix = "/ODF_best"); /** * @brief Read an optional int64 fixture-version field from a reference HDF5. diff --git a/src/LibMTRSim/ODFSampler.cpp b/src/LibMTRSim/ODFSampler.cpp index 281f075..b9c786c 100644 --- a/src/LibMTRSim/ODFSampler.cpp +++ b/src/LibMTRSim/ODFSampler.cpp @@ -7,9 +7,13 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ -ODFSampler::ODFSampler(std::mt19937_64 &rng) : m_Rng(rng) {} +ODFSampler::ODFSampler(std::mt19937_64& rng) +: m_Rng(rng) +{ +} // ───────────────────────────────────────────────────────────────────────────── // sampleN — port of sample_N_orientations_from_ODF.m. @@ -24,33 +28,40 @@ ODFSampler::ODFSampler(std::mt19937_64 &rng) : m_Rng(rng) {} // The bin width is inferred as uniform.phi1Bins[1] − uniform.phi1Bins[0], // matching MATLAB's bin_scaling_factor = degree_spacing/180*pi. -Eigen::MatrixXd ODFSampler::sampleN(int n, const ODFComponent &component, - const ODFComponent &uniform) { +Eigen::MatrixXd ODFSampler::sampleN(int n, const ODFComponent& component, const ODFComponent& uniform, ISimulationObserver* observer) +{ const int nBins = static_cast(component.odfVal.size()); - if (nBins < 1) { + if(nBins < 1) + { throw std::invalid_argument("ODFSampler::sampleN — odfVal is empty"); } - if (uniform.phi1Bins.size() != static_cast(nBins) || - uniform.phiBins.size() != static_cast(nBins) || - uniform.phi2Bins.size() != static_cast(nBins)) { - throw std::invalid_argument( - "ODFSampler::sampleN — uniform bin vectors do not match odfVal size"); + if(uniform.phi1Bins.size() != static_cast(nBins) || uniform.phiBins.size() != static_cast(nBins) || uniform.phi2Bins.size() != static_cast(nBins)) + { + throw std::invalid_argument("ODFSampler::sampleN — uniform bin vectors do not match odfVal size"); } // ── Build normalised CDF ────────────────────────────────────────────────── const double total = component.odfVal.sum(); std::vector cdf(static_cast(nBins) + 1); cdf[0] = 0.0; - for (int j = 0; j < nBins; ++j) { - cdf[static_cast(j) + 1] = - cdf[static_cast(j)] + component.odfVal[j] / total; + for(int j = 0; j < nBins; ++j) + { + cdf[static_cast(j) + 1] = cdf[static_cast(j)] + component.odfVal[j] / total; } // ── Draw N uniform samples and map to bin indices ───────────────────────── + // Poll the cancel flag at most once per 4096 samples (~microsecond latency); + // the modulo check short-circuits before any RNG draw so it cannot perturb results. + constexpr int kCheck = 4096; std::uniform_real_distribution uDist(0.0, 1.0); std::vector binIdx(static_cast(n)); - for (int i = 0; i < n; ++i) { + for(int i = 0; i < n; ++i) + { + if(observer != nullptr && (i % kCheck) == 0 && observer->shouldCancel()) + { + return Eigen::MatrixXd(0, 3); + } const double u = uDist(m_Rng); // upper_bound finds the first position where cdf > u; // the bin index is one before that position. @@ -64,16 +75,18 @@ Eigen::MatrixXd ODFSampler::sampleN(int n, const ODFComponent &component, // ── Infer bin width from the uniform ODF bin centres ───────────────────── // For standard 5° spacing: binWidth ≈ 5*π/180 rad. - const double binWidth = (nBins > 1) - ? (uniform.phi1Bins[1] - uniform.phi1Bins[0]) - : (5.0 * std::acos(-1.0) / 180.0); + const double binWidth = (nBins > 1) ? (uniform.phi1Bins[1] - uniform.phi1Bins[0]) : (5.0 * std::acos(-1.0) / 180.0); // ── Assign bin-centre coordinates with independent per-angle jitter ─────── - std::uniform_real_distribution jitter(-0.5 * binWidth, - 0.5 * binWidth); + std::uniform_real_distribution jitter(-0.5 * binWidth, 0.5 * binWidth); Eigen::MatrixXd result(n, 3); - for (int i = 0; i < n; ++i) { + for(int i = 0; i < n; ++i) + { + if(observer != nullptr && (i % kCheck) == 0 && observer->shouldCancel()) + { + return Eigen::MatrixXd(0, 3); + } const int b = binIdx[static_cast(i)]; result(i, 0) = uniform.phi1Bins[b] + jitter(m_Rng); result(i, 1) = uniform.phiBins[b] + jitter(m_Rng); @@ -88,9 +101,9 @@ Eigen::MatrixXd ODFSampler::sampleN(int n, const ODFComponent &component, // // Uses the same inverse-CDF approach as sampleN but draws a single orientation. -EulerAngles ODFSampler::sampleOne(const ODFComponent &component, - const ODFComponent &uniform) { - const Eigen::MatrixXd row = sampleN(1, component, uniform); +EulerAngles ODFSampler::sampleOne(const ODFComponent& component, const ODFComponent& uniform) +{ + const Eigen::MatrixXd row = sampleN(1, component, uniform); // n=1: no observer needed (cancel latency negligible) return EulerAngles{row(0, 0), row(0, 1), row(0, 2)}; } diff --git a/src/LibMTRSim/ODFSampler.hpp b/src/LibMTRSim/ODFSampler.hpp index fd20663..4d7433e 100644 --- a/src/LibMTRSim/ODFSampler.hpp +++ b/src/LibMTRSim/ODFSampler.hpp @@ -1,20 +1,22 @@ #pragma once +#include "ISimulationObserver.hpp" #include "libmtrsim_export.h" #include #include #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Discrete ODF data for one component. * * Matches the MATLAB struct fields stored in simulation_ODF.h5. */ -struct LIBMTRSIM_EXPORT ODFComponent { - Eigen::VectorXd - odfVal; ///< Probability mass for each Euler-space bin (N_bins,) +struct LIBMTRSIM_EXPORT ODFComponent +{ + Eigen::VectorXd odfVal; ///< Probability mass for each Euler-space bin (N_bins,) Eigen::VectorXd phi1Bins; ///< phi1 bin centres [rad] Eigen::VectorXd phiBins; ///< PHI bin centres [rad] Eigen::VectorXd phi2Bins; ///< phi2 bin centres [rad] @@ -23,7 +25,8 @@ struct LIBMTRSIM_EXPORT ODFComponent { /** * @brief Sampled orientation (Bunge Euler angles, radians). */ -struct LIBMTRSIM_EXPORT EulerAngles { +struct LIBMTRSIM_EXPORT EulerAngles +{ double phi1 = 0.0; double phi = 0.0; double phi2 = 0.0; @@ -35,9 +38,10 @@ struct LIBMTRSIM_EXPORT EulerAngles { * Combines sample_orientation_from_ODF.m and * sample_N_orientations_from_ODF.m. */ -class LIBMTRSIM_EXPORT ODFSampler { +class LIBMTRSIM_EXPORT ODFSampler +{ public: - explicit ODFSampler(std::mt19937_64 &rng); + explicit ODFSampler(std::mt19937_64& rng); /** * @brief Draw N orientations from the given component ODF. @@ -47,17 +51,15 @@ class LIBMTRSIM_EXPORT ODFSampler { * @param uniform Uniform (reference) ODF component for bin coordinates * @return Matrix of shape [N x 3]: columns are phi1, PHI, phi2 [rad] */ - Eigen::MatrixXd sampleN(int n, const ODFComponent &component, - const ODFComponent &uniform); + Eigen::MatrixXd sampleN(int n, const ODFComponent& component, const ODFComponent& uniform, ISimulationObserver* observer = nullptr); /** * @brief Draw a single orientation from the given component ODF. */ - EulerAngles sampleOne(const ODFComponent &component, - const ODFComponent &uniform); + EulerAngles sampleOne(const ODFComponent& component, const ODFComponent& uniform); private: - std::mt19937_64 &m_Rng; + std::mt19937_64& m_Rng; }; } // namespace mtrsim diff --git a/src/LibMTRSim/PGRFSimulation.cpp b/src/LibMTRSim/PGRFSimulation.cpp index cd915be..ac3903b 100644 --- a/src/LibMTRSim/PGRFSimulation.cpp +++ b/src/LibMTRSim/PGRFSimulation.cpp @@ -8,12 +8,16 @@ #include #include -#include +#include #include -namespace mtrsim { +namespace mtrsim +{ -PGRFSimulation::PGRFSimulation(std::mt19937_64 &rng) : m_Rng(rng) {} +PGRFSimulation::PGRFSimulation(std::mt19937_64& rng) +: m_Rng(rng) +{ +} // ───────────────────────────────────────────────────────────────────────────── // run @@ -31,40 +35,41 @@ PGRFSimulation::PGRFSimulation(std::mt19937_64 &rng) : m_Rng(rng) {} // boundary_conditions = 'nonperiodic' // mean_function_selected = 'stationary' (mu = 0 for all fields) -PGRFResult PGRFSimulation::run(const SimulationParams ¶ms) { +PGRFResult PGRFSimulation::run(const SimulationParams& params, ISimulationObserver* observer) +{ // ── Grid dimensions ────────────────────────────────────────────────────── const int nx = static_cast(std::round(params.xLen / params.dx)); const int ny = static_cast(std::round(params.yLen / params.dy)); - const int nz = - std::max(static_cast(std::round(params.zLen / params.dz)), 1); + const int nz = std::max(static_cast(std::round(params.zLen / params.dz)), 1); const int N = nx * ny * nz; const int numComponents = static_cast(params.volumeFractions.size()); const int numGaussians = numComponents - 1; - if (numGaussians < 1) { - throw std::invalid_argument( - "PGRFSimulation: need at least 2 volume fraction components"); + if(numGaussians < 1) + { + throw std::invalid_argument("PGRFSimulation: need at least 2 volume fraction components"); } - if (static_cast(params.thetaList.size()) < numGaussians) { - throw std::invalid_argument( - "PGRFSimulation: thetaList must have one row per latent Gaussian"); + if(static_cast(params.thetaList.size()) < numGaussians) + { + throw std::invalid_argument("PGRFSimulation: thetaList must have one row per latent Gaussian"); } - spdlog::info("PGRFSimulation: grid {}x{}x{} = {} voxels, {} components, {} " - "latent fields", - nx, ny, nz, N, numComponents, numGaussians); + if(observer != nullptr) + { + observer->info(fmt::format("PGRFSimulation: grid {}x{}x{} = {} voxels, {} components, {} latent fields", nx, ny, nz, N, numComponents, numGaussians)); + } // ── Select assignment-rule thresholds ──────────────────────────────────── - spdlog::info("PGRFSimulation: selecting assignment rule thresholds..."); + if(observer != nullptr) + { + observer->info("PGRFSimulation: selecting assignment rule thresholds..."); + } AssignmentRule ar(m_Rng); - const AssignmentRuleThresholds thresholds = - ar.selectThresholds(params.volumeFractions); + const AssignmentRuleThresholds thresholds = ar.selectThresholds(params.volumeFractions); // ── Exponential correlation function: rho(lag, theta) = exp(-|lag|/theta) ─ // Matches MATLAB: corr_func_name = 'exp', boundary_conditions = 'nonperiodic' - auto corrFn = [](double lag, double theta) -> double { - return std::exp(-std::abs(lag) / theta); - }; + auto corrFn = [](double lag, double theta) -> double { return std::exp(-std::abs(lag) / theta); }; GPGenerator gpGen(m_Rng, corrFn); @@ -72,31 +77,43 @@ PGRFResult PGRFSimulation::run(const SimulationParams ¶ms) { // Stationary mean: mu_const = 0 for all fields (matches MATLAB default) Eigen::MatrixXd zAll = Eigen::MatrixXd::Zero(N, numGaussians); - for (int h = 0; h < numGaussians; ++h) { - spdlog::info("PGRFSimulation: simulating latent Gaussian Y{} ...", h + 1); + for(int h = 0; h < numGaussians; ++h) + { + if(observer != nullptr) + { + if(observer->shouldCancel()) + { + return PGRFResult{}; // cancelled; simulateMTR detects this via its cancelled() lambda + } + observer->updateProgress(h, numGaussians, fmt::format("Simulating latent Gaussian field {}/{}", h + 1, numGaussians)); + } - const auto &thetaRow = params.thetaList[static_cast(h)]; - if (thetaRow.size() < 3) { - throw std::invalid_argument( - "PGRFSimulation: each thetaList row must have 3 elements [theta_x, " - "theta_y, theta_z]"); + const auto& thetaRow = params.thetaList[static_cast(h)]; + if(thetaRow.size() < 3) + { + throw std::invalid_argument("PGRFSimulation: each thetaList row must have 3 elements [theta_x, " + "theta_y, theta_z]"); } const std::array theta = {thetaRow[0], thetaRow[1], thetaRow[2]}; - zAll.col(h) = - gpGen.generate(params.dx, params.dy, params.dz, theta, nx, ny, nz); + zAll.col(h) = gpGen.generate(params.dx, params.dy, params.dz, theta, nx, ny, nz); } // ── Apply assignment rule ───────────────────────────────────────────────── - spdlog::info("PGRFSimulation: applying assignment rule..."); + if(observer != nullptr) + { + observer->info("PGRFSimulation: applying assignment rule..."); + } const Eigen::VectorXi mtrIndex = ar.evaluate(zAll, thresholds); // Log empirical volume fractions for verification - for (int j = 1; j <= numComponents; ++j) { + for(int j = 1; j <= numComponents; ++j) + { const int count = (mtrIndex.array() == j).count(); - spdlog::info(" P{} empirical = {:.3f} (target {:.3f})", j, - static_cast(count) / static_cast(N), - params.volumeFractions[static_cast(j - 1)]); + if(observer != nullptr) + { + observer->info(fmt::format(" P{} empirical = {:.3f} (target {:.3f})", j, static_cast(count) / static_cast(N), params.volumeFractions[static_cast(j - 1)])); + } } return PGRFResult{mtrIndex, zAll}; diff --git a/src/LibMTRSim/PGRFSimulation.hpp b/src/LibMTRSim/PGRFSimulation.hpp index 0dc1263..b83e2b9 100644 --- a/src/LibMTRSim/PGRFSimulation.hpp +++ b/src/LibMTRSim/PGRFSimulation.hpp @@ -1,16 +1,19 @@ #pragma once +#include "ISimulationObserver.hpp" #include "SimulationParams.hpp" #include "libmtrsim_export.h" #include #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Result of the plurigaussian random field simulation. */ -struct LIBMTRSIM_EXPORT PGRFResult { +struct LIBMTRSIM_EXPORT PGRFResult +{ // Component assignment for each voxel (1-based index), length N Eigen::VectorXi mtrIndex; @@ -26,9 +29,10 @@ struct LIBMTRSIM_EXPORT PGRFResult { * AssignmentRule to produce a categorical assignment over the simulation * volume. */ -class LIBMTRSIM_EXPORT PGRFSimulation { +class LIBMTRSIM_EXPORT PGRFSimulation +{ public: - explicit PGRFSimulation(std::mt19937_64 &rng); + explicit PGRFSimulation(std::mt19937_64& rng); /** * @brief Run the PGRF simulation. @@ -36,10 +40,10 @@ class LIBMTRSIM_EXPORT PGRFSimulation { * @param params Fully populated SimulationParams * @return PGRFResult containing voxel assignments and latent fields */ - PGRFResult run(const SimulationParams ¶ms); + PGRFResult run(const SimulationParams& params, ISimulationObserver* observer = nullptr); private: - std::mt19937_64 &m_Rng; + std::mt19937_64& m_Rng; }; } // namespace mtrsim diff --git a/src/LibMTRSim/PoleFigure.cpp b/src/LibMTRSim/PoleFigure.cpp index c3454b5..12dbd00 100644 --- a/src/LibMTRSim/PoleFigure.cpp +++ b/src/LibMTRSim/PoleFigure.cpp @@ -7,7 +7,8 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ // ───────────────────────────────────────────────────────────────────────────── // fromODF — port of convert_ODF_to_PF.m (direct computation, no prebuilt @@ -27,15 +28,15 @@ namespace mtrsim { // // The degSpacing parameter must match the spacing used to compute the ODF. -PoleFigureData PoleFigure::fromODF(const ODFComponent &component, - double degSpacing) { +PoleFigureData PoleFigure::fromODF(const ODFComponent& component, double degSpacing) +{ const int nTotal = static_cast(component.odfVal.size()); - if (nTotal < 1) { + if(nTotal < 1) + { throw std::invalid_argument("PoleFigure::fromODF — empty ODFComponent"); } - if (component.phi1Bins.size() != static_cast(nTotal) || - component.phiBins.size() != static_cast(nTotal) || - component.phi2Bins.size() != static_cast(nTotal)) { + if(component.phi1Bins.size() != static_cast(nTotal) || component.phiBins.size() != static_cast(nTotal) || component.phi2Bins.size() != static_cast(nTotal)) + { throw std::invalid_argument("PoleFigure::fromODF — ODFComponent bin " "vectors have inconsistent sizes"); } @@ -50,8 +51,7 @@ PoleFigureData PoleFigure::fromODF(const ODFComponent &component, const double halfDPHI = 0.5 * dPHI; // Prefactor from MATLAB: 4π² / (dphi1 * dphi2) - const double prefactor = - 4.0 * std::numbers::pi * std::numbers::pi / (dphi1 * dphi2); + const double prefactor = 4.0 * std::numbers::pi * std::numbers::pi / (dphi1 * dphi2); // ── Accumulate (X, Y, weight) for all non-zero bins // ────────────────────────── @@ -64,9 +64,11 @@ PoleFigureData PoleFigure::fromODF(const ODFComponent &component, double totalWeight = 0.0; - for (int m = 0; m < nTotal; ++m) { + for(int m = 0; m < nTotal; ++m) + { const double v = component.odfVal[m]; - if (v <= 0.0) { + if(v <= 0.0) + { continue; } @@ -86,7 +88,8 @@ PoleFigureData PoleFigure::fromODF(const ODFComponent &component, // ────────────────────────────── Singularity when h_z → −1 (PHI → π): skip // those bins. const double denom = 1.0 + h_z; - if (std::abs(denom) < 1.0e-8) { + if(std::abs(denom) < 1.0e-8) + { continue; } const double X = h_x / denom; @@ -94,9 +97,9 @@ PoleFigureData PoleFigure::fromODF(const ODFComponent &component, // ── Jacobian weight (MATLAB convert_ODF_to_PF.m formula) ───────────────── // |cos(PHI − dPHI/2) − cos(PHI + dPHI/2)| = 2·|sin(PHI)|·sin(dPHI/2) - const double cosDiff = - std::abs(std::cos(PHI_m - halfDPHI) - std::cos(PHI_m + halfDPHI)); - if (cosDiff < 1.0e-12) { + const double cosDiff = std::abs(std::cos(PHI_m - halfDPHI) - std::cos(PHI_m + halfDPHI)); + if(cosDiff < 1.0e-12) + { continue; // PHI ≈ 0 or π: vanishing angular area, skip } const double weight = v * prefactor / cosDiff; @@ -109,8 +112,10 @@ PoleFigureData PoleFigure::fromODF(const ODFComponent &component, // ── Normalise intensities // ───────────────────────────────────────────────────── - if (totalWeight > 0.0) { - for (double &w : wVec) { + if(totalWeight > 0.0) + { + for(double& w : wVec) + { w /= totalWeight; } } diff --git a/src/LibMTRSim/PoleFigure.hpp b/src/LibMTRSim/PoleFigure.hpp index 537d368..d0eaaf3 100644 --- a/src/LibMTRSim/PoleFigure.hpp +++ b/src/LibMTRSim/PoleFigure.hpp @@ -4,12 +4,14 @@ #include "libmtrsim_export.h" #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Pole figure data: projected (X, Y) coordinates and intensity values. */ -struct LIBMTRSIM_EXPORT PoleFigureData { +struct LIBMTRSIM_EXPORT PoleFigureData +{ Eigen::VectorXd x; ///< Stereographic X coordinates Eigen::VectorXd y; ///< Stereographic Y coordinates Eigen::VectorXd intensity; ///< Normalised intensity per bin @@ -23,7 +25,8 @@ struct LIBMTRSIM_EXPORT PoleFigureData { * to HexagonalOps::generatePoleFigure() for the stereographic * projection and intensity accumulation. */ -class LIBMTRSIM_EXPORT PoleFigure { +class LIBMTRSIM_EXPORT PoleFigure +{ public: PoleFigure() = default; @@ -35,8 +38,7 @@ class LIBMTRSIM_EXPORT PoleFigure { * @return PoleFigureData with stereographic coordinates and * intensities */ - PoleFigureData fromODF(const ODFComponent &component, - double degSpacing = 5.0); + PoleFigureData fromODF(const ODFComponent& component, double degSpacing = 5.0); }; } // namespace mtrsim diff --git a/src/LibMTRSim/QSimVN.cpp b/src/LibMTRSim/QSimVN.cpp index 3bced70..4588253 100644 --- a/src/LibMTRSim/QSimVN.cpp +++ b/src/LibMTRSim/QSimVN.cpp @@ -10,32 +10,37 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ -namespace { +namespace +{ // MATLAB sign(): -1, 0, or +1 -inline double matlabSign(double x) { +inline double matlabSign(double x) +{ return (x > 0.0) ? 1.0 : (x < 0.0 ? -1.0 : 0.0); } // Build quasi-random Halton base vector: sqrt of first (n-1) primes. // Matches MATLAB: ps = sqrt(primes(5*n*log(n+1)/4)); q = ps(1:n-1) -Eigen::VectorXd primeBasesForDim(int n) { +Eigen::VectorXd primeBasesForDim(int n) +{ const int need = n - 1; - if (need <= 0) { + if(need <= 0) + { return Eigen::VectorXd{}; } // Upper bound matches MATLAB formula; +20 ensures enough room - const int limit = std::max( - static_cast(5.0 * n * std::log(static_cast(n) + 1.0) / 4.0) + - 20, - 20); + const int limit = std::max(static_cast(5.0 * n * std::log(static_cast(n) + 1.0) / 4.0) + 20, 20); std::vector isComposite(static_cast(limit + 1), false); - for (int i = 2; i * i <= limit; ++i) { - if (!isComposite[static_cast(i)]) { - for (int j = i * i; j <= limit; j += i) { + for(int i = 2; i * i <= limit; ++i) + { + if(!isComposite[static_cast(i)]) + { + for(int j = i * i; j <= limit; j += i) + { isComposite[static_cast(j)] = true; } } @@ -43,8 +48,10 @@ Eigen::VectorXd primeBasesForDim(int n) { Eigen::VectorXd result(need); int found = 0; - for (int i = 2; i <= limit && found < need; ++i) { - if (!isComposite[static_cast(i)]) { + for(int i = 2; i <= limit && found < need; ++i) + { + if(!isComposite[static_cast(i)]) + { result(found++) = std::sqrt(static_cast(i)); } } @@ -56,62 +63,56 @@ Eigen::VectorXd primeBasesForDim(int n) { // Max absolute error < 1.15e-9 over (0, 1). // Reference: P. J. Acklam, "An algorithm for computing the inverse normal // cumulative distribution function", 2003. -double normalCDFInverse(double p) { +double normalCDFInverse(double p) +{ // Coefficients for the central-region rational approximation - static constexpr double a[] = {-3.969683028665376e+01, 2.209460984245205e+02, - -2.759285104469687e+02, 1.383577518672690e+02, - -3.066479806614716e+01, 2.506628277459239e+00}; - static constexpr double b[] = {-5.447609879822406e+01, 1.615858368580409e+02, - -1.556989798598866e+02, 6.680131188771972e+01, - -1.328068155288572e+01}; + static constexpr double a[] = {-3.969683028665376e+01, 2.209460984245205e+02, -2.759285104469687e+02, 1.383577518672690e+02, -3.066479806614716e+01, 2.506628277459239e+00}; + static constexpr double b[] = {-5.447609879822406e+01, 1.615858368580409e+02, -1.556989798598866e+02, 6.680131188771972e+01, -1.328068155288572e+01}; // Coefficients for the tail rational approximation - static constexpr double c[] = {-7.784894002430293e-03, -3.223964580411365e-01, - -2.400758277161838e+00, -2.549732539343734e+00, - 4.374664141464968e+00, 2.938163982698783e+00}; - static constexpr double d[] = {7.784695709041462e-03, 3.224671290700398e-01, - 2.445134137142996e+00, 3.754408661907416e+00}; + static constexpr double c[] = {-7.784894002430293e-03, -3.223964580411365e-01, -2.400758277161838e+00, -2.549732539343734e+00, 4.374664141464968e+00, 2.938163982698783e+00}; + static constexpr double d[] = {7.784695709041462e-03, 3.224671290700398e-01, 2.445134137142996e+00, 3.754408661907416e+00}; constexpr double p_low = 0.02425; constexpr double p_high = 1.0 - p_low; - if (p < p_low) { + if(p < p_low) + { // Lower tail const double q = std::sqrt(-2.0 * std::log(p)); - return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + - c[5]) / - ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0); + return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0); } - if (p <= p_high) { + if(p <= p_high) + { // Central region const double q = p - 0.5; const double r = q * q; - return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + - a[5]) * - q / - (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0); + return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0); } // Upper tail — mirror of lower tail const double q = std::sqrt(-2.0 * std::log(1.0 - p)); - return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + - c[5]) / - ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0); + return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0); } } // anonymous namespace // ───────────────────────────────────────────────────────────────────────────── -QSimVN::QSimVN(std::mt19937_64 &rng) : m_Rng(rng) {} +QSimVN::QSimVN(std::mt19937_64& rng) +: m_Rng(rng) +{ +} // ───────────────────────────────────────────────────────────────────────────── // Static helpers -double QSimVN::phi(double z) { +double QSimVN::phi(double z) +{ // Phi(z) = P(Z <= z) for Z ~ N(0,1). MATLAB: erfc(-z/sqrt(2))/2 return std::erfc(-z / std::sqrt(2.0)) / 2.0; } -double QSimVN::phiInv(double p) { +double QSimVN::phiInv(double p) +{ // Inverse normal CDF. MATLAB: -sqrt(2)*erfcinv(2*p) = normalCDFInverse(p) p = std::clamp(p, 1e-15, 1.0 - 1e-15); return normalCDFInverse(p); @@ -125,9 +126,8 @@ double QSimVN::phiInv(double p) { // The correct expression is "bi = (bp(i) - s)/cii" (uses the accumulated // inner product s). The C++ port uses the correct formula. -QSimVN::ChlrdrResult QSimVN::chlrdr(const Eigen::MatrixXd &r, - const Eigen::VectorXd &a, - const Eigen::VectorXd &b) const { +QSimVN::ChlrdrResult QSimVN::chlrdr(const Eigen::MatrixXd& r, const Eigen::VectorXd& a, const Eigen::VectorXd& b) const +{ const double ep = 1e-10; const int n = static_cast(r.rows()); const double sqtp = std::sqrt(2.0 * std::numbers::pi); @@ -138,8 +138,10 @@ QSimVN::ChlrdrResult QSimVN::chlrdr(const Eigen::MatrixXd &r, // Rescale to correlation matrix: divide each row/col by sqrt of diagonal Eigen::VectorXd d = c.diagonal().cwiseMax(0.0).cwiseSqrt(); - for (int i = 0; i < n; ++i) { - if (d(i) > 0.0) { + for(int i = 0; i < n; ++i) + { + if(d(i) > 0.0) + { c.col(i) /= d(i); c.row(i) /= d(i); ap(i) /= d(i); @@ -150,7 +152,8 @@ QSimVN::ChlrdrResult QSimVN::chlrdr(const Eigen::MatrixXd &r, // y holds the conditional mean used when searching for the optimal pivot Eigen::VectorXd y = Eigen::VectorXd::Zero(n); - for (int k = 0; k < n; ++k) { + for(int k = 0; k < n; ++k) + { // ── Search for the pivot that minimises de = Phi(bi) - Phi(ai) ────────── int pivotIdx = k; double ckk = 0.0; @@ -158,15 +161,18 @@ QSimVN::ChlrdrResult QSimVN::chlrdr(const Eigen::MatrixXd &r, double am = 0.0; double bm = 0.0; - for (int i = k; i < n; ++i) { - if (c(i, i) > std::numeric_limits::epsilon()) { + for(int i = k; i < n; ++i) + { + if(c(i, i) > std::numeric_limits::epsilon()) + { const double cii = std::sqrt(std::max(c(i, i), 0.0)); // s = c(i, 0:k-1) . y(0:k-1) (accumulated inner product) const double s = (k > 0) ? c.row(i).head(k).dot(y.head(k)) : 0.0; const double ai = (ap(i) - s) / cii; const double bi = (bp(i) - s) / cii; // corrected from MATLAB bug const double de = phi(bi) - phi(ai); - if (de <= dem) { + if(de <= dem) + { ckk = cii; dem = de; am = ai; @@ -177,7 +183,8 @@ QSimVN::ChlrdrResult QSimVN::chlrdr(const Eigen::MatrixXd &r, } // ── Symmetric row/col swap: move pivot to position k ──────────────────── - if (pivotIdx > k) { + if(pivotIdx > k) + { const int p = pivotIdx; std::swap(ap(p), ap(k)); @@ -187,14 +194,16 @@ QSimVN::ChlrdrResult QSimVN::chlrdr(const Eigen::MatrixXd &r, c(p, p) = c(k, k); // Swap completed Cholesky columns (first k elements of rows k and p) - if (k > 0) { + if(k > 0) + { Eigen::VectorXd tmp = c.row(p).head(k); c.row(p).head(k) = c.row(k).head(k); c.row(k).head(k) = tmp; } // Swap sub-columns strictly below row p (columns k and p, rows p+1..n-1) - if (p + 1 < n) { + if(p + 1 < n) + { Eigen::VectorXd tmp = c.col(p).tail(n - p - 1); c.col(p).tail(n - p - 1) = c.col(k).tail(n - p - 1); c.col(k).tail(n - p - 1) = tmp; @@ -202,44 +211,55 @@ QSimVN::ChlrdrResult QSimVN::chlrdr(const Eigen::MatrixXd &r, // Swap off-diagonal block between k+1 and p-1 // c(k+1..p-1, k) <-> c(p, k+1..p-1) - if (p - 1 >= k + 1) { + if(p - 1 >= k + 1) + { const int segLen = p - k - 1; Eigen::VectorXd tmp = c.col(k).segment(k + 1, segLen); - c.col(k).segment(k + 1, segLen) = - c.row(p).segment(k + 1, segLen).transpose(); + c.col(k).segment(k + 1, segLen) = c.row(p).segment(k + 1, segLen).transpose(); c.row(p).segment(k + 1, segLen) = tmp.transpose(); } } // ── Cholesky factorisation step ────────────────────────────────────────── - if (ckk > ep * static_cast(k + 1)) { + if(ckk > ep * static_cast(k + 1)) + { c(k, k) = ckk; - if (n - k - 1 > 0) { + if(n - k - 1 > 0) + { c.row(k).tail(n - k - 1).setZero(); } - for (int i = k + 1; i < n; ++i) { + for(int i = k + 1; i < n; ++i) + { c(i, k) /= ckk; // Rank-1 Schur complement update: c(i, k+1..i) -= c(i,k) * c(k+1..i, k) const int len = i - k; - c.row(i).segment(k + 1, len) -= - c(i, k) * c.col(k).segment(k + 1, len).transpose(); + c.row(i).segment(k + 1, len) -= c(i, k) * c.col(k).segment(k + 1, len).transpose(); } // Conditional mean for use in future pivot search - if (std::abs(dem) > ep) { - y(k) = (std::exp(-am * am / 2.0) - std::exp(-bm * bm / 2.0)) / - (sqtp * dem); - } else { - if (am < -10.0) { + if(std::abs(dem) > ep) + { + y(k) = (std::exp(-am * am / 2.0) - std::exp(-bm * bm / 2.0)) / (sqtp * dem); + } + else + { + if(am < -10.0) + { y(k) = bm; - } else if (bm > 10.0) { + } + else if(bm > 10.0) + { y(k) = am; - } else { + } + else + { y(k) = (am + bm) / 2.0; } } - } else { + } + else + { // Degenerate column — zero out and continue c.col(k).segment(k, n - k).setZero(); y(k) = 0.0; @@ -253,9 +273,8 @@ QSimVN::ChlrdrResult QSimVN::chlrdr(const Eigen::MatrixXd &r, // mvndns — inner integrand density evaluation. // Port of nested function mvndns() in qsimvn.m. -double QSimVN::mvndns(int n, const Eigen::MatrixXd &ch, double ci, double dci, - const Eigen::VectorXd &x, const Eigen::VectorXd &a, - const Eigen::VectorXd &b) const { +double QSimVN::mvndns(int n, const Eigen::MatrixXd& ch, double ci, double dci, const Eigen::VectorXd& x, const Eigen::VectorXd& a, const Eigen::VectorXd& b) const +{ const double cn = 37.5; Eigen::VectorXd y = Eigen::VectorXd::Zero(n - 1); double c = ci; @@ -263,7 +282,8 @@ double QSimVN::mvndns(int n, const Eigen::MatrixXd &ch, double ci, double dci, double p = dc; // MATLAB loop: for i = 2:n (1-based). C++ loop variable k = i-1, 0-based. - for (int k = 1; k < n; ++k) { + for(int k = 1; k < n; ++k) + { y(k - 1) = phiInv(c + x(k - 1) * dc); // MATLAB: s = ch(i, 1:i-1) * y(1:i-1) → C++: ch.row(k).head(k) . @@ -274,16 +294,22 @@ double QSimVN::mvndns(int n, const Eigen::MatrixXd &ch, double ci, double dci, const double bi = b(k) - s; double newC; - if (std::abs(ai) < cn * ct) { + if(std::abs(ai) < cn * ct) + { newC = phi(ai / ct); - } else { + } + else + { newC = (1.0 + matlabSign(ai)) / 2.0; } double newD; - if (std::abs(bi) < cn * ct) { + if(std::abs(bi) < cn * ct) + { newD = phi(bi / ct); - } else { + } + else + { newD = (1.0 + matlabSign(bi)) / 2.0; } @@ -299,9 +325,8 @@ double QSimVN::mvndns(int n, const Eigen::MatrixXd &ch, double ci, double dci, // compute — randomised quasi-Monte Carlo outer loop. // Port of the main body of qsimvn() in qsimvn.m. -std::pair QSimVN::compute(int m, const Eigen::MatrixXd &r, - const Eigen::VectorXd &a, - const Eigen::VectorXd &b) { +std::pair QSimVN::compute(int m, const Eigen::MatrixXd& r, const Eigen::VectorXd& a, const Eigen::VectorXd& b) +{ const int n = static_cast(r.rows()); auto [ch, as, bs] = chlrdr(r, a, b); @@ -312,16 +337,22 @@ std::pair QSimVN::compute(int m, const Eigen::MatrixXd &r, const double cn = 37.5; double ci; - if (std::abs(a0) < cn * ct0) { + if(std::abs(a0) < cn * ct0) + { ci = phi(a0 / ct0); - } else { + } + else + { ci = (1.0 + matlabSign(a0)) / 2.0; } double di; - if (std::abs(b0) < cn * ct0) { + if(std::abs(b0) < cn * ct0) + { di = phi(b0 / ct0); - } else { + } + else + { di = (1.0 + matlabSign(b0)) / 2.0; } @@ -330,29 +361,33 @@ std::pair QSimVN::compute(int m, const Eigen::MatrixXd &r, double e = 0.0; const int ns = 12; - const int nv = - std::max(m / ns, 1); // integer division matches MATLAB floor(m/ns) + const int nv = std::max(m / ns, 1); // integer division matches MATLAB floor(m/ns) // Quasi-random Halton base vector: sqrt of first (n-1) primes const Eigen::VectorXd q = primeBasesForDim(n); std::uniform_real_distribution uniform(0.0, 1.0); - for (int i = 1; i <= ns; ++i) { + for(int i = 1; i <= ns; ++i) + { double vi = 0.0; // Random shift vector for this scramble Eigen::VectorXd xr(n - 1); - for (int idx = 0; idx < n - 1; ++idx) { + for(int idx = 0; idx < n - 1; ++idx) + { xr(idx) = uniform(m_Rng); } - for (int j = 1; j <= nv; ++j) { + for(int j = 1; j <= nv; ++j) + { // MATLAB: x = abs(2*mod(j*q + xr, 1) - 1) Eigen::VectorXd x(n - 1); - for (int idx = 0; idx < n - 1; ++idx) { + for(int idx = 0; idx < n - 1; ++idx) + { double val = std::fmod(static_cast(j) * q(idx) + xr(idx), 1.0); - if (val < 0.0) { + if(val < 0.0) + { val += 1.0; } x(idx) = std::abs(2.0 * val - 1.0); @@ -366,11 +401,12 @@ std::pair QSimVN::compute(int m, const Eigen::MatrixXd &r, p += d; // Running error estimate (MATLAB lines 104-108) - if (std::abs(d) > 0.0) { - e = std::abs(d) * - std::sqrt(1.0 + (e / d) * (e / d) * static_cast(i - 2) / - static_cast(i)); - } else if (i > 1) { + if(std::abs(d) > 0.0) + { + e = std::abs(d) * std::sqrt(1.0 + (e / d) * (e / d) * static_cast(i - 2) / static_cast(i)); + } + else if(i > 1) + { e *= std::sqrt(static_cast(i - 2) / static_cast(i)); } } diff --git a/src/LibMTRSim/QSimVN.hpp b/src/LibMTRSim/QSimVN.hpp index b42ed8a..ccba295 100644 --- a/src/LibMTRSim/QSimVN.hpp +++ b/src/LibMTRSim/QSimVN.hpp @@ -5,7 +5,8 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Quasi-Monte Carlo estimator for the multivariate normal CDF. @@ -20,12 +21,13 @@ namespace mtrsim { * License note: the underlying algorithm is due to Alan Genz (BSD-style). * Full attribution is preserved in matlab/qsimvn.m. */ -class LIBMTRSIM_EXPORT QSimVN { +class LIBMTRSIM_EXPORT QSimVN +{ public: /** * @param rng Seeded RNG engine (shared with the rest of the simulation). */ - explicit QSimVN(std::mt19937_64 &rng); + explicit QSimVN(std::mt19937_64& rng); /** * @brief Estimate the MVN probability. @@ -36,29 +38,25 @@ class LIBMTRSIM_EXPORT QSimVN { * @param b Upper integration limits (length n, may be +inf) * @return {probability estimate, error estimate} */ - std::pair compute(int m, const Eigen::MatrixXd &r, - const Eigen::VectorXd &a, - const Eigen::VectorXd &b); + std::pair compute(int m, const Eigen::MatrixXd& r, const Eigen::VectorXd& a, const Eigen::VectorXd& b); private: // Cholesky decomposition with reordering (chlrdr in original) - struct ChlrdrResult { + struct ChlrdrResult + { Eigen::MatrixXd ch; Eigen::VectorXd ap; Eigen::VectorXd bp; }; - ChlrdrResult chlrdr(const Eigen::MatrixXd &r, const Eigen::VectorXd &a, - const Eigen::VectorXd &b) const; + ChlrdrResult chlrdr(const Eigen::MatrixXd& r, const Eigen::VectorXd& a, const Eigen::VectorXd& b) const; - double mvndns(int n, const Eigen::MatrixXd &ch, double ci, double dci, - const Eigen::VectorXd &x, const Eigen::VectorXd &a, - const Eigen::VectorXd &b) const; + double mvndns(int n, const Eigen::MatrixXd& ch, double ci, double dci, const Eigen::VectorXd& x, const Eigen::VectorXd& a, const Eigen::VectorXd& b) const; static double phi(double z); static double phiInv(double p); - std::mt19937_64 &m_Rng; + std::mt19937_64& m_Rng; }; } // namespace mtrsim diff --git a/src/LibMTRSim/SimulationObservers.hpp b/src/LibMTRSim/SimulationObservers.hpp new file mode 100644 index 0000000..76ec4b5 --- /dev/null +++ b/src/LibMTRSim/SimulationObservers.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "ISimulationObserver.hpp" + +#include + +#include +#include + +namespace mtrsim +{ + +/// No-op observer; the implicit default when none is supplied. +class NullObserver : public ISimulationObserver +{ +public: + void updateProgress(int64_t /*done*/, int64_t /*total*/, const std::string& /*message*/) override + { + } + [[nodiscard]] bool shouldCancel() const override + { + return false; + } +}; + +/// Logs progress via spdlog (throttled). Never cancels. +class ConsoleObserver : public ISimulationObserver +{ +public: + void updateProgress(int64_t done, int64_t total, const std::string& message) override + { + const int pct = (total > 0) ? static_cast(done * 100 / total) : -1; + if(pct != m_LastPct) + { + m_LastPct = pct; + if(pct >= 0) + { + spdlog::info("[{:3d}%] {}", pct, message); + } + else + { + spdlog::info("{}", message); + } + } + } + void info(const std::string& message) override + { + spdlog::info("{}", message); + } + + [[nodiscard]] bool shouldCancel() const override + { + return false; + } + +private: + int m_LastPct = -2; +}; + +} // namespace mtrsim diff --git a/src/LibMTRSim/SimulationParams.hpp b/src/LibMTRSim/SimulationParams.hpp index c6dda56..d5eabdf 100644 --- a/src/LibMTRSim/SimulationParams.hpp +++ b/src/LibMTRSim/SimulationParams.hpp @@ -6,14 +6,16 @@ #include #include -namespace mtrsim { +namespace mtrsim +{ /** * @brief Holds all parameters that drive a single MTR simulation run. * * These map directly to the top-level parameter block in simulate_MTRs.m. */ -struct LIBMTRSIM_EXPORT SimulationParams { +struct LIBMTRSIM_EXPORT SimulationParams +{ // Volume dimensions [mm] double xLen = 1.5 * 25.4; double yLen = 0.5 * 25.4; diff --git a/src/MTRSim/Filters/Algorithms/ComputeODF.cpp b/src/MTRSim/Filters/Algorithms/ComputeODF.cpp index d570f34..19864b4 100644 --- a/src/MTRSim/Filters/Algorithms/ComputeODF.cpp +++ b/src/MTRSim/Filters/Algorithms/ComputeODF.cpp @@ -2,9 +2,9 @@ #include "simplnx/Common/Range.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/Utilities/MaskCompareUtilities.hpp" #include "simplnx/Utilities/MessageHelper.hpp" #include "simplnx/Utilities/ParallelDataAlgorithm.hpp" -#include "simplnx/Utilities/MaskCompareUtilities.hpp" #include "LibMTRSim/ODFBuilder.hpp" @@ -32,14 +32,14 @@ using namespace nx::core; namespace { -// Worker functor invoked by ParallelDataAlgorithm. Each invocation owns a private -// accumulator vector and a private contributing-voxel count. On destruction the -// worker merges those private results into the shared master accumulator/count -// under a mutex. +// Worker functor invoked by ParallelDataAlgorithm. Each invocation owns a +// private accumulator vector and a private contributing-voxel count. On +// destruction the worker merges those private results into the shared master +// accumulator/count under a mutex. // // This pattern avoids any concurrent writes to the master state and matches the -// thread-safety guidance: per-thread (here, per-call) std::vector is the -// only mutable thing touched in the parallel section. +// thread-safety guidance: per-thread (here, per-call) std::vector is +// the only mutable thing touched in the parallel section. // // All orientation math goes through EbsdLib's `Euler`, // `OrientationMatrix`, and `LaueOps` directly. Float32 EBSD inputs @@ -94,7 +94,8 @@ class AccumulateWorker for(std::size_t i = range.min(); i < range.max(); ++i) { - // Cancel check kept at outer voxel loop only — inner per-symmetry-variant work is short. + // Cancel check kept at outer voxel loop only — inner per-symmetry-variant + // work is short. if((i & 0x3FFu) == 0 && m_ShouldCancel) { return; @@ -155,8 +156,9 @@ class AccumulateWorker deposits = numOps; } catch(const std::exception&) { - // Accumulator-size mismatch (the only ODFBuilder::accumulate failure mode). - // Counted but skipped — the master merger surfaces a warning if any failures occurred. + // Accumulator-size mismatch (the only ODFBuilder::accumulate failure + // mode). Counted but skipped — the master merger surfaces a warning if + // any failures occurred. ++localFailures; progressMessenger.sendProgressMessage(1); continue; @@ -214,9 +216,10 @@ Result<> ComputeODF::operator()() const auto& eulerAngles = m_DataStructure.getDataRefAs(m_InputValues->eulerAnglesPath); const auto& phases = m_DataStructure.getDataRefAs(m_InputValues->phasesPath); const auto& crystalStructures = m_DataStructure.getDataRefAs(m_InputValues->crystalStructuresPath); - // We get the pointer to the Array instead of a reference because it might not have been set because - // the bool "use_mask" might have been false, but we do NOT want to try to get the array - // 'on demand' in the loop. That is a BAD idea as is it really slow to do that. (10x slower). + // We get the pointer to the Array instead of a reference because it might not + // have been set because the bool "use_mask" might have been false, but we do + // NOT want to try to get the array 'on demand' in the loop. That is a BAD + // idea as is it really slow to do that. (10x slower). std::unique_ptr maskArray = nullptr; if(m_InputValues->useMask) { @@ -225,9 +228,12 @@ Result<> ComputeODF::operator()() maskArray = MaskCompareUtilities::InstantiateMaskCompare(m_DataStructure, m_InputValues->maskPath); } catch(const std::out_of_range& exception) { - // This really should NOT be happening as the path was verified during preflight BUT we may be calling this from - // somewhere else that is NOT going through the normal nx::core::IFilter API of Preflight and Execute - std::string message = fmt::format("Mask Array DataPath does not exist or is not of the correct type (Bool | UInt8) {}", m_InputValues->maskPath.toString()); + // This really should NOT be happening as the path was verified during + // preflight BUT we may be calling this from somewhere else that is NOT + // going through the normal nx::core::IFilter API of Preflight and Execute + std::string message = fmt::format("Mask Array DataPath does not exist or is not of the " + "correct type (Bool | UInt8) {}", + m_InputValues->maskPath.toString()); return MakeErrorResult(-506, message); } } @@ -239,17 +245,21 @@ Result<> ComputeODF::operator()() const std::size_t binCount = static_cast(m_InputValues->nphi1) * static_cast(m_InputValues->nPHI) * static_cast(m_InputValues->nphi2); if(outStore.getSize() != binCount) { - return MakeErrorResult(-12210, fmt::format("Output ODF array size mismatch: have {} but algorithm expected {} (= nphi1 * nPHI * nphi2).", outStore.getSize(), binCount)); + return MakeErrorResult(-12210, fmt::format("Output ODF array size mismatch: have {} but " + "algorithm expected {} (= nphi1 * nPHI * nphi2).", + outStore.getSize(), binCount)); } const std::size_t numVoxels = eulerAngles.getNumberOfTuples(); m_MessageHandler(IFilter::Message::Type::Info, - fmt::format("Computing ODF over {} voxels into {} bins ({} x {} x {}); smoothing = {}.", numVoxels, binCount, m_InputValues->nphi1, m_InputValues->nPHI, m_InputValues->nphi2, - m_InputValues->applySmoothing ? "enabled" : "disabled")); + fmt::format("Computing ODF over {} voxels into {} bins ({} x {} x {}); " + "smoothing = {}.", + numVoxels, binCount, m_InputValues->nphi1, m_InputValues->nPHI, m_InputValues->nphi2, m_InputValues->applySmoothing ? "enabled" : "disabled")); - // Set up the message helper / progress messenger BEFORE the parallel section so the worker - // can stamp throttled per-voxel progress through its private ProgressMessenger. + // Set up the message helper / progress messenger BEFORE the parallel section + // so the worker can stamp throttled per-voxel progress through its private + // ProgressMessenger. MessageHelper messageHelper(m_MessageHandler); auto progressHelper = messageHelper.createProgressMessageHelper(); progressHelper.setMaxProgresss(numVoxels); @@ -272,24 +282,25 @@ Result<> ComputeODF::operator()() ParallelDataAlgorithm parallelAlgorithm; parallelAlgorithm.setRange(0, numVoxels); - parallelAlgorithm.execute(AccumulateWorker(eulerAngles, phases, crystalStructures, maskArray.get(), params, masterValues, masterContributingCount, masterTotalDeposits, masterFailureCount, mergeMutex, - m_ShouldCancel, progressHelper)); + parallelAlgorithm.execute(AccumulateWorker(eulerAngles, phases, crystalStructures, maskArray.get(), params, masterValues, masterContributingCount, masterTotalDeposits, masterFailureCount, + mergeMutex, m_ShouldCancel, progressHelper)); if(m_ShouldCancel) { return {}; } - // Normalize by the total count of symmetric-equivalent orientation deposits (i.e. for each - // contributing voxel, the number of symmetric variants its phase produces). This matches the - // MATLAB calc_ODF.m convention (line 80: N = size(phi1_vec, 1) where phi1_vec is the - // post-symmetry-expansion orientation list). The contributing-voxel count is retained as a - // semantic guard for the "no voxels contributed" early-return path. + // Normalize by the total count of symmetric-equivalent orientation deposits + // (i.e. for each contributing voxel, the number of symmetric variants its + // phase produces). This matches the MATLAB calc_ODF.m convention (line 80: N + // = size(phi1_vec, 1) where phi1_vec is the post-symmetry-expansion + // orientation list). The contributing-voxel count is retained as a semantic + // guard for the "no voxels contributed" early-return path. if(masterContributingCount == 0) { - m_MessageHandler(IFilter::Message::Type::Warning, - "No voxels contributed to the ODF (mask filtered everything out, or all phases were 0). " - "Output ODF array left at all zeros to avoid divide-by-zero."); + m_MessageHandler(IFilter::Message::Type::Warning, "No voxels contributed to the ODF (mask filtered everything out, or " + "all phases were 0). " + "Output ODF array left at all zeros to avoid divide-by-zero."); } else { @@ -317,8 +328,7 @@ Result<> ComputeODF::operator()() const double rowMul = scale / std::sin(phiCenter); for(int32_t i = 0; i < m_InputValues->nphi1; ++i) { - const std::size_t baseIdx = (static_cast(i) * static_cast(m_InputValues->nPHI) + static_cast(j)) - * static_cast(m_InputValues->nphi2); + const std::size_t baseIdx = (static_cast(i) * static_cast(m_InputValues->nPHI) + static_cast(j)) * static_cast(m_InputValues->nphi2); for(int32_t k = 0; k < m_InputValues->nphi2; ++k) { masterValues[baseIdx + static_cast(k)] *= rowMul; @@ -332,17 +342,23 @@ Result<> ComputeODF::operator()() m_MessageHandler(IFilter::Message::Type::Info, "Output units: Count-Density (raw normalized histogram)."); } - m_MessageHandler(IFilter::Message::Type::Info, fmt::format("ODF accumulation complete: {} voxel(s) contributed; copying values to output array.", masterContributingCount)); + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("ODF accumulation complete: {} voxel(s) " + "contributed; copying values to output array.", + masterContributingCount)); - // Bulk copy into the output Float64 store via iterators (matches the T2 polish convention). + // Bulk copy into the output Float64 store via iterators (matches the T2 + // polish convention). std::copy(masterValues.begin(), masterValues.end(), outStore.begin()); - // Surface per-voxel expansion failures (unknown crystal-structure code, accumulator size mismatch) - // as a warning on the result so pipeline UIs can show them. The run itself succeeds — the failing - // voxels are simply excluded from the ODF. + // Surface per-voxel expansion failures (unknown crystal-structure code, + // accumulator size mismatch) as a warning on the result so pipeline UIs can + // show them. The run itself succeeds — the failing voxels are simply excluded + // from the ODF. if(masterFailureCount > 0) { - return MakeWarningVoidResult(-12213, fmt::format("Skipped {} voxel(s) due to expansion errors (e.g. unknown crystal-structure code); those voxels did not contribute to the ODF.", + return MakeWarningVoidResult(-12213, fmt::format("Skipped {} voxel(s) due to expansion errors (e.g. " + "unknown crystal-structure code); those voxels did " + "not contribute to the ODF.", masterFailureCount)); } diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.cpp b/src/MTRSim/Filters/Algorithms/MTRSim.cpp new file mode 100644 index 0000000..e1f81e7 --- /dev/null +++ b/src/MTRSim/Filters/Algorithms/MTRSim.cpp @@ -0,0 +1,186 @@ +#include "MTRSim.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" + +#include "LibMTRSim/IPFMapper.hpp" +#include "LibMTRSim/ISimulationObserver.hpp" +#include "LibMTRSim/MTRSimDriver.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +using namespace nx::core; + +namespace +{ +/** + * @brief Adapts mtrsim::ISimulationObserver to the simplnx filter message + * handler and cancel flag, so simulateMTR can report progress and be cancelled. + */ +class FilterObserver : public mtrsim::ISimulationObserver +{ +public: + FilterObserver(const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) + : m_MessageHandler(messageHandler) + , m_ShouldCancel(shouldCancel) + { + } + + void updateProgress(int64_t done, int64_t total, const std::string& message) override + { + const int32 progress = (total > 0) ? static_cast(done * 100 / total) : 0; + m_MessageHandler(IFilter::Message::Type::Progress, message, progress); + } + + void info(const std::string& message) override + { + m_MessageHandler(IFilter::Message::Type::Info, message); + } + + [[nodiscard]] bool shouldCancel() const override + { + return m_ShouldCancel.load(); + } + +private: + const IFilter::MessageHandler& m_MessageHandler; + const std::atomic_bool& m_ShouldCancel; +}; +} // namespace + +// ----------------------------------------------------------------------------- +MTRSim::MTRSim(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, MTRSimInputValues* inputValues) +: m_DataStructure(dataStructure) +, m_InputValues(inputValues) +, m_ShouldCancel(shouldCancel) +, m_MessageHandler(mesgHandler) +{ +} + +// ----------------------------------------------------------------------------- +MTRSim::~MTRSim() noexcept = default; + +// ----------------------------------------------------------------------------- +Result<> MTRSim::operator()() +{ + m_MessageHandler(IFilter::Message::Type::Info, "Reading ODF geometry..."); + + // 1. ODF grid geometry (degrees) from the input ODF ImageGeom. + const auto& odfGeom = m_DataStructure.getDataRefAs(m_InputValues->inputOdfGeometryPath); + const SizeVec3 odfDims = odfGeom.getDimensions(); // X=phi2, Y=PHI, Z=phi1 + const FloatVec3 odfSpacing = odfGeom.getSpacing(); // degrees + const int n2 = static_cast(odfDims[0]); + const int nPHI = static_cast(odfDims[1]); + const int n1 = static_cast(odfDims[2]); + + // 2. Reconstruct ODFComponents from the selected Float64 cell arrays. The + // store is row-major ZYX (phi1 slowest, phi2 fastest), exactly the flat + // layout gridToODFComponent expects, so we copy values in their natural + // order. + std::vector components; + components.reserve(m_InputValues->odfComponentPaths.size()); + for(const auto& path : m_InputValues->odfComponentPaths) + { + const auto& arr = m_DataStructure.getDataRefAs(path); + const auto& store = arr.getDataStoreRef(); + std::vector values(store.begin(), store.end()); + components.push_back(mtrsim::gridToODFComponent(values, n1, nPHI, n2, static_cast(odfSpacing[2]), static_cast(odfSpacing[1]), static_cast(odfSpacing[0]))); + } + + // 3. SimulationParams — always built from m_InputValues, which executeImpl has + // already populated from either the config file or the manual UI fields. + mtrsim::SimulationParams params; + params.xLen = m_InputValues->physicalSize[0]; + params.yLen = m_InputValues->physicalSize[1]; + params.zLen = m_InputValues->physicalSize[2]; + params.dx = m_InputValues->physicalSpacing[0]; + params.dy = m_InputValues->physicalSpacing[1]; + params.dz = m_InputValues->physicalSpacing[2]; + params.volumeFractions = m_InputValues->volumeFractions[0]; // 1 row + params.thetaList = m_InputValues->thetaList; + params.seed = m_InputValues->seed; + // The rng below is seeded from m_InputValues->seed; SimulationParams::seed is + // not consulted by simulateMTR, but we populate it for completeness. + + if(m_ShouldCancel) + { + return {}; + } + + // 4. Run simulation (SIMPLNX z,y,x order out). + m_MessageHandler(IFilter::Message::Type::Info, "Running MTR simulation (this may take a while for large volumes)..."); + std::mt19937_64 rng(m_InputValues->seed); + FilterObserver observer{m_MessageHandler, m_ShouldCancel}; + mtrsim::MTRSimResult sim; + try + { + sim = mtrsim::simulateMTR(params, components, rng, n1, nPHI, n2, &observer); + } catch(const std::exception& e) + { + return MakeErrorResult(-13550, fmt::format("MTR simulation failed: {}", e.what())); + } + + if(m_ShouldCancel || sim.cancelled) + { + return {}; + } + + // 5. Write MTR ids + Euler arrays (output geometry cell AM). + const DataPath cellAm = m_InputValues->outputGeometryPath.createChildPath(m_InputValues->cellAttrMatName); + auto& mtrIds = m_DataStructure.getDataRefAs(cellAm.createChildPath(m_InputValues->mtrIdsArrayName)); + auto& eulers = m_DataStructure.getDataRefAs(cellAm.createChildPath(m_InputValues->eulersArrayName)); + auto& mtrStore = mtrIds.getDataStoreRef(); + auto& eulerStore = eulers.getDataStoreRef(); + + const std::size_t N = sim.mtrIndex.size(); + for(std::size_t i = 0; i < N; ++i) + { + mtrStore[i] = sim.mtrIndex[i]; + eulerStore[i * 3 + 0] = static_cast(sim.phi1[i]); + eulerStore[i * 3 + 1] = static_cast(sim.phi[i]); + eulerStore[i * 3 + 2] = static_cast(sim.phi2[i]); + } + + m_MessageHandler(IFilter::Message::Type::Info, "MTR simulation complete."); + + if(m_InputValues->generatePolarColoring) + { + m_MessageHandler(IFilter::Message::Type::Info, "Computing polar coloring..."); + auto colorResult = applyPolarColoring(sim, cellAm); + if(colorResult.invalid()) + { + return colorResult; + } + } + + return {}; +} + +// ----------------------------------------------------------------------------- +Result<> MTRSim::applyPolarColoring(const mtrsim::MTRSimResult& sim, const DataPath& cellAttrMatPath) +{ + const std::size_t N = sim.phi1.size(); + Eigen::VectorXd phi1 = Eigen::Map(sim.phi1.data(), static_cast(N)); + Eigen::VectorXd phi = Eigen::Map(sim.phi.data(), static_cast(N)); + Eigen::VectorXd phi2 = Eigen::Map(sim.phi2.data(), static_cast(N)); + + mtrsim::IPFMapper mapper{mtrsim::CrystalSystem::HCP}; + const std::vector colors = mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, mtrsim::IPFColorScheme::MatLab); + + auto& rgb = m_DataStructure.getDataRefAs(cellAttrMatPath.createChildPath(m_InputValues->polarColorsArrayName)); + auto& store = rgb.getDataStoreRef(); + for(std::size_t i = 0; i < N; ++i) + { + store[i * 3 + 0] = colors[i].r; + store[i * 3 + 1] = colors[i].g; + store[i * 3 + 2] = colors[i].b; + } + return {}; +} diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.hpp b/src/MTRSim/Filters/Algorithms/MTRSim.hpp new file mode 100644 index 0000000..4c60ed6 --- /dev/null +++ b/src/MTRSim/Filters/Algorithms/MTRSim.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include "MTRSim/MTRSim_export.hpp" + +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/Filter/IFilter.hpp" + +#include "LibMTRSim/MTRSimDriver.hpp" + +#include + +namespace nx::core +{ + +struct MTRSIM_EXPORT MTRSimInputValues +{ + DataPath inputOdfGeometryPath; + std::vector odfComponentPaths; + std::vector> volumeFractions; // 1 row x N cols + std::vector> thetaList; // M rows x 3 cols + std::vector physicalSize; // [x,y,z] microns + std::vector physicalSpacing; // [x,y,z] microns + uint64 seed; + bool generatePolarColoring; + DataPath outputGeometryPath; + std::string cellAttrMatName; + std::string mtrIdsArrayName; + std::string eulersArrayName; + std::string polarColorsArrayName; +}; + +/** + * @class MTRSim + * @brief Algorithm that generates a synthetic microtexture (MTR) microstructure + * from an input ODF and a set of simulation parameters. The output ImageGeom + * and its cell arrays are created by the filter's preflight; this algorithm + * fills them. + */ +class MTRSIM_EXPORT MTRSim +{ +public: + MTRSim(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, MTRSimInputValues* inputValues); + ~MTRSim() noexcept; + + MTRSim(const MTRSim&) = delete; + MTRSim(MTRSim&&) noexcept = delete; + MTRSim& operator=(const MTRSim&) = delete; + MTRSim& operator=(MTRSim&&) noexcept = delete; + + Result<> operator()(); + +private: + Result<> applyPolarColoring(const mtrsim::MTRSimResult& sim, const DataPath& cellAttrMatPath); + DataStructure& m_DataStructure; + const MTRSimInputValues* m_InputValues = nullptr; + const std::atomic_bool& m_ShouldCancel; + const IFilter::MessageHandler& m_MessageHandler; +}; + +} // namespace nx::core diff --git a/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.cpp b/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.cpp index ecd45fe..1bdeeb9 100644 --- a/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.cpp +++ b/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.cpp @@ -57,7 +57,9 @@ Result<> ReadMTRSimODF::operator()() const auto& srcValues = components[c].values; if(store.getSize() != srcValues.size()) { - return MakeErrorResult(-12012, fmt::format("Component {} size mismatch: array has {} tuples but file provided {} values", c, store.getSize(), srcValues.size())); + return MakeErrorResult(-12012, fmt::format("Component {} size mismatch: array has {} tuples " + "but file provided {} values", + c, store.getSize(), srcValues.size())); } m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Copying component_{} ({} values)", c, srcValues.size())); diff --git a/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.cpp b/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.cpp index ddd08a2..781a529 100644 --- a/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.cpp +++ b/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.cpp @@ -33,7 +33,8 @@ Result<> WriteMTRSimODF::operator()() const auto& geom = m_DataStructure.getDataRefAs(m_InputValues->inputImageGeometry); - // Reverse the (phi2=X, PHI=Y, phi1=Z) ImageGeom convention back to phi1-first on-disk order. + // Reverse the (phi2=X, PHI=Y, phi1=Z) ImageGeom convention back to phi1-first + // on-disk order. const std::array dimsPhi1PHIPhi2 = {static_cast(geom.getNumZCells()), static_cast(geom.getNumYCells()), static_cast(geom.getNumXCells())}; const auto spacingXYZ = geom.getSpacing(); diff --git a/src/MTRSim/Filters/ComputeODFFilter.cpp b/src/MTRSim/Filters/ComputeODFFilter.cpp index b98cfce..83c971e 100644 --- a/src/MTRSim/Filters/ComputeODFFilter.cpp +++ b/src/MTRSim/Filters/ComputeODFFilter.cpp @@ -63,61 +63,82 @@ Parameters ComputeODFFilter::parameters() const params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); params.insert(std::make_unique(k_ApplySmoothing_Key, "Apply Smoothing", - "When true, each symmetric Euler tuple is distributed across 27 bins (1 center + 6 faces + 12 edges + 8 corners) using " - "the MATLAB calc_ODF.m tri-linear weights. When false, each tuple deposits 1.0 into its center bin only.", + "When true, each symmetric Euler tuple is distributed across 27 bins (1 " + "center + 6 faces + 12 edges + 8 corners) using " + "the MATLAB calc_ODF.m tri-linear weights. When false, each tuple " + "deposits 1.0 into its center bin only.", true)); params.insert(std::make_unique(k_BinSizeDeg_Key, "Bin Size [deg]", - "Uniform bin size in degrees, used for all three Bunge-Euler axes (phi1, PHI, phi2). 360/binSize and 180/binSize " + "Uniform bin size in degrees, used for all three Bunge-Euler axes (phi1, " + "PHI, phi2). 360/binSize and 180/binSize " "must both yield a positive integer (e.g. 5.0 -> 72 x 36 x 72 bins).", 5.0f)); params.insert(std::make_unique(k_OutputUnits_Key, "Output Units", - "Count-Density emits the raw normalized histogram (sums to 1 over all Bunge bins; bit-exact match to MATLAB calc_ODF.m). " - "MUD applies the per-PHI-row sin(PHI) Jacobian correction so values represent density on SO(3) divided by uniform " - "density (matches MTEX plot(odf) convention; sub-uniform regions read < 1, peak texture reads several MUD).", + "Count-Density emits the raw normalized histogram (sums to 1 over all " + "Bunge bins; bit-exact match to MATLAB calc_ODF.m). " + "MUD applies the per-PHI-row sin(PHI) Jacobian correction so values " + "represent density on SO(3) divided by uniform " + "density (matches MTEX plot(odf) convention; sub-uniform regions read < " + "1, peak texture reads several MUD).", 0ULL, ChoicesParameter::Choices{"Count-Density", "MUD"})); params.insertSeparator(Parameters::Separator{"Required Input Cell Data"}); params.insert(std::make_unique(k_EulerAngles_Key, "Cell Euler Angles", - "Per-voxel Bunge Euler angles (phi1, PHI, phi2) in radians. Must be a 3-component Float32 array on a cell-level " + "Per-voxel Bunge Euler angles (phi1, PHI, phi2) in radians. Must be a " + "3-component Float32 array on a cell-level " "AttributeMatrix.", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::float32}, ArraySelectionParameter::AllowedComponentShapes{{3}})); params.insert(std::make_unique(k_Phases_Key, "Cell Phases", - "Per-voxel phase label (1-based; 0 marks excluded voxels). Must be a 1-component Int32 array on the same " + "Per-voxel phase label (1-based; 0 marks excluded voxels). Must be a " + "1-component Int32 array on the same " "AttributeMatrix as the Euler angles.", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Optional Input Cell Data"}); params.insertLinkableParameter(std::make_unique(k_UseMask_Key, "Use Mask Array", - "When enabled, only voxels where the selected mask is true (and phase != 0) contribute to the ODF.", false)); + "When enabled, only voxels where the selected mask is true (and phase != " + "0) contribute to the ODF.", + false)); params.insert(std::make_unique(k_Mask_Key, "Mask", - "Per-voxel boolean mask. Must be a 1-component Bool array on the same AttributeMatrix as the Euler angles. Only " + "Per-voxel boolean mask. Must be a 1-component Bool array on the same " + "AttributeMatrix as the Euler angles. Only " "consulted when 'Use Mask Array' is enabled.", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::boolean, DataType::uint8}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Required Input Ensemble Data"}); params.insert(std::make_unique(k_CrystalStructures_Key, "Crystal Structures", - "Per-phase EbsdLib crystal-structure code (e.g. 0 = Hexagonal_High, 1 = Cubic_High). Must be a 1-component " - "UInt32 array on the ensemble-level AttributeMatrix; indexed by phase label.", + "Per-phase EbsdLib crystal-structure code (e.g. 0 = Hexagonal_High, 1 = " + "Cubic_High). Must be a 1-component " + "UInt32 array on the ensemble-level AttributeMatrix; indexed by phase " + "label.", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::uint32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); - params.insertSeparator(Parameters::Separator{"Output Mode"}); params.insertLinkableParameter(std::make_unique(k_OutputMode_Key, "Output Mode", - "Whether to create a new ODF ImageGeometry or append a new component to an existing one. In Append mode the bin " - "size is derived from the existing geometry's spacing and the user's bin size input is ignored.", + "Whether to create a new ODF ImageGeometry or append a new component to " + "an existing one. In Append mode the bin " + "size is derived from the existing geometry's spacing and the user's bin " + "size input is ignored.", 0ULL, ChoicesParameter::Choices{"Create New ODF Geometry", "Append to Existing ODF Geometry"})); params.insertSeparator(Parameters::Separator{"Output Data Object(s)"}); params.insert(std::make_unique(k_OutputImageGeometry_Key, "Output ODF Image Geometry", - "Path at which the ODF ImageGeom will be created. Its (X, Y, Z) dimensions map to (phi2, PHI, phi1).", DataPath({"ODF"}))); + "Path at which the ODF ImageGeom will be created. Its (X, Y, Z) " + "dimensions map to (phi2, PHI, phi1).", + DataPath({"ODF"}))); params.insert(std::make_unique(k_CellAttrMatName_Key, "Cell Attribute Matrix Name", - "Name of the cell AttributeMatrix created under the output ImageGeom. The Float64 ODF array is placed inside it.", "Cell Data")); + "Name of the cell AttributeMatrix created under the output ImageGeom. " + "The Float64 ODF array is placed inside it.", + "Cell Data")); params.insert(std::make_unique(k_ExistingOdfGeometry_Key, "Existing ODF Image Geometry", - "ImageGeom representing an existing Euler-space ODF grid that this filter will append a new component to. Spacing must be " + "ImageGeom representing an existing Euler-space ODF grid that this " + "filter will append a new component to. Spacing must be " "uniform across all three axes.", DataPath{}, GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image})); params.insert(std::make_unique(k_ComponentName_Key, "ODF Component Array Name", - "Name of the Float64 single-component ODF array created on the output cell AttributeMatrix.", "Component 1")); + "Name of the Float64 single-component ODF array created on the output " + "cell AttributeMatrix.", + "Component 1")); params.linkParameters(k_UseMask_Key, k_Mask_Key, true); params.linkParameters(k_OutputMode_Key, k_OutputImageGeometry_Key, std::make_any(0ULL)); @@ -141,8 +162,8 @@ IFilter::UniquePointer ComputeODFFilter::clone() const } //------------------------------------------------------------------------------ -IFilter::PreflightResult ComputeODFFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +IFilter::PreflightResult ComputeODFFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const { auto pOutputMode = filterArgs.value(k_OutputMode_Key); auto pOutputUnits = filterArgs.value(k_OutputUnits_Key); @@ -163,9 +184,10 @@ IFilter::PreflightResult ComputeODFFilter::preflightImpl(const DataStructure& da nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; - // 1. Determine bin size (& derived dims). In Create New mode we validate the user's input; - // in Append mode we derive from the existing geometry's (uniform) spacing and silently - // ignore the user's bin_size_deg input. + // 1. Determine bin size (& derived dims). In Create New mode we validate the + // user's input; + // in Append mode we derive from the existing geometry's (uniform) spacing + // and silently ignore the user's bin_size_deg input. double binSize = 0.0; int32_t nphi1 = 0; int32_t nPHI = 0; @@ -182,12 +204,13 @@ IFilter::PreflightResult ComputeODFFilter::preflightImpl(const DataStructure& da const double phi1Quotient = 360.0 / binSize; const double phiQuotient = 180.0 / binSize; constexpr double k_DivisorTolerance = 1.0e-6; - if(std::abs(phi1Quotient - std::round(phi1Quotient)) > k_DivisorTolerance || std::abs(phiQuotient - std::round(phiQuotient)) > k_DivisorTolerance || std::round(phi1Quotient) <= 0.0 - || std::round(phiQuotient) <= 0.0) + if(std::abs(phi1Quotient - std::round(phi1Quotient)) > k_DivisorTolerance || std::abs(phiQuotient - std::round(phiQuotient)) > k_DivisorTolerance || std::round(phi1Quotient) <= 0.0 || + std::round(phiQuotient) <= 0.0) { - return {MakeErrorResult( - -12200, - fmt::format("Bin size {} deg does not evenly divide 360 and 180. Pick a value such that 360/binSize and 180/binSize are both positive integers (e.g. 5.0, 2.0, 1.0).", pBinSizeDeg))}; + return {MakeErrorResult(-12200, fmt::format("Bin size {} deg does not evenly divide 360 and 180. " + "Pick a value such that 360/binSize and 180/binSize are " + "both positive integers (e.g. 5.0, 2.0, 1.0).", + pBinSizeDeg))}; } nphi1 = static_cast(std::round(360.0 / binSize)); nPHI = static_cast(std::round(180.0 / binSize)); @@ -195,24 +218,29 @@ IFilter::PreflightResult ComputeODFFilter::preflightImpl(const DataStructure& da } else { - // Append mode: validate the existing ODF ImageGeom up front so we can derive the bin size. + // Append mode: validate the existing ODF ImageGeom up front so we can + // derive the bin size. const auto* existingGeom = dataStructure.getDataAs(pExistingOdfGeomPath); if(existingGeom == nullptr) { - return {MakeErrorResult(-12206, fmt::format("Existing ODF Image Geometry '{}' must exist and be an ImageGeom.", pExistingOdfGeomPath.toString()))}; + return {MakeErrorResult(-12206, fmt::format("Existing ODF Image Geometry '{}' must exist and " + "be an ImageGeom.", + pExistingOdfGeomPath.toString()))}; } const FloatVec3 spacing = existingGeom->getSpacing(); constexpr float32 k_SpacingTolerance = 1.0e-6f; if(std::abs(spacing[0] - spacing[1]) > k_SpacingTolerance || std::abs(spacing[0] - spacing[2]) > k_SpacingTolerance) { - return {MakeErrorResult(-12207, - fmt::format("Existing ODF Image Geometry '{}' has non-uniform spacing ({}, {}, {}) across axes; ODF requires uniform bins on all three axes.", - pExistingOdfGeomPath.toString(), spacing[0], spacing[1], spacing[2]))}; + return {MakeErrorResult(-12207, fmt::format("Existing ODF Image Geometry '{}' has " + "non-uniform spacing ({}, {}, {}) across axes; " + "ODF requires uniform bins on all three axes.", + pExistingOdfGeomPath.toString(), spacing[0], spacing[1], spacing[2]))}; } if(spacing[0] <= 0.0f) { - return {MakeErrorResult( - -12207, fmt::format("Existing ODF Image Geometry '{}' has zero or negative spacing ({}); cannot derive a valid bin size.", pExistingOdfGeomPath.toString(), spacing[0]))}; + return {MakeErrorResult(-12207, fmt::format("Existing ODF Image Geometry '{}' has zero or negative " + "spacing ({}); cannot derive a valid bin size.", + pExistingOdfGeomPath.toString(), spacing[0]))}; } binSize = static_cast(spacing[0]); binSizeIsDerived = true; @@ -221,34 +249,39 @@ IFilter::PreflightResult ComputeODFFilter::preflightImpl(const DataStructure& da nPHI = static_cast(existingGeom->getNumYCells()); nphi1 = static_cast(existingGeom->getNumZCells()); - // Defense-in-depth: reject existing geometries whose dims don't match the 360/binSize (phi1, phi2) - // and 180/binSize (PHI) integer-divisor constraints. Catches both zero-sized axes and mismatched - // shape/spacing combinations (e.g. dims 100x50x100 with spacing 5.0 deg). + // Defense-in-depth: reject existing geometries whose dims don't match the + // 360/binSize (phi1, phi2) and 180/binSize (PHI) integer-divisor + // constraints. Catches both zero-sized axes and mismatched shape/spacing + // combinations (e.g. dims 100x50x100 with spacing 5.0 deg). const double expected360 = 360.0 / binSize; const double expected180 = 180.0 / binSize; constexpr double k_DivisorTolerance = 1.0e-6; - if(std::abs(expected360 - std::round(expected360)) > k_DivisorTolerance || std::abs(expected180 - std::round(expected180)) > k_DivisorTolerance - || std::round(expected360) != static_cast(nphi1) || std::round(expected180) != static_cast(nPHI) || std::round(expected360) != static_cast(nphi2)) + if(std::abs(expected360 - std::round(expected360)) > k_DivisorTolerance || std::abs(expected180 - std::round(expected180)) > k_DivisorTolerance || + std::round(expected360) != static_cast(nphi1) || std::round(expected180) != static_cast(nPHI) || std::round(expected360) != static_cast(nphi2)) { - return {MakeErrorResult(-12212, - fmt::format("Existing ODF geometry is inconsistent with a valid ODF grid: dims {}x{}x{} (phi1 x PHI x phi2) with derived bin size {:.4f} deg " - "do not satisfy the 360/binSize (phi1/phi2) and 180/binSize (PHI) integer-divisor constraints.", - nphi1, nPHI, nphi2, binSize))}; + return {MakeErrorResult(-12212, fmt::format("Existing ODF geometry is inconsistent with a " + "valid ODF grid: dims {}x{}x{} (phi1 x PHI x " + "phi2) with derived bin size {:.4f} deg " + "do not satisfy the 360/binSize (phi1/phi2) and " + "180/binSize (PHI) integer-divisor constraints.", + nphi1, nPHI, nphi2, binSize))}; } } - // Safety cap on total bin count. Each parallel worker allocates its own std::vector - // of this size, so a pathological user-supplied bin_size_deg (e.g. 0.1 → 360x180x360 ~ 23M bins - // x 8 bytes x N threads) can blow past usable RAM. 10^8 bins = 800 MB per accumulator is the - // hard upper bound we allow. + // Safety cap on total bin count. Each parallel worker allocates its own + // std::vector of this size, so a pathological user-supplied + // bin_size_deg (e.g. 0.1 → 360x180x360 ~ 23M bins x 8 bytes x N threads) can + // blow past usable RAM. 10^8 bins = 800 MB per accumulator is the hard upper + // bound we allow. constexpr std::size_t k_MaxBinCount = 100000000; const std::size_t totalBinCount = static_cast(nphi1) * static_cast(nPHI) * static_cast(nphi2); if(totalBinCount > k_MaxBinCount) { - return {MakeErrorResult(-12211, - fmt::format("Total bin count {} exceeds the safety limit of {}. Consider a larger bin_size_deg (e.g. 5.0 deg -> 72x36x72 = 186624 bins is typical; " - "1.0 deg -> 360x180x360 ~ 23M bins is already above the cap).", - totalBinCount, k_MaxBinCount))}; + return {MakeErrorResult(-12211, fmt::format("Total bin count {} exceeds the safety limit of {}. Consider a " + "larger bin_size_deg (e.g. 5.0 deg -> 72x36x72 = 186624 bins is " + "typical; " + "1.0 deg -> 360x180x360 ~ 23M bins is already above the cap).", + totalBinCount, k_MaxBinCount))}; } // Ensure all "Cell" arrays have the same number of tuples @@ -262,29 +295,33 @@ IFilter::PreflightResult ComputeODFFilter::preflightImpl(const DataStructure& da auto tupleValidityCheck = dataStructure.validateNumberOfTuples(dataPaths); if(!tupleValidityCheck) { - return {MakeErrorResult(-651, fmt::format("The following DataArrays all must have equal number of tuples but this was not satisfied.\n{}", tupleValidityCheck.error()))}; + return {MakeErrorResult(-651, fmt::format("The following DataArrays all must have equal number " + "of tuples but this was not satisfied.\n{}", + tupleValidityCheck.error()))}; } - // 3. Same parent (AttributeMatrix) for the cell-level inputs. const DataPath eulerParent = pEulerAnglesPath.getParent(); const DataPath phasesParent = pPhasesPath.getParent(); if(eulerParent != phasesParent) { - return {MakeErrorResult( - -12205, fmt::format("Cell Euler Angles ('{}') and Cell Phases ('{}') must live on the same AttributeMatrix.", pEulerAnglesPath.toString(), pPhasesPath.toString()))}; + return {MakeErrorResult(-12205, fmt::format("Cell Euler Angles ('{}') and Cell Phases ('{}') must live " + "on the same AttributeMatrix.", + pEulerAnglesPath.toString(), pPhasesPath.toString()))}; } if(pUseMask) { const DataPath maskParent = pMaskPath.getParent(); if(maskParent != eulerParent) { - return { - MakeErrorResult(-12205, fmt::format("Mask ('{}') must live on the same AttributeMatrix as the Cell Euler Angles ('{}').", pMaskPath.toString(), pEulerAnglesPath.toString()))}; + return {MakeErrorResult(-12205, fmt::format("Mask ('{}') must live on the same AttributeMatrix as " + "the Cell Euler Angles ('{}').", + pMaskPath.toString(), pEulerAnglesPath.toString()))}; } } - // 4. Mode-specific: resolve target cell AttributeMatrix path + component path, then check collisions. + // 4. Mode-specific: resolve target cell AttributeMatrix path + component + // path, then check collisions. DataPath targetImageGeomPath; DataPath cellAttrMatPath; if(!isAppendMode) @@ -294,31 +331,36 @@ IFilter::PreflightResult ComputeODFFilter::preflightImpl(const DataStructure& da } else { - // Append mode: discover the existing geometry's cell AttributeMatrix via getCellData()/getCellDataPath(). - // getCellDataPath() throws if there's no cell-data AttributeMatrix assigned, so use the nullable - // accessor first and fail gracefully with an error code instead. + // Append mode: discover the existing geometry's cell AttributeMatrix via + // getCellData()/getCellDataPath(). getCellDataPath() throws if there's no + // cell-data AttributeMatrix assigned, so use the nullable accessor first + // and fail gracefully with an error code instead. const auto& existingGeom = dataStructure.getDataRefAs(pExistingOdfGeomPath); if(existingGeom.getCellData() == nullptr) { - return {MakeErrorResult( - -12208, fmt::format("Existing ODF Image Geometry '{}' has no cell AttributeMatrix; cannot append a component.", pExistingOdfGeomPath.toString()))}; + return {MakeErrorResult(-12208, fmt::format("Existing ODF Image Geometry '{}' has no cell " + "AttributeMatrix; cannot append a component.", + pExistingOdfGeomPath.toString()))}; } cellAttrMatPath = existingGeom.getCellDataPath(); targetImageGeomPath = pExistingOdfGeomPath; - // Component-name collision: the new component must not already exist on the target cell AM. + // Component-name collision: the new component must not already exist on the + // target cell AM. const DataPath proposedComponentPath = cellAttrMatPath.createChildPath(pComponentName); if(dataStructure.getDataAs(proposedComponentPath) != nullptr) { - return {MakeErrorResult( - -12209, fmt::format("Component '{}' already exists on the target geometry's cell AttributeMatrix ('{}'); pick a different component name.", pComponentName, cellAttrMatPath.toString()))}; + return {MakeErrorResult(-12209, fmt::format("Component '{}' already exists on the target geometry's cell " + "AttributeMatrix ('{}'); pick a different component name.", + pComponentName, cellAttrMatPath.toString()))}; } } // 5. Output actions. if(!isAppendMode) { - // ImageGeom XYZ = (phi2, PHI, phi1); array tuple shape ZYX = (nphi1, nPHI, nphi2). + // ImageGeom XYZ = (phi2, PHI, phi1); array tuple shape ZYX = (nphi1, nPHI, + // nphi2). const std::vector imageGeomDimsXYZ = {static_cast(nphi2), static_cast(nPHI), static_cast(nphi1)}; const std::vector imageGeomOrigin = {0.0f, 0.0f, 0.0f}; const std::vector imageGeomSpacingXYZ = {static_cast(binSize), static_cast(binSize), static_cast(binSize)}; @@ -372,15 +414,17 @@ Result<> ComputeODFFilter::executeImpl(DataStructure& dataStructure, const Argum inputValues.outputImageGeometry = filterArgs.value(k_OutputImageGeometry_Key); inputValues.cellAttrMatName = filterArgs.value(k_CellAttrMatName_Key); - // Pre-compute axis dims here so the algorithm doesn't have to repeat it. Preflight has already - // validated that binSizeDeg cleanly divides 360 and 180, so std::round is exact. + // Pre-compute axis dims here so the algorithm doesn't have to repeat it. + // Preflight has already validated that binSizeDeg cleanly divides 360 and + // 180, so std::round is exact. inputValues.nphi1 = static_cast(std::round(360.0 / inputValues.binSizeDeg)); inputValues.nPHI = static_cast(std::round(180.0 / inputValues.binSizeDeg)); inputValues.nphi2 = inputValues.nphi1; } else { - // Append mode: override target geometry / cell attr matrix name / bin size from the existing geometry. + // Append mode: override target geometry / cell attr matrix name / bin size + // from the existing geometry. const auto pExistingOdfGeomPath = filterArgs.value(k_ExistingOdfGeometry_Key); const auto& existingGeom = dataStructure.getDataRefAs(pExistingOdfGeomPath); const FloatVec3 spacing = existingGeom.getSpacing(); diff --git a/src/MTRSim/Filters/ComputeODFFilter.hpp b/src/MTRSim/Filters/ComputeODFFilter.hpp index 6f0ffb2..632d44e 100644 --- a/src/MTRSim/Filters/ComputeODFFilter.hpp +++ b/src/MTRSim/Filters/ComputeODFFilter.hpp @@ -16,11 +16,12 @@ namespace nx::core * axes map to (phi2, PHI, phi1) in degrees. * * Supports two output modes (Milestone AJ, Task 7): - * - "Create New ODF Geometry" (T7a): create a new ImageGeom + cell AttributeMatrix + - * Float64 ODF component. - * - "Append to Existing ODF Geometry" (T7b): append a new Float64 ODF component to - * a user-selected existing ImageGeom's cell AttributeMatrix. In this mode the - * bin size is derived from the existing geometry's (uniform) spacing. + * - "Create New ODF Geometry" (T7a): create a new ImageGeom + cell + * AttributeMatrix + Float64 ODF component. + * - "Append to Existing ODF Geometry" (T7b): append a new Float64 ODF + * component to a user-selected existing ImageGeom's cell AttributeMatrix. In + * this mode the bin size is derived from the existing geometry's (uniform) + * spacing. */ class MTRSIM_EXPORT ComputeODFFilter : public IFilter { @@ -35,19 +36,19 @@ class MTRSIM_EXPORT ComputeODFFilter : public IFilter ComputeODFFilter& operator=(ComputeODFFilter&&) noexcept = delete; // Parameter Keys - static inline constexpr StringLiteral k_ApplySmoothing_Key = "apply_smoothing"; - static inline constexpr StringLiteral k_BinSizeDeg_Key = "bin_size_deg"; - static inline constexpr StringLiteral k_EulerAngles_Key = "euler_angles"; - static inline constexpr StringLiteral k_Phases_Key = "phases"; - static inline constexpr StringLiteral k_CrystalStructures_Key = "crystal_structures"; - static inline constexpr StringLiteral k_UseMask_Key = "use_mask"; - static inline constexpr StringLiteral k_Mask_Key = "mask"; - static inline constexpr StringLiteral k_OutputMode_Key = "output_mode"; - static inline constexpr StringLiteral k_OutputUnits_Key = "output_units"; - static inline constexpr StringLiteral k_OutputImageGeometry_Key = "output_image_geometry"; - static inline constexpr StringLiteral k_CellAttrMatName_Key = "cell_attribute_matrix_name"; - static inline constexpr StringLiteral k_ExistingOdfGeometry_Key = "existing_odf_geometry"; - static inline constexpr StringLiteral k_ComponentName_Key = "component_name"; + static constexpr StringLiteral k_ApplySmoothing_Key = "apply_smoothing"; + static constexpr StringLiteral k_BinSizeDeg_Key = "bin_size_deg"; + static constexpr StringLiteral k_EulerAngles_Key = "euler_angles_path"; + static constexpr StringLiteral k_Phases_Key = "phases_path"; + static constexpr StringLiteral k_CrystalStructures_Key = "crystal_structures_path"; + static constexpr StringLiteral k_UseMask_Key = "use_mask"; + static constexpr StringLiteral k_Mask_Key = "mask_path"; + static constexpr StringLiteral k_OutputMode_Key = "output_mode_index"; + static constexpr StringLiteral k_OutputUnits_Key = "output_units_index"; + static constexpr StringLiteral k_OutputImageGeometry_Key = "output_image_geometry_path"; + static constexpr StringLiteral k_CellAttrMatName_Key = "cell_attribute_matrix_name"; + static constexpr StringLiteral k_ExistingOdfGeometry_Key = "existing_odf_geometry_path"; + static constexpr StringLiteral k_ComponentName_Key = "component_name"; /** * @brief Reads SIMPL json and converts it simplnx Arguments. @@ -108,29 +109,40 @@ class MTRSIM_EXPORT ComputeODFFilter : public IFilter protected: /** - * @brief Takes in a DataStructure and checks that the filter can be run on it with the given arguments. - * Returns any warnings/errors. Also returns the changes that would be applied to the DataStructure. - * Some parts of the actions may not be completely filled out if all the required information is not available at preflight time. + * @brief Takes in a DataStructure and checks that the filter can be run on it + * with the given arguments. Returns any warnings/errors. Also returns the + * changes that would be applied to the DataStructure. Some parts of the + * actions may not be completely filled out if all the required information is + * not available at preflight time. * @param dataStructure The input DataStructure instance - * @param filterArgs These are the input values for each parameter that is required for the filter + * @param filterArgs These are the input values for each parameter that is + * required for the filter * @param messageHandler The MessageHandler object - * @param shouldCancel Atomic boolean value that can be checked to cancel the filter - * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path - * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + * @param shouldCancel Atomic boolean value that can be checked to cancel the + * filter + * @param executionContext The ExecutionContext that can be used to determine + * the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of + * those occurred during execution of this function */ PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const override; /** - * @brief Applies the filter's algorithm to the DataStructure with the given arguments. Returns any warnings/errors. - * On failure, there is no guarantee that the DataStructure is in a correct state. + * @brief Applies the filter's algorithm to the DataStructure with the given + * arguments. Returns any warnings/errors. On failure, there is no guarantee + * that the DataStructure is in a correct state. * @param dataStructure The input DataStructure instance - * @param filterArgs These are the input values for each parameter that is required for the filter + * @param filterArgs These are the input values for each parameter that is + * required for the filter * @param pipelineNode The node in the pipeline that is being executed * @param messageHandler The MessageHandler object - * @param shouldCancel Atomic boolean value that can be checked to cancel the filter - * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path - * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + * @param shouldCancel Atomic boolean value that can be checked to cancel the + * filter + * @param executionContext The ExecutionContext that can be used to determine + * the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of + * those occurred during execution of this function */ Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const override; diff --git a/src/MTRSim/Filters/MTRSimFilter.cpp b/src/MTRSim/Filters/MTRSimFilter.cpp new file mode 100644 index 0000000..9785bc6 --- /dev/null +++ b/src/MTRSim/Filters/MTRSimFilter.cpp @@ -0,0 +1,365 @@ +#include "MTRSimFilter.hpp" + +#include "MTRSim/Filters/Algorithms/MTRSim.hpp" + +#include "simplnx/Common/DataTypeUtilities.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/Filter/Actions/CreateArrayAction.hpp" +#include "simplnx/Filter/Actions/CreateImageGeometryAction.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/DataGroupCreationParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Parameters/DynamicTableParameter.hpp" +#include "simplnx/Parameters/FileSystemPathParameter.hpp" +#include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Parameters/MultiArraySelectionParameter.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Parameters/util/DynamicTableInfo.hpp" + +#include "LibMTRSim/ConfigIO.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +using namespace nx::core; + +namespace nx::core +{ +//------------------------------------------------------------------------------ +std::string MTRSimFilter::name() const +{ + return FilterTraits::name.str(); +} + +//------------------------------------------------------------------------------ +std::string MTRSimFilter::className() const +{ + return FilterTraits::className; +} + +//------------------------------------------------------------------------------ +Uuid MTRSimFilter::uuid() const +{ + return FilterTraits::uuid; +} + +//------------------------------------------------------------------------------ +std::string MTRSimFilter::humanName() const +{ + return "Generate Synthetic Microtexture"; +} + +//------------------------------------------------------------------------------ +std::vector MTRSimFilter::defaultTags() const +{ + return {className(), "MTRSim", "Synthetic", "Microtexture", "Generate"}; +} + +//------------------------------------------------------------------------------ +Parameters MTRSimFilter::parameters() const +{ + Parameters params; + + params.insertSeparator(Parameters::Separator{"Configuration Source"}); + params.insertLinkableParameter(std::make_unique(k_UseConfigFile_Key, "Load Simulation Parameters from Config File", + "When ON, read volume fractions, theta list, physical size/spacing, and seed from an MTRSim JSON config " + "file instead of the fields below.", + false)); + params.insert(std::make_unique(k_ConfigFilePath_Key, "MTRSim Config File (JSON)", + "MTRSim configuration JSON (same schema as the standalone tool). odfInputPath and nuggetVariance are ignored by " + "the filter.", + fs::path(""), FileSystemPathParameter::ExtensionsType{".json"}, FileSystemPathParameter::PathType::InputFile)); + + params.insertSeparator(Parameters::Separator{"Simulation Parameters"}); + { + DynamicTableInfo vfInfo; + vfInfo.setRowsInfo(DynamicTableInfo::StaticVectorInfo(1)); + vfInfo.setColsInfo(DynamicTableInfo::DynamicVectorInfo(1, 3, "Comp {}")); + params.insert(std::make_unique(k_VolumeFractions_Key, "Volume Fraction", + "One value per ODF component; must match the component count and sum " + "to 1.0.", + vfInfo)); + } + { + DynamicTableInfo thetaInfo; + thetaInfo.setRowsInfo(DynamicTableInfo::DynamicVectorInfo(1, 2, "Gaussian {}")); + thetaInfo.setColsInfo(DynamicTableInfo::StaticVectorInfo(DynamicTableInfo::HeadersListType{"theta_x", "theta_y", "theta_z"})); + params.insert(std::make_unique(k_ThetaList_Key, "Theta List", + "Correlation lengths [theta_x, theta_y, theta_z] per latent Gaussian. " + "Needs >= (components - 1) rows. Same length unit as Physical " + "Size/Spacing.", + thetaInfo)); + } + params.insert(std::make_unique(k_PhysicalSize_Key, "Physical Size (microns)", + "Domain extent X, Y, Z in microns. Set Z = 0 to generate a single-layer " + "(2D) microstructure.", + std::vector{38.1f, 12.7f, 0.0f}, std::vector{"X", "Y", "Z"})); + params.insert(std::make_unique(k_PhysicalSpacing_Key, "Physical Spacing (microns)", "Voxel spacing X,Y,Z.", std::vector{0.02f, 0.02f, 0.02f}, + std::vector{"X", "Y", "Z"})); + + params.insertSeparator(Parameters::Separator{"Input ODF"}); + params.insert(std::make_unique(k_InputOdfGeometry_Key, "Input ODF Geometry", "Image Geometry holding the ODF (from the Read/Compute ODF filters).", DataPath{}, + GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image})); + params.insert(std::make_unique(k_OdfComponentArrays_Key, "ODF Component Arrays", + "Ordered list of per-component ODF cell arrays. Order maps to Volume " + "Fraction columns.", + MultiArraySelectionParameter::ValueType{}, MultiArraySelectionParameter::AllowedTypes{IArray::ArrayType::DataArray}, + GetAllNumericTypes(), MultiArraySelectionParameter::AllowedComponentShapes{{1}})); + + params.insertSeparator(Parameters::Separator{"Random Number Seed Parameters"}); + params.insertLinkableParameter(std::make_unique(k_UseSeed_Key, "Use Seed for Random Generation", "When true the user can supply a fixed seed.", false)); + params.insert(std::make_unique>(k_SeedValue_Key, "Seed Value", "The seed fed into the random generator.", std::mt19937::default_seed)); + params.insert(std::make_unique(k_SeedArrayName_Key, "Stored Seed Value Array Name", "Top-level array recording the seed used.", "MTRSim SeedValue")); + + params.insertSeparator(Parameters::Separator{"Output Data Object(s)"}); + + params.insert(std::make_unique(k_OutputGeometry_Key, "Output Image Geometry", "Path of the new microstructure Image Geometry.", DataPath({"MTR Microstructure"}))); + params.insert(std::make_unique(k_CellAttrMatName_Key, "Cell Attribute Matrix Name", "Name of the created cell AttributeMatrix.", "Cell Data")); + params.insert(std::make_unique(k_MtrIdsArrayName_Key, "MTR Ids Array Name", "Int32 per-voxel MTR component id (1-based).", "MTRIds")); + params.insert(std::make_unique(k_EulersArrayName_Key, "Euler Angles Array Name", "Float32 3-component Bunge Euler angles [rad].", "Eulers")); + + params.insertLinkableParameter(std::make_unique(k_GeneratePolarColoring_Key, "Generate Polar Coloring", + "Create a 3-component UInt8 RGB array using the MATLAB polar color " + "mapping.", + false)); + params.insert(std::make_unique(k_PolarColorsArrayName_Key, "Polar Colors Array Name", "UInt8 3-component RGB polar coloring.", "Polar Colors")); + + params.linkParameters(k_UseSeed_Key, k_SeedValue_Key, true); + params.linkParameters(k_GeneratePolarColoring_Key, k_PolarColorsArrayName_Key, true); + + // Config-mode show/hide: when "Load from Config File" is ON, hide the manual simulation-parameter fields. + // + // Approach: DIRECT links (not nested). simplnx's Parameters::linkParameters() throws if the child key is + // itself a linkable/group key (see Parameters.cpp: "Group '{}' cannot be a child of group '{}'"), so we + // CANNOT make k_UseSeed_Key (a linkable controlling k_SeedValue_Key) a child of k_UseConfigFile_Key. + // Instead we link the non-linkable seed children (k_SeedValue_Key, k_SeedArrayName_Key) directly to + // useConfigFile==false. Note: a parameter active-state ORs across all of its groups, so k_SeedValue_Key + // stays visible while k_UseSeed_Key is ON regardless of config mode; the "Use Seed" checkbox itself remains + // visible in config mode but is inert because the config-file seed always wins (see executeImpl/preflightImpl). + params.linkParameters(k_UseConfigFile_Key, k_ConfigFilePath_Key, true); + params.linkParameters(k_UseConfigFile_Key, k_VolumeFractions_Key, false); + params.linkParameters(k_UseConfigFile_Key, k_ThetaList_Key, false); + params.linkParameters(k_UseConfigFile_Key, k_PhysicalSize_Key, false); + params.linkParameters(k_UseConfigFile_Key, k_PhysicalSpacing_Key, false); + params.linkParameters(k_UseConfigFile_Key, k_SeedValue_Key, false); + params.linkParameters(k_UseConfigFile_Key, k_SeedArrayName_Key, false); + + return params; +} + +//------------------------------------------------------------------------------ +IFilter::VersionType MTRSimFilter::parametersVersion() const +{ + return 1; +} + +//------------------------------------------------------------------------------ +IFilter::UniquePointer MTRSimFilter::clone() const +{ + return std::make_unique(); +} + +//------------------------------------------------------------------------------ +IFilter::PreflightResult MTRSimFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const +{ + auto pOdfArrays = filterArgs.value(k_OdfComponentArrays_Key); + const bool useConfig = filterArgs.value(k_UseConfigFile_Key); + auto pGenPolar = filterArgs.value(k_GeneratePolarColoring_Key); + auto pOutGeomPath = filterArgs.value(k_OutputGeometry_Key); + auto pCellAttrMatName = filterArgs.value(k_CellAttrMatName_Key); + auto pMtrIdsName = filterArgs.value(k_MtrIdsArrayName_Key); + auto pEulersName = filterArgs.value(k_EulersArrayName_Key); + auto pPolarName = filterArgs.value(k_PolarColorsArrayName_Key); + auto pSeedArrayName = filterArgs.value(k_SeedArrayName_Key); + + nx::core::Result resultOutputActions; + std::vector preflightUpdatedValues; + + // Source the simulation parameters either from the MTRSim JSON config file or from the manual fields. + std::vector> pVolumeFractions; + std::vector> pThetaList; + std::vector pSize; + std::vector pSpacing; + if(useConfig) + { + mtrsim::SimulationParams cfg; + try + { + cfg = mtrsim::parseConfigJson(filterArgs.value(k_ConfigFilePath_Key)); + } catch(const std::exception& e) + { + return {MakeErrorResult(-13520, fmt::format("MTRSim config file error: {}", e.what()))}; + } + pVolumeFractions = {cfg.volumeFractions}; + pThetaList = cfg.thetaList; + pSize = {static_cast(cfg.xLen), static_cast(cfg.yLen), static_cast(cfg.zLen)}; + pSpacing = {static_cast(cfg.dx), static_cast(cfg.dy), static_cast(cfg.dz)}; + } + else + { + pVolumeFractions = filterArgs.value(k_VolumeFractions_Key); + pThetaList = filterArgs.value(k_ThetaList_Key); + pSize = filterArgs.value>(k_PhysicalSize_Key); + pSpacing = filterArgs.value>(k_PhysicalSpacing_Key); + } + + const usize numComponents = pOdfArrays.size(); + if(numComponents < 2) + { + return {MakeErrorResult(-13501, "MTRSim requires at least 2 ODF component arrays.")}; + } + if(pVolumeFractions.size() != 1 || pVolumeFractions[0].size() != numComponents) + { + return {MakeErrorResult(-13502, fmt::format("Volume Fraction must be 1 row x {} columns (one " + "per ODF component).", + numComponents))}; + } + for(double v : pVolumeFractions[0]) + { + if(v < 0.0 || v > 1.0) + { + return {MakeErrorResult(-13507, fmt::format("Each Volume Fraction value must be in the range " + "[0, 1] (got {:.4f}).", + v))}; + } + } + double vfSum = 0.0; + for(double v : pVolumeFractions[0]) + { + vfSum += v; + } + if(std::abs(vfSum - 1.0) > 1.0e-3) + { + return {MakeErrorResult(-13503, fmt::format("Volume Fraction values must sum to 1.0 (got {:.4f}).", vfSum))}; + } + if(pThetaList.size() < numComponents - 1) + { + return {MakeErrorResult(-13504, fmt::format("Theta List needs at least {} rows (components - 1).", numComponents - 1))}; + } + for(const auto& row : pThetaList) + { + if(row.size() != 3) + { + return {MakeErrorResult(-13505, "Each Theta List row must have exactly 3 columns.")}; + } + } + + if(pSpacing[0] <= 0.0f || pSpacing[1] <= 0.0f) + { + return {MakeErrorResult(-13506, "Physical Spacing X and Y must be greater than 0.")}; + } + + const auto dim = [](float len, float sp) { return static_cast(std::max(std::lround(len / sp), 1L)); }; + const usize nx = dim(pSize[0], pSpacing[0]); + const usize ny = dim(pSize[1], pSpacing[1]); + const usize nz = (pSize[2] <= 0.0f) ? 1 : dim(pSize[2], pSpacing[2]); + + const std::vector imageGeomDimsXYZ = {nx, ny, nz}; + const std::vector origin = {0.0f, 0.0f, 0.0f}; + const std::vector spacingXYZ = {pSpacing[0], pSpacing[1], pSpacing[2]}; + const std::vector tupleShapeZYX = {nz, ny, nx}; + + resultOutputActions.value().appendAction(std::make_unique(pOutGeomPath, imageGeomDimsXYZ, origin, spacingXYZ, pCellAttrMatName)); + + const DataPath cellAttrMatPath = pOutGeomPath.createChildPath(pCellAttrMatName); + resultOutputActions.value().appendAction(std::make_unique(DataType::int32, tupleShapeZYX, std::vector{1}, cellAttrMatPath.createChildPath(pMtrIdsName))); + resultOutputActions.value().appendAction(std::make_unique(DataType::float32, tupleShapeZYX, std::vector{3}, cellAttrMatPath.createChildPath(pEulersName))); + if(pGenPolar) + { + resultOutputActions.value().appendAction(std::make_unique(DataType::uint8, tupleShapeZYX, std::vector{3}, cellAttrMatPath.createChildPath(pPolarName))); + } + resultOutputActions.value().appendAction(std::make_unique(DataType::uint64, std::vector{1}, std::vector{1}, DataPath({pSeedArrayName}))); + + if(useConfig) + { + preflightUpdatedValues.push_back({"Parameter Source", "Config file: " + filterArgs.value(k_ConfigFilePath_Key).string()}); + } + preflightUpdatedValues.push_back({"Output Grid (X, Y, Z)", fmt::format("{} x {} x {}", nx, ny, nz)}); + preflightUpdatedValues.push_back({"Number of ODF Components", std::to_string(numComponents)}); + + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; +} + +//------------------------------------------------------------------------------ +Result<> MTRSimFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + MTRSimInputValues inputValues; + inputValues.inputOdfGeometryPath = filterArgs.value(k_InputOdfGeometry_Key); + inputValues.odfComponentPaths = filterArgs.value(k_OdfComponentArrays_Key); + inputValues.generatePolarColoring = filterArgs.value(k_GeneratePolarColoring_Key); + inputValues.outputGeometryPath = filterArgs.value(k_OutputGeometry_Key); + inputValues.cellAttrMatName = filterArgs.value(k_CellAttrMatName_Key); + inputValues.mtrIdsArrayName = filterArgs.value(k_MtrIdsArrayName_Key); + inputValues.eulersArrayName = filterArgs.value(k_EulersArrayName_Key); + inputValues.polarColorsArrayName = filterArgs.value(k_PolarColorsArrayName_Key); + + // Resolve ALL simulation parameters (volume fractions, theta, size, spacing, seed) + // from whichever source the user has selected — config file or manual fields. + // The config file is parsed at most ONCE here; the algorithm is config-agnostic. + const bool useConfig = filterArgs.value(k_UseConfigFile_Key); + uint64 seed; + if(useConfig) + { + mtrsim::SimulationParams cfg; + try + { + cfg = mtrsim::parseConfigJson(filterArgs.value(k_ConfigFilePath_Key)); + } catch(const std::exception& e) + { + return MakeErrorResult(-13521, fmt::format("MTRSim config file error: {}", e.what())); + } + inputValues.volumeFractions = {cfg.volumeFractions}; + inputValues.thetaList = cfg.thetaList; + inputValues.physicalSize = {static_cast(cfg.xLen), static_cast(cfg.yLen), static_cast(cfg.zLen)}; + inputValues.physicalSpacing = {static_cast(cfg.dx), static_cast(cfg.dy), static_cast(cfg.dz)}; + seed = cfg.seed; + if(seed == 0) // seed:0 (or absent) means "generate one" + { + seed = static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); + } + } + else + { + inputValues.volumeFractions = filterArgs.value(k_VolumeFractions_Key); + inputValues.thetaList = filterArgs.value(k_ThetaList_Key); + inputValues.physicalSize = filterArgs.value>(k_PhysicalSize_Key); + inputValues.physicalSpacing = filterArgs.value>(k_PhysicalSpacing_Key); + seed = filterArgs.value(k_SeedValue_Key); + if(!filterArgs.value(k_UseSeed_Key)) + { + seed = static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); + } + } + dataStructure.getDataRefAs(DataPath({filterArgs.value(k_SeedArrayName_Key)}))[0] = seed; + inputValues.seed = seed; + + return MTRSim(dataStructure, messageHandler, shouldCancel, &inputValues)(); +} + +//------------------------------------------------------------------------------ +Result MTRSimFilter::FromSIMPLJson(const nlohmann::json& json) +{ + Arguments args = MTRSimFilter().getDefaultArguments(); + + std::vector> results; + + /* This is a NEW filter and has no SIMPL (DREAM3D v6) equivalent. */ + + Result<> conversionResult = MergeResults(std::move(results)); + + return ConvertResultTo(std::move(conversionResult), std::move(args)); +} + +} // namespace nx::core diff --git a/src/MTRSim/Filters/MTRSimFilter.hpp b/src/MTRSim/Filters/MTRSimFilter.hpp new file mode 100644 index 0000000..25e1a47 --- /dev/null +++ b/src/MTRSim/Filters/MTRSimFilter.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include "MTRSim/MTRSim_export.hpp" + +#include "simplnx/Common/StringLiteral.hpp" +#include "simplnx/Filter/FilterTraits.hpp" +#include "simplnx/Filter/IFilter.hpp" + +namespace nx::core +{ +/** + * @class MTRSimFilter + * @brief Generates a synthetic microtexture (MTR) microstructure from an input + * ODF and a set of simulation parameters, producing a new ImageGeom with + * per-voxel MTR Ids, Euler angles, and optional polar coloring. + */ +class MTRSIM_EXPORT MTRSimFilter : public IFilter +{ +public: + MTRSimFilter() = default; + ~MTRSimFilter() noexcept override = default; + + MTRSimFilter(const MTRSimFilter&) = delete; + MTRSimFilter(MTRSimFilter&&) noexcept = delete; + + MTRSimFilter& operator=(const MTRSimFilter&) = delete; + MTRSimFilter& operator=(MTRSimFilter&&) noexcept = delete; + + // Parameter Keys + static constexpr StringLiteral k_UseConfigFile_Key = "use_config_file"; + static constexpr StringLiteral k_ConfigFilePath_Key = "config_file_path"; + static constexpr StringLiteral k_InputOdfGeometry_Key = "input_odf_geometry_path"; + static constexpr StringLiteral k_OdfComponentArrays_Key = "odf_component_arrays"; + static constexpr StringLiteral k_VolumeFractions_Key = "volume_fractions"; + static constexpr StringLiteral k_ThetaList_Key = "theta_list"; + static constexpr StringLiteral k_PhysicalSize_Key = "physical_size"; + static constexpr StringLiteral k_PhysicalSpacing_Key = "physical_spacing"; + static constexpr StringLiteral k_UseSeed_Key = "use_seed"; + static constexpr StringLiteral k_SeedValue_Key = "seed_value"; + static constexpr StringLiteral k_SeedArrayName_Key = "seed_array_name"; + static constexpr StringLiteral k_GeneratePolarColoring_Key = "generate_polar_coloring"; + static constexpr StringLiteral k_OutputGeometry_Key = "output_geometry_path"; + static constexpr StringLiteral k_CellAttrMatName_Key = "cell_attribute_matrix_name"; + static constexpr StringLiteral k_MtrIdsArrayName_Key = "mtr_ids_array_name"; + static constexpr StringLiteral k_EulersArrayName_Key = "eulers_array_name"; + static constexpr StringLiteral k_PolarColorsArrayName_Key = "polar_colors_array_name"; + + /** + * @brief Reads SIMPL json and converts it simplnx Arguments. + * @param json + * @return Result + */ + static Result FromSIMPLJson(const nlohmann::json& json); + + /** + * @brief Returns the name of the filter. + * @return + */ + std::string name() const override; + + /** + * @brief Returns the C++ classname of this filter. + * @return + */ + std::string className() const override; + + /** + * @brief Returns the uuid of the filter. + * @return + */ + Uuid uuid() const override; + + /** + * @brief Returns the human readable name of the filter. + * @return + */ + std::string humanName() const override; + + /** + * @brief Returns the default tags for this filter. + * @return + */ + std::vector defaultTags() const override; + + /** + * @brief Returns the parameters of the filter (i.e. its inputs) + * @return + */ + Parameters parameters() const override; + + /** + * @brief Returns parameters version integer. + * Initial version should always be 1. + * Should be incremented everytime the parameters change. + * @return VersionType + */ + VersionType parametersVersion() const override; + + /** + * @brief Returns a copy of the filter. + * @return + */ + UniquePointer clone() const override; + +protected: + /** + * @brief Takes in a DataStructure and checks that the filter can be run on it + * with the given arguments. Returns any warnings/errors. Also returns the + * changes that would be applied to the DataStructure. Some parts of the + * actions may not be completely filled out if all the required information is + * not available at preflight time. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is + * required for the filter + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the + * filter + * @param executionContext The ExecutionContext that can be used to determine + * the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of + * those occurred during execution of this function + */ + PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; + + /** + * @brief Applies the filter's algorithm to the DataStructure with the given + * arguments. Returns any warnings/errors. On failure, there is no guarantee + * that the DataStructure is in a correct state. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is + * required for the filter + * @param pipelineNode The node in the pipeline that is being executed + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the + * filter + * @param executionContext The ExecutionContext that can be used to determine + * the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of + * those occurred during execution of this function + */ + Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; +}; +} // namespace nx::core + +SIMPLNX_DEF_FILTER_TRAITS(nx::core, MTRSimFilter, "f7f7a330-4bff-4a42-a573-09117a89a0a0"); diff --git a/src/MTRSim/Filters/ReadMTRSimODFFilter.cpp b/src/MTRSim/Filters/ReadMTRSimODFFilter.cpp index 5a0e05c..18dfb99 100644 --- a/src/MTRSim/Filters/ReadMTRSimODFFilter.cpp +++ b/src/MTRSim/Filters/ReadMTRSimODFFilter.cpp @@ -63,18 +63,23 @@ Parameters ReadMTRSimODFFilter::parameters() const params.insert(std::make_unique(k_InputFile_Key, "Input ODF File (HDF5)", "MATLAB-format MTRSim ODF HDF5 file to read.", fs::path(""), FileSystemPathParameter::ExtensionsType{".h5", ".hdf5"}, FileSystemPathParameter::PathType::InputFile)); params.insert(std::make_unique(k_Hdf5PathPrefix_Key, "HDF5 Path Prefix", - "Internal HDF5 group path where the ODF data lives. MATLAB-generated files " - "typically use '/ODF_best' (the default). If your file was saved in MATLAB " + "Internal HDF5 group path where the ODF data lives. MATLAB-generated " + "files " + "typically use '/ODF_best' (the default). If your file was saved in " + "MATLAB " "under a variable named 'my_sample_ODF', set this to '/my_sample_ODF'.", "/ODF_best")); params.insertSeparator(Parameters::Separator{"Output Data Object(s)"}); params.insert(std::make_unique(k_OutputImageGeometry_Key, "Created Image Geometry", - "Path at which the ImageGeom holding the ODF data will be created. One Float64 cell-data array is " + "Path at which the ImageGeom holding the ODF data will be created. One " + "Float64 cell-data array is " "created per ODF component in the file.", DataPath({"ODF"}))); params.insert(std::make_unique(k_CellAttrMatName_Key, "Cell Attribute Matrix Name", - "Name of the cell AttributeMatrix created under the output ImageGeom; per-component ODF arrays are placed inside it.", "Cell Data")); + "Name of the cell AttributeMatrix created under the output ImageGeom; " + "per-component ODF arrays are placed inside it.", + "Cell Data")); return params; } @@ -92,8 +97,8 @@ IFilter::UniquePointer ReadMTRSimODFFilter::clone() const } //------------------------------------------------------------------------------ -IFilter::PreflightResult ReadMTRSimODFFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +IFilter::PreflightResult ReadMTRSimODFFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const { auto pInputFile = filterArgs.value(k_InputFile_Key); auto pHdf5PathPrefix = filterArgs.value(k_Hdf5PathPrefix_Key); @@ -103,8 +108,9 @@ IFilter::PreflightResult ReadMTRSimODFFilter::preflightImpl(const DataStructure& nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; - // Read and validate ODF metadata from the file. Any error from readODFMetadata() - // is surfaced as a preflight failure so the UI shows it before execute() is called. + // Read and validate ODF metadata from the file. Any error from + // readODFMetadata() is surfaced as a preflight failure so the UI shows it + // before execute() is called. mtrsim::ODFFileMetadata metadata{}; try { @@ -122,8 +128,9 @@ IFilter::PreflightResult ReadMTRSimODFFilter::preflightImpl(const DataStructure& static_cast(metadata.spacingDegPhi1PHIPhi2[0])}; // Tuple shape for the cell-data arrays is ZYX-ordered (simplnx convention). - // This ordering also matches the row-major layout of /ODF_best/component_*/ODFval - // so execute() can copy values straight across without reshaping. + // This ordering also matches the row-major layout of + // /ODF_best/component_*/ODFval so execute() can copy values straight across + // without reshaping. const std::vector arrayTupleShapeZYX = {imageGeomDimsXYZ[2], imageGeomDimsXYZ[1], imageGeomDimsXYZ[0]}; const std::vector componentShape = {1}; @@ -142,10 +149,8 @@ IFilter::PreflightResult ReadMTRSimODFFilter::preflightImpl(const DataStructure& preflightUpdatedValues.push_back({"HDF5 Path Prefix", pHdf5PathPrefix}); preflightUpdatedValues.push_back({"Components Found", std::to_string(metadata.numComponents)}); - preflightUpdatedValues.push_back( - {"Bins (phi1 x PHI x phi2)", fmt::format("{} x {} x {}", metadata.dimsPhi1PHIPhi2[0], metadata.dimsPhi1PHIPhi2[1], metadata.dimsPhi1PHIPhi2[2])}); - preflightUpdatedValues.push_back( - {"ImageGeom Dimensions (X, Y, Z)", fmt::format("{} x {} x {}", metadata.dimsPhi1PHIPhi2[2], metadata.dimsPhi1PHIPhi2[1], metadata.dimsPhi1PHIPhi2[0])}); + preflightUpdatedValues.push_back({"Bins (phi1 x PHI x phi2)", fmt::format("{} x {} x {}", metadata.dimsPhi1PHIPhi2[0], metadata.dimsPhi1PHIPhi2[1], metadata.dimsPhi1PHIPhi2[2])}); + preflightUpdatedValues.push_back({"ImageGeom Dimensions (X, Y, Z)", fmt::format("{} x {} x {}", metadata.dimsPhi1PHIPhi2[2], metadata.dimsPhi1PHIPhi2[1], metadata.dimsPhi1PHIPhi2[0])}); preflightUpdatedValues.push_back( {"Spacing (X, Y, Z) [deg]", fmt::format("{:.4f}, {:.4f}, {:.4f}", metadata.spacingDegPhi1PHIPhi2[2], metadata.spacingDegPhi1PHIPhi2[1], metadata.spacingDegPhi1PHIPhi2[0])}); diff --git a/src/MTRSim/Filters/ReadMTRSimODFFilter.hpp b/src/MTRSim/Filters/ReadMTRSimODFFilter.hpp index e2a36b9..e764f2a 100644 --- a/src/MTRSim/Filters/ReadMTRSimODFFilter.hpp +++ b/src/MTRSim/Filters/ReadMTRSimODFFilter.hpp @@ -26,10 +26,10 @@ class MTRSIM_EXPORT ReadMTRSimODFFilter : public IFilter ReadMTRSimODFFilter& operator=(ReadMTRSimODFFilter&&) noexcept = delete; // Parameter Keys - static inline constexpr StringLiteral k_InputFile_Key = "input_file"; - static inline constexpr StringLiteral k_Hdf5PathPrefix_Key = "hdf5_path_prefix"; - static inline constexpr StringLiteral k_OutputImageGeometry_Key = "output_image_geometry"; - static inline constexpr StringLiteral k_CellAttrMatName_Key = "cell_attribute_matrix_name"; + static constexpr StringLiteral k_InputFile_Key = "input_file"; + static constexpr StringLiteral k_Hdf5PathPrefix_Key = "hdf5_path_prefix"; + static constexpr StringLiteral k_OutputImageGeometry_Key = "output_image_geometry_path"; + static constexpr StringLiteral k_CellAttrMatName_Key = "cell_attribute_matrix_name"; /** * @brief Reads SIMPL json and converts it simplnx Arguments. @@ -90,29 +90,40 @@ class MTRSIM_EXPORT ReadMTRSimODFFilter : public IFilter protected: /** - * @brief Takes in a DataStructure and checks that the filter can be run on it with the given arguments. - * Returns any warnings/errors. Also returns the changes that would be applied to the DataStructure. - * Some parts of the actions may not be completely filled out if all the required information is not available at preflight time. + * @brief Takes in a DataStructure and checks that the filter can be run on it + * with the given arguments. Returns any warnings/errors. Also returns the + * changes that would be applied to the DataStructure. Some parts of the + * actions may not be completely filled out if all the required information is + * not available at preflight time. * @param dataStructure The input DataStructure instance - * @param filterArgs These are the input values for each parameter that is required for the filter + * @param filterArgs These are the input values for each parameter that is + * required for the filter * @param messageHandler The MessageHandler object - * @param shouldCancel Atomic boolean value that can be checked to cancel the filter - * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path - * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + * @param shouldCancel Atomic boolean value that can be checked to cancel the + * filter + * @param executionContext The ExecutionContext that can be used to determine + * the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of + * those occurred during execution of this function */ PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const override; /** - * @brief Applies the filter's algorithm to the DataStructure with the given arguments. Returns any warnings/errors. - * On failure, there is no guarantee that the DataStructure is in a correct state. + * @brief Applies the filter's algorithm to the DataStructure with the given + * arguments. Returns any warnings/errors. On failure, there is no guarantee + * that the DataStructure is in a correct state. * @param dataStructure The input DataStructure instance - * @param filterArgs These are the input values for each parameter that is required for the filter + * @param filterArgs These are the input values for each parameter that is + * required for the filter * @param pipelineNode The node in the pipeline that is being executed * @param messageHandler The MessageHandler object - * @param shouldCancel Atomic boolean value that can be checked to cancel the filter - * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path - * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + * @param shouldCancel Atomic boolean value that can be checked to cancel the + * filter + * @param executionContext The ExecutionContext that can be used to determine + * the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of + * those occurred during execution of this function */ Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const override; diff --git a/src/MTRSim/Filters/WriteMTRSimODFFilter.cpp b/src/MTRSim/Filters/WriteMTRSimODFFilter.cpp index 3ecd939..017fb5f 100644 --- a/src/MTRSim/Filters/WriteMTRSimODFFilter.cpp +++ b/src/MTRSim/Filters/WriteMTRSimODFFilter.cpp @@ -57,21 +57,28 @@ Parameters WriteMTRSimODFFilter::parameters() const Parameters params; params.insertSeparator(Parameters::Separator{"Output Parameter(s)"}); - params.insert(std::make_unique(k_OutputFile_Key, "Output ODF File (HDF5)", "Path of the MATLAB-format MTRSim ODF HDF5 file to create. An existing file is overwritten.", + params.insert(std::make_unique(k_OutputFile_Key, "Output ODF File (HDF5)", + "Path of the MATLAB-format MTRSim ODF HDF5 file to create. An existing " + "file is overwritten.", fs::path(""), FileSystemPathParameter::ExtensionsType{".h5", ".hdf5"}, FileSystemPathParameter::PathType::OutputFile)); params.insert(std::make_unique(k_Hdf5PathPrefix_Key, "HDF5 Path Prefix", - "Internal HDF5 group path where the ODF data will be written. Defaults to " - "'/ODF_best' to match the MATLAB on-disk convention. Change this if you want " - "the data saved under a different top-level group name (e.g. '/my_sample_ODF').", + "Internal HDF5 group path where the ODF data will be written. Defaults " + "to " + "'/ODF_best' to match the MATLAB on-disk convention. Change this if you " + "want " + "the data saved under a different top-level group name (e.g. " + "'/my_sample_ODF').", "/ODF_best")); params.insertSeparator(Parameters::Separator{"Input Data Objects"}); params.insert(std::make_unique(k_InputImageGeometry_Key, "Input Image Geometry", - "ImageGeom whose (X,Y,Z) dimensions map to (phi2, PHI, phi1) on disk. The written axis ordering reverses this convention.", + "ImageGeom whose (X,Y,Z) dimensions map to (phi2, PHI, phi1) on disk. " + "The written axis ordering reverses this convention.", DataPath{}, GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image})); params.insert(std::make_unique(k_ODFComponents_Key, "ODF Component Arrays", - "Float64 single-component cell-data arrays on the input ImageGeom. Each array becomes one ODF component in the output file.", + "Float64 single-component cell-data arrays on the input ImageGeom. Each " + "array becomes one ODF component in the output file.", MultiArraySelectionParameter::ValueType{}, MultiArraySelectionParameter::AllowedTypes{IArray::ArrayType::DataArray}, MultiArraySelectionParameter::AllowedDataTypes{DataType::float64}, MultiArraySelectionParameter::AllowedComponentShapes{{1}})); @@ -126,7 +133,8 @@ IFilter::PreflightResult WriteMTRSimODFFilter::preflightImpl(const DataStructure for(const auto& arrayPath : pODFComponents) { - // Verify the array lives under the selected ImageGeom (its path must start with geomPath). + // Verify the array lives under the selected ImageGeom (its path must start + // with geomPath). const auto arrayPathVec = arrayPath.getPathVector(); bool onGeom = (arrayPathVec.size() > geomPathVec.size()); if(onGeom) @@ -142,7 +150,9 @@ IFilter::PreflightResult WriteMTRSimODFFilter::preflightImpl(const DataStructure } if(!onGeom) { - return {MakeErrorResult(-12102, fmt::format("Selected array '{}' does not belong to the input ImageGeom '{}'.", arrayPath.toString(), pInputImageGeomPath.toString()))}; + return {MakeErrorResult(-12102, fmt::format("Selected array '{}' does not belong to the input " + "ImageGeom '{}'.", + arrayPath.toString(), pInputImageGeomPath.toString()))}; } const auto* arr = dataStructure.getDataAs(arrayPath); @@ -153,8 +163,9 @@ IFilter::PreflightResult WriteMTRSimODFFilter::preflightImpl(const DataStructure if(arr->getNumberOfTuples() != expectedTuples) { - return {MakeErrorResult(-12104, - fmt::format("Selected array '{}' has {} tuples but the ImageGeom has {} cells.", arrayPath.toString(), arr->getNumberOfTuples(), expectedTuples))}; + return {MakeErrorResult(-12104, fmt::format("Selected array '{}' has {} tuples but the " + "ImageGeom has {} cells.", + arrayPath.toString(), arr->getNumberOfTuples(), expectedTuples))}; } } @@ -168,9 +179,10 @@ IFilter::PreflightResult WriteMTRSimODFFilter::preflightImpl(const DataStructure preflightUpdatedValues.push_back({"Grid (phi1 x PHI x phi2)", fmt::format("{} x {} x {}", numZ, numY, numX)}); preflightUpdatedValues.push_back({"Spacing (phi1, PHI, phi2) [deg]", fmt::format("{:.4f}, {:.4f}, {:.4f}", spacing[2], spacing[1], spacing[0])}); - // Estimate output size: N components * tuples * 8 bytes + some overhead for bin arrays - const std::size_t estBytes = static_cast(pODFComponents.size()) * static_cast(expectedTuples) * sizeof(double) - + static_cast(numX + numY + numZ + 3) * sizeof(double); + // Estimate output size: N components * tuples * 8 bytes + some overhead for + // bin arrays + const std::size_t estBytes = + static_cast(pODFComponents.size()) * static_cast(expectedTuples) * sizeof(double) + static_cast(numX + numY + numZ + 3) * sizeof(double); preflightUpdatedValues.push_back({"Estimated File Size [bytes]", std::to_string(estBytes)}); return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; diff --git a/src/MTRSim/Filters/WriteMTRSimODFFilter.hpp b/src/MTRSim/Filters/WriteMTRSimODFFilter.hpp index 726d324..5467199 100644 --- a/src/MTRSim/Filters/WriteMTRSimODFFilter.hpp +++ b/src/MTRSim/Filters/WriteMTRSimODFFilter.hpp @@ -29,10 +29,10 @@ class MTRSIM_EXPORT WriteMTRSimODFFilter : public IFilter WriteMTRSimODFFilter& operator=(WriteMTRSimODFFilter&&) noexcept = delete; // Parameter Keys - static inline constexpr StringLiteral k_OutputFile_Key = "output_file"; - static inline constexpr StringLiteral k_Hdf5PathPrefix_Key = "hdf5_path_prefix"; - static inline constexpr StringLiteral k_InputImageGeometry_Key = "input_image_geometry"; - static inline constexpr StringLiteral k_ODFComponents_Key = "odf_components"; + static constexpr StringLiteral k_OutputFile_Key = "output_file"; + static constexpr StringLiteral k_Hdf5PathPrefix_Key = "hdf5_path_prefix"; + static constexpr StringLiteral k_InputImageGeometry_Key = "input_image_geometry_path"; + static constexpr StringLiteral k_ODFComponents_Key = "odf_components"; /** * @brief Reads SIMPL json and converts it simplnx Arguments. @@ -93,29 +93,40 @@ class MTRSIM_EXPORT WriteMTRSimODFFilter : public IFilter protected: /** - * @brief Takes in a DataStructure and checks that the filter can be run on it with the given arguments. - * Returns any warnings/errors. Also returns the changes that would be applied to the DataStructure. - * Some parts of the actions may not be completely filled out if all the required information is not available at preflight time. + * @brief Takes in a DataStructure and checks that the filter can be run on it + * with the given arguments. Returns any warnings/errors. Also returns the + * changes that would be applied to the DataStructure. Some parts of the + * actions may not be completely filled out if all the required information is + * not available at preflight time. * @param dataStructure The input DataStructure instance - * @param filterArgs These are the input values for each parameter that is required for the filter + * @param filterArgs These are the input values for each parameter that is + * required for the filter * @param messageHandler The MessageHandler object - * @param shouldCancel Atomic boolean value that can be checked to cancel the filter - * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path - * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + * @param shouldCancel Atomic boolean value that can be checked to cancel the + * filter + * @param executionContext The ExecutionContext that can be used to determine + * the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of + * those occurred during execution of this function */ PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const override; /** - * @brief Applies the filter's algorithm to the DataStructure with the given arguments. Returns any warnings/errors. - * On failure, there is no guarantee that the DataStructure is in a correct state. + * @brief Applies the filter's algorithm to the DataStructure with the given + * arguments. Returns any warnings/errors. On failure, there is no guarantee + * that the DataStructure is in a correct state. * @param dataStructure The input DataStructure instance - * @param filterArgs These are the input values for each parameter that is required for the filter + * @param filterArgs These are the input values for each parameter that is + * required for the filter * @param pipelineNode The node in the pipeline that is being executed * @param messageHandler The MessageHandler object - * @param shouldCancel Atomic boolean value that can be checked to cancel the filter - * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path - * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + * @param shouldCancel Atomic boolean value that can be checked to cancel the + * filter + * @param executionContext The ExecutionContext that can be used to determine + * the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of + * those occurred during execution of this function */ Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const override; diff --git a/src/MTRSim/libmtrsim_export.h b/src/MTRSim/libmtrsim_export.h index aa82c0f..6c3cd60 100644 --- a/src/MTRSim/libmtrsim_export.h +++ b/src/MTRSim/libmtrsim_export.h @@ -1,6 +1,7 @@ /** -This file is here because we are including all the LibMTRSim sources into the simplnx plugin. -In order to do that, this file with this define needs to be here. +This file is here because we are including all the LibMTRSim sources into the +simplnx plugin. In order to do that, this file with this define needs to be +here. */ #define LIBMTRSIM_EXPORT diff --git a/src/app/main.cpp b/src/app/main.cpp index c87a742..7f8f65d 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1,33 +1,36 @@ // MTRsim simulation driver — CLI entry point. // Orchestrates PGRF simulation, ODF loading, orientation sampling, and output. +#include "ConfigIO.hpp" #include "IPFMapper.hpp" +#include "MTRSimDriver.hpp" #include "ODFSampler.hpp" -#include "PGRFSimulation.hpp" +#include "SimulationObservers.hpp" #include "SimulationParams.hpp" #include #include #include -#include #include #include #include -#include #include #include #include #include -namespace { +namespace +{ // ───────────────────────────────────────────────────────────────────────────── // HDF5 helper utilities -int64_t readH5Int64(hid_t fileId, const std::string &path) { +int64_t readH5Int64(hid_t fileId, const std::string& path) +{ const hid_t dsId = H5Dopen2(fileId, path.c_str(), H5P_DEFAULT); - if (dsId < 0) { + if(dsId < 0) + { throw std::runtime_error("HDF5: cannot open dataset: " + path); } int64_t val = 0; @@ -36,9 +39,11 @@ int64_t readH5Int64(hid_t fileId, const std::string &path) { return val; } -std::vector readH5Vector(hid_t fileId, const std::string &path) { +std::vector readH5Vector(hid_t fileId, const std::string& path) +{ const hid_t dsId = H5Dopen2(fileId, path.c_str(), H5P_DEFAULT); - if (dsId < 0) { + if(dsId < 0) + { throw std::runtime_error("HDF5: cannot open dataset: " + path); } @@ -63,10 +68,11 @@ std::vector readH5Vector(hid_t fileId, const std::string &path) { // /ODF_best/component_N/phi1_bins — (73,) float64 [rad] // /ODF_best/component_N/PHI_bins — (37,) float64 [rad] // /ODF_best/component_N/phi2_bins — (73,) float64 [rad] -std::vector -loadODFComponents(const std::string &hdfPath) { +std::vector loadODFComponents(const std::string& hdfPath) +{ const hid_t fileId = H5Fopen(hdfPath.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); - if (fileId < 0) { + if(fileId < 0) + { throw std::runtime_error("Cannot open HDF5 file: " + hdfPath); } @@ -76,7 +82,8 @@ loadODFComponents(const std::string &hdfPath) { std::vector components; components.reserve(static_cast(numComponents)); - for (int64_t j = 0; j < numComponents; ++j) { + for(int64_t j = 0; j < numComponents; ++j) + { const std::string prefix = "/ODF_best/component_" + std::to_string(j); auto odfVec = readH5Vector(fileId, prefix + "/ODFval"); @@ -85,19 +92,16 @@ loadODFComponents(const std::string &hdfPath) { auto phi2Vec = readH5Vector(fileId, prefix + "/phi2_bins"); mtrsim::ODFComponent comp; - comp.odfVal = Eigen::Map( - odfVec.data(), static_cast(odfVec.size())); - comp.phi1Bins = Eigen::Map( - phi1Vec.data(), static_cast(phi1Vec.size())); - comp.phiBins = Eigen::Map( - phiVec.data(), static_cast(phiVec.size())); - comp.phi2Bins = Eigen::Map( - phi2Vec.data(), static_cast(phi2Vec.size())); + comp.odfVal = Eigen::Map(odfVec.data(), static_cast(odfVec.size())); + comp.phi1Bins = Eigen::Map(phi1Vec.data(), static_cast(phi1Vec.size())); + comp.phiBins = Eigen::Map(phiVec.data(), static_cast(phiVec.size())); + comp.phi2Bins = Eigen::Map(phi2Vec.data(), static_cast(phi2Vec.size())); // Normalise so that odfVal sums to 1 (matches MATLAB pre-processing in // simulate_MTRs.m) const double total = comp.odfVal.sum(); - if (total > 0.0) { + if(total > 0.0) + { comp.odfVal /= total; } @@ -108,51 +112,13 @@ loadODFComponents(const std::string &hdfPath) { return components; } -// Build a uniform reference ODF with the flat 186624-element bin-centre arrays -// that ODFSampler expects. These match exactly what ODFCalculator::compute -// produces: -// phi1Bins[ix] = (i1 + 0.5) * 2π/72 -// phiBins[ix] = (iPHI + 0.5) * π/36 -// phi2Bins[ix] = (i2 + 0.5) * 2π/72 -// where ix = i1*(36*72) + iPHI*72 + i2. -mtrsim::ODFComponent buildUniformODF() { - constexpr int nBins1 = 72; - constexpr int nBinsPHI = 36; - constexpr int nBins2 = 72; - constexpr int nTotal = nBins1 * nBinsPHI * nBins2; // 186624 - - const double k_TwoPiOver = - 2.0 * std::numbers::pi / static_cast(nBins1); - const double k_PiOver = std::numbers::pi / static_cast(nBinsPHI); - - Eigen::VectorXd phi1Bins(nTotal); - Eigen::VectorXd phiBins(nTotal); - Eigen::VectorXd phi2Bins(nTotal); - - for (int ix = 0; ix < nTotal; ++ix) { - const int i1 = ix / (nBinsPHI * nBins2); - const int iPHI = (ix % (nBinsPHI * nBins2)) / nBins2; - const int i2 = ix % nBins2; - phi1Bins[ix] = (i1 + 0.5) * k_TwoPiOver; - phiBins[ix] = (iPHI + 0.5) * k_PiOver; - phi2Bins[ix] = (i2 + 0.5) * k_TwoPiOver; - } - - mtrsim::ODFComponent uni; - uni.odfVal = - Eigen::VectorXd::Constant(nTotal, 1.0 / static_cast(nTotal)); - uni.phi1Bins = std::move(phi1Bins); - uni.phiBins = std::move(phiBins); - uni.phi2Bins = std::move(phi2Bins); - return uni; -} - } // anonymous namespace // ───────────────────────────────────────────────────────────────────────────── // main -int main(int argc, char **argv) { +int main(int argc, char** argv) +{ CLI::App app{"MTRsim — Microtexture Region Simulator"}; std::string configPath; @@ -174,44 +140,20 @@ int main(int argc, char **argv) { params.outputDir = outputDir; params.seed = seed; // may be overridden by JSON (unless CLI --seed was set) - if (!configPath.empty()) { - std::ifstream f(configPath); - if (!f.is_open()) { - spdlog::error("Cannot open config file: {}", configPath); - return 1; - } - - try { - const nlohmann::json j = nlohmann::json::parse(f); - if (j.contains("xLen")) - params.xLen = j["xLen"].get(); - if (j.contains("yLen")) - params.yLen = j["yLen"].get(); - if (j.contains("zLen")) - params.zLen = j["zLen"].get(); - if (j.contains("dx")) - params.dx = j["dx"].get(); - if (j.contains("dy")) - params.dy = j["dy"].get(); - if (j.contains("dz")) - params.dz = j["dz"].get(); - if (j.contains("volumeFractions")) - params.volumeFractions = - j["volumeFractions"].get>(); - if (j.contains("thetaList")) - params.thetaList = - j["thetaList"].get>>(); - if (j.contains("nuggetVariance")) - params.nuggetVariance = j["nuggetVariance"].get>(); - if (j.contains("odfInputPath")) - params.odfInputPath = j["odfInputPath"].get(); - // JSON seed only applies when CLI --seed was not explicitly provided - // (seed == 0) - if (j.contains("seed") && params.seed == 0) { - params.seed = j["seed"].get(); + if(!configPath.empty()) + { + try + { + mtrsim::SimulationParams cfg = mtrsim::parseConfigJson(configPath); + cfg.outputDir = params.outputDir; // keep CLI-provided output dir + if(seed != 0) + { + cfg.seed = seed; // CLI --seed (non-zero) overrides JSON seed } - } catch (const nlohmann::json::exception &e) { - spdlog::error("JSON parse error: {}", e.what()); + params = cfg; + } catch(const std::exception& e) + { + spdlog::error("{}", e.what()); return 1; } } @@ -219,11 +161,14 @@ int main(int argc, char **argv) { // ── Setup RNG // ──────────────────────────────────────────────────────────────── std::mt19937_64 rng; - if (params.seed == 0) { + if(params.seed == 0) + { std::random_device rd; rng.seed(rd()); spdlog::info("Using random seed from std::random_device"); - } else { + } + else + { rng.seed(params.seed); spdlog::info("Using fixed seed: {}", params.seed); } @@ -232,102 +177,90 @@ int main(int argc, char **argv) { // ────────────────────────────────────────────────── const int nx = static_cast(std::round(params.xLen / params.dx)); const int ny = static_cast(std::round(params.yLen / params.dy)); - const int nz = - std::max(static_cast(std::round(params.zLen / params.dz)), 1); + const int nz = std::max(static_cast(std::round(params.zLen / params.dz)), 1); const int N = nx * ny * nz; spdlog::info("Grid: nx={} ny={} nz={} N={}", nx, ny, nz, N); - spdlog::info(" xLen={:.3f} mm yLen={:.3f} mm zLen={:.3f} mm", params.xLen, - params.yLen, params.zLen); + spdlog::info(" xLen={:.3f} mm yLen={:.3f} mm zLen={:.3f} mm", params.xLen, params.yLen, params.zLen); spdlog::info(" dx={} dy={} dz={} [mm]", params.dx, params.dy, params.dz); // ── Build spatial coordinate matrix - // ────────────────────────────────────────── Ordering matches - // simulate_MTRs.m: z outer → x middle → y inner. - // s(k) = [j*dx, i*dy, zix*dz] - // j ∈ [1,nx], i ∈ [1,ny], zix ∈ [1,nz] + // ────────────────────────────────────────── + // Row order matches simulateMTR's SIMPLNX z,y,x output: z slowest, x fastest. + // k = iz*(ny*nx) + iy*nx + ix + // spatialCoords(k) = [(ix+1)*dx, (iy+1)*dy, (iz+1)*dz] + // 1-based coordinate values are preserved (matching the original convention). Eigen::MatrixXd spatialCoords(N, 3); { - int k = 0; - for (int zix = 1; zix <= nz; ++zix) { - for (int j = 1; j <= nx; ++j) { - for (int i = 1; i <= ny; ++i, ++k) { - spatialCoords(k, 0) = j * params.dx; - spatialCoords(k, 1) = i * params.dy; - spatialCoords(k, 2) = zix * params.dz; + for(int iz = 0; iz < nz; ++iz) + { + for(int iy = 0; iy < ny; ++iy) + { + for(int ix = 0; ix < nx; ++ix) + { + const int k = iz * (ny * nx) + iy * nx + ix; + spatialCoords(k, 0) = (ix + 1) * params.dx; + spatialCoords(k, 1) = (iy + 1) * params.dy; + spatialCoords(k, 2) = (iz + 1) * params.dz; } } } } - // ── Run PGRF simulation - // ────────────────────────────────────────────────────── - spdlog::info("Running PGRF simulation..."); - mtrsim::PGRFSimulation pgrf{rng}; - const mtrsim::PGRFResult result = pgrf.run(params); - spdlog::info("PGRF simulation complete."); - // ── Load ODF components from HDF5 // ─────────────────────────────────────────── spdlog::info("Loading ODF from: {}", params.odfInputPath); std::vector odfComponents; - try { + try + { odfComponents = loadODFComponents(params.odfInputPath); - } catch (const std::exception &e) { + } catch(const std::exception& e) + { spdlog::error("ODF load failed: {}", e.what()); return 1; } - if (odfComponents.empty()) { + if(odfComponents.empty()) + { spdlog::error("No ODF components loaded."); return 1; } - // ── Build uniform reference ODF - // ────────────────────────────────────────────── - const mtrsim::ODFComponent uniformOdf = buildUniformODF(); + // ── Run full MTR simulation (PGRF + ODF sampling + remap to z,y,x order) + // ───────────────────────────────────────────────────────────────────────── + // The MATLAB ODF HDF5 layout is a fixed 5-degree Bunge-Euler grid: + // 72 (phi1) x 36 (PHI) x 72 (phi2) = 186624 bins. + constexpr int k_OdfBinsPhi1 = 72; + constexpr int k_OdfBinsPHI = 36; + constexpr int k_OdfBinsPhi2 = 72; - // ── Pre-sample N orientations per component (batched) - // ──────────────────────── - const int numComponents = static_cast(odfComponents.size()); - spdlog::info("Sampling orientations ({} components, N={} each)...", - numComponents, N); + spdlog::info("Running MTR simulation..."); + mtrsim::ConsoleObserver observer; + mtrsim::MTRSimResult sim = mtrsim::simulateMTR(params, odfComponents, rng, k_OdfBinsPhi1, k_OdfBinsPHI, k_OdfBinsPhi2, &observer); + spdlog::info("MTR simulation complete."); - std::vector orientSamples( - static_cast(numComponents)); - mtrsim::ODFSampler sampler{rng}; - - for (int j = 0; j < numComponents; ++j) { - spdlog::info(" Component {} / {}", j + 1, numComponents); - orientSamples[static_cast(j)] = sampler.sampleN( - N, odfComponents[static_cast(j)], uniformOdf); - } - spdlog::info("Orientation sampling complete."); - - // ── Assign orientations per voxel - // ─────────────────────────────────────────── PGRFResult.mtrIndex is 1-based - // → component = mtrIndex[i] - 1 (0-based) - Eigen::VectorXd phi1Vec(N); - Eigen::VectorXd phiVec(N); - Eigen::VectorXd phi2Vec(N); - - for (int i = 0; i < N; ++i) { - const int comp = result.mtrIndex[i] - 1; - phi1Vec[i] = orientSamples[static_cast(comp)](i, 0); - phiVec[i] = orientSamples[static_cast(comp)](i, 1); - phi2Vec[i] = orientSamples[static_cast(comp)](i, 2); + if(sim.cancelled) + { + spdlog::warn("Simulation was cancelled; no output written."); + return 0; } + Eigen::VectorXd phi1Vec = Eigen::Map(sim.phi1.data(), static_cast(sim.phi1.size())); + Eigen::VectorXd phiVec = Eigen::Map(sim.phi.data(), static_cast(sim.phi.size())); + Eigen::VectorXd phi2Vec = Eigen::Map(sim.phi2.data(), static_cast(sim.phi2.size())); + // ── Write IPF map PNG // ──────────────────────────────────────────────────────── const std::string ipfPath = params.outputDir + "/sim_IPF_map.png"; spdlog::info("Writing IPF map: {}", ipfPath); - try { + try + { mtrsim::IPFMapper mapper{mtrsim::CrystalSystem::HCP}; const Eigen::MatrixXd coords2D = spatialCoords.leftCols(2); mapper.writePNG(coords2D, phi1Vec, phiVec, phi2Vec, ipfPath); spdlog::info("IPF map written."); - } catch (const std::exception &e) { + } catch(const std::exception& e) + { spdlog::warn("IPF map write failed: {}", e.what()); } @@ -337,7 +270,8 @@ int main(int argc, char **argv) { spdlog::info("Writing results CSV: {}", csvPath); { std::ofstream csv(csvPath); - if (!csv.is_open()) { + if(!csv.is_open()) + { spdlog::error("Cannot open output file: {}", csvPath); return 1; } @@ -346,10 +280,12 @@ int main(int argc, char **argv) { csv << std::fixed; csv.precision(6); - for (int i = 0; i < N; ++i) { - csv << spatialCoords(i, 0) << ',' << spatialCoords(i, 1) << ',' - << spatialCoords(i, 2) << ',' << phi1Vec[i] << ',' << phiVec[i] << ',' - << phi2Vec[i] << ',' << result.mtrIndex[i] << '\n'; + // Use sim dimensions to tie loop bounds to the actual result. + const int simN = sim.nx * sim.ny * sim.nz; + for(int i = 0; i < simN; ++i) + { + csv << spatialCoords(i, 0) << ',' << spatialCoords(i, 1) << ',' << spatialCoords(i, 2) << ',' << phi1Vec[i] << ',' << phiVec[i] << ',' << phi2Vec[i] << ',' + << sim.mtrIndex[static_cast(i)] << '\n'; } } spdlog::info("Results CSV written."); diff --git a/src/tools/generate_pole_figures.cpp b/src/tools/generate_pole_figures.cpp index 1e53aed..59542b3 100644 --- a/src/tools/generate_pole_figures.cpp +++ b/src/tools/generate_pole_figures.cpp @@ -28,7 +28,8 @@ namespace fs = std::filesystem; // --------------------------------------------------------------------------- -struct VoxelRow { +struct VoxelRow +{ double phi1; double PHI; double phi2; @@ -36,10 +37,12 @@ struct VoxelRow { }; // --------------------------------------------------------------------------- -std::vector readCSV(const std::string &path) { +std::vector readCSV(const std::string& path) +{ std::vector rows; std::ifstream in(path); - if (!in.is_open()) { + if(!in.is_open()) + { std::cerr << "Error: cannot open " << path << "\n"; std::exit(1); } @@ -47,7 +50,8 @@ std::vector readCSV(const std::string &path) { std::string line; std::getline(in, line); // skip header - while (std::getline(in, line)) { + while(std::getline(in, line)) + { // Schema: x,y,z,phi1,PHI,phi2,mtr_index std::istringstream ss(line); std::string tok; @@ -71,13 +75,13 @@ std::vector readCSV(const std::string &path) { } // --------------------------------------------------------------------------- -ebsdlib::FloatArrayType::Pointer -toEulerArray(const std::vector &rows) { +ebsdlib::FloatArrayType::Pointer toEulerArray(const std::vector& rows) +{ std::vector cDims = {3}; - auto eulers = - ebsdlib::FloatArrayType::CreateArray(rows.size(), cDims, "Eulers", true); - for (size_t i = 0; i < rows.size(); i++) { - float *ptr = eulers->getTuplePointer(i); + auto eulers = ebsdlib::FloatArrayType::CreateArray(rows.size(), cDims, "Eulers", true); + for(size_t i = 0; i < rows.size(); i++) + { + float* ptr = eulers->getTuplePointer(i); ptr[0] = static_cast(rows[i].phi1); ptr[1] = static_cast(rows[i].PHI); ptr[2] = static_cast(rows[i].phi2); @@ -86,19 +90,21 @@ toEulerArray(const std::vector &rows) { } // --------------------------------------------------------------------------- -void writePNG(const ebsdlib::CompositePoleFigureResult &result, - const std::string &path) { +void writePNG(const ebsdlib::CompositePoleFigureResult& result, const std::string& path) +{ // Convert RGBA to RGB for stb_image_write int w = result.width; int h = result.height; std::vector rgb(w * h * 3); - const uint8_t *rgba = result.image->getPointer(0); - for (int i = 0; i < w * h; i++) { + const uint8_t* rgba = result.image->getPointer(0); + for(int i = 0; i < w * h; i++) + { rgb[i * 3 + 0] = rgba[i * 4 + 0]; rgb[i * 3 + 1] = rgba[i * 4 + 1]; rgb[i * 3 + 2] = rgba[i * 4 + 2]; } - if (stbi_write_png(path.c_str(), w, h, 3, rgb.data(), w * 3) == 0) { + if(stbi_write_png(path.c_str(), w, h, 3, rgb.data(), w * 3) == 0) + { std::cerr << "Error: failed to write " << path << "\n"; std::exit(1); } @@ -106,9 +112,8 @@ void writePNG(const ebsdlib::CompositePoleFigureResult &result, } // --------------------------------------------------------------------------- -void generatePoleFigure(const std::vector &rows, - const std::string &title, const std::string &outPath, - int imageDim) { +void generatePoleFigure(const std::vector& rows, const std::string& title, const std::string& outPath, int imageDim) +{ auto eulers = toEulerArray(rows); ebsdlib::CompositePoleFigureConfiguration_t config; @@ -126,15 +131,16 @@ void generatePoleFigure(const std::vector &rows, config.title = title; ebsdlib::PoleFigureCompositor compositor; - ebsdlib::CompositePoleFigureResult result = - compositor.generateCompositeImage(config); + ebsdlib::CompositePoleFigureResult result = compositor.generateCompositeImage(config); writePNG(result, outPath); } // --------------------------------------------------------------------------- -int main(int argc, char *argv[]) { - if (argc < 3) { +int main(int argc, char* argv[]) +{ + if(argc < 3) + { std::cerr << "Usage: generate_pole_figures " "[--label ] [--dim ]\n"; return 1; @@ -145,10 +151,14 @@ int main(int argc, char *argv[]) { std::string label = "cpp"; int imageDim = 1024; - for (int i = 3; i < argc; i++) { - if (std::string(argv[i]) == "--label" && i + 1 < argc) { + for(int i = 3; i < argc; i++) + { + if(std::string(argv[i]) == "--label" && i + 1 < argc) + { label = argv[++i]; - } else if (std::string(argv[i]) == "--dim" && i + 1 < argc) { + } + else if(std::string(argv[i]) == "--dim" && i + 1 < argc) + { imageDim = std::stoi(argv[++i]); } } @@ -161,34 +171,33 @@ int main(int argc, char *argv[]) { // Find unique components int maxComp = 0; - for (const auto &r : allRows) { - if (r.mtrIndex > maxComp) { + for(const auto& r : allRows) + { + if(r.mtrIndex > maxComp) + { maxComp = r.mtrIndex; } } // Split by component std::vector> byComponent(maxComp); - for (const auto &r : allRows) { + for(const auto& r : allRows) + { byComponent[r.mtrIndex - 1].push_back(r); } // Generate per-component pole figures - for (int c = 0; c < maxComp; c++) { - std::string title = "MTR Component " + std::to_string(c + 1) + " - ODF " + - std::to_string(c + 1) + - " (n=" + std::to_string(byComponent[c].size()) + ")"; - std::string path = outDir + "/" + label + "_component_" + - std::to_string(c + 1) + "_pf.png"; + for(int c = 0; c < maxComp; c++) + { + std::string title = "MTR Component " + std::to_string(c + 1) + " - ODF " + std::to_string(c + 1) + " (n=" + std::to_string(byComponent[c].size()) + ")"; + std::string path = outDir + "/" + label + "_component_" + std::to_string(c + 1) + "_pf.png"; std::cout << "Generating pole figure: " << title << "\n"; generatePoleFigure(byComponent[c], title, path, imageDim); } // Generate combined pole figure { - std::string title = - "All MTR Components Combined (n=" + std::to_string(allRows.size()) + - ")"; + std::string title = "All MTR Components Combined (n=" + std::to_string(allRows.size()) + ")"; std::string path = outDir + "/" + label + "_all_pf.png"; std::cout << "Generating pole figure: " << title << "\n"; generatePoleFigure(allRows, title, path, imageDim); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 01960a3..435a14d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,6 +7,7 @@ # Define the list of unit test source files set(${PLUGIN_NAME}UnitTest_SRCS ComputeODFTest.cpp + MTRSimTest.cpp ReadMTRSimODFTest.cpp WriteMTRSimODFTest.cpp ) diff --git a/test/ComputeODFTest.cpp b/test/ComputeODFTest.cpp index 0107760..81bde27 100644 --- a/test/ComputeODFTest.cpp +++ b/test/ComputeODFTest.cpp @@ -1,11 +1,12 @@ /** - * Synthetic unit tests for ComputeODFFilter (Milestone AJ, Task 7a — Create New mode). + * Synthetic unit tests for ComputeODFFilter (Milestone AJ, Task 7a — Create New + * mode). * - * Tests build the input EBSD DataStructure inline (no .dream3d/.h5 fixture files) - * and assert on the resulting ODF Float64 array. + * Tests build the input EBSD DataStructure inline (no .dream3d/.h5 fixture + * files) and assert on the resulting ODF Float64 array. * - * MATLAB-reference parity is intentionally NOT tested here; that comparison waits - * on the exemplar-storage decision (see Task 7 follow-up notes) and the + * MATLAB-reference parity is intentionally NOT tested here; that comparison + * waits on the exemplar-storage decision (see Task 7 follow-up notes) and the * PHI-boundary behavior of mtrsim::accumulate. */ @@ -51,7 +52,8 @@ using namespace nx::core::UnitTest; namespace { -// EbsdLib crystal-structure codes (matches ebsdlib::CrystalStructure namespace). +// EbsdLib crystal-structure codes (matches ebsdlib::CrystalStructure +// namespace). constexpr uint32_t k_HexagonalHigh = 0; constexpr uint32_t k_CubicHigh = 1; @@ -68,9 +70,9 @@ const std::string k_ComponentName = "Component 1"; return static_cast(iPhi1) * static_cast(nPHI) * static_cast(nphi2) + static_cast(iPHI) * static_cast(nphi2) + static_cast(iPhi2); } -// Builds an ImageGeom + cell AttributeMatrix + Euler/Phases/(Mask) arrays, and an -// ensemble AttributeMatrix + CrystalStructures array. Returns the DataStructure plus -// the four DataPaths that the filter will need. +// Builds an ImageGeom + cell AttributeMatrix + Euler/Phases/(Mask) arrays, and +// an ensemble AttributeMatrix + CrystalStructures array. Returns the +// DataStructure plus the four DataPaths that the filter will need. struct EbsdInputs { DataStructure dataStructure; @@ -94,9 +96,10 @@ EbsdInputs buildSyntheticEbsd(const std::vector>& eulersD EbsdInputs out; const usize numVoxels = eulersDeg.size(); - // Lay out the input EBSD ImageGeom as a 1-D strip (numVoxels x 1 x 1). The geometry shape - // is irrelevant to ODF output; only the per-voxel content matters. Using {N, 1, 1} keeps - // the cell tuple-shape unambiguous so the AttributeMatrix sees N tuples. + // Lay out the input EBSD ImageGeom as a 1-D strip (numVoxels x 1 x 1). The + // geometry shape is irrelevant to ODF output; only the per-voxel content + // matters. Using {N, 1, 1} keeps the cell tuple-shape unambiguous so the + // AttributeMatrix sees N tuples. ImageGeom* ebsdGeom = ImageGeom::Create(out.dataStructure, "EBSD"); ebsdGeom->setDimensions({numVoxels, 1, 1}); out.ebsdGeomPath = DataPath({"EBSD"}); @@ -129,8 +132,9 @@ EbsdInputs buildSyntheticEbsd(const std::vector>& eulersD out.maskPath = out.cellAttrMatPath.createChildPath("Mask"); } - // Ensemble AttributeMatrix at the top level (not on the geometry) — preflight only checks - // that the array is UInt32/1-component, not where it lives, which mirrors the spec. + // Ensemble AttributeMatrix at the top level (not on the geometry) — preflight + // only checks that the array is UInt32/1-component, not where it lives, which + // mirrors the spec. AttributeMatrix* ensembleAm = AttributeMatrix::Create(out.dataStructure, "EnsembleData", {2}); out.ensembleAttrMatPath = DataPath({"EnsembleData"}); @@ -142,8 +146,8 @@ EbsdInputs buildSyntheticEbsd(const std::vector>& eulersD return out; } -// Pre-fills a base Arguments object with all the required input paths. Tests then -// override individual values as needed. +// Pre-fills a base Arguments object with all the required input paths. Tests +// then override individual values as needed. Arguments makeBaseArgs(const EbsdInputs& inputs, bool applySmoothing, float32 binSizeDeg) { Arguments args; @@ -160,7 +164,8 @@ Arguments makeBaseArgs(const EbsdInputs& inputs, bool applySmoothing, float32 bi return args; } -// Returns the code of the first error in a preflight result, or 0 if there are no errors. +// Returns the code of the first error in a preflight result, or 0 if there are +// no errors. static int32 firstErrorCode(const IFilter::PreflightResult& r) { const auto& errors = r.outputActions.errors(); @@ -189,12 +194,14 @@ std::pair sumAndNonZeroCount(const DataStructure& ds) } // namespace -TEST_CASE("MTRSim::ComputeODFFilter: Single HCP voxel, no smoothing, produces a normalized ODF", "[MTRSim][ComputeODFFilter]") +TEST_CASE("MTRSim::ComputeODFFilter: Single HCP voxel, no smoothing, produces " + "a normalized ODF", + "[MTRSim][ComputeODFFilter]") { - // 1 voxel of HCP at (10, 20, 30) deg. With smoothing off, each of the 12 symmetric - // variants deposits exactly 1.0 into a (possibly shared) bin. Normalization divides - // by the total deposit count = 12 (matches MATLAB calc_ODF.m), so the resulting sum - // should be exactly 1.0. + // 1 voxel of HCP at (10, 20, 30) deg. With smoothing off, each of the 12 + // symmetric variants deposits exactly 1.0 into a (possibly shared) bin. + // Normalization divides by the total deposit count = 12 (matches MATLAB + // calc_ODF.m), so the resulting sum should be exactly 1.0. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); @@ -206,16 +213,19 @@ TEST_CASE("MTRSim::ComputeODFFilter: Single HCP voxel, no smoothing, produces a const auto [sum, nonZero] = sumAndNonZeroCount(inputs.dataStructure); REQUIRE(sum == Approx(1.0).margin(1.0e-9)); - // Some symmetric variants may map to the same bin, so >=1 and <=12 nonzero bins are valid. + // Some symmetric variants may map to the same bin, so >=1 and <=12 nonzero + // bins are valid. REQUIRE(nonZero >= 1); REQUIRE(nonZero <= 12); } -TEST_CASE("MTRSim::ComputeODFFilter: Single Cubic voxel, no smoothing, produces a normalized ODF", "[MTRSim][ComputeODFFilter]") +TEST_CASE("MTRSim::ComputeODFFilter: Single Cubic voxel, no smoothing, " + "produces a normalized ODF", + "[MTRSim][ComputeODFFilter]") { - // 1 voxel of Cubic_High at (10, 20, 30) deg. 24 symmetric variants × 1.0 each, normalized - // by the total deposit count = 24 (matches MATLAB calc_ODF.m), so the resulting sum should - // be exactly 1.0. + // 1 voxel of Cubic_High at (10, 20, 30) deg. 24 symmetric variants × 1.0 + // each, normalized by the total deposit count = 24 (matches MATLAB + // calc_ODF.m), so the resulting sum should be exactly 1.0. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_CubicHigh); Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); @@ -233,11 +243,12 @@ TEST_CASE("MTRSim::ComputeODFFilter: Single Cubic voxel, no smoothing, produces TEST_CASE("MTRSim::ComputeODFFilter: Smoothing distributes per MATLAB weights", "[MTRSim][ComputeODFFilter]") { - // 1 HCP voxel near a bin edge (12.5, 12.5, 12.5) deg. 12.5/5.0 = 2.5 so this value sits - // exactly on the boundary between bins 2 and 3 (NOT at a bin center). With smoothing on, - // each of the 12 symmetric variants distributes 1.0 across 27 bins (the MATLAB tri-linear - // weights sum to 1.0 regardless of where inside the cube the sample falls). Total = - // 12 x 1.0; normalize by total deposit count N=12 -> final sum = 1.0. + // 1 HCP voxel near a bin edge (12.5, 12.5, 12.5) deg. 12.5/5.0 = 2.5 so this + // value sits exactly on the boundary between bins 2 and 3 (NOT at a bin + // center). With smoothing on, each of the 12 symmetric variants + // distributes 1.0 across 27 bins (the MATLAB tri-linear weights sum to 1.0 + // regardless of where inside the cube the sample falls). Total = 12 x 1.0; + // normalize by total deposit count N=12 -> final sum = 1.0. EbsdInputs inputs = buildSyntheticEbsd({{{12.5f, 12.5f, 12.5f}}}, {1}, k_HexagonalHigh); Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); @@ -249,15 +260,17 @@ TEST_CASE("MTRSim::ComputeODFFilter: Smoothing distributes per MATLAB weights", const auto [sum, nonZero] = sumAndNonZeroCount(inputs.dataStructure); REQUIRE(sum == Approx(1.0).margin(1.0e-9)); - // Smoothing across 27 bins per variant means many more nonzero bins are expected. + // Smoothing across 27 bins per variant means many more nonzero bins are + // expected. REQUIRE(nonZero >= 12); } TEST_CASE("MTRSim::ComputeODFFilter: MUD output applies per-row sin(PHI) Jacobian", "[MTRSim][ComputeODFFilter]") { // Run the filter twice on identical input (same Eulers, smoothing on, default - // hex symmetry) — once in Count-Density mode and once in MUD mode — then verify - // that for every non-zero bin the per-bin ratio matches the per-PHI-row Jacobian + // hex symmetry) — once in Count-Density mode and once in MUD mode — then + // verify that for every non-zero bin the per-bin ratio matches the + // per-PHI-row Jacobian // factor = 8 * pi^2 / (step^3 * sin(PHI_center(j))) // This locks the conversion formula in place. Any future change (wrong row // index, missing pi^2, swapped step exponent, etc.) breaks this immediately. @@ -344,10 +357,11 @@ TEST_CASE("MTRSim::ComputeODFFilter: MUD output applies per-row sin(PHI) Jacobia TEST_CASE("MTRSim::ComputeODFFilter: MUD integrates to 8*pi^2 on SO(3)", "[MTRSim][ComputeODFFilter]") { - // Single-number sanity check: by construction the count-density ODF sums to 1.0 - // (it's a probability distribution on Bunge bins). Converting to MUD multiplies - // each bin by 8*pi^2 / (step^3 * sin(PHI_center)). If we then weight each bin by - // its true SO(3) volume element (step^3 * sin(PHI_center)), we should recover + // Single-number sanity check: by construction the count-density ODF sums + // to 1.0 (it's a probability distribution on Bunge bins). Converting to MUD + // multiplies each bin by 8*pi^2 / (step^3 * sin(PHI_center)). If we then + // weight each bin by its true SO(3) volume element (step^3 * + // sin(PHI_center)), we should recover // sum_bins (MUD[bin] * step^3 * sin(PHI_center(bin))) = 8*pi^2 // i.e. the SO(3) volume. This is the closed-form integral identity for MUD. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}, {{50.0f, 70.0f, 110.0f}}}, {1, 1}, k_HexagonalHigh); @@ -394,8 +408,8 @@ TEST_CASE("MTRSim::ComputeODFFilter: MUD integrates to 8*pi^2 on SO(3)", "[MTRSi TEST_CASE("MTRSim::ComputeODFFilter: Mask filters voxels", "[MTRSim][ComputeODFFilter]") { // 4 HCP voxels with distinct Eulers, mask = {true, false, true, false}. - // 2 contributing voxels × 12 variants = 24 deposits; normalized by total deposit - // count N=24 (matches MATLAB calc_ODF.m) → sum = 1.0. + // 2 contributing voxels × 12 variants = 24 deposits; normalized by total + // deposit count N=24 (matches MATLAB calc_ODF.m) → sum = 1.0. std::vector> eulers = {{{10.0f, 20.0f, 30.0f}}, {{40.0f, 50.0f, 60.0f}}, {{70.0f, 80.0f, 90.0f}}, {{15.0f, 25.0f, 35.0f}}}; std::vector phases = {1, 1, 1, 1}; std::vector mask = {true, false, true, false}; @@ -419,8 +433,8 @@ TEST_CASE("MTRSim::ComputeODFFilter: Mask filters voxels", "[MTRSim][ComputeODFF TEST_CASE("MTRSim::ComputeODFFilter: Phase 0 voxels are skipped", "[MTRSim][ComputeODFFilter]") { // 4 HCP voxels with phase pattern {1, 0, 1, 0}, all same Euler. No mask. - // 2 contributing × 12 variants = 24 deposits; normalize by total deposit count N=24 - // (matches MATLAB calc_ODF.m) → sum = 1.0. + // 2 contributing × 12 variants = 24 deposits; normalize by total deposit + // count N=24 (matches MATLAB calc_ODF.m) → sum = 1.0. std::vector> eulers = {{{10.0f, 20.0f, 30.0f}}, {{10.0f, 20.0f, 30.0f}}, {{10.0f, 20.0f, 30.0f}}, {{10.0f, 20.0f, 30.0f}}}; std::vector phases = {1, 0, 1, 0}; EbsdInputs inputs = buildSyntheticEbsd(eulers, phases, k_HexagonalHigh); @@ -440,9 +454,9 @@ TEST_CASE("MTRSim::ComputeODFFilter: Phase 0 voxels are skipped", "[MTRSim][Comp TEST_CASE("MTRSim::ComputeODFFilter: Empty mask DataPath disables the mask", "[MTRSim][ComputeODFFilter]") { - // 4 HCP voxels, all phase=1, distinct Eulers. UseMask = false (mask path is empty). - // 4 contributing × 12 variants = 48 deposits; normalize by total deposit count N=48 - // (matches MATLAB calc_ODF.m) → sum = 1.0. + // 4 HCP voxels, all phase=1, distinct Eulers. UseMask = false (mask path is + // empty). 4 contributing × 12 variants = 48 deposits; normalize by total + // deposit count N=48 (matches MATLAB calc_ODF.m) → sum = 1.0. std::vector> eulers = {{{10.0f, 20.0f, 30.0f}}, {{40.0f, 50.0f, 60.0f}}, {{70.0f, 80.0f, 90.0f}}, {{15.0f, 25.0f, 35.0f}}}; std::vector phases = {1, 1, 1, 1}; EbsdInputs inputs = buildSyntheticEbsd(eulers, phases, k_HexagonalHigh); @@ -471,11 +485,14 @@ TEST_CASE("MTRSim::ComputeODFFilter: Preflight rejects bad bin size", "[MTRSim][ REQUIRE(firstErrorCode(preflightResult) == -12200); } -TEST_CASE("MTRSim::ComputeODFFilter: Preflight rejects euler_angles wrong component count", "[MTRSim][ComputeODFFilter]") +TEST_CASE("MTRSim::ComputeODFFilter: Preflight rejects euler_angles wrong " + "component count", + "[MTRSim][ComputeODFFilter]") { - // Build a normal DataStructure, then swap in a wrong-component-count Float32 array - // for euler_angles. The ArraySelectionParameter component-shape constraint will cause - // the IFilter::preflight wrapper to flag it before preflightImpl is even called. + // Build a normal DataStructure, then swap in a wrong-component-count Float32 + // array for euler_angles. The ArraySelectionParameter component-shape + // constraint will cause the IFilter::preflight wrapper to flag it before + // preflightImpl is even called. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); // Add an alternate single-component Float32 array on the same cell AM. @@ -489,9 +506,11 @@ TEST_CASE("MTRSim::ComputeODFFilter: Preflight rejects euler_angles wrong compon ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); REQUIRE(preflightResult.outputActions.invalid()); - // The ArraySelectionParameter component-shape constraint is enforced by the IFilter::preflight - // wrapper (code -208 = FilterParameter::Constants::k_Validate_TupleShapeValue) BEFORE preflightImpl - // runs, so our own -12201 code never gets a chance to fire. Assert the wrapper code explicitly. + // The ArraySelectionParameter component-shape constraint is enforced by the + // IFilter::preflight wrapper (code -208 = + // FilterParameter::Constants::k_Validate_TupleShapeValue) BEFORE + // preflightImpl runs, so our own -12201 code never gets a chance to fire. + // Assert the wrapper code explicitly. REQUIRE(firstErrorCode(preflightResult) == -208); } @@ -499,8 +518,8 @@ TEST_CASE("MTRSim::ComputeODFFilter: Preflight rejects mismatched attribute matr { EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); - // Create a second cell AttributeMatrix on the same geometry with its own Phases array, - // so euler_angles and phases live on different parents. + // Create a second cell AttributeMatrix on the same geometry with its own + // Phases array, so euler_angles and phases live on different parents. ImageGeom& ebsdGeom = inputs.dataStructure.getDataRefAs(inputs.ebsdGeomPath); AttributeMatrix* otherAm = AttributeMatrix::Create(inputs.dataStructure, "OtherCellData", {1}, ebsdGeom.getId()); Int32Array* otherPhases = Int32Array::CreateWithStore>(inputs.dataStructure, "Phases", {1}, {1}, otherAm->getId()); @@ -535,23 +554,23 @@ TEST_CASE("MTRSim::ComputeODFFilter: Preflight populates updatedValues", "[MTRSi } // ----------------------------------------------------------------------------- -// Append-mode tests (Milestone AJ, Task 7b). Build on T7a: first run Create New to -// populate an ODF geometry, then run Append against that same geometry. +// Append-mode tests (Milestone AJ, Task 7b). Build on T7a: first run Create New +// to populate an ODF geometry, then run Append against that same geometry. // ----------------------------------------------------------------------------- namespace { -// Builds a pre-existing ImageGeom with the given XYZ dimensions, XYZ spacing, and a cell -// AttributeMatrix named "Cell Data". Optionally pre-populates a "Component 1" Float64 cell-data -// array with a constant value so tests can verify it's left alone by an Append call. +// Builds a pre-existing ImageGeom with the given XYZ dimensions, XYZ spacing, +// and a cell AttributeMatrix named "Cell Data". Optionally pre-populates a +// "Component 1" Float64 cell-data array with a constant value so tests can +// verify it's left alone by an Append call. struct ExistingOdfGeom { DataPath geomPath; DataPath cellAttrMatPath; }; -ExistingOdfGeom buildExistingOdfGeom(DataStructure& ds, const std::array& dimsXYZ, const std::array& spacingXYZ, bool seedComponent1 = false, - double seedValue = 0.0) +ExistingOdfGeom buildExistingOdfGeom(DataStructure& ds, const std::array& dimsXYZ, const std::array& spacingXYZ, bool seedComponent1 = false, double seedValue = 0.0) { ExistingOdfGeom out; ImageGeom* geom = ImageGeom::Create(ds, "ODF"); @@ -560,10 +579,12 @@ ExistingOdfGeom buildExistingOdfGeom(DataStructure& ds, const std::arraysetOrigin({0.0f, 0.0f, 0.0f}); out.geomPath = DataPath({"ODF"}); - // Tuple shape is ZYX for cell-data arrays — match the convention used by Create New mode. + // Tuple shape is ZYX for cell-data arrays — match the convention used by + // Create New mode. const std::vector tupleShapeZYX = {dimsXYZ[2], dimsXYZ[1], dimsXYZ[0]}; AttributeMatrix* cellAm = AttributeMatrix::Create(ds, k_CellAttrMatName, tupleShapeZYX, geom->getId()); - // Register the AttributeMatrix with the ImageGeom as its cell data so getCellDataPath() works. + // Register the AttributeMatrix with the ImageGeom as its cell data so + // getCellDataPath() works. geom->setCellData(cellAm->getId()); out.cellAttrMatPath = out.geomPath.createChildPath(k_CellAttrMatName); @@ -599,11 +620,13 @@ std::pair sumAndNonZeroOf(const DataStructure& ds, const DataPath } } // namespace -TEST_CASE("MTRSim::ComputeODFFilter: Append mode adds a new component to an existing ODF", "[MTRSim][ComputeODFFilter]") +TEST_CASE("MTRSim::ComputeODFFilter: Append mode adds a new component to an " + "existing ODF", + "[MTRSim][ComputeODFFilter]") { - // Run Create New first: 1 HCP voxel at (10, 20, 30) deg, no smoothing, bin size 5 deg. - // Expected Component 1 sum = 1.0 (12 deposits normalized by total deposit count N=12, - // matches MATLAB calc_ODF.m). + // Run Create New first: 1 HCP voxel at (10, 20, 30) deg, no smoothing, bin + // size 5 deg. Expected Component 1 sum = 1.0 (12 deposits normalized by total + // deposit count N=12, matches MATLAB calc_ODF.m). EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); { Arguments createArgs = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); @@ -618,7 +641,8 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode adds a new component to an exis const auto [sum1Before, nonZero1Before] = sumAndNonZeroOf(inputs.dataStructure, component1Path); REQUIRE(sum1Before == Approx(1.0).margin(1.0e-9)); - // Now Append a second component with the SAME EBSD input: sum should also be 1.0. + // Now Append a second component with the SAME EBSD input: sum should also + // be 1.0. const std::string k_AppendedComponentName = "Component 2"; Arguments appendArgs = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); appendArgs.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, std::make_any(1ULL)); @@ -637,8 +661,9 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode adds a new component to an exis REQUIRE(sum2 == Approx(1.0).margin(1.0e-9)); REQUIRE(nonZero2 == nonZero1Before); - // Defense-in-depth: the appended array must have the same tuple count as Component 1 - // (i.e. it was created against the existing geometry's dims, not a fresh set). + // Defense-in-depth: the appended array must have the same tuple count as + // Component 1 (i.e. it was created against the existing geometry's dims, not + // a fresh set). const auto& component1Arr = inputs.dataStructure.getDataRefAs(component1Path); const auto& component2Arr = inputs.dataStructure.getDataRefAs(component2Path); REQUIRE(component2Arr.getNumberOfTuples() == component1Arr.getNumberOfTuples()); @@ -651,7 +676,8 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode adds a new component to an exis TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects non-uniform spacing", "[MTRSim][ComputeODFFilter]") { - // Build EBSD inputs, then add a pre-existing ODF ImageGeom with non-uniform spacing. + // Build EBSD inputs, then add a pre-existing ODF ImageGeom with non-uniform + // spacing. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); buildExistingOdfGeom(inputs.dataStructure, {72, 36, 72}, {5.0f, 5.0f, 6.0f}); @@ -670,7 +696,8 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects non-uniform spacing", " { found12207 = true; // Also verify the error message is informative about the spacing issue. - const bool messageMentionsSpacing = (err.message.find("uniform") != std::string::npos || err.message.find("non-uniform") != std::string::npos || err.message.find("spacing") != std::string::npos); + const bool messageMentionsSpacing = + (err.message.find("uniform") != std::string::npos || err.message.find("non-uniform") != std::string::npos || err.message.find("spacing") != std::string::npos); REQUIRE(messageMentionsSpacing); break; } @@ -680,14 +707,17 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects non-uniform spacing", " TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects component-name collision", "[MTRSim][ComputeODFFilter]") { - // Existing ODF geom with a Component 1 seeded on it; Append call using the same component name. + // Existing ODF geom with a Component 1 seeded on it; Append call using the + // same component name. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); - buildExistingOdfGeom(inputs.dataStructure, {72, 36, 72}, {5.0f, 5.0f, 5.0f}, /*seedComponent1=*/true, /*seedValue=*/0.0); + buildExistingOdfGeom(inputs.dataStructure, {72, 36, 72}, {5.0f, 5.0f, 5.0f}, + /*seedComponent1=*/true, /*seedValue=*/0.0); Arguments appendArgs = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); appendArgs.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, std::make_any(1ULL)); appendArgs.insertOrAssign(ComputeODFFilter::k_ExistingOdfGeometry_Key, std::make_any(k_OutputGeomPath)); - // k_ComponentName_Key keeps its makeBaseArgs default = k_ComponentName = "Component 1" → collision. + // k_ComponentName_Key keeps its makeBaseArgs default = k_ComponentName = + // "Component 1" → collision. ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, appendArgs); @@ -706,7 +736,8 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects component-name collisio TEST_CASE("MTRSim::ComputeODFFilter: Append mode preflight reports derived bin size", "[MTRSim][ComputeODFFilter]") { - // Uniform 2.5-deg spacing existing geom; user passes bin_size_deg = 999.0 to prove it's ignored. + // Uniform 2.5-deg spacing existing geom; user passes bin_size_deg = 999.0 to + // prove it's ignored. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); buildExistingOdfGeom(inputs.dataStructure, {144, 72, 144}, {2.5f, 2.5f, 2.5f}); @@ -728,10 +759,11 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode preflight reports derived bin s TEST_CASE("MTRSim::ComputeODFFilter: Unknown crystal code produces warning not error", "[MTRSim][ComputeODFFilter]") { - // Build a synthetic EBSD with 1 voxel, phase=1, but crystal_structures[1] = 999u (unknown code). - // The per-voxel LaueOps lookup fails (out-of-range / nullptr); the algorithm increments - // the local failure count and surfaces a post-loop warning (-12213) rather than an error. - // No voxels contribute, so ODFval sum = 0. + // Build a synthetic EBSD with 1 voxel, phase=1, but crystal_structures[1] = + // 999u (unknown code). The per-voxel LaueOps lookup fails (out-of-range / + // nullptr); the algorithm increments the local failure count and surfaces a + // post-loop warning (-12213) rather than an error. No voxels contribute, so + // ODFval sum = 0. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, /*crystalCode=*/999u); Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); @@ -740,7 +772,8 @@ TEST_CASE("MTRSim::ComputeODFFilter: Unknown crystal code produces warning not e SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); auto executeResult = filter.execute(inputs.dataStructure, args); - // The execute result itself is VALID (the filter ran; just no voxels contributed). + // The execute result itself is VALID (the filter ran; just no voxels + // contributed). SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); // But there must be a warning with code -12213 on the result. @@ -765,12 +798,13 @@ TEST_CASE("MTRSim::ComputeODFFilter: Unknown crystal code produces warning not e TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects non-ImageGeom geometry", "[MTRSim][ComputeODFFilter]") { - // Build a DataStructure where /ODF resolves to a plain DataGroup (not an ImageGeom), - // with a valid EBSD input tree alongside. Run Append mode pointing at /ODF. - // The GeometrySelectionParameter wrapper enforces IGeometry::Type::Image and flags this - // with its own generic geometry-type code (-3) BEFORE preflightImpl runs, so our belt- - // and-suspenders -12206 in preflightImpl is shadowed. Assert the wrapper code explicitly, - // mirroring the pattern used by the "bad euler component count" test above. + // Build a DataStructure where /ODF resolves to a plain DataGroup (not an + // ImageGeom), with a valid EBSD input tree alongside. Run Append mode + // pointing at /ODF. The GeometrySelectionParameter wrapper enforces + // IGeometry::Type::Image and flags this with its own generic geometry-type + // code (-3) BEFORE preflightImpl runs, so our belt- and-suspenders -12206 in + // preflightImpl is shadowed. Assert the wrapper code explicitly, mirroring + // the pattern used by the "bad euler component count" test above. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); // Plant a DataGroup at /ODF instead of an ImageGeom. @@ -778,7 +812,8 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects non-ImageGeom geometry" REQUIRE(dg != nullptr); Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); - args.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, std::make_any(1ULL)); // Append + args.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, + std::make_any(1ULL)); // Append args.insertOrAssign(ComputeODFFilter::k_ExistingOdfGeometry_Key, std::make_any(DataPath({"ODF"}))); ComputeODFFilter filter; @@ -789,8 +824,9 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects non-ImageGeom geometry" TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects ImageGeom with no cell-data", "[MTRSim][ComputeODFFilter]") { - // Build a DataStructure with a bare ImageGeom at /ODF that has NO cell AttributeMatrix - // attached. Run Append mode. Preflight must reject with code -12208. + // Build a DataStructure with a bare ImageGeom at /ODF that has NO cell + // AttributeMatrix attached. Run Append mode. Preflight must reject with code + // -12208. EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); // Create a bare ImageGeom WITHOUT calling setCellData. @@ -822,7 +858,9 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects ImageGeom with no cell- namespace fs = std::filesystem; -TEST_CASE("MTRSim::ComputeODFFilter: Targeted MATLAB calc_ODF.m bin-by-bin validation", "[MTRSim][ComputeODFFilter]") +TEST_CASE("MTRSim::ComputeODFFilter: Targeted MATLAB calc_ODF.m bin-by-bin " + "validation", + "[MTRSim][ComputeODFFilter]") { // 12 hardcoded HCP orientations exercising PHI-near-pole, PHI-near-equator, // axis-wrap, and multi-axis generic cases. The reference HDF5 is produced @@ -856,31 +894,25 @@ TEST_CASE("MTRSim::ComputeODFFilter: Targeted MATLAB calc_ODF.m bin-by-bin valid // // # | (phi1 deg, PHI deg, phi2 deg) | bin (5 deg spacing) | Why // ---+--------------------------------+-----------------------+---------------------------------------------- - // 1| ( 12.5, 12.5, 12.5) | (2, 2, 2) | Pure mid-bin; baseline - // 2| ( 47.5, 27.5, 92.5) | (9, 5, 18) | Generic interior, no special structure - // 3| (137.5, 67.5, 217.5) | (27, 13, 43) | Multi-decimal-bin coverage; phi1/phi2 > 90 deg - // 4| ( 2.5, 2.5, 2.5) | (0, 0, 0) | All near zero; tests near-PHI=0 handling - // 5| ( 47.5, 2.5, 32.5) | (9, 0, 6) | PHI very small with non-trivial phi1/phi2 - // 6| ( 62.5, 2.5, 92.5) | (12, 0, 18) | PHI very small, generic phi1/phi2 - // 7| ( 47.5, 92.5, 32.5) | (9, 18, 6) | PHI just above pi/2 (equatorial regime) - // 8| ( 47.5, 177.5, 32.5) | (9, 35, 6) | PHI near pi (upper-pole regime) - // 9| ( 47.5, 87.5, 32.5) | (9, 17, 6) | PHI just below pi/2 (complement of #7) - // 10| (357.5, 32.5, 32.5) | (71, 6, 6) | phi1 near 360 deg (wrap regime) - // 11| ( 47.5, 32.5, 357.5) | (9, 6, 71) | phi2 near 360 deg (wrap regime) - // 12| (357.5, 87.5, 357.5) | (71, 17, 71) | All three near upper boundaries simultaneously + // 1| ( 12.5, 12.5, 12.5) | (2, 2, 2) | Pure mid-bin; + // baseline 2| ( 47.5, 27.5, 92.5) | (9, 5, 18) | + // Generic interior, no special structure 3| (137.5, 67.5, 217.5) | (27, + // 13, 43) | Multi-decimal-bin coverage; phi1/phi2 > 90 deg 4| + // ( 2.5, 2.5, 2.5) | (0, 0, 0) | All near zero; + // tests near-PHI=0 handling 5| ( 47.5, 2.5, 32.5) | (9, 0, 6) + // | PHI very small with non-trivial phi1/phi2 6| ( 62.5, 2.5, 92.5) + // | (12, 0, 18) | PHI very small, generic phi1/phi2 7| + // ( 47.5, 92.5, 32.5) | (9, 18, 6) | PHI just above + // pi/2 (equatorial regime) 8| ( 47.5, 177.5, 32.5) | (9, 35, 6) + // | PHI near pi (upper-pole regime) 9| ( 47.5, 87.5, 32.5) | (9, + // 17, 6) | PHI just below pi/2 (complement of #7) + // 10| (357.5, 32.5, 32.5) | (71, 6, 6) | phi1 near 360 + // deg (wrap regime) 11| ( 47.5, 32.5, 357.5) | (9, 6, 71) | phi2 + // near 360 deg (wrap regime) 12| (357.5, 87.5, 357.5) | (71, 17, + // 71) | All three near upper boundaries simultaneously const std::vector> eulersDeg = { - { 12.5f, 12.5f, 12.5f}, - { 47.5f, 27.5f, 92.5f}, - {137.5f, 67.5f, 217.5f}, - { 2.5f, 2.5f, 2.5f}, - { 47.5f, 2.5f, 32.5f}, - { 62.5f, 2.5f, 92.5f}, - { 47.5f, 92.5f, 32.5f}, - { 47.5f, 177.5f, 32.5f}, - { 47.5f, 87.5f, 32.5f}, - {357.5f, 32.5f, 32.5f}, - { 47.5f, 32.5f, 357.5f}, - {357.5f, 87.5f, 357.5f}, + {12.5f, 12.5f, 12.5f}, {47.5f, 27.5f, 92.5f}, {137.5f, 67.5f, 217.5f}, {2.5f, 2.5f, 2.5f}, {47.5f, 2.5f, 32.5f}, {62.5f, 2.5f, 92.5f}, + {47.5f, 92.5f, 32.5f}, {47.5f, 177.5f, 32.5f}, {47.5f, 87.5f, 32.5f}, {357.5f, 32.5f, 32.5f}, {47.5f, 32.5f, 357.5f}, {357.5f, 87.5f, 357.5f}, }; // buildSyntheticEbsd already converts deg -> rad internally (see helper). @@ -922,7 +954,9 @@ TEST_CASE("MTRSim::ComputeODFFilter: Targeted MATLAB calc_ODF.m bin-by-bin valid if(!refFixtureVersion.has_value()) { WARN("Reference HDF5 at " << refPath.string() << " has no /ODF_best/fixture_version field " - << "(produced by an older run_validation.m before the fixture-version mechanism). " << regenerateMsg); + << "(produced by an older run_validation.m " + "before the fixture-version mechanism). " + << regenerateMsg); return; } if(*refFixtureVersion != k_TargetedFixtureVersion) @@ -970,16 +1004,22 @@ TEST_CASE("MTRSim::ComputeODFFilter: Targeted MATLAB calc_ODF.m bin-by-bin valid } const double rmsDiff = std::sqrt(sumSqDiff / static_cast(refValues.size())); - INFO(fmt::format("max |diff| = {:.3e}, RMS = {:.3e}, failing bins = {} / {} (tolerance = {:.0e})", maxAbsDiff, rmsDiff, failingBins, refValues.size(), k_Tolerance)); + INFO(fmt::format("max |diff| = {:.3e}, RMS = {:.3e}, failing bins = {} / {} " + "(tolerance = {:.0e})", + maxAbsDiff, rmsDiff, failingBins, refValues.size(), k_Tolerance)); if(failingBins > 0) { - INFO(fmt::format("First failing bin: index {}, MATLAB = {:.6e}, C++ = {:.6e}, diff = {:.3e}", firstFailingIndex, refValues[firstFailingIndex], static_cast(outStore[firstFailingIndex]), + INFO(fmt::format("First failing bin: index {}, MATLAB = {:.6e}, C++ = " + "{:.6e}, diff = {:.3e}", + firstFailingIndex, refValues[firstFailingIndex], static_cast(outStore[firstFailingIndex]), std::abs(static_cast(outStore[firstFailingIndex]) - refValues[firstFailingIndex]))); } REQUIRE(failingBins == 0); } -TEST_CASE("MTRSim::ComputeODFFilter: Realistic 640x640 HCP scan validation against calc_ODF.m", "[MTRSim][ComputeODFFilter][validation]") +TEST_CASE("MTRSim::ComputeODFFilter: Realistic 640x640 HCP scan validation " + "against calc_ODF.m", + "[MTRSim][ComputeODFFilter][validation]") { // Phase 2 validation: load the 640x640 HCP titanium EBSD scan from // data/real_world_microtexture_data.dream3d, run ComputeODFFilter on the @@ -1030,9 +1070,9 @@ TEST_CASE("MTRSim::ComputeODFFilter: Realistic 640x640 HCP scan validation again const DataPath crystalStructuresPath = geomPath.createChildPath("CellEnsembleData").createChildPath("CrystalStructures"); const DataPath maskBoolPath = cellAttrMatPath.createChildPath("MaskBool"); - // Build a Bool mask alongside whatever-type Mask was loaded. ReadDREAM3DFilter - // may give it back as UInt8Array or BoolArray depending on stored metadata - // — handle both so we don't bad_cast on either. + // Build a Bool mask alongside whatever-type Mask was loaded. + // ReadDREAM3DFilter may give it back as UInt8Array or BoolArray depending on + // stored metadata — handle both so we don't bad_cast on either. { const auto* maskBase = dataStructure.getDataAs(maskUInt8Path); REQUIRE(maskBase != nullptr); @@ -1120,10 +1160,14 @@ TEST_CASE("MTRSim::ComputeODFFilter: Realistic 640x640 HCP scan validation again } const double rmsDiff = std::sqrt(sumSqDiff / static_cast(refValues.size())); - INFO(fmt::format("max |diff| = {:.3e}, RMS = {:.3e}, failing bins = {} / {} (tolerance = {:.0e})", maxAbsDiff, rmsDiff, failingBins, refValues.size(), k_Tolerance)); + INFO(fmt::format("max |diff| = {:.3e}, RMS = {:.3e}, failing bins = {} / {} " + "(tolerance = {:.0e})", + maxAbsDiff, rmsDiff, failingBins, refValues.size(), k_Tolerance)); if(failingBins > 0) { - INFO(fmt::format("First failing bin: index {}, MATLAB = {:.6e}, C++ = {:.6e}, diff = {:.3e}", firstFailingIndex, refValues[firstFailingIndex], static_cast(outStore[firstFailingIndex]), + INFO(fmt::format("First failing bin: index {}, MATLAB = {:.6e}, C++ = " + "{:.6e}, diff = {:.3e}", + firstFailingIndex, refValues[firstFailingIndex], static_cast(outStore[firstFailingIndex]), std::abs(static_cast(outStore[firstFailingIndex]) - refValues[firstFailingIndex]))); } REQUIRE(failingBins == 0); diff --git a/test/MTRSimTest.cpp b/test/MTRSimTest.cpp new file mode 100644 index 0000000..dbcc9d1 --- /dev/null +++ b/test/MTRSimTest.cpp @@ -0,0 +1,443 @@ +/** + * Unit tests for MTRSimFilter (Milestone AJ, Tasks 5 + 6 scaffold). + * + * Covers preflight parameter validation only (the algorithm body is a no-op + * stub at this stage): + * 1. Volume Fraction column count != numComponents -> invalid. + * 2. Theta List rows < numComponents - 1 -> invalid. + * 3. Volume Fraction values do not sum to 1.0 -> invalid. + * 4. Happy-path args -> preflight VALID (builds geometry + array actions). + * 5. Theta List rows with wrong column count (2 instead of 3) -> invalid + * (-13505). + */ + +#include + +#include "MTRSim/Filters/MTRSimFilter.hpp" + +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/Parameters/DynamicTableParameter.hpp" +#include "simplnx/Parameters/FileSystemPathParameter.hpp" +#include "simplnx/Parameters/MultiArraySelectionParameter.hpp" +#include "simplnx/UnitTest/UnitTestCommon.hpp" + +#include + +#include +#include +#include +#include +#include + +using namespace nx::core; +using namespace nx::core::UnitTest; + +namespace +{ +constexpr usize k_NPhi1 = 72; +constexpr usize k_NPHI = 36; +constexpr usize k_NPhi2 = 72; + +const DataPath k_OdfGeomPath({"ODF"}); +const std::string k_CellAttrMatName = "Cell Data"; + +// Builds an ODF ImageGeom (72x36x72, 5-degree spacing) with numComponents +// Float64 single-component cell arrays named component_0.., returning their +// DataPaths. +std::vector BuildOdfDataStructure(DataStructure& dataStructure, usize numComponents) +{ + ImageGeom* imageGeom = ImageGeom::Create(dataStructure, k_OdfGeomPath.getTargetName()); + imageGeom->setSpacing({5.0f, 5.0f, 5.0f}); + imageGeom->setOrigin({0.0f, 0.0f, 0.0f}); + imageGeom->setDimensions({k_NPhi2, k_NPHI, k_NPhi1}); // X(phi2), Y(PHI), Z(phi1) + + // ZYX tuple shape (slowest to fastest) + const ShapeType tupleShape = {k_NPhi1, k_NPHI, k_NPhi2}; + + AttributeMatrix* cellAM = AttributeMatrix::Create(dataStructure, k_CellAttrMatName, tupleShape, imageGeom->getId()); + + std::vector compPaths; + const DataPath cellAttrMatPath = k_OdfGeomPath.createChildPath(k_CellAttrMatName); + for(usize c = 0; c < numComponents; ++c) + { + const std::string name = fmt::format("component_{}", c); + CreateTestDataArray(dataStructure, name, tupleShape, {1}, cellAM->getId()); + compPaths.push_back(cellAttrMatPath.createChildPath(name)); + } + return compPaths; +} + +// Builds a valid argument set for the supplied component paths. +Arguments MakeValidArgs(const std::vector& compPaths) +{ + Arguments args; + args.insertOrAssign(MTRSimFilter::k_UseConfigFile_Key, false); + args.insertOrAssign(MTRSimFilter::k_ConfigFilePath_Key, FileSystemPathParameter::ValueType{}); + args.insertOrAssign(MTRSimFilter::k_InputOdfGeometry_Key, k_OdfGeomPath); + args.insertOrAssign(MTRSimFilter::k_OdfComponentArrays_Key, compPaths); + args.insertOrAssign(MTRSimFilter::k_VolumeFractions_Key, DynamicTableParameter::ValueType{{0.30, 0.35, 0.35}}); + args.insertOrAssign(MTRSimFilter::k_ThetaList_Key, DynamicTableParameter::ValueType{{0.1, 0.45, 0.1}, {0.08, 0.37, 0.08}}); + args.insertOrAssign(MTRSimFilter::k_PhysicalSize_Key, std::vector{2.0f, 2.0f, 0.0f}); + args.insertOrAssign(MTRSimFilter::k_PhysicalSpacing_Key, std::vector{0.02f, 0.02f, 0.02f}); + args.insertOrAssign(MTRSimFilter::k_UseSeed_Key, true); + args.insertOrAssign(MTRSimFilter::k_SeedValue_Key, static_cast(42)); + args.insertOrAssign(MTRSimFilter::k_SeedArrayName_Key, std::string("MTRSim SeedValue")); + args.insertOrAssign(MTRSimFilter::k_GeneratePolarColoring_Key, false); + args.insertOrAssign(MTRSimFilter::k_OutputGeometry_Key, DataPath({"MTR Microstructure"})); + args.insertOrAssign(MTRSimFilter::k_CellAttrMatName_Key, std::string("Cell Data")); + args.insertOrAssign(MTRSimFilter::k_MtrIdsArrayName_Key, std::string("MTRIds")); + args.insertOrAssign(MTRSimFilter::k_EulersArrayName_Key, std::string("Eulers")); + args.insertOrAssign(MTRSimFilter::k_PolarColorsArrayName_Key, std::string("Polar Colors")); + return args; +} + +// Writes the given JSON text to a unique temp file under the system temp dir and +// returns its path. Tests are responsible for removing it. +std::filesystem::path WriteTempConfig(const std::string& jsonText, const std::string& tag) +{ + const std::filesystem::path path = std::filesystem::temp_directory_path() / fmt::format("mtrsim_test_{}.json", tag); + std::ofstream out(path); + REQUIRE(out.is_open()); + out << jsonText; + REQUIRE(out.good()); + out.close(); + return path; +} +} // namespace + +TEST_CASE("MTRSim::MTRSimFilter: Valid preflight builds actions", "[MTRSim][MTRSimFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); +} + +TEST_CASE("MTRSim::MTRSimFilter: Rejects mismatched Volume Fraction column count", "[MTRSim][MTRSimFilter][ErrorPath]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + // Only 2 volume-fraction columns for 3 components. + args.insertOrAssign(MTRSimFilter::k_VolumeFractions_Key, DynamicTableParameter::ValueType{{0.5, 0.5}}); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); +} + +TEST_CASE("MTRSim::MTRSimFilter: Execute wires simulation to output arrays", "[MTRSim][MTRSimFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + // Fill every component array with a uniform value so ODF sampling is + // well-defined. + for(const auto& path : compPaths) + { + auto& arr = dataStructure.getDataRefAs(path); + arr.fill(1.0); + } + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + // size/spacing/VF come from MakeValidArgs (100x100 grid) + args.insertOrAssign(MTRSimFilter::k_UseSeed_Key, true); + args.insertOrAssign(MTRSimFilter::k_SeedValue_Key, static_cast(42)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // Output ImageGeom exists. + const DataPath outGeomPath({"MTR Microstructure"}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(outGeomPath)); + + const DataPath cellAm = outGeomPath.createChildPath("Cell Data"); + const usize expectedTuples = 100 * 100; + + // MTRIds: Int32, 1 component, 10000 tuples. + auto& mtrIds = dataStructure.getDataRefAs(cellAm.createChildPath("MTRIds")); + REQUIRE(mtrIds.getNumberOfComponents() == 1); + REQUIRE(mtrIds.getNumberOfTuples() == expectedTuples); + + // Eulers: Float32, 3 components, 10000 tuples. + auto& eulers = dataStructure.getDataRefAs(cellAm.createChildPath("Eulers")); + REQUIRE(eulers.getNumberOfComponents() == 3); + REQUIRE(eulers.getNumberOfTuples() == expectedTuples); + + // MTR ids in {1,2,3}; at least 2 distinct ids appear. Also accumulate + // empirical volume fractions for a loose wiring check. + const auto& mtrStore = mtrIds.getDataStoreRef(); + std::array counts = {0, 0, 0, 0}; + for(usize i = 0; i < mtrStore.getSize(); ++i) + { + const int32 id = mtrStore[i]; + REQUIRE(id >= 1); + REQUIRE(id <= 3); + counts[static_cast(id)]++; + } + usize distinct = 0; + for(usize id = 1; id <= 3; ++id) + { + if(counts[id] > 0) + { + distinct++; + } + } + REQUIRE(distinct >= 2); + + // Euler values finite and within Bunge bounds (interleaved 3/voxel). + constexpr float twoPi = 2.0f * static_cast(M_PI); + constexpr float pi = static_cast(M_PI); + const auto& eulerStore = eulers.getDataStoreRef(); + for(usize t = 0; t < expectedTuples; ++t) + { + const float phi1 = eulerStore[t * 3 + 0]; + const float Phi = eulerStore[t * 3 + 1]; + const float phi2 = eulerStore[t * 3 + 2]; + REQUIRE(std::isfinite(phi1)); + REQUIRE(std::isfinite(Phi)); + REQUIRE(std::isfinite(phi2)); + REQUIRE(phi1 >= 0.0f); + REQUIRE(phi1 <= twoPi); + REQUIRE(Phi >= 0.0f); + REQUIRE(Phi <= pi); + REQUIRE(phi2 >= 0.0f); + REQUIRE(phi2 <= twoPi); + } + + // Seed array records 42. + auto& seedArray = dataStructure.getDataRefAs(DataPath({"MTRSim SeedValue"})); + REQUIRE(seedArray[0] == 42); + + // Loose volume-fraction wiring check (NOT a statistics check; rigorous VF + // validation lives in the LibMTRSim statistical test). A 100x100 correlated + // field has real variance, so use a generous margin of 0.12. + const std::array targets = {0.0, 0.30, 0.35, 0.35}; + for(usize id = 1; id <= 3; ++id) + { + const double empirical = static_cast(counts[id]) / static_cast(expectedTuples); + REQUIRE(empirical == Approx(targets[id]).margin(0.12)); + } +} + +TEST_CASE("MTRSim::MTRSimFilter: Rejects too few Theta List rows", "[MTRSim][MTRSimFilter][ErrorPath]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + // 3 components require >= 2 theta rows; supply only 1. + args.insertOrAssign(MTRSimFilter::k_ThetaList_Key, DynamicTableParameter::ValueType{{0.1, 0.45, 0.1}}); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); +} + +TEST_CASE("MTRSim::MTRSimFilter: Rejects Volume Fraction not summing to 1.0", "[MTRSim][MTRSimFilter][ErrorPath]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + // Columns sum to 0.6, not 1.0. + args.insertOrAssign(MTRSimFilter::k_VolumeFractions_Key, DynamicTableParameter::ValueType{{0.2, 0.2, 0.2}}); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); +} + +TEST_CASE("MTRSim::MTRSimFilter: Execute with polar coloring ON fills Polar " + "Colors array", + "[MTRSim][MTRSimFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + for(const auto& path : compPaths) + { + auto& arr = dataStructure.getDataRefAs(path); + arr.fill(1.0); + } + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + args.insertOrAssign(MTRSimFilter::k_GeneratePolarColoring_Key, true); + args.insertOrAssign(MTRSimFilter::k_UseSeed_Key, true); + args.insertOrAssign(MTRSimFilter::k_SeedValue_Key, static_cast(42)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + const DataPath cellAm = DataPath({"MTR Microstructure"}).createChildPath("Cell Data"); + constexpr usize expectedTuples = 100 * 100; + + auto& rgb = dataStructure.getDataRefAs(cellAm.createChildPath("Polar Colors")); + REQUIRE(rgb.getNumberOfComponents() == 3); + REQUIRE(rgb.getNumberOfTuples() == expectedTuples); + uint64 sum = 0; + for(usize i = 0; i < rgb.getSize(); ++i) + { + sum += rgb[i]; + } + REQUIRE(sum > 0); +} + +TEST_CASE("MTRSim::MTRSimFilter: Execute with polar coloring OFF omits Polar " + "Colors array", + "[MTRSim][MTRSimFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + for(const auto& path : compPaths) + { + auto& arr = dataStructure.getDataRefAs(path); + arr.fill(1.0); + } + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + // k_GeneratePolarColoring_Key is false by default in MakeValidArgs + args.insertOrAssign(MTRSimFilter::k_UseSeed_Key, true); + args.insertOrAssign(MTRSimFilter::k_SeedValue_Key, static_cast(42)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + const DataPath cellAm = DataPath({"MTR Microstructure"}).createChildPath("Cell Data"); + REQUIRE(dataStructure.getDataAs(cellAm.createChildPath("Polar Colors")) == nullptr); +} + +TEST_CASE("MTRSim::MTRSimFilter: Rejects Theta List rows with wrong column count", "[MTRSim][MTRSimFilter][ErrorPath]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + // 2 rows supplied (enough for 3 components: needs >= 2), but each row has + // only 2 columns instead of 3 — must trigger the column-count check (-13505), + // not the row-count check (-13504). + args.insertOrAssign(MTRSimFilter::k_ThetaList_Key, DynamicTableParameter::ValueType{{0.1, 0.45}, {0.08, 0.37}}); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); +} + +TEST_CASE("MTRSim::MTRSimFilter: Config-mode preflight VALID and grid size from config", "[MTRSim][MTRSimFilter][Config]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + + // 3 volume fractions, 2 theta rows, small size; round(1.0/0.02)*round(0.6/0.02) = 50*30 = 1500 tuples. + const std::string json = R"({ + "xLen": 1.0, "yLen": 0.6, "zLen": 0.0, + "dx": 0.02, "dy": 0.02, "dz": 0.02, + "volumeFractions": [0.30, 0.35, 0.35], + "thetaList": [[0.10, 0.45, 0.10], [0.08, 0.37, 0.08]], + "seed": 42 + })"; + const std::filesystem::path configPath = WriteTempConfig(json, "valid"); + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + args.insertOrAssign(MTRSimFilter::k_UseConfigFile_Key, true); + args.insertOrAssign(MTRSimFilter::k_ConfigFilePath_Key, FileSystemPathParameter::ValueType{configPath}); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + const DataPath cellAm = DataPath({"MTR Microstructure"}).createChildPath("Cell Data"); + auto& mtrIds = dataStructure.getDataRefAs(cellAm.createChildPath("MTRIds")); + REQUIRE(mtrIds.getNumberOfTuples() == 1500); + + // Seed array records the config seed (42). + auto& seedArray = dataStructure.getDataRefAs(DataPath({"MTRSim SeedValue"})); + REQUIRE(seedArray[0] == 42); + + std::filesystem::remove(configPath); +} + +TEST_CASE("MTRSim::MTRSimFilter: Config-mode missing file rejects at preflight", "[MTRSim][MTRSimFilter][Config][ErrorPath]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + + const std::filesystem::path missing = std::filesystem::temp_directory_path() / "mtrsim_test_does_not_exist.json"; + std::filesystem::remove(missing); // ensure absent + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + args.insertOrAssign(MTRSimFilter::k_UseConfigFile_Key, true); + args.insertOrAssign(MTRSimFilter::k_ConfigFilePath_Key, FileSystemPathParameter::ValueType{missing}); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); +} + +TEST_CASE("MTRSim::MTRSimFilter: Config-mode VF/component mismatch rejects at preflight", "[MTRSim][MTRSimFilter][Config][ErrorPath]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + // Select 3 ODF components but provide only 2 volume fractions in the config (fires -13502). + const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + + const std::string json = R"({ + "xLen": 1.0, "yLen": 0.6, "zLen": 0.0, + "dx": 0.02, "dy": 0.02, "dz": 0.02, + "volumeFractions": [0.5, 0.5], + "thetaList": [[0.10, 0.45, 0.10], [0.08, 0.37, 0.08]], + "seed": 42 + })"; + const std::filesystem::path configPath = WriteTempConfig(json, "vf_mismatch"); + + MTRSimFilter filter; + Arguments args = MakeValidArgs(compPaths); + args.insertOrAssign(MTRSimFilter::k_UseConfigFile_Key, true); + args.insertOrAssign(MTRSimFilter::k_ConfigFilePath_Key, FileSystemPathParameter::ValueType{configPath}); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + + std::filesystem::remove(configPath); +} diff --git a/test/MTRSimTestUtils.hpp b/test/MTRSimTestUtils.hpp index 739049a..914f4df 100644 --- a/test/MTRSimTestUtils.hpp +++ b/test/MTRSimTestUtils.hpp @@ -7,4 +7,3 @@ #include namespace fs = std::filesystem; - diff --git a/test/ReadMTRSimODFTest.cpp b/test/ReadMTRSimODFTest.cpp index 746edc7..251ee8e 100644 --- a/test/ReadMTRSimODFTest.cpp +++ b/test/ReadMTRSimODFTest.cpp @@ -1,13 +1,14 @@ /** - * Unit tests for ReadMTRSimODFFilter (Milestone AJ, Task 3 + Task 2 path-prefix cross-cut). + * Unit tests for ReadMTRSimODFFilter (Milestone AJ, Task 3 + Task 2 path-prefix + * cross-cut). * * Covers: * 1. Happy path on the canonical /ODF_best exemplar, with assertions on the - * new PreflightUpdatedValues ("HDF5 Path Prefix" label) and explicit prefix - * arg. + * new PreflightUpdatedValues ("HDF5 Path Prefix" label) and explicit + * prefix arg. * 2. Error path: non-existent file -> preflight returns invalid. - * 3. Blank ODF fixture (prefix "/blank_ODF"): execute succeeds, one zero-valued - * Float64 component array is created. + * 3. Blank ODF fixture (prefix "/blank_ODF"): execute succeeds, one + * zero-valued Float64 component array is created. * 4. Uniform ODF fixture (prefix "/uniform_ODF"): execute succeeds, component * sums to ~1.0 and every value is strictly positive (the fixture is a * normalised-over-FZ ODF, NOT a per-cell uniform distribution — see repo @@ -72,10 +73,10 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Valid Filter Execution", "[MTRSim][ReadM auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - // PreflightUpdatedValues: must surface the prefix label so the UI can preview it. + // PreflightUpdatedValues: must surface the prefix label so the UI can preview + // it. REQUIRE(!preflightResult.outputValues.empty()); - const bool foundPrefixLabel = std::any_of(preflightResult.outputValues.begin(), preflightResult.outputValues.end(), - [](const IFilter::PreflightValue& v) { return v.name == "HDF5 Path Prefix"; }); + const bool foundPrefixLabel = std::any_of(preflightResult.outputValues.begin(), preflightResult.outputValues.end(), [](const IFilter::PreflightValue& v) { return v.name == "HDF5 Path Prefix"; }); REQUIRE(foundPrefixLabel); // Execute @@ -96,8 +97,9 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Valid Filter Execution", "[MTRSim][ReadM REQUIRE(std::fabs(spacing[1] - k_ExpectedSpacingDeg) < 1e-6f); REQUIRE(std::fabs(spacing[2] - k_ExpectedSpacingDeg) < 1e-6f); - // Verify each per-component Float64 array exists with the expected tuple count - // and that component_0's values sum to ~1.0 (normalized ODF, within the 1% MATLAB tolerance). + // Verify each per-component Float64 array exists with the expected tuple + // count and that component_0's values sum to ~1.0 (normalized ODF, within the + // 1% MATLAB tolerance). const DataPath cellAttrMatPath = imageGeomPath.createChildPath(cellAttrMatName); for(int32 c = 0; c < k_ExpectedNumComponents; ++c) { diff --git a/test/WriteMTRSimODFTest.cpp b/test/WriteMTRSimODFTest.cpp index a182f06..56497c7 100644 --- a/test/WriteMTRSimODFTest.cpp +++ b/test/WriteMTRSimODFTest.cpp @@ -1,5 +1,6 @@ /** - * Unit tests for WriteMTRSimODFFilter (Milestone AJ, Task 4 + Task 2 path-prefix cross-cut). + * Unit tests for WriteMTRSimODFFilter (Milestone AJ, Task 4 + Task 2 + * path-prefix cross-cut). * * Tests cover: * 1. Byte-exact round-trip through /ODF_best (back-compat). The prefix arg is @@ -90,7 +91,8 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: Round-trip through /ODF_best (back-comp SIMPLNX_RESULT_REQUIRE_VALID(readExecute.result); } - // Determine the component paths that were created so we can pass them to the writer. + // Determine the component paths that were created so we can pass them to the + // writer. const auto origComponents = mtrsim::readODFComponents(inputFile); REQUIRE(origComponents.size() >= 1); @@ -115,8 +117,7 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: Round-trip through /ODF_best (back-comp SIMPLNX_RESULT_REQUIRE_VALID(writePreflight.outputActions); // Assert the prefix-preview updated value surfaced. - const bool foundPrefixLabel = std::any_of(writePreflight.outputValues.begin(), writePreflight.outputValues.end(), - [](const IFilter::PreflightValue& v) { return v.name == "HDF5 Path Prefix"; }); + const bool foundPrefixLabel = std::any_of(writePreflight.outputValues.begin(), writePreflight.outputValues.end(), [](const IFilter::PreflightValue& v) { return v.name == "HDF5 Path Prefix"; }); REQUIRE(foundPrefixLabel); auto writeExecute = writeFilter.execute(dataStructure, writeArgs); @@ -140,7 +141,8 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: axis mapping preserves phi1-PHI-phi2 la { UnitTest::LoadPlugins(); - // Asymmetric dimensions so an axis permutation cannot accidentally produce the correct answer. + // Asymmetric dimensions so an axis permutation cannot accidentally produce + // the correct answer. constexpr std::size_t k_NumPhi1 = 24; // slowest on disk -> Z in ImageGeom constexpr std::size_t k_NumPHI = 18; // -> Y in ImageGeom constexpr std::size_t k_NumPhi2 = 72; // fastest on disk -> X in ImageGeom @@ -165,7 +167,8 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: axis mapping preserves phi1-PHI-phi2 la SIMPLNX_RESULT_REQUIRE_VALID(geomResult); } - // Create the single Float64 component array with ZYX tuple shape {nPhi1, nPHI, nPhi2}. + // Create the single Float64 component array with ZYX tuple shape {nPhi1, + // nPHI, nPhi2}. { std::vector tupleShapeZYX = {k_NumPhi1, k_NumPHI, k_NumPhi2}; CreateArrayAction arrayAction(DataType::float64, tupleShapeZYX, std::vector{1}, componentPath); @@ -173,11 +176,13 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: axis mapping preserves phi1-PHI-phi2 la SIMPLNX_RESULT_REQUIRE_VALID(arrayResult); } - // Initialize to zero, then place a single sentinel at a known (iPhi1, iPHI, iPhi2). + // Initialize to zero, then place a single sentinel at a known (iPhi1, iPHI, + // iPhi2). constexpr std::size_t k_iPhi1 = 3; constexpr std::size_t k_iPHI = 5; constexpr std::size_t k_iPhi2 = 7; - // Row-major flat index with phi1 slowest, phi2 fastest: 3*18*72 + 5*72 + 7 = 4255. + // Row-major flat index with phi1 slowest, phi2 fastest: 3*18*72 + 5*72 + 7 = + // 4255. constexpr std::size_t k_SentinelIndex = k_iPhi1 * (k_NumPHI * k_NumPhi2) + k_iPHI * k_NumPhi2 + k_iPhi2; static_assert(k_SentinelIndex == 4255, "Recompute sentinel flat index before asserting on it."); constexpr double k_SentinelValue = 42.0; @@ -262,8 +267,8 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: zero components rejects at preflight", namespace { -// Helper: read fixture with a given prefix, write it out with a distinct prefix, -// read back and verify byte-exact equality of the component values. +// Helper: read fixture with a given prefix, write it out with a distinct +// prefix, read back and verify byte-exact equality of the component values. void runDistinctPrefixRoundTrip(const std::string& fixtureName, const std::string& readPrefix, const std::string& writePrefix, const std::string& tempStem) { UnitTest::LoadPlugins(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5f89d9e..37ae792 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,12 +1,15 @@ add_executable(mtrsim_tests main.cpp + test_config_io.cpp test_placeholder.cpp test_qsimvn.cpp test_gp_generator.cpp test_odf_file_io.cpp test_odf_sampler.cpp + test_mtrsim_driver.cpp test_ipf_mapper.cpp test_odf_builder.cpp + test_observer.cpp ) target_include_directories(mtrsim_tests diff --git a/tests/test_config_io.cpp b/tests/test_config_io.cpp new file mode 100644 index 0000000..f89a53e --- /dev/null +++ b/tests/test_config_io.cpp @@ -0,0 +1,52 @@ +#include "ConfigIO.hpp" + +#include + +#include +#include +#include + +namespace +{ +std::string writeTemp(const std::string& contents) +{ + const std::string path = std::string(MTRSIM_TEST_DATA_DIR) + "/_tmp_config_io.json"; + std::ofstream o(path); + o << contents; + o.close(); + return path; +} +} // namespace + +TEST_CASE("parseConfigJson reads known fields including odfInputPath", "[config_io]") +{ + const std::string path = writeTemp(R"({ + "xLen": 38.1, "yLen": 12.7, "zLen": 0.0, + "dx": 0.02, "dy": 0.02, "dz": 0.02, + "volumeFractions": [0.30, 0.35, 0.35], + "thetaList": [[0.10,0.45,0.10],[0.08,0.37,0.08]], + "odfInputPath": "ignored.h5", "nuggetVariance": [0.6,0.7,0.7], "seed": 99 + })"); + const mtrsim::SimulationParams p = mtrsim::parseConfigJson(path); + REQUIRE(p.xLen == Approx(38.1)); + REQUIRE(p.dx == Approx(0.02)); + REQUIRE(p.volumeFractions.size() == 3); + REQUIRE(p.volumeFractions[1] == Approx(0.35)); + REQUIRE(p.thetaList.size() == 2); + REQUIRE(p.thetaList[0][1] == Approx(0.45)); + REQUIRE(p.odfInputPath == "ignored.h5"); + REQUIRE(p.seed == 99); + std::remove(path.c_str()); +} + +TEST_CASE("parseConfigJson throws on missing file", "[config_io]") +{ + REQUIRE_THROWS_AS(mtrsim::parseConfigJson("/nonexistent/path/nope.json"), std::runtime_error); +} + +TEST_CASE("parseConfigJson throws on malformed JSON", "[config_io]") +{ + const std::string path = writeTemp("{ this is not json"); + REQUIRE_THROWS_AS(mtrsim::parseConfigJson(path), std::runtime_error); + std::remove(path.c_str()); +} diff --git a/tests/test_gp_generator.cpp b/tests/test_gp_generator.cpp index 1ae22e0..015defa 100644 --- a/tests/test_gp_generator.cpp +++ b/tests/test_gp_generator.cpp @@ -14,14 +14,14 @@ using namespace mtrsim; // GPGenerator tests // ───────────────────────────────────────────────────────────────────────────── -namespace { +namespace +{ // Exponential correlation: rho(lag, theta) = exp(-|lag|/theta) -auto expCorrFn = [](double lag, double theta) -> double { - return std::exp(-std::abs(lag) / theta); -}; +auto expCorrFn = [](double lag, double theta) -> double { return std::exp(-std::abs(lag) / theta); }; } // anonymous namespace -TEST_CASE("GPGenerator: output length equals nx*ny*nz", "[gpgenerator]") { +TEST_CASE("GPGenerator: output length equals nx*ny*nz", "[gpgenerator]") +{ std::mt19937_64 rng(42); GPGenerator gpGen(rng, expCorrFn); @@ -29,8 +29,8 @@ TEST_CASE("GPGenerator: output length equals nx*ny*nz", "[gpgenerator]") { CHECK(gp.size() == 10 * 8 * 3); } -TEST_CASE("GPGenerator: 2D slab (nz=1) output length is nx*ny", - "[gpgenerator]") { +TEST_CASE("GPGenerator: 2D slab (nz=1) output length is nx*ny", "[gpgenerator]") +{ std::mt19937_64 rng(7); GPGenerator gpGen(rng, expCorrFn); @@ -38,7 +38,8 @@ TEST_CASE("GPGenerator: 2D slab (nz=1) output length is nx*ny", CHECK(gp.size() == 20 * 15); } -TEST_CASE("GPGenerator: same seed produces identical output", "[gpgenerator]") { +TEST_CASE("GPGenerator: same seed produces identical output", "[gpgenerator]") +{ auto gp1 = [&]() { std::mt19937_64 rng(999); GPGenerator gpGen(rng, expCorrFn); @@ -54,8 +55,8 @@ TEST_CASE("GPGenerator: same seed produces identical output", "[gpgenerator]") { CHECK(gp1.isApprox(gp2)); } -TEST_CASE("GPGenerator: different seeds produce different output", - "[gpgenerator]") { +TEST_CASE("GPGenerator: different seeds produce different output", "[gpgenerator]") +{ std::mt19937_64 rng1(1), rng2(2); GPGenerator g1(rng1, expCorrFn), g2(rng2, expCorrFn); @@ -65,8 +66,8 @@ TEST_CASE("GPGenerator: different seeds produce different output", CHECK_FALSE(out1.isApprox(out2)); } -TEST_CASE("GPGenerator: sample mean ≈ 0 and variance ≈ 1 on large 1D grid", - "[gpgenerator]") { +TEST_CASE("GPGenerator: sample mean ≈ 0 and variance ≈ 1 on large 1D grid", "[gpgenerator]") +{ // A large 1D grid (ny=nz=1) has enough voxels for reliable moment estimates. // The marginal distribution of each GP value is N(0,1) since Gamma(0,0) = 1. // Use a SHORT correlation length (theta=0.05) relative to spacing (h=0.02) so @@ -79,16 +80,15 @@ TEST_CASE("GPGenerator: sample mean ≈ 0 and variance ≈ 1 on large 1D grid", REQUIRE(gp.size() == nx); const double mean = gp.mean(); - const double var = - (gp.array() - mean).square().sum() / static_cast(nx - 1); + const double var = (gp.array() - mean).square().sum() / static_cast(nx - 1); // n_eff ≈ 5000/6 ≈ 833 → σ_mean ≈ 0.035, σ_var ≈ 0.05. Use generous margins. CHECK(mean == Approx(0.0).margin(0.15)); CHECK(var == Approx(1.0).margin(0.20)); } -TEST_CASE("GPGenerator: exponential autocorrelation approximately correct", - "[gpgenerator]") { +TEST_CASE("GPGenerator: exponential autocorrelation approximately correct", "[gpgenerator]") +{ // For a 1D GP with exp covariance rho(h,theta) = exp(-h/theta), // the lag-1 autocorrelation should be ≈ exp(-spacing/theta). std::mt19937_64 rng(77777); @@ -97,15 +97,13 @@ TEST_CASE("GPGenerator: exponential autocorrelation approximately correct", const int nx = 5000; const double spacing = 0.05; const double theta = 0.5; - auto gp = gpGen.generate(spacing, spacing, spacing, {theta, theta, theta}, nx, - 1, 1); + auto gp = gpGen.generate(spacing, spacing, spacing, {theta, theta, theta}, nx, 1, 1); // Estimate lag-1 autocorrelation const double mean = gp.mean(); const Eigen::VectorXd centred = gp.array() - mean; double cov0 = centred.dot(centred) / static_cast(nx); - double cov1 = centred.head(nx - 1).dot(centred.tail(nx - 1)) / - static_cast(nx - 1); + double cov1 = centred.head(nx - 1).dot(centred.tail(nx - 1)) / static_cast(nx - 1); const double rho_estimated = cov1 / cov0; const double rho_expected = std::exp(-spacing / theta); @@ -116,7 +114,8 @@ TEST_CASE("GPGenerator: exponential autocorrelation approximately correct", // PGRFSimulation tests // ───────────────────────────────────────────────────────────────────────────── -TEST_CASE("PGRFSimulation: output dimensions are correct", "[pgrfsimulation]") { +TEST_CASE("PGRFSimulation: output dimensions are correct", "[pgrfsimulation]") +{ SimulationParams params; // Small grid for speed params.xLen = 0.5; @@ -131,9 +130,8 @@ TEST_CASE("PGRFSimulation: output dimensions are correct", "[pgrfsimulation]") { const int nx = static_cast(std::round(params.xLen / params.dx)); // 5 const int ny = static_cast(std::round(params.yLen / params.dy)); // 5 const int nz = 1; - const int N = nx * ny * nz; // 25 - const int numGaussians = - static_cast(params.volumeFractions.size()) - 1; // 2 + const int N = nx * ny * nz; // 25 + const int numGaussians = static_cast(params.volumeFractions.size()) - 1; // 2 std::mt19937_64 rng(42); PGRFSimulation sim(rng); @@ -144,8 +142,8 @@ TEST_CASE("PGRFSimulation: output dimensions are correct", "[pgrfsimulation]") { CHECK(result.latentFields.cols() == numGaussians); } -TEST_CASE("PGRFSimulation: all voxel assignments are in valid range", - "[pgrfsimulation]") { +TEST_CASE("PGRFSimulation: all voxel assignments are in valid range", "[pgrfsimulation]") +{ SimulationParams params; params.xLen = 0.5; params.yLen = 0.5; @@ -166,8 +164,8 @@ TEST_CASE("PGRFSimulation: all voxel assignments are in valid range", CHECK((result.mtrIndex.array() <= numComponents).all()); } -TEST_CASE("PGRFSimulation: volume fractions approximately match targets [slow]", - "[pgrfsimulation][slow]") { +TEST_CASE("PGRFSimulation: volume fractions approximately match targets [slow]", "[pgrfsimulation][slow]") +{ // Use a grid large enough relative to the correlation length for reliable // estimates. theta=0.10, dx=0.02 → correlation range ≈ 3*theta = 0.3 mm. Grid // 4mm×4mm = 200×200 voxels → n_eff ≈ (4/0.3)^2 ≈ 178 independent patches. @@ -193,12 +191,10 @@ TEST_CASE("PGRFSimulation: volume fractions approximately match targets [slow]", // Allow generous tolerance: 3-sigma for the correlated field estimator const double tol = 0.10; - for (int j = 1; j <= numComponents; ++j) { + for(int j = 1; j <= numComponents; ++j) + { const int count = (result.mtrIndex.array() == j).count(); - const double empirical = - static_cast(count) / static_cast(N); - CHECK(empirical == - Approx(params.volumeFractions[static_cast(j - 1)]) - .margin(tol)); + const double empirical = static_cast(count) / static_cast(N); + CHECK(empirical == Approx(params.volumeFractions[static_cast(j - 1)]).margin(tol)); } } diff --git a/tests/test_ipf_mapper.cpp b/tests/test_ipf_mapper.cpp index b29e160..a0f6d67 100644 --- a/tests/test_ipf_mapper.cpp +++ b/tests/test_ipf_mapper.cpp @@ -15,35 +15,35 @@ using namespace mtrsim; // IPFMapper tests // ───────────────────────────────────────────────────────────────────────────── -TEST_CASE("IPFMapper::eulerToColors: output size equals input size", - "[ipfmapper]") { +TEST_CASE("IPFMapper::eulerToColors: output size equals input size", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::HCP}; const int N = 15; - Eigen::VectorXd phi1 = - Eigen::VectorXd::LinSpaced(N, 0.0, 2.0 * std::numbers::pi); + Eigen::VectorXd phi1 = Eigen::VectorXd::LinSpaced(N, 0.0, 2.0 * std::numbers::pi); Eigen::VectorXd phi = Eigen::VectorXd::LinSpaced(N, 0.0, std::numbers::pi); - Eigen::VectorXd phi2 = - Eigen::VectorXd::LinSpaced(N, 0.0, 2.0 * std::numbers::pi); + Eigen::VectorXd phi2 = Eigen::VectorXd::LinSpaced(N, 0.0, 2.0 * std::numbers::pi); const auto colors = mapper.eulerToColors(phi1, phi, phi2); CHECK(static_cast(colors.size()) == N); } -TEST_CASE("IPFMapper::eulerToColors: all channel values in [0, 255]", - "[ipfmapper]") { +TEST_CASE("IPFMapper::eulerToColors: all channel values in [0, 255]", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::HCP}; const int N = 20; Eigen::VectorXd phi1(N), phi(N), phi2(N); - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { phi1[i] = 0.314159 * i; phi[i] = 0.157080 * i; phi2[i] = 0.628318 * i; } const auto colors = mapper.eulerToColors(phi1, phi, phi2); - for (std::size_t k = 0; k < colors.size(); ++k) { + for(std::size_t k = 0; k < colors.size(); ++k) + { // uint8_t is already [0,255] by type — this just forces the check to show // up in test output CHECK(colors[k].r >= 0); @@ -54,7 +54,8 @@ TEST_CASE("IPFMapper::eulerToColors: all channel values in [0, 255]", TEST_CASE("IPFMapper::eulerToColors: identity orientation [0001] maps to red " "(EbsdLib)", - "[ipfmapper]") { + "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::HCP}; Eigen::VectorXd phi1(1), phi(1), phi2(1); @@ -68,8 +69,8 @@ TEST_CASE("IPFMapper::eulerToColors: identity orientation [0001] maps to red " CHECK(colors[0].b == 0); } -TEST_CASE("IPFMapper MatLab scheme: identity orientation [0001] maps to red", - "[ipfmapper]") { +TEST_CASE("IPFMapper MatLab scheme: identity orientation [0001] maps to red", "[ipfmapper]") +{ // phi1=0, PHI=0, phi2=0 → G=I → specimen normal [0,0,1] in crystal frame = // c-axis. Stereographic projection places [0001] at (X=0, Y=0), so r=0: // cmap1 = 1, cmap2 = 0, cmap3 = 0 → RGB = (255, 0, 0). @@ -80,31 +81,27 @@ TEST_CASE("IPFMapper MatLab scheme: identity orientation [0001] maps to red", phi[0] = 0.0; phi2[0] = 1.0e-15; - const auto colors = mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, - IPFColorScheme::MatLab); + const auto colors = mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, IPFColorScheme::MatLab); CHECK(colors[0].r == 255); CHECK(colors[0].g == 0); CHECK(colors[0].b == 0); } -TEST_CASE("IPFMapper MatLab scheme: output size equals input size", - "[ipfmapper]") { +TEST_CASE("IPFMapper MatLab scheme: output size equals input size", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::HCP}; const int N = 15; - Eigen::VectorXd phi1 = - Eigen::VectorXd::LinSpaced(N, 0.0, 2.0 * std::numbers::pi); + Eigen::VectorXd phi1 = Eigen::VectorXd::LinSpaced(N, 0.0, 2.0 * std::numbers::pi); Eigen::VectorXd phi = Eigen::VectorXd::LinSpaced(N, 0.0, std::numbers::pi); - Eigen::VectorXd phi2 = - Eigen::VectorXd::LinSpaced(N, 0.0, 2.0 * std::numbers::pi); + Eigen::VectorXd phi2 = Eigen::VectorXd::LinSpaced(N, 0.0, 2.0 * std::numbers::pi); - const auto colors = mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, - IPFColorScheme::MatLab); + const auto colors = mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, IPFColorScheme::MatLab); CHECK(static_cast(colors.size()) == N); } -TEST_CASE("IPFMapper MatLab scheme: same orientation gives identical colours", - "[ipfmapper]") { +TEST_CASE("IPFMapper MatLab scheme: same orientation gives identical colours", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::HCP}; const double p1 = 0.7; @@ -116,8 +113,7 @@ TEST_CASE("IPFMapper MatLab scheme: same orientation gives identical colours", phi << ph, ph, ph; phi2 << p2, p2, p2; - const auto colors = mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, - IPFColorScheme::MatLab); + const auto colors = mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, IPFColorScheme::MatLab); CHECK(colors[0].r == colors[1].r); CHECK(colors[0].g == colors[1].g); CHECK(colors[0].b == colors[1].b); @@ -126,7 +122,8 @@ TEST_CASE("IPFMapper MatLab scheme: same orientation gives identical colours", CHECK(colors[0].b == colors[2].b); } -TEST_CASE("IPFMapper MatLab scheme: FCC throws", "[ipfmapper]") { +TEST_CASE("IPFMapper MatLab scheme: FCC throws", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::FCC}; Eigen::VectorXd phi1(1), phi(1), phi2(1); @@ -134,13 +131,11 @@ TEST_CASE("IPFMapper MatLab scheme: FCC throws", "[ipfmapper]") { phi[0] = 0.0; phi2[0] = 0.1; - CHECK_THROWS_AS(mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, - IPFColorScheme::MatLab), - std::invalid_argument); + CHECK_THROWS_AS(mapper.eulerToColors(phi1, phi, phi2, {0.0, 0.0, 1.0}, IPFColorScheme::MatLab), std::invalid_argument); } -TEST_CASE("IPFMapper::writeIPFTriangleLegendMatLab: creates file on disk", - "[ipfmapper]") { +TEST_CASE("IPFMapper::writeIPFTriangleLegendMatLab: creates file on disk", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::HCP}; const std::string outPath = "/tmp/test_ipf_triangle_legend.png"; @@ -152,23 +147,22 @@ TEST_CASE("IPFMapper::writeIPFTriangleLegendMatLab: creates file on disk", std::filesystem::remove(outPath); } -TEST_CASE("IPFMapper::writeIPFTriangleLegendMatLab: FCC throws", - "[ipfmapper]") { +TEST_CASE("IPFMapper::writeIPFTriangleLegendMatLab: FCC throws", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::FCC}; - CHECK_THROWS_AS( - mapper.writeIPFTriangleLegendMatLab(256, "/tmp/should_not_exist.png"), - std::invalid_argument); + CHECK_THROWS_AS(mapper.writeIPFTriangleLegendMatLab(256, "/tmp/should_not_exist.png"), std::invalid_argument); CHECK_FALSE(std::filesystem::exists("/tmp/should_not_exist.png")); } -TEST_CASE("IPFMapper: compare EbsdLib vs MatLab colours", - "[ipfmapper][.print]") { +TEST_CASE("IPFMapper: compare EbsdLib vs MatLab colours", "[ipfmapper][.print]") +{ IPFMapper mapper{CrystalSystem::HCP}; // Representative Euler angles (radians) spanning different orientations - struct EulerSet { + struct EulerSet + { double phi1, phi, phi2; - const char *label; + const char* label; }; // clang-format off @@ -184,33 +178,29 @@ TEST_CASE("IPFMapper: compare EbsdLib vs MatLab colours", }}; // clang-format on - std::printf("\n%-30s | %-17s | %-17s\n", "Orientation", "EbsdLib (R,G,B)", - "MatLab (R,G,B)"); + std::printf("\n%-30s | %-17s | %-17s\n", "Orientation", "EbsdLib (R,G,B)", "MatLab (R,G,B)"); std::printf("-------------------------------+-------------------+------------" "------\n"); - for (const auto &a : angles) { + for(const auto& a : angles) + { Eigen::VectorXd p1(1), p(1), p2(1); p1[0] = a.phi1; p[0] = a.phi; p2[0] = a.phi2; - const auto ebsd = mapper.eulerToColors(p1, p, p2, {0.0, 0.0, 1.0}, - IPFColorScheme::EbsdLib); - const auto matlab = mapper.eulerToColors(p1, p, p2, {0.0, 0.0, 1.0}, - IPFColorScheme::MatLab); + const auto ebsd = mapper.eulerToColors(p1, p, p2, {0.0, 0.0, 1.0}, IPFColorScheme::EbsdLib); + const auto matlab = mapper.eulerToColors(p1, p, p2, {0.0, 0.0, 1.0}, IPFColorScheme::MatLab); - std::printf("%-30s | (%3d, %3d, %3d) | (%3d, %3d, %3d)\n", a.label, - ebsd[0].r, ebsd[0].g, ebsd[0].b, matlab[0].r, matlab[0].g, - matlab[0].b); + std::printf("%-30s | (%3d, %3d, %3d) | (%3d, %3d, %3d)\n", a.label, ebsd[0].r, ebsd[0].g, ebsd[0].b, matlab[0].r, matlab[0].g, matlab[0].b); } std::printf("\n"); CHECK(true); // keep Catch2 happy } -TEST_CASE("IPFMapper: [0001] to [2-1-10] sweep (PHI 0-90 by 1 deg)", - "[ipfmapper][.print]") { +TEST_CASE("IPFMapper: [0001] to [2-1-10] sweep (PHI 0-90 by 1 deg)", "[ipfmapper][.print]") +{ IPFMapper mapper{CrystalSystem::HCP}; // Walking from [0001] to [2-1-10]: @@ -222,29 +212,25 @@ TEST_CASE("IPFMapper: [0001] to [2-1-10] sweep (PHI 0-90 by 1 deg)", std::printf("\n PHI (deg) | EbsdLib (R,G,B) | MatLab (R,G,B)\n"); std::printf("-----------+-------------------+------------------\n"); - for (int deg = 0; deg <= 90; ++deg) { + for(int deg = 0; deg <= 90; ++deg) + { Eigen::VectorXd p1(1), p(1), p2(1); p1[0] = 0.0; p[0] = deg * k_Deg2Rad; - p2[0] = (deg == 0) ? 1.0e-15 - : 0.0; // nudge at exactly 0 to match existing convention + p2[0] = (deg == 0) ? 1.0e-15 : 0.0; // nudge at exactly 0 to match existing convention - const auto ebsd = mapper.eulerToColors(p1, p, p2, {0.0, 0.0, 1.0}, - IPFColorScheme::EbsdLib); - const auto matlab = mapper.eulerToColors(p1, p, p2, {0.0, 0.0, 1.0}, - IPFColorScheme::MatLab); + const auto ebsd = mapper.eulerToColors(p1, p, p2, {0.0, 0.0, 1.0}, IPFColorScheme::EbsdLib); + const auto matlab = mapper.eulerToColors(p1, p, p2, {0.0, 0.0, 1.0}, IPFColorScheme::MatLab); - std::printf(" %3d | (%3d, %3d, %3d) | (%3d, %3d, %3d)\n", deg, - ebsd[0].r, ebsd[0].g, ebsd[0].b, matlab[0].r, matlab[0].g, - matlab[0].b); + std::printf(" %3d | (%3d, %3d, %3d) | (%3d, %3d, %3d)\n", deg, ebsd[0].r, ebsd[0].g, ebsd[0].b, matlab[0].r, matlab[0].g, matlab[0].b); } std::printf("\n"); CHECK(true); } -TEST_CASE("IPFMapper::eulerToColors: same orientation gives identical colours", - "[ipfmapper]") { +TEST_CASE("IPFMapper::eulerToColors: same orientation gives identical colours", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::HCP}; const double p1 = 0.7; @@ -265,7 +251,8 @@ TEST_CASE("IPFMapper::eulerToColors: same orientation gives identical colours", CHECK(colors[0].b == colors[2].b); } -TEST_CASE("IPFMapper::eulerToColors: size mismatch throws", "[ipfmapper]") { +TEST_CASE("IPFMapper::eulerToColors: size mismatch throws", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::HCP}; Eigen::VectorXd phi1(3), phi(2), phi2(3); @@ -276,7 +263,8 @@ TEST_CASE("IPFMapper::eulerToColors: size mismatch throws", "[ipfmapper]") { CHECK_THROWS_AS(mapper.eulerToColors(phi1, phi, phi2), std::invalid_argument); } -TEST_CASE("IPFMapper::writePNG: creates file on disk", "[ipfmapper]") { +TEST_CASE("IPFMapper::writePNG: creates file on disk", "[ipfmapper]") +{ IPFMapper mapper{CrystalSystem::HCP}; // 3×3 regular spatial grid @@ -286,8 +274,10 @@ TEST_CASE("IPFMapper::writePNG: creates file on disk", "[ipfmapper]") { Eigen::MatrixXd coords(N, 2); Eigen::VectorXd phi1(N), phi(N), phi2(N); int idx = 0; - for (int iy = 0; iy < ny; ++iy) { - for (int ix = 0; ix < nx; ++ix) { + for(int iy = 0; iy < ny; ++iy) + { + for(int ix = 0; ix < nx; ++ix) + { coords(idx, 0) = static_cast(ix) * 0.1; coords(idx, 1) = static_cast(iy) * 0.1; phi1[idx] = 0.2 * idx; @@ -307,9 +297,11 @@ TEST_CASE("IPFMapper::writePNG: creates file on disk", "[ipfmapper]") { // PoleFigure tests // ───────────────────────────────────────────────────────────────────────────── -namespace { +namespace +{ // Build a small ODFComponent with a few non-zero bins. -ODFComponent makeTinyODF() { +ODFComponent makeTinyODF() +{ ODFCalculator calc; Eigen::VectorXd phi1(5), phi(5), phi2(5); phi1 << 0.3, 1.0, 2.0, 3.5, 5.0; @@ -319,7 +311,8 @@ ODFComponent makeTinyODF() { } } // anonymous namespace -TEST_CASE("PoleFigure::fromODF: returns non-empty data", "[polefigure]") { +TEST_CASE("PoleFigure::fromODF: returns non-empty data", "[polefigure]") +{ PoleFigure pf; const ODFComponent odf = makeTinyODF(); const PoleFigureData pfd = pf.fromODF(odf); @@ -329,7 +322,8 @@ TEST_CASE("PoleFigure::fromODF: returns non-empty data", "[polefigure]") { CHECK(pfd.intensity.size() == pfd.x.size()); } -TEST_CASE("PoleFigure::fromODF: all intensities non-negative", "[polefigure]") { +TEST_CASE("PoleFigure::fromODF: all intensities non-negative", "[polefigure]") +{ PoleFigure pf; const ODFComponent odf = makeTinyODF(); const PoleFigureData pfd = pf.fromODF(odf); @@ -337,8 +331,8 @@ TEST_CASE("PoleFigure::fromODF: all intensities non-negative", "[polefigure]") { CHECK((pfd.intensity.array() >= 0.0).all()); } -TEST_CASE("PoleFigure::fromODF: intensities normalised to sum ~= 1", - "[polefigure]") { +TEST_CASE("PoleFigure::fromODF: intensities normalised to sum ~= 1", "[polefigure]") +{ PoleFigure pf; const ODFComponent odf = makeTinyODF(); const PoleFigureData pfd = pf.fromODF(odf); @@ -346,14 +340,15 @@ TEST_CASE("PoleFigure::fromODF: intensities normalised to sum ~= 1", CHECK(pfd.intensity.sum() == Approx(1.0).margin(1.0e-9)); } -TEST_CASE("PoleFigure::fromODF: stereographic coords are finite", - "[polefigure]") { +TEST_CASE("PoleFigure::fromODF: stereographic coords are finite", "[polefigure]") +{ PoleFigure pf; const ODFComponent odf = makeTinyODF(); const PoleFigureData pfd = pf.fromODF(odf); // All projected X, Y values should be finite (no NaN/Inf) - for (int i = 0; i < pfd.x.size(); ++i) { + for(int i = 0; i < pfd.x.size(); ++i) + { CHECK(std::isfinite(pfd.x[i])); CHECK(std::isfinite(pfd.y[i])); } diff --git a/tests/test_mtrsim_driver.cpp b/tests/test_mtrsim_driver.cpp new file mode 100644 index 0000000..2a0ffe2 --- /dev/null +++ b/tests/test_mtrsim_driver.cpp @@ -0,0 +1,187 @@ +#include "ISimulationObserver.hpp" +#include "MTRSimDriver.hpp" + +#include + +#include +#include + +namespace +{ +class CancelAfterObserver : public mtrsim::ISimulationObserver +{ +public: + explicit CancelAfterObserver(int k) + : m_K(k) + { + } + void updateProgress(int64_t, int64_t, const std::string&) override + { + ++m_Count; + } + bool shouldCancel() const override + { + return m_Count >= m_K; + } + +private: + int m_K; + mutable int m_Count = 0; +}; +} // namespace + +TEST_CASE("buildUniformODF produces correct bin centres", "[mtrsim_driver]") +{ + const mtrsim::ODFComponent uni = mtrsim::buildUniformODF(72, 36, 72); + + REQUIRE(uni.odfVal.size() == 72 * 36 * 72); + REQUIRE(uni.phi1Bins.size() == 72 * 36 * 72); + + // Uniform mass: every bin equal, sums to 1. + REQUIRE(uni.odfVal.sum() == Approx(1.0)); + REQUIRE(uni.odfVal[0] == Approx(1.0 / (72.0 * 36.0 * 72.0))); + + // First bin centre: i1=iPHI=i2=0 -> all 0.5 * step. + REQUIRE(uni.phi1Bins[0] == Approx(0.5 * 2.0 * std::numbers::pi / 72.0)); + REQUIRE(uni.phiBins[0] == Approx(0.5 * std::numbers::pi / 36.0)); + REQUIRE(uni.phi2Bins[0] == Approx(0.5 * 2.0 * std::numbers::pi / 72.0)); +} + +TEST_CASE("gridToODFComponent derives bin centres in radians and normalizes", "[mtrsim_driver]") +{ + const int n1 = 72, nPHI = 36, n2 = 72; + std::vector values(n1 * nPHI * n2, 2.0); // unnormalized constant + + const mtrsim::ODFComponent c = mtrsim::gridToODFComponent(values, n1, nPHI, n2, 5.0, 5.0, 5.0); + + REQUIRE(c.odfVal.size() == n1 * nPHI * n2); + REQUIRE(c.odfVal.sum() == Approx(1.0)); // normalized + // 5 deg step -> first bin centre 2.5 deg in radians. + const double deg2rad = std::numbers::pi / 180.0; + REQUIRE(c.phi1Bins[0] == Approx(2.5 * deg2rad)); + REQUIRE(c.phiBins[0] == Approx(2.5 * deg2rad)); + REQUIRE(c.phi2Bins[0] == Approx(2.5 * deg2rad)); +} + +TEST_CASE("remapSimToZYX moves y-fastest data to x-fastest layout", "[mtrsim_driver]") +{ + // 2x3x2 grid (nx=2, ny=3, nz=2). Fill sim-order vector with its own index. + const int nx = 2, ny = 3, nz = 2; + std::vector in(nx * ny * nz); + for(int i = 0; i < nx * ny * nz; ++i) + { + in[i] = i; + } + + const std::vector out = mtrsim::remapSimToZYX(in, nx, ny, nz); + + // Spot-check: SIMPLNX (ix=1, iy=0, iz=0) -> kNx = 1. + // source sim index = iz*(nx*ny) + ix*ny + iy = 0 + 1*3 + 0 = 3. + REQUIRE(out[1] == 3); + // SIMPLNX (ix=0, iy=1, iz=0) -> kNx = 2; sim = 0 + 0 + 1 = 1. + REQUIRE(out[2] == 1); + // Non-zero iz slice: SIMPLNX (ix=1, iy=2, iz=1). + // kNx = 1*(3*2) + 2*2 + 1 = 6 + 4 + 1 = 11. + // sim = 1*(2*3) + 1*3 + 2 = 6 + 3 + 2 = 11. + REQUIRE(out[11] == 11); + // Non-zero iz slice: SIMPLNX (ix=0, iy=0, iz=1). + // kNx = 1*(3*2) + 0 + 0 = 6; sim = 1*(2*3) + 0 + 0 = 6. + REQUIRE(out[6] == 6); + // Same total size. + REQUIRE(out.size() == in.size()); +} + +TEST_CASE("simulateMTR reproduces target volume fractions (statistical)", "[mtrsim_driver][statistical]") +{ + mtrsim::SimulationParams params; + params.xLen = 6.0; + params.yLen = 6.0; + params.zLen = 0.0; + params.dx = 0.02; + params.dy = 0.02; + params.dz = 0.02; + params.volumeFractions = {0.30, 0.35, 0.35}; + params.thetaList = {{0.10, 0.45, 0.10}, {0.08, 0.37, 0.08}}; + params.seed = 42; + + std::vector comps = {mtrsim::buildUniformODF(72, 36, 72), mtrsim::buildUniformODF(72, 36, 72), mtrsim::buildUniformODF(72, 36, 72)}; + + std::mt19937_64 rng(params.seed); + const mtrsim::MTRSimResult r = mtrsim::simulateMTR(params, comps, rng, 72, 36, 72); + + const int N = r.nx * r.ny * r.nz; + REQUIRE(static_cast(r.mtrIndex.size()) == N); + + for(int v : r.mtrIndex) + { + REQUIRE(v >= 1); + REQUIRE(v <= 3); + } + + std::array counts{0, 0, 0}; + for(int v : r.mtrIndex) + { + counts[static_cast(v - 1)]++; + } + REQUIRE(static_cast(counts[0]) / N == Approx(0.30).margin(0.05)); + REQUIRE(static_cast(counts[1]) / N == Approx(0.35).margin(0.05)); + REQUIRE(static_cast(counts[2]) / N == Approx(0.35).margin(0.05)); + + REQUIRE(static_cast(r.phi1.size()) == N); + REQUIRE(static_cast(r.phi.size()) == N); + REQUIRE(static_cast(r.phi2.size()) == N); + + for(double a : r.phi1) + { + REQUIRE(a >= 0.0); + REQUIRE(a <= 2.0 * std::numbers::pi); + } + for(double a : r.phi) + { + REQUIRE(a >= 0.0); + REQUIRE(a <= std::numbers::pi); + } + for(double a : r.phi2) + { + REQUIRE(a >= 0.0); + REQUIRE(a <= 2.0 * std::numbers::pi); + } +} + +TEST_CASE("simulateMTR cancels early when observer requests it", "[mtrsim_driver]") +{ + mtrsim::SimulationParams params; + params.xLen = 6.0; + params.yLen = 6.0; + params.zLen = 0.0; + params.dx = 0.02; + params.dy = 0.02; + params.dz = 0.02; + params.volumeFractions = {0.30, 0.35, 0.35}; + params.thetaList = {{0.10, 0.45, 0.10}, {0.08, 0.37, 0.08}}; + params.seed = 42; + std::vector comps = {mtrsim::buildUniformODF(72, 36, 72), mtrsim::buildUniformODF(72, 36, 72), mtrsim::buildUniformODF(72, 36, 72)}; + std::mt19937_64 rng(params.seed); + CancelAfterObserver obs(1); // cancel at the first progress checkpoint + const mtrsim::MTRSimResult r = mtrsim::simulateMTR(params, comps, rng, 72, 36, 72, &obs); + REQUIRE(r.cancelled); + REQUIRE(r.mtrIndex.empty()); +} + +TEST_CASE("simulateMTR with nullptr observer is unaffected", "[mtrsim_driver]") +{ + mtrsim::SimulationParams params; + params.xLen = 2.0; + params.yLen = 2.0; + params.zLen = 0.0; + params.dx = 0.02; + params.dy = 0.02; + params.dz = 0.02; + params.volumeFractions = {0.30, 0.35, 0.35}; + params.thetaList = {{0.10, 0.45, 0.10}, {0.08, 0.37, 0.08}}; + std::vector comps = {mtrsim::buildUniformODF(72, 36, 72), mtrsim::buildUniformODF(72, 36, 72), mtrsim::buildUniformODF(72, 36, 72)}; + std::mt19937_64 rng(7); + const mtrsim::MTRSimResult r = mtrsim::simulateMTR(params, comps, rng, 72, 36, 72); + REQUIRE_FALSE(r.cancelled); + REQUIRE_FALSE(r.mtrIndex.empty()); +} diff --git a/tests/test_observer.cpp b/tests/test_observer.cpp new file mode 100644 index 0000000..0a6fbcf --- /dev/null +++ b/tests/test_observer.cpp @@ -0,0 +1,55 @@ +#include "SimulationObservers.hpp" + +#include + +#include + +namespace +{ +// Test double: records progress calls; can be told to cancel after K updates. +class RecordingObserver : public mtrsim::ISimulationObserver +{ +public: + explicit RecordingObserver(int cancelAfter = -1) + : m_CancelAfter(cancelAfter) + { + } + void updateProgress(int64_t done, int64_t total, const std::string& message) override + { + calls.push_back({done, total, message}); + } + bool shouldCancel() const override + { + return m_CancelAfter >= 0 && static_cast(calls.size()) >= m_CancelAfter; + } + struct Call + { + int64_t done; + int64_t total; + std::string message; + }; + std::vector calls; + +private: + int m_CancelAfter; +}; +} // namespace + +TEST_CASE("NullObserver never cancels and ignores progress", "[observer]") +{ + mtrsim::NullObserver obs; + obs.updateProgress(1, 10, "x"); + REQUIRE_FALSE(obs.shouldCancel()); +} + +TEST_CASE("RecordingObserver records and cancels after K", "[observer]") +{ + RecordingObserver obs(2); + REQUIRE_FALSE(obs.shouldCancel()); + obs.updateProgress(1, 10, "a"); + REQUIRE_FALSE(obs.shouldCancel()); + obs.updateProgress(2, 10, "b"); + REQUIRE(obs.shouldCancel()); + REQUIRE(obs.calls.size() == 2); + REQUIRE(obs.calls[1].done == 2); +} diff --git a/tests/test_odf_builder.cpp b/tests/test_odf_builder.cpp index 9e8b74d..adaa543 100644 --- a/tests/test_odf_builder.cpp +++ b/tests/test_odf_builder.cpp @@ -18,7 +18,8 @@ TEST_CASE("ODFBuilder::accumulate (no smoothing) hits exactly one bin per tuple" const mtrsim::ODFBuildParams p{72, 36, 72, 5.0, false}; std::vector values(72 * 36 * 72, 0.0); - // Tuple at bin (i_phi1=5, i_PHI=3, i_phi2=10) center → angles (27.5, 17.5, 52.5) deg + // Tuple at bin (i_phi1=5, i_PHI=3, i_phi2=10) center → angles + // (27.5, 17.5, 52.5) deg const std::vector> eulers = {{27.5 * k_DegToRad, 17.5 * k_DegToRad, 52.5 * k_DegToRad}}; mtrsim::accumulate(eulers, p, values); diff --git a/tests/test_odf_sampler.cpp b/tests/test_odf_sampler.cpp index 1b681dc..dccde12 100644 --- a/tests/test_odf_sampler.cpp +++ b/tests/test_odf_sampler.cpp @@ -1,3 +1,5 @@ +#include "ISimulationObserver.hpp" +#include "MTRSimDriver.hpp" #include "ODFCalculator.hpp" #include "ODFSampler.hpp" @@ -21,14 +23,15 @@ using namespace mtrsim; // ODFCalculator tests // ───────────────────────────────────────────────────────────────────────────── -TEST_CASE("ODFCalculator: output ODFComponent has correct sizes", - "[odfcalculator]") { +TEST_CASE("ODFCalculator: output ODFComponent has correct sizes", "[odfcalculator]") +{ ODFCalculator calc; // Small set of random orientations const int N = 10; Eigen::VectorXd phi1(N), phi(N), phi2(N); - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { phi1[i] = 0.1 * i; phi[i] = 0.05 * i; phi2[i] = 0.2 * i; @@ -43,7 +46,8 @@ TEST_CASE("ODFCalculator: output ODFComponent has correct sizes", CHECK(odf.phi2Bins.size() == expected); } -TEST_CASE("ODFCalculator: odfVal is non-negative", "[odfcalculator]") { +TEST_CASE("ODFCalculator: odfVal is non-negative", "[odfcalculator]") +{ ODFCalculator calc; Eigen::VectorXd phi1(3), phi(3), phi2(3); @@ -56,8 +60,8 @@ TEST_CASE("ODFCalculator: odfVal is non-negative", "[odfcalculator]") { CHECK((odf.odfVal.array() >= 0.0).all()); } -TEST_CASE("ODFCalculator: bin centres are within Euler-space range", - "[odfcalculator]") { +TEST_CASE("ODFCalculator: bin centres are within Euler-space range", "[odfcalculator]") +{ ODFCalculator calc; Eigen::VectorXd phi1(2), phi(2), phi2(2); @@ -78,8 +82,8 @@ TEST_CASE("ODFCalculator: bin centres are within Euler-space range", CHECK((odf.phi2Bins.array() <= twoPi).all()); } -TEST_CASE("ODFCalculator: sum of odfVal ≈ 1 for large grid", - "[odfcalculator]") { +TEST_CASE("ODFCalculator: sum of odfVal ≈ 1 for large grid", "[odfcalculator]") +{ // The smoothing weights sum to 1.0 per orientation, so the total ODF mass // should equal 1.0 (matching MATLAB unnormalised output with count/N // scaling). Use a small number of orientations to keep the test fast. @@ -88,7 +92,8 @@ TEST_CASE("ODFCalculator: sum of odfVal ≈ 1 for large grid", // 100 random-ish orientations distributed across Euler space const int N = 100; Eigen::VectorXd phi1(N), phi(N), phi2(N); - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { phi1[i] = 2.0 * std::numbers::pi * (static_cast(i) / N); phi[i] = std::numbers::pi * (static_cast(i) / N); phi2[i] = 2.0 * std::numbers::pi * (static_cast((i * 37) % N) / N); @@ -105,21 +110,23 @@ TEST_CASE("ODFCalculator: sum of odfVal ≈ 1 for large grid", // ODFSampler tests // ───────────────────────────────────────────────────────────────────────────── -namespace { +namespace +{ // Build a trivial ODFComponent: uniform probability across nBins bins, // bin centres at (i+0.5)*binWidth in all three Euler directions. -ODFComponent makeUniformODF(int nBins, double binWidth) { +ODFComponent makeUniformODF(int nBins, double binWidth) +{ ODFComponent c; c.odfVal = Eigen::VectorXd::Ones(nBins) / static_cast(nBins); - c.phi1Bins = Eigen::VectorXd::LinSpaced(nBins, 0.5 * binWidth, - (nBins - 0.5) * binWidth); + c.phi1Bins = Eigen::VectorXd::LinSpaced(nBins, 0.5 * binWidth, (nBins - 0.5) * binWidth); c.phiBins = c.phi1Bins; c.phi2Bins = c.phi1Bins; return c; } } // anonymous namespace -TEST_CASE("ODFSampler::sampleN: output dimensions are N x 3", "[odfsampler]") { +TEST_CASE("ODFSampler::sampleN: output dimensions are N x 3", "[odfsampler]") +{ std::mt19937_64 rng(42); ODFSampler sampler(rng); @@ -133,8 +140,8 @@ TEST_CASE("ODFSampler::sampleN: output dimensions are N x 3", "[odfsampler]") { CHECK(result.cols() == 3); } -TEST_CASE("ODFSampler::sampleN: same seed reproduces identical output", - "[odfsampler]") { +TEST_CASE("ODFSampler::sampleN: same seed reproduces identical output", "[odfsampler]") +{ const int nBins = 50; const ODFComponent uODF = makeUniformODF(nBins, 0.04); const int N = 20; @@ -156,7 +163,8 @@ TEST_CASE("ODFSampler::sampleN: same seed reproduces identical output", TEST_CASE("ODFSampler::sampleN: uniform ODF samples cover bin centres " "uniformly [slow]", - "[odfsampler][slow]") { + "[odfsampler][slow]") +{ // With a uniform ODF and a large draw, each bin should appear roughly // equally. std::mt19937_64 rng(12345); @@ -171,7 +179,8 @@ TEST_CASE("ODFSampler::sampleN: uniform ODF samples cover bin centres " // Count how many samples fall in each bin by phi1 Eigen::VectorXi counts = Eigen::VectorXi::Zero(nBins); - for (int i = 0; i < N; ++i) { + for(int i = 0; i < N; ++i) + { int bin = static_cast(std::floor(result(i, 0) / bw)); bin = std::clamp(bin, 0, nBins - 1); counts[bin]++; @@ -179,14 +188,14 @@ TEST_CASE("ODFSampler::sampleN: uniform ODF samples cover bin centres " // Each bin should receive ~N/nBins draws; allow ±30% relative tolerance const double expected = static_cast(N) / nBins; - for (int b = 0; b < nBins; ++b) { - CHECK(static_cast(counts[b]) == - Approx(expected).margin(0.30 * expected)); + for(int b = 0; b < nBins; ++b) + { + CHECK(static_cast(counts[b]) == Approx(expected).margin(0.30 * expected)); } } -TEST_CASE("ODFSampler::sampleOne: returns a single valid orientation", - "[odfsampler]") { +TEST_CASE("ODFSampler::sampleOne: returns a single valid orientation", "[odfsampler]") +{ std::mt19937_64 rng(99); ODFSampler sampler(rng); @@ -201,3 +210,28 @@ TEST_CASE("ODFSampler::sampleOne: returns a single valid orientation", CHECK(ea.phi >= -bw); CHECK(ea.phi2 >= -bw); } + +namespace +{ +class ImmediateCancel : public mtrsim::ISimulationObserver +{ +public: + void updateProgress(int64_t, int64_t, const std::string&) override + { + } + bool shouldCancel() const override + { + return true; + } +}; +} // namespace + +TEST_CASE("sampleN bails out promptly when observer cancels", "[odf_sampler]") +{ + mtrsim::ODFComponent uni = mtrsim::buildUniformODF(72, 36, 72); + std::mt19937_64 rng(1); + mtrsim::ODFSampler sampler{rng}; + ImmediateCancel cancel; + Eigen::MatrixXd out = sampler.sampleN(100000, uni, uni, &cancel); + REQUIRE(out.rows() == 0); +} diff --git a/tests/test_placeholder.cpp b/tests/test_placeholder.cpp index 5f469b8..eb20beb 100644 --- a/tests/test_placeholder.cpp +++ b/tests/test_placeholder.cpp @@ -2,4 +2,7 @@ // Placeholder test — replaced with real tests as each module is implemented. -TEST_CASE("Placeholder always passes", "[placeholder]") { REQUIRE(1 + 1 == 2); } +TEST_CASE("Placeholder always passes", "[placeholder]") +{ + REQUIRE(1 + 1 == 2); +} diff --git a/tests/test_qsimvn.cpp b/tests/test_qsimvn.cpp index d53f5b2..e668773 100644 --- a/tests/test_qsimvn.cpp +++ b/tests/test_qsimvn.cpp @@ -13,7 +13,8 @@ using namespace mtrsim; // QSimVN tests // ───────────────────────────────────────────────────────────────────────────── -TEST_CASE("QSimVN: univariate N(0,1) CDF matches std::erfc", "[qsimvn]") { +TEST_CASE("QSimVN: univariate N(0,1) CDF matches std::erfc", "[qsimvn]") +{ // P(Z <= z) for Z ~ N(0,1) with r=[1], a=-inf, b=z std::mt19937_64 rng(42); QSimVN qsimvn(rng); @@ -39,8 +40,8 @@ TEST_CASE("QSimVN: univariate N(0,1) CDF matches std::erfc", "[qsimvn]") { CHECK(p2 == Approx(0.025).margin(e2 + 5e-4)); } -TEST_CASE("QSimVN: bivariate independent N(0,I) quadrant probability ≈ 0.25", - "[qsimvn]") { +TEST_CASE("QSimVN: bivariate independent N(0,I) quadrant probability ≈ 0.25", "[qsimvn]") +{ // P(Z1 <= 0, Z2 <= 0) = 0.25 for Z ~ N(0,I_2) std::mt19937_64 rng(123); QSimVN qsimvn(rng); @@ -54,8 +55,8 @@ TEST_CASE("QSimVN: bivariate independent N(0,I) quadrant probability ≈ 0.25", CHECK(p == Approx(0.25).margin(e + 1e-4)); } -TEST_CASE("QSimVN: bivariate correlated N(0,R) matches known probability", - "[qsimvn]") { +TEST_CASE("QSimVN: bivariate correlated N(0,R) matches known probability", "[qsimvn]") +{ // Genz example from qsimvn.m header: // r = [4 3 2 1; 3 5 -1 1; 2 -1 4 2; 1 1 2 5] // a = -inf*[1 1 1 1]', b = [1 2 3 4]' @@ -68,10 +69,7 @@ TEST_CASE("QSimVN: bivariate correlated N(0,R) matches known probability", r << 4, 3, 2, 1, 3, 5, -1, 1, 2, -1, 4, 2, 1, 1, 2, 5; Eigen::VectorXd a(4), b(4); - a << -std::numeric_limits::infinity(), - -std::numeric_limits::infinity(), - -std::numeric_limits::infinity(), - -std::numeric_limits::infinity(); + a << -std::numeric_limits::infinity(), -std::numeric_limits::infinity(), -std::numeric_limits::infinity(), -std::numeric_limits::infinity(); b << 1.0, 2.0, 3.0, 4.0; auto [p, e] = qsimvn.compute(5000, r, a, b); @@ -81,7 +79,8 @@ TEST_CASE("QSimVN: bivariate correlated N(0,R) matches known probability", CHECK(e < 0.05); // error estimate within 5% } -TEST_CASE("QSimVN: symmetric interval gives correct probability", "[qsimvn]") { +TEST_CASE("QSimVN: symmetric interval gives correct probability", "[qsimvn]") +{ // P(-1 <= Z <= 1) = 2*Phi(1) - 1 ≈ 0.6827 for Z ~ N(0,1) std::mt19937_64 rng(999); QSimVN qsimvn(rng); @@ -101,8 +100,8 @@ TEST_CASE("QSimVN: symmetric interval gives correct probability", "[qsimvn]") { // AssignmentRule tests // ───────────────────────────────────────────────────────────────────────────── -TEST_CASE("AssignmentRule::selectThresholds: 3-component fractions sum to 1", - "[assignmentrule]") { +TEST_CASE("AssignmentRule::selectThresholds: 3-component fractions sum to 1", "[assignmentrule]") +{ // Standard MTRsim usage: 3 components with P = {0.30, 0.35, 0.35} std::mt19937_64 rng(42); AssignmentRule ar(rng); @@ -122,14 +121,12 @@ TEST_CASE("AssignmentRule::selectThresholds: 3-component fractions sum to 1", // Component 1: max_thresholds(1,1) should be a finite quantile CHECK(std::isfinite(thresholds.maxThresholds(1, 1))); // Component 2 (last): max_thresholds(2,*) = +inf - CHECK(thresholds.maxThresholds(2, 0) == - std::numeric_limits::infinity()); - CHECK(thresholds.maxThresholds(2, 1) == - std::numeric_limits::infinity()); + CHECK(thresholds.maxThresholds(2, 0) == std::numeric_limits::infinity()); + CHECK(thresholds.maxThresholds(2, 1) == std::numeric_limits::infinity()); } -TEST_CASE("AssignmentRule::evaluate: 2 Gaussians, simple box assignment", - "[assignmentrule]") { +TEST_CASE("AssignmentRule::evaluate: 2 Gaussians, simple box assignment", "[assignmentrule]") +{ // With 3 components and 2 Gaussians, manually construct thresholds and verify // evaluate. Component 1: z1 < 0, z2 in (-inf, +inf) → region where z1 < 0 // counts for comp 1 This is a simplified hand-crafted threshold set for @@ -171,9 +168,8 @@ TEST_CASE("AssignmentRule::evaluate: 2 Gaussians, simple box assignment", CHECK(assignments(3) == 1); } -TEST_CASE( - "AssignmentRule: end-to-end volume fractions are approximately recovered", - "[assignmentrule][slow]") { +TEST_CASE("AssignmentRule: end-to-end volume fractions are approximately recovered", "[assignmentrule][slow]") +{ // Verify that selectThresholds + evaluate reproduces the requested fractions. // Draw many samples from N(0, I_2) and check empirical fractions. const std::vector P = {0.30, 0.35, 0.35}; @@ -189,8 +185,10 @@ TEST_CASE( const int nSamples = 50000; std::normal_distribution normal(0.0, 1.0); Eigen::MatrixXd z(nSamples, numGaussians); - for (int i = 0; i < nSamples; ++i) { - for (int g = 0; g < numGaussians; ++g) { + for(int i = 0; i < nSamples; ++i) + { + for(int g = 0; g < numGaussians; ++g) + { z(i, g) = normal(rng); } } @@ -199,9 +197,11 @@ TEST_CASE( // Count each component Eigen::VectorXd empirical = Eigen::VectorXd::Zero(numComponents); - for (int i = 0; i < nSamples; ++i) { + for(int i = 0; i < nSamples; ++i) + { const int comp = assignments(i); - if (comp >= 1 && comp <= numComponents) { + if(comp >= 1 && comp <= numComponents) + { empirical(comp - 1) += 1.0; } } @@ -209,7 +209,8 @@ TEST_CASE( // Check that empirical fractions are within 2% of targets const double tol = 0.02; - for (int j = 0; j < numComponents; ++j) { + for(int j = 0; j < numComponents; ++j) + { CHECK(empirical(j) == Approx(P[static_cast(j)]).margin(tol)); } } diff --git a/wrapping/python/mtrsim.cpp b/wrapping/python/mtrsim.cpp index 1b2b336..aa32783 100644 --- a/wrapping/python/mtrsim.cpp +++ b/wrapping/python/mtrsim.cpp @@ -29,29 +29,32 @@ using namespace mtrsim; // Classes that take std::mt19937_64& store a reference to m_Engine; the // py::keep_alive<1,2> call policy ensures the Rng Python object outlives them. // --------------------------------------------------------------------------- -class Rng { +class Rng +{ public: explicit Rng(uint64_t seed = 0) - : m_Engine(seed == 0 ? std::random_device{}() : seed) {} + : m_Engine(seed == 0 ? std::random_device{}() : seed) + { + } - std::mt19937_64 &engine() { return m_Engine; } + std::mt19937_64& engine() + { + return m_Engine; + } private: std::mt19937_64 m_Engine; }; // --------------------------------------------------------------------------- -PYBIND11_MODULE(mtrsim, m) { +PYBIND11_MODULE(mtrsim, m) +{ m.doc() = "MTRSim Python bindings — Microtexture Region Simulator"; // ------------------------------------------------------------------------- // Rng // ------------------------------------------------------------------------- - py::class_( - m, "Rng", - "Owns a Mersenne-Twister 64 RNG. Pass to all stochastic constructors.") - .def(py::init(), "seed"_a = 0, - "Construct RNG; seed=0 uses std::random_device"); + py::class_(m, "Rng", "Owns a Mersenne-Twister 64 RNG. Pass to all stochastic constructors.").def(py::init(), "seed"_a = 0, "Construct RNG; seed=0 uses std::random_device"); // ------------------------------------------------------------------------- // CrystalSystem enum (defined in IPFMapper.hpp — used by IPFMapper to @@ -69,70 +72,47 @@ PYBIND11_MODULE(mtrsim, m) { // ------------------------------------------------------------------------- // SimulationParams // ------------------------------------------------------------------------- - py::class_( - m, "SimulationParams", - "All parameters that drive a single MTR simulation run.") + py::class_(m, "SimulationParams", "All parameters that drive a single MTR simulation run.") .def(py::init<>()) - .def_readwrite("x_len", &SimulationParams::xLen, - "Volume extent in X [mm]") - .def_readwrite("y_len", &SimulationParams::yLen, - "Volume extent in Y [mm]") - .def_readwrite("z_len", &SimulationParams::zLen, - "Volume extent in Z [mm] (0 = 2-D slice)") + .def_readwrite("x_len", &SimulationParams::xLen, "Volume extent in X [mm]") + .def_readwrite("y_len", &SimulationParams::yLen, "Volume extent in Y [mm]") + .def_readwrite("z_len", &SimulationParams::zLen, "Volume extent in Z [mm] (0 = 2-D slice)") .def_readwrite("dx", &SimulationParams::dx, "Voxel spacing in X [mm]") .def_readwrite("dy", &SimulationParams::dy, "Voxel spacing in Y [mm]") .def_readwrite("dz", &SimulationParams::dz, "Voxel spacing in Z [mm]") - .def_readwrite("volume_fractions", &SimulationParams::volumeFractions, - "Target volume fraction per MTR component (must sum to 1)") - .def_readwrite("theta_list", &SimulationParams::thetaList, - "Correlation lengths [num_gaussians x 3] (x,y,z)") - .def_readwrite("nugget_variance", &SimulationParams::nuggetVariance, - "Nugget variance per component") - .def_readwrite("odf_input_path", &SimulationParams::odfInputPath, - "Path to simulation_ODF.h5") - .def_readwrite("output_dir", &SimulationParams::outputDir, - "Directory for output files") - .def_readwrite("seed", &SimulationParams::seed, - "Random seed (0 = use std::random_device)"); + .def_readwrite("volume_fractions", &SimulationParams::volumeFractions, "Target volume fraction per MTR component (must sum to 1)") + .def_readwrite("theta_list", &SimulationParams::thetaList, "Correlation lengths [num_gaussians x 3] (x,y,z)") + .def_readwrite("nugget_variance", &SimulationParams::nuggetVariance, "Nugget variance per component") + .def_readwrite("odf_input_path", &SimulationParams::odfInputPath, "Path to simulation_ODF.h5") + .def_readwrite("output_dir", &SimulationParams::outputDir, "Directory for output files") + .def_readwrite("seed", &SimulationParams::seed, "Random seed (0 = use std::random_device)"); // ------------------------------------------------------------------------- // PGRFResult // ------------------------------------------------------------------------- py::class_(m, "PGRFResult", "Output of the PGRF simulation.") .def(py::init<>()) - .def_readwrite( - "mtr_index", &PGRFResult::mtrIndex, - "1-based component assignment per voxel, length N (VectorXi)") - .def_readwrite("latent_fields", &PGRFResult::latentFields, - "Latent GP values, shape [N x numGaussians] (MatrixXd)"); + .def_readwrite("mtr_index", &PGRFResult::mtrIndex, "1-based component assignment per voxel, length N (VectorXi)") + .def_readwrite("latent_fields", &PGRFResult::latentFields, "Latent GP values, shape [N x numGaussians] (MatrixXd)"); // ------------------------------------------------------------------------- // AssignmentRuleThresholds // ------------------------------------------------------------------------- - py::class_( - m, "AssignmentRuleThresholds", - "Threshold matrices produced by AssignmentRule.select_thresholds().") + py::class_(m, "AssignmentRuleThresholds", "Threshold matrices produced by AssignmentRule.select_thresholds().") .def(py::init<>()) .def_readwrite("num_gaussians", &AssignmentRuleThresholds::numGaussians) - .def_readwrite("min_thresholds", &AssignmentRuleThresholds::minThresholds, - "Shape [numComponents x numGaussians]") - .def_readwrite("max_thresholds", &AssignmentRuleThresholds::maxThresholds, - "Shape [numComponents x numGaussians]"); + .def_readwrite("min_thresholds", &AssignmentRuleThresholds::minThresholds, "Shape [numComponents x numGaussians]") + .def_readwrite("max_thresholds", &AssignmentRuleThresholds::maxThresholds, "Shape [numComponents x numGaussians]"); // ------------------------------------------------------------------------- // ODFComponent // ------------------------------------------------------------------------- - py::class_(m, "ODFComponent", - "Discrete ODF histogram for one MTR component.") + py::class_(m, "ODFComponent", "Discrete ODF histogram for one MTR component.") .def(py::init<>()) - .def_readwrite("odf_val", &ODFComponent::odfVal, - "Probability mass per bin (N_bins,)") - .def_readwrite("phi1_bins", &ODFComponent::phi1Bins, - "phi1 bin centres [rad]") - .def_readwrite("phi_bins", &ODFComponent::phiBins, - "PHI bin centres [rad]") - .def_readwrite("phi2_bins", &ODFComponent::phi2Bins, - "phi2 bin centres [rad]"); + .def_readwrite("odf_val", &ODFComponent::odfVal, "Probability mass per bin (N_bins,)") + .def_readwrite("phi1_bins", &ODFComponent::phi1Bins, "phi1 bin centres [rad]") + .def_readwrite("phi_bins", &ODFComponent::phiBins, "PHI bin centres [rad]") + .def_readwrite("phi2_bins", &ODFComponent::phi2Bins, "phi2 bin centres [rad]"); // ------------------------------------------------------------------------- // EulerAngles @@ -142,11 +122,7 @@ PYBIND11_MODULE(mtrsim, m) { .def_readwrite("phi1", &EulerAngles::phi1) .def_readwrite("phi", &EulerAngles::phi) .def_readwrite("phi2", &EulerAngles::phi2) - .def("__repr__", [](const EulerAngles &e) { - return "EulerAngles(phi1=" + std::to_string(e.phi1) + - ", phi=" + std::to_string(e.phi) + - ", phi2=" + std::to_string(e.phi2) + ")"; - }); + .def("__repr__", [](const EulerAngles& e) { return "EulerAngles(phi1=" + std::to_string(e.phi1) + ", phi=" + std::to_string(e.phi) + ", phi2=" + std::to_string(e.phi2) + ")"; }); // ------------------------------------------------------------------------- // RGBColor @@ -156,22 +132,16 @@ PYBIND11_MODULE(mtrsim, m) { .def_readwrite("r", &RGBColor::r) .def_readwrite("g", &RGBColor::g) .def_readwrite("b", &RGBColor::b) - .def("__repr__", [](const RGBColor &c) { - return "RGBColor(r=" + std::to_string(c.r) + - ", g=" + std::to_string(c.g) + ", b=" + std::to_string(c.b) + - ")"; - }); + .def("__repr__", [](const RGBColor& c) { return "RGBColor(r=" + std::to_string(c.r) + ", g=" + std::to_string(c.g) + ", b=" + std::to_string(c.b) + ")"; }); // ------------------------------------------------------------------------- // PoleFigureData // ------------------------------------------------------------------------- - py::class_(m, "PoleFigureData", - "Stereographic pole figure data.") + py::class_(m, "PoleFigureData", "Stereographic pole figure data.") .def(py::init<>()) .def_readwrite("x", &PoleFigureData::x, "Stereographic X coordinates") .def_readwrite("y", &PoleFigureData::y, "Stereographic Y coordinates") - .def_readwrite("intensity", &PoleFigureData::intensity, - "Normalised intensity per bin"); + .def_readwrite("intensity", &PoleFigureData::intensity, "Normalised intensity per bin"); // ------------------------------------------------------------------------- // EBSDData @@ -180,29 +150,28 @@ PYBIND11_MODULE(mtrsim, m) { // ------------------------------------------------------------------------- py::class_(m, "EBSDData", "EBSD scan data loaded from CSV files.") .def(py::init<>()) - .def_readwrite("spatial_coords", &EBSDData::spatialCoords, - "[N x 2] (X,Y) positions [mm]") - .def_readwrite("euler_angles", &EBSDData::eulerAngles, - "[N x 3] (phi1,PHI,phi2) [rad]") - .def_readwrite("parent_ids", &EBSDData::parentIds, - "MTR parent grain IDs, length N") + .def_readwrite("spatial_coords", &EBSDData::spatialCoords, "[N x 2] (X,Y) positions [mm]") + .def_readwrite("euler_angles", &EBSDData::eulerAngles, "[N x 3] (phi1,PHI,phi2) [rad]") + .def_readwrite("parent_ids", &EBSDData::parentIds, "MTR parent grain IDs, length N") .def_property( "is_mtr", // getter: Eigen::VectorX → numpy uint8 - [](const EBSDData &self) -> py::array_t { + [](const EBSDData& self) -> py::array_t { const auto n = self.isMTR.size(); py::array_t result(n); auto buf = result.mutable_unchecked<1>(); - for (Eigen::Index i = 0; i < n; ++i) { + for(Eigen::Index i = 0; i < n; ++i) + { buf(i) = self.isMTR(i) ? uint8_t{1} : uint8_t{0}; } return result; }, // setter: numpy uint8 → Eigen::VectorX - [](EBSDData &self, py::array_t arr) { + [](EBSDData& self, py::array_t arr) { auto buf = arr.unchecked<1>(); self.isMTR.resize(buf.shape(0)); - for (py::ssize_t i = 0; i < buf.shape(0); ++i) { + for(py::ssize_t i = 0; i < buf.shape(0); ++i) + { self.isMTR(i) = (buf(i) != 0); } }, @@ -211,44 +180,30 @@ PYBIND11_MODULE(mtrsim, m) { // ------------------------------------------------------------------------- // PGRFSimulation // ------------------------------------------------------------------------- - py::class_( - m, "PGRFSimulation", - "Orchestrates GPGenerator + AssignmentRule to produce a PGRF simulation.") - .def(py::init([](Rng &rng) { return new PGRFSimulation(rng.engine()); }), - "rng"_a, py::keep_alive<1, 2>()) - .def("run", &PGRFSimulation::run, "params"_a, - "Run the PGRF simulation and return a PGRFResult."); + py::class_(m, "PGRFSimulation", "Orchestrates GPGenerator + AssignmentRule to produce a PGRF simulation.") + .def(py::init([](Rng& rng) { return new PGRFSimulation(rng.engine()); }), "rng"_a, py::keep_alive<1, 2>()) + .def( + "run", [](PGRFSimulation& self, const SimulationParams& params) { return self.run(params, nullptr); }, "params"_a, "Run the PGRF simulation and return a PGRFResult."); // ------------------------------------------------------------------------- // GPGenerator // ------------------------------------------------------------------------- - py::class_( - m, "GPGenerator", - "Generates a separable GP field via the Kronecker-Cholesky method.") - .def(py::init([](Rng &rng, GPGenerator::CorrelationFn corrFn) { - return new GPGenerator(rng.engine(), corrFn); - }), - "rng"_a, "corr_fn"_a, py::keep_alive<1, 2>(), + py::class_(m, "GPGenerator", "Generates a separable GP field via the Kronecker-Cholesky method.") + .def(py::init([](Rng& rng, GPGenerator::CorrelationFn corrFn) { return new GPGenerator(rng.engine(), corrFn); }), "rng"_a, "corr_fn"_a, py::keep_alive<1, 2>(), "corr_fn(lag, theta) -> float: user-supplied correlation function") - .def("generate", &GPGenerator::generate, "hx"_a, "hy"_a, "hz"_a, - "theta"_a, "nx"_a, "ny"_a, "nz"_a, + .def("generate", &GPGenerator::generate, "hx"_a, "hy"_a, "hz"_a, "theta"_a, "nx"_a, "ny"_a, "nz"_a, "Draw one GP realisation on an nx*ny*nz grid. Returns flattened " "VectorXd of length nx*ny*nz."); // ------------------------------------------------------------------------- // AssignmentRule // ------------------------------------------------------------------------- - py::class_( - m, "AssignmentRule", - "Selects and evaluates the plurigaussian assignment rule.") - .def(py::init([](Rng &rng) { return new AssignmentRule(rng.engine()); }), - "rng"_a, py::keep_alive<1, 2>()) - .def("select_thresholds", &AssignmentRule::selectThresholds, - "volume_fractions"_a, + py::class_(m, "AssignmentRule", "Selects and evaluates the plurigaussian assignment rule.") + .def(py::init([](Rng& rng) { return new AssignmentRule(rng.engine()); }), "rng"_a, py::keep_alive<1, 2>()) + .def("select_thresholds", &AssignmentRule::selectThresholds, "volume_fractions"_a, "Determine Gaussian thresholds yielding the requested volume " "fractions.") - .def("evaluate", &AssignmentRule::evaluate, "z"_a, "thresholds"_a, - "Classify each voxel; returns 1-based component indices, length N."); + .def("evaluate", &AssignmentRule::evaluate, "z"_a, "thresholds"_a, "Classify each voxel; returns 1-based component indices, length N."); // ------------------------------------------------------------------------- // QSimVN @@ -256,8 +211,7 @@ PYBIND11_MODULE(mtrsim, m) { py::class_(m, "QSimVN", "Quasi-Monte Carlo estimator for the multivariate normal " "CDF (Genz 1992).") - .def(py::init([](Rng &rng) { return new QSimVN(rng.engine()); }), "rng"_a, - py::keep_alive<1, 2>()) + .def(py::init([](Rng& rng) { return new QSimVN(rng.engine()); }), "rng"_a, py::keep_alive<1, 2>()) .def("compute", &QSimVN::compute, "m"_a, "r"_a, "a"_a, "b"_a, "Estimate P(a <= X <= b) for X ~ N(0, R). Returns (probability, " "error)."); @@ -265,26 +219,21 @@ PYBIND11_MODULE(mtrsim, m) { // ------------------------------------------------------------------------- // ODFSampler // ------------------------------------------------------------------------- - py::class_( - m, "ODFSampler", - "Draws orientations from a discrete ODF by inverse-CDF sampling.") - .def(py::init([](Rng &rng) { return new ODFSampler(rng.engine()); }), - "rng"_a, py::keep_alive<1, 2>()) - .def("sample_n", &ODFSampler::sampleN, "n"_a, "component"_a, "uniform"_a, - "Draw n orientations; returns [N x 3] matrix (phi1, PHI, phi2) " - "[rad].") - .def("sample_one", &ODFSampler::sampleOne, "component"_a, "uniform"_a, - "Draw a single EulerAngles from the ODF."); + py::class_(m, "ODFSampler", "Draws orientations from a discrete ODF by inverse-CDF sampling.") + .def(py::init([](Rng& rng) { return new ODFSampler(rng.engine()); }), "rng"_a, py::keep_alive<1, 2>()) + .def( + "sample_n", [](ODFSampler& self, int n, const ODFComponent& component, const ODFComponent& uniform) { return self.sampleN(n, component, uniform, nullptr); }, "n"_a, "component"_a, + "uniform"_a, + "Draw n orientations; returns [N x 3] matrix (phi1, PHI, phi2) " + "[rad].") + .def("sample_one", &ODFSampler::sampleOne, "component"_a, "uniform"_a, "Draw a single EulerAngles from the ODF."); // ------------------------------------------------------------------------- // ODFCalculator // ------------------------------------------------------------------------- - py::class_( - m, "ODFCalculator", - "Computes a discrete ODF histogram from Euler angles.") + py::class_(m, "ODFCalculator", "Computes a discrete ODF histogram from Euler angles.") .def(py::init<>()) - .def("compute", &ODFCalculator::compute, "phi1"_a, "phi"_a, "phi2"_a, - "deg_spacing"_a = 5.0, + .def("compute", &ODFCalculator::compute, "phi1"_a, "phi"_a, "phi2"_a, "deg_spacing"_a = 5.0, "Compute ODFComponent from orientation arrays [rad]. deg_spacing " "controls bin width."); @@ -300,10 +249,8 @@ PYBIND11_MODULE(mtrsim, m) { // IPFColorScheme enum // ------------------------------------------------------------------------- py::enum_(m, "IPFColorScheme") - .value("EbsdLib", IPFColorScheme::EbsdLib, - "Standard EbsdLib/TSL IPF colouring") - .value("MatLab", IPFColorScheme::MatLab, - "Original MATLAB port colouring (Sparkman 2017)") + .value("EbsdLib", IPFColorScheme::EbsdLib, "Standard EbsdLib/TSL IPF colouring") + .value("MatLab", IPFColorScheme::MatLab, "Original MATLAB port colouring (Sparkman 2017)") .export_values(); // ------------------------------------------------------------------------- @@ -311,56 +258,38 @@ PYBIND11_MODULE(mtrsim, m) { // eulerToColors has std::array and IPFColorScheme default args // which pybind11 cannot express natively, so wrap in a lambda. // ------------------------------------------------------------------------- - py::class_(m, "IPFMapper", - "Builds an IPF colour map from Euler angles.") + py::class_(m, "IPFMapper", "Builds an IPF colour map from Euler angles.") .def(py::init(), "system"_a = CrystalSystem::HCP) .def( "euler_to_colors", - [](const IPFMapper &self, const Eigen::VectorXd &phi1, - const Eigen::VectorXd &phi, const Eigen::VectorXd &phi2, - std::array refDir, IPFColorScheme scheme) { + [](const IPFMapper& self, const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2, std::array refDir, IPFColorScheme scheme) { return self.eulerToColors(phi1, phi, phi2, refDir, scheme); }, - "phi1"_a, "phi"_a, "phi2"_a, - "ref_dir"_a = std::array{0.0, 0.0, 1.0}, - "scheme"_a = IPFColorScheme::EbsdLib, + "phi1"_a, "phi"_a, "phi2"_a, "ref_dir"_a = std::array{0.0, 0.0, 1.0}, "scheme"_a = IPFColorScheme::EbsdLib, "Convert Euler angles to a list of RGBColor; ref_dir defaults to Z = " "[0,0,1].") .def( "write_png", - [](const IPFMapper &self, const Eigen::MatrixXd &spatialCoords, - const Eigen::VectorXd &phi1, const Eigen::VectorXd &phi, - const Eigen::VectorXd &phi2, const std::string &outputPath, - IPFColorScheme scheme) { - self.writePNG(spatialCoords, phi1, phi, phi2, outputPath, scheme); - }, - "spatial_coords"_a, "phi1"_a, "phi"_a, "phi2"_a, "output_path"_a, - "scheme"_a = IPFColorScheme::EbsdLib, - "Render and save an IPF map PNG.") - .def("write_ipf_triangle_legend_matlab", - &IPFMapper::writeIPFTriangleLegendMatLab, "image_dim"_a, - "output_path"_a, + [](const IPFMapper& self, const Eigen::MatrixXd& spatialCoords, const Eigen::VectorXd& phi1, const Eigen::VectorXd& phi, const Eigen::VectorXd& phi2, const std::string& outputPath, + IPFColorScheme scheme) { self.writePNG(spatialCoords, phi1, phi, phi2, outputPath, scheme); }, + "spatial_coords"_a, "phi1"_a, "phi"_a, "phi2"_a, "output_path"_a, "scheme"_a = IPFColorScheme::EbsdLib, "Render and save an IPF map PNG.") + .def("write_ipf_triangle_legend_matlab", &IPFMapper::writeIPFTriangleLegendMatLab, "image_dim"_a, "output_path"_a, "Render the HCP IPF triangle legend using the MATLAB polar colour " "mapping and write it as PNG."); // ------------------------------------------------------------------------- // PoleFigure // ------------------------------------------------------------------------- - py::class_( - m, "PoleFigure", - "Converts a discrete ODF to stereographic pole figure data.") + py::class_(m, "PoleFigure", "Converts a discrete ODF to stereographic pole figure data.") .def(py::init<>()) - .def("from_odf", &PoleFigure::fromODF, "component"_a, - "deg_spacing"_a = 5.0, + .def("from_odf", &PoleFigure::fromODF, "component"_a, "deg_spacing"_a = 5.0, "Returns PoleFigureData with stereographic (x, y) and intensity " "arrays."); // ------------------------------------------------------------------------- // MTRDataLoader // ------------------------------------------------------------------------- - py::class_( - m, "MTRDataLoader", - "Loads experimental EBSD data from a directory of CSV files.") + py::class_(m, "MTRDataLoader", "Loads experimental EBSD data from a directory of CSV files.") .def(py::init<>()) .def("load", &MTRDataLoader::load, "directory_path"_a, "Load EulerAngles.csv, X/Y_Position.csv, ParentIds.csv, "