From c7d11fce776f9fc333bf5da87f794dc33feb5efb Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 14:02:31 -0400 Subject: [PATCH 01/39] docs: add Milestone AK design spec Integration of MTR representation codes into DREAM3D-NX filters: MTRSimFilter (algorithm MTRSim, "Generate Synthetic Microtexture"), filter-focused + statistical test strategy, and CI validation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/ISSUE_TEMPLATE/bug_report.yml | 99 +++++++++ .../ISSUE_TEMPLATE/documentation_report.yml | 73 +++++++ .../ISSUE_TEMPLATE/functionality_report.yml | 77 +++++++ .github/ISSUE_TEMPLATE/performance_report.yml | 96 ++++++++ .github/pull_request_template.md | 60 +++++ .github/workflows/format_pr.yml | 33 +++ .github/workflows/format_push.yml | 19 ++ .github/workflows/linux.yml | 79 +++++++ .github/workflows/macos.yml | 81 +++++++ .github/workflows/windows.yml | 64 ++++++ README.txt => ReadMe.md | 20 +- .../specs/2026-06-01-milestone-ak-design.md | 206 ++++++++++++++++++ 12 files changed, 898 insertions(+), 9 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation_report.yml create mode 100644 .github/ISSUE_TEMPLATE/functionality_report.yml create mode 100644 .github/ISSUE_TEMPLATE/performance_report.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/format_pr.yml create mode 100644 .github/workflows/format_push.yml create mode 100644 .github/workflows/linux.yml create mode 100644 .github/workflows/macos.yml create mode 100644 .github/workflows/windows.yml rename README.txt => ReadMe.md (82%) create mode 100644 docs/superpowers/specs/2026-06-01-milestone-ak-design.md 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/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/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..4f64f36 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-milestone-ak-design.md @@ -0,0 +1,206 @@ +# 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. | +| Seed | `UInt64Parameter` | `0` ⇒ seed from `std::random_device`. | +| Generate Polar Coloring | `BoolParameter` (default `false`) | Gates creation of the RGB output array. | +| *nuggetVariance* | — | **Not exposed.** Unused by the simulation. | + +> **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:** 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 | Type | Comps | Notes | + |---|---|---|---| + | MTR Index | Int32 | 1 | Values start at **1** (0 reserved, matches FeatureIds convention). | + | Euler Angles | 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).** The standalone driver iterates + `z → x → y` (`main.cpp`), producing `k = ((z)·nx + x)·ny + y`. A DREAM3D + ImageGeometry cell index is `(z·ny + y)·nx + x`. These orderings differ — the + algorithm must remap sim-order → geometry-order 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). From 71bc698c0b58f1360f64d1bb9cc7aa33b8a01ab2 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 14:41:06 -0400 Subject: [PATCH 02/39] docs: revise AK spec per review (seed pattern, output names, voxel order) - Random seed uses standard simplnx UseSeed/SeedValue/SeedArray pattern - Output defaults: geometry "MTR Microstructure", arrays "MTRIds"/"Eulers" - Clarify voxel remap target is SIMPLNX z,y,x (slowest->fastest) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/2026-06-01-milestone-ak-design.md | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/docs/superpowers/specs/2026-06-01-milestone-ak-design.md b/docs/superpowers/specs/2026-06-01-milestone-ak-design.md index 4f64f36..4fa8b13 100644 --- a/docs/superpowers/specs/2026-06-01-milestone-ak-design.md +++ b/docs/superpowers/specs/2026-06-01-milestone-ak-design.md @@ -61,10 +61,22 @@ work is implemented and verified. | 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. | -| Seed | `UInt64Parameter` | `0` ⇒ seed from `std::random_device`. | +| 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 @@ -76,16 +88,16 @@ work is implemented and verified. The filter **creates a new** Image Geometry (it does not write into the ODF geometry): -- **Geometry:** 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). +- **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 | Type | Comps | Notes | + | Array (default name) | Type | Comps | Notes | |---|---|---|---| - | MTR Index | Int32 | 1 | Values start at **1** (0 reserved, matches FeatureIds convention). | - | Euler Angles | Float32 | 3 | Bunge `phi1, PHI, phi2` [radians]. | - | Polar Colors | UInt8 | 3 | RGB. **Created only when** "Generate Polar Coloring" is on. | + | `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 @@ -114,12 +126,13 @@ built in. ### Concerns that drive correctness -1. **Voxel index remapping (highest risk).** The standalone driver iterates - `z → x → y` (`main.cpp`), producing `k = ((z)·nx + x)·ny + y`. A DREAM3D - ImageGeometry cell index is `(z·ny + y)·nx + x`. These orderings differ — the - algorithm must remap sim-order → geometry-order when filling cell arrays, or - the field comes out transposed. This remap gets a dedicated small-grid - deterministic test. +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 From 7d5689887a051690f33d0ba1f42db53721a37c82 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 14:47:23 -0400 Subject: [PATCH 03/39] docs: add Milestone AK implementation plan TDD plan for MTRSimFilter: LibMTRSim driver (buildUniformODF, gridToODFComponent, remapSimToZYX, simulateMTR), the filter + algorithm, deterministic + statistical tests, and CI validation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-06-01-milestone-ak.md | 1372 +++++++++++++++++ 1 file changed, 1372 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-01-milestone-ak.md 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..f228100 --- /dev/null +++ b/docs/superpowers/plans/2026-06-01-milestone-ak.md @@ -0,0 +1,1372 @@ +# 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 (note: `Catch::Approx`, not `Approx`), CMake, GitHub Actions. + +--- + +## 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`. +- `LibMTRSim.cmake` — add `MTRSimDriver.{hpp,cpp}` to the standalone library. +- `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() == Catch::Approx(1.0)); + REQUIRE(uni.odfVal[0] == Catch::Approx(1.0 / (72.0 * 36.0 * 72.0))); + + // First bin centre: i1=iPHI=i2=0 -> all 0.5 * step. + REQUIRE(uni.phi1Bins[0] == Catch::Approx(0.5 * 2.0 * std::numbers::pi / 72.0)); + REQUIRE(uni.phiBins[0] == Catch::Approx(0.5 * std::numbers::pi / 36.0)); + REQUIRE(uni.phi2Bins[0] == Catch::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() == Catch::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] == Catch::Approx(2.5 * deg2rad)); + REQUIRE(c.phiBins[0] == Catch::Approx(2.5 * deg2rad)); + REQUIRE(c.phi2Bins[0] == Catch::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 == Catch::Approx(0.30).margin(0.05)); + REQUIRE(static_cast(counts[1]) / N == Catch::Approx(0.35).margin(0.05)); + REQUIRE(static_cast(counts[2]) / N == Catch::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(-13001, "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(-13002, 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(-13003, 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(-13004, fmt::format("Theta List needs at least {} rows (components - 1).", numComponents - 1))}; + } + for (const auto& row : pThetaList) + { + if (row.size() != 3) + { + return {MakeErrorResult(-13005, "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 -13004), VF sum ≠ 1 +(error -13003). 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(-13050, 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 == Catch::Approx(0.30).margin(0.06)); + REQUIRE(counts[1] / n == Catch::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). From b8e5ef81ed3f8b414c77b562c5456ddfdd57d122 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 14:58:31 -0400 Subject: [PATCH 04/39] feat(lib): add buildUniformODF grid helper to MTRSimDriver Co-Authored-By: Claude Opus 4.8 (1M context) --- MTRSimPlugin.cmake | 2 ++ src/LibMTRSim/CMakeLists.txt | 2 ++ src/LibMTRSim/MTRSimDriver.cpp | 34 ++++++++++++++++++++++++++++++++++ src/LibMTRSim/MTRSimDriver.hpp | 26 ++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/test_mtrsim_driver.cpp | 21 +++++++++++++++++++++ 6 files changed, 86 insertions(+) create mode 100644 src/LibMTRSim/MTRSimDriver.cpp create mode 100644 src/LibMTRSim/MTRSimDriver.hpp create mode 100644 tests/test_mtrsim_driver.cpp diff --git a/MTRSimPlugin.cmake b/MTRSimPlugin.cmake index 0ce8d0f..347214a 100644 --- a/MTRSimPlugin.cmake +++ b/MTRSimPlugin.cmake @@ -112,6 +112,7 @@ 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 @@ -125,6 +126,7 @@ 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 ) target_sources(${PLUGIN_NAME} diff --git a/src/LibMTRSim/CMakeLists.txt b/src/LibMTRSim/CMakeLists.txt index 31664b2..b92717d 100644 --- a/src/LibMTRSim/CMakeLists.txt +++ b/src/LibMTRSim/CMakeLists.txt @@ -3,6 +3,7 @@ set(MTRSIM_HEADERS GPGenerator.hpp IPFMapper.hpp MTRDataLoader.hpp + MTRSimDriver.hpp ODFBuilder.hpp ODFCalculator.hpp ODFFileIO.hpp @@ -17,6 +18,7 @@ set(MTRSIM_SOURCES GPGenerator.cpp IPFMapper.cpp MTRDataLoader.cpp + MTRSimDriver.cpp ODFBuilder.cpp ODFCalculator.cpp ODFFileIO.cpp diff --git a/src/LibMTRSim/MTRSimDriver.cpp b/src/LibMTRSim/MTRSimDriver.cpp new file mode 100644 index 0000000..b455551 --- /dev/null +++ b/src/LibMTRSim/MTRSimDriver.cpp @@ -0,0 +1,34 @@ +#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 diff --git a/src/LibMTRSim/MTRSimDriver.hpp b/src/LibMTRSim/MTRSimDriver.hpp new file mode 100644 index 0000000..320d10c --- /dev/null +++ b/src/LibMTRSim/MTRSimDriver.hpp @@ -0,0 +1,26 @@ +#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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5f89d9e..4c5768e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(mtrsim_tests 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 ) diff --git a/tests/test_mtrsim_driver.cpp b/tests/test_mtrsim_driver.cpp new file mode 100644 index 0000000..7336935 --- /dev/null +++ b/tests/test_mtrsim_driver.cpp @@ -0,0 +1,21 @@ +#include "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)); +} From c27c1f664a94dcd25a64d6ea731e13f1a8ba7fdc Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 15:04:56 -0400 Subject: [PATCH 05/39] feat(lib): add gridToODFComponent reconstruction helper Co-Authored-By: Claude Opus 4.8 (1M context) --- src/LibMTRSim/MTRSimDriver.cpp | 31 +++++++++++++++++++++++++++++++ src/LibMTRSim/MTRSimDriver.hpp | 11 +++++++++++ tests/test_mtrsim_driver.cpp | 16 ++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/src/LibMTRSim/MTRSimDriver.cpp b/src/LibMTRSim/MTRSimDriver.cpp index b455551..90bf1bd 100644 --- a/src/LibMTRSim/MTRSimDriver.cpp +++ b/src/LibMTRSim/MTRSimDriver.cpp @@ -31,4 +31,35 @@ ODFComponent buildUniformODF(int n1, int nPHI, int n2) { 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; +} + } // namespace mtrsim diff --git a/src/LibMTRSim/MTRSimDriver.hpp b/src/LibMTRSim/MTRSimDriver.hpp index 320d10c..72c47d7 100644 --- a/src/LibMTRSim/MTRSimDriver.hpp +++ b/src/LibMTRSim/MTRSimDriver.hpp @@ -23,4 +23,15 @@ namespace mtrsim { */ 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); + } // namespace mtrsim diff --git a/tests/test_mtrsim_driver.cpp b/tests/test_mtrsim_driver.cpp index 7336935..f35c39f 100644 --- a/tests/test_mtrsim_driver.cpp +++ b/tests/test_mtrsim_driver.cpp @@ -19,3 +19,19 @@ TEST_CASE("buildUniformODF produces correct bin centres", "[mtrsim_driver]") { 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)); +} From 40bfe04e7e57f5278f8691af7b7c9bae24d7a2a5 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 15:09:14 -0400 Subject: [PATCH 06/39] feat(lib): add remapSimToZYX voxel-order helper Co-Authored-By: Claude Opus 4.8 (1M context) --- src/LibMTRSim/MTRSimDriver.hpp | 25 +++++++++++++++++++++++++ tests/test_mtrsim_driver.cpp | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/LibMTRSim/MTRSimDriver.hpp b/src/LibMTRSim/MTRSimDriver.hpp index 72c47d7..0a90df7 100644 --- a/src/LibMTRSim/MTRSimDriver.hpp +++ b/src/LibMTRSim/MTRSimDriver.hpp @@ -6,6 +6,7 @@ #include "SimulationParams.hpp" #include +#include #include #include #include @@ -34,4 +35,28 @@ LIBMTRSIM_EXPORT ODFComponent buildUniformODF(int n1, int nPHI, int n2); */ 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; +} + } // namespace mtrsim diff --git a/tests/test_mtrsim_driver.cpp b/tests/test_mtrsim_driver.cpp index f35c39f..ce4af65 100644 --- a/tests/test_mtrsim_driver.cpp +++ b/tests/test_mtrsim_driver.cpp @@ -35,3 +35,27 @@ TEST_CASE("gridToODFComponent derives bin centres in radians and normalizes", "[ 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()); +} From f331a9ecf9eaef0e16f03fd15e84162e8c5365e5 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 15:20:12 -0400 Subject: [PATCH 07/39] feat(lib): add simulateMTR end-to-end driver; main.cpp uses it Declares MTRSimResult struct and simulateMTR() in MTRSimDriver.hpp/.cpp. The function consolidates PGRF assignment, per-component ODF sampling, and per-voxel orientation assignment into a single call that returns results already remapped into SIMPLNX z,y,x voxel order. Refactors src/app/main.cpp to call simulateMTR() and removes the now- redundant local buildUniformODF() from the anonymous namespace. Adds a statistical end-to-end test (6x6 mm domain, seed=42) that verifies volume fractions land within 0.05 of targets (0.30/0.35/0.35) and that all output Euler angles are within their valid ranges. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/LibMTRSim/MTRSimDriver.cpp | 57 +++++++++++++++++++++ src/LibMTRSim/MTRSimDriver.hpp | 28 +++++++++++ src/app/main.cpp | 91 ++++------------------------------ tests/test_mtrsim_driver.cpp | 32 ++++++++++++ 4 files changed, 127 insertions(+), 81 deletions(-) diff --git a/src/LibMTRSim/MTRSimDriver.cpp b/src/LibMTRSim/MTRSimDriver.cpp index 90bf1bd..a29de7f 100644 --- a/src/LibMTRSim/MTRSimDriver.cpp +++ b/src/LibMTRSim/MTRSimDriver.cpp @@ -1,6 +1,12 @@ #include "MTRSimDriver.hpp" +#include "ODFSampler.hpp" +#include "PGRFSimulation.hpp" + +#include +#include #include +#include namespace mtrsim { @@ -62,4 +68,55 @@ ODFComponent gridToODFComponent(const std::vector& values, int n1, int n return c; } +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(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. + 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 index 0a90df7..30d20a4 100644 --- a/src/LibMTRSim/MTRSimDriver.hpp +++ b/src/LibMTRSim/MTRSimDriver.hpp @@ -59,4 +59,32 @@ std::vector remapSimToZYX(const std::vector& in, int nx, int ny, int nz) { 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; + 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); + } // namespace mtrsim diff --git a/src/app/main.cpp b/src/app/main.cpp index c87a742..377debb 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -2,8 +2,8 @@ // Orchestrates PGRF simulation, ODF loading, orientation sampling, and output. #include "IPFMapper.hpp" +#include "MTRSimDriver.hpp" #include "ODFSampler.hpp" -#include "PGRFSimulation.hpp" #include "SimulationParams.hpp" #include @@ -108,45 +108,6 @@ 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 // ───────────────────────────────────────────────────────────────────────────── @@ -260,13 +221,6 @@ int main(int argc, char **argv) { } } - // ── 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); @@ -283,40 +237,15 @@ int main(int argc, char **argv) { return 1; } - // ── Build uniform reference ODF - // ────────────────────────────────────────────── - const mtrsim::ODFComponent uniformOdf = buildUniformODF(); + // ── Run full MTR simulation (PGRF + ODF sampling + remap to z,y,x order) + // ───────────────────────────────────────────────────────────────────────── + spdlog::info("Running MTR simulation..."); + mtrsim::MTRSimResult sim = mtrsim::simulateMTR(params, odfComponents, rng, 72, 36, 72); + spdlog::info("MTR simulation complete."); - // ── Pre-sample N orientations per component (batched) - // ──────────────────────── - const int numComponents = static_cast(odfComponents.size()); - spdlog::info("Sampling orientations ({} components, N={} each)...", - numComponents, N); - - 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); - } + 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 // ──────────────────────────────────────────────────────── @@ -349,7 +278,7 @@ int main(int argc, char **argv) { 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'; + << phi2Vec[i] << ',' << sim.mtrIndex[static_cast(i)] << '\n'; } } spdlog::info("Results CSV written."); diff --git a/tests/test_mtrsim_driver.cpp b/tests/test_mtrsim_driver.cpp index ce4af65..5971294 100644 --- a/tests/test_mtrsim_driver.cpp +++ b/tests/test_mtrsim_driver.cpp @@ -2,6 +2,7 @@ #include +#include #include TEST_CASE("buildUniformODF produces correct bin centres", "[mtrsim_driver]") { @@ -59,3 +60,34 @@ TEST_CASE("remapSimToZYX moves y-fastest data to x-fastest layout", "[mtrsim_dri // 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)); + + 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); } +} From 6a79b632983efbe96ae24ec3601af7b5a5d8afaa Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 15:32:31 -0400 Subject: [PATCH 08/39] fix(lib): correct main.cpp voxel ordering + harden simulateMTR per review FIX 1: Rebuild spatialCoords in SIMPLNX z,y,x order (iz outer, iy middle, ix inner) so row k matches sim.phi*/mtrIndex[k]. The old loop used z-x-y order which mismatched coordinates with orientations for non-square grids. FIX 2: Replace magic literals 72, 36, 72 in the simulateMTR call with named constants (k_OdfBinsPhi1/PHI/Phi2) documenting the fixed 5-degree Bunge-Euler MATLAB ODF grid layout (186624 bins). FIX 3: Guard in simulateMTR that pgrf_result.mtrIndex.size() == N; throws std::runtime_error if the PGRF result dimensions are inconsistent. FIX 4: CSV loop now uses sim.nx*sim.ny*sim.nz to tie the bound to the actual result rather than the pre-call N. FIX 5: Add phi2 range check and phi1/phi/phi2 size-equality checks to the statistical driver test. FIX 6: Remove unused include from main.cpp; ODFSampler.hpp retained (ODFComponent is used directly). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/LibMTRSim/MTRSimDriver.cpp | 4 ++++ src/app/main.cpp | 36 +++++++++++++++++++++------------- tests/test_mtrsim_driver.cpp | 5 +++++ 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/LibMTRSim/MTRSimDriver.cpp b/src/LibMTRSim/MTRSimDriver.cpp index a29de7f..09cec3c 100644 --- a/src/LibMTRSim/MTRSimDriver.cpp +++ b/src/LibMTRSim/MTRSimDriver.cpp @@ -85,6 +85,10 @@ MTRSimResult simulateMTR(const SimulationParams& params, PGRFSimulation pgrf{rng}; const PGRFResult pgrf_result = pgrf.run(params); // throws on bad dims + 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()); diff --git a/src/app/main.cpp b/src/app/main.cpp index 377debb..85f4d63 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -14,7 +14,6 @@ #include #include -#include #include #include #include @@ -203,19 +202,20 @@ int main(int argc, char **argv) { 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; } } } @@ -239,8 +239,14 @@ int main(int argc, char **argv) { // ── 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; + spdlog::info("Running MTR simulation..."); - mtrsim::MTRSimResult sim = mtrsim::simulateMTR(params, odfComponents, rng, 72, 36, 72); + mtrsim::MTRSimResult sim = mtrsim::simulateMTR(params, odfComponents, rng, k_OdfBinsPhi1, k_OdfBinsPHI, k_OdfBinsPhi2); spdlog::info("MTR simulation complete."); Eigen::VectorXd phi1Vec = Eigen::Map(sim.phi1.data(), static_cast(sim.phi1.size())); @@ -275,7 +281,9 @@ int main(int argc, char **argv) { csv << std::fixed; csv.precision(6); - for (int i = 0; i < N; ++i) { + // 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'; diff --git a/tests/test_mtrsim_driver.cpp b/tests/test_mtrsim_driver.cpp index 5971294..14e94cd 100644 --- a/tests/test_mtrsim_driver.cpp +++ b/tests/test_mtrsim_driver.cpp @@ -88,6 +88,11 @@ TEST_CASE("simulateMTR reproduces target volume fractions (statistical)", "[mtrs 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); } } From 14fbb90cd36e692a42f141d9942539cccf2ea40b Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 15:41:13 -0400 Subject: [PATCH 09/39] feat(filter): scaffold MTRSimFilter + MTRSim algorithm, params, preflight validation + error tests Co-Authored-By: Claude Opus 4.8 (1M context) --- MTRSimPlugin.cmake | 2 + src/MTRSim/Filters/Algorithms/MTRSim.cpp | 26 +++ src/MTRSim/Filters/Algorithms/MTRSim.hpp | 57 ++++++ src/MTRSim/Filters/MTRSimFilter.cpp | 246 +++++++++++++++++++++++ src/MTRSim/Filters/MTRSimFilter.hpp | 134 ++++++++++++ test/CMakeLists.txt | 1 + test/MTRSimTest.cpp | 148 ++++++++++++++ 7 files changed, 614 insertions(+) create mode 100644 src/MTRSim/Filters/Algorithms/MTRSim.cpp create mode 100644 src/MTRSim/Filters/Algorithms/MTRSim.hpp create mode 100644 src/MTRSim/Filters/MTRSimFilter.cpp create mode 100644 src/MTRSim/Filters/MTRSimFilter.hpp create mode 100644 test/MTRSimTest.cpp diff --git a/MTRSimPlugin.cmake b/MTRSimPlugin.cmake index 347214a..f5c3e21 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 ) diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.cpp b/src/MTRSim/Filters/Algorithms/MTRSim.cpp new file mode 100644 index 0000000..f6c0f33 --- /dev/null +++ b/src/MTRSim/Filters/Algorithms/MTRSim.cpp @@ -0,0 +1,26 @@ +#include "MTRSim.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" + +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()() +{ + // NOTE: The algorithm body is implemented in a later task. For now this is a + // no-op stub so the filter scaffolding (preflight + parameter validation) can + // be built and tested independently. + return {}; +} diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.hpp b/src/MTRSim/Filters/Algorithms/MTRSim.hpp new file mode 100644 index 0000000..f7a1d00 --- /dev/null +++ b/src/MTRSim/Filters/Algorithms/MTRSim.hpp @@ -0,0 +1,57 @@ +#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 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: + 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/MTRSimFilter.cpp b/src/MTRSim/Filters/MTRSimFilter.cpp new file mode 100644 index 0000000..27d2fa6 --- /dev/null +++ b/src/MTRSim/Filters/MTRSimFilter.cpp @@ -0,0 +1,246 @@ +#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/GeometrySelectionParameter.hpp" +#include "simplnx/Parameters/MultiArraySelectionParameter.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Parameters/util/DynamicTableInfo.hpp" + +#include + +#include +#include +#include +#include + +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{"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{"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.", 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; +} + +//------------------------------------------------------------------------------ +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); + 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 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(-13001, "MTRSim requires at least 2 ODF component arrays.")}; + } + if(pVolumeFractions.size() != 1 || pVolumeFractions[0].size() != numComponents) + { + return {MakeErrorResult(-13002, 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(-13003, fmt::format("Volume Fraction values must sum to 1.0 (got {:.4f}).", vfSum))}; + } + if(pThetaList.size() < numComponents - 1) + { + return {MakeErrorResult(-13004, fmt::format("Theta List needs at least {} rows (components - 1).", numComponents - 1))}; + } + for(const auto& row : pThetaList) + { + if(row.size() != 3) + { + return {MakeErrorResult(-13005, "Each Theta List row must have exactly 3 columns.")}; + } + } + + 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}))); + + 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.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); + + 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)(); +} + +//------------------------------------------------------------------------------ +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..163b743 --- /dev/null +++ b/src/MTRSim/Filters/MTRSimFilter.hpp @@ -0,0 +1,134 @@ +#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 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"; + + /** + * @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/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/MTRSimTest.cpp b/test/MTRSimTest.cpp new file mode 100644 index 0000000..afd9c52 --- /dev/null +++ b/test/MTRSimTest.cpp @@ -0,0 +1,148 @@ +/** + * 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). + */ + +#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/MultiArraySelectionParameter.hpp" +#include "simplnx/UnitTest/UnitTestCommon.hpp" + +#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_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; +} +} // 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: 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); +} From 38bb3e0c2ba618b4a29b8d949d33e8a97b951ee3 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 15:50:40 -0400 Subject: [PATCH 10/39] fix(filter): harden MTRSimFilter preflight + conventions per review - Add zero-spacing guard (-13006) before dim() divisions in preflightImpl - Update Physical Size help text to document the 2D (Z=0) mode - Change physicalSize/physicalSpacing members from std::vector to std::vector to match what executeImpl reads from filterArgs - Change outputs separator label to "Output Data Object(s)" (matches ReadMTRSimODFFilter) - Remove premature #include "simplnx/DataStructure/DataArray.hpp" from stub MTRSim.cpp - Add preflight test for Theta List rows with wrong column count (-13005) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/MTRSim/Filters/Algorithms/MTRSim.cpp | 2 -- src/MTRSim/Filters/Algorithms/MTRSim.hpp | 4 ++-- src/MTRSim/Filters/MTRSimFilter.cpp | 9 +++++++-- test/MTRSimTest.cpp | 19 +++++++++++++++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.cpp b/src/MTRSim/Filters/Algorithms/MTRSim.cpp index f6c0f33..793e434 100644 --- a/src/MTRSim/Filters/Algorithms/MTRSim.cpp +++ b/src/MTRSim/Filters/Algorithms/MTRSim.cpp @@ -1,7 +1,5 @@ #include "MTRSim.hpp" -#include "simplnx/DataStructure/DataArray.hpp" - using namespace nx::core; // ----------------------------------------------------------------------------- diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.hpp b/src/MTRSim/Filters/Algorithms/MTRSim.hpp index f7a1d00..85f42f1 100644 --- a/src/MTRSim/Filters/Algorithms/MTRSim.hpp +++ b/src/MTRSim/Filters/Algorithms/MTRSim.hpp @@ -17,8 +17,8 @@ struct MTRSIM_EXPORT MTRSimInputValues 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 + std::vector physicalSize; // [x,y,z] microns + std::vector physicalSpacing; // [x,y,z] microns uint64 seed; bool generatePolarColoring; DataPath outputGeometryPath; diff --git a/src/MTRSim/Filters/MTRSimFilter.cpp b/src/MTRSim/Filters/MTRSimFilter.cpp index 27d2fa6..2977aa3 100644 --- a/src/MTRSim/Filters/MTRSimFilter.cpp +++ b/src/MTRSim/Filters/MTRSimFilter.cpp @@ -86,7 +86,7 @@ Parameters MTRSimFilter::parameters() const "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}, + 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"})); @@ -96,7 +96,7 @@ Parameters MTRSimFilter::parameters() const 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.insertSeparator(Parameters::Separator{"Output Data Object(s)"}); 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"}))); @@ -173,6 +173,11 @@ IFilter::PreflightResult MTRSimFilter::preflightImpl(const DataStructure& dataSt } } + if(pSpacing[0] <= 0.0f || pSpacing[1] <= 0.0f) + { + return {MakeErrorResult(-13006, "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]); diff --git a/test/MTRSimTest.cpp b/test/MTRSimTest.cpp index afd9c52..028e847 100644 --- a/test/MTRSimTest.cpp +++ b/test/MTRSimTest.cpp @@ -7,6 +7,7 @@ * 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 (-13005). */ #include @@ -146,3 +147,21 @@ TEST_CASE("MTRSim::MTRSimFilter: Rejects Volume Fraction not summing to 1.0", "[ auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); } + +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 (-13005), not the + // row-count check (-13004). + 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); +} From b1072695212dfa47f5c246b1d7ddd7403f5f1239 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 15:55:13 -0400 Subject: [PATCH 11/39] feat(filter): implement MTRSim algorithm execute + statistical wiring test Wire MTRSimFilter to the LibMTRSim simulateMTR entry point: reconstruct ODFComponents from the selected Float64 cell arrays, build SimulationParams from filter inputs, run the simulation, and write MTRIds + Eulers into the output geometry cell AttributeMatrix. Polar coloring (Task 8) is deferred. Add preflight non-negativity/range hardening (error -13007) requiring each Volume Fraction value to lie in [0, 1]. Add an end-to-end execute test (100x100 = 10000 voxels, seed 42, 3 components) that verifies array contract (types/components/tuples), id set {1,2,3}, finite Bunge-bounded Eulers, recorded seed, and loose volume fractions. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/MTRSim/Filters/Algorithms/MTRSim.cpp | 91 +++++++++++++++++++- src/MTRSim/Filters/MTRSimFilter.cpp | 7 ++ test/MTRSimTest.cpp | 103 +++++++++++++++++++++++ 3 files changed, 198 insertions(+), 3 deletions(-) diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.cpp b/src/MTRSim/Filters/Algorithms/MTRSim.cpp index 793e434..fdd6cc2 100644 --- a/src/MTRSim/Filters/Algorithms/MTRSim.cpp +++ b/src/MTRSim/Filters/Algorithms/MTRSim.cpp @@ -1,5 +1,17 @@ #include "MTRSim.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" + +#include "LibMTRSim/MTRSimDriver.hpp" + +#include + +#include +#include +#include +#include + using namespace nx::core; // ----------------------------------------------------------------------------- @@ -17,8 +29,81 @@ MTRSim::~MTRSim() noexcept = default; // ----------------------------------------------------------------------------- Result<> MTRSim::operator()() { - // NOTE: The algorithm body is implemented in a later task. For now this is a - // no-op stub so the filter scaffolding (preflight + parameter validation) can - // be built and tested independently. + 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 from filter inputs. + 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; + + 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); + mtrsim::MTRSimResult sim; + try + { + sim = mtrsim::simulateMTR(params, components, rng, n1, nPHI, n2); + } catch(const std::exception& e) + { + return MakeErrorResult(-13050, fmt::format("MTR simulation failed: {}", e.what())); + } + + if(m_ShouldCancel) + { + 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."); + // Polar coloring (Task 8) is handled in a later task; the optional array (if + // requested) is left created-but-unfilled here and populated next task. return {}; } diff --git a/src/MTRSim/Filters/MTRSimFilter.cpp b/src/MTRSim/Filters/MTRSimFilter.cpp index 2977aa3..199fdec 100644 --- a/src/MTRSim/Filters/MTRSimFilter.cpp +++ b/src/MTRSim/Filters/MTRSimFilter.cpp @@ -161,6 +161,13 @@ IFilter::PreflightResult MTRSimFilter::preflightImpl(const DataStructure& dataSt { return {MakeErrorResult(-13003, fmt::format("Volume Fraction values must sum to 1.0 (got {:.4f}).", vfSum))}; } + for(double v : pVolumeFractions[0]) + { + if(v < 0.0 || v > 1.0) + { + return {MakeErrorResult(-13007, fmt::format("Each Volume Fraction value must be in the range [0, 1] (got {:.4f}).", v))}; + } + } if(pThetaList.size() < numComponents - 1) { return {MakeErrorResult(-13004, fmt::format("Theta List needs at least {} rows (components - 1).", numComponents - 1))}; diff --git a/test/MTRSimTest.cpp b/test/MTRSimTest.cpp index 028e847..a83403e 100644 --- a/test/MTRSimTest.cpp +++ b/test/MTRSimTest.cpp @@ -23,6 +23,8 @@ #include +#include +#include #include using namespace nx::core; @@ -116,6 +118,107 @@ TEST_CASE("MTRSim::MTRSimFilter: Rejects mismatched Volume Fraction column count 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); + // Modest domain: 100x100 = 10000 voxels (Z=0 -> single layer). + 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_VolumeFractions_Key, DynamicTableParameter::ValueType{{0.30, 0.35, 0.35}}); + 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(); From 3c7039220d6beb8d4237e452fa7df8d468c84c1f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 16:00:44 -0400 Subject: [PATCH 12/39] fix(filter): drop dead seed write, reorder VF checks, tidy per review - Remove params.seed = m_InputValues->seed (dead write; rng is seeded directly) - Add clarifying comment noting simulateMTR uses the rng, not SimulationParams::seed - Remove unused #include from MTRSim.cpp - Move per-element VF range check (-13007) before sum check (-13003) so out-of-range values produce the precise diagnostic rather than the misleading sum message - Remove redundant size/spacing/VF args.insertOrAssign calls in execute test that duplicated what MakeValidArgs already sets; keep seed overrides Co-Authored-By: Claude Opus 4.8 (1M context) --- src/MTRSim/Filters/Algorithms/MTRSim.cpp | 3 +-- src/MTRSim/Filters/MTRSimFilter.cpp | 14 +++++++------- test/MTRSimTest.cpp | 5 +---- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.cpp b/src/MTRSim/Filters/Algorithms/MTRSim.cpp index fdd6cc2..6369903 100644 --- a/src/MTRSim/Filters/Algorithms/MTRSim.cpp +++ b/src/MTRSim/Filters/Algorithms/MTRSim.cpp @@ -7,7 +7,6 @@ #include -#include #include #include #include @@ -62,7 +61,7 @@ Result<> MTRSim::operator()() params.dz = m_InputValues->physicalSpacing[2]; params.volumeFractions = m_InputValues->volumeFractions[0]; // 1 row params.thetaList = m_InputValues->thetaList; - params.seed = m_InputValues->seed; + // Note: simulateMTR is driven by the seeded rng passed below; SimulationParams::seed is not consulted. if(m_ShouldCancel) { diff --git a/src/MTRSim/Filters/MTRSimFilter.cpp b/src/MTRSim/Filters/MTRSimFilter.cpp index 199fdec..e63d3d6 100644 --- a/src/MTRSim/Filters/MTRSimFilter.cpp +++ b/src/MTRSim/Filters/MTRSimFilter.cpp @@ -152,6 +152,13 @@ IFilter::PreflightResult MTRSimFilter::preflightImpl(const DataStructure& dataSt { return {MakeErrorResult(-13002, 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(-13007, 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]) { @@ -161,13 +168,6 @@ IFilter::PreflightResult MTRSimFilter::preflightImpl(const DataStructure& dataSt { return {MakeErrorResult(-13003, fmt::format("Volume Fraction values must sum to 1.0 (got {:.4f}).", vfSum))}; } - for(double v : pVolumeFractions[0]) - { - if(v < 0.0 || v > 1.0) - { - return {MakeErrorResult(-13007, fmt::format("Each Volume Fraction value must be in the range [0, 1] (got {:.4f}).", v))}; - } - } if(pThetaList.size() < numComponents - 1) { return {MakeErrorResult(-13004, fmt::format("Theta List needs at least {} rows (components - 1).", numComponents - 1))}; diff --git a/test/MTRSimTest.cpp b/test/MTRSimTest.cpp index a83403e..355dfd0 100644 --- a/test/MTRSimTest.cpp +++ b/test/MTRSimTest.cpp @@ -133,10 +133,7 @@ TEST_CASE("MTRSim::MTRSimFilter: Execute wires simulation to output arrays", "[M MTRSimFilter filter; Arguments args = MakeValidArgs(compPaths); - // Modest domain: 100x100 = 10000 voxels (Z=0 -> single layer). - 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_VolumeFractions_Key, DynamicTableParameter::ValueType{{0.30, 0.35, 0.35}}); + // size/spacing/VF come from MakeValidArgs (100x100 grid) args.insertOrAssign(MTRSimFilter::k_UseSeed_Key, true); args.insertOrAssign(MTRSimFilter::k_SeedValue_Key, static_cast(42)); From 0f46063710ad7a56fc7a9d902cc158b3d74377dc Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 16:03:33 -0400 Subject: [PATCH 13/39] ENH: Add optional polar coloring output to MTRSimFilter * Implement MTRSim::applyPolarColoring using IPFMapper with the MatLab HCP colour scheme and Z-axis reference direction * Call applyPolarColoring from operator() when generatePolarColoring is true; short-circuits on error; leaves no array when the flag is false * Add two unit tests: polar ON verifies array shape (3 components, 10000 tuples) and non-zero content; polar OFF asserts array is absent Signed-off-by: Michael Jackson --- src/MTRSim/Filters/Algorithms/MTRSim.cpp | 37 ++++++++++++- src/MTRSim/Filters/Algorithms/MTRSim.hpp | 3 ++ test/MTRSimTest.cpp | 66 ++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.cpp b/src/MTRSim/Filters/Algorithms/MTRSim.cpp index 6369903..3cbc4a6 100644 --- a/src/MTRSim/Filters/Algorithms/MTRSim.cpp +++ b/src/MTRSim/Filters/Algorithms/MTRSim.cpp @@ -3,8 +3,10 @@ #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "LibMTRSim/IPFMapper.hpp" #include "LibMTRSim/MTRSimDriver.hpp" +#include #include #include @@ -102,7 +104,38 @@ Result<> MTRSim::operator()() } m_MessageHandler(IFilter::Message::Type::Info, "MTR simulation complete."); - // Polar coloring (Task 8) is handled in a later task; the optional array (if - // requested) is left created-but-unfilled here and populated next task. + + 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 index 85f42f1..f6c2a72 100644 --- a/src/MTRSim/Filters/Algorithms/MTRSim.hpp +++ b/src/MTRSim/Filters/Algorithms/MTRSim.hpp @@ -6,6 +6,8 @@ #include "simplnx/DataStructure/DataStructure.hpp" #include "simplnx/Filter/IFilter.hpp" +#include "LibMTRSim/MTRSimDriver.hpp" + #include namespace nx::core @@ -48,6 +50,7 @@ class MTRSIM_EXPORT MTRSim 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; diff --git a/test/MTRSimTest.cpp b/test/MTRSimTest.cpp index 355dfd0..66586a1 100644 --- a/test/MTRSimTest.cpp +++ b/test/MTRSimTest.cpp @@ -248,6 +248,72 @@ TEST_CASE("MTRSim::MTRSimFilter: Rejects Volume Fraction not summing to 1.0", "[ 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(); From 4f41e57d6bf0e60e01266dc5e0d456e437d45f9f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 16:08:58 -0400 Subject: [PATCH 14/39] docs: add MTRSimFilter (Generate Synthetic Microtexture) documentation Documents all parameters, output arrays, preflight error codes, the units consistency requirement for Physical Size / Spacing / Theta List, and the expected single-stage progress behaviour for large volumes. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/MTRSimFilter.md | 81 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/MTRSimFilter.md diff --git a/docs/MTRSimFilter.md b/docs/MTRSimFilter.md new file mode 100644 index 0000000..122140b --- /dev/null +++ b/docs/MTRSimFilter.md @@ -0,0 +1,81 @@ +# 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. + +The algorithm reproduces the behavior of the MATLAB MTRSim research code (`matlab/simulateMTR.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. + +### Algorithm Overview + +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 as a single monolithic step 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 at the start of the simulation and again upon completion; intermediate cancellation is checked only at the boundaries of major stages, not inside the inner loop. This is expected behavior. + +## 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 | +| --- | --- | +| `-13001` | (Preflight) Fewer than 2 ODF component arrays were selected. MTRSim requires at least 2 components. | +| `-13002` | (Preflight) The **Volume Fraction** table must have exactly 1 row and exactly one column per ODF component. | +| `-13007` | (Preflight) One or more Volume Fraction values is outside the range [0, 1]. | +| `-13003` | (Preflight) Volume Fraction values do not sum to 1.0 (tolerance: 1 × 10⁻³). | +| `-13004` | (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. | +| `-13005` | (Preflight) A row in the **Theta List** does not have exactly 3 columns (`theta_x`, `theta_y`, `theta_z`). | +| `-13006` | (Preflight) Physical Spacing X or Y is ≤ 0. Both must be strictly positive; the Z spacing is unused when Physical Size Z ≤ 0. | +| `-13050` | (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 will be added in a future release.) + +## 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. From dea78561db210e044c5e20b58415015549b94395 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 16:10:38 -0400 Subject: [PATCH 15/39] docs: record AK plan execution corrections; ignore CTest Testing/ dir Plan updated with the Approx (not Catch::Approx) and src/LibMTRSim/CMakeLists.txt corrections discovered during execution. --- .gitignore | 1 + docs/MTRSimFilter.md | 16 +- .../plans/2026-06-01-milestone-ak.md | 52 +- src/LibMTRSim/IPFMapper.hpp | 6 +- src/LibMTRSim/MTRSimDriver.cpp | 47 +- src/LibMTRSim/MTRSimDriver.hpp | 39 +- src/LibMTRSim/ODFBuilder.cpp | 111 +- src/LibMTRSim/ODFBuilder.hpp | 24 +- src/LibMTRSim/ODFFileIO.cpp | 478 ++++--- src/LibMTRSim/ODFFileIO.hpp | 51 +- src/MTRSim/Filters/Algorithms/ComputeODF.cpp | 339 ++--- src/MTRSim/Filters/Algorithms/ComputeODF.hpp | 30 +- src/MTRSim/Filters/Algorithms/MTRSim.cpp | 105 +- src/MTRSim/Filters/Algorithms/MTRSim.hpp | 37 +- .../Filters/Algorithms/ReadMTRSimODF.cpp | 66 +- .../Filters/Algorithms/ReadMTRSimODF.hpp | 30 +- .../Filters/Algorithms/WriteMTRSimODF.cpp | 64 +- .../Filters/Algorithms/WriteMTRSimODF.hpp | 30 +- src/MTRSim/Filters/ComputeODFFilter.cpp | 541 ++++---- src/MTRSim/Filters/ComputeODFFilter.hpp | 97 +- src/MTRSim/Filters/MTRSimFilter.cpp | 312 +++-- src/MTRSim/Filters/MTRSimFilter.hpp | 101 +- src/MTRSim/Filters/ReadMTRSimODFFilter.cpp | 181 +-- src/MTRSim/Filters/ReadMTRSimODFFilter.hpp | 80 +- src/MTRSim/Filters/WriteMTRSimODFFilter.cpp | 218 ++-- src/MTRSim/Filters/WriteMTRSimODFFilter.hpp | 77 +- src/MTRSim/MTRSimPlugin.cpp | 17 +- src/MTRSim/MTRSimPlugin.hpp | 11 +- src/MTRSim/libmtrsim_export.h | 5 +- src/app/main.cpp | 15 +- test/ComputeODFTest.cpp | 1093 ++++++++++------- test/MTRSimTest.cpp | 209 ++-- test/MTRSimTestUtils.hpp | 1 - test/ReadMTRSimODFTest.cpp | 182 ++- test/WriteMTRSimODFTest.cpp | 272 ++-- tests/test_mtrsim_driver.cpp | 51 +- tests/test_odf_builder.cpp | 38 +- tests/test_odf_file_io.cpp | 48 +- 38 files changed, 2931 insertions(+), 2144 deletions(-) diff --git a/.gitignore b/.gitignore index 42cebde..32dc599 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ CLAUDE.md *.mat /docs/superpowers +/Testing diff --git a/docs/MTRSimFilter.md b/docs/MTRSimFilter.md index 122140b..448ea15 100644 --- a/docs/MTRSimFilter.md +++ b/docs/MTRSimFilter.md @@ -57,14 +57,14 @@ A scalar `UInt64` array (default name `MTRSim SeedValue`) is created at the top | Code | Meaning | | --- | --- | -| `-13001` | (Preflight) Fewer than 2 ODF component arrays were selected. MTRSim requires at least 2 components. | -| `-13002` | (Preflight) The **Volume Fraction** table must have exactly 1 row and exactly one column per ODF component. | -| `-13007` | (Preflight) One or more Volume Fraction values is outside the range [0, 1]. | -| `-13003` | (Preflight) Volume Fraction values do not sum to 1.0 (tolerance: 1 × 10⁻³). | -| `-13004` | (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. | -| `-13005` | (Preflight) A row in the **Theta List** does not have exactly 3 columns (`theta_x`, `theta_y`, `theta_z`). | -| `-13006` | (Preflight) Physical Spacing X or Y is ≤ 0. Both must be strictly positive; the Z spacing is unused when Physical Size Z ≤ 0. | -| `-13050` | (Execute) The core MTR simulation threw an unexpected exception. The error message includes the underlying cause. | +| `-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. | +| `-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 diff --git a/docs/superpowers/plans/2026-06-01-milestone-ak.md b/docs/superpowers/plans/2026-06-01-milestone-ak.md index f228100..9cc29cf 100644 --- a/docs/superpowers/plans/2026-06-01-milestone-ak.md +++ b/docs/superpowers/plans/2026-06-01-milestone-ak.md @@ -6,7 +6,11 @@ **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 (note: `Catch::Approx`, not `Approx`), CMake, GitHub Actions. +**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`). --- @@ -69,7 +73,7 @@ simplnx checkout is at `/Users/mjackson/Workspace7/simplnx`): - `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`. -- `LibMTRSim.cmake` — add `MTRSimDriver.{hpp,cpp}` to the standalone library. +- `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`. @@ -194,13 +198,13 @@ TEST_CASE("buildUniformODF produces correct bin centres", "[mtrsim_driver]") { REQUIRE(uni.phi1Bins.size() == 72 * 36 * 72); // Uniform mass: every bin equal, sums to 1. - REQUIRE(uni.odfVal.sum() == Catch::Approx(1.0)); - REQUIRE(uni.odfVal[0] == Catch::Approx(1.0 / (72.0 * 36.0 * 72.0))); + 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] == Catch::Approx(0.5 * 2.0 * std::numbers::pi / 72.0)); - REQUIRE(uni.phiBins[0] == Catch::Approx(0.5 * std::numbers::pi / 36.0)); - REQUIRE(uni.phi2Bins[0] == Catch::Approx(0.5 * 2.0 * std::numbers::pi / 72.0)); + 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)); } ``` @@ -266,12 +270,12 @@ TEST_CASE("gridToODFComponent derives bin centres in radians and normalizes", "[ mtrsim::gridToODFComponent(values, n1, nPHI, n2, 5.0, 5.0, 5.0); REQUIRE(c.odfVal.size() == n1 * nPHI * n2); - REQUIRE(c.odfVal.sum() == Catch::Approx(1.0)); // normalized + 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] == Catch::Approx(2.5 * deg2rad)); - REQUIRE(c.phiBins[0] == Catch::Approx(2.5 * deg2rad)); - REQUIRE(c.phi2Bins[0] == Catch::Approx(2.5 * deg2rad)); + REQUIRE(c.phi1Bins[0] == Approx(2.5 * deg2rad)); + REQUIRE(c.phiBins[0] == Approx(2.5 * deg2rad)); + REQUIRE(c.phi2Bins[0] == Approx(2.5 * deg2rad)); } ``` @@ -569,9 +573,9 @@ TEST_CASE("simulateMTR reproduces target volume fractions (statistical)", "[mtrs // 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 == Catch::Approx(0.30).margin(0.05)); - REQUIRE(static_cast(counts[1]) / N == Catch::Approx(0.35).margin(0.05)); - REQUIRE(static_cast(counts[2]) / N == Catch::Approx(0.35).margin(0.05)); + 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); } @@ -821,31 +825,31 @@ IFilter::PreflightResult MTRSimFilter::preflightImpl(const DataStructure& dataSt const usize numComponents = pOdfArrays.size(); if (numComponents < 2) { - return {MakeErrorResult(-13001, "MTRSim requires at least 2 ODF component arrays.")}; + 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(-13002, fmt::format("Volume Fraction must be 1 row x {} columns (one per ODF component).", 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(-13003, fmt::format("Volume Fraction values must sum to 1.0 (got {:.4f}).", vfSum))}; + 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(-13004, fmt::format("Theta List needs at least {} rows (components - 1).", 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(-13005, "Each Theta List row must have exactly 3 columns.")}; + return {MakeErrorResult(-13505, "Each Theta List row must have exactly 3 columns.")}; } } @@ -1005,8 +1009,8 @@ TEST_CASE("MTRSimFilter: preflight rejects mismatched volume fraction count", "[ 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 -13004), VF sum ≠ 1 -(error -13003). Each follows the same builder with one field changed. +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** @@ -1090,7 +1094,7 @@ Result<> MTRSim::operator()() sim = mtrsim::simulateMTR(params, components, rng, n1, nPHI, n2); } catch (const std::exception& e) { - return MakeErrorResult(-13050, fmt::format("MTR simulation failed: {}", e.what())); + return MakeErrorResult(-13550, fmt::format("MTR simulation failed: {}", e.what())); } if (m_ShouldCancel) { return {}; } @@ -1166,8 +1170,8 @@ TEST_CASE("MTRSimFilter: execute produces valid MTR ids + Eulers", "[MTRSim]") counts[v - 1]++; } const double n = static_cast(ids.getNumberOfTuples()); - REQUIRE(counts[0] / n == Catch::Approx(0.30).margin(0.06)); - REQUIRE(counts[1] / n == Catch::Approx(0.35).margin(0.06)); + 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"})); diff --git a/src/LibMTRSim/IPFMapper.hpp b/src/LibMTRSim/IPFMapper.hpp index 766f79a..ae64d28 100644 --- a/src/LibMTRSim/IPFMapper.hpp +++ b/src/LibMTRSim/IPFMapper.hpp @@ -17,8 +17,10 @@ namespace mtrsim { * 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) + HCP, ///< Hexagonal close-packed (maps to + ///< ebsdlib::CrystalStructure::Hexagonal_High) + FCC, ///< Face-centred cubic (maps to + ///< ebsdlib::CrystalStructure::Cubic_High) }; /** diff --git a/src/LibMTRSim/MTRSimDriver.cpp b/src/LibMTRSim/MTRSimDriver.cpp index 09cec3c..3497197 100644 --- a/src/LibMTRSim/MTRSimDriver.cpp +++ b/src/LibMTRSim/MTRSimDriver.cpp @@ -30,14 +30,17 @@ ODFComponent buildUniformODF(int n1, int nPHI, int n2) { } ODFComponent uni; - uni.odfVal = Eigen::VectorXd::Constant(nTotal, 1.0 / static_cast(nTotal)); + 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) { +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; @@ -68,17 +71,19 @@ ODFComponent gridToODFComponent(const std::vector& values, int n1, int n return c; } -MTRSimResult simulateMTR(const SimulationParams& params, - const std::vector& odfComponents, - std::mt19937_64& rng, - int n1, int nPHI, int n2) { +MTRSimResult simulateMTR(const SimulationParams ¶ms, + 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 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"); + 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). @@ -86,16 +91,19 @@ MTRSimResult simulateMTR(const SimulationParams& params, const PGRFResult pgrf_result = pgrf.run(params); // throws on bad dims if (static_cast(pgrf_result.mtrIndex.size()) != N) { - throw std::runtime_error("simulateMTR: PGRF result size does not match grid dimensions"); + 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)); + 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); + orientSamples[static_cast(j)] = sampler.sampleN( + N, odfComponents[static_cast(j)], uniformOdf); } // 3. Assign per-voxel orientation by component (sim order). @@ -106,9 +114,12 @@ MTRSimResult simulateMTR(const SimulationParams& params, 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); + 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. @@ -117,9 +128,9 @@ MTRSimResult simulateMTR(const SimulationParams& params, 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); + out.phi1 = remapSimToZYX(phi1Sim, nx, ny, nz); + out.phi = remapSimToZYX(phiSim, nx, ny, nz); + out.phi2 = remapSimToZYX(phi2Sim, nx, ny, nz); return out; } diff --git a/src/LibMTRSim/MTRSimDriver.hpp b/src/LibMTRSim/MTRSimDriver.hpp index 30d20a4..ab02a16 100644 --- a/src/LibMTRSim/MTRSimDriver.hpp +++ b/src/LibMTRSim/MTRSimDriver.hpp @@ -2,7 +2,7 @@ #include "libmtrsim_export.h" -#include "ODFSampler.hpp" // mtrsim::ODFComponent, mtrsim::EulerAngles +#include "ODFSampler.hpp" // mtrsim::ODFComponent, mtrsim::EulerAngles #include "SimulationParams.hpp" #include @@ -28,12 +28,15 @@ 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). + * 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); +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. @@ -45,13 +48,15 @@ LIBMTRSIM_EXPORT ODFComponent gridToODFComponent(const std::vector& valu * @tparam T element type (int or double). */ template -std::vector remapSimToZYX(const std::vector& in, int nx, int ny, int nz) { +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; + 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]; } } @@ -66,10 +71,10 @@ 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 + 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 }; /** @@ -77,14 +82,16 @@ struct LIBMTRSIM_EXPORT MTRSimResult { * 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 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); +LIBMTRSIM_EXPORT MTRSimResult +simulateMTR(const SimulationParams ¶ms, + const std::vector &odfComponents, + std::mt19937_64 &rng, int n1, int nPHI, int n2); } // namespace mtrsim diff --git a/src/LibMTRSim/ODFBuilder.cpp b/src/LibMTRSim/ODFBuilder.cpp index fb6c7e8..e7b1459 100644 --- a/src/LibMTRSim/ODFBuilder.cpp +++ b/src/LibMTRSim/ODFBuilder.cpp @@ -5,11 +5,9 @@ #include #include -namespace mtrsim -{ +namespace mtrsim { -namespace -{ +namespace { // ----------------------------------------------------------------------------- // MATLAB calc_ODF.m tri-linear smoothing weights. @@ -21,85 +19,90 @@ constexpr double k_EdgeWeight = 0.16 / 12.0; constexpr double k_CornerWeight = 0.06 / 8.0; // 6 face neighbors: +/-1 along exactly one axis. -constexpr int k_FaceOffsets[6][3] = {{-1, 0, 0}, {+1, 0, 0}, {0, -1, 0}, {0, +1, 0}, {0, 0, -1}, {0, 0, +1}}; +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 /// calc_ODF.m's jf_minus/jf_plus/kf_minus/kf_plus/lf_minus/lf_plus logic /// (lines 95-113), which IS uniform modulo on every axis. -inline int32_t wrap(int32_t i, int32_t n) -{ - return ((i % n) + n) % n; -} +inline int32_t wrap(int32_t i, int32_t n) { return ((i % n) + n) % n; } /// Clamp a bin index to [0, n-1]. Matches MATLAB calc_ODF.m's upper-bound /// clamping for bin assignment (lines 43-48): a sample exactly at the upper /// axis bound (2*pi for phi1/phi2, pi for PHI) goes to the LAST bin, not /// wrap-around to bin 0. Negative inputs (which shouldn't occur for /// non-negative Bunge angles) defensively wrap via wrap(). -inline int32_t clampBin(int32_t i, int32_t n) -{ - if(i < 0) - { +inline int32_t clampBin(int32_t i, int32_t n) { + if (i < 0) { return wrap(i, n); } - if(i >= n) - { + if (i >= n) { return n - 1; } return i; } /// 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); +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); } } // namespace -void accumulate(const std::vector>& eulersRad, const ODFBuildParams& params, std::vector& values) -{ - const std::size_t expectedSize = static_cast(params.nphi1) * static_cast(params.nPHI) * static_cast(params.nphi2); - if(values.size() != expectedSize) - { +void accumulate(const std::vector> &eulersRad, + const ODFBuildParams ¶ms, std::vector &values) { + const std::size_t expectedSize = static_cast(params.nphi1) * + static_cast(params.nPHI) * + static_cast(params.nphi2); + if (values.size() != expectedSize) { throw std::invalid_argument("ODFBuilder::accumulate: values size mismatch"); } constexpr double k_RadToDeg = 180.0 / std::numbers::pi; - for(const auto& tuple : eulersRad) - { + for (const auto &tuple : eulersRad) { const double phi1Deg = tuple[0] * k_RadToDeg; 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). - 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); - - if(!params.smoothing) - { + // 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); + + if (!params.smoothing) { values[linearize(iPhi1, iPHI, iPhi2, params.nPHI, params.nphi2)] += 1.0; continue; } // Center. - values[linearize(iPhi1, iPHI, iPhi2, params.nPHI, params.nphi2)] += k_CenterWeight; + values[linearize(iPhi1, iPHI, iPhi2, params.nPHI, params.nphi2)] += + k_CenterWeight; // Faces (6). - for(const auto& offs : k_FaceOffsets) - { + for (const auto &offs : k_FaceOffsets) { const int32_t jj = wrap(iPhi1 + offs[0], params.nphi1); const int32_t kk = wrap(iPHI + offs[1], params.nPHI); const int32_t ll = wrap(iPhi2 + offs[2], params.nphi2); @@ -107,8 +110,7 @@ void accumulate(const std::vector>& eulersRad, const ODFBu } // Edges (12). - for(const auto& offs : k_EdgeOffsets) - { + for (const auto &offs : k_EdgeOffsets) { const int32_t jj = wrap(iPhi1 + offs[0], params.nphi1); const int32_t kk = wrap(iPHI + offs[1], params.nPHI); const int32_t ll = wrap(iPhi2 + offs[2], params.nphi2); @@ -116,30 +118,25 @@ void accumulate(const std::vector>& eulersRad, const ODFBu } // Corners (8): +/-1 along all three axes. - for(int dj = -1; dj <= 1; dj += 2) - { - for(int dk = -1; dk <= 1; dk += 2) - { - for(int dl = -1; dl <= 1; dl += 2) - { + for (int dj = -1; dj <= 1; dj += 2) { + for (int dk = -1; dk <= 1; dk += 2) { + for (int dl = -1; dl <= 1; dl += 2) { const int32_t jj = wrap(iPhi1 + dj, params.nphi1); const int32_t kk = wrap(iPHI + dk, params.nPHI); const int32_t ll = wrap(iPhi2 + dl, params.nphi2); - values[linearize(jj, kk, ll, params.nPHI, params.nphi2)] += k_CornerWeight; + values[linearize(jj, kk, ll, params.nPHI, params.nphi2)] += + k_CornerWeight; } } } } } -void normalize(std::vector& values, double normalizer) -{ - if(normalizer == 0.0) - { +void normalize(std::vector &values, double normalizer) { + if (normalizer == 0.0) { return; } - for(auto& v : values) - { + for (auto &v : values) { v /= normalizer; } } diff --git a/src/LibMTRSim/ODFBuilder.hpp b/src/LibMTRSim/ODFBuilder.hpp index 097308b..904b293 100644 --- a/src/LibMTRSim/ODFBuilder.hpp +++ b/src/LibMTRSim/ODFBuilder.hpp @@ -6,8 +6,7 @@ #include #include -namespace mtrsim -{ +namespace mtrsim { /** * @brief Discretization parameters used by ODFBuilder::accumulate. @@ -16,13 +15,14 @@ namespace mtrsim * linear = i_phi1 * (nPHI * nphi2) + i_PHI * nphi2 + i_phi2 * (phi1 is the slowest-varying axis, phi2 the fastest). */ -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). +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). }; /** @@ -41,7 +41,9 @@ struct LIBMTRSIM_EXPORT ODFBuildParams * @throws std::invalid_argument if @p values.size() does not match * nphi1 * nPHI * nphi2. */ -LIBMTRSIM_EXPORT void accumulate(const std::vector>& eulersRad, const ODFBuildParams& params, std::vector& values); +LIBMTRSIM_EXPORT void +accumulate(const std::vector> &eulersRad, + const ODFBuildParams ¶ms, std::vector &values); /** * @brief In-place division: values[i] /= normalizer. @@ -49,6 +51,6 @@ LIBMTRSIM_EXPORT void accumulate(const std::vector>& euler * No-op when @p normalizer equals 0.0 (avoids divide-by-zero blow-ups from * empty accumulators). */ -LIBMTRSIM_EXPORT void normalize(std::vector& values, double normalizer); +LIBMTRSIM_EXPORT void normalize(std::vector &values, double normalizer); } // namespace mtrsim diff --git a/src/LibMTRSim/ODFFileIO.cpp b/src/LibMTRSim/ODFFileIO.cpp index a108766..161992f 100644 --- a/src/LibMTRSim/ODFFileIO.cpp +++ b/src/LibMTRSim/ODFFileIO.cpp @@ -11,41 +11,28 @@ #include #include -namespace mtrsim -{ +namespace mtrsim { -namespace -{ +namespace { // ───────────────────────────────────────────────────────────────────────────── // RAII closers for HDF5 handles. Each holds an hid_t, closes on destruction // if still open, and exposes release()/reset() for explicit hand-off. // ───────────────────────────────────────────────────────────────────────────── -template -class H5Handle -{ +template class H5Handle { public: H5Handle() = default; - explicit H5Handle(hid_t id) - : m_Id(id) - { - } + explicit H5Handle(hid_t id) : m_Id(id) {} - H5Handle(const H5Handle&) = delete; - H5Handle& operator=(const H5Handle&) = delete; + H5Handle(const H5Handle &) = delete; + H5Handle &operator=(const H5Handle &) = delete; - H5Handle(H5Handle&& other) noexcept - : m_Id(other.m_Id) - { - other.m_Id = -1; - } + H5Handle(H5Handle &&other) noexcept : m_Id(other.m_Id) { other.m_Id = -1; } - H5Handle& operator=(H5Handle&& other) noexcept - { - if(this != &other) - { + H5Handle &operator=(H5Handle &&other) noexcept { + if (this != &other) { reset(); m_Id = other.m_Id; other.m_Id = -1; @@ -53,29 +40,18 @@ class H5Handle return *this; } - ~H5Handle() - { - reset(); - } + ~H5Handle() { reset(); } - void reset(hid_t newId = -1) - { - if(m_Id >= 0) - { + void reset(hid_t newId = -1) { + if (m_Id >= 0) { CloseFn(m_Id); } m_Id = newId; } - hid_t get() const - { - return m_Id; - } + hid_t get() const { return m_Id; } - bool valid() const - { - return m_Id >= 0; - } + bool valid() const { return m_Id >= 0; } private: hid_t m_Id{-1}; @@ -94,123 +70,113 @@ using DataspaceHandle = H5Handle<&H5Sclose>; // of an expected failure path (e.g. probing a group that we know might not // exist). // ───────────────────────────────────────────────────────────────────────────── -class H5ErrorSuppressor -{ +class H5ErrorSuppressor { public: - H5ErrorSuppressor() - { + H5ErrorSuppressor() { H5Eget_auto2(H5E_DEFAULT, &m_OldFunc, &m_OldClientData); H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr); } - ~H5ErrorSuppressor() - { + ~H5ErrorSuppressor() { H5Eset_auto2(H5E_DEFAULT, m_OldFunc, m_OldClientData); } - H5ErrorSuppressor(const H5ErrorSuppressor&) = delete; - H5ErrorSuppressor(H5ErrorSuppressor&&) = delete; - H5ErrorSuppressor& operator=(const H5ErrorSuppressor&) = delete; - H5ErrorSuppressor& operator=(H5ErrorSuppressor&&) = delete; + H5ErrorSuppressor(const H5ErrorSuppressor &) = delete; + H5ErrorSuppressor(H5ErrorSuppressor &&) = delete; + H5ErrorSuppressor &operator=(const H5ErrorSuppressor &) = delete; + H5ErrorSuppressor &operator=(H5ErrorSuppressor &&) = delete; private: H5E_auto2_t m_OldFunc = nullptr; - void* m_OldClientData = nullptr; + void *m_OldClientData = nullptr; }; // ───────────────────────────────────────────────────────────────────────────── // Low-level HDF5 read/write helpers // ───────────────────────────────────────────────────────────────────────────── -int64_t readScalarInt64(hid_t loc, const std::string& path) -{ +int64_t readScalarInt64(hid_t loc, const std::string &path) { DatasetHandle ds{H5Dopen2(loc, path.c_str(), H5P_DEFAULT)}; - if(!ds.valid()) - { + if (!ds.valid()) { throw std::runtime_error("HDF5: cannot open dataset: " + path); } int64_t value = 0; - if(H5Dread(ds.get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT, &value) < 0) - { - throw std::runtime_error("HDF5: failed to read scalar int64 dataset: " + path); + if (H5Dread(ds.get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT, + &value) < 0) { + throw std::runtime_error("HDF5: failed to read scalar int64 dataset: " + + path); } return value; } -std::vector readDoubleVector(hid_t loc, const std::string& path) -{ +std::vector readDoubleVector(hid_t loc, const std::string &path) { DatasetHandle ds{H5Dopen2(loc, path.c_str(), H5P_DEFAULT)}; - if(!ds.valid()) - { + if (!ds.valid()) { throw std::runtime_error("HDF5: cannot open dataset: " + path); } DataspaceHandle space{H5Dget_space(ds.get())}; - if(!space.valid()) - { + if (!space.valid()) { throw std::runtime_error("HDF5: cannot get dataspace for dataset: " + path); } const int ndims = H5Sget_simple_extent_ndims(space.get()); - if(ndims < 0) - { + if (ndims < 0) { throw std::runtime_error("HDF5: failed to get rank of dataset: " + path); } - if(ndims < 1) - { + if (ndims < 1) { throw std::runtime_error("HDF5: dataset has invalid rank: " + path); } std::vector dims(static_cast(ndims)); - if(H5Sget_simple_extent_dims(space.get(), dims.data(), nullptr) < 0) - { + if (H5Sget_simple_extent_dims(space.get(), dims.data(), nullptr) < 0) { throw std::runtime_error("HDF5: failed to read dims of dataset: " + path); } hsize_t total = 1; - for(hsize_t d : dims) - { + for (hsize_t d : dims) { total *= d; } std::vector buf(total); - if(H5Dread(ds.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, buf.data()) < 0) - { + if (H5Dread(ds.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, + buf.data()) < 0) { throw std::runtime_error("HDF5: failed to read double dataset: " + path); } return buf; } -void writeScalarInt64(hid_t loc, const std::string& name, int64_t value) -{ +void writeScalarInt64(hid_t loc, const std::string &name, int64_t value) { DataspaceHandle space{H5Screate(H5S_SCALAR)}; - if(!space.valid()) - { - throw std::runtime_error("HDF5: failed to create scalar dataspace for: " + name); + if (!space.valid()) { + throw std::runtime_error("HDF5: failed to create scalar dataspace for: " + + name); } - DatasetHandle ds{H5Dcreate2(loc, name.c_str(), H5T_NATIVE_INT64, space.get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)}; - if(!ds.valid()) - { - throw std::runtime_error("HDF5: failed to create scalar int64 dataset: " + name); + DatasetHandle ds{H5Dcreate2(loc, name.c_str(), H5T_NATIVE_INT64, space.get(), + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)}; + if (!ds.valid()) { + throw std::runtime_error("HDF5: failed to create scalar int64 dataset: " + + name); } - if(H5Dwrite(ds.get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT, &value) < 0) - { - throw std::runtime_error("HDF5: failed to write scalar int64 dataset: " + name); + if (H5Dwrite(ds.get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT, + &value) < 0) { + throw std::runtime_error("HDF5: failed to write scalar int64 dataset: " + + name); } } -void writeDoubleVector(hid_t loc, const std::string& name, const std::vector& buf) -{ +void writeDoubleVector(hid_t loc, const std::string &name, + const std::vector &buf) { const hsize_t dims[1] = {static_cast(buf.size())}; DataspaceHandle space{H5Screate_simple(1, dims, nullptr)}; - if(!space.valid()) - { - throw std::runtime_error("HDF5: failed to create simple dataspace for: " + name); + if (!space.valid()) { + throw std::runtime_error("HDF5: failed to create simple dataspace for: " + + name); } - DatasetHandle ds{H5Dcreate2(loc, name.c_str(), H5T_NATIVE_DOUBLE, space.get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)}; - if(!ds.valid()) - { + DatasetHandle ds{H5Dcreate2(loc, name.c_str(), H5T_NATIVE_DOUBLE, space.get(), + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)}; + if (!ds.valid()) { throw std::runtime_error("HDF5: failed to create double dataset: " + name); } - if(H5Dwrite(ds.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, buf.data()) < 0) - { + if (H5Dwrite(ds.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, + buf.data()) < 0) { throw std::runtime_error("HDF5: failed to write double dataset: " + name); } } @@ -224,28 +190,28 @@ constexpr double k_SpacingTolRad = 1.0e-12; // Verify a bin-edge array is strictly monotonically increasing and uniformly // spaced (step consistent within k_SpacingTolRad). Returns the uniform step in // radians. Throws on any violation. -double validateUniformBins(const std::vector& edges, const std::string& axisName) -{ - if(edges.size() < 2) - { - throw std::runtime_error("ODFFileIO: bin-edge array too short on axis: " + axisName); +double validateUniformBins(const std::vector &edges, + const std::string &axisName) { + if (edges.size() < 2) { + throw std::runtime_error("ODFFileIO: bin-edge array too short on axis: " + + axisName); } const double step0 = edges[1] - edges[0]; - if(step0 <= 0.0) - { - throw std::runtime_error("ODFFileIO: bin edges not strictly increasing on axis: " + axisName); + if (step0 <= 0.0) { + throw std::runtime_error( + "ODFFileIO: bin edges not strictly increasing on axis: " + axisName); } - for(std::size_t i = 1; i + 1 < edges.size(); ++i) - { + for (std::size_t i = 1; i + 1 < edges.size(); ++i) { const double step = edges[i + 1] - edges[i]; - if(step <= 0.0) - { - throw std::runtime_error("ODFFileIO: bin edges not strictly increasing on axis: " + axisName); + if (step <= 0.0) { + throw std::runtime_error( + "ODFFileIO: bin edges not strictly increasing on axis: " + axisName); } - if(std::abs(step - step0) > k_SpacingTolRad) - { + if (std::abs(step - step0) > k_SpacingTolRad) { std::ostringstream oss; - oss << "ODFFileIO: non-uniform bin spacing on axis " << axisName << " at index " << i << " (step=" << step << ", expected=" << step0 << ", tol=" << k_SpacingTolRad << ")"; + oss << "ODFFileIO: non-uniform bin spacing on axis " << axisName + << " at index " << i << " (step=" << step << ", expected=" << step0 + << ", tol=" << k_SpacingTolRad << ")"; throw std::runtime_error(oss.str()); } } @@ -253,10 +219,8 @@ double validateUniformBins(const std::vector& edges, const std::string& } // Byte-exact compare of two double arrays. Sizes must match first. -bool byteEqual(const std::vector& a, const std::vector& b) -{ - if(a.size() != b.size()) - { +bool byteEqual(const std::vector &a, const std::vector &b) { + if (a.size() != b.size()) { return false; } return std::memcmp(a.data(), b.data(), a.size() * sizeof(double)) == 0; @@ -272,37 +236,31 @@ bool byteEqual(const std::vector& a, const std::vector& b) // "ODF_best" -> "/ODF_best" // "/ODF_best/" -> "/ODF_best" // "/a/b/" -> "/a/b" -std::string normalizePathPrefix(const std::string& prefix) -{ - if(prefix.empty()) - { +std::string normalizePathPrefix(const std::string &prefix) { + if (prefix.empty()) { return "/"; } std::string out = prefix; - if(out.front() != '/') - { + if (out.front() != '/') { out.insert(out.begin(), '/'); } - while(out.size() > 1 && out.back() == '/') - { + while (out.size() > 1 && out.back() == '/') { out.pop_back(); } return out; } -std::string componentGroupName(const std::string& normalizedPrefix, int64_t idx) -{ - if(normalizedPrefix == "/") - { +std::string componentGroupName(const std::string &normalizedPrefix, + int64_t idx) { + if (normalizedPrefix == "/") { return "/component_" + std::to_string(idx); } return normalizedPrefix + "/component_" + std::to_string(idx); } -std::string joinPath(const std::string& normalizedPrefix, const std::string& leaf) -{ - if(normalizedPrefix == "/") - { +std::string joinPath(const std::string &normalizedPrefix, + const std::string &leaf) { + if (normalizedPrefix == "/") { return "/" + leaf; } return normalizedPrefix + "/" + leaf; @@ -314,11 +272,11 @@ std::string joinPath(const std::string& normalizedPrefix, const std::string& lea // Public API // ───────────────────────────────────────────────────────────────────────────── -ODFFileMetadata readODFMetadata(const std::filesystem::path& file, const std::string& pathPrefix) -{ - if(!std::filesystem::exists(file)) - { - throw std::runtime_error("ODFFileIO: file does not exist: " + file.string()); +ODFFileMetadata readODFMetadata(const std::filesystem::path &file, + const std::string &pathPrefix) { + if (!std::filesystem::exists(file)) { + throw std::runtime_error("ODFFileIO: file does not exist: " + + file.string()); } const std::string prefix = normalizePathPrefix(pathPrefix); @@ -328,43 +286,41 @@ ODFFileMetadata readODFMetadata(const std::filesystem::path& file, const std::st H5ErrorSuppressor suppressErrors; FileHandle f{H5Fopen(file.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT)}; - if(!f.valid()) - { - throw std::runtime_error("ODFFileIO: cannot open HDF5 file: " + file.string()); + if (!f.valid()) { + throw std::runtime_error("ODFFileIO: cannot open HDF5 file: " + + file.string()); } // Verify the prefix group exists (skip this check when prefix == "/" which is // always valid — it's the implicit root). - if(prefix != "/") - { + if (prefix != "/") { const htri_t prefixExists = H5Lexists(f.get(), prefix.c_str(), H5P_DEFAULT); - if(prefixExists < 0) - { - throw std::runtime_error("ODFFileIO: HDF5 error probing '" + prefix + "' in " + file.string()); + if (prefixExists < 0) { + throw std::runtime_error("ODFFileIO: HDF5 error probing '" + prefix + + "' in " + file.string()); } - if(prefixExists == 0) - { - throw std::runtime_error("ODFFileIO: group '" + prefix + "' not found in file " + file.string()); + if (prefixExists == 0) { + throw std::runtime_error("ODFFileIO: group '" + prefix + + "' not found in file " + file.string()); } } - const int64_t numComponents = readScalarInt64(f.get(), joinPath(prefix, "num_components")); - if(numComponents < 1) - { - throw std::runtime_error("ODFFileIO: num_components must be >= 1, got " + std::to_string(numComponents)); + const int64_t numComponents = + readScalarInt64(f.get(), joinPath(prefix, "num_components")); + if (numComponents < 1) { + throw std::runtime_error("ODFFileIO: num_components must be >= 1, got " + + std::to_string(numComponents)); } // Verify every expected component group exists - for(int64_t j = 0; j < numComponents; ++j) - { + for (int64_t j = 0; j < numComponents; ++j) { const std::string group = componentGroupName(prefix, j); const htri_t exists = H5Lexists(f.get(), group.c_str(), H5P_DEFAULT); - if(exists < 0) - { - throw std::runtime_error("ODFFileIO: HDF5 error probing '" + group + "' in " + file.string()); + if (exists < 0) { + throw std::runtime_error("ODFFileIO: HDF5 error probing '" + group + + "' in " + file.string()); } - if(exists == 0) - { + if (exists == 0) { throw std::runtime_error("ODFFileIO: missing component group: " + group); } } @@ -382,63 +338,64 @@ ODFFileMetadata readODFMetadata(const std::filesystem::path& file, const std::st const int64_t nphi1 = static_cast(refPhi1.size()) - 1; const int64_t nPHI = static_cast(refPHI.size()) - 1; const int64_t nphi2 = static_cast(refPhi2.size()) - 1; - const std::size_t expectedOdfSize = static_cast(nphi1) * static_cast(nPHI) * static_cast(nphi2); + const std::size_t expectedOdfSize = static_cast(nphi1) * + static_cast(nPHI) * + static_cast(nphi2); // Validate all components: bins byte-identical, ODFval size correct - for(int64_t j = 0; j < numComponents; ++j) - { + for (int64_t j = 0; j < numComponents; ++j) { const std::string base = componentGroupName(prefix, j); - if(j > 0) - { - const std::vector phi1Arr = readDoubleVector(f.get(), base + "/phi1_bins"); - const std::vector PHIArr = readDoubleVector(f.get(), base + "/PHI_bins"); - const std::vector phi2Arr = readDoubleVector(f.get(), base + "/phi2_bins"); - - if(!byteEqual(phi1Arr, refPhi1)) - { - throw std::runtime_error("ODFFileIO: phi1_bins differs from component_0 in " + base); + if (j > 0) { + const std::vector phi1Arr = + readDoubleVector(f.get(), base + "/phi1_bins"); + const std::vector PHIArr = + readDoubleVector(f.get(), base + "/PHI_bins"); + const std::vector phi2Arr = + readDoubleVector(f.get(), base + "/phi2_bins"); + + if (!byteEqual(phi1Arr, refPhi1)) { + throw std::runtime_error( + "ODFFileIO: phi1_bins differs from component_0 in " + base); } - if(!byteEqual(PHIArr, refPHI)) - { - throw std::runtime_error("ODFFileIO: PHI_bins differs from component_0 in " + base); + if (!byteEqual(PHIArr, refPHI)) { + throw std::runtime_error( + "ODFFileIO: PHI_bins differs from component_0 in " + base); } - if(!byteEqual(phi2Arr, refPhi2)) - { - throw std::runtime_error("ODFFileIO: phi2_bins differs from component_0 in " + base); + if (!byteEqual(phi2Arr, refPhi2)) { + throw std::runtime_error( + "ODFFileIO: phi2_bins differs from component_0 in " + base); } } const std::string odfvalPath = base + "/ODFval"; DatasetHandle ds{H5Dopen2(f.get(), odfvalPath.c_str(), H5P_DEFAULT)}; - if(!ds.valid()) - { + if (!ds.valid()) { throw std::runtime_error("ODFFileIO: missing ODFval in " + base); } DataspaceHandle space{H5Dget_space(ds.get())}; - if(!space.valid()) - { - throw std::runtime_error("ODFFileIO: cannot get dataspace for ODFval in " + base); + if (!space.valid()) { + throw std::runtime_error( + "ODFFileIO: cannot get dataspace for ODFval in " + base); } const int ndims = H5Sget_simple_extent_ndims(space.get()); - if(ndims < 0) - { - throw std::runtime_error("ODFFileIO: failed to get rank of dataset '" + odfvalPath + "' in " + file.string()); + if (ndims < 0) { + throw std::runtime_error("ODFFileIO: failed to get rank of dataset '" + + odfvalPath + "' in " + file.string()); } std::vector dims(static_cast(ndims)); - if(H5Sget_simple_extent_dims(space.get(), dims.data(), nullptr) < 0) - { - throw std::runtime_error("ODFFileIO: failed to read dims of dataset '" + odfvalPath + "' in " + file.string()); + if (H5Sget_simple_extent_dims(space.get(), dims.data(), nullptr) < 0) { + throw std::runtime_error("ODFFileIO: failed to read dims of dataset '" + + odfvalPath + "' in " + file.string()); } hsize_t total = 1; - for(hsize_t d : dims) - { + for (hsize_t d : dims) { total *= d; } - if(total != expectedOdfSize) - { + if (total != expectedOdfSize) { std::ostringstream oss; - oss << "ODFFileIO: ODFval size mismatch in " << base << " (got " << total << ", expected " << expectedOdfSize << ")"; + oss << "ODFFileIO: ODFval size mismatch in " << base << " (got " << total + << ", expected " << expectedOdfSize << ")"; throw std::runtime_error(oss.str()); } } @@ -448,12 +405,14 @@ ODFFileMetadata readODFMetadata(const std::filesystem::path& file, const std::st ODFFileMetadata md; md.numComponents = numComponents; md.dimsPhi1PHIPhi2 = {nphi1, nPHI, nphi2}; - md.spacingDegPhi1PHIPhi2 = {stepPhi1 * k_RadToDeg, stepPHI * k_RadToDeg, stepPhi2 * k_RadToDeg}; + md.spacingDegPhi1PHIPhi2 = {stepPhi1 * k_RadToDeg, stepPHI * k_RadToDeg, + stepPhi2 * k_RadToDeg}; return md; } -std::vector readODFComponents(const std::filesystem::path& file, const std::string& pathPrefix) -{ +std::vector +readODFComponents(const std::filesystem::path &file, + const std::string &pathPrefix) { const std::string prefix = normalizePathPrefix(pathPrefix); // Full validation up front — throws if anything is wrong. @@ -463,24 +422,26 @@ std::vector readODFComponents(const std::filesystem::path& fil H5ErrorSuppressor suppressErrors; FileHandle f{H5Fopen(file.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT)}; - if(!f.valid()) - { - throw std::runtime_error("ODFFileIO: cannot open HDF5 file: " + file.string()); + if (!f.valid()) { + throw std::runtime_error("ODFFileIO: cannot open HDF5 file: " + + file.string()); } - const std::size_t expectedSize = static_cast(md.dimsPhi1PHIPhi2[0]) * static_cast(md.dimsPhi1PHIPhi2[1]) * static_cast(md.dimsPhi1PHIPhi2[2]); + const std::size_t expectedSize = + static_cast(md.dimsPhi1PHIPhi2[0]) * + static_cast(md.dimsPhi1PHIPhi2[1]) * + static_cast(md.dimsPhi1PHIPhi2[2]); std::vector out; out.reserve(static_cast(md.numComponents)); - for(int64_t j = 0; j < md.numComponents; ++j) - { + for (int64_t j = 0; j < md.numComponents; ++j) { const std::string base = componentGroupName(prefix, j); std::vector buf = readDoubleVector(f.get(), base + "/ODFval"); - if(buf.size() != expectedSize) - { + if (buf.size() != expectedSize) { // readODFMetadata should already have caught this, but verify. std::ostringstream oss; - oss << "ODFFileIO: ODFval size mismatch in " << base << " (got " << buf.size() << ", expected " << expectedSize << ")"; + oss << "ODFFileIO: ODFval size mismatch in " << base << " (got " + << buf.size() << ", expected " << expectedSize << ")"; throw std::runtime_error(oss.str()); } ODFFileComponent comp; @@ -491,29 +452,32 @@ 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) -{ - if(components.empty()) - { - throw std::runtime_error("ODFFileIO::writeODFFile: components vector is empty"); +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()) { + throw std::runtime_error( + "ODFFileIO::writeODFFile: components vector is empty"); } const std::string prefix = normalizePathPrefix(pathPrefix); - for(std::size_t i = 0; i < 3; ++i) - { - if(dimsPhi1PHIPhi2[i] <= 0) - { + for (std::size_t i = 0; i < 3; ++i) { + if (dimsPhi1PHIPhi2[i] <= 0) { throw std::runtime_error("ODFFileIO::writeODFFile: dims must all be > 0"); } } - const std::size_t expectedSize = static_cast(dimsPhi1PHIPhi2[0]) * static_cast(dimsPhi1PHIPhi2[1]) * static_cast(dimsPhi1PHIPhi2[2]); - for(std::size_t ci = 0; ci < components.size(); ++ci) - { - if(components[ci].values.size() != expectedSize) - { + const std::size_t expectedSize = + static_cast(dimsPhi1PHIPhi2[0]) * + static_cast(dimsPhi1PHIPhi2[1]) * + static_cast(dimsPhi1PHIPhi2[2]); + for (std::size_t ci = 0; ci < components.size(); ++ci) { + if (components[ci].values.size() != expectedSize) { std::ostringstream oss; - oss << "ODFFileIO::writeODFFile: component " << ci << " has values.size()=" << components[ci].values.size() << ", expected " << expectedSize; + oss << "ODFFileIO::writeODFFile: component " << ci + << " has values.size()=" << components[ci].values.size() + << ", expected " << expectedSize; throw std::runtime_error(oss.str()); } } @@ -527,20 +491,23 @@ void writeODFFile(const std::filesystem::path& file, const std::array edges(static_cast(n) + 1); const double stepRad = stepDeg * k_DegToRad; - for(std::size_t i = 0; i < edges.size(); ++i) - { + for (std::size_t i = 0; i < edges.size(); ++i) { edges[i] = static_cast(i) * stepRad; } return edges; }; - const std::vector phi1Edges = buildEdges(dimsPhi1PHIPhi2[0], spacingDegPhi1PHIPhi2[0]); - const std::vector PHIEdges = buildEdges(dimsPhi1PHIPhi2[1], spacingDegPhi1PHIPhi2[1]); - const std::vector phi2Edges = buildEdges(dimsPhi1PHIPhi2[2], spacingDegPhi1PHIPhi2[2]); + const std::vector phi1Edges = + buildEdges(dimsPhi1PHIPhi2[0], spacingDegPhi1PHIPhi2[0]); + const std::vector PHIEdges = + buildEdges(dimsPhi1PHIPhi2[1], spacingDegPhi1PHIPhi2[1]); + const std::vector phi2Edges = + buildEdges(dimsPhi1PHIPhi2[2], spacingDegPhi1PHIPhi2[2]); - FileHandle f{H5Fcreate(file.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT)}; - if(!f.valid()) - { - throw std::runtime_error("ODFFileIO::writeODFFile: cannot create HDF5 file: " + file.string()); + FileHandle f{H5Fcreate(file.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, + H5P_DEFAULT)}; + if (!f.valid()) { + throw std::runtime_error( + "ODFFileIO::writeODFFile: cannot create HDF5 file: " + file.string()); } // When the normalized prefix is "/" we write directly to the file root and @@ -548,25 +515,26 @@ void writeODFFile(const std::filesystem::path& file, const std::array(components.size())); + writeScalarInt64(containerId, "num_components", + static_cast(components.size())); - for(std::size_t j = 0; j < components.size(); ++j) - { + for (std::size_t j = 0; j < components.size(); ++j) { const std::string name = "component_" + std::to_string(j); - GroupHandle compGroup{H5Gcreate2(containerId, name.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT)}; - if(!compGroup.valid()) - { - throw std::runtime_error("ODFFileIO::writeODFFile: cannot create group " + name); + GroupHandle compGroup{H5Gcreate2(containerId, name.c_str(), H5P_DEFAULT, + H5P_DEFAULT, H5P_DEFAULT)}; + if (!compGroup.valid()) { + throw std::runtime_error("ODFFileIO::writeODFFile: cannot create group " + + name); } writeDoubleVector(compGroup.get(), "ODFval", components[j].values); writeDoubleVector(compGroup.get(), "phi1_bins", phi1Edges); @@ -575,11 +543,12 @@ void writeODFFile(const std::filesystem::path& file, const std::array tryReadFixtureVersion(const std::filesystem::path& file, const std::string& pathPrefix) -{ - if(!std::filesystem::exists(file)) - { - throw std::runtime_error("ODFFileIO::tryReadFixtureVersion: file does not exist: " + file.string()); +std::optional tryReadFixtureVersion(const std::filesystem::path &file, + const std::string &pathPrefix) { + if (!std::filesystem::exists(file)) { + throw std::runtime_error( + "ODFFileIO::tryReadFixtureVersion: file does not exist: " + + file.string()); } const std::string prefix = normalizePathPrefix(pathPrefix); @@ -588,19 +557,20 @@ std::optional tryReadFixtureVersion(const std::filesystem::path& file, H5ErrorSuppressor suppressErrors; FileHandle f{H5Fopen(file.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT)}; - if(!f.valid()) - { - throw std::runtime_error("ODFFileIO::tryReadFixtureVersion: cannot open HDF5 file: " + file.string()); + if (!f.valid()) { + throw std::runtime_error( + "ODFFileIO::tryReadFixtureVersion: cannot open HDF5 file: " + + file.string()); } // Field is optional: probe with H5Lexists and return nullopt if absent. const htri_t exists = H5Lexists(f.get(), versionPath.c_str(), H5P_DEFAULT); - if(exists < 0) - { - throw std::runtime_error("ODFFileIO::tryReadFixtureVersion: HDF5 error probing '" + versionPath + "' in " + file.string()); + if (exists < 0) { + throw std::runtime_error( + "ODFFileIO::tryReadFixtureVersion: HDF5 error probing '" + versionPath + + "' in " + file.string()); } - if(exists == 0) - { + if (exists == 0) { return std::nullopt; } return readScalarInt64(f.get(), versionPath); diff --git a/src/LibMTRSim/ODFFileIO.hpp b/src/LibMTRSim/ODFFileIO.hpp index c64ae91..6755e19 100644 --- a/src/LibMTRSim/ODFFileIO.hpp +++ b/src/LibMTRSim/ODFFileIO.hpp @@ -18,11 +18,13 @@ namespace mtrsim { * over Euler space (phi1, PHI, phi2). All components in a single file share * identical bin-edge arrays. */ -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. - std::array spacingDegPhi1PHIPhi2; ///< Uniform bin size in DEGREES, one per axis. +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. + std::array + spacingDegPhi1PHIPhi2; ///< Uniform bin size in DEGREES, one per axis. }; /** @@ -32,20 +34,21 @@ struct LIBMTRSIM_EXPORT ODFFileMetadata * index(i1, iPHI, i2) = i1 * (nPHI * nphi2) + iPHI * nphi2 + i2 * Size is nphi1 * nPHI * nphi2. */ -struct LIBMTRSIM_EXPORT ODFFileComponent -{ +struct LIBMTRSIM_EXPORT ODFFileComponent { std::vector values; }; /** - * @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. @@ -58,7 +61,9 @@ struct LIBMTRSIM_EXPORT ODFFileComponent * prefix group, missing dataset, inconsistent bins, * non-uniform spacing, etc.) */ -LIBMTRSIM_EXPORT ODFFileMetadata readODFMetadata(const std::filesystem::path& file, const std::string& pathPrefix = "/ODF_best"); +LIBMTRSIM_EXPORT ODFFileMetadata +readODFMetadata(const std::filesystem::path &file, + const std::string &pathPrefix = "/ODF_best"); /** * @brief Full read of metadata plus every component's ODFval array. @@ -68,7 +73,9 @@ LIBMTRSIM_EXPORT ODFFileMetadata readODFMetadata(const std::filesystem::path& fi * * @throws std::runtime_error on any validation or read failure. */ -LIBMTRSIM_EXPORT std::vector readODFComponents(const std::filesystem::path& file, const std::string& pathPrefix = "/ODF_best"); +LIBMTRSIM_EXPORT std::vector +readODFComponents(const std::filesystem::path &file, + const std::string &pathPrefix = "/ODF_best"); /** * @brief Write an HDF5 file round-trip-compatible with the MATLAB ODF format. @@ -78,7 +85,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 +99,12 @@ 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. @@ -111,6 +120,8 @@ LIBMTRSIM_EXPORT void writeODFFile(const std::filesystem::path& file, * @throws std::runtime_error if the file can't be opened or the field exists * but isn't an int64 scalar. */ -LIBMTRSIM_EXPORT std::optional tryReadFixtureVersion(const std::filesystem::path& file, const std::string& pathPrefix = "/ODF_best"); +LIBMTRSIM_EXPORT std::optional +tryReadFixtureVersion(const std::filesystem::path &file, + const std::string &pathPrefix = "/ODF_best"); } // namespace mtrsim diff --git a/src/MTRSim/Filters/Algorithms/ComputeODF.cpp b/src/MTRSim/Filters/Algorithms/ComputeODF.cpp index d570f34..7a6f3bd 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" @@ -29,48 +29,44 @@ using namespace nx::core; -namespace -{ +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 // are promoted to `double` at the function boundary; all subsequent math is // double precision (matches EbsdLib's internal convention and the project's // double-precision orientation rule). -class AccumulateWorker -{ +class AccumulateWorker { public: - AccumulateWorker(const Float32Array& eulers, const Int32Array& phases, const UInt32Array& crystalStructures, const MaskCompareUtilities::MaskCompare* mask, const mtrsim::ODFBuildParams& params, - std::vector& masterValues, std::size_t& masterContributingCount, std::size_t& masterTotalDeposits, std::size_t& masterFailureCount, std::mutex& mergeMutex, - const std::atomic_bool& shouldCancel, ProgressMessageHelper& progressHelper) - : m_Eulers(eulers) - , m_Phases(phases) - , m_CrystalStructures(crystalStructures) - , m_Mask(mask) - , m_Params(params) - , m_MasterValues(masterValues) - , m_MasterContributingCount(masterContributingCount) - , m_MasterTotalDeposits(masterTotalDeposits) - , m_MasterFailureCount(masterFailureCount) - , m_MergeMutex(mergeMutex) - , m_ShouldCancel(shouldCancel) - , m_ProgressHelper(progressHelper) - { - } - - void operator()(const Range& range) const - { - if(m_ShouldCancel) - { + AccumulateWorker(const Float32Array &eulers, const Int32Array &phases, + const UInt32Array &crystalStructures, + const MaskCompareUtilities::MaskCompare *mask, + const mtrsim::ODFBuildParams ¶ms, + std::vector &masterValues, + std::size_t &masterContributingCount, + std::size_t &masterTotalDeposits, + std::size_t &masterFailureCount, std::mutex &mergeMutex, + const std::atomic_bool &shouldCancel, + ProgressMessageHelper &progressHelper) + : m_Eulers(eulers), m_Phases(phases), + m_CrystalStructures(crystalStructures), m_Mask(mask), m_Params(params), + m_MasterValues(masterValues), + m_MasterContributingCount(masterContributingCount), + m_MasterTotalDeposits(masterTotalDeposits), + m_MasterFailureCount(masterFailureCount), m_MergeMutex(mergeMutex), + m_ShouldCancel(shouldCancel), m_ProgressHelper(progressHelper) {} + + void operator()(const Range &range) const { + if (m_ShouldCancel) { return; } @@ -80,42 +76,42 @@ class AccumulateWorker std::size_t localTotalDeposits = 0; std::size_t localFailures = 0; - auto progressMessenger = m_ProgressHelper.createProgressMessenger(std::chrono::milliseconds(500)); - const std::size_t crystalStructTuples = m_CrystalStructures.getNumberOfTuples(); + auto progressMessenger = m_ProgressHelper.createProgressMessenger( + std::chrono::milliseconds(500)); + const std::size_t crystalStructTuples = + m_CrystalStructures.getNumberOfTuples(); // Per-thread cache of EbsdLib's LaueOps table. The vector is cheap to copy // (vector of shared_ptr) and avoids re-querying the global table on each // voxel. Indexed by ebsdlib::CrystalStructure::*** codes. - const std::vector allOps = ebsdlib::LaueOps::GetAllOrientationOps(); + const std::vector allOps = + ebsdlib::LaueOps::GetAllOrientationOps(); // Reusable scratch for the per-voxel symmetric expansion. Sized to the // maximum LaueOps count (24 for cubic) on first use; reused thereafter. std::vector> symEulers; - 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. - if((i & 0x3FFu) == 0 && m_ShouldCancel) - { + 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. + if ((i & 0x3FFu) == 0 && m_ShouldCancel) { return; } // Mask filter (if present) and zero-phase filter. - if(m_Mask != nullptr && !m_Mask->isTrue(i)) - { + if (m_Mask != nullptr && !m_Mask->isTrue(i)) { progressMessenger.sendProgressMessage(1); continue; } const int32 phase = m_Phases[i]; - if(phase <= 0 || static_cast(phase) >= crystalStructTuples) - { + if (phase <= 0 || + static_cast(phase) >= crystalStructTuples) { progressMessenger.sendProgressMessage(1); continue; } const uint32 code = m_CrystalStructures[static_cast(phase)]; - if(code >= allOps.size() || allOps[code] == nullptr) - { + if (code >= allOps.size() || allOps[code] == nullptr) { ++localFailures; progressMessenger.sendProgressMessage(1); continue; @@ -124,16 +120,18 @@ class AccumulateWorker const std::size_t numOps = laueOps->getNumSymOps(); // Promote single-precision EBSD inputs to double immediately. - const ebsdlib::EulerDType inputEu(static_cast(m_Eulers[3 * i + 0]), static_cast(m_Eulers[3 * i + 1]), static_cast(m_Eulers[3 * i + 2])); - const ebsdlib::Matrix3X3D Gpassive = inputEu.toOrientationMatrix().toGMatrix(); + const ebsdlib::EulerDType inputEu( + static_cast(m_Eulers[3 * i + 0]), + static_cast(m_Eulers[3 * i + 1]), + static_cast(m_Eulers[3 * i + 2])); + const ebsdlib::Matrix3X3D Gpassive = + inputEu.toOrientationMatrix().toGMatrix(); symEulers.resize(numOps); std::size_t deposits = 0; - try - { - for(std::size_t k = 0; k < numOps; ++k) - { + try { + for (std::size_t k = 0; k < numOps; ++k) { // EbsdLib stores the ACTIVE symmetry operator; transpose for passive. // Symmetric variant: R = O_passive * G_passive_input. const ebsdlib::Matrix3X3D Op_active = laueOps->getMatSymOpD(k); @@ -141,7 +139,8 @@ class AccumulateWorker // Extract Euler back via canonical om2eu (handles degenerate-PHI // and the eps-snap correctly). Wrap into [0, 2π) / [0, π] / [0, 2π). - 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(); // Pragmatic conversion: ODFBuilder::accumulate currently consumes a @@ -153,10 +152,10 @@ class AccumulateWorker mtrsim::accumulate(symEulers, m_Params, localValues); 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. + } 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. ++localFailures; progressMessenger.sendProgressMessage(1); continue; @@ -169,8 +168,7 @@ class AccumulateWorker // Merge into master under a mutex. { std::lock_guard lock(m_MergeMutex); - for(std::size_t b = 0; b < binCount; ++b) - { + for (std::size_t b = 0; b < binCount; ++b) { m_MasterValues[b] += localValues[b]; } m_MasterContributingCount += localContributing; @@ -180,76 +178,92 @@ class AccumulateWorker } private: - const Float32Array& m_Eulers; - const Int32Array& m_Phases; - const UInt32Array& m_CrystalStructures; - const MaskCompareUtilities::MaskCompare* m_Mask; - const mtrsim::ODFBuildParams& m_Params; - std::vector& m_MasterValues; - std::size_t& m_MasterContributingCount; - std::size_t& m_MasterTotalDeposits; - std::size_t& m_MasterFailureCount; - std::mutex& m_MergeMutex; - const std::atomic_bool& m_ShouldCancel; - ProgressMessageHelper& m_ProgressHelper; + const Float32Array &m_Eulers; + const Int32Array &m_Phases; + const UInt32Array &m_CrystalStructures; + const MaskCompareUtilities::MaskCompare *m_Mask; + const mtrsim::ODFBuildParams &m_Params; + std::vector &m_MasterValues; + std::size_t &m_MasterContributingCount; + std::size_t &m_MasterTotalDeposits; + std::size_t &m_MasterFailureCount; + std::mutex &m_MergeMutex; + const std::atomic_bool &m_ShouldCancel; + ProgressMessageHelper &m_ProgressHelper; }; } // namespace // ----------------------------------------------------------------------------- -ComputeODF::ComputeODF(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ComputeODFInputValues* inputValues) -: m_DataStructure(dataStructure) -, m_InputValues(inputValues) -, m_ShouldCancel(shouldCancel) -, m_MessageHandler(mesgHandler) -{ -} +ComputeODF::ComputeODF(DataStructure &dataStructure, + const IFilter::MessageHandler &mesgHandler, + const std::atomic_bool &shouldCancel, + ComputeODFInputValues *inputValues) + : m_DataStructure(dataStructure), m_InputValues(inputValues), + m_ShouldCancel(shouldCancel), m_MessageHandler(mesgHandler) {} // ----------------------------------------------------------------------------- ComputeODF::~ComputeODF() noexcept = default; // ----------------------------------------------------------------------------- -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). +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). std::unique_ptr maskArray = nullptr; - if(m_InputValues->useMask) - { - try - { - 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()); + if (m_InputValues->useMask) { + try { + 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()); return MakeErrorResult(-506, message); } } - const DataPath outArrayPath = m_InputValues->outputImageGeometry.createChildPath(m_InputValues->cellAttrMatName).createChildPath(m_InputValues->componentName); - auto& outArray = m_DataStructure.getDataRefAs(outArrayPath); - auto& outStore = outArray.getDataStoreRef(); - - 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)); + const DataPath outArrayPath = + m_InputValues->outputImageGeometry + .createChildPath(m_InputValues->cellAttrMatName) + .createChildPath(m_InputValues->componentName); + auto &outArray = m_DataStructure.getDataRefAs(outArrayPath); + auto &outStore = outArray.getDataStoreRef(); + + 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)); } 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")); - - // Set up the message helper / progress messenger BEFORE the parallel section so the worker - // can stamp throttled per-voxel progress through its private ProgressMessenger. + 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")); + + // 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); @@ -261,38 +275,41 @@ Result<> ComputeODF::operator()() std::size_t masterFailureCount = 0; std::mutex mergeMutex; - const mtrsim::ODFBuildParams params{m_InputValues->nphi1, m_InputValues->nPHI, m_InputValues->nphi2, m_InputValues->binSizeDeg, m_InputValues->applySmoothing}; + const mtrsim::ODFBuildParams params{ + m_InputValues->nphi1, m_InputValues->nPHI, m_InputValues->nphi2, + m_InputValues->binSizeDeg, m_InputValues->applySmoothing}; - if(numVoxels == 0) - { - m_MessageHandler(IFilter::Message::Type::Info, "No input voxels — output ODF array left at all zeros."); + if (numVoxels == 0) { + m_MessageHandler(IFilter::Message::Type::Info, + "No input voxels — output ODF array left at all zeros."); std::copy(masterValues.begin(), masterValues.end(), outStore.begin()); return {}; } 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) - { + 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. - 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."); - } - else - { + // 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."); + } else { mtrsim::normalize(masterValues, static_cast(masterTotalDeposits)); } @@ -307,43 +324,51 @@ Result<> ComputeODF::operator()() // MUD[i,j,k] = acc[i,j,k] * 8*pi^2 / (step^3 * sin(PHI_center(j))) // This matches MTEX's plot(odf) convention: sub-uniform regions read < 1 // MUD, peak texture reads several MUD. - if(m_InputValues->outputUnits == 1ULL && masterContributingCount != 0) - { + if (m_InputValues->outputUnits == 1ULL && masterContributingCount != 0) { const double stepRad = m_InputValues->binSizeDeg * std::numbers::pi / 180.0; - const double scale = 8.0 * std::numbers::pi * std::numbers::pi / (stepRad * stepRad * stepRad); - for(int32_t j = 0; j < m_InputValues->nPHI; ++j) - { + const double scale = 8.0 * std::numbers::pi * std::numbers::pi / + (stepRad * stepRad * stepRad); + for (int32_t j = 0; j < m_InputValues->nPHI; ++j) { const double phiCenter = (static_cast(j) + 0.5) * stepRad; 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); - for(int32_t k = 0; k < m_InputValues->nphi2; ++k) - { + 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); + for (int32_t k = 0; k < m_InputValues->nphi2; ++k) { masterValues[baseIdx + static_cast(k)] *= rowMul; } } } - m_MessageHandler(IFilter::Message::Type::Info, "Output units: MUD (per-PHI-row sin(PHI) Jacobian applied)."); - } - else - { - m_MessageHandler(IFilter::Message::Type::Info, "Output units: Count-Density (raw normalized histogram)."); + m_MessageHandler( + IFilter::Message::Type::Info, + "Output units: MUD (per-PHI-row sin(PHI) Jacobian applied)."); + } else { + 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. - 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.", - masterFailureCount)); + // 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.", + masterFailureCount)); } return {}; diff --git a/src/MTRSim/Filters/Algorithms/ComputeODF.hpp b/src/MTRSim/Filters/Algorithms/ComputeODF.hpp index 6a8f766..479e24d 100644 --- a/src/MTRSim/Filters/Algorithms/ComputeODF.hpp +++ b/src/MTRSim/Filters/Algorithms/ComputeODF.hpp @@ -9,11 +9,9 @@ #include #include -namespace nx::core -{ +namespace nx::core { -struct MTRSIM_EXPORT ComputeODFInputValues -{ +struct MTRSIM_EXPORT ComputeODFInputValues { bool applySmoothing; double binSizeDeg; bool useMask; @@ -40,24 +38,26 @@ struct MTRSIM_EXPORT ComputeODFInputValues * @brief This algorithm implements support code for the ComputeODFFilter */ -class MTRSIM_EXPORT ComputeODF -{ +class MTRSIM_EXPORT ComputeODF { public: - ComputeODF(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ComputeODFInputValues* inputValues); + ComputeODF(DataStructure &dataStructure, + const IFilter::MessageHandler &mesgHandler, + const std::atomic_bool &shouldCancel, + ComputeODFInputValues *inputValues); ~ComputeODF() noexcept; - ComputeODF(const ComputeODF&) = delete; - ComputeODF(ComputeODF&&) noexcept = delete; - ComputeODF& operator=(const ComputeODF&) = delete; - ComputeODF& operator=(ComputeODF&&) noexcept = delete; + ComputeODF(const ComputeODF &) = delete; + ComputeODF(ComputeODF &&) noexcept = delete; + ComputeODF &operator=(const ComputeODF &) = delete; + ComputeODF &operator=(ComputeODF &&) noexcept = delete; Result<> operator()(); private: - DataStructure& m_DataStructure; - const ComputeODFInputValues* m_InputValues = nullptr; - const std::atomic_bool& m_ShouldCancel; - const IFilter::MessageHandler& m_MessageHandler; + DataStructure &m_DataStructure; + const ComputeODFInputValues *m_InputValues = nullptr; + const std::atomic_bool &m_ShouldCancel; + const IFilter::MessageHandler &m_MessageHandler; }; } // namespace nx::core diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.cpp b/src/MTRSim/Filters/Algorithms/MTRSim.cpp index 3cbc4a6..42386e4 100644 --- a/src/MTRSim/Filters/Algorithms/MTRSim.cpp +++ b/src/MTRSim/Filters/Algorithms/MTRSim.cpp @@ -16,24 +16,23 @@ 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(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()() -{ +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 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]); @@ -42,15 +41,18 @@ Result<> MTRSim::operator()() // 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. + // 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(); + 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]))); + components.push_back(mtrsim::gridToODFComponent( + values, n1, nPHI, n2, static_cast(odfSpacing[2]), + static_cast(odfSpacing[1]), + static_cast(odfSpacing[0]))); } // 3. SimulationParams from filter inputs. @@ -63,40 +65,42 @@ Result<> MTRSim::operator()() params.dz = m_InputValues->physicalSpacing[2]; params.volumeFractions = m_InputValues->volumeFractions[0]; // 1 row params.thetaList = m_InputValues->thetaList; - // Note: simulateMTR is driven by the seeded rng passed below; SimulationParams::seed is not consulted. + // Note: simulateMTR is driven by the seeded rng passed below; + // SimulationParams::seed is not consulted. - if(m_ShouldCancel) - { + 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)..."); + m_MessageHandler( + IFilter::Message::Type::Info, + "Running MTR simulation (this may take a while for large volumes)..."); std::mt19937_64 rng(m_InputValues->seed); mtrsim::MTRSimResult sim; - try - { + try { sim = mtrsim::simulateMTR(params, components, rng, n1, nPHI, n2); - } catch(const std::exception& e) - { - return MakeErrorResult(-13050, fmt::format("MTR simulation failed: {}", e.what())); + } catch (const std::exception &e) { + return MakeErrorResult(-13050, + fmt::format("MTR simulation failed: {}", e.what())); } - if(m_ShouldCancel) - { + if (m_ShouldCancel) { 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 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) - { + 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]); @@ -105,12 +109,11 @@ Result<> MTRSim::operator()() m_MessageHandler(IFilter::Message::Type::Info, "MTR simulation complete."); - if(m_InputValues->generatePolarColoring) - { - m_MessageHandler(IFilter::Message::Type::Info, "Computing polar coloring..."); + if (m_InputValues->generatePolarColoring) { + m_MessageHandler(IFilter::Message::Type::Info, + "Computing polar coloring..."); auto colorResult = applyPolarColoring(sim, cellAm); - if(colorResult.invalid()) - { + if (colorResult.invalid()) { return colorResult; } } @@ -119,20 +122,24 @@ Result<> MTRSim::operator()() } // ----------------------------------------------------------------------------- -Result<> MTRSim::applyPolarColoring(const mtrsim::MTRSimResult& sim, const DataPath& cellAttrMatPath) -{ +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)); + 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); + 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) - { + 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; diff --git a/src/MTRSim/Filters/Algorithms/MTRSim.hpp b/src/MTRSim/Filters/Algorithms/MTRSim.hpp index f6c2a72..f849708 100644 --- a/src/MTRSim/Filters/Algorithms/MTRSim.hpp +++ b/src/MTRSim/Filters/Algorithms/MTRSim.hpp @@ -10,11 +10,9 @@ #include -namespace nx::core -{ +namespace nx::core { -struct MTRSIM_EXPORT MTRSimInputValues -{ +struct MTRSIM_EXPORT MTRSimInputValues { DataPath inputOdfGeometryPath; std::vector odfComponentPaths; std::vector> volumeFractions; // 1 row x N cols @@ -33,28 +31,31 @@ struct MTRSIM_EXPORT MTRSimInputValues /** * @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. + * 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 -{ +class MTRSIM_EXPORT MTRSim { public: - MTRSim(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, MTRSimInputValues* inputValues); + 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; + 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; + 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..b854bb3 100644 --- a/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.cpp +++ b/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.cpp @@ -13,54 +13,60 @@ using namespace nx::core; // ----------------------------------------------------------------------------- -ReadMTRSimODF::ReadMTRSimODF(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadMTRSimODFInputValues* inputValues) -: m_DataStructure(dataStructure) -, m_InputValues(inputValues) -, m_ShouldCancel(shouldCancel) -, m_MessageHandler(mesgHandler) -{ -} +ReadMTRSimODF::ReadMTRSimODF(DataStructure &dataStructure, + const IFilter::MessageHandler &mesgHandler, + const std::atomic_bool &shouldCancel, + ReadMTRSimODFInputValues *inputValues) + : m_DataStructure(dataStructure), m_InputValues(inputValues), + m_ShouldCancel(shouldCancel), m_MessageHandler(mesgHandler) {} // ----------------------------------------------------------------------------- ReadMTRSimODF::~ReadMTRSimODF() noexcept = default; // ----------------------------------------------------------------------------- -Result<> ReadMTRSimODF::operator()() -{ - m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Reading MTRSim ODF file: {}", m_InputValues->inputFile.string())); +Result<> ReadMTRSimODF::operator()() { + m_MessageHandler(IFilter::Message::Type::Info, + fmt::format("Reading MTRSim ODF file: {}", + m_InputValues->inputFile.string())); std::vector components; - try - { - components = mtrsim::readODFComponents(m_InputValues->inputFile, m_InputValues->hdf5PathPrefix); - } catch(const std::exception& e) - { - return MakeErrorResult(-12010, fmt::format("ODF file read failed: {}", e.what())); + try { + components = mtrsim::readODFComponents(m_InputValues->inputFile, + m_InputValues->hdf5PathPrefix); + } catch (const std::exception &e) { + return MakeErrorResult(-12010, + fmt::format("ODF file read failed: {}", e.what())); } // The preflight/execute contract guarantees the DataStructure has one // Float64 array per component (created via CreateArrayAction in preflight). // Per-array size consistency is still verified below against store.getSize(). - const DataPath cellAttrMatPath = m_InputValues->outputImageGeometryPath.createChildPath(m_InputValues->cellAttrMatName); + const DataPath cellAttrMatPath = + m_InputValues->outputImageGeometryPath.createChildPath( + m_InputValues->cellAttrMatName); - for(size_t c = 0; c < components.size(); ++c) - { - if(m_ShouldCancel) - { + for (size_t c = 0; c < components.size(); ++c) { + if (m_ShouldCancel) { return {}; } - const DataPath componentPath = cellAttrMatPath.createChildPath(fmt::format("component_{}", c)); - auto& componentArray = m_DataStructure.getDataRefAs(componentPath); - auto& store = componentArray.getDataStoreRef(); - - 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())); + const DataPath componentPath = + cellAttrMatPath.createChildPath(fmt::format("component_{}", c)); + auto &componentArray = + m_DataStructure.getDataRefAs(componentPath); + auto &store = componentArray.getDataStoreRef(); + + 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())); } - m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Copying component_{} ({} values)", c, srcValues.size())); + m_MessageHandler( + IFilter::Message::Type::Info, + fmt::format("Copying component_{} ({} values)", c, srcValues.size())); std::copy(srcValues.begin(), srcValues.end(), store.begin()); } diff --git a/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.hpp b/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.hpp index 57d29d6..aed37f1 100644 --- a/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.hpp +++ b/src/MTRSim/Filters/Algorithms/ReadMTRSimODF.hpp @@ -9,11 +9,9 @@ #include #include -namespace nx::core -{ +namespace nx::core { -struct MTRSIM_EXPORT ReadMTRSimODFInputValues -{ +struct MTRSIM_EXPORT ReadMTRSimODFInputValues { std::filesystem::path inputFile; std::string hdf5PathPrefix; DataPath outputImageGeometryPath; @@ -27,24 +25,26 @@ struct MTRSIM_EXPORT ReadMTRSimODFInputValues * output ImageGeom's cell AttributeMatrix. */ -class MTRSIM_EXPORT ReadMTRSimODF -{ +class MTRSIM_EXPORT ReadMTRSimODF { public: - ReadMTRSimODF(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ReadMTRSimODFInputValues* inputValues); + ReadMTRSimODF(DataStructure &dataStructure, + const IFilter::MessageHandler &mesgHandler, + const std::atomic_bool &shouldCancel, + ReadMTRSimODFInputValues *inputValues); ~ReadMTRSimODF() noexcept; - ReadMTRSimODF(const ReadMTRSimODF&) = delete; - ReadMTRSimODF(ReadMTRSimODF&&) noexcept = delete; - ReadMTRSimODF& operator=(const ReadMTRSimODF&) = delete; - ReadMTRSimODF& operator=(ReadMTRSimODF&&) noexcept = delete; + ReadMTRSimODF(const ReadMTRSimODF &) = delete; + ReadMTRSimODF(ReadMTRSimODF &&) noexcept = delete; + ReadMTRSimODF &operator=(const ReadMTRSimODF &) = delete; + ReadMTRSimODF &operator=(ReadMTRSimODF &&) noexcept = delete; Result<> operator()(); private: - DataStructure& m_DataStructure; - const ReadMTRSimODFInputValues* m_InputValues = nullptr; - const std::atomic_bool& m_ShouldCancel; - const IFilter::MessageHandler& m_MessageHandler; + DataStructure &m_DataStructure; + const ReadMTRSimODFInputValues *m_InputValues = nullptr; + const std::atomic_bool &m_ShouldCancel; + const IFilter::MessageHandler &m_MessageHandler; }; } // namespace nx::core diff --git a/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.cpp b/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.cpp index ddd08a2..519aabd 100644 --- a/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.cpp +++ b/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.cpp @@ -15,29 +15,36 @@ using namespace nx::core; // ----------------------------------------------------------------------------- -WriteMTRSimODF::WriteMTRSimODF(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, WriteMTRSimODFInputValues* inputValues) -: m_DataStructure(dataStructure) -, m_InputValues(inputValues) -, m_ShouldCancel(shouldCancel) -, m_MessageHandler(mesgHandler) -{ -} +WriteMTRSimODF::WriteMTRSimODF(DataStructure &dataStructure, + const IFilter::MessageHandler &mesgHandler, + const std::atomic_bool &shouldCancel, + WriteMTRSimODFInputValues *inputValues) + : m_DataStructure(dataStructure), m_InputValues(inputValues), + m_ShouldCancel(shouldCancel), m_MessageHandler(mesgHandler) {} // ----------------------------------------------------------------------------- WriteMTRSimODF::~WriteMTRSimODF() noexcept = default; // ----------------------------------------------------------------------------- -Result<> WriteMTRSimODF::operator()() -{ - m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Writing MTRSim ODF file: {}", m_InputValues->outputFile.string())); +Result<> WriteMTRSimODF::operator()() { + m_MessageHandler(IFilter::Message::Type::Info, + fmt::format("Writing MTRSim ODF file: {}", + m_InputValues->outputFile.string())); - const auto& geom = m_DataStructure.getDataRefAs(m_InputValues->inputImageGeometry); + 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. - const std::array dimsPhi1PHIPhi2 = {static_cast(geom.getNumZCells()), static_cast(geom.getNumYCells()), static_cast(geom.getNumXCells())}; + // 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(); - const std::array spacingDegPhi1PHIPhi2 = {static_cast(spacingXYZ[2]), static_cast(spacingXYZ[1]), static_cast(spacingXYZ[0])}; + const std::array spacingDegPhi1PHIPhi2 = { + static_cast(spacingXYZ[2]), static_cast(spacingXYZ[1]), + static_cast(spacingXYZ[0])}; // Per-component size validation against the geometry happens in preflight; // writeODFFile re-validates values.size() == dims[0]*dims[1]*dims[2] as a @@ -45,29 +52,32 @@ Result<> WriteMTRSimODF::operator()() std::vector components; components.reserve(m_InputValues->odfComponents.size()); - for(std::size_t c = 0; c < m_InputValues->odfComponents.size(); ++c) - { - if(m_ShouldCancel) - { + for (std::size_t c = 0; c < m_InputValues->odfComponents.size(); ++c) { + if (m_ShouldCancel) { return {}; } - const auto& componentArray = m_DataStructure.getDataRefAs(m_InputValues->odfComponents[c]); - const auto& store = componentArray.getDataStoreRef(); + const auto &componentArray = m_DataStructure.getDataRefAs( + m_InputValues->odfComponents[c]); + const auto &store = componentArray.getDataStoreRef(); - m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Packing component_{} ({} values) from '{}'", c, store.getSize(), m_InputValues->odfComponents[c].toString())); + m_MessageHandler(IFilter::Message::Type::Info, + fmt::format("Packing component_{} ({} values) from '{}'", + c, store.getSize(), + m_InputValues->odfComponents[c].toString())); mtrsim::ODFFileComponent comp; comp.values.assign(store.begin(), store.end()); components.push_back(std::move(comp)); } - try - { - mtrsim::writeODFFile(m_InputValues->outputFile, dimsPhi1PHIPhi2, spacingDegPhi1PHIPhi2, components, m_InputValues->hdf5PathPrefix); - } catch(const std::exception& e) - { - return MakeErrorResult(-12110, fmt::format("ODF write failed: {}", e.what())); + try { + mtrsim::writeODFFile(m_InputValues->outputFile, dimsPhi1PHIPhi2, + spacingDegPhi1PHIPhi2, components, + m_InputValues->hdf5PathPrefix); + } catch (const std::exception &e) { + return MakeErrorResult(-12110, + fmt::format("ODF write failed: {}", e.what())); } return {}; diff --git a/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.hpp b/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.hpp index ef6d9e8..bbc64ce 100644 --- a/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.hpp +++ b/src/MTRSim/Filters/Algorithms/WriteMTRSimODF.hpp @@ -9,11 +9,9 @@ #include #include -namespace nx::core -{ +namespace nx::core { -struct MTRSIM_EXPORT WriteMTRSimODFInputValues -{ +struct MTRSIM_EXPORT WriteMTRSimODFInputValues { std::filesystem::path outputFile; std::string hdf5PathPrefix; DataPath inputImageGeometry; @@ -28,24 +26,26 @@ struct MTRSIM_EXPORT WriteMTRSimODFInputValues * convention back to the on-disk phi1-first axis order. */ -class MTRSIM_EXPORT WriteMTRSimODF -{ +class MTRSIM_EXPORT WriteMTRSimODF { public: - WriteMTRSimODF(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, WriteMTRSimODFInputValues* inputValues); + WriteMTRSimODF(DataStructure &dataStructure, + const IFilter::MessageHandler &mesgHandler, + const std::atomic_bool &shouldCancel, + WriteMTRSimODFInputValues *inputValues); ~WriteMTRSimODF() noexcept; - WriteMTRSimODF(const WriteMTRSimODF&) = delete; - WriteMTRSimODF(WriteMTRSimODF&&) noexcept = delete; - WriteMTRSimODF& operator=(const WriteMTRSimODF&) = delete; - WriteMTRSimODF& operator=(WriteMTRSimODF&&) noexcept = delete; + WriteMTRSimODF(const WriteMTRSimODF &) = delete; + WriteMTRSimODF(WriteMTRSimODF &&) noexcept = delete; + WriteMTRSimODF &operator=(const WriteMTRSimODF &) = delete; + WriteMTRSimODF &operator=(WriteMTRSimODF &&) noexcept = delete; Result<> operator()(); private: - DataStructure& m_DataStructure; - const WriteMTRSimODFInputValues* m_InputValues = nullptr; - const std::atomic_bool& m_ShouldCancel; - const IFilter::MessageHandler& m_MessageHandler; + DataStructure &m_DataStructure; + const WriteMTRSimODFInputValues *m_InputValues = nullptr; + const std::atomic_bool &m_ShouldCancel; + const IFilter::MessageHandler &m_MessageHandler; }; } // namespace nx::core diff --git a/src/MTRSim/Filters/ComputeODFFilter.cpp b/src/MTRSim/Filters/ComputeODFFilter.cpp index b98cfce..03aff0b 100644 --- a/src/MTRSim/Filters/ComputeODFFilter.cpp +++ b/src/MTRSim/Filters/ComputeODFFilter.cpp @@ -24,195 +24,250 @@ using namespace nx::core; -namespace nx::core -{ +namespace nx::core { //------------------------------------------------------------------------------ -std::string ComputeODFFilter::name() const -{ +std::string ComputeODFFilter::name() const { return FilterTraits::name.str(); } //------------------------------------------------------------------------------ -std::string ComputeODFFilter::className() const -{ +std::string ComputeODFFilter::className() const { return FilterTraits::className; } //------------------------------------------------------------------------------ -Uuid ComputeODFFilter::uuid() const -{ +Uuid ComputeODFFilter::uuid() const { return FilterTraits::uuid; } //------------------------------------------------------------------------------ -std::string ComputeODFFilter::humanName() const -{ +std::string ComputeODFFilter::humanName() const { return "Compute ODF From Euler Angles"; } //------------------------------------------------------------------------------ -std::vector ComputeODFFilter::defaultTags() const -{ - return {className(), "MTRSim", "ODF", "EBSD", "Compute", "Crystallography", "Statistics"}; +std::vector ComputeODFFilter::defaultTags() const { + return {className(), "MTRSim", "ODF", "EBSD", + "Compute", "Crystallography", "Statistics"}; } //------------------------------------------------------------------------------ -Parameters ComputeODFFilter::parameters() const -{ +Parameters ComputeODFFilter::parameters() const { Parameters params; 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.", - 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 " - "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).", - 0ULL, ChoicesParameter::Choices{"Count-Density", "MUD"})); + 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.", + 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 " + "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).", + 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 " - "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 " - "AttributeMatrix as the Euler angles.", - DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); + 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 " + "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 " + "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)); - 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 " - "consulted when 'Use Mask Array' is enabled.", - DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::boolean, DataType::uint8}, ArraySelectionParameter::AllowedComponentShapes{{1}})); + 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)); + 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 " + "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.", - DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::uint32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); - + 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.", + 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.", - 0ULL, ChoicesParameter::Choices{"Create New ODF Geometry", "Append to Existing ODF Geometry"})); + 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.", + 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"}))); - 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")); - 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 " - "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")); + 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"}))); + 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")); + 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 " + "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")); params.linkParameters(k_UseMask_Key, k_Mask_Key, true); - params.linkParameters(k_OutputMode_Key, k_OutputImageGeometry_Key, std::make_any(0ULL)); - params.linkParameters(k_OutputMode_Key, k_CellAttrMatName_Key, std::make_any(0ULL)); - params.linkParameters(k_OutputMode_Key, k_BinSizeDeg_Key, std::make_any(0ULL)); - params.linkParameters(k_OutputMode_Key, k_ExistingOdfGeometry_Key, std::make_any(1ULL)); + params.linkParameters(k_OutputMode_Key, k_OutputImageGeometry_Key, + std::make_any(0ULL)); + params.linkParameters(k_OutputMode_Key, k_CellAttrMatName_Key, + std::make_any(0ULL)); + params.linkParameters(k_OutputMode_Key, k_BinSizeDeg_Key, + std::make_any(0ULL)); + params.linkParameters(k_OutputMode_Key, k_ExistingOdfGeometry_Key, + std::make_any(1ULL)); return params; } //------------------------------------------------------------------------------ -IFilter::VersionType ComputeODFFilter::parametersVersion() const -{ - return 3; -} +IFilter::VersionType ComputeODFFilter::parametersVersion() const { return 3; } //------------------------------------------------------------------------------ -IFilter::UniquePointer ComputeODFFilter::clone() const -{ +IFilter::UniquePointer ComputeODFFilter::clone() const { return std::make_unique(); } //------------------------------------------------------------------------------ -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); +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); auto pApplySmoothing = filterArgs.value(k_ApplySmoothing_Key); auto pBinSizeDeg = filterArgs.value(k_BinSizeDeg_Key); auto pEulerAnglesPath = filterArgs.value(k_EulerAngles_Key); auto pPhasesPath = filterArgs.value(k_Phases_Key); - auto pCrystalStructuresPath = filterArgs.value(k_CrystalStructures_Key); + auto pCrystalStructuresPath = + filterArgs.value(k_CrystalStructures_Key); auto pUseMask = filterArgs.value(k_UseMask_Key); auto pMaskPath = filterArgs.value(k_Mask_Key); - auto pOutputImageGeomPath = filterArgs.value(k_OutputImageGeometry_Key); - auto pCellAttrMatName = filterArgs.value(k_CellAttrMatName_Key); - auto pExistingOdfGeomPath = filterArgs.value(k_ExistingOdfGeometry_Key); - auto pComponentName = filterArgs.value(k_ComponentName_Key); + auto pOutputImageGeomPath = + filterArgs.value(k_OutputImageGeometry_Key); + auto pCellAttrMatName = filterArgs.value( + k_CellAttrMatName_Key); + auto pExistingOdfGeomPath = + filterArgs.value(k_ExistingOdfGeometry_Key); + auto pComponentName = + filterArgs.value(k_ComponentName_Key); const bool isAppendMode = (pOutputMode == 1ULL); 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; int32_t nphi2 = 0; bool binSizeIsDerived = false; - if(!isAppendMode) - { - if(pBinSizeDeg <= 0.0f) - { - return {MakeErrorResult(-12200, fmt::format("Bin size must be > 0; got {}.", pBinSizeDeg))}; + if (!isAppendMode) { + if (pBinSizeDeg <= 0.0f) { + return {MakeErrorResult( + -12200, fmt::format("Bin size must be > 0; got {}.", pBinSizeDeg))}; } binSize = static_cast(pBinSizeDeg); 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))}; + 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)); nphi2 = nphi1; - } - else - { - // 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()))}; + } else { + // 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()))}; } 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]))}; + 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]))}; } - if(spacing[0] <= 0.0f) - { + 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]))}; + -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,184 +276,249 @@ 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)) - { - 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))}; + 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))}; } } - // 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))}; + 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))}; } // Ensure all "Cell" arrays have the same number of tuples std::vector dataPaths; dataPaths.push_back(pPhasesPath); dataPaths.push_back(pEulerAnglesPath); - if(pUseMask) - { + if (pUseMask) { dataPaths.push_back(pMaskPath); } 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()))}; + 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()))}; } - // 3. Same parent (AttributeMatrix) for the cell-level inputs. const DataPath eulerParent = pEulerAnglesPath.getParent(); const DataPath phasesParent = pPhasesPath.getParent(); - if(eulerParent != phasesParent) - { + if (eulerParent != phasesParent) { return {MakeErrorResult( - -12205, fmt::format("Cell Euler Angles ('{}') and Cell Phases ('{}') must live on the same AttributeMatrix.", pEulerAnglesPath.toString(), pPhasesPath.toString()))}; + -12205, + fmt::format("Cell Euler Angles ('{}') and Cell Phases ('{}') must live " + "on the same AttributeMatrix.", + pEulerAnglesPath.toString(), pPhasesPath.toString()))}; } - if(pUseMask) - { + 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()))}; + if (maskParent != eulerParent) { + 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) - { + if (!isAppendMode) { targetImageGeomPath = pOutputImageGeomPath; cellAttrMatPath = pOutputImageGeomPath.createChildPath(pCellAttrMatName); - } - 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. - const auto& existingGeom = dataStructure.getDataRefAs(pExistingOdfGeomPath); - if(existingGeom.getCellData() == nullptr) - { + } 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. + 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()))}; + -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. - const DataPath proposedComponentPath = cellAttrMatPath.createChildPath(pComponentName); - if(dataStructure.getDataAs(proposedComponentPath) != nullptr) - { + // 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()))}; + -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). - const std::vector imageGeomDimsXYZ = {static_cast(nphi2), static_cast(nPHI), static_cast(nphi1)}; + if (!isAppendMode) { + // 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)}; - auto createImageGeomAction = std::make_unique(pOutputImageGeomPath, imageGeomDimsXYZ, imageGeomOrigin, imageGeomSpacingXYZ, pCellAttrMatName); + const std::vector imageGeomSpacingXYZ = { + static_cast(binSize), static_cast(binSize), + static_cast(binSize)}; + auto createImageGeomAction = std::make_unique( + pOutputImageGeomPath, imageGeomDimsXYZ, imageGeomOrigin, + imageGeomSpacingXYZ, pCellAttrMatName); resultOutputActions.value().appendAction(std::move(createImageGeomAction)); } { - const std::vector arrayTupleShapeZYX = {static_cast(nphi1), static_cast(nPHI), static_cast(nphi2)}; + const std::vector arrayTupleShapeZYX = {static_cast(nphi1), + static_cast(nPHI), + static_cast(nphi2)}; const std::vector componentShape = {1}; - const DataPath componentPath = cellAttrMatPath.createChildPath(pComponentName); - auto createArrayAction = std::make_unique(DataType::float64, arrayTupleShapeZYX, componentShape, componentPath); + const DataPath componentPath = + cellAttrMatPath.createChildPath(pComponentName); + auto createArrayAction = std::make_unique( + DataType::float64, arrayTupleShapeZYX, componentShape, componentPath); resultOutputActions.value().appendAction(std::move(createArrayAction)); } // 6. PreflightUpdatedValues so the UI can preview the dims/settings. - const std::string binSizeLabel = binSizeIsDerived ? fmt::format("{:.4f} (derived from existing geometry; user input ignored)", binSize) : fmt::format("{:.4f}", binSize); - preflightUpdatedValues.push_back({"Output Mode", isAppendMode ? std::string{"Append to Existing ODF Geometry"} : std::string{"Create New ODF Geometry"}}); - preflightUpdatedValues.push_back({"Target ODF Image Geometry", targetImageGeomPath.toString()}); + const std::string binSizeLabel = + binSizeIsDerived + ? fmt::format( + "{:.4f} (derived from existing geometry; user input ignored)", + binSize) + : fmt::format("{:.4f}", binSize); + preflightUpdatedValues.push_back( + {"Output Mode", isAppendMode + ? std::string{"Append to Existing ODF Geometry"} + : std::string{"Create New ODF Geometry"}}); + preflightUpdatedValues.push_back( + {"Target ODF Image Geometry", targetImageGeomPath.toString()}); preflightUpdatedValues.push_back({"Bin Size [deg]", binSizeLabel}); - preflightUpdatedValues.push_back({"Bins (phi1 x PHI x phi2)", fmt::format("{} x {} x {}", nphi1, nPHI, nphi2)}); - preflightUpdatedValues.push_back({"ImageGeom Dimensions (X, Y, Z)", fmt::format("{} x {} x {}", nphi2, nPHI, nphi1)}); - preflightUpdatedValues.push_back({"Total Bin Count", std::to_string(totalBinCount)}); - preflightUpdatedValues.push_back({"Smoothing", pApplySmoothing ? std::string{"enabled"} : std::string{"disabled"}}); - preflightUpdatedValues.push_back({"Output Units", (pOutputUnits == 1ULL) ? std::string{"MUD (Jacobian sin(PHI) applied)"} : std::string{"Count-Density (raw histogram)"}}); - preflightUpdatedValues.push_back({"Mask", pUseMask ? pMaskPath.toString() : std::string{"(none)"}}); + preflightUpdatedValues.push_back( + {"Bins (phi1 x PHI x phi2)", + fmt::format("{} x {} x {}", nphi1, nPHI, nphi2)}); + preflightUpdatedValues.push_back( + {"ImageGeom Dimensions (X, Y, Z)", + fmt::format("{} x {} x {}", nphi2, nPHI, nphi1)}); + preflightUpdatedValues.push_back( + {"Total Bin Count", std::to_string(totalBinCount)}); + preflightUpdatedValues.push_back( + {"Smoothing", + pApplySmoothing ? std::string{"enabled"} : std::string{"disabled"}}); + preflightUpdatedValues.push_back( + {"Output Units", (pOutputUnits == 1ULL) + ? std::string{"MUD (Jacobian sin(PHI) applied)"} + : std::string{"Count-Density (raw histogram)"}}); + preflightUpdatedValues.push_back( + {"Mask", pUseMask ? pMaskPath.toString() : std::string{"(none)"}}); return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } //------------------------------------------------------------------------------ -Result<> ComputeODFFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const -{ - const auto pOutputMode = filterArgs.value(k_OutputMode_Key); +Result<> ComputeODFFilter::executeImpl( + DataStructure &dataStructure, const Arguments &filterArgs, + const PipelineFilter *pipelineNode, const MessageHandler &messageHandler, + const std::atomic_bool &shouldCancel, + const ExecutionContext &executionContext) const { + const auto pOutputMode = + filterArgs.value(k_OutputMode_Key); const bool isAppendMode = (pOutputMode == 1ULL); ComputeODFInputValues inputValues; inputValues.applySmoothing = filterArgs.value(k_ApplySmoothing_Key); - inputValues.outputUnits = filterArgs.value(k_OutputUnits_Key); + inputValues.outputUnits = + filterArgs.value(k_OutputUnits_Key); inputValues.useMask = filterArgs.value(k_UseMask_Key); inputValues.eulerAnglesPath = filterArgs.value(k_EulerAngles_Key); inputValues.phasesPath = filterArgs.value(k_Phases_Key); - inputValues.crystalStructuresPath = filterArgs.value(k_CrystalStructures_Key); + inputValues.crystalStructuresPath = + filterArgs.value(k_CrystalStructures_Key); inputValues.maskPath = filterArgs.value(k_Mask_Key); - inputValues.componentName = filterArgs.value(k_ComponentName_Key); - - if(!isAppendMode) - { - inputValues.binSizeDeg = static_cast(filterArgs.value(k_BinSizeDeg_Key)); - 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. - inputValues.nphi1 = static_cast(std::round(360.0 / inputValues.binSizeDeg)); - inputValues.nPHI = static_cast(std::round(180.0 / inputValues.binSizeDeg)); + inputValues.componentName = + filterArgs.value(k_ComponentName_Key); + + if (!isAppendMode) { + inputValues.binSizeDeg = + static_cast(filterArgs.value(k_BinSizeDeg_Key)); + 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. + 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. - const auto pExistingOdfGeomPath = filterArgs.value(k_ExistingOdfGeometry_Key); - const auto& existingGeom = dataStructure.getDataRefAs(pExistingOdfGeomPath); + } else { + // 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(); inputValues.outputImageGeometry = pExistingOdfGeomPath; - inputValues.cellAttrMatName = existingGeom.getCellDataPath().getTargetName(); + inputValues.cellAttrMatName = + existingGeom.getCellDataPath().getTargetName(); inputValues.binSizeDeg = static_cast(spacing[0]); inputValues.nphi2 = static_cast(existingGeom.getNumXCells()); inputValues.nPHI = static_cast(existingGeom.getNumYCells()); inputValues.nphi1 = static_cast(existingGeom.getNumZCells()); } - return ComputeODF(dataStructure, messageHandler, shouldCancel, &inputValues)(); + return ComputeODF(dataStructure, messageHandler, shouldCancel, + &inputValues)(); } //------------------------------------------------------------------------------ -Result ComputeODFFilter::FromSIMPLJson(const nlohmann::json& json) -{ +Result ComputeODFFilter::FromSIMPLJson(const nlohmann::json &json) { Arguments args = ComputeODFFilter().getDefaultArguments(); std::vector> results; @@ -407,7 +527,8 @@ Result ComputeODFFilter::FromSIMPLJson(const nlohmann::json& json) Result<> conversionResult = MergeResults(std::move(results)); - return ConvertResultTo(std::move(conversionResult), std::move(args)); + return ConvertResultTo(std::move(conversionResult), + std::move(args)); } } // namespace nx::core diff --git a/src/MTRSim/Filters/ComputeODFFilter.hpp b/src/MTRSim/Filters/ComputeODFFilter.hpp index 6f0ffb2..95773ca 100644 --- a/src/MTRSim/Filters/ComputeODFFilter.hpp +++ b/src/MTRSim/Filters/ComputeODFFilter.hpp @@ -6,8 +6,7 @@ #include "simplnx/Filter/FilterTraits.hpp" #include "simplnx/Filter/IFilter.hpp" -namespace nx::core -{ +namespace nx::core { /** * @class ComputeODFFilter * @brief Builds an Orientation Distribution Function (ODF) from per-voxel EBSD @@ -16,37 +15,42 @@ 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 -{ +class MTRSIM_EXPORT ComputeODFFilter : public IFilter { public: ComputeODFFilter() = default; ~ComputeODFFilter() noexcept override = default; - ComputeODFFilter(const ComputeODFFilter&) = delete; - ComputeODFFilter(ComputeODFFilter&&) noexcept = delete; + ComputeODFFilter(const ComputeODFFilter &) = delete; + ComputeODFFilter(ComputeODFFilter &&) noexcept = delete; - ComputeODFFilter& operator=(const ComputeODFFilter&) = delete; - ComputeODFFilter& operator=(ComputeODFFilter&&) noexcept = delete; + ComputeODFFilter &operator=(const ComputeODFFilter &) = delete; + ComputeODFFilter &operator=(ComputeODFFilter &&) noexcept = delete; // Parameter Keys - static inline constexpr StringLiteral k_ApplySmoothing_Key = "apply_smoothing"; + 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_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_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"; /** @@ -54,7 +58,7 @@ class MTRSIM_EXPORT ComputeODFFilter : public IFilter * @param json * @return Result */ - static Result FromSIMPLJson(const nlohmann::json& json); + static Result FromSIMPLJson(const nlohmann::json &json); /** * @brief Returns the name of the filter. @@ -108,33 +112,52 @@ 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; + 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; + 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, ComputeODFFilter, "4811df3f-a5ce-4b90-b8f0-16b9050f7a8d"); +SIMPLNX_DEF_FILTER_TRAITS(nx::core, ComputeODFFilter, + "4811df3f-a5ce-4b90-b8f0-16b9050f7a8d"); diff --git a/src/MTRSim/Filters/MTRSimFilter.cpp b/src/MTRSim/Filters/MTRSimFilter.cpp index e63d3d6..3918998 100644 --- a/src/MTRSim/Filters/MTRSimFilter.cpp +++ b/src/MTRSim/Filters/MTRSimFilter.cpp @@ -26,110 +26,145 @@ using namespace nx::core; -namespace nx::core -{ +namespace nx::core { //------------------------------------------------------------------------------ -std::string MTRSimFilter::name() const -{ +std::string MTRSimFilter::name() const { return FilterTraits::name.str(); } //------------------------------------------------------------------------------ -std::string MTRSimFilter::className() const -{ +std::string MTRSimFilter::className() const { return FilterTraits::className; } //------------------------------------------------------------------------------ -Uuid MTRSimFilter::uuid() const -{ - return FilterTraits::uuid; -} +Uuid MTRSimFilter::uuid() const { return FilterTraits::uuid; } //------------------------------------------------------------------------------ -std::string MTRSimFilter::humanName() const -{ +std::string MTRSimFilter::humanName() const { return "Generate Synthetic Microtexture"; } //------------------------------------------------------------------------------ -std::vector MTRSimFilter::defaultTags() const -{ +std::vector MTRSimFilter::defaultTags() const { return {className(), "MTRSim", "Synthetic", "Microtexture", "Generate"}; } //------------------------------------------------------------------------------ -Parameters MTRSimFilter::parameters() const -{ +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}, GetAllNumericTypes(), - MultiArraySelectionParameter::AllowedComponentShapes{{1}})); + 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{"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)); + 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)); + 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{"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.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{"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.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.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); + params.linkParameters(k_GeneratePolarColoring_Key, k_PolarColorsArrayName_Key, + true); return params; } //------------------------------------------------------------------------------ -IFilter::VersionType MTRSimFilter::parametersVersion() const -{ - return 1; -} +IFilter::VersionType MTRSimFilter::parametersVersion() const { return 1; } //------------------------------------------------------------------------------ -IFilter::UniquePointer MTRSimFilter::clone() const -{ +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); - auto pVolumeFractions = filterArgs.value(k_VolumeFractions_Key); - auto pThetaList = filterArgs.value(k_ThetaList_Key); +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 pGenPolar = filterArgs.value(k_GeneratePolarColoring_Key); @@ -144,106 +179,144 @@ IFilter::PreflightResult MTRSimFilter::preflightImpl(const DataStructure& dataSt std::vector preflightUpdatedValues; const usize numComponents = pOdfArrays.size(); - if(numComponents < 2) - { - return {MakeErrorResult(-13001, "MTRSim requires at least 2 ODF component arrays.")}; + if (numComponents < 2) { + return {MakeErrorResult( + -13501, "MTRSim requires at least 2 ODF component arrays.")}; } - if(pVolumeFractions.size() != 1 || pVolumeFractions[0].size() != numComponents) - { - return {MakeErrorResult(-13002, fmt::format("Volume Fraction must be 1 row x {} columns (one per ODF component).", numComponents))}; + 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(-13007, fmt::format("Each Volume Fraction value must be in the range [0, 1] (got {:.4f}).", v))}; + 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]) - { + for (double v : pVolumeFractions[0]) { vfSum += v; } - if(std::abs(vfSum - 1.0) > 1.0e-3) - { - return {MakeErrorResult(-13003, fmt::format("Volume Fraction values must sum to 1.0 (got {:.4f}).", vfSum))}; + 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(-13004, fmt::format("Theta List needs at least {} rows (components - 1).", numComponents - 1))}; + 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(-13005, "Each Theta List row must have exactly 3 columns.")}; + 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(-13006, "Physical Spacing X and Y must be greater than 0.")}; + 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 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 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( + 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}))); + 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)}); + 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 -{ +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); + 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); 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()); + 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; + 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) -{ +Result MTRSimFilter::FromSIMPLJson(const nlohmann::json &json) { Arguments args = MTRSimFilter().getDefaultArguments(); std::vector> results; @@ -252,7 +325,8 @@ Result MTRSimFilter::FromSIMPLJson(const nlohmann::json& json) Result<> conversionResult = MergeResults(std::move(results)); - return ConvertResultTo(std::move(conversionResult), std::move(args)); + 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 index 163b743..9458f92 100644 --- a/src/MTRSim/Filters/MTRSimFilter.hpp +++ b/src/MTRSim/Filters/MTRSimFilter.hpp @@ -6,49 +6,57 @@ #include "simplnx/Filter/FilterTraits.hpp" #include "simplnx/Filter/IFilter.hpp" -namespace nx::core -{ +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 -{ +class MTRSIM_EXPORT MTRSimFilter : public IFilter { public: MTRSimFilter() = default; ~MTRSimFilter() noexcept override = default; - MTRSimFilter(const MTRSimFilter&) = delete; - MTRSimFilter(MTRSimFilter&&) noexcept = delete; + MTRSimFilter(const MTRSimFilter &) = delete; + MTRSimFilter(MTRSimFilter &&) noexcept = delete; - MTRSimFilter& operator=(const MTRSimFilter&) = delete; - MTRSimFilter& operator=(MTRSimFilter&&) noexcept = delete; + MTRSimFilter &operator=(const MTRSimFilter &) = delete; + MTRSimFilter &operator=(MTRSimFilter &&) noexcept = delete; // Parameter Keys - 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_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_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"; + 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"; /** * @brief Reads SIMPL json and converts it simplnx Arguments. * @param json * @return Result */ - static Result FromSIMPLJson(const nlohmann::json& json); + static Result FromSIMPLJson(const nlohmann::json &json); /** * @brief Returns the name of the filter. @@ -102,33 +110,52 @@ class MTRSIM_EXPORT MTRSimFilter : 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; + 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; + 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"); +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..3f0d923 100644 --- a/src/MTRSim/Filters/ReadMTRSimODFFilter.cpp +++ b/src/MTRSim/Filters/ReadMTRSimODFFilter.cpp @@ -22,153 +22,187 @@ namespace fs = std::filesystem; using namespace nx::core; -namespace nx::core -{ +namespace nx::core { //------------------------------------------------------------------------------ -std::string ReadMTRSimODFFilter::name() const -{ +std::string ReadMTRSimODFFilter::name() const { return FilterTraits::name.str(); } //------------------------------------------------------------------------------ -std::string ReadMTRSimODFFilter::className() const -{ +std::string ReadMTRSimODFFilter::className() const { return FilterTraits::className; } //------------------------------------------------------------------------------ -Uuid ReadMTRSimODFFilter::uuid() const -{ +Uuid ReadMTRSimODFFilter::uuid() const { return FilterTraits::uuid; } //------------------------------------------------------------------------------ -std::string ReadMTRSimODFFilter::humanName() const -{ +std::string ReadMTRSimODFFilter::humanName() const { return "Read MTRSim ODF (HDF5)"; } //------------------------------------------------------------------------------ -std::vector ReadMTRSimODFFilter::defaultTags() const -{ +std::vector ReadMTRSimODFFilter::defaultTags() const { return {className(), "IO", "Input", "Read", "MTRSim", "ODF"}; } //------------------------------------------------------------------------------ -Parameters ReadMTRSimODFFilter::parameters() const -{ +Parameters ReadMTRSimODFFilter::parameters() const { Parameters params; params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); - 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 " - "under a variable named 'my_sample_ODF', set this to '/my_sample_ODF'.", - "/ODF_best")); + 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 " + "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 " - "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")); + 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 " + "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")); return params; } //------------------------------------------------------------------------------ -IFilter::VersionType ReadMTRSimODFFilter::parametersVersion() const -{ +IFilter::VersionType ReadMTRSimODFFilter::parametersVersion() const { return 1; } //------------------------------------------------------------------------------ -IFilter::UniquePointer ReadMTRSimODFFilter::clone() const -{ +IFilter::UniquePointer ReadMTRSimODFFilter::clone() const { return std::make_unique(); } //------------------------------------------------------------------------------ -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); - auto pOutputImageGeomPath = filterArgs.value(k_OutputImageGeometry_Key); - auto pCellAttrMatName = filterArgs.value(k_CellAttrMatName_Key); +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); + auto pOutputImageGeomPath = + filterArgs.value(k_OutputImageGeometry_Key); + auto pCellAttrMatName = filterArgs.value( + k_CellAttrMatName_Key); 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 - { + try { metadata = mtrsim::readODFMetadata(pInputFile, pHdf5PathPrefix); - } catch(const std::exception& e) - { - return {MakeErrorResult(-12001, fmt::format("ODF file read failed: {}", e.what()))}; + } catch (const std::exception &e) { + return {MakeErrorResult( + -12001, fmt::format("ODF file read failed: {}", e.what()))}; } // Axis mapping: phi1 is slowest-varying in the file, phi2 fastest. // ImageGeom XYZ must have phi2 as X (fastest), PHI as Y, phi1 as Z (slowest). - const std::vector imageGeomDimsXYZ = {static_cast(metadata.dimsPhi1PHIPhi2[2]), static_cast(metadata.dimsPhi1PHIPhi2[1]), static_cast(metadata.dimsPhi1PHIPhi2[0])}; + const std::vector imageGeomDimsXYZ = { + static_cast(metadata.dimsPhi1PHIPhi2[2]), + static_cast(metadata.dimsPhi1PHIPhi2[1]), + static_cast(metadata.dimsPhi1PHIPhi2[0])}; const std::vector imageGeomOrigin = {0.0f, 0.0f, 0.0f}; - const std::vector imageGeomSpacingXYZ = {static_cast(metadata.spacingDegPhi1PHIPhi2[2]), static_cast(metadata.spacingDegPhi1PHIPhi2[1]), - static_cast(metadata.spacingDegPhi1PHIPhi2[0])}; + const std::vector imageGeomSpacingXYZ = { + static_cast(metadata.spacingDegPhi1PHIPhi2[2]), + static_cast(metadata.spacingDegPhi1PHIPhi2[1]), + 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. - const std::vector arrayTupleShapeZYX = {imageGeomDimsXYZ[2], imageGeomDimsXYZ[1], imageGeomDimsXYZ[0]}; + // 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}; { - auto createImageGeomAction = std::make_unique(pOutputImageGeomPath, imageGeomDimsXYZ, imageGeomOrigin, imageGeomSpacingXYZ, pCellAttrMatName); + auto createImageGeomAction = std::make_unique( + pOutputImageGeomPath, imageGeomDimsXYZ, imageGeomOrigin, + imageGeomSpacingXYZ, pCellAttrMatName); resultOutputActions.value().appendAction(std::move(createImageGeomAction)); } - const DataPath cellAttrMatPath = pOutputImageGeomPath.createChildPath(pCellAttrMatName); - for(int64_t c = 0; c < metadata.numComponents; ++c) - { - DataPath componentPath = cellAttrMatPath.createChildPath(fmt::format("component_{}", c)); - auto createArrayAction = std::make_unique(DataType::float64, arrayTupleShapeZYX, componentShape, componentPath); + const DataPath cellAttrMatPath = + pOutputImageGeomPath.createChildPath(pCellAttrMatName); + for (int64_t c = 0; c < metadata.numComponents; ++c) { + DataPath componentPath = + cellAttrMatPath.createChildPath(fmt::format("component_{}", c)); + auto createArrayAction = std::make_unique( + DataType::float64, arrayTupleShapeZYX, componentShape, componentPath); resultOutputActions.value().appendAction(std::move(createArrayAction)); } 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])}); + {"Components Found", std::to_string(metadata.numComponents)}); preflightUpdatedValues.push_back( - {"ImageGeom Dimensions (X, Y, Z)", fmt::format("{} x {} x {}", metadata.dimsPhi1PHIPhi2[2], metadata.dimsPhi1PHIPhi2[1], metadata.dimsPhi1PHIPhi2[0])}); + {"Bins (phi1 x PHI x phi2)", + fmt::format("{} x {} x {}", metadata.dimsPhi1PHIPhi2[0], + metadata.dimsPhi1PHIPhi2[1], metadata.dimsPhi1PHIPhi2[2])}); preflightUpdatedValues.push_back( - {"Spacing (X, Y, Z) [deg]", fmt::format("{:.4f}, {:.4f}, {:.4f}", metadata.spacingDegPhi1PHIPhi2[2], metadata.spacingDegPhi1PHIPhi2[1], metadata.spacingDegPhi1PHIPhi2[0])}); + {"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])}); return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } //------------------------------------------------------------------------------ -Result<> ReadMTRSimODFFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const -{ +Result<> ReadMTRSimODFFilter::executeImpl( + DataStructure &dataStructure, const Arguments &filterArgs, + const PipelineFilter *pipelineNode, const MessageHandler &messageHandler, + const std::atomic_bool &shouldCancel, + const ExecutionContext &executionContext) const { ReadMTRSimODFInputValues inputValues; - inputValues.inputFile = filterArgs.value(k_InputFile_Key); - inputValues.hdf5PathPrefix = filterArgs.value(k_Hdf5PathPrefix_Key); - inputValues.outputImageGeometryPath = filterArgs.value(k_OutputImageGeometry_Key); - inputValues.cellAttrMatName = filterArgs.value(k_CellAttrMatName_Key); - - return ReadMTRSimODF(dataStructure, messageHandler, shouldCancel, &inputValues)(); + inputValues.inputFile = + filterArgs.value(k_InputFile_Key); + inputValues.hdf5PathPrefix = + filterArgs.value(k_Hdf5PathPrefix_Key); + inputValues.outputImageGeometryPath = + filterArgs.value(k_OutputImageGeometry_Key); + inputValues.cellAttrMatName = + filterArgs.value( + k_CellAttrMatName_Key); + + return ReadMTRSimODF(dataStructure, messageHandler, shouldCancel, + &inputValues)(); } //------------------------------------------------------------------------------ -Result ReadMTRSimODFFilter::FromSIMPLJson(const nlohmann::json& json) -{ +Result +ReadMTRSimODFFilter::FromSIMPLJson(const nlohmann::json &json) { Arguments args = ReadMTRSimODFFilter().getDefaultArguments(); std::vector> results; @@ -177,7 +211,8 @@ Result ReadMTRSimODFFilter::FromSIMPLJson(const nlohmann::json& json) Result<> conversionResult = MergeResults(std::move(results)); - return ConvertResultTo(std::move(conversionResult), std::move(args)); + return ConvertResultTo(std::move(conversionResult), + std::move(args)); } } // namespace nx::core diff --git a/src/MTRSim/Filters/ReadMTRSimODFFilter.hpp b/src/MTRSim/Filters/ReadMTRSimODFFilter.hpp index e2a36b9..e0599fa 100644 --- a/src/MTRSim/Filters/ReadMTRSimODFFilter.hpp +++ b/src/MTRSim/Filters/ReadMTRSimODFFilter.hpp @@ -6,37 +6,38 @@ #include "simplnx/Filter/FilterTraits.hpp" #include "simplnx/Filter/IFilter.hpp" -namespace nx::core -{ +namespace nx::core { /** * @class ReadMTRSimODFFilter * @brief Reads a MATLAB-format MTRSim ODF HDF5 file into an ImageGeom with * one Float64 single-component cell-data array per ODF component. */ -class MTRSIM_EXPORT ReadMTRSimODFFilter : public IFilter -{ +class MTRSIM_EXPORT ReadMTRSimODFFilter : public IFilter { public: ReadMTRSimODFFilter() = default; ~ReadMTRSimODFFilter() noexcept override = default; - ReadMTRSimODFFilter(const ReadMTRSimODFFilter&) = delete; - ReadMTRSimODFFilter(ReadMTRSimODFFilter&&) noexcept = delete; + ReadMTRSimODFFilter(const ReadMTRSimODFFilter &) = delete; + ReadMTRSimODFFilter(ReadMTRSimODFFilter &&) noexcept = delete; - ReadMTRSimODFFilter& operator=(const ReadMTRSimODFFilter&) = delete; - ReadMTRSimODFFilter& operator=(ReadMTRSimODFFilter&&) noexcept = delete; + ReadMTRSimODFFilter &operator=(const ReadMTRSimODFFilter &) = delete; + 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 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"; /** * @brief Reads SIMPL json and converts it simplnx Arguments. * @param json * @return Result */ - static Result FromSIMPLJson(const nlohmann::json& json); + static Result FromSIMPLJson(const nlohmann::json &json); /** * @brief Returns the name of the filter. @@ -90,33 +91,52 @@ 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; + 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; + 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, ReadMTRSimODFFilter, "2b1a4841-65d7-4315-9fe3-d66c88e5755c"); +SIMPLNX_DEF_FILTER_TRAITS(nx::core, ReadMTRSimODFFilter, + "2b1a4841-65d7-4315-9fe3-d66c88e5755c"); diff --git a/src/MTRSim/Filters/WriteMTRSimODFFilter.cpp b/src/MTRSim/Filters/WriteMTRSimODFFilter.cpp index 3ecd939..4e28ccf 100644 --- a/src/MTRSim/Filters/WriteMTRSimODFFilter.cpp +++ b/src/MTRSim/Filters/WriteMTRSimODFFilter.cpp @@ -19,142 +19,158 @@ namespace fs = std::filesystem; using namespace nx::core; -namespace nx::core -{ +namespace nx::core { //------------------------------------------------------------------------------ -std::string WriteMTRSimODFFilter::name() const -{ +std::string WriteMTRSimODFFilter::name() const { return FilterTraits::name.str(); } //------------------------------------------------------------------------------ -std::string WriteMTRSimODFFilter::className() const -{ +std::string WriteMTRSimODFFilter::className() const { return FilterTraits::className; } //------------------------------------------------------------------------------ -Uuid WriteMTRSimODFFilter::uuid() const -{ +Uuid WriteMTRSimODFFilter::uuid() const { return FilterTraits::uuid; } //------------------------------------------------------------------------------ -std::string WriteMTRSimODFFilter::humanName() const -{ +std::string WriteMTRSimODFFilter::humanName() const { return "Write MTRSim ODF (HDF5)"; } //------------------------------------------------------------------------------ -std::vector WriteMTRSimODFFilter::defaultTags() const -{ +std::vector WriteMTRSimODFFilter::defaultTags() const { return {className(), "IO", "Output", "Write", "MTRSim", "ODF"}; } //------------------------------------------------------------------------------ -Parameters WriteMTRSimODFFilter::parameters() const -{ +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.", - 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').", - "/ODF_best")); + 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').", + "/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.", - 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.", - MultiArraySelectionParameter::ValueType{}, MultiArraySelectionParameter::AllowedTypes{IArray::ArrayType::DataArray}, - MultiArraySelectionParameter::AllowedDataTypes{DataType::float64}, MultiArraySelectionParameter::AllowedComponentShapes{{1}})); + 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.", + 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.", + MultiArraySelectionParameter::ValueType{}, + MultiArraySelectionParameter::AllowedTypes{IArray::ArrayType::DataArray}, + MultiArraySelectionParameter::AllowedDataTypes{DataType::float64}, + MultiArraySelectionParameter::AllowedComponentShapes{{1}})); return params; } //------------------------------------------------------------------------------ -IFilter::VersionType WriteMTRSimODFFilter::parametersVersion() const -{ +IFilter::VersionType WriteMTRSimODFFilter::parametersVersion() const { return 1; } //------------------------------------------------------------------------------ -IFilter::UniquePointer WriteMTRSimODFFilter::clone() const -{ +IFilter::UniquePointer WriteMTRSimODFFilter::clone() const { return std::make_unique(); } //------------------------------------------------------------------------------ -IFilter::PreflightResult WriteMTRSimODFFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const -{ - auto pOutputFile = filterArgs.value(k_OutputFile_Key); - auto pHdf5PathPrefix = filterArgs.value(k_Hdf5PathPrefix_Key); - auto pInputImageGeomPath = filterArgs.value(k_InputImageGeometry_Key); - auto pODFComponents = filterArgs.value(k_ODFComponents_Key); +IFilter::PreflightResult WriteMTRSimODFFilter::preflightImpl( + const DataStructure &dataStructure, const Arguments &filterArgs, + const MessageHandler &messageHandler, const std::atomic_bool &shouldCancel, + const ExecutionContext &executionContext) const { + auto pOutputFile = + filterArgs.value(k_OutputFile_Key); + auto pHdf5PathPrefix = + filterArgs.value(k_Hdf5PathPrefix_Key); + auto pInputImageGeomPath = + filterArgs.value(k_InputImageGeometry_Key); + auto pODFComponents = + filterArgs.value( + k_ODFComponents_Key); nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; - if(pODFComponents.empty()) - { - return {MakeErrorResult(-12100, "Select at least one ODF component array.")}; + if (pODFComponents.empty()) { + return {MakeErrorResult( + -12100, "Select at least one ODF component array.")}; } - const auto* geom = dataStructure.getDataAs(pInputImageGeomPath); - if(geom == nullptr) - { - return {MakeErrorResult(-12101, "Input geometry must be an ImageGeom.")}; + const auto *geom = dataStructure.getDataAs(pInputImageGeomPath); + if (geom == nullptr) { + return {MakeErrorResult( + -12101, "Input geometry must be an ImageGeom.")}; } const usize numX = geom->getNumXCells(); const usize numY = geom->getNumYCells(); const usize numZ = geom->getNumZCells(); - if(numX < 2 || numY < 2 || numZ < 2) - { - return {MakeErrorResult(-12105, "Degenerate geometry rejected (< 2 bins on some axis).")}; + if (numX < 2 || numY < 2 || numZ < 2) { + return {MakeErrorResult( + -12105, "Degenerate geometry rejected (< 2 bins on some axis).")}; } const usize expectedTuples = numX * numY * numZ; const auto geomPathVec = pInputImageGeomPath.getPathVector(); - for(const auto& arrayPath : pODFComponents) - { - // Verify the array lives under the selected ImageGeom (its path must start with geomPath). + for (const auto &arrayPath : pODFComponents) { + // 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) - { - for(usize i = 0; i < geomPathVec.size(); ++i) - { - if(arrayPathVec[i] != geomPathVec[i]) - { + if (onGeom) { + for (usize i = 0; i < geomPathVec.size(); ++i) { + if (arrayPathVec[i] != geomPathVec[i]) { onGeom = false; break; } } } - if(!onGeom) - { - return {MakeErrorResult(-12102, fmt::format("Selected array '{}' does not belong to the input ImageGeom '{}'.", arrayPath.toString(), pInputImageGeomPath.toString()))}; + if (!onGeom) { + return {MakeErrorResult( + -12102, + fmt::format("Selected array '{}' does not belong to the input " + "ImageGeom '{}'.", + arrayPath.toString(), pInputImageGeomPath.toString()))}; } - const auto* arr = dataStructure.getDataAs(arrayPath); - if(arr == nullptr) - { - return {MakeErrorResult(-12103, fmt::format("Selected array '{}' must be a Float64 DataArray.", arrayPath.toString()))}; + const auto *arr = dataStructure.getDataAs(arrayPath); + if (arr == nullptr) { + return {MakeErrorResult( + -12103, + fmt::format("Selected array '{}' must be a Float64 DataArray.", + arrayPath.toString()))}; } - if(arr->getNumberOfTuples() != expectedTuples) - { - return {MakeErrorResult(-12104, - fmt::format("Selected array '{}' has {} tuples but the ImageGeom has {} cells.", arrayPath.toString(), arr->getNumberOfTuples(), expectedTuples))}; + if (arr->getNumberOfTuples() != expectedTuples) { + return {MakeErrorResult( + -12104, fmt::format("Selected array '{}' has {} tuples but the " + "ImageGeom has {} cells.", + arrayPath.toString(), arr->getNumberOfTuples(), + expectedTuples))}; } } @@ -163,36 +179,55 @@ IFilter::PreflightResult WriteMTRSimODFFilter::preflightImpl(const DataStructure // the array list. const auto spacing = geom->getSpacing(); preflightUpdatedValues.push_back({"HDF5 Path Prefix", pHdf5PathPrefix}); - preflightUpdatedValues.push_back({"Components to Write", std::to_string(pODFComponents.size())}); - preflightUpdatedValues.push_back({"Tuples per Component", std::to_string(expectedTuples)}); - 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); - preflightUpdatedValues.push_back({"Estimated File Size [bytes]", std::to_string(estBytes)}); + preflightUpdatedValues.push_back( + {"Components to Write", std::to_string(pODFComponents.size())}); + preflightUpdatedValues.push_back( + {"Tuples per Component", std::to_string(expectedTuples)}); + 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); + preflightUpdatedValues.push_back( + {"Estimated File Size [bytes]", std::to_string(estBytes)}); return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } //------------------------------------------------------------------------------ -Result<> WriteMTRSimODFFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, - const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const -{ +Result<> WriteMTRSimODFFilter::executeImpl( + DataStructure &dataStructure, const Arguments &filterArgs, + const PipelineFilter *pipelineNode, const MessageHandler &messageHandler, + const std::atomic_bool &shouldCancel, + const ExecutionContext &executionContext) const { WriteMTRSimODFInputValues inputValues; - inputValues.outputFile = filterArgs.value(k_OutputFile_Key); - inputValues.hdf5PathPrefix = filterArgs.value(k_Hdf5PathPrefix_Key); - inputValues.inputImageGeometry = filterArgs.value(k_InputImageGeometry_Key); - inputValues.odfComponents = filterArgs.value(k_ODFComponents_Key); - - return WriteMTRSimODF(dataStructure, messageHandler, shouldCancel, &inputValues)(); + inputValues.outputFile = + filterArgs.value(k_OutputFile_Key); + inputValues.hdf5PathPrefix = + filterArgs.value(k_Hdf5PathPrefix_Key); + inputValues.inputImageGeometry = + filterArgs.value(k_InputImageGeometry_Key); + inputValues.odfComponents = + filterArgs.value( + k_ODFComponents_Key); + + return WriteMTRSimODF(dataStructure, messageHandler, shouldCancel, + &inputValues)(); } //------------------------------------------------------------------------------ -Result WriteMTRSimODFFilter::FromSIMPLJson(const nlohmann::json& json) -{ +Result +WriteMTRSimODFFilter::FromSIMPLJson(const nlohmann::json &json) { Arguments args = WriteMTRSimODFFilter().getDefaultArguments(); std::vector> results; @@ -201,7 +236,8 @@ Result WriteMTRSimODFFilter::FromSIMPLJson(const nlohmann::json& json Result<> conversionResult = MergeResults(std::move(results)); - return ConvertResultTo(std::move(conversionResult), std::move(args)); + return ConvertResultTo(std::move(conversionResult), + std::move(args)); } } // namespace nx::core diff --git a/src/MTRSim/Filters/WriteMTRSimODFFilter.hpp b/src/MTRSim/Filters/WriteMTRSimODFFilter.hpp index 726d324..8a20f24 100644 --- a/src/MTRSim/Filters/WriteMTRSimODFFilter.hpp +++ b/src/MTRSim/Filters/WriteMTRSimODFFilter.hpp @@ -6,8 +6,7 @@ #include "simplnx/Filter/FilterTraits.hpp" #include "simplnx/Filter/IFilter.hpp" -namespace nx::core -{ +namespace nx::core { /** * @class WriteMTRSimODFFilter * @brief Writes the selected Float64 single-component cell-data arrays on an @@ -16,22 +15,23 @@ namespace nx::core * on-disk axis order so the output round-trips losslessly with * ReadMTRSimODFFilter. */ -class MTRSIM_EXPORT WriteMTRSimODFFilter : public IFilter -{ +class MTRSIM_EXPORT WriteMTRSimODFFilter : public IFilter { public: WriteMTRSimODFFilter() = default; ~WriteMTRSimODFFilter() noexcept override = default; - WriteMTRSimODFFilter(const WriteMTRSimODFFilter&) = delete; - WriteMTRSimODFFilter(WriteMTRSimODFFilter&&) noexcept = delete; + WriteMTRSimODFFilter(const WriteMTRSimODFFilter &) = delete; + WriteMTRSimODFFilter(WriteMTRSimODFFilter &&) noexcept = delete; - WriteMTRSimODFFilter& operator=(const WriteMTRSimODFFilter&) = delete; - WriteMTRSimODFFilter& operator=(WriteMTRSimODFFilter&&) noexcept = delete; + WriteMTRSimODFFilter &operator=(const WriteMTRSimODFFilter &) = delete; + 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_Hdf5PathPrefix_Key = + "hdf5_path_prefix"; + static inline constexpr StringLiteral k_InputImageGeometry_Key = + "input_image_geometry"; static inline constexpr StringLiteral k_ODFComponents_Key = "odf_components"; /** @@ -39,7 +39,7 @@ class MTRSIM_EXPORT WriteMTRSimODFFilter : public IFilter * @param json * @return Result */ - static Result FromSIMPLJson(const nlohmann::json& json); + static Result FromSIMPLJson(const nlohmann::json &json); /** * @brief Returns the name of the filter. @@ -93,33 +93,52 @@ 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; + 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; + 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, WriteMTRSimODFFilter, "8012f71d-10d9-47bb-9dbf-6250e2c32396"); +SIMPLNX_DEF_FILTER_TRAITS(nx::core, WriteMTRSimODFFilter, + "8012f71d-10d9-47bb-9dbf-6250e2c32396"); diff --git a/src/MTRSim/MTRSimPlugin.cpp b/src/MTRSim/MTRSimPlugin.cpp index e66a26d..970c992 100644 --- a/src/MTRSim/MTRSimPlugin.cpp +++ b/src/MTRSim/MTRSimPlugin.cpp @@ -4,26 +4,25 @@ using namespace nx::core; -namespace -{ +namespace { // Plugin Uuid -constexpr AbstractPlugin::IdType k_ID = *Uuid::FromString("f6bacee6-310a-4853-80f2-8092f4333560"); +constexpr AbstractPlugin::IdType k_ID = + *Uuid::FromString("f6bacee6-310a-4853-80f2-8092f4333560"); } // namespace MTRSimPlugin::MTRSimPlugin() -: AbstractPlugin(k_ID, "MTRSim", "Plugin to hold highly experimental filters", "BlueQuartz Software, LLC") -{ + : AbstractPlugin(k_ID, "MTRSim", + "Plugin to hold highly experimental filters", + "BlueQuartz Software, LLC") { std::vector<::FilterCreationFunc> filterFuncs = ::GetPluginFilterList(); - for(const auto& filterFunc : filterFuncs) - { + for (const auto &filterFunc : filterFuncs) { addFilter(filterFunc); } } MTRSimPlugin::~MTRSimPlugin() = default; -AbstractPlugin::SIMPLMapType MTRSimPlugin::getSimplToSimplnxMap() const -{ +AbstractPlugin::SIMPLMapType MTRSimPlugin::getSimplToSimplnxMap() const { return {}; } diff --git a/src/MTRSim/MTRSimPlugin.hpp b/src/MTRSim/MTRSimPlugin.hpp index 4ec642b..8a38b92 100644 --- a/src/MTRSim/MTRSimPlugin.hpp +++ b/src/MTRSim/MTRSimPlugin.hpp @@ -4,17 +4,16 @@ #include "simplnx/Plugin/AbstractPlugin.hpp" -class MTRSIM_EXPORT MTRSimPlugin : public nx::core::AbstractPlugin -{ +class MTRSIM_EXPORT MTRSimPlugin : public nx::core::AbstractPlugin { public: MTRSimPlugin(); ~MTRSimPlugin() override; - MTRSimPlugin(const MTRSimPlugin&) = delete; - MTRSimPlugin(MTRSimPlugin&&) = delete; + MTRSimPlugin(const MTRSimPlugin &) = delete; + MTRSimPlugin(MTRSimPlugin &&) = delete; - MTRSimPlugin& operator=(const MTRSimPlugin&) = delete; - MTRSimPlugin& operator=(MTRSimPlugin&&) = delete; + MTRSimPlugin &operator=(const MTRSimPlugin &) = delete; + MTRSimPlugin &operator=(MTRSimPlugin &&) = delete; /** * @brief Returns a map of UUIDs as strings, where SIMPL UUIDs are keys to 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 85f4d63..306a6d7 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -246,12 +246,16 @@ int main(int argc, char **argv) { constexpr int k_OdfBinsPhi2 = 72; spdlog::info("Running MTR simulation..."); - mtrsim::MTRSimResult sim = mtrsim::simulateMTR(params, odfComponents, rng, k_OdfBinsPhi1, k_OdfBinsPHI, k_OdfBinsPhi2); + mtrsim::MTRSimResult sim = mtrsim::simulateMTR( + params, odfComponents, rng, k_OdfBinsPhi1, k_OdfBinsPHI, k_OdfBinsPhi2); spdlog::info("MTR simulation complete."); - 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())); + 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 // ──────────────────────────────────────────────────────── @@ -286,7 +290,8 @@ int main(int argc, char **argv) { 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'; + << phi2Vec[i] << ',' << sim.mtrIndex[static_cast(i)] + << '\n'; } } spdlog::info("Results CSV written."); diff --git a/test/ComputeODFTest.cpp b/test/ComputeODFTest.cpp index 0107760..65ebc01 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. */ @@ -49,9 +50,9 @@ using namespace nx::core; using namespace nx::core::UnitTest; -namespace -{ -// EbsdLib crystal-structure codes (matches ebsdlib::CrystalStructure namespace). +namespace { +// EbsdLib crystal-structure codes (matches ebsdlib::CrystalStructure +// namespace). constexpr uint32_t k_HexagonalHigh = 0; constexpr uint32_t k_CubicHigh = 1; @@ -63,16 +64,18 @@ const std::string k_ComponentName = "Component 1"; // Convenience: returns the linear index for the Float64 ODF cell-data array // given (i_phi1, i_PHI, i_phi2). Mirrors the row-major linearization used // inside ODFBuilder (phi1 slowest, phi2 fastest). -[[maybe_unused]] usize odfLinear(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); +[[maybe_unused]] usize odfLinear(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); } -// 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 -{ +// 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; DataPath ebsdGeomPath; DataPath cellAttrMatPath; @@ -83,33 +86,38 @@ struct EbsdInputs DataPath maskPath; }; -EbsdInputs buildSyntheticEbsd(const std::vector>& eulersDeg, const std::vector& phaseLabels, uint32_t crystalCodeForPhase1, const std::vector* maskValues = nullptr) -{ +EbsdInputs +buildSyntheticEbsd(const std::vector> &eulersDeg, + const std::vector &phaseLabels, + uint32_t crystalCodeForPhase1, + const std::vector *maskValues = nullptr) { REQUIRE(eulersDeg.size() == phaseLabels.size()); - if(maskValues != nullptr) - { + if (maskValues != nullptr) { REQUIRE(maskValues->size() == eulersDeg.size()); } 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. - ImageGeom* ebsdGeom = ImageGeom::Create(out.dataStructure, "EBSD"); + // 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"}); - AttributeMatrix* cellAm = AttributeMatrix::Create(out.dataStructure, k_CellAttrMatName, {numVoxels}, ebsdGeom->getId()); + AttributeMatrix *cellAm = AttributeMatrix::Create( + out.dataStructure, k_CellAttrMatName, {numVoxels}, ebsdGeom->getId()); out.cellAttrMatPath = out.ebsdGeomPath.createChildPath(k_CellAttrMatName); - Float32Array* eulerArr = Float32Array::CreateWithStore>(out.dataStructure, "EulerAngles", {numVoxels}, {3}, cellAm->getId()); - Int32Array* phaseArr = Int32Array::CreateWithStore>(out.dataStructure, "Phases", {numVoxels}, {1}, cellAm->getId()); + Float32Array *eulerArr = Float32Array::CreateWithStore>( + out.dataStructure, "EulerAngles", {numVoxels}, {3}, cellAm->getId()); + Int32Array *phaseArr = Int32Array::CreateWithStore>( + out.dataStructure, "Phases", {numVoxels}, {1}, cellAm->getId()); constexpr double k_DegToRad = std::numbers::pi / 180.0; - for(usize i = 0; i < numVoxels; ++i) - { + for (usize i = 0; i < numVoxels; ++i) { (*eulerArr)[3 * i + 0] = static_cast(eulersDeg[i][0] * k_DegToRad); (*eulerArr)[3 * i + 1] = static_cast(eulersDeg[i][1] * k_DegToRad); (*eulerArr)[3 * i + 2] = static_cast(eulersDeg[i][2] * k_DegToRad); @@ -119,68 +127,81 @@ EbsdInputs buildSyntheticEbsd(const std::vector>& eulersD out.eulerAnglesPath = out.cellAttrMatPath.createChildPath("EulerAngles"); out.phasesPath = out.cellAttrMatPath.createChildPath("Phases"); - if(maskValues != nullptr) - { - BoolArray* maskArr = BoolArray::CreateWithStore>(out.dataStructure, "Mask", {numVoxels}, {1}, cellAm->getId()); - for(usize i = 0; i < numVoxels; ++i) - { + if (maskValues != nullptr) { + BoolArray *maskArr = BoolArray::CreateWithStore>( + out.dataStructure, "Mask", {numVoxels}, {1}, cellAm->getId()); + for (usize i = 0; i < numVoxels; ++i) { (*maskArr)[i] = (*maskValues)[i]; } 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. - AttributeMatrix* ensembleAm = AttributeMatrix::Create(out.dataStructure, "EnsembleData", {2}); + // 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"}); - UInt32Array* csArr = UInt32Array::CreateWithStore>(out.dataStructure, "CrystalStructures", {2}, {1}, ensembleAm->getId()); + UInt32Array *csArr = UInt32Array::CreateWithStore>( + out.dataStructure, "CrystalStructures", {2}, {1}, ensembleAm->getId()); (*csArr)[0] = 999; // phase 0 = "no phase" — never indexed by the algorithm (*csArr)[1] = crystalCodeForPhase1; - out.crystalStructuresPath = out.ensembleAttrMatPath.createChildPath("CrystalStructures"); + out.crystalStructuresPath = + out.ensembleAttrMatPath.createChildPath("CrystalStructures"); return out; } -// 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) -{ +// 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; - args.insertOrAssign(ComputeODFFilter::k_ApplySmoothing_Key, std::make_any(applySmoothing)); - args.insertOrAssign(ComputeODFFilter::k_BinSizeDeg_Key, std::make_any(binSizeDeg)); - args.insertOrAssign(ComputeODFFilter::k_EulerAngles_Key, std::make_any(inputs.eulerAnglesPath)); - args.insertOrAssign(ComputeODFFilter::k_Phases_Key, std::make_any(inputs.phasesPath)); - args.insertOrAssign(ComputeODFFilter::k_CrystalStructures_Key, std::make_any(inputs.crystalStructuresPath)); - args.insertOrAssign(ComputeODFFilter::k_UseMask_Key, std::make_any(false)); - args.insertOrAssign(ComputeODFFilter::k_Mask_Key, std::make_any(DataPath{})); - args.insertOrAssign(ComputeODFFilter::k_OutputImageGeometry_Key, std::make_any(k_OutputGeomPath)); - args.insertOrAssign(ComputeODFFilter::k_CellAttrMatName_Key, std::make_any(k_CellAttrMatName)); - args.insertOrAssign(ComputeODFFilter::k_ComponentName_Key, std::make_any(k_ComponentName)); + args.insertOrAssign(ComputeODFFilter::k_ApplySmoothing_Key, + std::make_any(applySmoothing)); + args.insertOrAssign(ComputeODFFilter::k_BinSizeDeg_Key, + std::make_any(binSizeDeg)); + args.insertOrAssign(ComputeODFFilter::k_EulerAngles_Key, + std::make_any(inputs.eulerAnglesPath)); + args.insertOrAssign(ComputeODFFilter::k_Phases_Key, + std::make_any(inputs.phasesPath)); + args.insertOrAssign(ComputeODFFilter::k_CrystalStructures_Key, + std::make_any(inputs.crystalStructuresPath)); + args.insertOrAssign(ComputeODFFilter::k_UseMask_Key, + std::make_any(false)); + args.insertOrAssign(ComputeODFFilter::k_Mask_Key, + std::make_any(DataPath{})); + args.insertOrAssign(ComputeODFFilter::k_OutputImageGeometry_Key, + std::make_any(k_OutputGeomPath)); + args.insertOrAssign( + ComputeODFFilter::k_CellAttrMatName_Key, + std::make_any(k_CellAttrMatName)); + args.insertOrAssign( + ComputeODFFilter::k_ComponentName_Key, + std::make_any(k_ComponentName)); return args; } -// 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(); +// 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(); return errors.empty() ? 0 : errors[0].code; } // Returns (sum, nonZeroBinCount) over the output ODF array. -std::pair sumAndNonZeroCount(const DataStructure& ds) -{ - const DataPath odfPath = k_OutputGeomPath.createChildPath(k_CellAttrMatName).createChildPath(k_ComponentName); - const auto& odfArr = ds.getDataRefAs(odfPath); - const auto& store = odfArr.getDataStoreRef(); +std::pair sumAndNonZeroCount(const DataStructure &ds) { + const DataPath odfPath = k_OutputGeomPath.createChildPath(k_CellAttrMatName) + .createChildPath(k_ComponentName); + const auto &odfArr = ds.getDataRefAs(odfPath); + const auto &store = odfArr.getDataStoreRef(); double sum = 0.0; usize nonZero = 0; - for(usize i = 0; i < store.getSize(); ++i) - { + for (usize i = 0; i < store.getSize(); ++i) { const double v = store[i]; sum += v; - if(v != 0.0) - { + if (v != 0.0) { ++nonZero; } } @@ -189,14 +210,17 @@ std::pair sumAndNonZeroCount(const DataStructure& ds) } // namespace -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. - EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); +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. + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -206,18 +230,22 @@ 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]") -{ - // 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); +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. + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_CubicHigh); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -231,15 +259,18 @@ TEST_CASE("MTRSim::ComputeODFFilter: Single Cubic voxel, no smoothing, produces REQUIRE(nonZero <= 24); } -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. - EbsdInputs inputs = buildSyntheticEbsd({{{12.5f, 12.5f, 12.5f}}}, {1}, k_HexagonalHigh); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); +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. + EbsdInputs inputs = + buildSyntheticEbsd({{{12.5f, 12.5f, 12.5f}}}, {1}, k_HexagonalHigh); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -249,15 +280,18 @@ 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]") -{ +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. @@ -274,8 +308,10 @@ TEST_CASE("MTRSim::ComputeODFFilter: MUD output applies per-row sin(PHI) Jacobia std::vector phases = {1, 1, 1, 1}; EbsdInputs inputsA = buildSyntheticEbsd(eulers, phases, k_HexagonalHigh); - Arguments argsA = makeBaseArgs(inputsA, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); - argsA.insertOrAssign(ComputeODFFilter::k_OutputUnits_Key, std::make_any(0ULL)); + Arguments argsA = + makeBaseArgs(inputsA, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); + argsA.insertOrAssign(ComputeODFFilter::k_OutputUnits_Key, + std::make_any(0ULL)); ComputeODFFilter filterA; auto preflightA = filterA.preflight(inputsA.dataStructure, argsA); SIMPLNX_RESULT_REQUIRE_VALID(preflightA.outputActions); @@ -283,76 +319,88 @@ TEST_CASE("MTRSim::ComputeODFFilter: MUD output applies per-row sin(PHI) Jacobia SIMPLNX_RESULT_REQUIRE_VALID(executeA.result); EbsdInputs inputsB = buildSyntheticEbsd(eulers, phases, k_HexagonalHigh); - Arguments argsB = makeBaseArgs(inputsB, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); - argsB.insertOrAssign(ComputeODFFilter::k_OutputUnits_Key, std::make_any(1ULL)); + Arguments argsB = + makeBaseArgs(inputsB, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); + argsB.insertOrAssign(ComputeODFFilter::k_OutputUnits_Key, + std::make_any(1ULL)); ComputeODFFilter filterB; auto preflightB = filterB.preflight(inputsB.dataStructure, argsB); SIMPLNX_RESULT_REQUIRE_VALID(preflightB.outputActions); auto executeB = filterB.execute(inputsB.dataStructure, argsB); SIMPLNX_RESULT_REQUIRE_VALID(executeB.result); - const DataPath odfPath = k_OutputGeomPath.createChildPath(k_CellAttrMatName).createChildPath(k_ComponentName); - const auto& cdStore = inputsA.dataStructure.getDataRefAs(odfPath).getDataStoreRef(); - const auto& mudStore = inputsB.dataStructure.getDataRefAs(odfPath).getDataStoreRef(); + const DataPath odfPath = k_OutputGeomPath.createChildPath(k_CellAttrMatName) + .createChildPath(k_ComponentName); + const auto &cdStore = + inputsA.dataStructure.getDataRefAs(odfPath) + .getDataStoreRef(); + const auto &mudStore = + inputsB.dataStructure.getDataRefAs(odfPath) + .getDataStoreRef(); REQUIRE(cdStore.getSize() == mudStore.getSize()); constexpr int32_t nphi1 = 72; constexpr int32_t nPHI = 36; constexpr int32_t nphi2 = 72; - REQUIRE(cdStore.getSize() == static_cast(nphi1) * static_cast(nPHI) * static_cast(nphi2)); + REQUIRE(cdStore.getSize() == static_cast(nphi1) * + static_cast(nPHI) * + static_cast(nphi2)); const double stepRad = 5.0 * std::numbers::pi / 180.0; - const double scale = 8.0 * std::numbers::pi * std::numbers::pi / (stepRad * stepRad * stepRad); + const double scale = + 8.0 * std::numbers::pi * std::numbers::pi / (stepRad * stepRad * stepRad); std::size_t binsCompared = 0; std::size_t binsFailed = 0; double maxRelErr = 0.0; - for(int32_t j = 0; j < nPHI; ++j) - { + for (int32_t j = 0; j < nPHI; ++j) { const double phiCenter = (static_cast(j) + 0.5) * stepRad; const double expectedRowMul = scale / std::sin(phiCenter); - for(int32_t i = 0; i < nphi1; ++i) - { - for(int32_t k = 0; k < nphi2; ++k) - { + for (int32_t i = 0; i < nphi1; ++i) { + for (int32_t k = 0; k < nphi2; ++k) { const usize idx = odfLinear(i, j, k, nPHI, nphi2); const double cd = cdStore[idx]; const double mud = mudStore[idx]; - if(cd == 0.0) - { + if (cd == 0.0) { REQUIRE(mud == 0.0); continue; } ++binsCompared; const double expected = cd * expectedRowMul; - const double rel = std::abs(mud - expected) / std::max(std::abs(expected), 1.0e-300); - if(rel > maxRelErr) - { + const double rel = + std::abs(mud - expected) / std::max(std::abs(expected), 1.0e-300); + if (rel > maxRelErr) { maxRelErr = rel; } - if(rel > 1.0e-12) - { + if (rel > 1.0e-12) { ++binsFailed; } } } } - INFO(fmt::format("compared {} non-zero bins; max relative error = {:.3e}; failing = {}", binsCompared, maxRelErr, binsFailed)); + INFO(fmt::format( + "compared {} non-zero bins; max relative error = {:.3e}; failing = {}", + binsCompared, maxRelErr, binsFailed)); REQUIRE(binsCompared > 0); REQUIRE(binsFailed == 0); } -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 +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 // 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); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); - args.insertOrAssign(ComputeODFFilter::k_OutputUnits_Key, std::make_any(1ULL)); + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}, {{50.0f, 70.0f, 110.0f}}}, + {1, 1}, k_HexagonalHigh); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); + args.insertOrAssign(ComputeODFFilter::k_OutputUnits_Key, + std::make_any(1ULL)); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -360,8 +408,11 @@ TEST_CASE("MTRSim::ComputeODFFilter: MUD integrates to 8*pi^2 on SO(3)", "[MTRSi auto executeResult = filter.execute(inputs.dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); - const DataPath odfPath = k_OutputGeomPath.createChildPath(k_CellAttrMatName).createChildPath(k_ComponentName); - const auto& mudStore = inputs.dataStructure.getDataRefAs(odfPath).getDataStoreRef(); + const DataPath odfPath = k_OutputGeomPath.createChildPath(k_CellAttrMatName) + .createChildPath(k_ComponentName); + const auto &mudStore = + inputs.dataStructure.getDataRefAs(odfPath) + .getDataStoreRef(); constexpr int32_t nphi1 = 72; constexpr int32_t nPHI = 36; @@ -370,14 +421,12 @@ TEST_CASE("MTRSim::ComputeODFFilter: MUD integrates to 8*pi^2 on SO(3)", "[MTRSi const double step3 = stepRad * stepRad * stepRad; double integral = 0.0; - for(int32_t j = 0; j < nPHI; ++j) - { - const double rowVol = step3 * std::sin((static_cast(j) + 0.5) * stepRad); + for (int32_t j = 0; j < nPHI; ++j) { + const double rowVol = + step3 * std::sin((static_cast(j) + 0.5) * stepRad); double rowSum = 0.0; - for(int32_t i = 0; i < nphi1; ++i) - { - for(int32_t k = 0; k < nphi2; ++k) - { + for (int32_t i = 0; i < nphi1; ++i) { + for (int32_t k = 0; k < nphi2; ++k) { rowSum += mudStore[odfLinear(i, j, k, nPHI, nphi2)]; } } @@ -387,23 +436,31 @@ TEST_CASE("MTRSim::ComputeODFFilter: MUD integrates to 8*pi^2 on SO(3)", "[MTRSi // 8*pi^2 ≈ 78.9568. Tolerance of 1e-9 is comfortable since both sides are // pure double-precision arithmetic with no symmetry-cancellation traps. const double expected = 8.0 * std::numbers::pi * std::numbers::pi; - INFO(fmt::format("integral = {:.10f}, expected = {:.10f}", integral, expected)); + INFO(fmt::format("integral = {:.10f}, expected = {:.10f}", integral, + expected)); REQUIRE(integral == Approx(expected).margin(1.0e-9)); } -TEST_CASE("MTRSim::ComputeODFFilter: Mask filters voxels", "[MTRSim][ComputeODFFilter]") -{ +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. - 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}}}; + // 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}; - EbsdInputs inputs = buildSyntheticEbsd(eulers, phases, k_HexagonalHigh, &mask); + EbsdInputs inputs = + buildSyntheticEbsd(eulers, phases, k_HexagonalHigh, &mask); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); - args.insertOrAssign(ComputeODFFilter::k_UseMask_Key, std::make_any(true)); - args.insertOrAssign(ComputeODFFilter::k_Mask_Key, std::make_any(inputs.maskPath)); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); + args.insertOrAssign(ComputeODFFilter::k_UseMask_Key, + std::make_any(true)); + args.insertOrAssign(ComputeODFFilter::k_Mask_Key, + std::make_any(inputs.maskPath)); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -416,16 +473,20 @@ TEST_CASE("MTRSim::ComputeODFFilter: Mask filters voxels", "[MTRSim][ComputeODFF REQUIRE(nonZero >= 1); } -TEST_CASE("MTRSim::ComputeODFFilter: Phase 0 voxels are skipped", "[MTRSim][ComputeODFFilter]") -{ +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. - 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}}}; + // 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); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -438,16 +499,20 @@ TEST_CASE("MTRSim::ComputeODFFilter: Phase 0 voxels are skipped", "[MTRSim][Comp REQUIRE(nonZero >= 1); } -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. - 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}}}; +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. + 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); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -460,10 +525,12 @@ TEST_CASE("MTRSim::ComputeODFFilter: Empty mask DataPath disables the mask", "[M REQUIRE(nonZero >= 1); } -TEST_CASE("MTRSim::ComputeODFFilter: Preflight rejects bad bin size", "[MTRSim][ComputeODFFilter]") -{ - EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/7.0f); +TEST_CASE("MTRSim::ComputeODFFilter: Preflight rejects bad bin size", + "[MTRSim][ComputeODFFilter]") { + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/7.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -471,44 +538,62 @@ 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]") -{ - // 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); +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. + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); // Add an alternate single-component Float32 array on the same cell AM. - AttributeMatrix& cellAm = inputs.dataStructure.getDataRefAs(inputs.cellAttrMatPath); - Float32Array::CreateWithStore>(inputs.dataStructure, "BadEulers", {1}, {1}, cellAm.getId()); + AttributeMatrix &cellAm = inputs.dataStructure.getDataRefAs( + inputs.cellAttrMatPath); + Float32Array::CreateWithStore>( + inputs.dataStructure, "BadEulers", {1}, {1}, cellAm.getId()); const DataPath badPath = inputs.cellAttrMatPath.createChildPath("BadEulers"); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); - args.insertOrAssign(ComputeODFFilter::k_EulerAngles_Key, std::make_any(badPath)); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); + args.insertOrAssign(ComputeODFFilter::k_EulerAngles_Key, + std::make_any(badPath)); 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); } -TEST_CASE("MTRSim::ComputeODFFilter: Preflight rejects mismatched attribute matrices", "[MTRSim][ComputeODFFilter]") -{ - 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. - 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()); +TEST_CASE( + "MTRSim::ComputeODFFilter: Preflight rejects mismatched attribute matrices", + "[MTRSim][ComputeODFFilter]") { + 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. + 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()); (*otherPhases)[0] = 1; - const DataPath otherPhasesPath = inputs.ebsdGeomPath.createChildPath("OtherCellData").createChildPath("Phases"); + const DataPath otherPhasesPath = + inputs.ebsdGeomPath.createChildPath("OtherCellData") + .createChildPath("Phases"); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); - args.insertOrAssign(ComputeODFFilter::k_Phases_Key, std::make_any(otherPhasesPath)); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); + args.insertOrAssign(ComputeODFFilter::k_Phases_Key, + std::make_any(otherPhasesPath)); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -516,10 +601,12 @@ TEST_CASE("MTRSim::ComputeODFFilter: Preflight rejects mismatched attribute matr REQUIRE(firstErrorCode(preflightResult) == -12205); } -TEST_CASE("MTRSim::ComputeODFFilter: Preflight populates updatedValues", "[MTRSim][ComputeODFFilter]") -{ - EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); +TEST_CASE("MTRSim::ComputeODFFilter: Preflight populates updatedValues", + "[MTRSim][ComputeODFFilter]") { + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -527,52 +614,63 @@ TEST_CASE("MTRSim::ComputeODFFilter: Preflight populates updatedValues", "[MTRSi REQUIRE(!preflightResult.outputValues.empty()); - const bool foundBinSize = std::any_of(preflightResult.outputValues.begin(), preflightResult.outputValues.end(), [](const IFilter::PreflightValue& v) { return v.name == "Bin Size [deg]"; }); + const bool foundBinSize = std::any_of(preflightResult.outputValues.begin(), + preflightResult.outputValues.end(), + [](const IFilter::PreflightValue &v) { + return v.name == "Bin Size [deg]"; + }); REQUIRE(foundBinSize); - const bool foundBins = std::any_of(preflightResult.outputValues.begin(), preflightResult.outputValues.end(), [](const IFilter::PreflightValue& v) { return v.name == "Bins (phi1 x PHI x phi2)"; }); + const bool foundBins = std::any_of( + preflightResult.outputValues.begin(), preflightResult.outputValues.end(), + [](const IFilter::PreflightValue &v) { + return v.name == "Bins (phi1 x PHI x phi2)"; + }); REQUIRE(foundBins); } // ----------------------------------------------------------------------------- -// 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. -struct ExistingOdfGeom -{ +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. +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"); + ImageGeom *geom = ImageGeom::Create(ds, "ODF"); geom->setDimensions({dimsXYZ[0], dimsXYZ[1], dimsXYZ[2]}); geom->setSpacing({spacingXYZ[0], spacingXYZ[1], spacingXYZ[2]}); geom->setOrigin({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. + AttributeMatrix *cellAm = AttributeMatrix::Create( + ds, k_CellAttrMatName, tupleShapeZYX, geom->getId()); + // Register the AttributeMatrix with the ImageGeom as its cell data so + // getCellDataPath() works. geom->setCellData(cellAm->getId()); out.cellAttrMatPath = out.geomPath.createChildPath(k_CellAttrMatName); - if(seedComponent1) - { - Float64Array* seedArr = Float64Array::CreateWithStore>(ds, k_ComponentName, tupleShapeZYX, {1}, cellAm->getId()); - auto& store = seedArr->getDataStoreRef(); - for(usize i = 0; i < store.getSize(); ++i) - { + if (seedComponent1) { + Float64Array *seedArr = Float64Array::CreateWithStore>( + ds, k_ComponentName, tupleShapeZYX, {1}, cellAm->getId()); + auto &store = seedArr->getDataStoreRef(); + for (usize i = 0; i < store.getSize(); ++i) { store[i] = seedValue; } } @@ -580,18 +678,16 @@ ExistingOdfGeom buildExistingOdfGeom(DataStructure& ds, const std::array sumAndNonZeroOf(const DataStructure& ds, const DataPath& path) -{ - const auto& arr = ds.getDataRefAs(path); - const auto& store = arr.getDataStoreRef(); +std::pair sumAndNonZeroOf(const DataStructure &ds, + const DataPath &path) { + const auto &arr = ds.getDataRefAs(path); + const auto &store = arr.getDataStoreRef(); double sum = 0.0; usize nonZero = 0; - for(usize i = 0; i < store.getSize(); ++i) - { + for (usize i = 0; i < store.getSize(); ++i) { const double v = store[i]; sum += v; - if(v != 0.0) - { + if (v != 0.0) { ++nonZero; } } @@ -599,14 +695,17 @@ 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]") -{ - // 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); +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). + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); { - Arguments createArgs = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); + Arguments createArgs = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, createArgs); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); @@ -614,16 +713,25 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode adds a new component to an exis SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); } - const DataPath component1Path = k_OutputGeomPath.createChildPath(k_CellAttrMatName).createChildPath(k_ComponentName); - const auto [sum1Before, nonZero1Before] = sumAndNonZeroOf(inputs.dataStructure, component1Path); + const DataPath component1Path = + k_OutputGeomPath.createChildPath(k_CellAttrMatName) + .createChildPath(k_ComponentName); + 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)); - appendArgs.insertOrAssign(ComputeODFFilter::k_ExistingOdfGeometry_Key, std::make_any(k_OutputGeomPath)); - appendArgs.insertOrAssign(ComputeODFFilter::k_ComponentName_Key, std::make_any(k_AppendedComponentName)); + 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)); + appendArgs.insertOrAssign(ComputeODFFilter::k_ComponentName_Key, + std::make_any( + k_AppendedComponentName)); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, appendArgs); @@ -631,46 +739,63 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode adds a new component to an exis auto executeResult = filter.execute(inputs.dataStructure, appendArgs); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); - const DataPath component2Path = k_OutputGeomPath.createChildPath(k_CellAttrMatName).createChildPath(k_AppendedComponentName); - REQUIRE_NOTHROW(inputs.dataStructure.getDataRefAs(component2Path)); - const auto [sum2, nonZero2] = sumAndNonZeroOf(inputs.dataStructure, component2Path); + const DataPath component2Path = + k_OutputGeomPath.createChildPath(k_CellAttrMatName) + .createChildPath(k_AppendedComponentName); + REQUIRE_NOTHROW( + inputs.dataStructure.getDataRefAs(component2Path)); + const auto [sum2, nonZero2] = + sumAndNonZeroOf(inputs.dataStructure, component2Path); 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). - const auto& component1Arr = inputs.dataStructure.getDataRefAs(component1Path); - const auto& component2Arr = inputs.dataStructure.getDataRefAs(component2Path); - REQUIRE(component2Arr.getNumberOfTuples() == component1Arr.getNumberOfTuples()); + // 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()); // Component 1 must be unchanged. - const auto [sum1After, nonZero1After] = sumAndNonZeroOf(inputs.dataStructure, component1Path); + const auto [sum1After, nonZero1After] = + sumAndNonZeroOf(inputs.dataStructure, component1Path); REQUIRE(sum1After == Approx(sum1Before).margin(1.0e-9)); REQUIRE(nonZero1After == nonZero1Before); } -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. - EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); +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. + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); buildExistingOdfGeom(inputs.dataStructure, {72, 36, 72}, {5.0f, 5.0f, 6.0f}); - 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)); - appendArgs.insertOrAssign(ComputeODFFilter::k_ComponentName_Key, std::make_any(std::string{"Component 2"})); + 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)); + appendArgs.insertOrAssign(ComputeODFFilter::k_ComponentName_Key, + std::make_any( + std::string{"Component 2"})); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, appendArgs); REQUIRE(preflightResult.outputActions.invalid()); bool found12207 = false; - for(const auto& err : preflightResult.outputActions.errors()) - { - if(err.code == -12207) - { + for (const auto &err : preflightResult.outputActions.errors()) { + if (err.code == -12207) { 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; } @@ -678,25 +803,31 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects non-uniform spacing", " REQUIRE(found12207); } -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. - 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); - - 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. +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. + 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); + + 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. ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, appendArgs); REQUIRE(preflightResult.outputActions.invalid()); bool found12209 = false; - for(const auto& err : preflightResult.outputActions.errors()) - { - if(err.code == -12209) - { + for (const auto &err : preflightResult.outputActions.errors()) { + if (err.code == -12209) { found12209 = true; break; } @@ -704,52 +835,68 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects component-name collisio REQUIRE(found12209); } -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. - EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); - buildExistingOdfGeom(inputs.dataStructure, {144, 72, 144}, {2.5f, 2.5f, 2.5f}); - - Arguments appendArgs = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/999.0f); - appendArgs.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, std::make_any(1ULL)); - appendArgs.insertOrAssign(ComputeODFFilter::k_ExistingOdfGeometry_Key, std::make_any(k_OutputGeomPath)); - appendArgs.insertOrAssign(ComputeODFFilter::k_ComponentName_Key, std::make_any(std::string{"Component 2"})); +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. + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); + buildExistingOdfGeom(inputs.dataStructure, {144, 72, 144}, + {2.5f, 2.5f, 2.5f}); + + Arguments appendArgs = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/999.0f); + appendArgs.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, + std::make_any(1ULL)); + appendArgs.insertOrAssign(ComputeODFFilter::k_ExistingOdfGeometry_Key, + std::make_any(k_OutputGeomPath)); + appendArgs.insertOrAssign(ComputeODFFilter::k_ComponentName_Key, + std::make_any( + std::string{"Component 2"})); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, appendArgs); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - const auto it = std::find_if(preflightResult.outputValues.begin(), preflightResult.outputValues.end(), [](const IFilter::PreflightValue& v) { return v.name == "Bin Size [deg]"; }); + const auto it = std::find_if(preflightResult.outputValues.begin(), + preflightResult.outputValues.end(), + [](const IFilter::PreflightValue &v) { + return v.name == "Bin Size [deg]"; + }); REQUIRE(it != preflightResult.outputValues.end()); // Derived bin size should be 2.5, NOT 999.0. REQUIRE(it->value.find("2.5") != std::string::npos); REQUIRE(it->value.find("999") == std::string::npos); } -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. - EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, /*crystalCode=*/999u); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); +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. + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, /*crystalCode=*/999u); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); 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. - const auto& warnings = executeResult.result.warnings(); + const auto &warnings = executeResult.result.warnings(); bool found12213 = false; - for(const auto& w : warnings) - { - if(w.code == -12213) - { + for (const auto &w : warnings) { + if (w.code == -12213) { found12213 = true; REQUIRE(w.message.find("1 voxel") != std::string::npos); break; @@ -763,23 +910,30 @@ TEST_CASE("MTRSim::ComputeODFFilter: Unknown crystal code produces warning not e REQUIRE(nonZeroCount == 0); } -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. - EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); +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. + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); // Plant a DataGroup at /ODF instead of an ImageGeom. - auto* dg = DataGroup::Create(inputs.dataStructure, "ODF"); + auto *dg = DataGroup::Create(inputs.dataStructure, "ODF"); 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_ExistingOdfGeometry_Key, std::make_any(DataPath({"ODF"}))); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); + args.insertOrAssign( + ComputeODFFilter::k_OutputMode_Key, + std::make_any(1ULL)); // Append + args.insertOrAssign(ComputeODFFilter::k_ExistingOdfGeometry_Key, + std::make_any(DataPath({"ODF"}))); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -787,23 +941,29 @@ TEST_CASE("MTRSim::ComputeODFFilter: Append mode rejects non-ImageGeom geometry" REQUIRE(firstErrorCode(preflightResult) == -3); } -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. - EbsdInputs inputs = buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); +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. + EbsdInputs inputs = + buildSyntheticEbsd({{{10.0f, 20.0f, 30.0f}}}, {1}, k_HexagonalHigh); // Create a bare ImageGeom WITHOUT calling setCellData. - auto* geom = ImageGeom::Create(inputs.dataStructure, "ODF"); + auto *geom = ImageGeom::Create(inputs.dataStructure, "ODF"); REQUIRE(geom != nullptr); geom->setDimensions({72, 36, 72}); geom->setSpacing({5.0f, 5.0f, 5.0f}); geom->setOrigin({0.0f, 0.0f, 0.0f}); // Intentionally skip setCellData — leaves getCellData() == nullptr. - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); - args.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, std::make_any(1ULL)); - args.insertOrAssign(ComputeODFFilter::k_ExistingOdfGeometry_Key, std::make_any(DataPath({"ODF"}))); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/false, /*binSizeDeg=*/5.0f); + args.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, + std::make_any(1ULL)); + args.insertOrAssign(ComputeODFFilter::k_ExistingOdfGeometry_Key, + std::make_any(DataPath({"ODF"}))); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -822,8 +982,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 // by matlab/run_validation.m which runs calc_ODF.m on the SAME 12-orientation @@ -856,37 +1017,35 @@ 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). // It mirrors what calc_ODF.m receives (phi1/PHI/phi2 in radians). - EbsdInputs inputs = buildSyntheticEbsd(eulersDeg, std::vector(eulersDeg.size(), 1), k_HexagonalHigh); - Arguments args = makeBaseArgs(inputs, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); + EbsdInputs inputs = buildSyntheticEbsd( + eulersDeg, std::vector(eulersDeg.size(), 1), k_HexagonalHigh); + Arguments args = + makeBaseArgs(inputs, /*applySmoothing=*/true, /*binSizeDeg=*/5.0f); ComputeODFFilter filter; auto preflightResult = filter.preflight(inputs.dataStructure, args); @@ -910,36 +1069,43 @@ TEST_CASE("MTRSim::ComputeODFFilter: Targeted MATLAB calc_ODF.m bin-by-bin valid // v1: original boundary-stress fixture (deprecated; FP-precision pathology) // v2: FP-precision-safe mid-bin fixture constexpr int64 k_TargetedFixtureVersion = 2; - const fs::path refPath = fs::path(fmt::format("{}/calc_odf_reference_targeted.h5", unit_test::k_DataDir.view())); - const std::string regenerateMsg = "run `matlab -batch \"addpath('matlab'); run_validation();\"` " - "from the repo root to (re)generate it. Skipping bin-by-bin comparison."; - if(!fs::exists(refPath)) - { - WARN("Reference HDF5 not present at " << refPath.string() << " - " << regenerateMsg); + const fs::path refPath = fs::path(fmt::format( + "{}/calc_odf_reference_targeted.h5", unit_test::k_DataDir.view())); + const std::string regenerateMsg = + "run `matlab -batch \"addpath('matlab'); run_validation();\"` " + "from the repo root to (re)generate it. Skipping bin-by-bin comparison."; + if (!fs::exists(refPath)) { + WARN("Reference HDF5 not present at " << refPath.string() << " - " + << regenerateMsg); return; } - const std::optional refFixtureVersion = mtrsim::tryReadFixtureVersion(refPath.string()); - 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); + const std::optional refFixtureVersion = + mtrsim::tryReadFixtureVersion(refPath.string()); + 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); return; } - if(*refFixtureVersion != k_TargetedFixtureVersion) - { - WARN("Reference HDF5 fixture_version mismatch: file has v" << *refFixtureVersion << " but test expects v" << k_TargetedFixtureVersion << ". " << regenerateMsg); + if (*refFixtureVersion != k_TargetedFixtureVersion) { + WARN("Reference HDF5 fixture_version mismatch: file has v" + << *refFixtureVersion << " but test expects v" + << k_TargetedFixtureVersion << ". " << regenerateMsg); return; } const auto refComponents = mtrsim::readODFComponents(refPath.string()); REQUIRE(refComponents.size() == 1); - const auto& refValues = refComponents[0].values; + const auto &refValues = refComponents[0].values; // Compute output ODF array (default geometry/cell-attr-mat/component path, // mirroring sumAndNonZeroCount above). - const DataPath odfPath = k_OutputGeomPath.createChildPath(k_CellAttrMatName).createChildPath(k_ComponentName); - const auto& outArr = inputs.dataStructure.getDataRefAs(odfPath); - const auto& outStore = outArr.getDataStoreRef(); + const DataPath odfPath = k_OutputGeomPath.createChildPath(k_CellAttrMatName) + .createChildPath(k_ComponentName); + const auto &outArr = inputs.dataStructure.getDataRefAs(odfPath); + const auto &outStore = outArr.getDataStoreRef(); REQUIRE(outStore.getSize() == refValues.size()); // Bin-by-bin diff. Tolerance 1e-9 -- both sides do the same arithmetic in @@ -951,36 +1117,41 @@ TEST_CASE("MTRSim::ComputeODFFilter: Targeted MATLAB calc_ODF.m bin-by-bin valid std::size_t failingBins = 0; std::size_t firstFailingIndex = SIZE_MAX; constexpr double k_Tolerance = 1.0e-9; - for(std::size_t i = 0; i < refValues.size(); ++i) - { - const double diff = std::abs(static_cast(outStore[i]) - refValues[i]); - if(diff > maxAbsDiff) - { + for (std::size_t i = 0; i < refValues.size(); ++i) { + const double diff = + std::abs(static_cast(outStore[i]) - refValues[i]); + if (diff > maxAbsDiff) { maxAbsDiff = diff; } sumSqDiff += diff * diff; - if(diff > k_Tolerance) - { - if(firstFailingIndex == SIZE_MAX) - { + if (diff > k_Tolerance) { + if (firstFailingIndex == SIZE_MAX) { firstFailingIndex = i; } ++failingBins; } } - 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)); - if(failingBins > 0) - { - 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]))); + 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)); + if (failingBins > 0) { + 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 // ~357k masked phase-1 voxels, and bin-by-bin diff against the MATLAB @@ -995,16 +1166,19 @@ TEST_CASE("MTRSim::ComputeODFFilter: Realistic 640x640 HCP scan validation again // before invoking the filter. UnitTest::LoadPlugins(); - const fs::path inputFile = fs::path(fmt::format("{}/real_world_microtexture_data.dream3d", unit_test::k_DataDir.view())); - const fs::path refFile = fs::path(fmt::format("{}/calc_odf_reference_realistic.h5", unit_test::k_DataDir.view())); - if(!fs::exists(inputFile)) - { - WARN("Realistic EBSD fixture not present at " << inputFile.string() << " - skipping."); + const fs::path inputFile = fs::path(fmt::format( + "{}/real_world_microtexture_data.dream3d", unit_test::k_DataDir.view())); + const fs::path refFile = fs::path(fmt::format( + "{}/calc_odf_reference_realistic.h5", unit_test::k_DataDir.view())); + if (!fs::exists(inputFile)) { + WARN("Realistic EBSD fixture not present at " << inputFile.string() + << " - skipping."); return; } - if(!fs::exists(refFile)) - { - WARN("Realistic MATLAB reference not present at " << refFile.string() << " - run matlab/run_validation.m to generate it. Skipping."); + if (!fs::exists(refFile)) { + WARN("Realistic MATLAB reference not present at " + << refFile.string() + << " - run matlab/run_validation.m to generate it. Skipping."); return; } @@ -1014,7 +1188,9 @@ TEST_CASE("MTRSim::ComputeODFFilter: Realistic 640x640 HCP scan validation again ReadDREAM3DFilter readFilter; Arguments readArgs; Dream3dImportParameter::ImportData importData(inputFile); - readArgs.insertOrAssign(ReadDREAM3DFilter::k_ImportFileData, std::make_any(importData)); + readArgs.insertOrAssign( + ReadDREAM3DFilter::k_ImportFileData, + std::make_any(importData)); auto preflight = readFilter.preflight(dataStructure, readArgs); SIMPLNX_RESULT_REQUIRE_VALID(preflight.outputActions); auto execute = readFilter.execute(dataStructure, readArgs); @@ -1024,63 +1200,77 @@ TEST_CASE("MTRSim::ComputeODFFilter: Realistic 640x640 HCP scan validation again // Paths inside the loaded DataStructure (per the .dream3d's internal layout). const DataPath geomPath({"16_Micro_Res"}); const DataPath cellAttrMatPath = geomPath.createChildPath("CellData"); - const DataPath eulerAnglesPath = cellAttrMatPath.createChildPath("EulerAngles"); + const DataPath eulerAnglesPath = + cellAttrMatPath.createChildPath("EulerAngles"); const DataPath phasesPath = cellAttrMatPath.createChildPath("Phases"); const DataPath maskUInt8Path = cellAttrMatPath.createChildPath("Mask"); - const DataPath crystalStructuresPath = geomPath.createChildPath("CellEnsembleData").createChildPath("CrystalStructures"); + 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); + const auto *maskBase = dataStructure.getDataAs(maskUInt8Path); REQUIRE(maskBase != nullptr); INFO("Loaded Mask type name: " << maskBase->getTypeName()); const usize numTuples = maskBase->getNumberOfTuples(); - auto* cellAm = dataStructure.getDataAs(cellAttrMatPath); + auto *cellAm = dataStructure.getDataAs(cellAttrMatPath); REQUIRE(cellAm != nullptr); - auto* maskBool = BoolArray::CreateWithStore>(dataStructure, "MaskBool", {numTuples}, {1}, cellAm->getId()); + auto *maskBool = BoolArray::CreateWithStore>( + dataStructure, "MaskBool", {numTuples}, {1}, cellAm->getId()); REQUIRE(maskBool != nullptr); - auto& boolStore = maskBool->getDataStoreRef(); + auto &boolStore = maskBool->getDataStoreRef(); - if(const auto* maskU8 = dataStructure.getDataAs(maskUInt8Path); maskU8 != nullptr) - { - const auto& u8Store = maskU8->getDataStoreRef(); - for(usize i = 0; i < numTuples; ++i) - { + if (const auto *maskU8 = dataStructure.getDataAs(maskUInt8Path); + maskU8 != nullptr) { + const auto &u8Store = maskU8->getDataStoreRef(); + for (usize i = 0; i < numTuples; ++i) { boolStore.setValue(i, u8Store.getValue(i) != 0); } - } - else if(const auto* maskBl = dataStructure.getDataAs(maskUInt8Path); maskBl != nullptr) - { - const auto& blStore = maskBl->getDataStoreRef(); - for(usize i = 0; i < numTuples; ++i) - { + } else if (const auto *maskBl = + dataStructure.getDataAs(maskUInt8Path); + maskBl != nullptr) { + const auto &blStore = maskBl->getDataStoreRef(); + for (usize i = 0; i < numTuples; ++i) { boolStore.setValue(i, blStore.getValue(i)); } - } - else - { - FAIL("Mask array at " + maskUInt8Path.toString() + " is neither UInt8Array nor BoolArray"); + } else { + FAIL("Mask array at " + maskUInt8Path.toString() + + " is neither UInt8Array nor BoolArray"); } } // --- Run ComputeODFFilter ComputeODFFilter filter; Arguments args; - args.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, std::make_any(0ULL)); - args.insertOrAssign(ComputeODFFilter::k_ApplySmoothing_Key, std::make_any(true)); - args.insertOrAssign(ComputeODFFilter::k_BinSizeDeg_Key, std::make_any(5.0f)); - args.insertOrAssign(ComputeODFFilter::k_EulerAngles_Key, std::make_any(eulerAnglesPath)); - args.insertOrAssign(ComputeODFFilter::k_Phases_Key, std::make_any(phasesPath)); - args.insertOrAssign(ComputeODFFilter::k_CrystalStructures_Key, std::make_any(crystalStructuresPath)); - args.insertOrAssign(ComputeODFFilter::k_UseMask_Key, std::make_any(true)); - args.insertOrAssign(ComputeODFFilter::k_Mask_Key, std::make_any(maskBoolPath)); - args.insertOrAssign(ComputeODFFilter::k_OutputImageGeometry_Key, std::make_any(DataPath({"ODF"}))); - args.insertOrAssign(ComputeODFFilter::k_CellAttrMatName_Key, std::make_any(std::string{"Cell Data"})); - args.insertOrAssign(ComputeODFFilter::k_ComponentName_Key, std::make_any(std::string{"Component 1"})); + args.insertOrAssign(ComputeODFFilter::k_OutputMode_Key, + std::make_any(0ULL)); + args.insertOrAssign(ComputeODFFilter::k_ApplySmoothing_Key, + std::make_any(true)); + args.insertOrAssign(ComputeODFFilter::k_BinSizeDeg_Key, + std::make_any(5.0f)); + args.insertOrAssign(ComputeODFFilter::k_EulerAngles_Key, + std::make_any(eulerAnglesPath)); + args.insertOrAssign(ComputeODFFilter::k_Phases_Key, + std::make_any(phasesPath)); + args.insertOrAssign(ComputeODFFilter::k_CrystalStructures_Key, + std::make_any(crystalStructuresPath)); + args.insertOrAssign(ComputeODFFilter::k_UseMask_Key, + std::make_any(true)); + args.insertOrAssign(ComputeODFFilter::k_Mask_Key, + std::make_any(maskBoolPath)); + args.insertOrAssign(ComputeODFFilter::k_OutputImageGeometry_Key, + std::make_any(DataPath({"ODF"}))); + args.insertOrAssign(ComputeODFFilter::k_CellAttrMatName_Key, + std::make_any( + std::string{"Cell Data"})); + args.insertOrAssign(ComputeODFFilter::k_ComponentName_Key, + std::make_any( + std::string{"Component 1"})); auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); @@ -1090,10 +1280,11 @@ TEST_CASE("MTRSim::ComputeODFFilter: Realistic 640x640 HCP scan validation again // --- Diff bin-by-bin against the MATLAB reference const auto refComponents = mtrsim::readODFComponents(refFile); REQUIRE(refComponents.size() == 1); - const auto& refValues = refComponents[0].values; + const auto &refValues = refComponents[0].values; - const auto& outArr = dataStructure.getDataRefAs(DataPath({"ODF", "Cell Data", "Component 1"})); - const auto& outStore = outArr.getDataStoreRef(); + const auto &outArr = dataStructure.getDataRefAs( + DataPath({"ODF", "Cell Data", "Component 1"})); + const auto &outStore = outArr.getDataStoreRef(); REQUIRE(outStore.getSize() == refValues.size()); double maxAbsDiff = 0.0; @@ -1101,30 +1292,34 @@ TEST_CASE("MTRSim::ComputeODFFilter: Realistic 640x640 HCP scan validation again std::size_t failingBins = 0; std::size_t firstFailingIndex = SIZE_MAX; constexpr double k_Tolerance = 1.0e-9; - for(std::size_t i = 0; i < refValues.size(); ++i) - { - const double diff = std::abs(static_cast(outStore[i]) - refValues[i]); - if(diff > maxAbsDiff) - { + for (std::size_t i = 0; i < refValues.size(); ++i) { + const double diff = + std::abs(static_cast(outStore[i]) - refValues[i]); + if (diff > maxAbsDiff) { maxAbsDiff = diff; } sumSqDiff += diff * diff; - if(diff > k_Tolerance) - { - if(firstFailingIndex == SIZE_MAX) - { + if (diff > k_Tolerance) { + if (firstFailingIndex == SIZE_MAX) { firstFailingIndex = i; } ++failingBins; } } - 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)); - if(failingBins > 0) - { - 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]))); + 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)); + if (failingBins > 0) { + 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 index 66586a1..23db9da 100644 --- a/test/MTRSimTest.cpp +++ b/test/MTRSimTest.cpp @@ -7,7 +7,8 @@ * 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 (-13005). + * 5. Theta List rows with wrong column count (2 instead of 3) -> invalid + * (-13005). */ #include @@ -30,8 +31,7 @@ using namespace nx::core; using namespace nx::core::UnitTest; -namespace -{ +namespace { constexpr usize k_NPhi1 = 72; constexpr usize k_NPHI = 36; constexpr usize k_NPhi2 = 72; @@ -42,58 +42,73 @@ 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()); +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) + 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()); + 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 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()); + 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 MakeValidArgs(const std::vector &compPaths) { Arguments args; 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_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_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")); + 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; } } // namespace -TEST_CASE("MTRSim::MTRSimFilter: Valid preflight builds actions", "[MTRSim][MTRSimFilter]") -{ +TEST_CASE("MTRSim::MTRSimFilter: Valid preflight builds actions", + "[MTRSim][MTRSimFilter]") { UnitTest::LoadPlugins(); DataStructure dataStructure; - const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + const std::vector compPaths = + BuildOdfDataStructure(dataStructure, 3); MTRSimFilter filter; Arguments args = MakeValidArgs(compPaths); @@ -102,32 +117,36 @@ TEST_CASE("MTRSim::MTRSimFilter: Valid preflight builds actions", "[MTRSim][MTRS SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); } -TEST_CASE("MTRSim::MTRSimFilter: Rejects mismatched Volume Fraction column count", "[MTRSim][MTRSimFilter][ErrorPath]") -{ +TEST_CASE( + "MTRSim::MTRSimFilter: Rejects mismatched Volume Fraction column count", + "[MTRSim][MTRSimFilter][ErrorPath]") { UnitTest::LoadPlugins(); DataStructure dataStructure; - const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + 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}}); + 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]") -{ +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); + 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); } @@ -151,31 +170,30 @@ TEST_CASE("MTRSim::MTRSimFilter: Execute wires simulation to output arrays", "[M const usize expectedTuples = 100 * 100; // MTRIds: Int32, 1 component, 10000 tuples. - auto& mtrIds = dataStructure.getDataRefAs(cellAm.createChildPath("MTRIds")); + 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")); + 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(); + // 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) - { + 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) - { + for (usize id = 1; id <= 3; ++id) { + if (counts[id] > 0) { distinct++; } } @@ -184,9 +202,8 @@ TEST_CASE("MTRSim::MTRSimFilter: Execute wires simulation to output arrays", "[M // 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 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]; @@ -202,61 +219,67 @@ TEST_CASE("MTRSim::MTRSimFilter: Execute wires simulation to output arrays", "[M } // Seed array records 42. - auto& seedArray = dataStructure.getDataRefAs(DataPath({"MTRSim SeedValue"})); + 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); + 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]") -{ +TEST_CASE("MTRSim::MTRSimFilter: Rejects too few Theta List rows", + "[MTRSim][MTRSimFilter][ErrorPath]") { UnitTest::LoadPlugins(); DataStructure dataStructure; - const std::vector compPaths = BuildOdfDataStructure(dataStructure, 3); + 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}}); + 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]") -{ +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); + 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}}); + 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]") -{ +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); + const std::vector compPaths = + BuildOdfDataStructure(dataStructure, 3); + for (const auto &path : compPaths) { + auto &arr = dataStructure.getDataRefAs(path); arr.fill(1.0); } @@ -272,29 +295,31 @@ TEST_CASE("MTRSim::MTRSimFilter: Execute with polar coloring ON fills Polar Colo auto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); - const DataPath cellAm = DataPath({"MTR Microstructure"}).createChildPath("Cell Data"); + const DataPath cellAm = + DataPath({"MTR Microstructure"}).createChildPath("Cell Data"); constexpr usize expectedTuples = 100 * 100; - auto& rgb = dataStructure.getDataRefAs(cellAm.createChildPath("Polar Colors")); + 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) - { + 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]") -{ +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); + const std::vector compPaths = + BuildOdfDataStructure(dataStructure, 3); + for (const auto &path : compPaths) { + auto &arr = dataStructure.getDataRefAs(path); arr.fill(1.0); } @@ -310,23 +335,29 @@ TEST_CASE("MTRSim::MTRSimFilter: Execute with polar coloring OFF omits Polar Col 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); + 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]") -{ +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); + 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 (-13005), not the - // row-count check (-13004). - args.insertOrAssign(MTRSimFilter::k_ThetaList_Key, DynamicTableParameter::ValueType{{0.1, 0.45}, {0.08, 0.37}}); + // 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 (-13005), + // not the row-count check (-13004). + 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); 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..6f49df9 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 @@ -39,21 +40,22 @@ namespace fs = std::filesystem; using namespace nx::core; using namespace nx::core::UnitTest; -namespace -{ +namespace { constexpr usize k_ExpectedNPhi1 = 72; constexpr usize k_ExpectedNPHI = 36; constexpr usize k_ExpectedNPhi2 = 72; -constexpr usize k_ExpectedNumTuples = k_ExpectedNPhi1 * k_ExpectedNPHI * k_ExpectedNPhi2; // 186624 +constexpr usize k_ExpectedNumTuples = + k_ExpectedNPhi1 * k_ExpectedNPHI * k_ExpectedNPhi2; // 186624 constexpr float32 k_ExpectedSpacingDeg = 5.0f; constexpr int32 k_ExpectedNumComponents = 3; } // namespace -TEST_CASE("MTRSim::ReadMTRSimODFFilter: Valid Filter Execution", "[MTRSim][ReadMTRSimODFFilter]") -{ +TEST_CASE("MTRSim::ReadMTRSimODFFilter: Valid Filter Execution", + "[MTRSim][ReadMTRSimODFFilter]") { UnitTest::LoadPlugins(); - const fs::path inputFile = fs::path(fmt::format("{}/simulation_ODF.h5", unit_test::k_DataDir.view())); + const fs::path inputFile = fs::path( + fmt::format("{}/simulation_ODF.h5", unit_test::k_DataDir.view())); REQUIRE(fs::exists(inputFile)); const DataPath imageGeomPath({"ODF"}); @@ -63,19 +65,30 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Valid Filter Execution", "[MTRSim][ReadM ReadMTRSimODFFilter filter; Arguments args; - args.insertOrAssign(ReadMTRSimODFFilter::k_InputFile_Key, std::make_any(inputFile)); - args.insertOrAssign(ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(std::string{"/ODF_best"})); - args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, std::make_any(imageGeomPath)); - args.insertOrAssign(ReadMTRSimODFFilter::k_CellAttrMatName_Key, std::make_any(cellAttrMatName)); + args.insertOrAssign( + ReadMTRSimODFFilter::k_InputFile_Key, + std::make_any(inputFile)); + args.insertOrAssign( + ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(std::string{"/ODF_best"})); + args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, + std::make_any(imageGeomPath)); + args.insertOrAssign( + ReadMTRSimODFFilter::k_CellAttrMatName_Key, + std::make_any(cellAttrMatName)); // Preflight 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 @@ -84,7 +97,7 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Valid Filter Execution", "[MTRSim][ReadM // Verify the ImageGeom was created at /ODF with the expected dims & spacing. REQUIRE_NOTHROW(dataStructure.getDataRefAs(imageGeomPath)); - const auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); + const auto &imageGeom = dataStructure.getDataRefAs(imageGeomPath); const auto dims = imageGeom.getDimensions(); REQUIRE(dims[0] == k_ExpectedNPhi2); // X <- phi2 @@ -96,49 +109,60 @@ 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). - const DataPath cellAttrMatPath = imageGeomPath.createChildPath(cellAttrMatName); - for(int32 c = 0; c < k_ExpectedNumComponents; ++c) - { - const DataPath componentPath = cellAttrMatPath.createChildPath(fmt::format("component_{}", c)); + // 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) { + const DataPath componentPath = + cellAttrMatPath.createChildPath(fmt::format("component_{}", c)); REQUIRE_NOTHROW(dataStructure.getDataRefAs(componentPath)); - const auto& componentArray = dataStructure.getDataRefAs(componentPath); + const auto &componentArray = + dataStructure.getDataRefAs(componentPath); REQUIRE(componentArray.getNumberOfTuples() == k_ExpectedNumTuples); REQUIRE(componentArray.getNumberOfComponents() == 1); } - const auto& component0 = dataStructure.getDataRefAs(cellAttrMatPath.createChildPath("component_0")); + const auto &component0 = dataStructure.getDataRefAs( + cellAttrMatPath.createChildPath("component_0")); double sum0 = 0.0; - for(usize i = 0; i < component0.getSize(); ++i) - { + for (usize i = 0; i < component0.getSize(); ++i) { sum0 += component0[i]; } REQUIRE(std::fabs(sum0 - 1.0) < 1e-2); } -TEST_CASE("MTRSim::ReadMTRSimODFFilter: Missing File", "[MTRSim][ReadMTRSimODFFilter][ErrorPath]") -{ +TEST_CASE("MTRSim::ReadMTRSimODFFilter: Missing File", + "[MTRSim][ReadMTRSimODFFilter][ErrorPath]") { UnitTest::LoadPlugins(); DataStructure dataStructure; ReadMTRSimODFFilter filter; Arguments args; - args.insertOrAssign(ReadMTRSimODFFilter::k_InputFile_Key, std::make_any(fs::path("/nonexistent.h5"))); - args.insertOrAssign(ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(std::string{"/ODF_best"})); - args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, std::make_any(DataPath({"ODF"}))); - args.insertOrAssign(ReadMTRSimODFFilter::k_CellAttrMatName_Key, std::make_any(std::string{"Cell Data"})); + args.insertOrAssign(ReadMTRSimODFFilter::k_InputFile_Key, + std::make_any( + fs::path("/nonexistent.h5"))); + args.insertOrAssign( + ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(std::string{"/ODF_best"})); + args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, + std::make_any(DataPath({"ODF"}))); + args.insertOrAssign(ReadMTRSimODFFilter::k_CellAttrMatName_Key, + std::make_any( + std::string{"Cell Data"})); auto preflightResult = filter.preflight(dataStructure, args); REQUIRE(preflightResult.outputActions.invalid()); } -TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads blank ODF fixture", "[MTRSim][ReadMTRSimODFFilter]") -{ +TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads blank ODF fixture", + "[MTRSim][ReadMTRSimODFFilter]") { UnitTest::LoadPlugins(); - const fs::path inputFile = fs::path(fmt::format("{}/blank_ODF.h5", unit_test::k_DataDir.view())); + const fs::path inputFile = + fs::path(fmt::format("{}/blank_ODF.h5", unit_test::k_DataDir.view())); REQUIRE(fs::exists(inputFile)); const DataPath imageGeomPath({"ODF"}); @@ -148,10 +172,17 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads blank ODF fixture", "[MTRSim][Read ReadMTRSimODFFilter filter; Arguments args; - args.insertOrAssign(ReadMTRSimODFFilter::k_InputFile_Key, std::make_any(inputFile)); - args.insertOrAssign(ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(std::string{"/blank_ODF"})); - args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, std::make_any(imageGeomPath)); - args.insertOrAssign(ReadMTRSimODFFilter::k_CellAttrMatName_Key, std::make_any(cellAttrMatName)); + args.insertOrAssign( + ReadMTRSimODFFilter::k_InputFile_Key, + std::make_any(inputFile)); + args.insertOrAssign( + ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(std::string{"/blank_ODF"})); + args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, + std::make_any(imageGeomPath)); + args.insertOrAssign( + ReadMTRSimODFFilter::k_CellAttrMatName_Key, + std::make_any(cellAttrMatName)); auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); @@ -160,28 +191,30 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads blank ODF fixture", "[MTRSim][Read SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); REQUIRE_NOTHROW(dataStructure.getDataRefAs(imageGeomPath)); - const auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); + const auto &imageGeom = dataStructure.getDataRefAs(imageGeomPath); const auto dims = imageGeom.getDimensions(); REQUIRE(dims[0] == k_ExpectedNPhi2); REQUIRE(dims[1] == k_ExpectedNPHI); REQUIRE(dims[2] == k_ExpectedNPhi1); - const DataPath componentPath = imageGeomPath.createChildPath(cellAttrMatName).createChildPath("component_0"); + const DataPath componentPath = imageGeomPath.createChildPath(cellAttrMatName) + .createChildPath("component_0"); REQUIRE_NOTHROW(dataStructure.getDataRefAs(componentPath)); - const auto& component0 = dataStructure.getDataRefAs(componentPath); + const auto &component0 = + dataStructure.getDataRefAs(componentPath); REQUIRE(component0.getNumberOfTuples() == k_ExpectedNumTuples); - for(usize i = 0; i < component0.getSize(); ++i) - { + for (usize i = 0; i < component0.getSize(); ++i) { REQUIRE(component0[i] == 0.0); } } -TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads uniform ODF fixture", "[MTRSim][ReadMTRSimODFFilter]") -{ +TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads uniform ODF fixture", + "[MTRSim][ReadMTRSimODFFilter]") { UnitTest::LoadPlugins(); - const fs::path inputFile = fs::path(fmt::format("{}/uniform_ODF.h5", unit_test::k_DataDir.view())); + const fs::path inputFile = + fs::path(fmt::format("{}/uniform_ODF.h5", unit_test::k_DataDir.view())); REQUIRE(fs::exists(inputFile)); const DataPath imageGeomPath({"ODF"}); @@ -191,10 +224,17 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads uniform ODF fixture", "[MTRSim][Re ReadMTRSimODFFilter filter; Arguments args; - args.insertOrAssign(ReadMTRSimODFFilter::k_InputFile_Key, std::make_any(inputFile)); - args.insertOrAssign(ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(std::string{"/uniform_ODF"})); - args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, std::make_any(imageGeomPath)); - args.insertOrAssign(ReadMTRSimODFFilter::k_CellAttrMatName_Key, std::make_any(cellAttrMatName)); + args.insertOrAssign( + ReadMTRSimODFFilter::k_InputFile_Key, + std::make_any(inputFile)); + args.insertOrAssign( + ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(std::string{"/uniform_ODF"})); + args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, + std::make_any(imageGeomPath)); + args.insertOrAssign( + ReadMTRSimODFFilter::k_CellAttrMatName_Key, + std::make_any(cellAttrMatName)); auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); @@ -202,9 +242,11 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads uniform ODF fixture", "[MTRSim][Re auto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); - const DataPath componentPath = imageGeomPath.createChildPath(cellAttrMatName).createChildPath("component_0"); + const DataPath componentPath = imageGeomPath.createChildPath(cellAttrMatName) + .createChildPath("component_0"); REQUIRE_NOTHROW(dataStructure.getDataRefAs(componentPath)); - const auto& component0 = dataStructure.getDataRefAs(componentPath); + const auto &component0 = + dataStructure.getDataRefAs(componentPath); REQUIRE(component0.getNumberOfTuples() == k_ExpectedNumTuples); // The source .mat's ODFval is a normalised ODF (sums to 1.0) but is NOT a @@ -213,11 +255,9 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads uniform ODF fixture", "[MTRSim][Re // overall normalisation. double sum = 0.0; bool allPositive = true; - for(usize i = 0; i < component0.getSize(); ++i) - { + for (usize i = 0; i < component0.getSize(); ++i) { const double v = component0[i]; - if(v <= 0.0) - { + if (v <= 0.0) { allPositive = false; } sum += v; @@ -226,21 +266,29 @@ TEST_CASE("MTRSim::ReadMTRSimODFFilter: Reads uniform ODF fixture", "[MTRSim][Re REQUIRE(std::fabs(sum - 1.0) < 1e-9); } -TEST_CASE("MTRSim::ReadMTRSimODFFilter: Rejects wrong path prefix", "[MTRSim][ReadMTRSimODFFilter][ErrorPath]") -{ +TEST_CASE("MTRSim::ReadMTRSimODFFilter: Rejects wrong path prefix", + "[MTRSim][ReadMTRSimODFFilter][ErrorPath]") { UnitTest::LoadPlugins(); - const fs::path inputFile = fs::path(fmt::format("{}/simulation_ODF.h5", unit_test::k_DataDir.view())); + const fs::path inputFile = fs::path( + fmt::format("{}/simulation_ODF.h5", unit_test::k_DataDir.view())); REQUIRE(fs::exists(inputFile)); DataStructure dataStructure; ReadMTRSimODFFilter filter; Arguments args; - args.insertOrAssign(ReadMTRSimODFFilter::k_InputFile_Key, std::make_any(inputFile)); - args.insertOrAssign(ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(std::string{"/does_not_exist"})); - args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, std::make_any(DataPath({"ODF"}))); - args.insertOrAssign(ReadMTRSimODFFilter::k_CellAttrMatName_Key, std::make_any(std::string{"Cell Data"})); + args.insertOrAssign( + ReadMTRSimODFFilter::k_InputFile_Key, + std::make_any(inputFile)); + args.insertOrAssign(ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any( + std::string{"/does_not_exist"})); + args.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, + std::make_any(DataPath({"ODF"}))); + args.insertOrAssign(ReadMTRSimODFFilter::k_CellAttrMatName_Key, + std::make_any( + std::string{"Cell Data"})); auto preflightResult = filter.preflight(dataStructure, args); REQUIRE(preflightResult.outputActions.invalid()); diff --git a/test/WriteMTRSimODFTest.cpp b/test/WriteMTRSimODFTest.cpp index a182f06..b9ccc73 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 @@ -56,17 +57,20 @@ namespace fs = std::filesystem; using namespace nx::core; using namespace nx::core::UnitTest; -TEST_CASE("MTRSim::WriteMTRSimODFFilter: Round-trip through /ODF_best (back-compat)", "[MTRSim][WriteMTRSimODFFilter]") -{ +TEST_CASE( + "MTRSim::WriteMTRSimODFFilter: Round-trip through /ODF_best (back-compat)", + "[MTRSim][WriteMTRSimODFFilter]") { UnitTest::LoadPlugins(); - const fs::path inputFile = fs::path(fmt::format("{}/simulation_ODF.h5", unit_test::k_DataDir.view())); + const fs::path inputFile = fs::path( + fmt::format("{}/simulation_ODF.h5", unit_test::k_DataDir.view())); REQUIRE(fs::exists(inputFile)); - const fs::path outputFile = fs::temp_directory_path() / fmt::format("mtrsim_export_roundtrip_{}.h5", MTRSIM_GET_PID()); + const fs::path outputFile = + fs::temp_directory_path() / + fmt::format("mtrsim_export_roundtrip_{}.h5", MTRSIM_GET_PID()); std::error_code removeErr; - if(fs::exists(outputFile)) - { + if (fs::exists(outputFile)) { fs::remove(outputFile, removeErr); } @@ -79,10 +83,17 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: Round-trip through /ODF_best (back-comp { ReadMTRSimODFFilter readFilter; Arguments readArgs; - readArgs.insertOrAssign(ReadMTRSimODFFilter::k_InputFile_Key, std::make_any(inputFile)); - readArgs.insertOrAssign(ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(std::string{"/ODF_best"})); - readArgs.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, std::make_any(imageGeomPath)); - readArgs.insertOrAssign(ReadMTRSimODFFilter::k_CellAttrMatName_Key, std::make_any(cellAttrMatName)); + readArgs.insertOrAssign( + ReadMTRSimODFFilter::k_InputFile_Key, + std::make_any(inputFile)); + readArgs.insertOrAssign( + ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(std::string{"/ODF_best"})); + readArgs.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, + std::make_any(imageGeomPath)); + readArgs.insertOrAssign( + ReadMTRSimODFFilter::k_CellAttrMatName_Key, + std::make_any(cellAttrMatName)); auto readPreflight = readFilter.preflight(dataStructure, readArgs); SIMPLNX_RESULT_REQUIRE_VALID(readPreflight.outputActions); @@ -90,33 +101,45 @@ 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); - const DataPath cellAttrMatPath = imageGeomPath.createChildPath(cellAttrMatName); + const DataPath cellAttrMatPath = + imageGeomPath.createChildPath(cellAttrMatName); std::vector componentPaths; componentPaths.reserve(origComponents.size()); - for(std::size_t c = 0; c < origComponents.size(); ++c) - { - componentPaths.push_back(cellAttrMatPath.createChildPath(fmt::format("component_{}", c))); + for (std::size_t c = 0; c < origComponents.size(); ++c) { + componentPaths.push_back( + cellAttrMatPath.createChildPath(fmt::format("component_{}", c))); } // Export to the temp file. { WriteMTRSimODFFilter writeFilter; Arguments writeArgs; - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_OutputFile_Key, std::make_any(outputFile)); - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(std::string{"/ODF_best"})); - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_InputImageGeometry_Key, std::make_any(imageGeomPath)); - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_ODFComponents_Key, std::make_any(componentPaths)); + writeArgs.insertOrAssign( + WriteMTRSimODFFilter::k_OutputFile_Key, + std::make_any(outputFile)); + writeArgs.insertOrAssign( + WriteMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(std::string{"/ODF_best"})); + writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_InputImageGeometry_Key, + std::make_any(imageGeomPath)); + writeArgs.insertOrAssign( + WriteMTRSimODFFilter::k_ODFComponents_Key, + std::make_any(componentPaths)); auto writePreflight = writeFilter.preflight(dataStructure, writeArgs); 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); @@ -128,19 +151,20 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: Round-trip through /ODF_best (back-comp // Byte-exact comparison via libmtrsim. auto roundComponents = mtrsim::readODFComponents(outputFile); REQUIRE(origComponents.size() == roundComponents.size()); - for(std::size_t c = 0; c < origComponents.size(); ++c) - { + for (std::size_t c = 0; c < origComponents.size(); ++c) { REQUIRE(origComponents[c].values == roundComponents[c].values); } fs::remove(outputFile, removeErr); } -TEST_CASE("MTRSim::WriteMTRSimODFFilter: axis mapping preserves phi1-PHI-phi2 layout", "[MTRSim][WriteMTRSimODFFilter][AxisMapping]") -{ +TEST_CASE( + "MTRSim::WriteMTRSimODFFilter: axis mapping preserves phi1-PHI-phi2 layout", + "[MTRSim][WriteMTRSimODFFilter][AxisMapping]") { 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 @@ -150,63 +174,85 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: axis mapping preserves phi1-PHI-phi2 la const DataPath imageGeomPath({"ODF"}); const std::string cellAttrMatName = "Cell Data"; - const DataPath cellAttrMatPath = imageGeomPath.createChildPath(cellAttrMatName); + const DataPath cellAttrMatPath = + imageGeomPath.createChildPath(cellAttrMatName); const DataPath componentPath = cellAttrMatPath.createChildPath("component_0"); DataStructure dataStructure; // Build the ImageGeom via CreateImageGeometryAction (XYZ = phi2, PHI, phi1). { - CreateImageGeometryAction::DimensionType dims = {k_NumPhi2, k_NumPHI, k_NumPhi1}; + CreateImageGeometryAction::DimensionType dims = {k_NumPhi2, k_NumPHI, + k_NumPhi1}; CreateImageGeometryAction::OriginType origin = {0.0f, 0.0f, 0.0f}; - CreateImageGeometryAction::SpacingType spacing = {k_SpacingPhi2Deg, k_SpacingPHIDeg, k_SpacingPhi1Deg}; - CreateImageGeometryAction geomAction(imageGeomPath, dims, origin, spacing, cellAttrMatName, IGeometry::LengthUnit::Unknown); - auto geomResult = geomAction.apply(dataStructure, IDataAction::Mode::Execute); + CreateImageGeometryAction::SpacingType spacing = { + k_SpacingPhi2Deg, k_SpacingPHIDeg, k_SpacingPhi1Deg}; + CreateImageGeometryAction geomAction(imageGeomPath, dims, origin, spacing, + cellAttrMatName, + IGeometry::LengthUnit::Unknown); + auto geomResult = + geomAction.apply(dataStructure, IDataAction::Mode::Execute); 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); - auto arrayResult = arrayAction.apply(dataStructure, IDataAction::Mode::Execute); + CreateArrayAction arrayAction(DataType::float64, tupleShapeZYX, + std::vector{1}, componentPath); + auto arrayResult = + arrayAction.apply(dataStructure, IDataAction::Mode::Execute); 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. - 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."); + // 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; { - auto& componentArray = dataStructure.getDataRefAs(componentPath); - auto& store = componentArray.getDataStoreRef(); - for(std::size_t i = 0; i < store.getSize(); ++i) - { + auto &componentArray = + dataStructure.getDataRefAs(componentPath); + auto &store = componentArray.getDataStoreRef(); + for (std::size_t i = 0; i < store.getSize(); ++i) { store.setValue(i, 0.0); } store.setValue(k_SentinelIndex, k_SentinelValue); } // Write out via the filter. - const fs::path outputFile = fs::temp_directory_path() / fmt::format("mtrsim_axis_mapping_{}.h5", MTRSIM_GET_PID()); + const fs::path outputFile = + fs::temp_directory_path() / + fmt::format("mtrsim_axis_mapping_{}.h5", MTRSIM_GET_PID()); std::error_code removeErr; - if(fs::exists(outputFile)) - { + if (fs::exists(outputFile)) { fs::remove(outputFile, removeErr); } { WriteMTRSimODFFilter writeFilter; Arguments writeArgs; - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_OutputFile_Key, std::make_any(outputFile)); - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(std::string{"/ODF_best"})); - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_InputImageGeometry_Key, std::make_any(imageGeomPath)); - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_ODFComponents_Key, std::make_any(std::vector{componentPath})); + writeArgs.insertOrAssign( + WriteMTRSimODFFilter::k_OutputFile_Key, + std::make_any(outputFile)); + writeArgs.insertOrAssign( + WriteMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(std::string{"/ODF_best"})); + writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_InputImageGeometry_Key, + std::make_any(imageGeomPath)); + writeArgs.insertOrAssign( + WriteMTRSimODFFilter::k_ODFComponents_Key, + std::make_any( + std::vector{componentPath})); auto writePreflight = writeFilter.preflight(dataStructure, writeArgs); SIMPLNX_RESULT_REQUIRE_VALID(writePreflight.outputActions); @@ -219,22 +265,26 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: axis mapping preserves phi1-PHI-phi2 la // Read back the metadata and verify axis mapping was reversed correctly. auto meta = mtrsim::readODFMetadata(outputFile); REQUIRE(meta.numComponents == 1); - REQUIRE(meta.dimsPhi1PHIPhi2 == std::array{static_cast(k_NumPhi1), static_cast(k_NumPHI), static_cast(k_NumPhi2)}); - REQUIRE(std::abs(meta.spacingDegPhi1PHIPhi2[0] - static_cast(k_SpacingPhi1Deg)) < 1e-9); - REQUIRE(std::abs(meta.spacingDegPhi1PHIPhi2[1] - static_cast(k_SpacingPHIDeg)) < 1e-9); - REQUIRE(std::abs(meta.spacingDegPhi1PHIPhi2[2] - static_cast(k_SpacingPhi2Deg)) < 1e-9); + REQUIRE(meta.dimsPhi1PHIPhi2 == + std::array{static_cast(k_NumPhi1), + static_cast(k_NumPHI), + static_cast(k_NumPhi2)}); + REQUIRE(std::abs(meta.spacingDegPhi1PHIPhi2[0] - + static_cast(k_SpacingPhi1Deg)) < 1e-9); + REQUIRE(std::abs(meta.spacingDegPhi1PHIPhi2[1] - + static_cast(k_SpacingPHIDeg)) < 1e-9); + REQUIRE(std::abs(meta.spacingDegPhi1PHIPhi2[2] - + static_cast(k_SpacingPhi2Deg)) < 1e-9); auto comps = mtrsim::readODFComponents(outputFile); REQUIRE(comps.size() == 1); - const auto& vals = comps[0].values; + const auto &vals = comps[0].values; REQUIRE(vals.size() == k_NumPhi1 * k_NumPHI * k_NumPhi2); REQUIRE(vals[k_SentinelIndex] == k_SentinelValue); std::size_t nonzeroCount = 0; - for(std::size_t i = 0; i < vals.size(); ++i) - { - if(vals[i] != 0.0) - { + for (std::size_t i = 0; i < vals.size(); ++i) { + if (vals[i] != 0.0) { ++nonzeroCount; } } @@ -243,38 +293,49 @@ TEST_CASE("MTRSim::WriteMTRSimODFFilter: axis mapping preserves phi1-PHI-phi2 la fs::remove(outputFile, removeErr); } -TEST_CASE("MTRSim::WriteMTRSimODFFilter: zero components rejects at preflight", "[MTRSim][WriteMTRSimODFFilter][ErrorPath]") -{ +TEST_CASE("MTRSim::WriteMTRSimODFFilter: zero components rejects at preflight", + "[MTRSim][WriteMTRSimODFFilter][ErrorPath]") { UnitTest::LoadPlugins(); DataStructure dataStructure; WriteMTRSimODFFilter filter; Arguments args; - args.insertOrAssign(WriteMTRSimODFFilter::k_OutputFile_Key, std::make_any(fs::temp_directory_path() / "mtrsim_should_not_be_written.h5")); - args.insertOrAssign(WriteMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(std::string{"/ODF_best"})); - args.insertOrAssign(WriteMTRSimODFFilter::k_InputImageGeometry_Key, std::make_any(DataPath({"ODF"}))); - args.insertOrAssign(WriteMTRSimODFFilter::k_ODFComponents_Key, std::make_any(std::vector{})); + args.insertOrAssign( + WriteMTRSimODFFilter::k_OutputFile_Key, + std::make_any( + fs::temp_directory_path() / "mtrsim_should_not_be_written.h5")); + args.insertOrAssign( + WriteMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(std::string{"/ODF_best"})); + args.insertOrAssign(WriteMTRSimODFFilter::k_InputImageGeometry_Key, + std::make_any(DataPath({"ODF"}))); + args.insertOrAssign(WriteMTRSimODFFilter::k_ODFComponents_Key, + std::make_any( + std::vector{})); auto preflightResult = filter.preflight(dataStructure, args); REQUIRE(preflightResult.outputActions.invalid()); } -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. -void runDistinctPrefixRoundTrip(const std::string& fixtureName, const std::string& readPrefix, const std::string& writePrefix, const std::string& tempStem) -{ +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. +void runDistinctPrefixRoundTrip(const std::string &fixtureName, + const std::string &readPrefix, + const std::string &writePrefix, + const std::string &tempStem) { UnitTest::LoadPlugins(); - const fs::path inputFile = fs::path(fmt::format("{}/{}", unit_test::k_DataDir.view(), fixtureName)); + const fs::path inputFile = + fs::path(fmt::format("{}/{}", unit_test::k_DataDir.view(), fixtureName)); REQUIRE(fs::exists(inputFile)); - const fs::path outputFile = fs::temp_directory_path() / fmt::format("{}_{}.h5", tempStem, MTRSIM_GET_PID()); + const fs::path outputFile = + fs::temp_directory_path() / + fmt::format("{}_{}.h5", tempStem, MTRSIM_GET_PID()); std::error_code removeErr; - if(fs::exists(outputFile)) - { + if (fs::exists(outputFile)) { fs::remove(outputFile, removeErr); } @@ -286,10 +347,17 @@ void runDistinctPrefixRoundTrip(const std::string& fixtureName, const std::strin { ReadMTRSimODFFilter readFilter; Arguments readArgs; - readArgs.insertOrAssign(ReadMTRSimODFFilter::k_InputFile_Key, std::make_any(inputFile)); - readArgs.insertOrAssign(ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(readPrefix)); - readArgs.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, std::make_any(imageGeomPath)); - readArgs.insertOrAssign(ReadMTRSimODFFilter::k_CellAttrMatName_Key, std::make_any(cellAttrMatName)); + readArgs.insertOrAssign( + ReadMTRSimODFFilter::k_InputFile_Key, + std::make_any(inputFile)); + readArgs.insertOrAssign( + ReadMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(readPrefix)); + readArgs.insertOrAssign(ReadMTRSimODFFilter::k_OutputImageGeometry_Key, + std::make_any(imageGeomPath)); + readArgs.insertOrAssign( + ReadMTRSimODFFilter::k_CellAttrMatName_Key, + std::make_any(cellAttrMatName)); auto readPreflight = readFilter.preflight(dataStructure, readArgs); SIMPLNX_RESULT_REQUIRE_VALID(readPreflight.outputActions); @@ -300,21 +368,29 @@ void runDistinctPrefixRoundTrip(const std::string& fixtureName, const std::strin const auto origComponents = mtrsim::readODFComponents(inputFile, readPrefix); REQUIRE(origComponents.size() >= 1); - const DataPath cellAttrMatPath = imageGeomPath.createChildPath(cellAttrMatName); + const DataPath cellAttrMatPath = + imageGeomPath.createChildPath(cellAttrMatName); std::vector componentPaths; componentPaths.reserve(origComponents.size()); - for(std::size_t c = 0; c < origComponents.size(); ++c) - { - componentPaths.push_back(cellAttrMatPath.createChildPath(fmt::format("component_{}", c))); + for (std::size_t c = 0; c < origComponents.size(); ++c) { + componentPaths.push_back( + cellAttrMatPath.createChildPath(fmt::format("component_{}", c))); } { WriteMTRSimODFFilter writeFilter; Arguments writeArgs; - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_OutputFile_Key, std::make_any(outputFile)); - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_Hdf5PathPrefix_Key, std::make_any(writePrefix)); - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_InputImageGeometry_Key, std::make_any(imageGeomPath)); - writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_ODFComponents_Key, std::make_any(componentPaths)); + writeArgs.insertOrAssign( + WriteMTRSimODFFilter::k_OutputFile_Key, + std::make_any(outputFile)); + writeArgs.insertOrAssign( + WriteMTRSimODFFilter::k_Hdf5PathPrefix_Key, + std::make_any(writePrefix)); + writeArgs.insertOrAssign(WriteMTRSimODFFilter::k_InputImageGeometry_Key, + std::make_any(imageGeomPath)); + writeArgs.insertOrAssign( + WriteMTRSimODFFilter::k_ODFComponents_Key, + std::make_any(componentPaths)); auto writePreflight = writeFilter.preflight(dataStructure, writeArgs); SIMPLNX_RESULT_REQUIRE_VALID(writePreflight.outputActions); @@ -327,15 +403,13 @@ void runDistinctPrefixRoundTrip(const std::string& fixtureName, const std::strin // The default prefix should NOT exist at the destination — only writePrefix. // Library reads against the default "/ODF_best" must fail. // (Only meaningful when writePrefix != "/ODF_best".) - if(writePrefix != "/ODF_best") - { + if (writePrefix != "/ODF_best") { REQUIRE_THROWS(mtrsim::readODFMetadata(outputFile)); } auto roundComponents = mtrsim::readODFComponents(outputFile, writePrefix); REQUIRE(origComponents.size() == roundComponents.size()); - for(std::size_t c = 0; c < origComponents.size(); ++c) - { + for (std::size_t c = 0; c < origComponents.size(); ++c) { REQUIRE(origComponents[c].values == roundComponents[c].values); } @@ -343,12 +417,16 @@ void runDistinctPrefixRoundTrip(const std::string& fixtureName, const std::strin } } // namespace -TEST_CASE("MTRSim::WriteMTRSimODFFilter: Round-trip blank_ODF with distinct prefix", "[MTRSim][WriteMTRSimODFFilter]") -{ - runDistinctPrefixRoundTrip("blank_ODF.h5", "/blank_ODF", "/blank_ODF_exported", "mtrsim_blank_rt"); +TEST_CASE( + "MTRSim::WriteMTRSimODFFilter: Round-trip blank_ODF with distinct prefix", + "[MTRSim][WriteMTRSimODFFilter]") { + runDistinctPrefixRoundTrip("blank_ODF.h5", "/blank_ODF", + "/blank_ODF_exported", "mtrsim_blank_rt"); } -TEST_CASE("MTRSim::WriteMTRSimODFFilter: Round-trip uniform_ODF with distinct prefix", "[MTRSim][WriteMTRSimODFFilter]") -{ - runDistinctPrefixRoundTrip("uniform_ODF.h5", "/uniform_ODF", "/uniform_ODF_exported", "mtrsim_uniform_rt"); +TEST_CASE( + "MTRSim::WriteMTRSimODFFilter: Round-trip uniform_ODF with distinct prefix", + "[MTRSim][WriteMTRSimODFFilter]") { + runDistinctPrefixRoundTrip("uniform_ODF.h5", "/uniform_ODF", + "/uniform_ODF_exported", "mtrsim_uniform_rt"); } diff --git a/tests/test_mtrsim_driver.cpp b/tests/test_mtrsim_driver.cpp index 14e94cd..930d97e 100644 --- a/tests/test_mtrsim_driver.cpp +++ b/tests/test_mtrsim_driver.cpp @@ -21,7 +21,8 @@ TEST_CASE("buildUniformODF produces correct bin centres", "[mtrsim_driver]") { 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]") { +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 @@ -37,11 +38,14 @@ TEST_CASE("gridToODFComponent derives bin centres in radians and normalizes", "[ REQUIRE(c.phi2Bins[0] == Approx(2.5 * deg2rad)); } -TEST_CASE("remapSimToZYX moves y-fastest data to x-fastest layout", "[mtrsim_driver]") { +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; } + for (int i = 0; i < nx * ny * nz; ++i) { + in[i] = i; + } const std::vector out = mtrsim::remapSimToZYX(in, nx, ny, nz); @@ -61,29 +65,39 @@ TEST_CASE("remapSimToZYX moves y-fastest data to x-fastest layout", "[mtrsim_dri REQUIRE(out.size() == in.size()); } -TEST_CASE("simulateMTR reproduces target volume fractions (statistical)", "[mtrsim_driver][statistical]") { +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.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), 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 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); } + 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)]++; } + 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)); @@ -92,7 +106,16 @@ TEST_CASE("simulateMTR reproduces target volume fractions (statistical)", "[mtrs 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); } + 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); + } } diff --git a/tests/test_odf_builder.cpp b/tests/test_odf_builder.cpp index 9e8b74d..a2a4695 100644 --- a/tests/test_odf_builder.cpp +++ b/tests/test_odf_builder.cpp @@ -12,19 +12,21 @@ // Test 1: No-smoothing hits exactly one bin per tuple. // ----------------------------------------------------------------------------- -TEST_CASE("ODFBuilder::accumulate (no smoothing) hits exactly one bin per tuple", "[ODFBuilder]") -{ +TEST_CASE( + "ODFBuilder::accumulate (no smoothing) hits exactly one bin per tuple", + "[ODFBuilder]") { constexpr double k_DegToRad = std::numbers::pi / 180.0; 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 - const std::vector> eulers = {{27.5 * k_DegToRad, 17.5 * k_DegToRad, 52.5 * k_DegToRad}}; + // 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); double total = 0.0; - for(double v : values) - { + for (double v : values) { total += v; } REQUIRE(total == Approx(1.0).epsilon(1e-12)); @@ -37,19 +39,19 @@ TEST_CASE("ODFBuilder::accumulate (no smoothing) hits exactly one bin per tuple" // Test 2: Smoothing distributes per MATLAB weights, summing to 1.0. // ----------------------------------------------------------------------------- -TEST_CASE("ODFBuilder::accumulate (smoothing) distributes per MATLAB weights", "[ODFBuilder]") -{ +TEST_CASE("ODFBuilder::accumulate (smoothing) distributes per MATLAB weights", + "[ODFBuilder]") { constexpr double k_DegToRad = std::numbers::pi / 180.0; const mtrsim::ODFBuildParams p{72, 36, 72, 5.0, true}; std::vector values(72 * 36 * 72, 0.0); - const std::vector> eulers = {{27.5 * k_DegToRad, 17.5 * k_DegToRad, 52.5 * k_DegToRad}}; + const std::vector> eulers = { + {27.5 * k_DegToRad, 17.5 * k_DegToRad, 52.5 * k_DegToRad}}; mtrsim::accumulate(eulers, p, values); // Total contribution per tuple = 0.332 + 0.448 + 0.16 + 0.06 = 1.0 double total = 0.0; - for(double v : values) - { + for (double v : values) { total += v; } REQUIRE(total == Approx(1.0).epsilon(1e-9)); @@ -62,8 +64,7 @@ TEST_CASE("ODFBuilder::accumulate (smoothing) distributes per MATLAB weights", " // Test 3: normalize() divides in place. // ----------------------------------------------------------------------------- -TEST_CASE("ODFBuilder::normalize divides in place", "[ODFBuilder]") -{ +TEST_CASE("ODFBuilder::normalize divides in place", "[ODFBuilder]") { std::vector v = {2.0, 4.0, 8.0}; mtrsim::normalize(v, 2.0); REQUIRE(v == std::vector{1.0, 2.0, 4.0}); @@ -73,19 +74,20 @@ TEST_CASE("ODFBuilder::normalize divides in place", "[ODFBuilder]") // Test 4: accumulate throws on values-size mismatch. // ----------------------------------------------------------------------------- -TEST_CASE("ODFBuilder::accumulate throws on values-size mismatch", "[ODFBuilder]") -{ +TEST_CASE("ODFBuilder::accumulate throws on values-size mismatch", + "[ODFBuilder]") { const mtrsim::ODFBuildParams p{72, 36, 72, 5.0, false}; std::vector values(100, 0.0); // wrong size - REQUIRE_THROWS_AS(mtrsim::accumulate({{0.1, 0.2, 0.3}}, p, values), std::invalid_argument); + REQUIRE_THROWS_AS(mtrsim::accumulate({{0.1, 0.2, 0.3}}, p, values), + std::invalid_argument); } // ----------------------------------------------------------------------------- // Test 5: normalize with 0.0 is a no-op. // ----------------------------------------------------------------------------- -TEST_CASE("ODFBuilder::normalize is a no-op when normalizer is zero", "[ODFBuilder]") -{ +TEST_CASE("ODFBuilder::normalize is a no-op when normalizer is zero", + "[ODFBuilder]") { std::vector v = {1.0, 2.0, 3.0}; mtrsim::normalize(v, 0.0); REQUIRE(v == std::vector{1.0, 2.0, 3.0}); diff --git a/tests/test_odf_file_io.cpp b/tests/test_odf_file_io.cpp index 5eb6262..259cccd 100644 --- a/tests/test_odf_file_io.cpp +++ b/tests/test_odf_file_io.cpp @@ -13,14 +13,12 @@ namespace fs = std::filesystem; using namespace mtrsim; -namespace -{ +namespace { const fs::path k_DataDir{MTRSIM_TEST_DATA_DIR}; const fs::path k_ExemplarFile = k_DataDir / "simulation_ODF.h5"; } // namespace -TEST_CASE("ODFFileIO::readODFMetadata against exemplar", "[odffileio]") -{ +TEST_CASE("ODFFileIO::readODFMetadata against exemplar", "[odffileio]") { REQUIRE(fs::exists(k_ExemplarFile)); const ODFFileMetadata md = readODFMetadata(k_ExemplarFile); @@ -34,32 +32,28 @@ TEST_CASE("ODFFileIO::readODFMetadata against exemplar", "[odffileio]") CHECK(md.spacingDegPhi1PHIPhi2[2] == Approx(5.0).margin(1e-9)); } -TEST_CASE("ODFFileIO::readODFMetadata throws on missing file", "[odffileio]") -{ +TEST_CASE("ODFFileIO::readODFMetadata throws on missing file", "[odffileio]") { const fs::path bogus = k_DataDir / "this_file_does_not_exist_XYZ.h5"; REQUIRE_FALSE(fs::exists(bogus)); CHECK_THROWS_AS(readODFMetadata(bogus), std::runtime_error); } -TEST_CASE("ODFFileIO::readODFComponents against exemplar", "[odffileio]") -{ +TEST_CASE("ODFFileIO::readODFComponents against exemplar", "[odffileio]") { REQUIRE(fs::exists(k_ExemplarFile)); const std::vector comps = readODFComponents(k_ExemplarFile); REQUIRE(comps.size() == 3); - const std::size_t expectedSize = static_cast(72 * 36 * 72); // 186624 - for(std::size_t ci = 0; ci < comps.size(); ++ci) - { + const std::size_t expectedSize = + static_cast(72 * 36 * 72); // 186624 + for (std::size_t ci = 0; ci < comps.size(); ++ci) { CHECK(comps[ci].values.size() == expectedSize); bool allNonNegative = true; - for(double v : comps[ci].values) - { - if(v < 0.0) - { + for (double v : comps[ci].values) { + if (v < 0.0) { allNonNegative = false; break; } @@ -70,24 +64,26 @@ TEST_CASE("ODFFileIO::readODFComponents against exemplar", "[odffileio]") // component sums to approximately 1.0 within ~1% (observed: 1.0009, // 0.9999, 0.9973). Downstream code (see src/app/main.cpp) explicitly // re-normalises after reading. - const double sum = std::accumulate(comps[ci].values.begin(), comps[ci].values.end(), 0.0); + const double sum = + std::accumulate(comps[ci].values.begin(), comps[ci].values.end(), 0.0); CHECK(sum == Approx(1.0).margin(1e-2)); } } -TEST_CASE("ODFFileIO round-trip read/write/read", "[odffileio]") -{ +TEST_CASE("ODFFileIO round-trip read/write/read", "[odffileio]") { REQUIRE(fs::exists(k_ExemplarFile)); const ODFFileMetadata srcMd = readODFMetadata(k_ExemplarFile); - const std::vector srcComps = readODFComponents(k_ExemplarFile); + const std::vector srcComps = + readODFComponents(k_ExemplarFile); const fs::path outPath = fs::temp_directory_path() / "mtrsim_roundtrip.h5"; std::error_code ec; fs::remove(outPath, ec); // ignore error if file doesn't exist - writeODFFile(outPath, srcMd.dimsPhi1PHIPhi2, srcMd.spacingDegPhi1PHIPhi2, srcComps); + writeODFFile(outPath, srcMd.dimsPhi1PHIPhi2, srcMd.spacingDegPhi1PHIPhi2, + srcComps); REQUIRE(fs::exists(outPath)); @@ -96,13 +92,15 @@ TEST_CASE("ODFFileIO round-trip read/write/read", "[odffileio]") CHECK(rtMd.numComponents == srcMd.numComponents); CHECK(rtMd.dimsPhi1PHIPhi2 == srcMd.dimsPhi1PHIPhi2); - CHECK(rtMd.spacingDegPhi1PHIPhi2[0] == Approx(srcMd.spacingDegPhi1PHIPhi2[0]).margin(1e-9)); - CHECK(rtMd.spacingDegPhi1PHIPhi2[1] == Approx(srcMd.spacingDegPhi1PHIPhi2[1]).margin(1e-9)); - CHECK(rtMd.spacingDegPhi1PHIPhi2[2] == Approx(srcMd.spacingDegPhi1PHIPhi2[2]).margin(1e-9)); + CHECK(rtMd.spacingDegPhi1PHIPhi2[0] == + Approx(srcMd.spacingDegPhi1PHIPhi2[0]).margin(1e-9)); + CHECK(rtMd.spacingDegPhi1PHIPhi2[1] == + Approx(srcMd.spacingDegPhi1PHIPhi2[1]).margin(1e-9)); + CHECK(rtMd.spacingDegPhi1PHIPhi2[2] == + Approx(srcMd.spacingDegPhi1PHIPhi2[2]).margin(1e-9)); REQUIRE(rtComps.size() == srcComps.size()); - for(std::size_t ci = 0; ci < srcComps.size(); ++ci) - { + for (std::size_t ci = 0; ci < srcComps.size(); ++ci) { CHECK(rtComps[ci].values == srcComps[ci].values); } From 78e6d5366ed0bdc03d6e601bfa860d39302c0783 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 1 Jun 2026 16:53:10 -0400 Subject: [PATCH 16/39] docs: add MTRSimFilter overview/algorithm/ODF figures Embed the conceptual "what it generates" and "how it works" slides (converted from output/avatar_slides) plus an ODF Euler-space figure into the filter documentation. Also correct the Execute error code in the table to -13050 to match the algorithm source. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/Images/mtrsim_algorithm.png | Bin 0 -> 317627 bytes docs/Images/mtrsim_odf_euler_space.png | Bin 0 -> 93622 bytes docs/Images/mtrsim_overview.png | Bin 0 -> 349196 bytes docs/MTRSimFilter.md | 14 +++++++++++++- src/MTRSim/Filters/Algorithms/MTRSim.cpp | 2 +- 5 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 docs/Images/mtrsim_algorithm.png create mode 100644 docs/Images/mtrsim_odf_euler_space.png create mode 100644 docs/Images/mtrsim_overview.png diff --git a/docs/Images/mtrsim_algorithm.png b/docs/Images/mtrsim_algorithm.png new file mode 100644 index 0000000000000000000000000000000000000000..3359b1a3778257e719d6e7a44012de4c554dbcaf GIT binary patch literal 317627 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 z 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⁣e3y~w!NZgM?f+O@vm&); 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�sYF8V?BfzQ#R5 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 i<7zO8B7bziLo+KciuG-XPz9d2{pMs`folY8p3p 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 literal 0 HcmV?d00001 diff --git a/docs/Images/mtrsim_overview.png b/docs/Images/mtrsim_overview.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad234f2206f1c685caed9b636627a843d63c2df GIT binary patch literal 349196 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